mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
Compare commits
107 Commits
waleedlati
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8848780f56 | ||
|
|
fefeb010de | ||
|
|
ee6c7f98ff | ||
|
|
64758af2b6 | ||
|
|
8c09e19293 | ||
|
|
feb1c88d2f | ||
|
|
78007c11a0 | ||
|
|
bac1d5e588 | ||
|
|
7fdab14266 | ||
|
|
3b9e663f25 | ||
|
|
381bc1d556 | ||
|
|
20c05644ab | ||
|
|
f9d73db65c | ||
|
|
e2e53aba76 | ||
|
|
727bb1cadb | ||
|
|
e2e29cefd7 | ||
|
|
45f053a383 | ||
|
|
225d5d551a | ||
|
|
a78f3f9c2e | ||
|
|
080a0a6123 | ||
|
|
fc6fe193fa | ||
|
|
bbc704fe05 | ||
|
|
c016537564 | ||
|
|
4c94f3cf78 | ||
|
|
27a11a269d | ||
|
|
2c174ca4f6 | ||
|
|
ac831b85b2 | ||
|
|
8527ae5d3b | ||
|
|
076c835ba2 | ||
|
|
df6ceb61a4 | ||
|
|
2ede12aa0e | ||
|
|
42fb434354 | ||
|
|
dcebe3ae97 | ||
|
|
e39c534ee3 | ||
|
|
b95a0491a0 | ||
|
|
a79c8a75ce | ||
|
|
282ec8c58c | ||
|
|
e45fbe0184 | ||
|
|
512558dcb3 | ||
|
|
35411e465e | ||
|
|
72e28baa07 | ||
|
|
d99dd86bf2 | ||
|
|
7898e5d75f | ||
|
|
df62502903 | ||
|
|
1a2aa6949e | ||
|
|
4544fd4519 | ||
|
|
019630bdc8 | ||
|
|
90f592797a | ||
|
|
d091441e39 | ||
|
|
7d4dd26760 | ||
|
|
0abeac77e1 | ||
|
|
e9c94fa462 | ||
|
|
72eea64bf6 | ||
|
|
27460f847c | ||
|
|
c7643198dc | ||
|
|
e5aef6184a | ||
|
|
4ae5b1b620 | ||
|
|
5c334874eb | ||
|
|
d3d58a9615 | ||
|
|
1d59eca90a | ||
|
|
e1359b09d6 | ||
|
|
35b3646330 | ||
|
|
5c47ea58f8 | ||
|
|
c4f4e6b48c | ||
|
|
1728c370de | ||
|
|
82e58a5082 | ||
|
|
336c065234 | ||
|
|
b3713642b2 | ||
|
|
b9b930bb63 | ||
|
|
f1ead2ed55 | ||
|
|
30377d775b | ||
|
|
d013132d0e | ||
|
|
7b0ce8064a | ||
|
|
0ea73263df | ||
|
|
edc502384b | ||
|
|
e2be99263c | ||
|
|
f6b461ad47 | ||
|
|
e4d35735b1 | ||
|
|
b4064c57fb | ||
|
|
eac41ca105 | ||
|
|
d2c3c1c39e | ||
|
|
8f3e864751 | ||
|
|
23c3072784 | ||
|
|
33fdb11396 | ||
|
|
21156dd54a | ||
|
|
c05e2e0fc8 | ||
|
|
a7c1e510e6 | ||
|
|
271624a402 | ||
|
|
dda012eae9 | ||
|
|
2dd6d3d1e6 | ||
|
|
b90bb75cda | ||
|
|
fb233d003d | ||
|
|
34df3333d1 | ||
|
|
23677d41a0 | ||
|
|
a489f91085 | ||
|
|
ed6e7845cc | ||
|
|
e698f9fe14 | ||
|
|
db1798267e | ||
|
|
5f1d5e0618 | ||
|
|
ed5645166e | ||
|
|
50e42c2041 | ||
|
|
e70e1ec8c5 | ||
|
|
4c474e03c1 | ||
|
|
b0980b1e09 | ||
|
|
66ce673629 | ||
|
|
f37e4b67c7 | ||
|
|
7a1a46067d |
@@ -71,12 +71,14 @@ export const {service}Connector: ConnectorConfig = {
|
||||
],
|
||||
|
||||
listDocuments: async (accessToken, sourceConfig, cursor) => {
|
||||
// Paginate via cursor, extract text, compute SHA-256 hash
|
||||
// Return metadata stubs with contentDeferred: true (if per-doc content fetch needed)
|
||||
// Or full documents with content (if list API returns content inline)
|
||||
// Return { documents: ExternalDocument[], nextCursor?, hasMore }
|
||||
},
|
||||
|
||||
getDocument: async (accessToken, sourceConfig, externalId) => {
|
||||
// Return ExternalDocument or null
|
||||
// Fetch full content for a single document
|
||||
// Return ExternalDocument with contentDeferred: false, or null
|
||||
},
|
||||
|
||||
validateConfig: async (accessToken, sourceConfig) => {
|
||||
@@ -281,26 +283,110 @@ Every document returned from `listDocuments`/`getDocument` must include:
|
||||
{
|
||||
externalId: string // Source-specific unique ID
|
||||
title: string // Document title
|
||||
content: string // Extracted plain text
|
||||
content: string // Extracted plain text (or '' if contentDeferred)
|
||||
contentDeferred?: boolean // true = content will be fetched via getDocument
|
||||
mimeType: 'text/plain' // Always text/plain (content is extracted)
|
||||
contentHash: string // SHA-256 of content (change detection)
|
||||
contentHash: string // Metadata-based hash for change detection
|
||||
sourceUrl?: string // Link back to original (stored on document record)
|
||||
metadata?: Record<string, unknown> // Source-specific data (fed to mapTags)
|
||||
}
|
||||
```
|
||||
|
||||
## Content Hashing (Required)
|
||||
## Content Deferral (Required for file/content-download connectors)
|
||||
|
||||
The sync engine uses content hashes for change detection:
|
||||
**All connectors that require per-document API calls to fetch content MUST use `contentDeferred: true`.** This is the standard pattern — `listDocuments` returns lightweight metadata stubs, and content is fetched lazily by the sync engine via `getDocument` only for new/changed documents.
|
||||
|
||||
This pattern is critical for reliability: the sync engine processes documents in batches and enqueues each batch for processing immediately. If a sync times out, all previously-batched documents are already queued. Without deferral, content downloads during listing can exhaust the sync task's time budget before any documents are saved.
|
||||
|
||||
### When to use `contentDeferred: true`
|
||||
|
||||
- The service's list API does NOT return document content (only metadata)
|
||||
- Content requires a separate download/export API call per document
|
||||
- Examples: Google Drive, OneDrive, SharePoint, Dropbox, Notion, Confluence, Gmail, Obsidian, Evernote, GitHub
|
||||
|
||||
### When NOT to use `contentDeferred`
|
||||
|
||||
- The list API already returns the full content inline (e.g., Slack messages, Reddit posts, HubSpot notes)
|
||||
- No per-document API call is needed to get content
|
||||
|
||||
### Content Hash Strategy
|
||||
|
||||
Use a **metadata-based** `contentHash` — never a content-based hash. The hash must be derivable from the list response metadata alone, so the sync engine can detect changes without downloading content.
|
||||
|
||||
Good metadata hash sources:
|
||||
- `modifiedTime` / `lastModifiedDateTime` — changes when file is edited
|
||||
- Git blob SHA — unique per content version
|
||||
- API-provided content hash (e.g., Dropbox `content_hash`)
|
||||
- Version number (e.g., Confluence page version)
|
||||
|
||||
Format: `{service}:{id}:{changeIndicator}`
|
||||
|
||||
```typescript
|
||||
async function computeContentHash(content: string): Promise<string> {
|
||||
const data = new TextEncoder().encode(content)
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', data)
|
||||
return Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, '0')).join('')
|
||||
// Google Drive: modifiedTime changes on edit
|
||||
contentHash: `gdrive:${file.id}:${file.modifiedTime ?? ''}`
|
||||
|
||||
// GitHub: blob SHA is a content-addressable hash
|
||||
contentHash: `gitsha:${item.sha}`
|
||||
|
||||
// Dropbox: API provides content_hash
|
||||
contentHash: `dropbox:${entry.id}:${entry.content_hash ?? entry.server_modified}`
|
||||
|
||||
// Confluence: version number increments on edit
|
||||
contentHash: `confluence:${page.id}:${page.version.number}`
|
||||
```
|
||||
|
||||
**Critical invariant:** The `contentHash` MUST be identical whether produced by `listDocuments` (stub) or `getDocument` (full doc). Both should use the same stub function to guarantee this.
|
||||
|
||||
### Implementation Pattern
|
||||
|
||||
```typescript
|
||||
// 1. Create a stub function (sync, no API calls)
|
||||
function fileToStub(file: ServiceFile): ExternalDocument {
|
||||
return {
|
||||
externalId: file.id,
|
||||
title: file.name || 'Untitled',
|
||||
content: '',
|
||||
contentDeferred: true,
|
||||
mimeType: 'text/plain',
|
||||
sourceUrl: `https://service.com/file/${file.id}`,
|
||||
contentHash: `service:${file.id}:${file.modifiedTime ?? ''}`,
|
||||
metadata: { /* fields needed by mapTags */ },
|
||||
}
|
||||
}
|
||||
|
||||
// 2. listDocuments returns stubs (fast, metadata only)
|
||||
listDocuments: async (accessToken, sourceConfig, cursor) => {
|
||||
const response = await fetchWithRetry(listUrl, { ... })
|
||||
const files = (await response.json()).files
|
||||
const documents = files.map(fileToStub)
|
||||
return { documents, nextCursor, hasMore }
|
||||
}
|
||||
|
||||
// 3. getDocument fetches content and returns full doc with SAME contentHash
|
||||
getDocument: async (accessToken, sourceConfig, externalId) => {
|
||||
const metadata = await fetchWithRetry(metadataUrl, { ... })
|
||||
const file = await metadata.json()
|
||||
if (file.trashed) return null
|
||||
|
||||
try {
|
||||
const content = await fetchContent(accessToken, file)
|
||||
if (!content.trim()) return null
|
||||
const stub = fileToStub(file)
|
||||
return { ...stub, content, contentDeferred: false }
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to fetch content for: ${file.name}`, { error })
|
||||
return null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Reference Implementations
|
||||
|
||||
- **Google Drive**: `connectors/google-drive/google-drive.ts` — file download/export with `modifiedTime` hash
|
||||
- **GitHub**: `connectors/github/github.ts` — git blob SHA hash
|
||||
- **Notion**: `connectors/notion/notion.ts` — blocks API with `last_edited_time` hash
|
||||
- **Confluence**: `connectors/confluence/confluence.ts` — version number hash
|
||||
|
||||
## tagDefinitions — Declared Tag Definitions
|
||||
|
||||
Declare which tags the connector populates using semantic IDs. Shown in the add-connector modal as opt-out checkboxes.
|
||||
@@ -409,7 +495,10 @@ export const CONNECTOR_REGISTRY: ConnectorRegistry = {
|
||||
|
||||
## Reference Implementations
|
||||
|
||||
- **OAuth**: `apps/sim/connectors/confluence/confluence.ts` — multiple config field types, `mapTags`, label fetching
|
||||
- **OAuth + contentDeferred**: `apps/sim/connectors/google-drive/google-drive.ts` — file download with metadata-based hash, `orderBy` for deterministic pagination
|
||||
- **OAuth + contentDeferred (blocks API)**: `apps/sim/connectors/notion/notion.ts` — complex block content extraction deferred to `getDocument`
|
||||
- **OAuth + contentDeferred (git)**: `apps/sim/connectors/github/github.ts` — blob SHA hash, tree listing
|
||||
- **OAuth + inline content**: `apps/sim/connectors/confluence/confluence.ts` — multiple config field types, `mapTags`, label fetching
|
||||
- **API key**: `apps/sim/connectors/fireflies/fireflies.ts` — GraphQL API with Bearer token auth
|
||||
|
||||
## Checklist
|
||||
@@ -425,7 +514,9 @@ export const CONNECTOR_REGISTRY: ConnectorRegistry = {
|
||||
- `selectorKey` exists in `hooks/selectors/registry.ts`
|
||||
- `dependsOn` references selector field IDs (not `canonicalParamId`)
|
||||
- Dependency `canonicalParamId` values exist in `SELECTOR_CONTEXT_FIELDS`
|
||||
- [ ] `listDocuments` handles pagination and computes content hashes
|
||||
- [ ] `listDocuments` handles pagination with metadata-based content hashes
|
||||
- [ ] `contentDeferred: true` used if content requires per-doc API calls (file download, export, blocks fetch)
|
||||
- [ ] `contentHash` is metadata-based (not content-based) and identical between stub and `getDocument`
|
||||
- [ ] `sourceUrl` set on each ExternalDocument (full URL, not relative)
|
||||
- [ ] `metadata` includes source-specific data for tag mapping
|
||||
- [ ] `tagDefinitions` declared for each semantic key returned by `mapTags`
|
||||
|
||||
@@ -141,12 +141,24 @@ For each API endpoint the connector calls:
|
||||
|
||||
## Step 6: Validate Data Transformation
|
||||
|
||||
### Content Deferral (CRITICAL)
|
||||
Connectors that require per-document API calls to fetch content (file download, export, blocks fetch) MUST use `contentDeferred: true`. This is the standard pattern for reliability — without it, content downloads during listing can exhaust the sync task's time budget before any documents are saved.
|
||||
|
||||
- [ ] If the connector downloads content per-doc during `listDocuments`, it MUST use `contentDeferred: true` instead
|
||||
- [ ] `listDocuments` returns lightweight stubs with `content: ''` and `contentDeferred: true`
|
||||
- [ ] `getDocument` fetches actual content and returns the full document with `contentDeferred: false`
|
||||
- [ ] A shared stub function (e.g., `fileToStub`) is used by both `listDocuments` and `getDocument` to guarantee `contentHash` consistency
|
||||
- [ ] `contentHash` is metadata-based (e.g., `service:{id}:{modifiedTime}`), NOT content-based — it must be derivable from list metadata alone
|
||||
- [ ] The `contentHash` is identical whether produced by `listDocuments` or `getDocument`
|
||||
|
||||
Connectors where the list API already returns content inline (e.g., Slack messages, Reddit posts) do NOT need `contentDeferred`.
|
||||
|
||||
### 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`
|
||||
- [ ] `contentHash` uses a metadata-based format (e.g., `service:{id}:{modifiedTime}`) for connectors with `contentDeferred: true`, or `computeContentHash` from `@/connectors/utils` for inline-content connectors
|
||||
- [ ] `sourceUrl` is a valid, complete URL back to the original resource (not relative)
|
||||
- [ ] `metadata` contains all fields referenced by `mapTags` and `tagDefinitions`
|
||||
|
||||
@@ -200,6 +212,8 @@ For each API endpoint the connector calls:
|
||||
- [ ] Fetches a single document by `externalId`
|
||||
- [ ] Returns `null` for 404 / not found (does not throw)
|
||||
- [ ] Returns the same `ExternalDocument` shape as `listDocuments`
|
||||
- [ ] If `listDocuments` uses `contentDeferred: true`, `getDocument` MUST fetch actual content and return `contentDeferred: false`
|
||||
- [ ] If `listDocuments` uses `contentDeferred: true`, `getDocument` MUST use the same stub function to ensure `contentHash` is identical
|
||||
- [ ] 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)
|
||||
@@ -253,6 +267,8 @@ Group findings by severity:
|
||||
- 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
|
||||
- Per-document content download in `listDocuments` without `contentDeferred: true` — causes sync timeouts for large document sets
|
||||
- `contentHash` mismatch between `listDocuments` stub and `getDocument` return — causes unnecessary re-processing every sync
|
||||
|
||||
**Warning** (incorrect behavior, data quality issues, or convention violations):
|
||||
- HTML content not stripped via `htmlToPlainText`
|
||||
@@ -300,6 +316,7 @@ After fixing, confirm:
|
||||
- [ ] 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 content deferral: `contentDeferred: true` used when per-doc content fetch required, metadata-based `contentHash` consistent between stub and `getDocument`
|
||||
- [ ] 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
|
||||
|
||||
@@ -192,7 +192,7 @@ In the block config (`blocks/blocks/{service}.ts`), add `hideWhenHosted: true` t
|
||||
},
|
||||
```
|
||||
|
||||
The visibility is controlled by `isSubBlockHiddenByHostedKey()` in `lib/workflows/subblocks/visibility.ts`, which checks the `isHosted` feature flag.
|
||||
The visibility is controlled by `isSubBlockHidden()` in `lib/workflows/subblocks/visibility.ts`, which checks both the `isHosted` feature flag (`hideWhenHosted`) and optional env var conditions (`hideWhenEnvSet`).
|
||||
|
||||
### Excluding Specific Operations from Hosted Key Support
|
||||
|
||||
|
||||
14
.github/workflows/ci.yml
vendored
14
.github/workflows/ci.yml
vendored
@@ -2,9 +2,9 @@ name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, staging]
|
||||
branches: [main, staging, dev]
|
||||
pull_request:
|
||||
branches: [main, staging]
|
||||
branches: [main, staging, dev]
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
detect-version:
|
||||
name: Detect Version
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging')
|
||||
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging' || github.ref == 'refs/heads/dev')
|
||||
outputs:
|
||||
version: ${{ steps.extract.outputs.version }}
|
||||
is_release: ${{ steps.extract.outputs.is_release }}
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
build-amd64:
|
||||
name: Build AMD64
|
||||
needs: [test-build, detect-version]
|
||||
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging')
|
||||
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging' || github.ref == 'refs/heads/dev')
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -75,8 +75,8 @@ jobs:
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
role-to-assume: ${{ github.ref == 'refs/heads/main' && secrets.AWS_ROLE_TO_ASSUME || secrets.STAGING_AWS_ROLE_TO_ASSUME }}
|
||||
aws-region: ${{ github.ref == 'refs/heads/main' && secrets.AWS_REGION || secrets.STAGING_AWS_REGION }}
|
||||
role-to-assume: ${{ github.ref == 'refs/heads/main' && secrets.AWS_ROLE_TO_ASSUME || github.ref == 'refs/heads/dev' && secrets.DEV_AWS_ROLE_TO_ASSUME || secrets.STAGING_AWS_ROLE_TO_ASSUME }}
|
||||
aws-region: ${{ github.ref == 'refs/heads/main' && secrets.AWS_REGION || github.ref == 'refs/heads/dev' && secrets.DEV_AWS_REGION || secrets.STAGING_AWS_REGION }}
|
||||
|
||||
- name: Login to Amazon ECR
|
||||
id: login-ecr
|
||||
@@ -109,6 +109,8 @@ jobs:
|
||||
# ECR tags (always build for ECR)
|
||||
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
|
||||
ECR_TAG="latest"
|
||||
elif [ "${{ github.ref }}" = "refs/heads/dev" ]; then
|
||||
ECR_TAG="dev"
|
||||
else
|
||||
ECR_TAG="staging"
|
||||
fi
|
||||
|
||||
6
.github/workflows/images.yml
vendored
6
.github/workflows/images.yml
vendored
@@ -36,8 +36,8 @@ jobs:
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
role-to-assume: ${{ github.ref == 'refs/heads/main' && secrets.AWS_ROLE_TO_ASSUME || secrets.STAGING_AWS_ROLE_TO_ASSUME }}
|
||||
aws-region: ${{ github.ref == 'refs/heads/main' && secrets.AWS_REGION || secrets.STAGING_AWS_REGION }}
|
||||
role-to-assume: ${{ github.ref == 'refs/heads/main' && secrets.AWS_ROLE_TO_ASSUME || github.ref == 'refs/heads/dev' && secrets.DEV_AWS_ROLE_TO_ASSUME || secrets.STAGING_AWS_ROLE_TO_ASSUME }}
|
||||
aws-region: ${{ github.ref == 'refs/heads/main' && secrets.AWS_REGION || github.ref == 'refs/heads/dev' && secrets.DEV_AWS_REGION || secrets.STAGING_AWS_REGION }}
|
||||
|
||||
- name: Login to Amazon ECR
|
||||
id: login-ecr
|
||||
@@ -70,6 +70,8 @@ jobs:
|
||||
# ECR tags (always build for ECR)
|
||||
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
|
||||
ECR_TAG="latest"
|
||||
elif [ "${{ github.ref }}" = "refs/heads/dev" ]; then
|
||||
ECR_TAG="dev"
|
||||
else
|
||||
ECR_TAG="staging"
|
||||
fi
|
||||
|
||||
@@ -113,7 +113,7 @@ cd packages/db && bunx drizzle-kit migrate --config=./drizzle.config.ts
|
||||
5. Start development servers:
|
||||
|
||||
```bash
|
||||
bun run dev:full # Starts both Next.js app and realtime socket server
|
||||
bun run dev:full # Starts Next.js app and realtime socket server
|
||||
```
|
||||
|
||||
Or run separately: `bun run dev` (Next.js) and `cd apps/sim && bun run dev:sockets` (realtime).
|
||||
|
||||
@@ -18,7 +18,7 @@ export const metadata = {
|
||||
metadataBase: new URL('https://docs.sim.ai'),
|
||||
title: {
|
||||
default: 'Sim Documentation — Build AI Agents & Run Your Agentic Workforce',
|
||||
template: '%s',
|
||||
template: '%s | 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.',
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -5,6 +5,7 @@
|
||||
import type { ComponentType, SVGProps } from 'react'
|
||||
import {
|
||||
A2AIcon,
|
||||
AgentMailIcon,
|
||||
AhrefsIcon,
|
||||
AirtableIcon,
|
||||
AirweaveIcon,
|
||||
@@ -45,6 +46,7 @@ import {
|
||||
EnrichSoIcon,
|
||||
EvernoteIcon,
|
||||
ExaAIIcon,
|
||||
ExtendIcon,
|
||||
EyeIcon,
|
||||
FathomIcon,
|
||||
FirecrawlIcon,
|
||||
@@ -74,6 +76,7 @@ import {
|
||||
GoogleVaultIcon,
|
||||
GrafanaIcon,
|
||||
GrainIcon,
|
||||
GranolaIcon,
|
||||
GreenhouseIcon,
|
||||
GreptileIcon,
|
||||
HexIcon,
|
||||
@@ -84,12 +87,13 @@ import {
|
||||
IncidentioIcon,
|
||||
InfisicalIcon,
|
||||
IntercomIcon,
|
||||
IroncladIcon,
|
||||
JinaAIIcon,
|
||||
JiraIcon,
|
||||
JiraServiceManagementIcon,
|
||||
KalshiIcon,
|
||||
KetchIcon,
|
||||
LangsmithIcon,
|
||||
LaunchDarklyIcon,
|
||||
LemlistIcon,
|
||||
LinearIcon,
|
||||
LinkedInIcon,
|
||||
@@ -125,6 +129,7 @@ import {
|
||||
PolymarketIcon,
|
||||
PostgresIcon,
|
||||
PosthogIcon,
|
||||
ProfoundIcon,
|
||||
PulseIcon,
|
||||
QdrantIcon,
|
||||
QuiverIcon,
|
||||
@@ -135,9 +140,11 @@ import {
|
||||
ResendIcon,
|
||||
RevenueCatIcon,
|
||||
RipplingIcon,
|
||||
RootlyIcon,
|
||||
S3Icon,
|
||||
SalesforceIcon,
|
||||
SearchIcon,
|
||||
SecretsManagerIcon,
|
||||
SendgridIcon,
|
||||
SentryIcon,
|
||||
SerperIcon,
|
||||
@@ -153,6 +160,7 @@ import {
|
||||
StagehandIcon,
|
||||
StripeIcon,
|
||||
SupabaseIcon,
|
||||
TailscaleIcon,
|
||||
TavilyIcon,
|
||||
TelegramIcon,
|
||||
TextractIcon,
|
||||
@@ -182,6 +190,7 @@ type IconComponent = ComponentType<SVGProps<SVGSVGElement>>
|
||||
|
||||
export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
a2a: A2AIcon,
|
||||
agentmail: AgentMailIcon,
|
||||
ahrefs: AhrefsIcon,
|
||||
airtable: AirtableIcon,
|
||||
airweave: AirweaveIcon,
|
||||
@@ -219,6 +228,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
enrich: EnrichSoIcon,
|
||||
evernote: EvernoteIcon,
|
||||
exa: ExaAIIcon,
|
||||
extend_v2: ExtendIcon,
|
||||
fathom: FathomIcon,
|
||||
file_v3: DocumentIcon,
|
||||
firecrawl: FirecrawlIcon,
|
||||
@@ -248,6 +258,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
google_vault: GoogleVaultIcon,
|
||||
grafana: GrafanaIcon,
|
||||
grain: GrainIcon,
|
||||
granola: GranolaIcon,
|
||||
greenhouse: GreenhouseIcon,
|
||||
greptile: GreptileIcon,
|
||||
hex: HexIcon,
|
||||
@@ -259,13 +270,14 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
incidentio: IncidentioIcon,
|
||||
infisical: InfisicalIcon,
|
||||
intercom_v2: IntercomIcon,
|
||||
ironclad: IroncladIcon,
|
||||
jina: JinaAIIcon,
|
||||
jira: JiraIcon,
|
||||
jira_service_management: JiraServiceManagementIcon,
|
||||
kalshi_v2: KalshiIcon,
|
||||
ketch: KetchIcon,
|
||||
knowledge: PackageSearchIcon,
|
||||
langsmith: LangsmithIcon,
|
||||
launchdarkly: LaunchDarklyIcon,
|
||||
lemlist: LemlistIcon,
|
||||
linear: LinearIcon,
|
||||
linkedin: LinkedInIcon,
|
||||
@@ -300,6 +312,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
polymarket: PolymarketIcon,
|
||||
postgresql: PostgresIcon,
|
||||
posthog: PosthogIcon,
|
||||
profound: ProfoundIcon,
|
||||
pulse_v2: PulseIcon,
|
||||
qdrant: QdrantIcon,
|
||||
quiver: QuiverIcon,
|
||||
@@ -310,9 +323,11 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
resend: ResendIcon,
|
||||
revenuecat: RevenueCatIcon,
|
||||
rippling: RipplingIcon,
|
||||
rootly: RootlyIcon,
|
||||
s3: S3Icon,
|
||||
salesforce: SalesforceIcon,
|
||||
search: SearchIcon,
|
||||
secrets_manager: SecretsManagerIcon,
|
||||
sendgrid: SendgridIcon,
|
||||
sentry: SentryIcon,
|
||||
serper: SerperIcon,
|
||||
@@ -329,6 +344,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
stripe: StripeIcon,
|
||||
stt_v2: STTIcon,
|
||||
supabase: SupabaseIcon,
|
||||
tailscale: TailscaleIcon,
|
||||
tavily: TavilyIcon,
|
||||
telegram: TelegramIcon,
|
||||
textract_v2: TextractIcon,
|
||||
|
||||
@@ -131,7 +131,7 @@ Erkennt personenbezogene Daten mithilfe von Microsoft Presidio. Unterstützt üb
|
||||
**Anwendungsfälle:**
|
||||
- Blockieren von Inhalten mit sensiblen persönlichen Informationen
|
||||
- Maskieren von personenbezogenen Daten vor der Protokollierung oder Speicherung
|
||||
- Einhaltung der DSGVO, HIPAA und anderer Datenschutzbestimmungen
|
||||
- Einhaltung der DSGVO und anderer Datenschutzbestimmungen
|
||||
- Bereinigung von Benutzereingaben vor der Verarbeitung
|
||||
|
||||
## Konfiguration
|
||||
|
||||
@@ -132,7 +132,7 @@ Detects personally identifiable information using Microsoft Presidio. Supports o
|
||||
**Use Cases:**
|
||||
- Block content containing sensitive personal information
|
||||
- Mask PII before logging or storing data
|
||||
- Compliance with GDPR, HIPAA, and other privacy regulations
|
||||
- Compliance with GDPR and other privacy regulations
|
||||
- Sanitize user inputs before processing
|
||||
|
||||
## Configuration
|
||||
|
||||
206
apps/docs/content/docs/en/credentials/google-service-account.mdx
Normal file
206
apps/docs/content/docs/en/credentials/google-service-account.mdx
Normal file
@@ -0,0 +1,206 @@
|
||||
---
|
||||
title: Google Service Accounts
|
||||
description: Set up Google service accounts with domain-wide delegation for Gmail, Sheets, Drive, Calendar, and other Google services
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps'
|
||||
import { Image } from '@/components/ui/image'
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
Google service accounts with domain-wide delegation let your workflows access Google APIs on behalf of users in your Google Workspace domain — without requiring each user to complete an OAuth consent flow. This is ideal for automated workflows that need to send emails, read spreadsheets, or manage files across your organization.
|
||||
|
||||
For example, you could build a workflow that iterates through a list of employees, impersonates each one to read their Google Docs, and uploads the contents to a shared knowledge base — all without requiring any of those users to sign in.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before adding a service account to Sim, you need to configure it in the Google Cloud Console and Google Workspace Admin Console.
|
||||
|
||||
### 1. Create a Service Account in Google Cloud
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
Go to the [Google Cloud Console](https://console.cloud.google.com/) and select your project (or create one)
|
||||
</Step>
|
||||
<Step>
|
||||
Navigate to **IAM & Admin** → **Service Accounts**
|
||||
</Step>
|
||||
<Step>
|
||||
Click **Create Service Account**, give it a name and description, then click **Create and Continue**
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/credentials/gcp-create-service-account.png"
|
||||
alt="Google Cloud Console — Create service account form"
|
||||
width={700}
|
||||
height={500}
|
||||
className="my-4"
|
||||
/>
|
||||
</div>
|
||||
</Step>
|
||||
<Step>
|
||||
Skip the optional role and user access steps and click **Done**
|
||||
</Step>
|
||||
<Step>
|
||||
Click on the newly created service account, go to the **Keys** tab, and click **Add Key** → **Create new key**
|
||||
</Step>
|
||||
<Step>
|
||||
Select **JSON** as the key type and click **Create**. A JSON key file will download — keep this safe
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/credentials/gcp-create-private-key.png"
|
||||
alt="Google Cloud Console — Create private key dialog with JSON selected"
|
||||
width={700}
|
||||
height={400}
|
||||
className="my-4"
|
||||
/>
|
||||
</div>
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<Callout type="warn">
|
||||
The JSON key file contains your service account's private key. Treat it like a password — do not commit it to source control or share it publicly.
|
||||
</Callout>
|
||||
|
||||
### 2. Enable the Required APIs
|
||||
|
||||
In the Google Cloud Console, go to **APIs & Services** → **Library** and enable the APIs for the services your workflows will use. See the [scopes reference](#scopes-reference) below for the full list of APIs by service.
|
||||
|
||||
### 3. Set Up Domain-Wide Delegation
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
In the Google Cloud Console, go to **IAM & Admin** → **Service Accounts**, click on your service account, and copy the **Client ID** (the numeric ID, not the email)
|
||||
</Step>
|
||||
<Step>
|
||||
Open the [Google Workspace Admin Console](https://admin.google.com/) and navigate to **Security** → **Access and data control** → **API controls**
|
||||
</Step>
|
||||
<Step>
|
||||
Click **Manage Domain Wide Delegation**, then click **Add new**
|
||||
</Step>
|
||||
<Step>
|
||||
Paste the **Client ID** from your service account, then add the OAuth scopes for the services your workflows need. Copy the full scope URLs from the [scopes reference](#scopes-reference) below — only authorize scopes for services you plan to use.
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/credentials/gcp-add-client-id.png"
|
||||
alt="Google Workspace Admin Console — Add a new client ID with OAuth scopes"
|
||||
width={350}
|
||||
height={300}
|
||||
className="my-4"
|
||||
/>
|
||||
</div>
|
||||
</Step>
|
||||
<Step>
|
||||
Click **Authorize**
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<Callout type="info">
|
||||
Domain-wide delegation must be configured by a Google Workspace admin. If you are not an admin, send the Client ID and required scopes to your admin.
|
||||
</Callout>
|
||||
|
||||
### Scopes Reference
|
||||
|
||||
The table below lists every Google service that supports service account authentication in Sim, the API to enable in Google Cloud Console, and the delegation scopes to authorize. Copy the scope string for each service you need and paste it into the Google Workspace Admin Console.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="whitespace-nowrap">Service</th>
|
||||
<th className="whitespace-nowrap">API to Enable</th>
|
||||
<th>Delegation Scopes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>Gmail</td><td>Gmail API</td><td><code>{'https://www.googleapis.com/auth/gmail.send'}</code><br/><code>{'https://www.googleapis.com/auth/gmail.modify'}</code><br/><code>{'https://www.googleapis.com/auth/gmail.labels'}</code></td></tr>
|
||||
<tr><td>Google Sheets</td><td>Google Sheets API, Google Drive API</td><td><code>{'https://www.googleapis.com/auth/drive'}</code><br/><code>{'https://www.googleapis.com/auth/drive.file'}</code></td></tr>
|
||||
<tr><td>Google Drive</td><td>Google Drive API</td><td><code>{'https://www.googleapis.com/auth/drive'}</code><br/><code>{'https://www.googleapis.com/auth/drive.file'}</code></td></tr>
|
||||
<tr><td>Google Docs</td><td>Google Docs API, Google Drive API</td><td><code>{'https://www.googleapis.com/auth/drive'}</code><br/><code>{'https://www.googleapis.com/auth/drive.file'}</code></td></tr>
|
||||
<tr><td>Google Slides</td><td>Google Slides API, Google Drive API</td><td><code>{'https://www.googleapis.com/auth/drive'}</code><br/><code>{'https://www.googleapis.com/auth/drive.file'}</code></td></tr>
|
||||
<tr><td>Google Forms</td><td>Google Forms API, Google Drive API</td><td><code>{'https://www.googleapis.com/auth/drive'}</code><br/><code>{'https://www.googleapis.com/auth/forms.body'}</code><br/><code>{'https://www.googleapis.com/auth/forms.responses.readonly'}</code></td></tr>
|
||||
<tr><td>Google Calendar</td><td>Google Calendar API</td><td><code>{'https://www.googleapis.com/auth/calendar'}</code></td></tr>
|
||||
<tr><td>Google Contacts</td><td>People API</td><td><code>{'https://www.googleapis.com/auth/contacts'}</code></td></tr>
|
||||
<tr><td>BigQuery</td><td>BigQuery API</td><td><code>{'https://www.googleapis.com/auth/bigquery'}</code></td></tr>
|
||||
<tr><td>Google Tasks</td><td>Tasks API</td><td><code>{'https://www.googleapis.com/auth/tasks'}</code></td></tr>
|
||||
<tr><td>Google Vault</td><td>Vault API, Cloud Storage API</td><td><code>{'https://www.googleapis.com/auth/ediscovery'}</code><br/><code>{'https://www.googleapis.com/auth/devstorage.read_only'}</code></td></tr>
|
||||
<tr><td>Google Groups</td><td>Admin SDK API</td><td><code>{'https://www.googleapis.com/auth/admin.directory.group'}</code><br/><code>{'https://www.googleapis.com/auth/admin.directory.group.member'}</code></td></tr>
|
||||
<tr><td>Google Meet</td><td>Google Meet API</td><td><code>{'https://www.googleapis.com/auth/meetings.space.created'}</code><br/><code>{'https://www.googleapis.com/auth/meetings.space.readonly'}</code></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<Callout type="info">
|
||||
You only need to enable APIs and authorize scopes for the services you plan to use. When authorizing multiple services, combine their scope strings with commas into a single entry in the Admin Console.
|
||||
</Callout>
|
||||
|
||||
## Adding the Service Account to Sim
|
||||
|
||||
Once Google Cloud and Workspace are configured, add the service account as a credential in Sim.
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
Open your workspace **Settings** and go to the **Integrations** tab
|
||||
</Step>
|
||||
<Step>
|
||||
Search for "Google Service Account" and click **Connect**
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/credentials/integrations-service-account.png"
|
||||
alt="Integrations page showing Google Service Account"
|
||||
width={800}
|
||||
height={150}
|
||||
className="my-4"
|
||||
/>
|
||||
</div>
|
||||
</Step>
|
||||
<Step>
|
||||
Paste the full contents of your JSON key file into the text area
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/credentials/add-service-account.png"
|
||||
alt="Add Google Service Account dialog"
|
||||
width={350}
|
||||
height={420}
|
||||
className="my-6"
|
||||
/>
|
||||
</div>
|
||||
</Step>
|
||||
<Step>
|
||||
Give the credential a display name (the service account email is used by default)
|
||||
</Step>
|
||||
<Step>
|
||||
Click **Save**
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
The JSON key file is validated for the required fields (`type`, `client_email`, `private_key`, `project_id`) and encrypted before being stored.
|
||||
|
||||
## Using Delegated Access in Workflows
|
||||
|
||||
When you use a Google block (Gmail, Sheets, Drive, etc.) in a workflow and select a service account credential, an **Impersonate User Email** field appears below the credential selector.
|
||||
|
||||
Enter the email address of the Google Workspace user you want the service account to act as. For example, if you enter `alice@yourcompany.com`, the workflow will send emails from Alice's account, read her spreadsheets, or access her calendar — depending on the scopes you authorized.
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/credentials/workflow-impersonated-account.png"
|
||||
alt="Gmail block in a workflow showing the Impersonated Account field with a service account credential"
|
||||
width={800}
|
||||
height={350}
|
||||
className="my-4"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Callout type="warn">
|
||||
The impersonated email must belong to a user in the Google Workspace domain where you configured domain-wide delegation. Impersonating external email addresses will fail.
|
||||
</Callout>
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "Can I use a service account without domain-wide delegation?", answer: "Yes, but it will only be able to access resources owned by the service account itself (e.g., spreadsheets shared directly with the service account email). Without delegation, you cannot impersonate users or access their personal data like Gmail." },
|
||||
{ question: "What happens if the impersonation email field is left blank?", answer: "The service account will authenticate as itself. This works for accessing shared resources (like a Google Sheet shared with the service account email) but will fail for user-specific APIs like Gmail." },
|
||||
{ question: "Can I use the same service account for multiple Google services?", answer: "Yes. A single service account can be used across Gmail, Sheets, Drive, Calendar, and other Google services — as long as the required API is enabled in Google Cloud and the corresponding scopes are authorized in the Workspace admin console." },
|
||||
{ question: "How do I rotate the service account key?", answer: "Create a new JSON key in the Google Cloud Console under your service account's Keys tab, then update the credential in Sim with the new key. Delete the old key from Google Cloud once the new one is working." },
|
||||
{ question: "Does the impersonated user need a Google Workspace license?", answer: "Yes. Domain-wide delegation only works with users who have a Google Workspace account in the domain. Consumer Gmail accounts (e.g., @gmail.com) cannot be impersonated." },
|
||||
]} />
|
||||
5
apps/docs/content/docs/en/credentials/meta.json
Normal file
5
apps/docs/content/docs/en/credentials/meta.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"title": "Credentials",
|
||||
"pages": ["index", "google-service-account"],
|
||||
"defaultOpen": false
|
||||
}
|
||||
592
apps/docs/content/docs/en/tools/agentmail.mdx
Normal file
592
apps/docs/content/docs/en/tools/agentmail.mdx
Normal file
@@ -0,0 +1,592 @@
|
||||
---
|
||||
title: AgentMail
|
||||
description: Manage email inboxes, threads, and messages with AgentMail
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="agentmail"
|
||||
color="#000000"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[AgentMail](https://agentmail.to/) is an API-first email platform built for agents and automation. AgentMail lets you create email inboxes on the fly, send and receive messages, reply to threads, manage drafts, and organize conversations with labels — all through a simple REST API designed for programmatic access.
|
||||
|
||||
**Why AgentMail?**
|
||||
- **Agent-Native Email:** Purpose-built for AI agents and automation — create inboxes, send messages, and manage threads without human-facing UI overhead.
|
||||
- **Full Email Lifecycle:** Send new messages, reply to threads, forward emails, manage drafts, and schedule sends — all from a single API.
|
||||
- **Thread & Conversation Management:** Organize emails into threads with full read, reply, forward, and label support for structured conversation tracking.
|
||||
- **Draft Workflow:** Compose drafts, update them, schedule sends, and dispatch when ready — perfect for review-before-send workflows.
|
||||
- **Label Organization:** Tag threads and messages with custom labels for filtering, routing, and downstream automation.
|
||||
|
||||
**Using AgentMail in Sim**
|
||||
|
||||
Sim's AgentMail integration connects your agentic workflows directly to AgentMail using an API key. With 20 operations spanning inboxes, threads, messages, and drafts, you can build powerful email automations without writing backend code.
|
||||
|
||||
**Key benefits of using AgentMail in Sim:**
|
||||
- **Dynamic inbox creation:** Spin up new inboxes on the fly for each agent, workflow, or customer — perfect for multi-tenant email handling.
|
||||
- **Automated email processing:** List and read incoming messages, then trigger downstream actions based on content, sender, or labels.
|
||||
- **Conversational email:** Reply to threads and forward messages to keep conversations flowing naturally within your automated workflows.
|
||||
- **Draft and review workflows:** Create drafts, update them with AI-generated content, and send when approved — ideal for human-in-the-loop patterns.
|
||||
- **Email organization:** Apply labels to threads and messages to categorize, filter, and route emails through your automation pipeline.
|
||||
|
||||
Whether you're building an AI email assistant, automating customer support replies, processing incoming leads, or managing multi-agent email workflows, AgentMail in Sim gives you direct, secure access to the full AgentMail API — no middleware required. Simply configure your API key, select the operation you need, and let Sim handle the rest.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate AgentMail into your workflow. Create and manage email inboxes, send and receive messages, reply to threads, manage drafts, and organize threads with labels. Requires API Key.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `agentmail_create_draft`
|
||||
|
||||
Create a new email draft in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox to create the draft in |
|
||||
| `to` | string | No | Recipient email addresses \(comma-separated\) |
|
||||
| `subject` | string | No | Draft subject line |
|
||||
| `text` | string | No | Plain text draft body |
|
||||
| `html` | string | No | HTML draft body |
|
||||
| `cc` | string | No | CC recipient email addresses \(comma-separated\) |
|
||||
| `bcc` | string | No | BCC recipient email addresses \(comma-separated\) |
|
||||
| `inReplyTo` | string | No | ID of message being replied to |
|
||||
| `sendAt` | string | No | ISO 8601 timestamp to schedule sending |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `draftId` | string | Unique identifier for the draft |
|
||||
| `inboxId` | string | Inbox the draft belongs to |
|
||||
| `subject` | string | Draft subject |
|
||||
| `to` | array | Recipient email addresses |
|
||||
| `cc` | array | CC email addresses |
|
||||
| `bcc` | array | BCC email addresses |
|
||||
| `text` | string | Plain text content |
|
||||
| `html` | string | HTML content |
|
||||
| `preview` | string | Draft preview text |
|
||||
| `labels` | array | Labels assigned to the draft |
|
||||
| `inReplyTo` | string | Message ID this draft replies to |
|
||||
| `sendStatus` | string | Send status \(scheduled, sending, failed\) |
|
||||
| `sendAt` | string | Scheduled send time |
|
||||
| `createdAt` | string | Creation timestamp |
|
||||
| `updatedAt` | string | Last updated timestamp |
|
||||
|
||||
### `agentmail_create_inbox`
|
||||
|
||||
Create a new email inbox with AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `username` | string | No | Username for the inbox email address |
|
||||
| `domain` | string | No | Domain for the inbox email address |
|
||||
| `displayName` | string | No | Display name for the inbox |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `inboxId` | string | Unique identifier for the inbox |
|
||||
| `email` | string | Email address of the inbox |
|
||||
| `displayName` | string | Display name of the inbox |
|
||||
| `createdAt` | string | Creation timestamp |
|
||||
| `updatedAt` | string | Last updated timestamp |
|
||||
|
||||
### `agentmail_delete_draft`
|
||||
|
||||
Delete an email draft in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox containing the draft |
|
||||
| `draftId` | string | Yes | ID of the draft to delete |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `deleted` | boolean | Whether the draft was successfully deleted |
|
||||
|
||||
### `agentmail_delete_inbox`
|
||||
|
||||
Delete an email inbox in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox to delete |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `deleted` | boolean | Whether the inbox was successfully deleted |
|
||||
|
||||
### `agentmail_delete_thread`
|
||||
|
||||
Delete an email thread in AgentMail (moves to trash, or permanently deletes if already in trash)
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox containing the thread |
|
||||
| `threadId` | string | Yes | ID of the thread to delete |
|
||||
| `permanent` | boolean | No | Force permanent deletion instead of moving to trash |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `deleted` | boolean | Whether the thread was successfully deleted |
|
||||
|
||||
### `agentmail_forward_message`
|
||||
|
||||
Forward an email message to new recipients in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox containing the message |
|
||||
| `messageId` | string | Yes | ID of the message to forward |
|
||||
| `to` | string | Yes | Recipient email addresses \(comma-separated\) |
|
||||
| `subject` | string | No | Override subject line |
|
||||
| `text` | string | No | Additional plain text to prepend |
|
||||
| `html` | string | No | Additional HTML to prepend |
|
||||
| `cc` | string | No | CC recipient email addresses \(comma-separated\) |
|
||||
| `bcc` | string | No | BCC recipient email addresses \(comma-separated\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `messageId` | string | ID of the forwarded message |
|
||||
| `threadId` | string | ID of the thread |
|
||||
|
||||
### `agentmail_get_draft`
|
||||
|
||||
Get details of a specific email draft in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox the draft belongs to |
|
||||
| `draftId` | string | Yes | ID of the draft to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `draftId` | string | Unique identifier for the draft |
|
||||
| `inboxId` | string | Inbox the draft belongs to |
|
||||
| `subject` | string | Draft subject |
|
||||
| `to` | array | Recipient email addresses |
|
||||
| `cc` | array | CC email addresses |
|
||||
| `bcc` | array | BCC email addresses |
|
||||
| `text` | string | Plain text content |
|
||||
| `html` | string | HTML content |
|
||||
| `preview` | string | Draft preview text |
|
||||
| `labels` | array | Labels assigned to the draft |
|
||||
| `inReplyTo` | string | Message ID this draft replies to |
|
||||
| `sendStatus` | string | Send status \(scheduled, sending, failed\) |
|
||||
| `sendAt` | string | Scheduled send time |
|
||||
| `createdAt` | string | Creation timestamp |
|
||||
| `updatedAt` | string | Last updated timestamp |
|
||||
|
||||
### `agentmail_get_inbox`
|
||||
|
||||
Get details of a specific email inbox in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `inboxId` | string | Unique identifier for the inbox |
|
||||
| `email` | string | Email address of the inbox |
|
||||
| `displayName` | string | Display name of the inbox |
|
||||
| `createdAt` | string | Creation timestamp |
|
||||
| `updatedAt` | string | Last updated timestamp |
|
||||
|
||||
### `agentmail_get_message`
|
||||
|
||||
Get details of a specific email message in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox containing the message |
|
||||
| `messageId` | string | Yes | ID of the message to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `messageId` | string | Unique identifier for the message |
|
||||
| `threadId` | string | ID of the thread this message belongs to |
|
||||
| `from` | string | Sender email address |
|
||||
| `to` | array | Recipient email addresses |
|
||||
| `cc` | array | CC email addresses |
|
||||
| `bcc` | array | BCC email addresses |
|
||||
| `subject` | string | Message subject |
|
||||
| `text` | string | Plain text content |
|
||||
| `html` | string | HTML content |
|
||||
| `createdAt` | string | Creation timestamp |
|
||||
|
||||
### `agentmail_get_thread`
|
||||
|
||||
Get details of a specific email thread including messages in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox containing the thread |
|
||||
| `threadId` | string | Yes | ID of the thread to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `threadId` | string | Unique identifier for the thread |
|
||||
| `subject` | string | Thread subject |
|
||||
| `senders` | array | List of sender email addresses |
|
||||
| `recipients` | array | List of recipient email addresses |
|
||||
| `messageCount` | number | Number of messages in the thread |
|
||||
| `labels` | array | Labels assigned to the thread |
|
||||
| `lastMessageAt` | string | Timestamp of last message |
|
||||
| `createdAt` | string | Creation timestamp |
|
||||
| `updatedAt` | string | Last updated timestamp |
|
||||
| `messages` | array | Messages in the thread |
|
||||
| ↳ `messageId` | string | Unique identifier for the message |
|
||||
| ↳ `from` | string | Sender email address |
|
||||
| ↳ `to` | array | Recipient email addresses |
|
||||
| ↳ `cc` | array | CC email addresses |
|
||||
| ↳ `bcc` | array | BCC email addresses |
|
||||
| ↳ `subject` | string | Message subject |
|
||||
| ↳ `text` | string | Plain text content |
|
||||
| ↳ `html` | string | HTML content |
|
||||
| ↳ `createdAt` | string | Creation timestamp |
|
||||
|
||||
### `agentmail_list_drafts`
|
||||
|
||||
List email drafts in an inbox in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox to list drafts from |
|
||||
| `limit` | number | No | Maximum number of drafts to return |
|
||||
| `pageToken` | string | No | Pagination token for next page of results |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `drafts` | array | List of drafts |
|
||||
| ↳ `draftId` | string | Unique identifier for the draft |
|
||||
| ↳ `inboxId` | string | Inbox the draft belongs to |
|
||||
| ↳ `subject` | string | Draft subject |
|
||||
| ↳ `to` | array | Recipient email addresses |
|
||||
| ↳ `cc` | array | CC email addresses |
|
||||
| ↳ `bcc` | array | BCC email addresses |
|
||||
| ↳ `preview` | string | Draft preview text |
|
||||
| ↳ `sendStatus` | string | Send status \(scheduled, sending, failed\) |
|
||||
| ↳ `sendAt` | string | Scheduled send time |
|
||||
| ↳ `createdAt` | string | Creation timestamp |
|
||||
| ↳ `updatedAt` | string | Last updated timestamp |
|
||||
| `count` | number | Total number of drafts |
|
||||
| `nextPageToken` | string | Token for retrieving the next page |
|
||||
|
||||
### `agentmail_list_inboxes`
|
||||
|
||||
List all email inboxes in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `limit` | number | No | Maximum number of inboxes to return |
|
||||
| `pageToken` | string | No | Pagination token for next page of results |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `inboxes` | array | List of inboxes |
|
||||
| ↳ `inboxId` | string | Unique identifier for the inbox |
|
||||
| ↳ `email` | string | Email address of the inbox |
|
||||
| ↳ `displayName` | string | Display name of the inbox |
|
||||
| ↳ `createdAt` | string | Creation timestamp |
|
||||
| ↳ `updatedAt` | string | Last updated timestamp |
|
||||
| `count` | number | Total number of inboxes |
|
||||
| `nextPageToken` | string | Token for retrieving the next page |
|
||||
|
||||
### `agentmail_list_messages`
|
||||
|
||||
List messages in an inbox in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox to list messages from |
|
||||
| `limit` | number | No | Maximum number of messages to return |
|
||||
| `pageToken` | string | No | Pagination token for next page of results |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `messages` | array | List of messages in the inbox |
|
||||
| ↳ `messageId` | string | Unique identifier for the message |
|
||||
| ↳ `from` | string | Sender email address |
|
||||
| ↳ `to` | array | Recipient email addresses |
|
||||
| ↳ `subject` | string | Message subject |
|
||||
| ↳ `preview` | string | Message preview text |
|
||||
| ↳ `createdAt` | string | Creation timestamp |
|
||||
| `count` | number | Total number of messages |
|
||||
| `nextPageToken` | string | Token for retrieving the next page |
|
||||
|
||||
### `agentmail_list_threads`
|
||||
|
||||
List email threads in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox to list threads from |
|
||||
| `limit` | number | No | Maximum number of threads to return |
|
||||
| `pageToken` | string | No | Pagination token for next page of results |
|
||||
| `labels` | string | No | Comma-separated labels to filter threads by |
|
||||
| `before` | string | No | Filter threads before this ISO 8601 timestamp |
|
||||
| `after` | string | No | Filter threads after this ISO 8601 timestamp |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `threads` | array | List of email threads |
|
||||
| ↳ `threadId` | string | Unique identifier for the thread |
|
||||
| ↳ `subject` | string | Thread subject |
|
||||
| ↳ `senders` | array | List of sender email addresses |
|
||||
| ↳ `recipients` | array | List of recipient email addresses |
|
||||
| ↳ `messageCount` | number | Number of messages in the thread |
|
||||
| ↳ `lastMessageAt` | string | Timestamp of last message |
|
||||
| ↳ `createdAt` | string | Creation timestamp |
|
||||
| ↳ `updatedAt` | string | Last updated timestamp |
|
||||
| `count` | number | Total number of threads |
|
||||
| `nextPageToken` | string | Token for retrieving the next page |
|
||||
|
||||
### `agentmail_reply_message`
|
||||
|
||||
Reply to an existing email message in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox to reply from |
|
||||
| `messageId` | string | Yes | ID of the message to reply to |
|
||||
| `text` | string | No | Plain text reply body |
|
||||
| `html` | string | No | HTML reply body |
|
||||
| `to` | string | No | Override recipient email addresses \(comma-separated\) |
|
||||
| `cc` | string | No | CC email addresses \(comma-separated\) |
|
||||
| `bcc` | string | No | BCC email addresses \(comma-separated\) |
|
||||
| `replyAll` | boolean | No | Reply to all recipients of the original message |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `messageId` | string | ID of the sent reply message |
|
||||
| `threadId` | string | ID of the thread |
|
||||
|
||||
### `agentmail_send_draft`
|
||||
|
||||
Send an existing email draft in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox containing the draft |
|
||||
| `draftId` | string | Yes | ID of the draft to send |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `messageId` | string | ID of the sent message |
|
||||
| `threadId` | string | ID of the thread |
|
||||
|
||||
### `agentmail_send_message`
|
||||
|
||||
Send an email message from an AgentMail inbox
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox to send from |
|
||||
| `to` | string | Yes | Recipient email address \(comma-separated for multiple\) |
|
||||
| `subject` | string | Yes | Email subject line |
|
||||
| `text` | string | No | Plain text email body |
|
||||
| `html` | string | No | HTML email body |
|
||||
| `cc` | string | No | CC recipient email addresses \(comma-separated\) |
|
||||
| `bcc` | string | No | BCC recipient email addresses \(comma-separated\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `threadId` | string | ID of the created thread |
|
||||
| `messageId` | string | ID of the sent message |
|
||||
| `subject` | string | Email subject line |
|
||||
| `to` | string | Recipient email address |
|
||||
|
||||
### `agentmail_update_draft`
|
||||
|
||||
Update an existing email draft in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox containing the draft |
|
||||
| `draftId` | string | Yes | ID of the draft to update |
|
||||
| `to` | string | No | Recipient email addresses \(comma-separated\) |
|
||||
| `subject` | string | No | Draft subject line |
|
||||
| `text` | string | No | Plain text draft body |
|
||||
| `html` | string | No | HTML draft body |
|
||||
| `cc` | string | No | CC recipient email addresses \(comma-separated\) |
|
||||
| `bcc` | string | No | BCC recipient email addresses \(comma-separated\) |
|
||||
| `sendAt` | string | No | ISO 8601 timestamp to schedule sending |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `draftId` | string | Unique identifier for the draft |
|
||||
| `inboxId` | string | Inbox the draft belongs to |
|
||||
| `subject` | string | Draft subject |
|
||||
| `to` | array | Recipient email addresses |
|
||||
| `cc` | array | CC email addresses |
|
||||
| `bcc` | array | BCC email addresses |
|
||||
| `text` | string | Plain text content |
|
||||
| `html` | string | HTML content |
|
||||
| `preview` | string | Draft preview text |
|
||||
| `labels` | array | Labels assigned to the draft |
|
||||
| `inReplyTo` | string | Message ID this draft replies to |
|
||||
| `sendStatus` | string | Send status \(scheduled, sending, failed\) |
|
||||
| `sendAt` | string | Scheduled send time |
|
||||
| `createdAt` | string | Creation timestamp |
|
||||
| `updatedAt` | string | Last updated timestamp |
|
||||
|
||||
### `agentmail_update_inbox`
|
||||
|
||||
Update the display name of an email inbox in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox to update |
|
||||
| `displayName` | string | Yes | New display name for the inbox |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `inboxId` | string | Unique identifier for the inbox |
|
||||
| `email` | string | Email address of the inbox |
|
||||
| `displayName` | string | Display name of the inbox |
|
||||
| `createdAt` | string | Creation timestamp |
|
||||
| `updatedAt` | string | Last updated timestamp |
|
||||
|
||||
### `agentmail_update_message`
|
||||
|
||||
Add or remove labels on an email message in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox containing the message |
|
||||
| `messageId` | string | Yes | ID of the message to update |
|
||||
| `addLabels` | string | No | Comma-separated labels to add to the message |
|
||||
| `removeLabels` | string | No | Comma-separated labels to remove from the message |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `messageId` | string | Unique identifier for the message |
|
||||
| `labels` | array | Current labels on the message |
|
||||
|
||||
### `agentmail_update_thread`
|
||||
|
||||
Add or remove labels on an email thread in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox containing the thread |
|
||||
| `threadId` | string | Yes | ID of the thread to update |
|
||||
| `addLabels` | string | No | Comma-separated labels to add to the thread |
|
||||
| `removeLabels` | string | No | Comma-separated labels to remove from the thread |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `threadId` | string | Unique identifier for the thread |
|
||||
| `labels` | array | Current labels on the thread |
|
||||
|
||||
|
||||
@@ -359,6 +359,35 @@ List tasks in Attio, optionally filtered by record, assignee, or completion stat
|
||||
| ↳ `createdAt` | string | When the task was created |
|
||||
| `count` | number | Number of tasks returned |
|
||||
|
||||
### `attio_get_task`
|
||||
|
||||
Get a single task by ID from Attio
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `taskId` | string | Yes | The ID of the task to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `taskId` | string | The task ID |
|
||||
| `content` | string | The task content |
|
||||
| `deadlineAt` | string | The task deadline |
|
||||
| `isCompleted` | boolean | Whether the task is completed |
|
||||
| `linkedRecords` | array | Records linked to this task |
|
||||
| ↳ `targetObjectId` | string | The linked object ID |
|
||||
| ↳ `targetRecordId` | string | The linked record ID |
|
||||
| `assignees` | array | Task assignees |
|
||||
| ↳ `type` | string | The assignee actor type \(e.g. workspace-member\) |
|
||||
| ↳ `id` | string | The assignee actor ID |
|
||||
| `createdByActor` | object | The actor who created this task |
|
||||
| ↳ `type` | string | The actor type \(e.g. workspace-member, api-token, system\) |
|
||||
| ↳ `id` | string | The actor ID |
|
||||
| `createdAt` | string | When the task was created |
|
||||
|
||||
### `attio_create_task`
|
||||
|
||||
Create a task in Attio
|
||||
@@ -1012,8 +1041,8 @@ Update a webhook in Attio (target URL and/or subscriptions)
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `webhookId` | string | Yes | The webhook ID to update |
|
||||
| `targetUrl` | string | Yes | HTTPS target URL for webhook delivery |
|
||||
| `subscriptions` | string | Yes | JSON array of subscriptions, e.g. \[\{"event_type":"note.created"\}\] |
|
||||
| `targetUrl` | string | No | HTTPS target URL for webhook delivery |
|
||||
| `subscriptions` | string | No | JSON array of subscriptions, e.g. \[\{"event_type":"note.created"\}\] |
|
||||
|
||||
#### Output
|
||||
|
||||
|
||||
39
apps/docs/content/docs/en/tools/extend.mdx
Normal file
39
apps/docs/content/docs/en/tools/extend.mdx
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
title: Extend
|
||||
description: Parse and extract content from documents
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="extend_v2"
|
||||
color="#000000"
|
||||
/>
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Extend AI into the workflow. Parse and extract structured content from documents or file references.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `extend_parser`
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `filePath` | string | No | URL to a document to be processed |
|
||||
| `file` | file | No | Document file to be processed |
|
||||
| `fileUpload` | object | No | File upload data from file-upload component |
|
||||
| `outputFormat` | string | No | Target output format \(markdown or spatial\). Defaults to markdown. |
|
||||
| `chunking` | string | No | Chunking strategy \(page, document, or section\). Defaults to page. |
|
||||
| `engine` | string | No | Parsing engine \(parse_performance or parse_light\). Defaults to parse_performance. |
|
||||
| `apiKey` | string | Yes | Extend API key |
|
||||
|
||||
#### Output
|
||||
|
||||
This tool does not produce any outputs.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: File
|
||||
description: Read and parse multiple files
|
||||
description: Read and write workspace files
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
@@ -27,7 +27,7 @@ The File Parser tool is particularly useful for scenarios where your agents need
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Upload files directly or import from external URLs to get UserFile objects for use in other blocks.
|
||||
Read and parse files from uploads or URLs, write new workspace files, or append content to existing files.
|
||||
|
||||
|
||||
|
||||
@@ -52,4 +52,45 @@ Parse one or more uploaded files or files from URLs (text, PDF, CSV, images, etc
|
||||
| `files` | file[] | Parsed files as UserFile objects |
|
||||
| `combinedContent` | string | Combined content of all parsed files |
|
||||
|
||||
### `file_write`
|
||||
|
||||
Create a new workspace file. If a file with the same name already exists, a numeric suffix is added (e.g.,
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `fileName` | string | Yes | File name \(e.g., "data.csv"\). If a file with this name exists, a numeric suffix is added automatically. |
|
||||
| `content` | string | Yes | The text content to write to the file. |
|
||||
| `contentType` | string | No | MIME type for new files \(e.g., "text/plain"\). Auto-detected from file extension if omitted. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | File ID |
|
||||
| `name` | string | File name |
|
||||
| `size` | number | File size in bytes |
|
||||
| `url` | string | URL to access the file |
|
||||
|
||||
### `file_append`
|
||||
|
||||
Append content to an existing workspace file. The file must already exist. Content is added to the end of the file.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `fileName` | string | Yes | Name of an existing workspace file to append to. |
|
||||
| `content` | string | Yes | The text content to append to the file. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | File ID |
|
||||
| `name` | string | File name |
|
||||
| `size` | number | File size in bytes |
|
||||
| `url` | string | URL to access the file |
|
||||
|
||||
|
||||
|
||||
92
apps/docs/content/docs/en/tools/granola.mdx
Normal file
92
apps/docs/content/docs/en/tools/granola.mdx
Normal file
@@ -0,0 +1,92 @@
|
||||
---
|
||||
title: Granola
|
||||
description: Access meeting notes and transcripts from Granola
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="granola"
|
||||
color="#B2C147"
|
||||
/>
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Granola into your workflow to retrieve meeting notes, summaries, attendees, and transcripts.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `granola_list_notes`
|
||||
|
||||
Lists meeting notes from Granola with optional date filters and pagination.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Granola API key |
|
||||
| `createdBefore` | string | No | Return notes created before this date \(ISO 8601\) |
|
||||
| `createdAfter` | string | No | Return notes created after this date \(ISO 8601\) |
|
||||
| `updatedAfter` | string | No | Return notes updated after this date \(ISO 8601\) |
|
||||
| `cursor` | string | No | Pagination cursor from a previous response |
|
||||
| `pageSize` | number | No | Number of notes per page \(1-30, default 10\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `notes` | json | List of meeting notes |
|
||||
| ↳ `id` | string | Note ID |
|
||||
| ↳ `title` | string | Note title |
|
||||
| ↳ `ownerName` | string | Note owner name |
|
||||
| ↳ `ownerEmail` | string | Note owner email |
|
||||
| ↳ `createdAt` | string | Creation timestamp |
|
||||
| ↳ `updatedAt` | string | Last update timestamp |
|
||||
| `hasMore` | boolean | Whether more notes are available |
|
||||
| `cursor` | string | Pagination cursor for the next page |
|
||||
|
||||
### `granola_get_note`
|
||||
|
||||
Retrieves a specific meeting note from Granola by ID, including summary, attendees, calendar event details, and optionally the transcript.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Granola API key |
|
||||
| `noteId` | string | Yes | The note ID \(e.g., not_1d3tmYTlCICgjy\) |
|
||||
| `includeTranscript` | string | No | Whether to include the meeting transcript |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Note ID |
|
||||
| `title` | string | Note title |
|
||||
| `ownerName` | string | Note owner name |
|
||||
| `ownerEmail` | string | Note owner email |
|
||||
| `createdAt` | string | Creation timestamp |
|
||||
| `updatedAt` | string | Last update timestamp |
|
||||
| `summaryText` | string | Plain text summary of the meeting |
|
||||
| `summaryMarkdown` | string | Markdown-formatted summary of the meeting |
|
||||
| `attendees` | json | Meeting attendees |
|
||||
| ↳ `name` | string | Attendee name |
|
||||
| ↳ `email` | string | Attendee email |
|
||||
| `folders` | json | Folders the note belongs to |
|
||||
| ↳ `id` | string | Folder ID |
|
||||
| ↳ `name` | string | Folder name |
|
||||
| `calendarEventTitle` | string | Calendar event title |
|
||||
| `calendarOrganiser` | string | Calendar event organiser email |
|
||||
| `calendarEventId` | string | Calendar event ID |
|
||||
| `scheduledStartTime` | string | Scheduled start time |
|
||||
| `scheduledEndTime` | string | Scheduled end time |
|
||||
| `invitees` | json | Calendar event invitee emails |
|
||||
| `transcript` | json | Meeting transcript entries \(only if requested\) |
|
||||
| ↳ `speaker` | string | Speaker source \(microphone or speaker\) |
|
||||
| ↳ `text` | string | Transcript text |
|
||||
| ↳ `startTime` | string | Segment start time |
|
||||
| ↳ `endTime` | string | Segment end time |
|
||||
|
||||
|
||||
@@ -1,259 +0,0 @@
|
||||
---
|
||||
title: Ironclad
|
||||
description: Contract lifecycle management with Ironclad
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="ironclad"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Ironclad](https://ironcladapp.com/) is a leading contract lifecycle management (CLM) platform that helps legal teams streamline contract creation, negotiation, approval, and storage. It enables organizations to manage the entire contract process from start to finish in a single, unified platform.
|
||||
|
||||
With Ironclad, you can:
|
||||
|
||||
- **Automate contract workflows**: Create and manage contract workflows using configurable templates with built-in approval routing
|
||||
- **Manage records**: Store and organize contract records with custom metadata, properties, and linked relationships
|
||||
- **Track approvals**: Monitor approval status across workflows with conditional approval groups
|
||||
- **Collaborate with comments**: Add and view comments on workflows for team communication and audit trails
|
||||
|
||||
In Sim, the Ironclad integration enables your agents to interact with Ironclad's workflow and records APIs programmatically. This allows for seamless contract operations like creating new workflows from templates, updating contract metadata, managing records, and tracking approvals - all within your agent workflows. Use Ironclad to automate contract lifecycle processes, keep records in sync, and enable your agents to participate in contract management decisions.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Manage workflows and records in Ironclad. Create and track contract workflows, manage records, view approvals, add comments, and update metadata. Requires an Ironclad OAuth connection.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `ironclad_create_workflow`
|
||||
|
||||
Create a new workflow in Ironclad using a specified template and attributes.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `template` | string | Yes | The template ID to use for the workflow |
|
||||
| `attributes` | string | No | JSON object of workflow attributes |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | The ID of the created workflow |
|
||||
| `status` | string | The status of the workflow |
|
||||
| `template` | string | The template used for the workflow |
|
||||
| `creator` | string | The creator of the workflow |
|
||||
|
||||
### `ironclad_list_workflows`
|
||||
|
||||
List all workflows in Ironclad with pagination support.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `page` | number | No | Page number \(starting from 0\) |
|
||||
| `perPage` | number | No | Number of results per page \(max 100\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `workflows` | json | List of workflows |
|
||||
| `page` | number | Current page number |
|
||||
| `pageSize` | number | Number of results per page |
|
||||
| `count` | number | Total number of workflows |
|
||||
|
||||
### `ironclad_get_workflow`
|
||||
|
||||
Retrieve details of a specific workflow by its ID.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `workflowId` | string | Yes | The unique identifier of the workflow |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | The workflow ID |
|
||||
| `status` | string | The workflow status |
|
||||
| `template` | string | The template used for the workflow |
|
||||
| `creator` | string | The creator of the workflow |
|
||||
| `step` | string | The current step of the workflow |
|
||||
| `attributes` | json | The workflow attributes |
|
||||
|
||||
### `ironclad_update_workflow_metadata`
|
||||
|
||||
Update attributes on a workflow. The workflow must be in the Review step. Supports set and remove actions.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `workflowId` | string | Yes | The unique identifier of the workflow |
|
||||
| `actions` | string | Yes | JSON array of actions. Each action has "action" \(set/remove\), "field", and optionally "value". |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether the update was successful |
|
||||
|
||||
### `ironclad_cancel_workflow`
|
||||
|
||||
Cancel a workflow instance by its ID.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `workflowId` | string | Yes | The unique identifier of the workflow to cancel |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether the cancellation was successful |
|
||||
|
||||
### `ironclad_list_workflow_approvals`
|
||||
|
||||
List all triggered approvals for a specific workflow. Conditional approvals that have not been triggered will not appear.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `workflowId` | string | Yes | The unique identifier of the workflow |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `approvals` | json | List of triggered approval groups for the workflow |
|
||||
|
||||
### `ironclad_add_comment`
|
||||
|
||||
Add a comment to a workflow.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `workflowId` | string | Yes | The unique identifier of the workflow |
|
||||
| `comment` | string | Yes | The comment text to add |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether the comment was added successfully |
|
||||
|
||||
### `ironclad_list_workflow_comments`
|
||||
|
||||
List all comments on a workflow.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `workflowId` | string | Yes | The unique identifier of the workflow |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `comments` | json | List of comments on the workflow |
|
||||
|
||||
### `ironclad_create_record`
|
||||
|
||||
Create a new record in Ironclad with a specified type, name, and properties.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `recordType` | string | Yes | The record type \(e.g., "contract", "Statement of Work"\) |
|
||||
| `name` | string | Yes | A human-readable name for the record |
|
||||
| `properties` | string | No | JSON object of properties. Each property has a "type" \(string/number/email/date/monetary_amount\) and "value". |
|
||||
| `links` | string | No | JSON array of linked record objects, each with a "recordId" field |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | The ID of the created record |
|
||||
| `name` | string | The name of the record |
|
||||
| `type` | string | The type of the record |
|
||||
|
||||
### `ironclad_list_records`
|
||||
|
||||
List all records in Ironclad with pagination and optional filtering by last updated time.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `page` | number | No | Page number \(starting from 0\) |
|
||||
| `pageSize` | number | No | Number of results per page \(max 100\) |
|
||||
| `lastUpdated` | string | No | Filter records updated on or after this ISO 8601 timestamp |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `records` | json | List of records |
|
||||
| `page` | number | Current page number |
|
||||
| `pageSize` | number | Number of results per page |
|
||||
| `count` | number | Total number of records |
|
||||
|
||||
### `ironclad_get_record`
|
||||
|
||||
Retrieve details of a specific record by its ID.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `recordId` | string | Yes | The unique identifier of the record |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | The record ID |
|
||||
| `name` | string | The record name |
|
||||
| `type` | string | The record type |
|
||||
| `properties` | json | The record properties |
|
||||
| `createdAt` | string | When the record was created |
|
||||
| `updatedAt` | string | When the record was last updated |
|
||||
|
||||
### `ironclad_update_record`
|
||||
|
||||
Update metadata fields on an existing record.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `recordId` | string | Yes | The unique identifier of the record to update |
|
||||
| `properties` | string | Yes | JSON object of fields to update \(e.g., \{"fieldName": "newValue"\}\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | The record ID |
|
||||
| `name` | string | The record name |
|
||||
| `type` | string | The record type |
|
||||
|
||||
|
||||
149
apps/docs/content/docs/en/tools/ketch.mdx
Normal file
149
apps/docs/content/docs/en/tools/ketch.mdx
Normal file
@@ -0,0 +1,149 @@
|
||||
---
|
||||
title: Ketch
|
||||
description: Manage privacy consent, subscriptions, and data subject rights
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="ketch"
|
||||
color="#9B5CFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Ketch](https://www.ketch.com/) is an AI-powered privacy, consent, and data governance platform that helps organizations automate compliance with global privacy regulations. It provides tools for managing consent preferences, handling data subject rights requests, and controlling subscription communications.
|
||||
|
||||
With Ketch, you can:
|
||||
|
||||
- **Retrieve consent status**: Query the current consent preferences for any data subject across configured purposes and legal bases
|
||||
- **Update consent preferences**: Set or modify consent for specific purposes (e.g., analytics, marketing) with the appropriate legal basis (opt-in, opt-out, disclosure)
|
||||
- **Manage subscriptions**: Get and update subscription topic preferences and global controls across contact methods like email and SMS
|
||||
- **Invoke data subject rights**: Submit privacy rights requests including data access, deletion, correction, and processing restriction under regulations like GDPR and CCPA
|
||||
|
||||
To use Ketch, drop the Ketch block into your workflow and provide your organization code, property code, and environment code. The Ketch Web API is a public API — no API key or OAuth credentials are required. Identity is determined by the organization and property codes along with the data subject's identity (e.g., email address).
|
||||
|
||||
These capabilities let you automate privacy compliance workflows, respond to user consent changes in real time, and manage data subject rights requests as part of your broader automation pipelines.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Ketch into the workflow. Retrieve and update consent preferences, manage subscription topics and controls, and submit data subject rights requests for access, deletion, correction, or processing restriction.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `ketch_get_consent`
|
||||
|
||||
Retrieve consent status for a data subject. Returns the current consent preferences for each configured purpose.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `organizationCode` | string | Yes | Ketch organization code |
|
||||
| `propertyCode` | string | Yes | Digital property code defined in Ketch |
|
||||
| `environmentCode` | string | Yes | Environment code defined in Ketch \(e.g., "production"\) |
|
||||
| `jurisdictionCode` | string | No | Jurisdiction code \(e.g., "gdpr", "ccpa"\) |
|
||||
| `identities` | json | Yes | Identity map \(e.g., \{"email": "user@example.com"\}\) |
|
||||
| `purposes` | json | No | Optional purposes to filter the consent query |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `purposes` | object | Map of purpose codes to consent status and legal basis |
|
||||
| ↳ `allowed` | string | Consent status for the purpose: "granted" or "denied" |
|
||||
| ↳ `legalBasisCode` | string | Legal basis code \(e.g., "consent_optin", "consent_optout", "disclosure", "other"\) |
|
||||
| `vendors` | object | Map of vendor consent statuses |
|
||||
|
||||
### `ketch_set_consent`
|
||||
|
||||
Update consent preferences for a data subject. Sets the consent status for specified purposes with the appropriate legal basis.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `organizationCode` | string | Yes | Ketch organization code |
|
||||
| `propertyCode` | string | Yes | Digital property code defined in Ketch |
|
||||
| `environmentCode` | string | Yes | Environment code defined in Ketch \(e.g., "production"\) |
|
||||
| `jurisdictionCode` | string | No | Jurisdiction code \(e.g., "gdpr", "ccpa"\) |
|
||||
| `identities` | json | Yes | Identity map \(e.g., \{"email": "user@example.com"\}\) |
|
||||
| `purposes` | json | Yes | Map of purpose codes to consent settings \(e.g., \{"analytics": \{"allowed": "granted", "legalBasisCode": "consent_optin"\}\}\) |
|
||||
| `collectedAt` | number | No | UNIX timestamp when consent was collected \(defaults to current time\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `purposes` | object | Updated consent status map of purpose codes to consent settings |
|
||||
| ↳ `allowed` | string | Consent status for the purpose: "granted" or "denied" |
|
||||
| ↳ `legalBasisCode` | string | Legal basis code \(e.g., "consent_optin", "consent_optout", "disclosure", "other"\) |
|
||||
|
||||
### `ketch_get_subscriptions`
|
||||
|
||||
Retrieve subscription preferences for a data subject. Returns the current subscription topic and control statuses.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `organizationCode` | string | Yes | Ketch organization code |
|
||||
| `propertyCode` | string | Yes | Digital property code defined in Ketch |
|
||||
| `environmentCode` | string | Yes | Environment code defined in Ketch \(e.g., "production"\) |
|
||||
| `identities` | json | Yes | Identity map \(e.g., \{"email": "user@example.com"\}\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `topics` | object | Map of topic codes to contact method settings \(e.g., \{"newsletter": \{"email": \{"status": "granted"\}\}\}\) |
|
||||
| `controls` | object | Map of control codes to settings \(e.g., \{"global_unsubscribe": \{"status": "denied"\}\}\) |
|
||||
|
||||
### `ketch_set_subscriptions`
|
||||
|
||||
Update subscription preferences for a data subject. Sets topic and control statuses for email, SMS, and other contact methods.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `organizationCode` | string | Yes | Ketch organization code |
|
||||
| `propertyCode` | string | Yes | Digital property code defined in Ketch |
|
||||
| `environmentCode` | string | Yes | Environment code defined in Ketch \(e.g., "production"\) |
|
||||
| `identities` | json | Yes | Identity map \(e.g., \{"email": "user@example.com"\}\) |
|
||||
| `topics` | json | No | Map of topic codes to contact method settings \(e.g., \{"newsletter": \{"email": \{"status": "granted"\}, "sms": \{"status": "denied"\}\}\}\) |
|
||||
| `controls` | json | No | Map of control codes to settings \(e.g., \{"global_unsubscribe": \{"status": "denied"\}\}\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether the subscription preferences were updated |
|
||||
|
||||
### `ketch_invoke_right`
|
||||
|
||||
Submit a data subject rights request (e.g., access, delete, correct, restrict processing). Initiates a privacy rights workflow in Ketch.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `organizationCode` | string | Yes | Ketch organization code |
|
||||
| `propertyCode` | string | Yes | Digital property code defined in Ketch |
|
||||
| `environmentCode` | string | Yes | Environment code defined in Ketch \(e.g., "production"\) |
|
||||
| `jurisdictionCode` | string | Yes | Jurisdiction code \(e.g., "gdpr", "ccpa"\) |
|
||||
| `rightCode` | string | Yes | Privacy right code to invoke \(e.g., "access", "delete", "correct", "restrict_processing"\) |
|
||||
| `identities` | json | Yes | Identity map \(e.g., \{"email": "user@example.com"\}\) |
|
||||
| `userData` | json | No | Optional data subject information \(e.g., \{"email": "user@example.com", "firstName": "John", "lastName": "Doe"\}\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether the rights request was submitted |
|
||||
| `message` | string | Response message from Ketch |
|
||||
|
||||
|
||||
388
apps/docs/content/docs/en/tools/launchdarkly.mdx
Normal file
388
apps/docs/content/docs/en/tools/launchdarkly.mdx
Normal file
@@ -0,0 +1,388 @@
|
||||
---
|
||||
title: LaunchDarkly
|
||||
description: Manage feature flags with LaunchDarkly.
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="launchdarkly"
|
||||
color="#191919"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[LaunchDarkly](https://launchdarkly.com/) is a feature management platform that enables teams to safely deploy, control, and measure their software features at scale.
|
||||
|
||||
With the LaunchDarkly integration in Sim, you can:
|
||||
|
||||
- **Feature flag management** — List, create, update, toggle, and delete feature flags programmatically. Toggle flags on or off in specific environments using LaunchDarkly's semantic patch API.
|
||||
- **Flag status monitoring** — Check whether a flag is active, inactive, new, or launched in a given environment. Track the last time a flag was evaluated.
|
||||
- **Project and environment management** — List all projects and their environments to understand your LaunchDarkly organization structure.
|
||||
- **User segments** — List user segments within a project and environment to understand how your audience is organized for targeting.
|
||||
- **Team visibility** — List account members and their roles for auditing and access management workflows.
|
||||
- **Audit log** — Retrieve recent audit log entries to track who changed what, when. Filter entries by resource type for targeted monitoring.
|
||||
|
||||
In Sim, the LaunchDarkly integration enables your agents to automate feature flag operations as part of their workflows. This allows for automation scenarios such as toggling flags on/off based on deployment pipeline events, monitoring flag status and alerting on stale or unused flags, auditing flag changes by querying the audit log after deployments, syncing flag metadata with your project management tools, and listing all feature flags across projects for governance.
|
||||
|
||||
## Authentication
|
||||
|
||||
This integration uses a LaunchDarkly API key. You can create personal access tokens or service tokens in the LaunchDarkly dashboard under **Account Settings > Authorization**. The API key is passed directly in the `Authorization` header (no `Bearer` prefix).
|
||||
|
||||
## Need Help?
|
||||
|
||||
If you encounter issues with the LaunchDarkly integration, contact us at [help@sim.ai](mailto:help@sim.ai)
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate LaunchDarkly into your workflow. List, create, update, toggle, and delete feature flags. Manage projects, environments, segments, members, and audit logs. Requires API Key.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `launchdarkly_create_flag`
|
||||
|
||||
Create a new feature flag in a LaunchDarkly project.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | LaunchDarkly API key |
|
||||
| `projectKey` | string | Yes | The project key to create the flag in |
|
||||
| `name` | string | Yes | Human-readable name for the feature flag |
|
||||
| `key` | string | Yes | Unique key for the feature flag \(used in code\) |
|
||||
| `description` | string | No | Description of the feature flag |
|
||||
| `tags` | string | No | Comma-separated list of tags |
|
||||
| `temporary` | boolean | No | Whether the flag is temporary \(default true\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `key` | string | The unique key of the feature flag |
|
||||
| `name` | string | The human-readable name of the feature flag |
|
||||
| `kind` | string | The type of flag \(boolean or multivariate\) |
|
||||
| `description` | string | Description of the feature flag |
|
||||
| `temporary` | boolean | Whether the flag is temporary |
|
||||
| `archived` | boolean | Whether the flag is archived |
|
||||
| `deprecated` | boolean | Whether the flag is deprecated |
|
||||
| `creationDate` | number | Unix timestamp in milliseconds when the flag was created |
|
||||
| `tags` | array | Tags applied to the flag |
|
||||
| `variations` | array | The variations for this feature flag |
|
||||
| ↳ `value` | string | The variation value |
|
||||
| ↳ `name` | string | The variation name |
|
||||
| ↳ `description` | string | The variation description |
|
||||
| `maintainerId` | string | The ID of the member who maintains this flag |
|
||||
|
||||
### `launchdarkly_delete_flag`
|
||||
|
||||
Delete a feature flag from a LaunchDarkly project.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | LaunchDarkly API key |
|
||||
| `projectKey` | string | Yes | The project key |
|
||||
| `flagKey` | string | Yes | The feature flag key to delete |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `deleted` | boolean | Whether the flag was successfully deleted |
|
||||
|
||||
### `launchdarkly_get_audit_log`
|
||||
|
||||
List audit log entries from your LaunchDarkly account.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | LaunchDarkly API key |
|
||||
| `limit` | number | No | Maximum number of entries to return \(default 10, max 20\) |
|
||||
| `spec` | string | No | Filter expression \(e.g., "resourceType:flag"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `entries` | array | List of audit log entries |
|
||||
| ↳ `id` | string | The audit log entry ID |
|
||||
| ↳ `date` | number | Unix timestamp in milliseconds |
|
||||
| ↳ `kind` | string | The type of action performed |
|
||||
| ↳ `name` | string | The name of the resource acted on |
|
||||
| ↳ `description` | string | Full description of the action |
|
||||
| ↳ `shortDescription` | string | Short description of the action |
|
||||
| ↳ `memberEmail` | string | Email of the member who performed the action |
|
||||
| ↳ `targetName` | string | Name of the target resource |
|
||||
| ↳ `targetKind` | string | Kind of the target resource |
|
||||
| `totalCount` | number | Total number of audit log entries |
|
||||
|
||||
### `launchdarkly_get_flag`
|
||||
|
||||
Get a single feature flag by key from a LaunchDarkly project.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | LaunchDarkly API key |
|
||||
| `projectKey` | string | Yes | The project key |
|
||||
| `flagKey` | string | Yes | The feature flag key |
|
||||
| `environmentKey` | string | No | Filter flag configuration to a specific environment |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `key` | string | The unique key of the feature flag |
|
||||
| `name` | string | The human-readable name of the feature flag |
|
||||
| `kind` | string | The type of flag \(boolean or multivariate\) |
|
||||
| `description` | string | Description of the feature flag |
|
||||
| `temporary` | boolean | Whether the flag is temporary |
|
||||
| `archived` | boolean | Whether the flag is archived |
|
||||
| `deprecated` | boolean | Whether the flag is deprecated |
|
||||
| `creationDate` | number | Unix timestamp in milliseconds when the flag was created |
|
||||
| `tags` | array | Tags applied to the flag |
|
||||
| `variations` | array | The variations for this feature flag |
|
||||
| ↳ `value` | string | The variation value |
|
||||
| ↳ `name` | string | The variation name |
|
||||
| ↳ `description` | string | The variation description |
|
||||
| `maintainerId` | string | The ID of the member who maintains this flag |
|
||||
| `on` | boolean | Whether the flag is on in the requested environment \(null if no single environment was specified\) |
|
||||
|
||||
### `launchdarkly_get_flag_status`
|
||||
|
||||
Get the status of a feature flag across environments (active, inactive, launched, etc.).
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | LaunchDarkly API key |
|
||||
| `projectKey` | string | Yes | The project key |
|
||||
| `flagKey` | string | Yes | The feature flag key |
|
||||
| `environmentKey` | string | Yes | The environment key |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `name` | string | The flag status \(new, active, inactive, launched\) |
|
||||
| `lastRequested` | string | Timestamp of the last evaluation |
|
||||
| `defaultVal` | string | The default variation value |
|
||||
|
||||
### `launchdarkly_list_environments`
|
||||
|
||||
List environments in a LaunchDarkly project.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | LaunchDarkly API key |
|
||||
| `projectKey` | string | Yes | The project key to list environments for |
|
||||
| `limit` | number | No | Maximum number of environments to return \(default 20\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `environments` | array | List of environments |
|
||||
| ↳ `id` | string | The environment ID |
|
||||
| ↳ `key` | string | The unique environment key |
|
||||
| ↳ `name` | string | The environment name |
|
||||
| ↳ `color` | string | The color assigned to this environment |
|
||||
| ↳ `apiKey` | string | The server-side SDK key for this environment |
|
||||
| ↳ `mobileKey` | string | The mobile SDK key for this environment |
|
||||
| ↳ `tags` | array | Tags applied to the environment |
|
||||
| `totalCount` | number | Total number of environments |
|
||||
|
||||
### `launchdarkly_list_flags`
|
||||
|
||||
List feature flags in a LaunchDarkly project.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | LaunchDarkly API key |
|
||||
| `projectKey` | string | Yes | The project key to list flags for |
|
||||
| `environmentKey` | string | No | Filter flag configurations to a specific environment |
|
||||
| `tag` | string | No | Filter flags by tag name |
|
||||
| `limit` | number | No | Maximum number of flags to return \(default 20\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `flags` | array | List of feature flags |
|
||||
| ↳ `key` | string | The unique key of the feature flag |
|
||||
| ↳ `name` | string | The human-readable name of the feature flag |
|
||||
| ↳ `kind` | string | The type of flag \(boolean or multivariate\) |
|
||||
| ↳ `description` | string | Description of the feature flag |
|
||||
| ↳ `temporary` | boolean | Whether the flag is temporary |
|
||||
| ↳ `archived` | boolean | Whether the flag is archived |
|
||||
| ↳ `deprecated` | boolean | Whether the flag is deprecated |
|
||||
| ↳ `creationDate` | number | Unix timestamp in milliseconds when the flag was created |
|
||||
| ↳ `tags` | array | Tags applied to the flag |
|
||||
| ↳ `variations` | array | The variations for this feature flag |
|
||||
| ↳ `value` | string | The variation value |
|
||||
| ↳ `name` | string | The variation name |
|
||||
| ↳ `description` | string | The variation description |
|
||||
| ↳ `maintainerId` | string | The ID of the member who maintains this flag |
|
||||
| `totalCount` | number | Total number of flags |
|
||||
|
||||
### `launchdarkly_list_members`
|
||||
|
||||
List account members in your LaunchDarkly organization.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | LaunchDarkly API key |
|
||||
| `limit` | number | No | Maximum number of members to return \(default 20\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `members` | array | List of account members |
|
||||
| ↳ `id` | string | The member ID |
|
||||
| ↳ `email` | string | The member email address |
|
||||
| ↳ `firstName` | string | The member first name |
|
||||
| ↳ `lastName` | string | The member last name |
|
||||
| ↳ `role` | string | The member role \(reader, writer, admin, owner\) |
|
||||
| ↳ `lastSeen` | number | Unix timestamp of last activity |
|
||||
| ↳ `creationDate` | number | Unix timestamp when the member was created |
|
||||
| ↳ `verified` | boolean | Whether the member email is verified |
|
||||
| `totalCount` | number | Total number of members |
|
||||
|
||||
### `launchdarkly_list_projects`
|
||||
|
||||
List all projects in your LaunchDarkly account.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | LaunchDarkly API key |
|
||||
| `limit` | number | No | Maximum number of projects to return \(default 20\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `projects` | array | List of projects |
|
||||
| ↳ `id` | string | The project ID |
|
||||
| ↳ `key` | string | The unique project key |
|
||||
| ↳ `name` | string | The project name |
|
||||
| ↳ `tags` | array | Tags applied to the project |
|
||||
| `totalCount` | number | Total number of projects |
|
||||
|
||||
### `launchdarkly_list_segments`
|
||||
|
||||
List user segments in a LaunchDarkly project and environment.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | LaunchDarkly API key |
|
||||
| `projectKey` | string | Yes | The project key |
|
||||
| `environmentKey` | string | Yes | The environment key |
|
||||
| `limit` | number | No | Maximum number of segments to return \(default 20\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `segments` | array | List of user segments |
|
||||
| ↳ `key` | string | The unique segment key |
|
||||
| ↳ `name` | string | The segment name |
|
||||
| ↳ `description` | string | The segment description |
|
||||
| ↳ `tags` | array | Tags applied to the segment |
|
||||
| ↳ `creationDate` | number | Unix timestamp in milliseconds when the segment was created |
|
||||
| ↳ `unbounded` | boolean | Whether this is an unbounded \(big\) segment |
|
||||
| ↳ `included` | array | User keys explicitly included in the segment |
|
||||
| ↳ `excluded` | array | User keys explicitly excluded from the segment |
|
||||
| `totalCount` | number | Total number of segments |
|
||||
|
||||
### `launchdarkly_toggle_flag`
|
||||
|
||||
Toggle a feature flag on or off in a specific LaunchDarkly environment.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | LaunchDarkly API key |
|
||||
| `projectKey` | string | Yes | The project key |
|
||||
| `flagKey` | string | Yes | The feature flag key to toggle |
|
||||
| `environmentKey` | string | Yes | The environment key to toggle the flag in |
|
||||
| `enabled` | boolean | Yes | Whether to turn the flag on \(true\) or off \(false\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `key` | string | The unique key of the feature flag |
|
||||
| `name` | string | The human-readable name of the feature flag |
|
||||
| `kind` | string | The type of flag \(boolean or multivariate\) |
|
||||
| `description` | string | Description of the feature flag |
|
||||
| `temporary` | boolean | Whether the flag is temporary |
|
||||
| `archived` | boolean | Whether the flag is archived |
|
||||
| `deprecated` | boolean | Whether the flag is deprecated |
|
||||
| `creationDate` | number | Unix timestamp in milliseconds when the flag was created |
|
||||
| `tags` | array | Tags applied to the flag |
|
||||
| `variations` | array | The variations for this feature flag |
|
||||
| ↳ `value` | string | The variation value |
|
||||
| ↳ `name` | string | The variation name |
|
||||
| ↳ `description` | string | The variation description |
|
||||
| `maintainerId` | string | The ID of the member who maintains this flag |
|
||||
| `on` | boolean | Whether the flag is now on in the target environment |
|
||||
|
||||
### `launchdarkly_update_flag`
|
||||
|
||||
Update a feature flag metadata (name, description, tags, temporary, archived) using semantic patch.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | LaunchDarkly API key |
|
||||
| `projectKey` | string | Yes | The project key |
|
||||
| `flagKey` | string | Yes | The feature flag key to update |
|
||||
| `updateName` | string | No | New name for the flag |
|
||||
| `updateDescription` | string | No | New description for the flag |
|
||||
| `addTags` | string | No | Comma-separated tags to add |
|
||||
| `removeTags` | string | No | Comma-separated tags to remove |
|
||||
| `archive` | boolean | No | Set to true to archive, false to restore |
|
||||
| `comment` | string | No | Optional comment explaining the update |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `key` | string | The unique key of the feature flag |
|
||||
| `name` | string | The human-readable name of the feature flag |
|
||||
| `kind` | string | The type of flag \(boolean or multivariate\) |
|
||||
| `description` | string | Description of the feature flag |
|
||||
| `temporary` | boolean | Whether the flag is temporary |
|
||||
| `archived` | boolean | Whether the flag is archived |
|
||||
| `deprecated` | boolean | Whether the flag is deprecated |
|
||||
| `creationDate` | number | Unix timestamp in milliseconds when the flag was created |
|
||||
| `tags` | array | Tags applied to the flag |
|
||||
| `variations` | array | The variations for this feature flag |
|
||||
| ↳ `value` | string | The variation value |
|
||||
| ↳ `name` | string | The variation name |
|
||||
| ↳ `description` | string | The variation description |
|
||||
| `maintainerId` | string | The ID of the member who maintains this flag |
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"pages": [
|
||||
"index",
|
||||
"a2a",
|
||||
"agentmail",
|
||||
"ahrefs",
|
||||
"airtable",
|
||||
"airweave",
|
||||
@@ -39,6 +40,7 @@
|
||||
"enrich",
|
||||
"evernote",
|
||||
"exa",
|
||||
"extend",
|
||||
"fathom",
|
||||
"file",
|
||||
"firecrawl",
|
||||
@@ -68,6 +70,7 @@
|
||||
"google_vault",
|
||||
"grafana",
|
||||
"grain",
|
||||
"granola",
|
||||
"greenhouse",
|
||||
"greptile",
|
||||
"hex",
|
||||
@@ -79,13 +82,14 @@
|
||||
"incidentio",
|
||||
"infisical",
|
||||
"intercom",
|
||||
"ironclad",
|
||||
"jina",
|
||||
"jira",
|
||||
"jira_service_management",
|
||||
"kalshi",
|
||||
"ketch",
|
||||
"knowledge",
|
||||
"langsmith",
|
||||
"launchdarkly",
|
||||
"lemlist",
|
||||
"linear",
|
||||
"linkedin",
|
||||
@@ -120,6 +124,7 @@
|
||||
"polymarket",
|
||||
"postgresql",
|
||||
"posthog",
|
||||
"profound",
|
||||
"pulse",
|
||||
"qdrant",
|
||||
"quiver",
|
||||
@@ -130,9 +135,11 @@
|
||||
"resend",
|
||||
"revenuecat",
|
||||
"rippling",
|
||||
"rootly",
|
||||
"s3",
|
||||
"salesforce",
|
||||
"search",
|
||||
"secrets_manager",
|
||||
"sendgrid",
|
||||
"sentry",
|
||||
"serper",
|
||||
@@ -150,6 +157,7 @@
|
||||
"stt",
|
||||
"supabase",
|
||||
"table",
|
||||
"tailscale",
|
||||
"tavily",
|
||||
"telegram",
|
||||
"textract",
|
||||
|
||||
626
apps/docs/content/docs/en/tools/profound.mdx
Normal file
626
apps/docs/content/docs/en/tools/profound.mdx
Normal file
@@ -0,0 +1,626 @@
|
||||
---
|
||||
title: Profound
|
||||
description: AI visibility and analytics with Profound
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="profound"
|
||||
color="#000000"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Profound](https://tryprofound.com/) is an AI visibility and analytics platform that helps brands understand how they appear across AI-powered search engines, chatbots, and assistants. It tracks mentions, citations, sentiment, bot traffic, and referral patterns across platforms like ChatGPT, Perplexity, Google AI Overviews, and more.
|
||||
|
||||
With the Profound integration in Sim, you can:
|
||||
|
||||
- **Monitor AI Visibility**: Track share of voice, visibility scores, and mention counts across AI platforms for your brand and competitors.
|
||||
- **Analyze Sentiment**: Measure how positively or negatively your brand is discussed in AI-generated responses.
|
||||
- **Track Citations**: See which URLs are being cited by AI models and your citation share relative to competitors.
|
||||
- **Monitor Bot Traffic**: Analyze AI crawler activity on your domain, including GPTBot, ClaudeBot, and other AI agents, with hourly granularity.
|
||||
- **Track Referral Traffic**: Monitor human visits arriving from AI platforms to your website.
|
||||
- **Explore Prompt Data**: Access raw prompt-answer pairs, query fanouts, and prompt volume trends across AI platforms.
|
||||
- **Optimize Content**: Get AEO (Answer Engine Optimization) scores and actionable recommendations to improve how AI models reference your content.
|
||||
- **Manage Categories & Assets**: List and explore your tracked categories, assets (brands), topics, tags, personas, and regions.
|
||||
|
||||
These tools let your agents automate AI visibility monitoring, competitive intelligence, and content optimization workflows. To use the Profound integration, you'll need a Profound account with API access.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Track how your brand appears across AI platforms. Monitor visibility scores, sentiment, citations, bot traffic, referrals, content optimization, and prompt volumes with Profound.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `profound_list_categories`
|
||||
|
||||
List all organization categories in Profound
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Profound API Key |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `categories` | json | List of organization categories |
|
||||
| ↳ `id` | string | Category ID |
|
||||
| ↳ `name` | string | Category name |
|
||||
|
||||
### `profound_list_regions`
|
||||
|
||||
List all organization regions in Profound
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Profound API Key |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `regions` | json | List of organization regions |
|
||||
| ↳ `id` | string | Region ID \(UUID\) |
|
||||
| ↳ `name` | string | Region name |
|
||||
|
||||
### `profound_list_models`
|
||||
|
||||
List all AI models/platforms tracked in Profound
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Profound API Key |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `models` | json | List of AI models/platforms |
|
||||
| ↳ `id` | string | Model ID \(UUID\) |
|
||||
| ↳ `name` | string | Model/platform name |
|
||||
|
||||
### `profound_list_domains`
|
||||
|
||||
List all organization domains in Profound
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Profound API Key |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `domains` | json | List of organization domains |
|
||||
| ↳ `id` | string | Domain ID \(UUID\) |
|
||||
| ↳ `name` | string | Domain name |
|
||||
| ↳ `createdAt` | string | When the domain was added |
|
||||
|
||||
### `profound_list_assets`
|
||||
|
||||
List all organization assets (companies/brands) across all categories in Profound
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Profound API Key |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `assets` | json | List of organization assets with category info |
|
||||
| ↳ `id` | string | Asset ID |
|
||||
| ↳ `name` | string | Asset/company name |
|
||||
| ↳ `website` | string | Asset website URL |
|
||||
| ↳ `alternateDomains` | json | Alternate domain names |
|
||||
| ↳ `isOwned` | boolean | Whether this asset is owned by the organization |
|
||||
| ↳ `createdAt` | string | When the asset was created |
|
||||
| ↳ `logoUrl` | string | URL of the asset logo |
|
||||
| ↳ `categoryId` | string | Category ID the asset belongs to |
|
||||
| ↳ `categoryName` | string | Category name |
|
||||
|
||||
### `profound_list_personas`
|
||||
|
||||
List all organization personas across all categories in Profound
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Profound API Key |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `personas` | json | List of organization personas with profile details |
|
||||
| ↳ `id` | string | Persona ID |
|
||||
| ↳ `name` | string | Persona name |
|
||||
| ↳ `categoryId` | string | Category ID |
|
||||
| ↳ `categoryName` | string | Category name |
|
||||
| ↳ `persona` | json | Persona profile with behavior, employment, and demographics |
|
||||
|
||||
### `profound_category_topics`
|
||||
|
||||
List topics for a specific category in Profound
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Profound API Key |
|
||||
| `categoryId` | string | Yes | Category ID \(UUID\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `topics` | json | List of topics in the category |
|
||||
| ↳ `id` | string | Topic ID \(UUID\) |
|
||||
| ↳ `name` | string | Topic name |
|
||||
|
||||
### `profound_category_tags`
|
||||
|
||||
List tags for a specific category in Profound
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Profound API Key |
|
||||
| `categoryId` | string | Yes | Category ID \(UUID\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `tags` | json | List of tags in the category |
|
||||
| ↳ `id` | string | Tag ID \(UUID\) |
|
||||
| ↳ `name` | string | Tag name |
|
||||
|
||||
### `profound_category_prompts`
|
||||
|
||||
List prompts for a specific category in Profound
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Profound API Key |
|
||||
| `categoryId` | string | Yes | Category ID \(UUID\) |
|
||||
| `limit` | number | No | Maximum number of results \(default 10000, max 10000\) |
|
||||
| `cursor` | string | No | Pagination cursor from previous response |
|
||||
| `orderDir` | string | No | Sort direction: asc or desc \(default desc\) |
|
||||
| `promptType` | string | No | Comma-separated prompt types to filter: visibility, sentiment |
|
||||
| `topicId` | string | No | Comma-separated topic IDs \(UUIDs\) to filter by |
|
||||
| `tagId` | string | No | Comma-separated tag IDs \(UUIDs\) to filter by |
|
||||
| `regionId` | string | No | Comma-separated region IDs \(UUIDs\) to filter by |
|
||||
| `platformId` | string | No | Comma-separated platform IDs \(UUIDs\) to filter by |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `totalRows` | number | Total number of prompts |
|
||||
| `nextCursor` | string | Cursor for next page of results |
|
||||
| `prompts` | json | List of prompts |
|
||||
| ↳ `id` | string | Prompt ID |
|
||||
| ↳ `prompt` | string | Prompt text |
|
||||
| ↳ `promptType` | string | Prompt type \(visibility or sentiment\) |
|
||||
| ↳ `topicId` | string | Topic ID |
|
||||
| ↳ `topicName` | string | Topic name |
|
||||
| ↳ `tags` | json | Associated tags |
|
||||
| ↳ `regions` | json | Associated regions |
|
||||
| ↳ `platforms` | json | Associated platforms |
|
||||
| ↳ `createdAt` | string | When the prompt was created |
|
||||
|
||||
### `profound_category_assets`
|
||||
|
||||
List assets (companies/brands) for a specific category in Profound
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Profound API Key |
|
||||
| `categoryId` | string | Yes | Category ID \(UUID\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `assets` | json | List of assets in the category |
|
||||
| ↳ `id` | string | Asset ID |
|
||||
| ↳ `name` | string | Asset/company name |
|
||||
| ↳ `website` | string | Website URL |
|
||||
| ↳ `alternateDomains` | json | Alternate domain names |
|
||||
| ↳ `isOwned` | boolean | Whether the asset is owned by the organization |
|
||||
| ↳ `createdAt` | string | When the asset was created |
|
||||
| ↳ `logoUrl` | string | URL of the asset logo |
|
||||
|
||||
### `profound_category_personas`
|
||||
|
||||
List personas for a specific category in Profound
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Profound API Key |
|
||||
| `categoryId` | string | Yes | Category ID \(UUID\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `personas` | json | List of personas in the category |
|
||||
| ↳ `id` | string | Persona ID |
|
||||
| ↳ `name` | string | Persona name |
|
||||
| ↳ `persona` | json | Persona profile with behavior, employment, and demographics |
|
||||
|
||||
### `profound_visibility_report`
|
||||
|
||||
Query AI visibility report for a category in Profound
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Profound API Key |
|
||||
| `categoryId` | string | Yes | Category ID \(UUID\) |
|
||||
| `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) |
|
||||
| `endDate` | string | Yes | End date \(YYYY-MM-DD or ISO 8601\) |
|
||||
| `metrics` | string | Yes | Comma-separated metrics: share_of_voice, mentions_count, visibility_score, executions, average_position |
|
||||
| `dimensions` | string | No | Comma-separated dimensions: date, region, topic, model, asset_name, prompt, tag, persona |
|
||||
| `dateInterval` | string | No | Date interval: hour, day, week, month, year |
|
||||
| `filters` | string | No | JSON array of filter objects, e.g. \[\{"field":"asset_name","operator":"is","value":"Company"\}\] |
|
||||
| `limit` | number | No | Maximum number of results \(default 10000, max 50000\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `totalRows` | number | Total number of rows in the report |
|
||||
| `data` | json | Report data rows with metrics and dimension values |
|
||||
| ↳ `metrics` | json | Array of metric values matching requested metrics order |
|
||||
| ↳ `dimensions` | json | Array of dimension values matching requested dimensions order |
|
||||
|
||||
### `profound_sentiment_report`
|
||||
|
||||
Query sentiment report for a category in Profound
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Profound API Key |
|
||||
| `categoryId` | string | Yes | Category ID \(UUID\) |
|
||||
| `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) |
|
||||
| `endDate` | string | Yes | End date \(YYYY-MM-DD or ISO 8601\) |
|
||||
| `metrics` | string | Yes | Comma-separated metrics: positive, negative, occurrences |
|
||||
| `dimensions` | string | No | Comma-separated dimensions: theme, date, region, topic, model, asset_name, tag, prompt, sentiment_type, persona |
|
||||
| `dateInterval` | string | No | Date interval: hour, day, week, month, year |
|
||||
| `filters` | string | No | JSON array of filter objects, e.g. \[\{"field":"asset_name","operator":"is","value":"Company"\}\] |
|
||||
| `limit` | number | No | Maximum number of results \(default 10000, max 50000\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `totalRows` | number | Total number of rows in the report |
|
||||
| `data` | json | Report data rows with metrics and dimension values |
|
||||
| ↳ `metrics` | json | Array of metric values matching requested metrics order |
|
||||
| ↳ `dimensions` | json | Array of dimension values matching requested dimensions order |
|
||||
|
||||
### `profound_citations_report`
|
||||
|
||||
Query citations report for a category in Profound
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Profound API Key |
|
||||
| `categoryId` | string | Yes | Category ID \(UUID\) |
|
||||
| `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) |
|
||||
| `endDate` | string | Yes | End date \(YYYY-MM-DD or ISO 8601\) |
|
||||
| `metrics` | string | Yes | Comma-separated metrics: count, citation_share |
|
||||
| `dimensions` | string | No | Comma-separated dimensions: hostname, path, date, region, topic, model, tag, prompt, url, root_domain, persona, citation_category |
|
||||
| `dateInterval` | string | No | Date interval: hour, day, week, month, year |
|
||||
| `filters` | string | No | JSON array of filter objects, e.g. \[\{"field":"hostname","operator":"is","value":"example.com"\}\] |
|
||||
| `limit` | number | No | Maximum number of results \(default 10000, max 50000\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `totalRows` | number | Total number of rows in the report |
|
||||
| `data` | json | Report data rows with metrics and dimension values |
|
||||
| ↳ `metrics` | json | Array of metric values matching requested metrics order |
|
||||
| ↳ `dimensions` | json | Array of dimension values matching requested dimensions order |
|
||||
|
||||
### `profound_query_fanouts`
|
||||
|
||||
Query fanout report showing how AI models expand prompts into sub-queries in Profound
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Profound API Key |
|
||||
| `categoryId` | string | Yes | Category ID \(UUID\) |
|
||||
| `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) |
|
||||
| `endDate` | string | Yes | End date \(YYYY-MM-DD or ISO 8601\) |
|
||||
| `metrics` | string | Yes | Comma-separated metrics: fanouts_per_execution, total_fanouts, share |
|
||||
| `dimensions` | string | No | Comma-separated dimensions: prompt, query, model, region, date |
|
||||
| `dateInterval` | string | No | Date interval: hour, day, week, month, year |
|
||||
| `filters` | string | No | JSON array of filter objects |
|
||||
| `limit` | number | No | Maximum number of results \(default 10000, max 50000\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `totalRows` | number | Total number of rows in the report |
|
||||
| `data` | json | Report data rows with metrics and dimension values |
|
||||
| ↳ `metrics` | json | Array of metric values matching requested metrics order |
|
||||
| ↳ `dimensions` | json | Array of dimension values matching requested dimensions order |
|
||||
|
||||
### `profound_prompt_answers`
|
||||
|
||||
Get raw prompt answers data for a category in Profound
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Profound API Key |
|
||||
| `categoryId` | string | Yes | Category ID \(UUID\) |
|
||||
| `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) |
|
||||
| `endDate` | string | Yes | End date \(YYYY-MM-DD or ISO 8601\) |
|
||||
| `filters` | string | No | JSON array of filter objects, e.g. \[\{"field":"prompt_type","operator":"is","value":"visibility"\}\] |
|
||||
| `limit` | number | No | Maximum number of results \(default 10000, max 50000\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `totalRows` | number | Total number of answer rows |
|
||||
| `data` | json | Raw prompt answer data |
|
||||
| ↳ `prompt` | string | The prompt text |
|
||||
| ↳ `promptType` | string | Prompt type \(visibility or sentiment\) |
|
||||
| ↳ `response` | string | AI model response text |
|
||||
| ↳ `mentions` | json | Companies/assets mentioned in the response |
|
||||
| ↳ `citations` | json | URLs cited in the response |
|
||||
| ↳ `topic` | string | Topic name |
|
||||
| ↳ `region` | string | Region name |
|
||||
| ↳ `model` | string | AI model/platform name |
|
||||
| ↳ `asset` | string | Asset name |
|
||||
| ↳ `createdAt` | string | Timestamp when the answer was collected |
|
||||
|
||||
### `profound_bots_report`
|
||||
|
||||
Query bot traffic report with hourly granularity for a domain in Profound
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Profound API Key |
|
||||
| `domain` | string | Yes | Domain to query bot traffic for \(e.g. example.com\) |
|
||||
| `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) |
|
||||
| `endDate` | string | No | End date \(YYYY-MM-DD or ISO 8601\). Defaults to now |
|
||||
| `metrics` | string | Yes | Comma-separated metrics: count, citations, indexing, training, last_visit |
|
||||
| `dimensions` | string | No | Comma-separated dimensions: date, hour, path, bot_name, bot_provider, bot_type |
|
||||
| `dateInterval` | string | No | Date interval: hour, day, week, month, year |
|
||||
| `filters` | string | No | JSON array of filter objects, e.g. \[\{"field":"bot_name","operator":"is","value":"GPTBot"\}\] |
|
||||
| `limit` | number | No | Maximum number of results \(default 10000, max 50000\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `totalRows` | number | Total number of rows in the report |
|
||||
| `data` | json | Report data rows with metrics and dimension values |
|
||||
| ↳ `metrics` | json | Array of metric values matching requested metrics order |
|
||||
| ↳ `dimensions` | json | Array of dimension values matching requested dimensions order |
|
||||
|
||||
### `profound_referrals_report`
|
||||
|
||||
Query human referral traffic report with hourly granularity for a domain in Profound
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Profound API Key |
|
||||
| `domain` | string | Yes | Domain to query referral traffic for \(e.g. example.com\) |
|
||||
| `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) |
|
||||
| `endDate` | string | No | End date \(YYYY-MM-DD or ISO 8601\). Defaults to now |
|
||||
| `metrics` | string | Yes | Comma-separated metrics: visits, last_visit |
|
||||
| `dimensions` | string | No | Comma-separated dimensions: date, hour, path, referral_source, referral_type |
|
||||
| `dateInterval` | string | No | Date interval: hour, day, week, month, year |
|
||||
| `filters` | string | No | JSON array of filter objects, e.g. \[\{"field":"referral_source","operator":"is","value":"openai"\}\] |
|
||||
| `limit` | number | No | Maximum number of results \(default 10000, max 50000\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `totalRows` | number | Total number of rows in the report |
|
||||
| `data` | json | Report data rows with metrics and dimension values |
|
||||
| ↳ `metrics` | json | Array of metric values matching requested metrics order |
|
||||
| ↳ `dimensions` | json | Array of dimension values matching requested dimensions order |
|
||||
|
||||
### `profound_raw_logs`
|
||||
|
||||
Get raw traffic logs with filters for a domain in Profound
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Profound API Key |
|
||||
| `domain` | string | Yes | Domain to query logs for \(e.g. example.com\) |
|
||||
| `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) |
|
||||
| `endDate` | string | No | End date \(YYYY-MM-DD or ISO 8601\). Defaults to now |
|
||||
| `dimensions` | string | No | Comma-separated dimensions: timestamp, method, host, path, status_code, ip, user_agent, referer, bytes_sent, duration_ms, query_params |
|
||||
| `filters` | string | No | JSON array of filter objects, e.g. \[\{"field":"path","operator":"contains","value":"/blog"\}\] |
|
||||
| `limit` | number | No | Maximum number of results \(default 10000, max 50000\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `totalRows` | number | Total number of log entries |
|
||||
| `data` | json | Log data rows with metrics and dimension values |
|
||||
| ↳ `metrics` | json | Array of metric values \(count\) |
|
||||
| ↳ `dimensions` | json | Array of dimension values matching requested dimensions order |
|
||||
|
||||
### `profound_bot_logs`
|
||||
|
||||
Get identified bot visit logs with filters for a domain in Profound
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Profound API Key |
|
||||
| `domain` | string | Yes | Domain to query bot logs for \(e.g. example.com\) |
|
||||
| `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) |
|
||||
| `endDate` | string | No | End date \(YYYY-MM-DD or ISO 8601\). Defaults to now |
|
||||
| `dimensions` | string | No | Comma-separated dimensions: timestamp, method, host, path, status_code, ip, user_agent, referer, bytes_sent, duration_ms, query_params, bot_name, bot_provider, bot_types |
|
||||
| `filters` | string | No | JSON array of filter objects, e.g. \[\{"field":"bot_name","operator":"is","value":"GPTBot"\}\] |
|
||||
| `limit` | number | No | Maximum number of results \(default 10000, max 50000\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `totalRows` | number | Total number of bot log entries |
|
||||
| `data` | json | Bot log data rows with metrics and dimension values |
|
||||
| ↳ `metrics` | json | Array of metric values \(count\) |
|
||||
| ↳ `dimensions` | json | Array of dimension values matching requested dimensions order |
|
||||
|
||||
### `profound_list_optimizations`
|
||||
|
||||
List content optimization entries for an asset in Profound
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Profound API Key |
|
||||
| `assetId` | string | Yes | Asset ID \(UUID\) |
|
||||
| `limit` | number | No | Maximum number of results \(default 10000, max 50000\) |
|
||||
| `offset` | number | No | Offset for pagination \(default 0\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `totalRows` | number | Total number of optimization entries |
|
||||
| `optimizations` | json | List of content optimization entries |
|
||||
| ↳ `id` | string | Optimization ID \(UUID\) |
|
||||
| ↳ `title` | string | Content title |
|
||||
| ↳ `createdAt` | string | When the optimization was created |
|
||||
| ↳ `extractedInput` | string | Extracted input text |
|
||||
| ↳ `type` | string | Content type: file, text, or url |
|
||||
| ↳ `status` | string | Optimization status |
|
||||
|
||||
### `profound_optimization_analysis`
|
||||
|
||||
Get detailed content optimization analysis for a specific content item in Profound
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Profound API Key |
|
||||
| `assetId` | string | Yes | Asset ID \(UUID\) |
|
||||
| `contentId` | string | Yes | Content/optimization ID \(UUID\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `content` | json | The analyzed content |
|
||||
| ↳ `format` | string | Content format: markdown or html |
|
||||
| ↳ `value` | string | Content text |
|
||||
| `aeoContentScore` | json | AEO content score with target zone |
|
||||
| ↳ `value` | number | AEO score value |
|
||||
| ↳ `targetZone` | json | Target zone range |
|
||||
| ↳ `low` | number | Low end of target range |
|
||||
| ↳ `high` | number | High end of target range |
|
||||
| `analysis` | json | Analysis breakdown by category |
|
||||
| ↳ `breakdown` | json | Array of scoring breakdowns |
|
||||
| ↳ `title` | string | Category title |
|
||||
| ↳ `weight` | number | Category weight |
|
||||
| ↳ `score` | number | Category score |
|
||||
| `recommendations` | json | Content optimization recommendations |
|
||||
| ↳ `title` | string | Recommendation title |
|
||||
| ↳ `status` | string | Status: done or pending |
|
||||
| ↳ `impact` | json | Impact details with section and score |
|
||||
| ↳ `suggestion` | json | Suggestion text and rationale |
|
||||
| ↳ `text` | string | Suggestion text |
|
||||
| ↳ `rationale` | string | Why this recommendation matters |
|
||||
|
||||
### `profound_prompt_volume`
|
||||
|
||||
Query prompt volume data to understand search demand across AI platforms in Profound
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Profound API Key |
|
||||
| `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) |
|
||||
| `endDate` | string | Yes | End date \(YYYY-MM-DD or ISO 8601\) |
|
||||
| `metrics` | string | Yes | Comma-separated metrics: volume, change |
|
||||
| `dimensions` | string | No | Comma-separated dimensions: keyword, date, platform, country_code, matching_type, frequency |
|
||||
| `dateInterval` | string | No | Date interval: hour, day, week, month, year |
|
||||
| `filters` | string | No | JSON array of filter objects, e.g. \[\{"field":"keyword","operator":"contains","value":"best"\}\] |
|
||||
| `limit` | number | No | Maximum number of results \(default 10000, max 50000\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `totalRows` | number | Total number of rows in the report |
|
||||
| `data` | json | Volume data rows with metrics and dimension values |
|
||||
| ↳ `metrics` | json | Array of metric values matching requested metrics order |
|
||||
| ↳ `dimensions` | json | Array of dimension values matching requested dimensions order |
|
||||
|
||||
### `profound_citation_prompts`
|
||||
|
||||
Get prompts that cite a specific domain across AI platforms in Profound
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Profound API Key |
|
||||
| `inputDomain` | string | Yes | Domain to look up citations for \(e.g. ramp.com\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `data` | json | Citation prompt data for the queried domain |
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
891
apps/docs/content/docs/en/tools/rootly.mdx
Normal file
891
apps/docs/content/docs/en/tools/rootly.mdx
Normal file
@@ -0,0 +1,891 @@
|
||||
---
|
||||
title: Rootly
|
||||
description: Manage incidents, alerts, and on-call with Rootly
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="rootly"
|
||||
color="#6C72C8"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Rootly](https://rootly.com/) is an incident management platform that helps teams respond to, mitigate, and learn from incidents — all without leaving Slack or your existing tools. Rootly automates on-call alerting, incident workflows, status page updates, and retrospectives so engineering teams can resolve issues faster and reduce toil.
|
||||
|
||||
**Why Rootly?**
|
||||
- **End-to-End Incident Management:** Create, track, update, and resolve incidents with full lifecycle support — from initial triage through retrospective.
|
||||
- **On-Call Alerting:** Create and manage alerts with deduplication, routing, and escalation to ensure the right people are notified immediately.
|
||||
- **Timeline Events:** Add structured timeline events to incidents for clear, auditable incident narratives.
|
||||
- **Service Catalog:** Maintain a catalog of services and map them to incidents for precise impact tracking.
|
||||
- **Severity & Prioritization:** Use configurable severity levels to prioritize incidents and drive appropriate response urgency.
|
||||
- **Retrospectives:** Access post-incident retrospectives to identify root causes, capture learnings, and drive reliability improvements.
|
||||
|
||||
**Using Rootly in Sim**
|
||||
|
||||
Sim's Rootly integration connects your agentic workflows directly to your Rootly account using an API key. With operations spanning incidents, alerts, services, severities, teams, environments, functionalities, incident types, and retrospectives, you can build powerful incident management automations without writing backend code.
|
||||
|
||||
**Key benefits of using Rootly in Sim:**
|
||||
- **Automated incident creation:** Trigger incident creation from monitoring alerts, customer reports, or anomaly detection workflows with full metadata including severity, services, and teams.
|
||||
- **Incident lifecycle automation:** Automatically update incident status, add timeline events, and attach mitigation or resolution messages as your response progresses.
|
||||
- **Alert management:** Create and list alerts with deduplication support to integrate Rootly into your existing monitoring and notification pipelines.
|
||||
- **Organizational awareness:** Query services, severities, teams, environments, functionalities, and incident types to build context-aware incident workflows.
|
||||
- **Retrospective insights:** List and filter retrospectives to feed post-incident learnings into continuous improvement workflows.
|
||||
|
||||
Whether you're automating incident response, building on-call alerting pipelines, or driving post-incident learning, Rootly in Sim gives you direct, secure access to the Rootly API — no middleware required. Simply configure your API key, select the operation you need, and let Sim handle the rest.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Rootly incident management into workflows. Create and manage incidents, alerts, services, severities, and retrospectives.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `rootly_create_incident`
|
||||
|
||||
Create a new incident in Rootly with optional severity, services, and teams.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `title` | string | No | The title of the incident \(auto-generated if not provided\) |
|
||||
| `summary` | string | No | A summary of the incident |
|
||||
| `severityId` | string | No | Severity ID to attach to the incident |
|
||||
| `status` | string | No | Incident status \(in_triage, started, detected, acknowledged, mitigated, resolved, closed, cancelled, scheduled, in_progress, completed\) |
|
||||
| `kind` | string | No | Incident kind \(normal, normal_sub, test, test_sub, example, example_sub, backfilled, scheduled, scheduled_sub\) |
|
||||
| `serviceIds` | string | No | Comma-separated service IDs to attach |
|
||||
| `environmentIds` | string | No | Comma-separated environment IDs to attach |
|
||||
| `groupIds` | string | No | Comma-separated team/group IDs to attach |
|
||||
| `incidentTypeIds` | string | No | Comma-separated incident type IDs to attach |
|
||||
| `functionalityIds` | string | No | Comma-separated functionality IDs to attach |
|
||||
| `labels` | string | No | Labels as JSON object, e.g. \{"platform":"osx","version":"1.29"\} |
|
||||
| `private` | boolean | No | Create as a private incident \(cannot be undone\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `incident` | object | The created incident |
|
||||
| ↳ `id` | string | Unique incident ID |
|
||||
| ↳ `sequentialId` | number | Sequential incident number |
|
||||
| ↳ `title` | string | Incident title |
|
||||
| ↳ `slug` | string | Incident slug |
|
||||
| ↳ `kind` | string | Incident kind |
|
||||
| ↳ `summary` | string | Incident summary |
|
||||
| ↳ `status` | string | Incident status |
|
||||
| ↳ `private` | boolean | Whether the incident is private |
|
||||
| ↳ `url` | string | URL to the incident |
|
||||
| ↳ `shortUrl` | string | Short URL to the incident |
|
||||
| ↳ `severityName` | string | Severity name |
|
||||
| ↳ `severityId` | string | Severity ID |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| ↳ `startedAt` | string | Start date |
|
||||
| ↳ `mitigatedAt` | string | Mitigation date |
|
||||
| ↳ `resolvedAt` | string | Resolution date |
|
||||
| ↳ `closedAt` | string | Closed date |
|
||||
|
||||
### `rootly_get_incident`
|
||||
|
||||
Retrieve a single incident by ID from Rootly.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `incidentId` | string | Yes | The ID of the incident to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `incident` | object | The incident details |
|
||||
| ↳ `id` | string | Unique incident ID |
|
||||
| ↳ `sequentialId` | number | Sequential incident number |
|
||||
| ↳ `title` | string | Incident title |
|
||||
| ↳ `slug` | string | Incident slug |
|
||||
| ↳ `kind` | string | Incident kind |
|
||||
| ↳ `summary` | string | Incident summary |
|
||||
| ↳ `status` | string | Incident status |
|
||||
| ↳ `private` | boolean | Whether the incident is private |
|
||||
| ↳ `url` | string | URL to the incident |
|
||||
| ↳ `shortUrl` | string | Short URL to the incident |
|
||||
| ↳ `severityName` | string | Severity name |
|
||||
| ↳ `severityId` | string | Severity ID |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| ↳ `startedAt` | string | Start date |
|
||||
| ↳ `mitigatedAt` | string | Mitigation date |
|
||||
| ↳ `resolvedAt` | string | Resolution date |
|
||||
| ↳ `closedAt` | string | Closed date |
|
||||
|
||||
### `rootly_update_incident`
|
||||
|
||||
Update an existing incident in Rootly (status, severity, summary, etc.).
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `incidentId` | string | Yes | The ID of the incident to update |
|
||||
| `title` | string | No | Updated incident title |
|
||||
| `summary` | string | No | Updated incident summary |
|
||||
| `severityId` | string | No | Updated severity ID |
|
||||
| `status` | string | No | Updated status \(in_triage, started, detected, acknowledged, mitigated, resolved, closed, cancelled, scheduled, in_progress, completed\) |
|
||||
| `kind` | string | No | Incident kind \(normal, normal_sub, test, test_sub, example, example_sub, backfilled, scheduled, scheduled_sub\) |
|
||||
| `private` | boolean | No | Set incident as private \(cannot be undone\) |
|
||||
| `serviceIds` | string | No | Comma-separated service IDs |
|
||||
| `environmentIds` | string | No | Comma-separated environment IDs |
|
||||
| `groupIds` | string | No | Comma-separated team/group IDs |
|
||||
| `incidentTypeIds` | string | No | Comma-separated incident type IDs to attach |
|
||||
| `functionalityIds` | string | No | Comma-separated functionality IDs to attach |
|
||||
| `labels` | string | No | Labels as JSON object, e.g. \{"platform":"osx","version":"1.29"\} |
|
||||
| `mitigationMessage` | string | No | How was the incident mitigated? |
|
||||
| `resolutionMessage` | string | No | How was the incident resolved? |
|
||||
| `cancellationMessage` | string | No | Why was the incident cancelled? |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `incident` | object | The updated incident |
|
||||
| ↳ `id` | string | Unique incident ID |
|
||||
| ↳ `sequentialId` | number | Sequential incident number |
|
||||
| ↳ `title` | string | Incident title |
|
||||
| ↳ `slug` | string | Incident slug |
|
||||
| ↳ `kind` | string | Incident kind |
|
||||
| ↳ `summary` | string | Incident summary |
|
||||
| ↳ `status` | string | Incident status |
|
||||
| ↳ `private` | boolean | Whether the incident is private |
|
||||
| ↳ `url` | string | URL to the incident |
|
||||
| ↳ `shortUrl` | string | Short URL to the incident |
|
||||
| ↳ `severityName` | string | Severity name |
|
||||
| ↳ `severityId` | string | Severity ID |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| ↳ `startedAt` | string | Start date |
|
||||
| ↳ `mitigatedAt` | string | Mitigation date |
|
||||
| ↳ `resolvedAt` | string | Resolution date |
|
||||
| ↳ `closedAt` | string | Closed date |
|
||||
|
||||
### `rootly_list_incidents`
|
||||
|
||||
List incidents from Rootly with optional filtering by status, severity, and more.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `status` | string | No | Filter by status \(in_triage, started, detected, acknowledged, mitigated, resolved, closed, cancelled, scheduled, in_progress, completed\) |
|
||||
| `severity` | string | No | Filter by severity slug |
|
||||
| `search` | string | No | Search term to filter incidents |
|
||||
| `services` | string | No | Filter by service slugs \(comma-separated\) |
|
||||
| `teams` | string | No | Filter by team slugs \(comma-separated\) |
|
||||
| `environments` | string | No | Filter by environment slugs \(comma-separated\) |
|
||||
| `sort` | string | No | Sort order \(e.g., -created_at, created_at, -started_at\) |
|
||||
| `pageSize` | number | No | Number of items per page \(default: 20\) |
|
||||
| `pageNumber` | number | No | Page number for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `incidents` | array | List of incidents |
|
||||
| ↳ `id` | string | Unique incident ID |
|
||||
| ↳ `sequentialId` | number | Sequential incident number |
|
||||
| ↳ `title` | string | Incident title |
|
||||
| ↳ `slug` | string | Incident slug |
|
||||
| ↳ `kind` | string | Incident kind |
|
||||
| ↳ `summary` | string | Incident summary |
|
||||
| ↳ `status` | string | Incident status |
|
||||
| ↳ `private` | boolean | Whether the incident is private |
|
||||
| ↳ `url` | string | URL to the incident |
|
||||
| ↳ `shortUrl` | string | Short URL to the incident |
|
||||
| ↳ `severityName` | string | Severity name |
|
||||
| ↳ `severityId` | string | Severity ID |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| ↳ `startedAt` | string | Start date |
|
||||
| ↳ `mitigatedAt` | string | Mitigation date |
|
||||
| ↳ `resolvedAt` | string | Resolution date |
|
||||
| ↳ `closedAt` | string | Closed date |
|
||||
| `totalCount` | number | Total number of incidents returned |
|
||||
|
||||
### `rootly_create_alert`
|
||||
|
||||
Create a new alert in Rootly for on-call notification and routing.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `summary` | string | Yes | The summary of the alert |
|
||||
| `description` | string | No | A detailed description of the alert |
|
||||
| `source` | string | Yes | The source of the alert \(e.g., api, manual, datadog, pagerduty\) |
|
||||
| `status` | string | No | Alert status on creation \(open, triggered\) |
|
||||
| `serviceIds` | string | No | Comma-separated service IDs to attach |
|
||||
| `groupIds` | string | No | Comma-separated team/group IDs to attach |
|
||||
| `environmentIds` | string | No | Comma-separated environment IDs to attach |
|
||||
| `externalId` | string | No | External ID for the alert |
|
||||
| `externalUrl` | string | No | External URL for the alert |
|
||||
| `deduplicationKey` | string | No | Alerts sharing the same deduplication key are treated as a single alert |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `alert` | object | The created alert |
|
||||
| ↳ `id` | string | Unique alert ID |
|
||||
| ↳ `shortId` | string | Short alert ID |
|
||||
| ↳ `summary` | string | Alert summary |
|
||||
| ↳ `description` | string | Alert description |
|
||||
| ↳ `source` | string | Alert source |
|
||||
| ↳ `status` | string | Alert status |
|
||||
| ↳ `externalId` | string | External ID |
|
||||
| ↳ `externalUrl` | string | External URL |
|
||||
| ↳ `deduplicationKey` | string | Deduplication key |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| ↳ `startedAt` | string | Start date |
|
||||
| ↳ `endedAt` | string | End date |
|
||||
|
||||
### `rootly_list_alerts`
|
||||
|
||||
List alerts from Rootly with optional filtering by status, source, and services.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `status` | string | No | Filter by status \(open, triggered, acknowledged, resolved\) |
|
||||
| `source` | string | No | Filter by source \(e.g., api, datadog, pagerduty\) |
|
||||
| `services` | string | No | Filter by service slugs \(comma-separated\) |
|
||||
| `environments` | string | No | Filter by environment slugs \(comma-separated\) |
|
||||
| `groups` | string | No | Filter by team/group slugs \(comma-separated\) |
|
||||
| `pageSize` | number | No | Number of items per page \(default: 20\) |
|
||||
| `pageNumber` | number | No | Page number for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `alerts` | array | List of alerts |
|
||||
| ↳ `id` | string | Unique alert ID |
|
||||
| ↳ `shortId` | string | Short alert ID |
|
||||
| ↳ `summary` | string | Alert summary |
|
||||
| ↳ `description` | string | Alert description |
|
||||
| ↳ `source` | string | Alert source |
|
||||
| ↳ `status` | string | Alert status |
|
||||
| ↳ `externalId` | string | External ID |
|
||||
| ↳ `externalUrl` | string | External URL |
|
||||
| ↳ `deduplicationKey` | string | Deduplication key |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| ↳ `startedAt` | string | Start date |
|
||||
| ↳ `endedAt` | string | End date |
|
||||
| `totalCount` | number | Total number of alerts returned |
|
||||
|
||||
### `rootly_add_incident_event`
|
||||
|
||||
Add a timeline event to an existing incident in Rootly.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `incidentId` | string | Yes | The ID of the incident to add the event to |
|
||||
| `event` | string | Yes | The summary/description of the event |
|
||||
| `visibility` | string | No | Event visibility \(internal or external\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `eventId` | string | The ID of the created event |
|
||||
| `event` | string | The event summary |
|
||||
| `visibility` | string | Event visibility \(internal or external\) |
|
||||
| `occurredAt` | string | When the event occurred |
|
||||
| `createdAt` | string | Creation date |
|
||||
| `updatedAt` | string | Last update date |
|
||||
|
||||
### `rootly_list_services`
|
||||
|
||||
List services from Rootly with optional search filtering.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `search` | string | No | Search term to filter services |
|
||||
| `pageSize` | number | No | Number of items per page \(default: 20\) |
|
||||
| `pageNumber` | number | No | Page number for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `services` | array | List of services |
|
||||
| ↳ `id` | string | Unique service ID |
|
||||
| ↳ `name` | string | Service name |
|
||||
| ↳ `slug` | string | Service slug |
|
||||
| ↳ `description` | string | Service description |
|
||||
| ↳ `color` | string | Service color |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| `totalCount` | number | Total number of services returned |
|
||||
|
||||
### `rootly_list_severities`
|
||||
|
||||
List severity levels configured in Rootly.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `search` | string | No | Search term to filter severities |
|
||||
| `pageSize` | number | No | Number of items per page \(default: 20\) |
|
||||
| `pageNumber` | number | No | Page number for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `severities` | array | List of severity levels |
|
||||
| ↳ `id` | string | Unique severity ID |
|
||||
| ↳ `name` | string | Severity name |
|
||||
| ↳ `slug` | string | Severity slug |
|
||||
| ↳ `description` | string | Severity description |
|
||||
| ↳ `severity` | string | Severity level \(critical, high, medium, low\) |
|
||||
| ↳ `color` | string | Severity color |
|
||||
| ↳ `position` | number | Display position |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| `totalCount` | number | Total number of severities returned |
|
||||
|
||||
### `rootly_list_teams`
|
||||
|
||||
List teams (groups) configured in Rootly.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `search` | string | No | Search term to filter teams |
|
||||
| `pageSize` | number | No | Number of items per page \(default: 20\) |
|
||||
| `pageNumber` | number | No | Page number for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `teams` | array | List of teams |
|
||||
| ↳ `id` | string | Unique team ID |
|
||||
| ↳ `name` | string | Team name |
|
||||
| ↳ `slug` | string | Team slug |
|
||||
| ↳ `description` | string | Team description |
|
||||
| ↳ `color` | string | Team color |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| `totalCount` | number | Total number of teams returned |
|
||||
|
||||
### `rootly_list_environments`
|
||||
|
||||
List environments configured in Rootly.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `search` | string | No | Search term to filter environments |
|
||||
| `pageSize` | number | No | Number of items per page \(default: 20\) |
|
||||
| `pageNumber` | number | No | Page number for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `environments` | array | List of environments |
|
||||
| ↳ `id` | string | Unique environment ID |
|
||||
| ↳ `name` | string | Environment name |
|
||||
| ↳ `slug` | string | Environment slug |
|
||||
| ↳ `description` | string | Environment description |
|
||||
| ↳ `color` | string | Environment color |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| `totalCount` | number | Total number of environments returned |
|
||||
|
||||
### `rootly_list_incident_types`
|
||||
|
||||
List incident types configured in Rootly.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `search` | string | No | Filter incident types by name |
|
||||
| `pageSize` | number | No | Number of items per page \(default: 20\) |
|
||||
| `pageNumber` | number | No | Page number for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `incidentTypes` | array | List of incident types |
|
||||
| ↳ `id` | string | Unique incident type ID |
|
||||
| ↳ `name` | string | Incident type name |
|
||||
| ↳ `slug` | string | Incident type slug |
|
||||
| ↳ `description` | string | Incident type description |
|
||||
| ↳ `color` | string | Incident type color |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| `totalCount` | number | Total number of incident types returned |
|
||||
|
||||
### `rootly_list_functionalities`
|
||||
|
||||
List functionalities configured in Rootly.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `search` | string | No | Search term to filter functionalities |
|
||||
| `pageSize` | number | No | Number of items per page \(default: 20\) |
|
||||
| `pageNumber` | number | No | Page number for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `functionalities` | array | List of functionalities |
|
||||
| ↳ `id` | string | Unique functionality ID |
|
||||
| ↳ `name` | string | Functionality name |
|
||||
| ↳ `slug` | string | Functionality slug |
|
||||
| ↳ `description` | string | Functionality description |
|
||||
| ↳ `color` | string | Functionality color |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| `totalCount` | number | Total number of functionalities returned |
|
||||
|
||||
### `rootly_list_retrospectives`
|
||||
|
||||
List incident retrospectives (post-mortems) from Rootly.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `status` | string | No | Filter by status \(draft, published\) |
|
||||
| `search` | string | No | Search term to filter retrospectives |
|
||||
| `pageSize` | number | No | Number of items per page \(default: 20\) |
|
||||
| `pageNumber` | number | No | Page number for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `retrospectives` | array | List of retrospectives |
|
||||
| ↳ `id` | string | Unique retrospective ID |
|
||||
| ↳ `title` | string | Retrospective title |
|
||||
| ↳ `status` | string | Status \(draft or published\) |
|
||||
| ↳ `url` | string | URL to the retrospective |
|
||||
| ↳ `startedAt` | string | Incident start date |
|
||||
| ↳ `mitigatedAt` | string | Mitigation date |
|
||||
| ↳ `resolvedAt` | string | Resolution date |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| `totalCount` | number | Total number of retrospectives returned |
|
||||
|
||||
### `rootly_delete_incident`
|
||||
|
||||
Delete an incident by ID from Rootly.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `incidentId` | string | Yes | The ID of the incident to delete |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether the deletion succeeded |
|
||||
| `message` | string | Result message |
|
||||
|
||||
### `rootly_get_alert`
|
||||
|
||||
Retrieve a single alert by ID from Rootly.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `alertId` | string | Yes | The ID of the alert to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `alert` | object | The alert details |
|
||||
| ↳ `id` | string | Unique alert ID |
|
||||
| ↳ `shortId` | string | Short alert ID |
|
||||
| ↳ `summary` | string | Alert summary |
|
||||
| ↳ `description` | string | Alert description |
|
||||
| ↳ `source` | string | Alert source |
|
||||
| ↳ `status` | string | Alert status |
|
||||
| ↳ `externalId` | string | External ID |
|
||||
| ↳ `externalUrl` | string | External URL |
|
||||
| ↳ `deduplicationKey` | string | Deduplication key |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| ↳ `startedAt` | string | Start date |
|
||||
| ↳ `endedAt` | string | End date |
|
||||
|
||||
### `rootly_update_alert`
|
||||
|
||||
Update an existing alert in Rootly.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `alertId` | string | Yes | The ID of the alert to update |
|
||||
| `summary` | string | No | Updated alert summary |
|
||||
| `description` | string | No | Updated alert description |
|
||||
| `source` | string | No | Updated alert source |
|
||||
| `serviceIds` | string | No | Comma-separated service IDs to attach |
|
||||
| `groupIds` | string | No | Comma-separated team/group IDs to attach |
|
||||
| `environmentIds` | string | No | Comma-separated environment IDs to attach |
|
||||
| `externalId` | string | No | Updated external ID |
|
||||
| `externalUrl` | string | No | Updated external URL |
|
||||
| `deduplicationKey` | string | No | Updated deduplication key |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `alert` | object | The updated alert |
|
||||
| ↳ `id` | string | Unique alert ID |
|
||||
| ↳ `shortId` | string | Short alert ID |
|
||||
| ↳ `summary` | string | Alert summary |
|
||||
| ↳ `description` | string | Alert description |
|
||||
| ↳ `source` | string | Alert source |
|
||||
| ↳ `status` | string | Alert status |
|
||||
| ↳ `externalId` | string | External ID |
|
||||
| ↳ `externalUrl` | string | External URL |
|
||||
| ↳ `deduplicationKey` | string | Deduplication key |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| ↳ `startedAt` | string | Start date |
|
||||
| ↳ `endedAt` | string | End date |
|
||||
|
||||
### `rootly_acknowledge_alert`
|
||||
|
||||
Acknowledge an alert in Rootly.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `alertId` | string | Yes | The ID of the alert to acknowledge |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `alert` | object | The acknowledged alert |
|
||||
| ↳ `id` | string | Unique alert ID |
|
||||
| ↳ `shortId` | string | Short alert ID |
|
||||
| ↳ `summary` | string | Alert summary |
|
||||
| ↳ `description` | string | Alert description |
|
||||
| ↳ `source` | string | Alert source |
|
||||
| ↳ `status` | string | Alert status |
|
||||
| ↳ `externalId` | string | External ID |
|
||||
| ↳ `externalUrl` | string | External URL |
|
||||
| ↳ `deduplicationKey` | string | Deduplication key |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| ↳ `startedAt` | string | Start date |
|
||||
| ↳ `endedAt` | string | End date |
|
||||
|
||||
### `rootly_resolve_alert`
|
||||
|
||||
Resolve an alert in Rootly.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `alertId` | string | Yes | The ID of the alert to resolve |
|
||||
| `resolutionMessage` | string | No | Message describing how the alert was resolved |
|
||||
| `resolveRelatedIncidents` | boolean | No | Whether to also resolve related incidents |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `alert` | object | The resolved alert |
|
||||
| ↳ `id` | string | Unique alert ID |
|
||||
| ↳ `shortId` | string | Short alert ID |
|
||||
| ↳ `summary` | string | Alert summary |
|
||||
| ↳ `description` | string | Alert description |
|
||||
| ↳ `source` | string | Alert source |
|
||||
| ↳ `status` | string | Alert status |
|
||||
| ↳ `externalId` | string | External ID |
|
||||
| ↳ `externalUrl` | string | External URL |
|
||||
| ↳ `deduplicationKey` | string | Deduplication key |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| ↳ `startedAt` | string | Start date |
|
||||
| ↳ `endedAt` | string | End date |
|
||||
|
||||
### `rootly_create_action_item`
|
||||
|
||||
Create a new action item for an incident in Rootly.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `incidentId` | string | Yes | The ID of the incident to add the action item to |
|
||||
| `summary` | string | Yes | The title of the action item |
|
||||
| `description` | string | No | A detailed description of the action item |
|
||||
| `kind` | string | No | The kind of action item \(task, follow_up\) |
|
||||
| `priority` | string | No | Priority level \(high, medium, low\) |
|
||||
| `status` | string | No | Action item status \(open, in_progress, cancelled, done\) |
|
||||
| `assignedToUserId` | string | No | The user ID to assign the action item to |
|
||||
| `dueDate` | string | No | Due date for the action item |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `actionItem` | object | The created action item |
|
||||
| ↳ `id` | string | Unique action item ID |
|
||||
| ↳ `summary` | string | Action item title |
|
||||
| ↳ `description` | string | Action item description |
|
||||
| ↳ `kind` | string | Action item kind \(task, follow_up\) |
|
||||
| ↳ `priority` | string | Priority level |
|
||||
| ↳ `status` | string | Action item status |
|
||||
| ↳ `dueDate` | string | Due date |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
|
||||
### `rootly_list_action_items`
|
||||
|
||||
List action items for an incident in Rootly.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `incidentId` | string | Yes | The ID of the incident to list action items for |
|
||||
| `pageSize` | number | No | Number of items per page \(default: 20\) |
|
||||
| `pageNumber` | number | No | Page number for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `actionItems` | array | List of action items |
|
||||
| ↳ `id` | string | Unique action item ID |
|
||||
| ↳ `summary` | string | Action item title |
|
||||
| ↳ `description` | string | Action item description |
|
||||
| ↳ `kind` | string | Action item kind \(task, follow_up\) |
|
||||
| ↳ `priority` | string | Priority level |
|
||||
| ↳ `status` | string | Action item status |
|
||||
| ↳ `dueDate` | string | Due date |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| `totalCount` | number | Total number of action items returned |
|
||||
|
||||
### `rootly_list_users`
|
||||
|
||||
List users from Rootly with optional search and email filtering.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `search` | string | No | Search term to filter users |
|
||||
| `email` | string | No | Filter users by email address |
|
||||
| `pageSize` | number | No | Number of items per page \(default: 20\) |
|
||||
| `pageNumber` | number | No | Page number for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `users` | array | List of users |
|
||||
| ↳ `id` | string | Unique user ID |
|
||||
| ↳ `email` | string | User email address |
|
||||
| ↳ `firstName` | string | User first name |
|
||||
| ↳ `lastName` | string | User last name |
|
||||
| ↳ `fullName` | string | User full name |
|
||||
| ↳ `timeZone` | string | User time zone |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| `totalCount` | number | Total number of users returned |
|
||||
|
||||
### `rootly_list_on_calls`
|
||||
|
||||
List current on-call entries from Rootly with optional filtering.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `scheduleIds` | string | No | Comma-separated schedule IDs to filter by |
|
||||
| `escalationPolicyIds` | string | No | Comma-separated escalation policy IDs to filter by |
|
||||
| `userIds` | string | No | Comma-separated user IDs to filter by |
|
||||
| `serviceIds` | string | No | Comma-separated service IDs to filter by |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `onCalls` | array | List of on-call entries |
|
||||
| ↳ `id` | string | Unique on-call entry ID |
|
||||
| ↳ `userId` | string | ID of the on-call user |
|
||||
| ↳ `userName` | string | Name of the on-call user |
|
||||
| ↳ `scheduleId` | string | ID of the associated schedule |
|
||||
| ↳ `scheduleName` | string | Name of the associated schedule |
|
||||
| ↳ `escalationPolicyId` | string | ID of the associated escalation policy |
|
||||
| ↳ `startTime` | string | On-call start time |
|
||||
| ↳ `endTime` | string | On-call end time |
|
||||
| `totalCount` | number | Total number of on-call entries returned |
|
||||
|
||||
### `rootly_list_schedules`
|
||||
|
||||
List on-call schedules from Rootly with optional search filtering.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `search` | string | No | Search term to filter schedules |
|
||||
| `pageSize` | number | No | Number of items per page \(default: 20\) |
|
||||
| `pageNumber` | number | No | Page number for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `schedules` | array | List of schedules |
|
||||
| ↳ `id` | string | Unique schedule ID |
|
||||
| ↳ `name` | string | Schedule name |
|
||||
| ↳ `description` | string | Schedule description |
|
||||
| ↳ `allTimeCoverage` | boolean | Whether schedule provides 24/7 coverage |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| `totalCount` | number | Total number of schedules returned |
|
||||
|
||||
### `rootly_list_escalation_policies`
|
||||
|
||||
List escalation policies from Rootly with optional search filtering.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `search` | string | No | Search term to filter escalation policies |
|
||||
| `pageSize` | number | No | Number of items per page \(default: 20\) |
|
||||
| `pageNumber` | number | No | Page number for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `escalationPolicies` | array | List of escalation policies |
|
||||
| ↳ `id` | string | Unique escalation policy ID |
|
||||
| ↳ `name` | string | Escalation policy name |
|
||||
| ↳ `description` | string | Escalation policy description |
|
||||
| ↳ `repeatCount` | number | Number of times to repeat escalation |
|
||||
| ↳ `groupIds` | array | Associated group IDs |
|
||||
| ↳ `serviceIds` | array | Associated service IDs |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| `totalCount` | number | Total number of escalation policies returned |
|
||||
|
||||
### `rootly_list_causes`
|
||||
|
||||
List causes from Rootly with optional search filtering.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `search` | string | No | Search term to filter causes |
|
||||
| `pageSize` | number | No | Number of items per page \(default: 20\) |
|
||||
| `pageNumber` | number | No | Page number for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `causes` | array | List of causes |
|
||||
| ↳ `id` | string | Unique cause ID |
|
||||
| ↳ `name` | string | Cause name |
|
||||
| ↳ `slug` | string | Cause slug |
|
||||
| ↳ `description` | string | Cause description |
|
||||
| ↳ `position` | number | Cause position |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| `totalCount` | number | Total number of causes returned |
|
||||
|
||||
### `rootly_list_playbooks`
|
||||
|
||||
List playbooks from Rootly with pagination support.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `pageSize` | number | No | Number of items per page \(default: 20\) |
|
||||
| `pageNumber` | number | No | Page number for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `playbooks` | array | List of playbooks |
|
||||
| ↳ `id` | string | Unique playbook ID |
|
||||
| ↳ `title` | string | Playbook title |
|
||||
| ↳ `summary` | string | Playbook summary |
|
||||
| ↳ `externalUrl` | string | External URL |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| `totalCount` | number | Total number of playbooks returned |
|
||||
|
||||
|
||||
157
apps/docs/content/docs/en/tools/secrets_manager.mdx
Normal file
157
apps/docs/content/docs/en/tools/secrets_manager.mdx
Normal file
@@ -0,0 +1,157 @@
|
||||
---
|
||||
title: AWS Secrets Manager
|
||||
description: Connect to AWS Secrets Manager
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="secrets_manager"
|
||||
color="linear-gradient(45deg, #BD0816 0%, #FF5252 100%)"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[AWS Secrets Manager](https://aws.amazon.com/secrets-manager/) is a secrets management service that helps you protect access to your applications, services, and IT resources. It enables you to rotate, manage, and retrieve database credentials, API keys, and other secrets throughout their lifecycle.
|
||||
|
||||
With AWS Secrets Manager, you can:
|
||||
|
||||
- **Securely store secrets**: Encrypt secrets at rest using AWS KMS encryption keys
|
||||
- **Retrieve secrets programmatically**: Access secrets from your applications and workflows without hardcoding credentials
|
||||
- **Rotate secrets automatically**: Configure automatic rotation for supported services like RDS, Redshift, and DocumentDB
|
||||
- **Audit access**: Track secret access and changes through AWS CloudTrail integration
|
||||
- **Control access with IAM**: Use fine-grained IAM policies to manage who can access which secrets
|
||||
- **Replicate across regions**: Automatically replicate secrets to multiple AWS regions for disaster recovery
|
||||
|
||||
In Sim, the AWS Secrets Manager integration allows your workflows to securely retrieve credentials and configuration values at runtime, create and manage secrets as part of automation pipelines, and maintain a centralized secrets store that your agents can access. This is particularly useful for workflows that need to authenticate with external services, rotate credentials, or manage sensitive configuration across environments — all without exposing secrets in your workflow definitions.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate AWS Secrets Manager into the workflow. Can retrieve, create, update, list, and delete secrets.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `secrets_manager_get_secret`
|
||||
|
||||
Retrieve a secret value from AWS Secrets Manager
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `accessKeyId` | string | Yes | AWS access key ID |
|
||||
| `secretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `secretId` | string | Yes | The name or ARN of the secret to retrieve |
|
||||
| `versionId` | string | No | The unique identifier of the version to retrieve |
|
||||
| `versionStage` | string | No | The staging label of the version to retrieve \(e.g., AWSCURRENT, AWSPREVIOUS\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `name` | string | Name of the secret |
|
||||
| `secretValue` | string | The decrypted secret value |
|
||||
| `arn` | string | ARN of the secret |
|
||||
| `versionId` | string | Version ID of the secret |
|
||||
| `versionStages` | array | Staging labels attached to this version |
|
||||
| `createdDate` | string | Date the secret was created |
|
||||
|
||||
### `secrets_manager_list_secrets`
|
||||
|
||||
List secrets stored in AWS Secrets Manager
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `accessKeyId` | string | Yes | AWS access key ID |
|
||||
| `secretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `maxResults` | number | No | Maximum number of secrets to return \(1-100, default 100\) |
|
||||
| `nextToken` | string | No | Pagination token from a previous request |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `secrets` | json | List of secrets with name, ARN, description, and dates |
|
||||
| `nextToken` | string | Pagination token for the next page of results |
|
||||
| `count` | number | Number of secrets returned |
|
||||
|
||||
### `secrets_manager_create_secret`
|
||||
|
||||
Create a new secret in AWS Secrets Manager
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `accessKeyId` | string | Yes | AWS access key ID |
|
||||
| `secretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `name` | string | Yes | Name of the secret to create |
|
||||
| `secretValue` | string | Yes | The secret value \(plain text or JSON string\) |
|
||||
| `description` | string | No | Description of the secret |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Operation status message |
|
||||
| `name` | string | Name of the created secret |
|
||||
| `arn` | string | ARN of the created secret |
|
||||
| `versionId` | string | Version ID of the created secret |
|
||||
|
||||
### `secrets_manager_update_secret`
|
||||
|
||||
Update the value of an existing secret in AWS Secrets Manager
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `accessKeyId` | string | Yes | AWS access key ID |
|
||||
| `secretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `secretId` | string | Yes | The name or ARN of the secret to update |
|
||||
| `secretValue` | string | Yes | The new secret value \(plain text or JSON string\) |
|
||||
| `description` | string | No | Updated description of the secret |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Operation status message |
|
||||
| `name` | string | Name of the updated secret |
|
||||
| `arn` | string | ARN of the updated secret |
|
||||
| `versionId` | string | Version ID of the updated secret |
|
||||
|
||||
### `secrets_manager_delete_secret`
|
||||
|
||||
Delete a secret from AWS Secrets Manager
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `accessKeyId` | string | Yes | AWS access key ID |
|
||||
| `secretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `secretId` | string | Yes | The name or ARN of the secret to delete |
|
||||
| `recoveryWindowInDays` | number | No | Number of days before permanent deletion \(7-30, default 30\) |
|
||||
| `forceDelete` | boolean | No | If true, immediately delete without recovery window |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Operation status message |
|
||||
| `name` | string | Name of the deleted secret |
|
||||
| `arn` | string | ARN of the deleted secret |
|
||||
| `deletionDate` | string | Scheduled deletion date |
|
||||
|
||||
|
||||
498
apps/docs/content/docs/en/tools/tailscale.mdx
Normal file
498
apps/docs/content/docs/en/tools/tailscale.mdx
Normal file
@@ -0,0 +1,498 @@
|
||||
---
|
||||
title: Tailscale
|
||||
description: Manage devices and network settings in your Tailscale tailnet
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="tailscale"
|
||||
color="#2E2D2D"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
## Overview
|
||||
|
||||
[Tailscale](https://tailscale.com) is a zero-config mesh VPN built on WireGuard that makes it easy to connect devices, services, and users across any network. The Tailscale block lets you automate network management tasks like device provisioning, access control, route management, and DNS configuration directly from your Sim workflows.
|
||||
|
||||
## Authentication
|
||||
|
||||
The Tailscale block uses API key authentication. To get an API key:
|
||||
|
||||
1. Go to the [Tailscale admin console](https://login.tailscale.com/admin/settings/keys)
|
||||
2. Navigate to **Settings > Keys**
|
||||
3. Click **Generate API key**
|
||||
4. Set an expiry (1-90 days) and copy the key (starts with `tskey-api-`)
|
||||
|
||||
You must have an **Owner**, **Admin**, **IT admin**, or **Network admin** role to generate API keys.
|
||||
|
||||
## Tailnet Identifier
|
||||
|
||||
Every operation requires a **tailnet** parameter. This is typically your organization's domain name (e.g., `example.com`). You can also use `"-"` to refer to your default tailnet.
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
- **Device inventory**: List and monitor all devices connected to your network
|
||||
- **Automated provisioning**: Create and manage auth keys to pre-authorize new devices
|
||||
- **Access control**: Authorize or deauthorize devices, manage device tags for ACL policies
|
||||
- **Route management**: View and enable subnet routes for devices acting as subnet routers
|
||||
- **DNS management**: Configure nameservers, MagicDNS, and search paths
|
||||
- **Key lifecycle**: Create, list, inspect, and revoke auth keys
|
||||
- **User auditing**: List all users in the tailnet and their roles
|
||||
- **Policy review**: Retrieve the current ACL policy for inspection or backup
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Interact with the Tailscale API to manage devices, DNS, ACLs, auth keys, users, and routes across your tailnet.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `tailscale_list_devices`
|
||||
|
||||
List all devices in the tailnet
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Tailscale API key |
|
||||
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `devices` | array | List of devices in the tailnet |
|
||||
| ↳ `id` | string | Device ID |
|
||||
| ↳ `name` | string | Device name |
|
||||
| ↳ `hostname` | string | Device hostname |
|
||||
| ↳ `user` | string | Associated user |
|
||||
| ↳ `os` | string | Operating system |
|
||||
| ↳ `clientVersion` | string | Tailscale client version |
|
||||
| ↳ `addresses` | array | Tailscale IP addresses |
|
||||
| ↳ `tags` | array | Device tags |
|
||||
| ↳ `authorized` | boolean | Whether the device is authorized |
|
||||
| ↳ `blocksIncomingConnections` | boolean | Whether the device blocks incoming connections |
|
||||
| ↳ `lastSeen` | string | Last seen timestamp |
|
||||
| ↳ `created` | string | Creation timestamp |
|
||||
| `count` | number | Total number of devices |
|
||||
|
||||
### `tailscale_get_device`
|
||||
|
||||
Get details of a specific device by ID
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Tailscale API key |
|
||||
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
|
||||
| `deviceId` | string | Yes | Device ID |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Device ID |
|
||||
| `name` | string | Device name |
|
||||
| `hostname` | string | Device hostname |
|
||||
| `user` | string | Associated user |
|
||||
| `os` | string | Operating system |
|
||||
| `clientVersion` | string | Tailscale client version |
|
||||
| `addresses` | array | Tailscale IP addresses |
|
||||
| `tags` | array | Device tags |
|
||||
| `authorized` | boolean | Whether the device is authorized |
|
||||
| `blocksIncomingConnections` | boolean | Whether the device blocks incoming connections |
|
||||
| `lastSeen` | string | Last seen timestamp |
|
||||
| `created` | string | Creation timestamp |
|
||||
| `isExternal` | boolean | Whether the device is external |
|
||||
| `updateAvailable` | boolean | Whether an update is available |
|
||||
| `machineKey` | string | Machine key |
|
||||
| `nodeKey` | string | Node key |
|
||||
|
||||
### `tailscale_delete_device`
|
||||
|
||||
Remove a device from the tailnet
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Tailscale API key |
|
||||
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
|
||||
| `deviceId` | string | Yes | Device ID to delete |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether the device was successfully deleted |
|
||||
| `deviceId` | string | ID of the deleted device |
|
||||
|
||||
### `tailscale_authorize_device`
|
||||
|
||||
Authorize or deauthorize a device on the tailnet
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Tailscale API key |
|
||||
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
|
||||
| `deviceId` | string | Yes | Device ID to authorize |
|
||||
| `authorized` | boolean | Yes | Whether to authorize \(true\) or deauthorize \(false\) the device |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether the operation succeeded |
|
||||
| `deviceId` | string | Device ID |
|
||||
| `authorized` | boolean | Authorization status after the operation |
|
||||
|
||||
### `tailscale_set_device_tags`
|
||||
|
||||
Set tags on a device in the tailnet
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Tailscale API key |
|
||||
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
|
||||
| `deviceId` | string | Yes | Device ID |
|
||||
| `tags` | string | Yes | Comma-separated list of tags \(e.g., "tag:server,tag:production"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether the tags were successfully set |
|
||||
| `deviceId` | string | Device ID |
|
||||
| `tags` | array | Tags set on the device |
|
||||
|
||||
### `tailscale_get_device_routes`
|
||||
|
||||
Get the subnet routes for a device
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Tailscale API key |
|
||||
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
|
||||
| `deviceId` | string | Yes | Device ID |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `advertisedRoutes` | array | Subnet routes the device is advertising |
|
||||
| `enabledRoutes` | array | Subnet routes that are approved/enabled |
|
||||
|
||||
### `tailscale_set_device_routes`
|
||||
|
||||
Set the enabled subnet routes for a device
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Tailscale API key |
|
||||
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
|
||||
| `deviceId` | string | Yes | Device ID |
|
||||
| `routes` | string | Yes | Comma-separated list of subnet routes to enable \(e.g., "10.0.0.0/24,192.168.1.0/24"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `advertisedRoutes` | array | Subnet routes the device is advertising |
|
||||
| `enabledRoutes` | array | Subnet routes that are now enabled |
|
||||
|
||||
### `tailscale_update_device_key`
|
||||
|
||||
Enable or disable key expiry on a device
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Tailscale API key |
|
||||
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
|
||||
| `deviceId` | string | Yes | Device ID |
|
||||
| `keyExpiryDisabled` | boolean | Yes | Whether to disable key expiry \(true\) or enable it \(false\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether the operation succeeded |
|
||||
| `deviceId` | string | Device ID |
|
||||
| `keyExpiryDisabled` | boolean | Whether key expiry is now disabled |
|
||||
|
||||
### `tailscale_list_dns_nameservers`
|
||||
|
||||
Get the DNS nameservers configured for the tailnet
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Tailscale API key |
|
||||
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `dns` | array | List of DNS nameserver addresses |
|
||||
| `magicDNS` | boolean | Whether MagicDNS is enabled |
|
||||
|
||||
### `tailscale_set_dns_nameservers`
|
||||
|
||||
Set the DNS nameservers for the tailnet
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Tailscale API key |
|
||||
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
|
||||
| `dns` | string | Yes | Comma-separated list of DNS nameserver IP addresses \(e.g., "8.8.8.8,8.8.4.4"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `dns` | array | Updated list of DNS nameserver addresses |
|
||||
| `magicDNS` | boolean | Whether MagicDNS is enabled |
|
||||
|
||||
### `tailscale_get_dns_preferences`
|
||||
|
||||
Get the DNS preferences for the tailnet including MagicDNS status
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Tailscale API key |
|
||||
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `magicDNS` | boolean | Whether MagicDNS is enabled |
|
||||
|
||||
### `tailscale_set_dns_preferences`
|
||||
|
||||
Set DNS preferences for the tailnet (enable/disable MagicDNS)
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Tailscale API key |
|
||||
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
|
||||
| `magicDNS` | boolean | Yes | Whether to enable \(true\) or disable \(false\) MagicDNS |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `magicDNS` | boolean | Updated MagicDNS status |
|
||||
|
||||
### `tailscale_get_dns_searchpaths`
|
||||
|
||||
Get the DNS search paths configured for the tailnet
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Tailscale API key |
|
||||
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `searchPaths` | array | List of DNS search path domains |
|
||||
|
||||
### `tailscale_set_dns_searchpaths`
|
||||
|
||||
Set the DNS search paths for the tailnet
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Tailscale API key |
|
||||
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
|
||||
| `searchPaths` | string | Yes | Comma-separated list of DNS search path domains \(e.g., "corp.example.com,internal.example.com"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `searchPaths` | array | Updated list of DNS search path domains |
|
||||
|
||||
### `tailscale_list_users`
|
||||
|
||||
List all users in the tailnet
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Tailscale API key |
|
||||
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `users` | array | List of users in the tailnet |
|
||||
| ↳ `id` | string | User ID |
|
||||
| ↳ `displayName` | string | Display name |
|
||||
| ↳ `loginName` | string | Login name / email |
|
||||
| ↳ `profilePicURL` | string | Profile picture URL |
|
||||
| ↳ `role` | string | User role \(owner, admin, member, etc.\) |
|
||||
| ↳ `status` | string | User status \(active, suspended, etc.\) |
|
||||
| ↳ `type` | string | User type \(member, shared, tagged\) |
|
||||
| ↳ `created` | string | Creation timestamp |
|
||||
| ↳ `lastSeen` | string | Last seen timestamp |
|
||||
| ↳ `deviceCount` | number | Number of devices owned by user |
|
||||
| `count` | number | Total number of users |
|
||||
|
||||
### `tailscale_create_auth_key`
|
||||
|
||||
Create a new auth key for the tailnet to pre-authorize devices
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Tailscale API key |
|
||||
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
|
||||
| `reusable` | boolean | No | Whether the key can be used more than once |
|
||||
| `ephemeral` | boolean | No | Whether devices authenticated with this key are ephemeral |
|
||||
| `preauthorized` | boolean | No | Whether devices are pre-authorized \(skip manual approval\) |
|
||||
| `tags` | string | No | Comma-separated list of tags for devices using this key \(e.g., "tag:server,tag:prod"\) |
|
||||
| `description` | string | No | Description for the auth key |
|
||||
| `expirySeconds` | number | No | Key expiry time in seconds \(default: 90 days\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Auth key ID |
|
||||
| `key` | string | The auth key value \(only shown once at creation\) |
|
||||
| `description` | string | Key description |
|
||||
| `created` | string | Creation timestamp |
|
||||
| `expires` | string | Expiration timestamp |
|
||||
| `revoked` | string | Revocation timestamp \(empty if not revoked\) |
|
||||
| `capabilities` | object | Key capabilities |
|
||||
| ↳ `reusable` | boolean | Whether the key is reusable |
|
||||
| ↳ `ephemeral` | boolean | Whether devices are ephemeral |
|
||||
| ↳ `preauthorized` | boolean | Whether devices are pre-authorized |
|
||||
| ↳ `tags` | array | Tags applied to devices using this key |
|
||||
|
||||
### `tailscale_list_auth_keys`
|
||||
|
||||
List all auth keys in the tailnet
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Tailscale API key |
|
||||
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `keys` | array | List of auth keys |
|
||||
| ↳ `id` | string | Auth key ID |
|
||||
| ↳ `description` | string | Key description |
|
||||
| ↳ `created` | string | Creation timestamp |
|
||||
| ↳ `expires` | string | Expiration timestamp |
|
||||
| ↳ `revoked` | string | Revocation timestamp |
|
||||
| ↳ `capabilities` | object | Key capabilities |
|
||||
| ↳ `reusable` | boolean | Whether the key is reusable |
|
||||
| ↳ `ephemeral` | boolean | Whether devices are ephemeral |
|
||||
| ↳ `preauthorized` | boolean | Whether devices are pre-authorized |
|
||||
| ↳ `tags` | array | Tags applied to devices |
|
||||
| `count` | number | Total number of auth keys |
|
||||
|
||||
### `tailscale_get_auth_key`
|
||||
|
||||
Get details of a specific auth key
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Tailscale API key |
|
||||
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
|
||||
| `keyId` | string | Yes | Auth key ID |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Auth key ID |
|
||||
| `description` | string | Key description |
|
||||
| `created` | string | Creation timestamp |
|
||||
| `expires` | string | Expiration timestamp |
|
||||
| `revoked` | string | Revocation timestamp |
|
||||
| `capabilities` | object | Key capabilities |
|
||||
| ↳ `reusable` | boolean | Whether the key is reusable |
|
||||
| ↳ `ephemeral` | boolean | Whether devices are ephemeral |
|
||||
| ↳ `preauthorized` | boolean | Whether devices are pre-authorized |
|
||||
| ↳ `tags` | array | Tags applied to devices using this key |
|
||||
|
||||
### `tailscale_delete_auth_key`
|
||||
|
||||
Revoke and delete an auth key
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Tailscale API key |
|
||||
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
|
||||
| `keyId` | string | Yes | Auth key ID to delete |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether the auth key was successfully deleted |
|
||||
| `keyId` | string | ID of the deleted auth key |
|
||||
|
||||
### `tailscale_get_acl`
|
||||
|
||||
Get the current ACL policy for the tailnet
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Tailscale API key |
|
||||
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `acl` | string | ACL policy as JSON string |
|
||||
| `etag` | string | ETag for the current ACL version \(use with If-Match header for updates\) |
|
||||
|
||||
|
||||
@@ -131,7 +131,7 @@ Detecta información de identificación personal utilizando Microsoft Presidio.
|
||||
**Casos de uso:**
|
||||
- Bloquear contenido que contiene información personal sensible
|
||||
- Enmascarar PII antes de registrar o almacenar datos
|
||||
- Cumplimiento de GDPR, HIPAA y otras regulaciones de privacidad
|
||||
- Cumplimiento de GDPR y otras regulaciones de privacidad
|
||||
- Sanear entradas de usuario antes del procesamiento
|
||||
|
||||
## Configuración
|
||||
|
||||
@@ -131,7 +131,7 @@ Détecte les informations personnelles identifiables à l'aide de Microsoft Pres
|
||||
**Cas d'utilisation :**
|
||||
- Bloquer le contenu contenant des informations personnelles sensibles
|
||||
- Masquer les PII avant de journaliser ou stocker des données
|
||||
- Conformité avec le RGPD, HIPAA et autres réglementations sur la confidentialité
|
||||
- Conformité avec le RGPD et autres réglementations sur la confidentialité
|
||||
- Assainir les entrées utilisateur avant traitement
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -131,7 +131,7 @@ Microsoft Presidioを使用して個人を特定できる情報を検出しま
|
||||
**ユースケース:**
|
||||
- 機密性の高い個人情報を含むコンテンツをブロック
|
||||
- データのログ記録や保存前にPIIをマスク
|
||||
- GDPR、HIPAA、その他のプライバシー規制への準拠
|
||||
- GDPR、その他のプライバシー規制への準拠
|
||||
- 処理前のユーザー入力のサニタイズ
|
||||
|
||||
## 設定
|
||||
|
||||
@@ -131,7 +131,7 @@ Guardrails 模块通过针对多种验证类型检查内容,验证并保护您
|
||||
**使用场景:**
|
||||
- 阻止包含敏感个人信息的内容
|
||||
- 在记录或存储数据之前屏蔽 PII
|
||||
- 符合 GDPR、HIPAA 和其他隐私法规
|
||||
- 符合 GDPR 和其他隐私法规
|
||||
- 在处理之前清理用户输入
|
||||
|
||||
## 配置
|
||||
|
||||
BIN
apps/docs/public/static/credentials/add-service-account.png
Normal file
BIN
apps/docs/public/static/credentials/add-service-account.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 59 KiB |
BIN
apps/docs/public/static/credentials/gcp-add-client-id.png
Normal file
BIN
apps/docs/public/static/credentials/gcp-add-client-id.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 75 KiB |
BIN
apps/docs/public/static/credentials/gcp-create-private-key.png
Normal file
BIN
apps/docs/public/static/credentials/gcp-create-private-key.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 92 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
@@ -28,6 +28,15 @@ API_ENCRYPTION_KEY=your_api_encryption_key # Use `openssl rand -hex 32` to gener
|
||||
# OLLAMA_URL=http://localhost:11434 # URL for local Ollama server - uncomment if using local models
|
||||
# VLLM_BASE_URL=http://localhost:8000 # Base URL for your self-hosted vLLM (OpenAI-compatible)
|
||||
# VLLM_API_KEY= # Optional bearer token if your vLLM instance requires auth
|
||||
# FIREWORKS_API_KEY= # Optional Fireworks AI API key for model listing
|
||||
# NEXT_PUBLIC_BEDROCK_DEFAULT_CREDENTIALS=true # Set when using AWS default credential chain (IAM roles, ECS task roles, IRSA). Hides credential fields in Agent block UI.
|
||||
# AZURE_OPENAI_ENDPOINT= # Azure OpenAI endpoint (hides field in UI when set alongside NEXT_PUBLIC_AZURE_CONFIGURED)
|
||||
# AZURE_OPENAI_API_KEY= # Azure OpenAI API key
|
||||
# AZURE_OPENAI_API_VERSION= # Azure OpenAI API version
|
||||
# AZURE_ANTHROPIC_ENDPOINT= # Azure Anthropic endpoint (AI Foundry)
|
||||
# AZURE_ANTHROPIC_API_KEY= # Azure Anthropic API key
|
||||
# AZURE_ANTHROPIC_API_VERSION= # Azure Anthropic API version (e.g., 2023-06-01)
|
||||
# NEXT_PUBLIC_AZURE_CONFIGURED=true # Set when Azure credentials are pre-configured above. Hides endpoint/key/version fields in Agent block UI.
|
||||
|
||||
# Admin API (Optional - for self-hosted GitOps)
|
||||
# ADMIN_API_KEY= # Use `openssl rand -hex 32` to generate. Enables admin API for workflow export/import.
|
||||
|
||||
6
apps/sim/app/(auth)/components/auth-button-classes.ts
Normal file
6
apps/sim/app/(auth)/components/auth-button-classes.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/** Shared className for primary auth/status CTA buttons on dark auth surfaces. */
|
||||
export const AUTH_PRIMARY_CTA_BASE =
|
||||
'inline-flex h-[32px] items-center justify-center gap-2 rounded-[5px] border border-[var(--auth-primary-btn-border)] bg-[var(--auth-primary-btn-bg)] px-2.5 font-[430] font-season text-[var(--auth-primary-btn-text)] text-sm transition-colors hover:border-[var(--auth-primary-btn-hover-border)] hover:bg-[var(--auth-primary-btn-hover-bg)] hover:text-[var(--auth-primary-btn-hover-text)] disabled:cursor-not-allowed disabled:opacity-50' as const
|
||||
|
||||
/** Full-width variant used for primary auth form submit buttons. */
|
||||
export const AUTH_SUBMIT_BTN = `${AUTH_PRIMARY_CTA_BASE} w-full` as const
|
||||
@@ -1,102 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef, useState } from 'react'
|
||||
import { ArrowRight, ChevronRight, Loader2 } from 'lucide-react'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { useBrandConfig } from '@/ee/whitelabeling'
|
||||
|
||||
export interface BrandedButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
loading?: boolean
|
||||
loadingText?: string
|
||||
showArrow?: boolean
|
||||
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.
|
||||
*/
|
||||
export const BrandedButton = forwardRef<HTMLButtonElement, BrandedButtonProps>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
loading = false,
|
||||
loadingText,
|
||||
showArrow = true,
|
||||
fullWidth = true,
|
||||
className,
|
||||
disabled,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const brand = useBrandConfig()
|
||||
const hasCustomColor = brand.isWhitelabeled && Boolean(brand.theme?.primaryColor)
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
|
||||
const handleMouseEnter = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setIsHovered(true)
|
||||
onMouseEnter?.(e)
|
||||
}
|
||||
|
||||
const handleMouseLeave = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setIsHovered(false)
|
||||
onMouseLeave?.(e)
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
{...props}
|
||||
disabled={disabled || loading}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
className={cn(
|
||||
'group inline-flex h-[32px] items-center justify-center gap-2 rounded-[5px] border px-2.5 font-[430] font-season text-sm transition-colors disabled:cursor-not-allowed disabled:opacity-50',
|
||||
!hasCustomColor &&
|
||||
'border-[var(--white)] bg-[var(--white)] 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
|
||||
}
|
||||
>
|
||||
{loading ? (
|
||||
<span className='flex items-center gap-2'>
|
||||
<Loader2 className='h-4 w-4 animate-spin' />
|
||||
{loadingText ? `${loadingText}...` : children}
|
||||
</span>
|
||||
) : showArrow ? (
|
||||
<span className='flex items-center gap-1'>
|
||||
{children}
|
||||
<span className='inline-flex transition-transform duration-200 group-hover:translate-x-0.5'>
|
||||
{isHovered ? (
|
||||
<ArrowRight className='h-4 w-4' aria-hidden='true' />
|
||||
) : (
|
||||
<ChevronRight className='h-4 w-4' aria-hidden='true' />
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
BrandedButton.displayName = 'BrandedButton'
|
||||
@@ -4,23 +4,18 @@ import { useRouter } from 'next/navigation'
|
||||
import { Button } from '@/components/emcn'
|
||||
import { getEnv, isTruthy } from '@/lib/core/config/env'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { AUTH_SUBMIT_BTN } from '@/app/(auth)/components/auth-button-classes'
|
||||
|
||||
interface SSOLoginButtonProps {
|
||||
callbackURL?: string
|
||||
className?: string
|
||||
// Visual variant for button styling and placement contexts
|
||||
// - 'primary' matches the main auth action button style
|
||||
// - 'outline' matches social provider buttons
|
||||
variant?: 'primary' | 'outline'
|
||||
// Optional class used when variant is primary to match brand/gradient
|
||||
primaryClassName?: string
|
||||
}
|
||||
|
||||
export function SSOLoginButton({
|
||||
callbackURL,
|
||||
className,
|
||||
variant = 'outline',
|
||||
primaryClassName,
|
||||
}: SSOLoginButtonProps) {
|
||||
const router = useRouter()
|
||||
|
||||
@@ -33,11 +28,6 @@ export function SSOLoginButton({
|
||||
router.push(ssoUrl)
|
||||
}
|
||||
|
||||
const primaryBtnClasses = cn(
|
||||
primaryClassName || 'branded-button-gradient',
|
||||
'flex w-full items-center justify-center gap-2 rounded-[10px] border font-medium text-base text-white transition-all duration-200'
|
||||
)
|
||||
|
||||
const outlineBtnClasses = cn('w-full rounded-[10px]')
|
||||
|
||||
return (
|
||||
@@ -45,7 +35,7 @@ export function SSOLoginButton({
|
||||
type='button'
|
||||
onClick={handleSSOClick}
|
||||
variant={variant === 'outline' ? 'outline' : undefined}
|
||||
className={cn(variant === 'outline' ? outlineBtnClasses : primaryBtnClasses, className)}
|
||||
className={cn(variant === 'outline' ? outlineBtnClasses : AUTH_SUBMIT_BTN, className)}
|
||||
>
|
||||
Sign in with SSO
|
||||
</Button>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useRef, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { Eye, EyeOff } from 'lucide-react'
|
||||
import { Eye, EyeOff, Loader2 } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import {
|
||||
@@ -20,10 +20,9 @@ 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 { BrandedButton } from '@/app/(auth)/components/branded-button'
|
||||
import { AUTH_SUBMIT_BTN } from '@/app/(auth)/components/auth-button-classes'
|
||||
import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons'
|
||||
import { SSOLoginButton } from '@/app/(auth)/components/sso-login-button'
|
||||
import { useBrandedButtonClass } from '@/hooks/use-branded-button-class'
|
||||
|
||||
const logger = createLogger('LoginForm')
|
||||
|
||||
@@ -87,8 +86,6 @@ export default function LoginPage({
|
||||
const [passwordErrors, setPasswordErrors] = useState<string[]>([])
|
||||
const [showValidationError, setShowValidationError] = useState(false)
|
||||
const [formError, setFormError] = useState<string | null>(null)
|
||||
const buttonClass = useBrandedButtonClass()
|
||||
|
||||
const callbackUrlParam = searchParams?.get('callbackUrl')
|
||||
const isValidCallbackUrl = callbackUrlParam ? validateCallbackUrl(callbackUrlParam) : false
|
||||
const invalidCallbackRef = useRef(false)
|
||||
@@ -353,11 +350,7 @@ export default function LoginPage({
|
||||
{/* SSO Login Button (primary top-only when it is the only method) */}
|
||||
{showTopSSO && (
|
||||
<div className='mt-8'>
|
||||
<SSOLoginButton
|
||||
callbackURL={callbackUrl}
|
||||
variant='primary'
|
||||
primaryClassName={buttonClass}
|
||||
/>
|
||||
<SSOLoginButton callbackURL={callbackUrl} variant='primary' />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -454,14 +447,16 @@ export default function LoginPage({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<BrandedButton
|
||||
type='submit'
|
||||
disabled={isLoading}
|
||||
loading={isLoading}
|
||||
loadingText='Signing in'
|
||||
>
|
||||
Sign in
|
||||
</BrandedButton>
|
||||
<button type='submit' disabled={isLoading} className={AUTH_SUBMIT_BTN}>
|
||||
{isLoading ? (
|
||||
<span className='flex items-center gap-2'>
|
||||
<Loader2 className='h-4 w-4 animate-spin' />
|
||||
Signing in...
|
||||
</span>
|
||||
) : (
|
||||
'Sign in'
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
@@ -488,11 +483,7 @@ export default function LoginPage({
|
||||
callbackURL={callbackUrl}
|
||||
>
|
||||
{ssoEnabled && !hasOnlySSO && (
|
||||
<SSOLoginButton
|
||||
callbackURL={callbackUrl}
|
||||
variant='outline'
|
||||
primaryClassName={buttonClass}
|
||||
/>
|
||||
<SSOLoginButton callbackURL={callbackUrl} variant='outline' />
|
||||
)}
|
||||
</SocialLoginButtons>
|
||||
</div>
|
||||
@@ -571,14 +562,16 @@ export default function LoginPage({
|
||||
<p>{resetStatus.message}</p>
|
||||
</div>
|
||||
)}
|
||||
<BrandedButton
|
||||
type='submit'
|
||||
disabled={isSubmittingReset}
|
||||
loading={isSubmittingReset}
|
||||
loadingText='Sending'
|
||||
>
|
||||
Send Reset Link
|
||||
</BrandedButton>
|
||||
<button type='submit' disabled={isSubmittingReset} className={AUTH_SUBMIT_BTN}>
|
||||
{isSubmittingReset ? (
|
||||
<span className='flex items-center gap-2'>
|
||||
<Loader2 className='h-4 w-4 animate-spin' />
|
||||
Sending...
|
||||
</span>
|
||||
) : (
|
||||
'Send Reset Link'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</ModalBody>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { ArrowLeftRight } from 'lucide-react'
|
||||
import { ArrowLeftRight, Loader2 } from 'lucide-react'
|
||||
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 { BrandedButton } from '@/app/(auth)/components/branded-button'
|
||||
import { AUTH_SUBMIT_BTN } from '@/app/(auth)/components/auth-button-classes'
|
||||
|
||||
const SCOPE_DESCRIPTIONS: Record<string, string> = {
|
||||
openid: 'Verify your identity',
|
||||
@@ -150,7 +150,9 @@ export default function OAuthConsentPage() {
|
||||
</p>
|
||||
</div>
|
||||
<div className='mt-8 w-full max-w-[410px] space-y-3'>
|
||||
<BrandedButton onClick={() => router.push('/')}>Return to Home</BrandedButton>
|
||||
<button onClick={() => router.push('/')} className={AUTH_SUBMIT_BTN}>
|
||||
Return to Home
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -230,7 +232,7 @@ export default function OAuthConsentPage() {
|
||||
|
||||
{scopes.length > 0 && (
|
||||
<div className='mt-5 w-full max-w-[410px]'>
|
||||
<div className='rounded-lg border p-4'>
|
||||
<div className='rounded-lg border border-[var(--landing-bg-elevated)] p-4'>
|
||||
<p className='mb-3 font-medium text-sm'>This will allow the application to:</p>
|
||||
<ul className='space-y-2'>
|
||||
{scopes.map((s) => (
|
||||
@@ -257,15 +259,20 @@ export default function OAuthConsentPage() {
|
||||
>
|
||||
Deny
|
||||
</Button>
|
||||
<BrandedButton
|
||||
fullWidth
|
||||
showArrow={false}
|
||||
loading={submitting}
|
||||
loadingText='Authorizing'
|
||||
<button
|
||||
onClick={() => handleConsent(true)}
|
||||
disabled={submitting}
|
||||
className={AUTH_SUBMIT_BTN}
|
||||
>
|
||||
Allow
|
||||
</BrandedButton>
|
||||
{submitting ? (
|
||||
<span className='flex items-center gap-2'>
|
||||
<Loader2 className='h-4 w-4 animate-spin' />
|
||||
Authorizing...
|
||||
</span>
|
||||
) : (
|
||||
'Allow'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Eye, EyeOff } from 'lucide-react'
|
||||
import { Eye, EyeOff, Loader2 } from 'lucide-react'
|
||||
import { Input, Label } from '@/components/emcn'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { BrandedButton } from '@/app/(auth)/components/branded-button'
|
||||
import { AUTH_SUBMIT_BTN } from '@/app/(auth)/components/auth-button-classes'
|
||||
|
||||
interface RequestResetFormProps {
|
||||
email: string
|
||||
@@ -64,14 +64,16 @@ export function RequestResetForm({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<BrandedButton
|
||||
type='submit'
|
||||
disabled={isSubmitting}
|
||||
loading={isSubmitting}
|
||||
loadingText='Sending'
|
||||
>
|
||||
Send Reset Link
|
||||
</BrandedButton>
|
||||
<button type='submit' disabled={isSubmitting} className={AUTH_SUBMIT_BTN}>
|
||||
{isSubmitting ? (
|
||||
<span className='flex items-center gap-2'>
|
||||
<Loader2 className='h-4 w-4 animate-spin' />
|
||||
Sending...
|
||||
</span>
|
||||
) : (
|
||||
'Send Reset Link'
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -219,14 +221,16 @@ export function SetNewPasswordForm({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<BrandedButton
|
||||
type='submit'
|
||||
disabled={isSubmitting || !token}
|
||||
loading={isSubmitting}
|
||||
loadingText='Resetting'
|
||||
>
|
||||
Reset Password
|
||||
</BrandedButton>
|
||||
<button type='submit' disabled={isSubmitting || !token} className={AUTH_SUBMIT_BTN}>
|
||||
{isSubmitting ? (
|
||||
<span className='flex items-center gap-2'>
|
||||
<Loader2 className='h-4 w-4 animate-spin' />
|
||||
Resetting...
|
||||
</span>
|
||||
) : (
|
||||
'Reset Password'
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { Suspense, useMemo, useRef, useState } from 'react'
|
||||
import { Turnstile, type TurnstileInstance } from '@marsidev/react-turnstile'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { Eye, EyeOff } from 'lucide-react'
|
||||
import { Eye, EyeOff, Loader2 } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { Input, Label } from '@/components/emcn'
|
||||
@@ -11,10 +11,9 @@ 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 { BrandedButton } from '@/app/(auth)/components/branded-button'
|
||||
import { AUTH_SUBMIT_BTN } from '@/app/(auth)/components/auth-button-classes'
|
||||
import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons'
|
||||
import { SSOLoginButton } from '@/app/(auth)/components/sso-login-button'
|
||||
import { useBrandedButtonClass } from '@/hooks/use-branded-button-class'
|
||||
|
||||
const logger = createLogger('SignupForm')
|
||||
|
||||
@@ -96,8 +95,6 @@ function SignupFormContent({
|
||||
const captchaResolveRef = useRef<((token: string) => void) | null>(null)
|
||||
const captchaRejectRef = useRef<((reason: Error) => void) | null>(null)
|
||||
const turnstileSiteKey = useMemo(() => getEnv('NEXT_PUBLIC_TURNSTILE_SITE_KEY'), [])
|
||||
const buttonClass = useBrandedButtonClass()
|
||||
|
||||
const redirectUrl = useMemo(
|
||||
() => searchParams.get('redirect') || searchParams.get('callbackUrl') || '',
|
||||
[searchParams]
|
||||
@@ -380,11 +377,7 @@ function SignupFormContent({
|
||||
return hasOnlySSO
|
||||
})() && (
|
||||
<div className='mt-8'>
|
||||
<SSOLoginButton
|
||||
callbackURL={redirectUrl || '/workspace'}
|
||||
variant='primary'
|
||||
primaryClassName={buttonClass}
|
||||
/>
|
||||
<SSOLoginButton callbackURL={redirectUrl || '/workspace'} variant='primary' />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -548,15 +541,16 @@ function SignupFormContent({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<BrandedButton
|
||||
type='submit'
|
||||
disabled={isLoading}
|
||||
loading={isLoading}
|
||||
loadingText='Creating account'
|
||||
className='!mt-6'
|
||||
>
|
||||
Create account
|
||||
</BrandedButton>
|
||||
<button type='submit' disabled={isLoading} className={cn('!mt-6', AUTH_SUBMIT_BTN)}>
|
||||
{isLoading ? (
|
||||
<span className='flex items-center gap-2'>
|
||||
<Loader2 className='h-4 w-4 animate-spin' />
|
||||
Creating account...
|
||||
</span>
|
||||
) : (
|
||||
'Create account'
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
@@ -602,11 +596,7 @@ function SignupFormContent({
|
||||
isProduction={isProduction}
|
||||
>
|
||||
{isTruthy(getEnv('NEXT_PUBLIC_SSO_ENABLED')) && (
|
||||
<SSOLoginButton
|
||||
callbackURL={redirectUrl || '/workspace'}
|
||||
variant='outline'
|
||||
primaryClassName={buttonClass}
|
||||
/>
|
||||
<SSOLoginButton callbackURL={redirectUrl || '/workspace'} variant='outline' />
|
||||
)}
|
||||
</SocialLoginButtons>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
'use client'
|
||||
|
||||
import { Suspense, useEffect, useState } from 'react'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { InputOTP, InputOTPGroup, InputOTPSlot } from '@/components/emcn'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { BrandedButton } from '@/app/(auth)/components/branded-button'
|
||||
import { AUTH_SUBMIT_BTN } from '@/app/(auth)/components/auth-button-classes'
|
||||
import { useVerification } from '@/app/(auth)/verify/use-verification'
|
||||
|
||||
interface VerifyContentProps {
|
||||
@@ -110,15 +111,20 @@ function VerificationForm({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<BrandedButton
|
||||
<button
|
||||
onClick={verifyCode}
|
||||
disabled={!isOtpComplete || isLoading}
|
||||
loading={isLoading}
|
||||
loadingText='Verifying'
|
||||
showArrow={false}
|
||||
className={AUTH_SUBMIT_BTN}
|
||||
>
|
||||
Verify Email
|
||||
</BrandedButton>
|
||||
{isLoading ? (
|
||||
<span className='flex items-center gap-2'>
|
||||
<Loader2 className='h-4 w-4 animate-spin' />
|
||||
Verifying...
|
||||
</span>
|
||||
) : (
|
||||
'Verify Email'
|
||||
)}
|
||||
</button>
|
||||
|
||||
{hasEmailService && (
|
||||
<div className='text-center'>
|
||||
|
||||
@@ -288,7 +288,6 @@ export default function Collaboration() {
|
||||
width={876}
|
||||
height={480}
|
||||
className='h-full w-auto object-left md:min-w-[100vw]'
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
<div className='hidden lg:block'>
|
||||
|
||||
@@ -14,13 +14,14 @@ export const DEMO_REQUEST_REGION_VALUES = [
|
||||
'other',
|
||||
] as const
|
||||
|
||||
export const DEMO_REQUEST_USER_COUNT_VALUES = [
|
||||
export const DEMO_REQUEST_COMPANY_SIZE_VALUES = [
|
||||
'1_10',
|
||||
'11_50',
|
||||
'51_200',
|
||||
'201_500',
|
||||
'501_1000',
|
||||
'1000_plus',
|
||||
'1001_10000',
|
||||
'10000_plus',
|
||||
] as const
|
||||
|
||||
export const DEMO_REQUEST_REGION_OPTIONS = [
|
||||
@@ -32,13 +33,14 @@ export const DEMO_REQUEST_REGION_OPTIONS = [
|
||||
{ value: 'other', label: 'Other' },
|
||||
] as const
|
||||
|
||||
export const DEMO_REQUEST_USER_COUNT_OPTIONS = [
|
||||
{ value: '1_10', label: '1-10' },
|
||||
{ value: '11_50', label: '11-50' },
|
||||
{ value: '51_200', label: '51-200' },
|
||||
{ value: '201_500', label: '201-500' },
|
||||
{ value: '501_1000', label: '501-1,000' },
|
||||
{ value: '1000_plus', label: '1,000+' },
|
||||
export const DEMO_REQUEST_COMPANY_SIZE_OPTIONS = [
|
||||
{ value: '1_10', label: '1–10' },
|
||||
{ value: '11_50', label: '11–50' },
|
||||
{ value: '51_200', label: '51–200' },
|
||||
{ value: '201_500', label: '201–500' },
|
||||
{ value: '501_1000', label: '501–1,000' },
|
||||
{ value: '1001_10000', label: '1,001–10,000' },
|
||||
{ value: '10000_plus', label: '10,000+' },
|
||||
] as const
|
||||
|
||||
export const demoRequestSchema = z.object({
|
||||
@@ -74,8 +76,8 @@ export const demoRequestSchema = z.object({
|
||||
region: z.enum(DEMO_REQUEST_REGION_VALUES, {
|
||||
errorMap: () => ({ message: 'Please select a region' }),
|
||||
}),
|
||||
userCount: z.enum(DEMO_REQUEST_USER_COUNT_VALUES, {
|
||||
errorMap: () => ({ message: 'Please select the number of users' }),
|
||||
companySize: z.enum(DEMO_REQUEST_COMPANY_SIZE_VALUES, {
|
||||
errorMap: () => ({ message: 'Please select company size' }),
|
||||
}),
|
||||
details: z.string().trim().min(1, 'Details are required').max(2000),
|
||||
})
|
||||
@@ -86,6 +88,6 @@ export function getDemoRequestRegionLabel(value: DemoRequestPayload['region']):
|
||||
return DEMO_REQUEST_REGION_OPTIONS.find((option) => option.value === value)?.label ?? value
|
||||
}
|
||||
|
||||
export function getDemoRequestUserCountLabel(value: DemoRequestPayload['userCount']): string {
|
||||
return DEMO_REQUEST_USER_COUNT_OPTIONS.find((option) => option.value === value)?.label ?? value
|
||||
export function getDemoRequestCompanySizeLabel(value: DemoRequestPayload['companySize']): string {
|
||||
return DEMO_REQUEST_COMPANY_SIZE_OPTIONS.find((option) => option.value === value)?.label ?? value
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ import {
|
||||
} from '@/components/emcn'
|
||||
import { Check } from '@/components/emcn/icons'
|
||||
import {
|
||||
DEMO_REQUEST_COMPANY_SIZE_OPTIONS,
|
||||
DEMO_REQUEST_REGION_OPTIONS,
|
||||
DEMO_REQUEST_USER_COUNT_OPTIONS,
|
||||
type DemoRequestPayload,
|
||||
demoRequestSchema,
|
||||
} from '@/app/(home)/components/demo-request/consts'
|
||||
@@ -36,13 +36,13 @@ interface DemoRequestFormState {
|
||||
companyEmail: string
|
||||
phoneNumber: string
|
||||
region: DemoRequestPayload['region'] | ''
|
||||
userCount: DemoRequestPayload['userCount'] | ''
|
||||
companySize: DemoRequestPayload['companySize'] | ''
|
||||
details: string
|
||||
}
|
||||
|
||||
const SUBMIT_SUCCESS_MESSAGE = "We'll be in touch soon!"
|
||||
const COMBOBOX_REGIONS = [...DEMO_REQUEST_REGION_OPTIONS]
|
||||
const COMBOBOX_USER_COUNTS = [...DEMO_REQUEST_USER_COUNT_OPTIONS]
|
||||
const COMBOBOX_COMPANY_SIZES = [...DEMO_REQUEST_COMPANY_SIZE_OPTIONS]
|
||||
|
||||
const INITIAL_FORM_STATE: DemoRequestFormState = {
|
||||
firstName: '',
|
||||
@@ -50,7 +50,7 @@ const INITIAL_FORM_STATE: DemoRequestFormState = {
|
||||
companyEmail: '',
|
||||
phoneNumber: '',
|
||||
region: '',
|
||||
userCount: '',
|
||||
companySize: '',
|
||||
details: '',
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ export function DemoRequestModal({ children, theme = 'dark' }: DemoRequestModalP
|
||||
companyEmail: fieldErrors.companyEmail?.[0],
|
||||
phoneNumber: fieldErrors.phoneNumber?.[0],
|
||||
region: fieldErrors.region?.[0],
|
||||
userCount: fieldErrors.userCount?.[0],
|
||||
companySize: fieldErrors.companySize?.[0],
|
||||
details: fieldErrors.details?.[0],
|
||||
})
|
||||
return
|
||||
@@ -235,13 +235,13 @@ export function DemoRequestModal({ children, theme = 'dark' }: DemoRequestModalP
|
||||
filterOptions={false}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField htmlFor='userCount' label='Number of users' error={errors.userCount}>
|
||||
<FormField htmlFor='companySize' label='Company size' error={errors.companySize}>
|
||||
<Combobox
|
||||
options={COMBOBOX_USER_COUNTS}
|
||||
value={form.userCount}
|
||||
selectedValue={form.userCount}
|
||||
options={COMBOBOX_COMPANY_SIZES}
|
||||
value={form.companySize}
|
||||
selectedValue={form.companySize}
|
||||
onChange={(value) =>
|
||||
updateField('userCount', value as DemoRequestPayload['userCount'])
|
||||
updateField('companySize', value as DemoRequestPayload['companySize'])
|
||||
}
|
||||
placeholder='Select'
|
||||
editable={false}
|
||||
|
||||
@@ -81,6 +81,56 @@ function ProviderPreviewIcon({ providerId }: { providerId?: string }) {
|
||||
)
|
||||
}
|
||||
|
||||
interface FeatureToggleItemProps {
|
||||
feature: PermissionFeature
|
||||
enabled: boolean
|
||||
color: string
|
||||
isInView: boolean
|
||||
delay: number
|
||||
textClassName: string
|
||||
transition: Record<string, unknown>
|
||||
onToggle: () => void
|
||||
}
|
||||
|
||||
function FeatureToggleItem({
|
||||
feature,
|
||||
enabled,
|
||||
color,
|
||||
isInView,
|
||||
delay,
|
||||
textClassName,
|
||||
transition,
|
||||
onToggle,
|
||||
}: FeatureToggleItemProps) {
|
||||
return (
|
||||
<motion.div
|
||||
key={feature.key}
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
aria-label={`Toggle ${feature.name}`}
|
||||
aria-pressed={enabled}
|
||||
className='flex cursor-pointer items-center gap-2 rounded-[4px] py-0.5'
|
||||
initial={{ opacity: 0, x: -6 }}
|
||||
animate={isInView ? { opacity: 1, x: 0 } : {}}
|
||||
transition={{ ...transition, delay }}
|
||||
onClick={onToggle}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault()
|
||||
onToggle()
|
||||
}
|
||||
}}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<CheckboxIcon checked={enabled} color={color} />
|
||||
<ProviderPreviewIcon providerId={feature.providerId} />
|
||||
<span className={textClassName} style={{ color: enabled ? '#F6F6F6AA' : '#F6F6F640' }}>
|
||||
{feature.name}
|
||||
</span>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
export function AccessControlPanel() {
|
||||
const ref = useRef(null)
|
||||
const isInView = useInView(ref, { once: true, margin: '-40px' })
|
||||
@@ -97,39 +147,25 @@ export function AccessControlPanel() {
|
||||
|
||||
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]'>
|
||||
<span className='font-[430] font-season text-[#F6F6F6]/55 text-[10px] uppercase leading-none tracking-[0.08em]'>
|
||||
{category.label}
|
||||
</span>
|
||||
<div className='mt-2 grid grid-cols-2 gap-x-4 gap-y-2'>
|
||||
{category.features.map((feature, featIdx) => {
|
||||
const enabled = accessState[feature.key]
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={feature.key}
|
||||
className='flex cursor-pointer items-center gap-2 rounded-[4px] py-0.5'
|
||||
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>
|
||||
)
|
||||
})}
|
||||
{category.features.map((feature, featIdx) => (
|
||||
<FeatureToggleItem
|
||||
key={feature.key}
|
||||
feature={feature}
|
||||
enabled={accessState[feature.key]}
|
||||
color={category.color}
|
||||
isInView={isInView}
|
||||
delay={0.05 + (offsetBefore + featIdx) * 0.04}
|
||||
textClassName='truncate font-[430] font-season text-[13px] leading-none tracking-[0.02em]'
|
||||
transition={{ duration: 0.3 }}
|
||||
onToggle={() =>
|
||||
setAccessState((prev) => ({ ...prev, [feature.key]: !prev[feature.key] }))
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -140,12 +176,11 @@ export function AccessControlPanel() {
|
||||
<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]'>
|
||||
<span className='font-[430] font-season text-[#F6F6F6]/55 text-[10px] uppercase leading-none tracking-[0.08em]'>
|
||||
{category.label}
|
||||
</span>
|
||||
<div className='mt-2 grid grid-cols-2 gap-x-4 gap-y-2'>
|
||||
{category.features.map((feature, featIdx) => {
|
||||
const enabled = accessState[feature.key]
|
||||
const currentIndex =
|
||||
PERMISSION_CATEGORIES.slice(0, catIdx).reduce(
|
||||
(sum, c) => sum + c.features.length,
|
||||
@@ -153,30 +188,19 @@ export function AccessControlPanel() {
|
||||
) + featIdx
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
<FeatureToggleItem
|
||||
key={feature.key}
|
||||
className='flex cursor-pointer items-center gap-2 rounded-[4px] py-0.5'
|
||||
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={() =>
|
||||
feature={feature}
|
||||
enabled={accessState[feature.key]}
|
||||
color={category.color}
|
||||
isInView={isInView}
|
||||
delay={0.1 + currentIndex * 0.04}
|
||||
textClassName='truncate font-[430] font-season text-[11px] leading-none tracking-[0.02em] transition-opacity duration-200'
|
||||
transition={{ duration: 0.3, ease: [0.25, 0.46, 0.45, 0.94] }}
|
||||
onToggle={() =>
|
||||
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>
|
||||
|
||||
@@ -146,14 +146,14 @@ function AuditRow({ entry, index }: AuditRowProps) {
|
||||
</div>
|
||||
|
||||
{/* Time */}
|
||||
<span className='w-[56px] shrink-0 font-[430] font-season text-[#F6F6F6]/30 text-[11px] leading-none tracking-[0.02em]'>
|
||||
<span className='w-[56px] shrink-0 font-[430] font-season text-[#F6F6F6]/55 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]/60'> · </span>
|
||||
<span className='text-[#F6F6F6]/55'>{entry.description}</span>
|
||||
</span>
|
||||
</span>
|
||||
@@ -166,14 +166,14 @@ export 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) => ({
|
||||
const [entries, setEntries] = useState<LogEntry[]>(() => {
|
||||
const now = Date.now()
|
||||
return ENTRY_TEMPLATES.slice(0, 6).map((t, i) => ({
|
||||
...t,
|
||||
id: i,
|
||||
insertedAt: now - INITIAL_OFFSETS_MS[i],
|
||||
}))
|
||||
)
|
||||
})
|
||||
const [, tick] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -208,10 +208,9 @@ export function AuditLogPreview() {
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{
|
||||
layout: {
|
||||
type: 'spring',
|
||||
stiffness: 350,
|
||||
damping: 50,
|
||||
mass: 0.8,
|
||||
type: 'tween',
|
||||
duration: 0.32,
|
||||
ease: [0.25, 0.46, 0.45, 0.94],
|
||||
},
|
||||
y: { duration: 0.32, ease: [0.25, 0.46, 0.45, 0.94] },
|
||||
opacity: { duration: 0.25 },
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
* 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 cert (SOC 2) 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 SOC 2 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?".
|
||||
*/
|
||||
@@ -66,7 +66,7 @@ const FEATURE_TAGS = [
|
||||
function TrustStrip() {
|
||||
return (
|
||||
<div className='mx-6 mt-4 grid grid-cols-1 overflow-hidden rounded-lg border border-[var(--landing-bg-elevated)] sm:grid-cols-3 md:mx-8'>
|
||||
{/* SOC 2 + HIPAA combined */}
|
||||
{/* SOC 2 */}
|
||||
<Link
|
||||
href='https://app.vanta.com/sim.ai/trust/v35ia0jil4l7dteqjgaktn'
|
||||
target='_blank'
|
||||
@@ -83,10 +83,10 @@ function TrustStrip() {
|
||||
/>
|
||||
<div className='flex flex-col gap-[3px]'>
|
||||
<strong className='font-[430] font-season text-small text-white leading-none'>
|
||||
SOC 2 & HIPAA
|
||||
SOC 2
|
||||
</strong>
|
||||
<span className='font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-subtle)_30%,transparent)] text-xs leading-none tracking-[0.02em] transition-colors group-hover:text-[color-mix(in_srgb,var(--landing-text-subtle)_55%,transparent)]'>
|
||||
Type II · PHI protected →
|
||||
<span className='font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-subtle)_55%,transparent)] text-xs leading-none tracking-[0.02em] transition-colors group-hover:text-[color-mix(in_srgb,var(--landing-text-subtle)_75%,transparent)]'>
|
||||
Type II →
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
@@ -105,7 +105,7 @@ function TrustStrip() {
|
||||
<strong className='font-[430] font-season text-small text-white leading-none'>
|
||||
Open Source
|
||||
</strong>
|
||||
<span className='font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-subtle)_30%,transparent)] text-xs leading-none tracking-[0.02em] transition-colors group-hover:text-[color-mix(in_srgb,var(--landing-text-subtle)_55%,transparent)]'>
|
||||
<span className='font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-subtle)_55%,transparent)] text-xs leading-none tracking-[0.02em] transition-colors group-hover:text-[color-mix(in_srgb,var(--landing-text-subtle)_75%,transparent)]'>
|
||||
View on GitHub →
|
||||
</span>
|
||||
</div>
|
||||
@@ -120,7 +120,7 @@ function TrustStrip() {
|
||||
<strong className='font-[430] font-season text-small text-white leading-none'>
|
||||
SSO & SCIM
|
||||
</strong>
|
||||
<span className='font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-subtle)_30%,transparent)] text-xs leading-none tracking-[0.02em]'>
|
||||
<span className='font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-subtle)_55%,transparent)] text-xs leading-none tracking-[0.02em]'>
|
||||
Okta, Azure AD, Google
|
||||
</span>
|
||||
</div>
|
||||
@@ -165,7 +165,7 @@ export default function Enterprise() {
|
||||
<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]'>
|
||||
<p className='mt-2 max-w-[480px] font-[430] font-season text-[#F6F6F6]/70 text-[14px] leading-[150%] tracking-[0.02em]'>
|
||||
Every action is captured with full actor attribution.
|
||||
</p>
|
||||
</div>
|
||||
@@ -179,7 +179,7 @@ export default function Enterprise() {
|
||||
<h3 className='font-[430] font-season text-[16px] text-white leading-[120%] tracking-[-0.01em]'>
|
||||
Access Control
|
||||
</h3>
|
||||
<p className='mt-1.5 font-[430] font-season text-[#F6F6F6]/50 text-[14px] leading-[150%] tracking-[0.02em]'>
|
||||
<p className='mt-1.5 font-[430] font-season text-[#F6F6F6]/70 text-[14px] leading-[150%] tracking-[0.02em]'>
|
||||
Restrict providers, surfaces, and tools per group.
|
||||
</p>
|
||||
</div>
|
||||
@@ -211,7 +211,7 @@ export default function Enterprise() {
|
||||
(tag, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className='enterprise-feature-marquee-tag whitespace-nowrap border-[var(--landing-bg-elevated)] border-r px-5 py-4 font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-subtle)_40%,transparent)] text-small leading-none tracking-[0.02em] hover:bg-white/[0.04] hover:text-[color-mix(in_srgb,var(--landing-text-subtle)_55%,transparent)]'
|
||||
className='enterprise-feature-marquee-tag whitespace-nowrap border-[var(--landing-bg-elevated)] border-r px-5 py-4 font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-subtle)_60%,transparent)] text-small leading-none tracking-[0.02em] hover:bg-white/[0.04] hover:text-[color-mix(in_srgb,var(--landing-text-subtle)_80%,transparent)]'
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
@@ -221,7 +221,7 @@ export default function Enterprise() {
|
||||
</div>
|
||||
|
||||
<div className='flex items-center justify-between border-[var(--landing-bg-elevated)] border-t px-6 py-5 md:px-8 md:py-6'>
|
||||
<p className='font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-subtle)_40%,transparent)] text-base leading-[150%] tracking-[0.02em]'>
|
||||
<p className='font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-subtle)_60%,transparent)] text-base leading-[150%] tracking-[0.02em]'>
|
||||
Ready for growth?
|
||||
</p>
|
||||
<DemoRequestModal>
|
||||
|
||||
@@ -190,7 +190,6 @@ export default function Features() {
|
||||
width={1440}
|
||||
height={366}
|
||||
className='h-auto w-full'
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ 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'
|
||||
import { useAnimatedPlaceholder } from '@/hooks/use-animated-placeholder'
|
||||
|
||||
const MAX_HEIGHT = 120
|
||||
|
||||
@@ -67,6 +67,7 @@ export function FooterCTA() {
|
||||
type='button'
|
||||
onClick={handleSubmit}
|
||||
disabled={isEmpty}
|
||||
aria-label='Submit message'
|
||||
className='flex h-[28px] w-[28px] items-center justify-center rounded-full border-0 p-0 transition-colors'
|
||||
style={{
|
||||
background: isEmpty ? '#C0C0C0' : '#1C1C1C',
|
||||
|
||||
@@ -26,6 +26,9 @@ const RESOURCES_LINKS: FooterItem[] = [
|
||||
{ label: 'Blog', href: '/blog' },
|
||||
// { label: 'Templates', href: '/templates' },
|
||||
{ label: 'Docs', href: 'https://docs.sim.ai', external: true },
|
||||
{ label: 'Models', href: '/models' },
|
||||
// { label: 'Academy', href: '/academy' },
|
||||
{ label: 'Partners', href: '/partners' },
|
||||
{ label: 'Careers', href: 'https://jobs.ashbyhq.com/sim', external: true },
|
||||
{ label: 'Changelog', href: '/changelog' },
|
||||
]
|
||||
|
||||
@@ -42,7 +42,7 @@ export default function Hero() {
|
||||
1,000+ integrations and LLMs — including OpenAI, Claude, Gemini, Mistral, and xAI — to
|
||||
deploy and orchestrate agentic workflows. Create agents, workflows, knowledge bases, tables,
|
||||
and docs. Trusted by over 100,000 builders at startups and Fortune 500 companies. SOC2 and
|
||||
HIPAA compliant.
|
||||
SOC2 compliant.
|
||||
</p>
|
||||
|
||||
<div
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import { File } from '@/components/emcn/icons'
|
||||
import { DocxIcon, PdfIcon } from '@/components/icons/document-icons'
|
||||
import type {
|
||||
PreviewColumn,
|
||||
PreviewRow,
|
||||
} from '@/app/(home)/components/landing-preview/components/landing-preview-resource/landing-preview-resource'
|
||||
import {
|
||||
LandingPreviewResource,
|
||||
ownerCell,
|
||||
} from '@/app/(home)/components/landing-preview/components/landing-preview-resource/landing-preview-resource'
|
||||
|
||||
/** Generic audio/zip icon using basic SVG since no dedicated component exists */
|
||||
function AudioIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
viewBox='0 0 24 24'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
className={className}
|
||||
>
|
||||
<path d='M9 18V5l12-2v13' />
|
||||
<circle cx='6' cy='18' r='3' />
|
||||
<circle cx='18' cy='16' r='3' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function JsonlIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
viewBox='0 0 24 24'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
className={className}
|
||||
>
|
||||
<path d='M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z' />
|
||||
<path d='M14 2v4a2 2 0 0 0 2 2h4' />
|
||||
<path d='M10 9H8' />
|
||||
<path d='M16 13H8' />
|
||||
<path d='M16 17H8' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function ZipIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
viewBox='0 0 24 24'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
className={className}
|
||||
>
|
||||
<path d='M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z' />
|
||||
<path d='M14 2v4a2 2 0 0 0 2 2h4' />
|
||||
<path d='M10 6h1' />
|
||||
<path d='M10 10h1' />
|
||||
<path d='M10 14h1' />
|
||||
<path d='M9 18h2v2h-2z' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
const COLUMNS: PreviewColumn[] = [
|
||||
{ id: 'name', header: 'Name' },
|
||||
{ id: 'size', header: 'Size' },
|
||||
{ id: 'type', header: 'Type' },
|
||||
{ id: 'created', header: 'Created' },
|
||||
{ id: 'owner', header: 'Owner' },
|
||||
]
|
||||
|
||||
const ROWS: PreviewRow[] = [
|
||||
{
|
||||
id: '1',
|
||||
cells: {
|
||||
name: { icon: <PdfIcon className='h-[14px] w-[14px]' />, label: 'Q1 Performance Report.pdf' },
|
||||
size: { label: '2.4 MB' },
|
||||
type: { icon: <PdfIcon className='h-[14px] w-[14px]' />, label: 'PDF' },
|
||||
created: { label: '3 hours ago' },
|
||||
owner: ownerCell('T', 'Theo L.'),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
cells: {
|
||||
name: { icon: <ZipIcon className='h-[14px] w-[14px]' />, label: 'product-screenshots.zip' },
|
||||
size: { label: '18.7 MB' },
|
||||
type: { icon: <ZipIcon className='h-[14px] w-[14px]' />, label: 'ZIP' },
|
||||
created: { label: '1 day ago' },
|
||||
owner: ownerCell('A', 'Alex M.'),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
cells: {
|
||||
name: { icon: <JsonlIcon className='h-[14px] w-[14px]' />, label: 'training-dataset.jsonl' },
|
||||
size: { label: '892 KB' },
|
||||
type: { icon: <JsonlIcon className='h-[14px] w-[14px]' />, label: 'JSONL' },
|
||||
created: { label: '3 days ago' },
|
||||
owner: ownerCell('J', 'Jordan P.'),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
cells: {
|
||||
name: { icon: <PdfIcon className='h-[14px] w-[14px]' />, label: 'brand-guidelines.pdf' },
|
||||
size: { label: '5.1 MB' },
|
||||
type: { icon: <PdfIcon className='h-[14px] w-[14px]' />, label: 'PDF' },
|
||||
created: { label: '1 week ago' },
|
||||
owner: ownerCell('S', 'Sarah K.'),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
cells: {
|
||||
name: { icon: <AudioIcon className='h-[14px] w-[14px]' />, label: 'customer-interviews.mp3' },
|
||||
size: { label: '45.2 MB' },
|
||||
type: { icon: <AudioIcon className='h-[14px] w-[14px]' />, label: 'Audio' },
|
||||
created: { label: 'March 20th, 2026' },
|
||||
owner: ownerCell('V', 'Vik M.'),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
cells: {
|
||||
name: { icon: <DocxIcon className='h-[14px] w-[14px]' />, label: 'onboarding-playbook.docx' },
|
||||
size: { label: '1.1 MB' },
|
||||
type: { icon: <DocxIcon className='h-[14px] w-[14px]' />, label: 'DOCX' },
|
||||
created: { label: 'March 14th, 2026' },
|
||||
owner: ownerCell('S', 'Sarah K.'),
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* Static landing preview of the Files workspace page.
|
||||
*/
|
||||
export function LandingPreviewFiles() {
|
||||
return (
|
||||
<LandingPreviewResource
|
||||
icon={File}
|
||||
title='Files'
|
||||
createLabel='Upload file'
|
||||
searchPlaceholder='Search files...'
|
||||
columns={COLUMNS}
|
||||
rows={ROWS}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
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'
|
||||
import { useAnimatedPlaceholder } from '@/hooks/use-animated-placeholder'
|
||||
|
||||
const C = {
|
||||
SURFACE: '#292929',
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import { Database } from '@/components/emcn/icons'
|
||||
import {
|
||||
AirtableIcon,
|
||||
AsanaIcon,
|
||||
ConfluenceIcon,
|
||||
GoogleDocsIcon,
|
||||
GoogleDriveIcon,
|
||||
JiraIcon,
|
||||
SalesforceIcon,
|
||||
SlackIcon,
|
||||
ZendeskIcon,
|
||||
} from '@/components/icons'
|
||||
import type {
|
||||
PreviewColumn,
|
||||
PreviewRow,
|
||||
} from '@/app/(home)/components/landing-preview/components/landing-preview-resource/landing-preview-resource'
|
||||
import { LandingPreviewResource } from '@/app/(home)/components/landing-preview/components/landing-preview-resource/landing-preview-resource'
|
||||
|
||||
const DB_ICON = <Database className='h-[14px] w-[14px]' />
|
||||
|
||||
function connectorIcons(icons: React.ComponentType<{ className?: string }>[]) {
|
||||
return {
|
||||
content: (
|
||||
<div className='flex items-center gap-1'>
|
||||
{icons.map((Icon, i) => (
|
||||
<Icon key={i} className='h-3.5 w-3.5 flex-shrink-0' />
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
const COLUMNS: PreviewColumn[] = [
|
||||
{ id: 'name', header: 'Name' },
|
||||
{ id: 'documents', header: 'Documents' },
|
||||
{ id: 'tokens', header: 'Tokens' },
|
||||
{ id: 'connectors', header: 'Connectors' },
|
||||
{ id: 'created', header: 'Created' },
|
||||
]
|
||||
|
||||
const ROWS: PreviewRow[] = [
|
||||
{
|
||||
id: '1',
|
||||
cells: {
|
||||
name: { icon: DB_ICON, label: 'Product Documentation' },
|
||||
documents: { label: '847' },
|
||||
tokens: { label: '1,284,392' },
|
||||
connectors: connectorIcons([AsanaIcon, GoogleDocsIcon]),
|
||||
created: { label: '2 days ago' },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
cells: {
|
||||
name: { icon: DB_ICON, label: 'Customer Support KB' },
|
||||
documents: { label: '234' },
|
||||
tokens: { label: '892,104' },
|
||||
connectors: connectorIcons([ZendeskIcon, SlackIcon]),
|
||||
created: { label: '1 week ago' },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
cells: {
|
||||
name: { icon: DB_ICON, label: 'Engineering Wiki' },
|
||||
documents: { label: '1,203' },
|
||||
tokens: { label: '2,847,293' },
|
||||
connectors: connectorIcons([ConfluenceIcon, JiraIcon]),
|
||||
created: { label: 'March 12th, 2026' },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
cells: {
|
||||
name: { icon: DB_ICON, label: 'Marketing Assets' },
|
||||
documents: { label: '189' },
|
||||
tokens: { label: '634,821' },
|
||||
connectors: connectorIcons([GoogleDriveIcon, AirtableIcon]),
|
||||
created: { label: 'March 5th, 2026' },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
cells: {
|
||||
name: { icon: DB_ICON, label: 'Sales Playbook' },
|
||||
documents: { label: '92' },
|
||||
tokens: { label: '418,570' },
|
||||
connectors: connectorIcons([SalesforceIcon]),
|
||||
created: { label: 'February 28th, 2026' },
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export function LandingPreviewKnowledge() {
|
||||
return (
|
||||
<LandingPreviewResource
|
||||
icon={Database}
|
||||
title='Knowledge Base'
|
||||
createLabel='New base'
|
||||
searchPlaceholder='Search knowledge bases...'
|
||||
columns={COLUMNS}
|
||||
rows={ROWS}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
'use client'
|
||||
|
||||
import { useMemo, useState } from 'react'
|
||||
import { Download } from 'lucide-react'
|
||||
import { ArrowUpDown, Badge, Library, ListFilter, Search } from '@/components/emcn'
|
||||
import type { BadgeProps } from '@/components/emcn/components/badge/badge'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
|
||||
interface LogRow {
|
||||
id: string
|
||||
workflowName: string
|
||||
workflowColor: string
|
||||
date: string
|
||||
status: 'completed' | 'error' | 'running'
|
||||
cost: string
|
||||
trigger: 'webhook' | 'api' | 'schedule' | 'manual' | 'mcp' | 'chat'
|
||||
triggerLabel: string
|
||||
duration: string
|
||||
}
|
||||
|
||||
type BadgeVariant = BadgeProps['variant']
|
||||
|
||||
const STATUS_VARIANT: Record<LogRow['status'], BadgeVariant> = {
|
||||
completed: 'gray',
|
||||
error: 'red',
|
||||
running: 'amber',
|
||||
}
|
||||
|
||||
const STATUS_LABELS: Record<LogRow['status'], string> = {
|
||||
completed: 'Completed',
|
||||
error: 'Error',
|
||||
running: 'Running',
|
||||
}
|
||||
|
||||
const TRIGGER_VARIANT: Record<LogRow['trigger'], BadgeVariant> = {
|
||||
webhook: 'orange',
|
||||
api: 'blue',
|
||||
schedule: 'green',
|
||||
manual: 'gray-secondary',
|
||||
mcp: 'cyan',
|
||||
chat: 'purple',
|
||||
}
|
||||
|
||||
const MOCK_LOGS: LogRow[] = [
|
||||
{
|
||||
id: '1',
|
||||
workflowName: 'Customer Onboarding',
|
||||
workflowColor: '#4f8ef7',
|
||||
date: 'Apr 1 10:42 AM',
|
||||
status: 'running',
|
||||
cost: '-',
|
||||
trigger: 'webhook',
|
||||
triggerLabel: 'Webhook',
|
||||
duration: '-',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
workflowName: 'Lead Enrichment',
|
||||
workflowColor: '#33C482',
|
||||
date: 'Apr 1 09:15 AM',
|
||||
status: 'error',
|
||||
cost: '318 credits',
|
||||
trigger: 'api',
|
||||
triggerLabel: 'API',
|
||||
duration: '2.7s',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
workflowName: 'Email Campaign',
|
||||
workflowColor: '#a855f7',
|
||||
date: 'Apr 1 08:30 AM',
|
||||
status: 'completed',
|
||||
cost: '89 credits',
|
||||
trigger: 'schedule',
|
||||
triggerLabel: 'Schedule',
|
||||
duration: '0.8s',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
workflowName: 'Data Pipeline',
|
||||
workflowColor: '#f97316',
|
||||
date: 'Mar 31 10:14 PM',
|
||||
status: 'completed',
|
||||
cost: '241 credits',
|
||||
trigger: 'webhook',
|
||||
triggerLabel: 'Webhook',
|
||||
duration: '4.1s',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
workflowName: 'Invoice Processing',
|
||||
workflowColor: '#ec4899',
|
||||
date: 'Mar 31 08:45 PM',
|
||||
status: 'completed',
|
||||
cost: '112 credits',
|
||||
trigger: 'manual',
|
||||
triggerLabel: 'Manual',
|
||||
duration: '0.9s',
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
workflowName: 'Support Triage',
|
||||
workflowColor: '#0ea5e9',
|
||||
date: 'Mar 31 07:22 PM',
|
||||
status: 'completed',
|
||||
cost: '197 credits',
|
||||
trigger: 'api',
|
||||
triggerLabel: 'API',
|
||||
duration: '1.6s',
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
workflowName: 'Content Moderator',
|
||||
workflowColor: '#f59e0b',
|
||||
date: 'Mar 31 06:11 PM',
|
||||
status: 'error',
|
||||
cost: '284 credits',
|
||||
trigger: 'schedule',
|
||||
triggerLabel: 'Schedule',
|
||||
duration: '3.2s',
|
||||
},
|
||||
]
|
||||
|
||||
type SortKey = 'workflowName' | 'date' | 'status' | 'cost' | 'trigger' | 'duration'
|
||||
|
||||
const COL_HEADERS: { key: SortKey; label: string }[] = [
|
||||
{ key: 'workflowName', label: 'Workflow' },
|
||||
{ key: 'date', label: 'Date' },
|
||||
{ key: 'status', label: 'Status' },
|
||||
{ key: 'cost', label: 'Cost' },
|
||||
{ key: 'trigger', label: 'Trigger' },
|
||||
{ key: 'duration', label: 'Duration' },
|
||||
]
|
||||
|
||||
export function LandingPreviewLogs() {
|
||||
const [search, setSearch] = useState('')
|
||||
const [sortKey, setSortKey] = useState<SortKey | null>(null)
|
||||
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc')
|
||||
const [activeTab, setActiveTab] = useState<'logs' | 'dashboard'>('logs')
|
||||
|
||||
function handleSort(key: SortKey) {
|
||||
if (sortKey === key) {
|
||||
setSortDir((d) => (d === 'asc' ? 'desc' : 'asc'))
|
||||
} else {
|
||||
setSortKey(key)
|
||||
setSortDir('asc')
|
||||
}
|
||||
}
|
||||
|
||||
const sorted = useMemo(() => {
|
||||
const q = search.toLowerCase()
|
||||
const filtered = q
|
||||
? MOCK_LOGS.filter(
|
||||
(log) =>
|
||||
log.workflowName.toLowerCase().includes(q) ||
|
||||
log.triggerLabel.toLowerCase().includes(q) ||
|
||||
STATUS_LABELS[log.status].toLowerCase().includes(q)
|
||||
)
|
||||
: MOCK_LOGS
|
||||
|
||||
if (!sortKey) return filtered
|
||||
return [...filtered].sort((a, b) => {
|
||||
const av = sortKey === 'cost' ? a.cost.replace(/\D/g, '') : a[sortKey]
|
||||
const bv = sortKey === 'cost' ? b.cost.replace(/\D/g, '') : b[sortKey]
|
||||
const cmp = av.localeCompare(bv, undefined, { numeric: true, sensitivity: 'base' })
|
||||
return sortDir === 'asc' ? cmp : -cmp
|
||||
})
|
||||
}, [search, sortKey, sortDir])
|
||||
|
||||
return (
|
||||
<div className='flex h-full flex-1 flex-col overflow-hidden bg-[var(--bg)]'>
|
||||
{/* Header */}
|
||||
<div className='border-[var(--border)] border-b px-6 py-2.5'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<Library className='h-[14px] w-[14px] text-[var(--text-icon)]' />
|
||||
<h1 className='font-medium text-[var(--text-body)] text-sm'>Logs</h1>
|
||||
</div>
|
||||
<div className='flex items-center gap-1'>
|
||||
<div className='flex cursor-default items-center rounded-md px-2 py-1 text-[var(--text-secondary)] text-caption'>
|
||||
<Download className='mr-1.5 h-[14px] w-[14px] text-[var(--text-icon)]' />
|
||||
Export
|
||||
</div>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => setActiveTab('logs')}
|
||||
className='rounded-md px-2 py-1 text-caption transition-colors'
|
||||
style={{
|
||||
backgroundColor: activeTab === 'logs' ? 'var(--surface-active)' : 'transparent',
|
||||
color: activeTab === 'logs' ? 'var(--text-body)' : 'var(--text-secondary)',
|
||||
}}
|
||||
>
|
||||
Logs
|
||||
</button>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => setActiveTab('dashboard')}
|
||||
className='rounded-md px-2 py-1 text-caption transition-colors'
|
||||
style={{
|
||||
backgroundColor:
|
||||
activeTab === 'dashboard' ? 'var(--surface-active)' : 'transparent',
|
||||
color: activeTab === 'dashboard' ? 'var(--text-body)' : 'var(--text-secondary)',
|
||||
}}
|
||||
>
|
||||
Dashboard
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Options bar */}
|
||||
<div className='border-[var(--border)] border-b px-6 py-2.5'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex flex-1 items-center gap-2.5'>
|
||||
<Search className='h-[14px] w-[14px] flex-shrink-0 text-[var(--text-icon)]' />
|
||||
<input
|
||||
type='text'
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder='Search logs...'
|
||||
className='flex-1 bg-transparent text-[var(--text-body)] text-caption outline-none placeholder:text-[var(--text-subtle)]'
|
||||
/>
|
||||
</div>
|
||||
<div className='flex items-center gap-1.5'>
|
||||
<div className='flex cursor-default items-center rounded-md px-2 py-1 text-[var(--text-secondary)] text-caption'>
|
||||
<ListFilter className='mr-1.5 h-[14px] w-[14px] text-[var(--text-icon)]' />
|
||||
Filter
|
||||
</div>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => handleSort(sortKey ?? 'workflowName')}
|
||||
className='flex cursor-default items-center rounded-md px-2 py-1 text-[var(--text-secondary)] text-caption transition-colors hover-hover:bg-[var(--surface-3)]'
|
||||
>
|
||||
<ArrowUpDown className='mr-1.5 h-[14px] w-[14px] text-[var(--text-icon)]' />
|
||||
Sort
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table — uses <table> for pixel-perfect column alignment with headers */}
|
||||
<div className='min-h-0 flex-1 overflow-hidden'>
|
||||
<table className='w-full table-fixed text-sm'>
|
||||
<colgroup>
|
||||
<col style={{ width: '22%' }} />
|
||||
<col style={{ width: '18%' }} />
|
||||
<col style={{ width: '13%' }} />
|
||||
<col style={{ width: '15%' }} />
|
||||
<col style={{ width: '14%' }} />
|
||||
<col style={{ width: '18%' }} />
|
||||
</colgroup>
|
||||
<thead className='shadow-[inset_0_-1px_0_var(--border)]'>
|
||||
<tr>
|
||||
{COL_HEADERS.map(({ key, label }) => (
|
||||
<th
|
||||
key={key}
|
||||
className='h-10 px-6 py-1.5 text-left align-middle font-normal text-caption'
|
||||
>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => handleSort(key)}
|
||||
className={cn(
|
||||
'flex items-center gap-1 transition-colors hover-hover:text-[var(--text-secondary)]',
|
||||
sortKey === key ? 'text-[var(--text-secondary)]' : 'text-[var(--text-muted)]'
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
{sortKey === key && <ArrowUpDown className='h-[10px] w-[10px] opacity-60' />}
|
||||
</button>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sorted.map((log) => (
|
||||
<tr
|
||||
key={log.id}
|
||||
className='h-[44px] cursor-default transition-colors hover-hover:bg-[var(--surface-3)]'
|
||||
>
|
||||
<td className='px-6 align-middle'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div
|
||||
className='h-[10px] w-[10px] flex-shrink-0 rounded-[3px] border-[1.5px]'
|
||||
style={{
|
||||
backgroundColor: log.workflowColor,
|
||||
borderColor: `${log.workflowColor}60`,
|
||||
backgroundClip: 'padding-box',
|
||||
}}
|
||||
/>
|
||||
<span className='min-w-0 truncate font-medium text-[var(--text-primary)] text-caption'>
|
||||
{log.workflowName}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className='px-6 align-middle text-[var(--text-secondary)] text-caption'>
|
||||
{log.date}
|
||||
</td>
|
||||
<td className='px-6 align-middle'>
|
||||
<Badge variant={STATUS_VARIANT[log.status]} size='sm' dot>
|
||||
{STATUS_LABELS[log.status]}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className='px-6 align-middle text-[var(--text-secondary)] text-caption'>
|
||||
{log.cost}
|
||||
</td>
|
||||
<td className='px-6 align-middle'>
|
||||
<Badge variant={TRIGGER_VARIANT[log.trigger]} size='sm'>
|
||||
{log.triggerLabel}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className='px-6 align-middle text-[var(--text-secondary)] text-caption'>
|
||||
{log.duration}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
'use client'
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { ArrowUpDown, ListFilter, Plus, Search } from '@/components/emcn'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
|
||||
export interface PreviewColumn {
|
||||
id: string
|
||||
header: string
|
||||
width?: number
|
||||
}
|
||||
|
||||
export interface PreviewCell {
|
||||
icon?: ReactNode
|
||||
label?: string
|
||||
content?: ReactNode
|
||||
}
|
||||
|
||||
export interface PreviewRow {
|
||||
id: string
|
||||
cells: Record<string, PreviewCell>
|
||||
}
|
||||
|
||||
interface LandingPreviewResourceProps {
|
||||
icon: React.ComponentType<{ className?: string }>
|
||||
title: string
|
||||
createLabel: string
|
||||
searchPlaceholder: string
|
||||
columns: PreviewColumn[]
|
||||
rows: PreviewRow[]
|
||||
onRowClick?: (id: string) => void
|
||||
}
|
||||
|
||||
export function ownerCell(initial: string, name: string): PreviewCell {
|
||||
return {
|
||||
icon: (
|
||||
<span className='flex h-[14px] w-[14px] flex-shrink-0 items-center justify-center rounded-full border border-[var(--border)] bg-[var(--surface-3)] font-medium text-[8px] text-[var(--text-secondary)]'>
|
||||
{initial}
|
||||
</span>
|
||||
),
|
||||
label: name,
|
||||
}
|
||||
}
|
||||
|
||||
export function LandingPreviewResource({
|
||||
icon: Icon,
|
||||
title,
|
||||
createLabel,
|
||||
searchPlaceholder,
|
||||
columns,
|
||||
rows,
|
||||
onRowClick,
|
||||
}: LandingPreviewResourceProps) {
|
||||
const [search, setSearch] = useState('')
|
||||
const [sortColId, setSortColId] = useState<string | null>(null)
|
||||
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc')
|
||||
|
||||
function handleSortClick(colId: string) {
|
||||
if (sortColId === colId) {
|
||||
setSortDir((d) => (d === 'asc' ? 'desc' : 'asc'))
|
||||
} else {
|
||||
setSortColId(colId)
|
||||
setSortDir('asc')
|
||||
}
|
||||
}
|
||||
|
||||
const sorted = useMemo(() => {
|
||||
const q = search.toLowerCase()
|
||||
const filtered = q
|
||||
? rows.filter((row) =>
|
||||
Object.values(row.cells).some((cell) => cell.label?.toLowerCase().includes(q))
|
||||
)
|
||||
: rows
|
||||
|
||||
if (!sortColId) return filtered
|
||||
return [...filtered].sort((a, b) => {
|
||||
const av = a.cells[sortColId]?.label ?? ''
|
||||
const bv = b.cells[sortColId]?.label ?? ''
|
||||
const cmp = av.localeCompare(bv, undefined, { numeric: true, sensitivity: 'base' })
|
||||
return sortDir === 'asc' ? cmp : -cmp
|
||||
})
|
||||
}, [rows, search, sortColId, sortDir])
|
||||
|
||||
return (
|
||||
<div className='flex h-full flex-1 flex-col overflow-hidden bg-[var(--bg)]'>
|
||||
{/* Header */}
|
||||
<div className='border-[var(--border)] border-b px-6 py-2.5'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<Icon className='h-[14px] w-[14px] text-[var(--text-icon)]' />
|
||||
<h1 className='font-medium text-[var(--text-body)] text-sm'>{title}</h1>
|
||||
</div>
|
||||
<div className='flex cursor-default items-center rounded-md px-2 py-1 text-[var(--text-secondary)] text-caption'>
|
||||
<Plus className='mr-1.5 h-[14px] w-[14px] text-[var(--text-icon)]' />
|
||||
{createLabel}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Options bar */}
|
||||
<div className='border-[var(--border)] border-b px-6 py-2.5'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex flex-1 items-center gap-2.5'>
|
||||
<Search className='h-[14px] w-[14px] flex-shrink-0 text-[var(--text-icon)]' />
|
||||
<input
|
||||
type='text'
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder={searchPlaceholder}
|
||||
className='flex-1 bg-transparent text-[var(--text-body)] text-caption outline-none placeholder:text-[var(--text-subtle)]'
|
||||
/>
|
||||
</div>
|
||||
<div className='flex items-center gap-1.5'>
|
||||
<div className='flex cursor-default items-center rounded-md px-2 py-1 text-[var(--text-secondary)] text-caption'>
|
||||
<ListFilter className='mr-1.5 h-[14px] w-[14px] text-[var(--text-icon)]' />
|
||||
Filter
|
||||
</div>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => handleSortClick(sortColId ?? columns[0]?.id)}
|
||||
className='flex cursor-default items-center rounded-md px-2 py-1 text-[var(--text-secondary)] text-caption transition-colors hover-hover:bg-[var(--surface-3)]'
|
||||
>
|
||||
<ArrowUpDown className='mr-1.5 h-[14px] w-[14px] text-[var(--text-icon)]' />
|
||||
Sort
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className='min-h-0 flex-1 overflow-hidden'>
|
||||
<table className='w-full table-fixed text-sm'>
|
||||
<colgroup>
|
||||
{columns.map((col, i) => (
|
||||
<col
|
||||
key={col.id}
|
||||
style={i === 0 ? { minWidth: col.width ?? 200 } : { width: col.width ?? 160 }}
|
||||
/>
|
||||
))}
|
||||
</colgroup>
|
||||
<thead className='shadow-[inset_0_-1px_0_var(--border)]'>
|
||||
<tr>
|
||||
{columns.map((col) => (
|
||||
<th
|
||||
key={col.id}
|
||||
className='h-10 px-6 py-1.5 text-left align-middle font-normal text-caption'
|
||||
>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => handleSortClick(col.id)}
|
||||
className={cn(
|
||||
'flex items-center gap-1 transition-colors hover-hover:text-[var(--text-secondary)]',
|
||||
sortColId === col.id
|
||||
? 'text-[var(--text-secondary)]'
|
||||
: 'text-[var(--text-muted)]'
|
||||
)}
|
||||
>
|
||||
{col.header}
|
||||
{sortColId === col.id && (
|
||||
<ArrowUpDown className='h-[10px] w-[10px] opacity-60' />
|
||||
)}
|
||||
</button>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sorted.map((row) => (
|
||||
<tr
|
||||
key={row.id}
|
||||
onClick={() => onRowClick?.(row.id)}
|
||||
className={cn(
|
||||
'transition-colors hover-hover:bg-[var(--surface-3)]',
|
||||
onRowClick && 'cursor-pointer'
|
||||
)}
|
||||
>
|
||||
{columns.map((col, colIdx) => {
|
||||
const cell = row.cells[col.id]
|
||||
return (
|
||||
<td key={col.id} className='px-6 py-2.5 align-middle'>
|
||||
{cell?.content ? (
|
||||
cell.content
|
||||
) : (
|
||||
<span
|
||||
className={cn(
|
||||
'flex min-w-0 items-center gap-3 font-medium text-sm',
|
||||
colIdx === 0
|
||||
? 'text-[var(--text-body)]'
|
||||
: 'text-[var(--text-secondary)]'
|
||||
)}
|
||||
>
|
||||
{cell?.icon && (
|
||||
<span className='flex-shrink-0 text-[var(--text-icon)]'>
|
||||
{cell.icon}
|
||||
</span>
|
||||
)}
|
||||
<span className='truncate'>{cell?.label ?? '—'}</span>
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { Calendar } from '@/components/emcn/icons'
|
||||
import type {
|
||||
PreviewColumn,
|
||||
PreviewRow,
|
||||
} from '@/app/(home)/components/landing-preview/components/landing-preview-resource/landing-preview-resource'
|
||||
import { LandingPreviewResource } from '@/app/(home)/components/landing-preview/components/landing-preview-resource/landing-preview-resource'
|
||||
|
||||
const CAL_ICON = <Calendar className='h-[14px] w-[14px]' />
|
||||
|
||||
const COLUMNS: PreviewColumn[] = [
|
||||
{ id: 'task', header: 'Task' },
|
||||
{ id: 'schedule', header: 'Schedule', width: 240 },
|
||||
{ id: 'nextRun', header: 'Next Run' },
|
||||
{ id: 'lastRun', header: 'Last Run' },
|
||||
]
|
||||
|
||||
const ROWS: PreviewRow[] = [
|
||||
{
|
||||
id: '1',
|
||||
cells: {
|
||||
task: { icon: CAL_ICON, label: 'Sync CRM contacts' },
|
||||
schedule: { label: 'Recurring, every day at 9:00 AM' },
|
||||
nextRun: { label: 'Tomorrow' },
|
||||
lastRun: { label: '2 hours ago' },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
cells: {
|
||||
task: { icon: CAL_ICON, label: 'Generate weekly report' },
|
||||
schedule: { label: 'Recurring, every Monday at 8:00 AM' },
|
||||
nextRun: { label: 'In 5 days' },
|
||||
lastRun: { label: '6 days ago' },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
cells: {
|
||||
task: { icon: CAL_ICON, label: 'Clean up stale files' },
|
||||
schedule: { label: 'Recurring, every Sunday at midnight' },
|
||||
nextRun: { label: 'In 2 days' },
|
||||
lastRun: { label: '6 days ago' },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
cells: {
|
||||
task: { icon: CAL_ICON, label: 'Send performance digest' },
|
||||
schedule: { label: 'Recurring, every Friday at 5:00 PM' },
|
||||
nextRun: { label: 'In 3 days' },
|
||||
lastRun: { label: '3 days ago' },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
cells: {
|
||||
task: { icon: CAL_ICON, label: 'Backup production data' },
|
||||
schedule: { label: 'Recurring, every 4 hours' },
|
||||
nextRun: { label: 'In 2 hours' },
|
||||
lastRun: { label: '2 hours ago' },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
cells: {
|
||||
task: { icon: CAL_ICON, label: 'Scrape competitor pricing' },
|
||||
schedule: { label: 'Recurring, every Tuesday at 6:00 AM' },
|
||||
nextRun: { label: 'In 6 days' },
|
||||
lastRun: { label: '1 week ago' },
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* Static landing preview of the Scheduled Tasks workspace page.
|
||||
*/
|
||||
export function LandingPreviewScheduledTasks() {
|
||||
return (
|
||||
<LandingPreviewResource
|
||||
icon={Calendar}
|
||||
title='Scheduled Tasks'
|
||||
createLabel='New scheduled task'
|
||||
searchPlaceholder='Search scheduled tasks...'
|
||||
columns={COLUMNS}
|
||||
rows={ROWS}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -10,14 +10,25 @@ import {
|
||||
Settings,
|
||||
Table,
|
||||
} from '@/components/emcn/icons'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import type { PreviewWorkflow } from '@/app/(home)/components/landing-preview/components/landing-preview-workflow/workflow-data'
|
||||
|
||||
export type SidebarView =
|
||||
| 'home'
|
||||
| 'workflow'
|
||||
| 'tables'
|
||||
| 'files'
|
||||
| 'knowledge'
|
||||
| 'logs'
|
||||
| 'scheduled-tasks'
|
||||
|
||||
interface LandingPreviewSidebarProps {
|
||||
workflows: PreviewWorkflow[]
|
||||
activeWorkflowId: string
|
||||
activeView: 'home' | 'workflow'
|
||||
activeView: SidebarView
|
||||
onSelectWorkflow: (id: string) => void
|
||||
onSelectHome: () => void
|
||||
onSelectNav: (id: SidebarView) => void
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,7 +50,7 @@ const C = {
|
||||
const WORKSPACE_NAV = [
|
||||
{ id: 'tables', label: 'Tables', icon: Table },
|
||||
{ id: 'files', label: 'Files', icon: File },
|
||||
{ id: 'knowledge-base', label: 'Knowledge Base', icon: Database },
|
||||
{ id: 'knowledge', label: 'Knowledge Base', icon: Database },
|
||||
{ id: 'scheduled-tasks', label: 'Scheduled Tasks', icon: Calendar },
|
||||
{ id: 'logs', label: 'Logs', icon: Library },
|
||||
] as const
|
||||
@@ -49,20 +60,42 @@ const FOOTER_NAV = [
|
||||
{ id: 'settings', label: 'Settings', icon: Settings },
|
||||
] as const
|
||||
|
||||
function StaticNavItem({
|
||||
function NavItem({
|
||||
icon: Icon,
|
||||
label,
|
||||
isActive,
|
||||
onClick,
|
||||
}: {
|
||||
icon: React.ComponentType<{ className?: string; style?: React.CSSProperties }>
|
||||
label: string
|
||||
isActive?: boolean
|
||||
onClick?: () => void
|
||||
}) {
|
||||
if (!onClick) {
|
||||
return (
|
||||
<div className='pointer-events-none mx-0.5 flex h-[28px] items-center gap-2 rounded-[8px] px-2'>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='pointer-events-none mx-0.5 flex h-[28px] items-center gap-2 rounded-[8px] px-2'>
|
||||
<button
|
||||
type='button'
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
'mx-0.5 flex h-[28px] items-center gap-2 rounded-[8px] px-2 transition-colors hover-hover:bg-[var(--c-active)]',
|
||||
isActive && 'bg-[var(--c-active)]'
|
||||
)}
|
||||
>
|
||||
<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>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -77,13 +110,16 @@ export function LandingPreviewSidebar({
|
||||
activeView,
|
||||
onSelectWorkflow,
|
||||
onSelectHome,
|
||||
onSelectNav,
|
||||
}: LandingPreviewSidebarProps) {
|
||||
const isHomeActive = activeView === 'home'
|
||||
|
||||
return (
|
||||
<div
|
||||
className='flex h-full w-[248px] flex-shrink-0 flex-col pt-3'
|
||||
style={{ backgroundColor: C.SURFACE_1 }}
|
||||
style={
|
||||
{ backgroundColor: C.SURFACE_1, '--c-active': C.SURFACE_ACTIVE } as React.CSSProperties
|
||||
}
|
||||
>
|
||||
{/* Workspace Header */}
|
||||
<div className='flex-shrink-0 px-2.5'>
|
||||
@@ -116,21 +152,17 @@ export function LandingPreviewSidebar({
|
||||
<button
|
||||
type='button'
|
||||
onClick={onSelectHome}
|
||||
className='mx-0.5 flex h-[28px] items-center gap-2 rounded-[8px] px-2 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'
|
||||
}}
|
||||
className={cn(
|
||||
'mx-0.5 flex h-[28px] items-center gap-2 rounded-[8px] px-2 transition-colors hover-hover:bg-[var(--c-active)]',
|
||||
isHomeActive && 'bg-[var(--c-active)]'
|
||||
)}
|
||||
>
|
||||
<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' />
|
||||
<NavItem icon={Search} label='Search' />
|
||||
</div>
|
||||
|
||||
{/* Workspace */}
|
||||
@@ -142,7 +174,13 @@ export function LandingPreviewSidebar({
|
||||
</div>
|
||||
<div className='flex flex-col gap-0.5 px-2'>
|
||||
{WORKSPACE_NAV.map((item) => (
|
||||
<StaticNavItem key={item.id} icon={item.icon} label={item.label} />
|
||||
<NavItem
|
||||
key={item.id}
|
||||
icon={item.icon}
|
||||
label={item.label}
|
||||
isActive={activeView === item.id}
|
||||
onClick={() => onSelectNav(item.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -164,14 +202,10 @@ export function LandingPreviewSidebar({
|
||||
key={workflow.id}
|
||||
type='button'
|
||||
onClick={() => onSelectWorkflow(workflow.id)}
|
||||
className='group mx-0.5 flex h-[28px] w-full items-center gap-2 rounded-[8px] px-2 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'
|
||||
}}
|
||||
className={cn(
|
||||
'mx-0.5 flex h-[28px] w-full items-center gap-2 rounded-[8px] px-2 transition-colors hover-hover:bg-[#363636]',
|
||||
isActive && 'bg-[#363636]'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className='h-[14px] w-[14px] flex-shrink-0 rounded-[4px] border-[2.5px]'
|
||||
@@ -197,7 +231,7 @@ export function LandingPreviewSidebar({
|
||||
{/* Footer */}
|
||||
<div className='flex flex-shrink-0 flex-col gap-0.5 px-2 pt-[9px] pb-2'>
|
||||
{FOOTER_NAV.map((item) => (
|
||||
<StaticNavItem key={item.id} icon={item.icon} label={item.label} />
|
||||
<NavItem key={item.id} icon={item.icon} label={item.label} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,552 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Checkbox } from '@/components/emcn'
|
||||
import {
|
||||
ChevronDown,
|
||||
Columns3,
|
||||
Rows3,
|
||||
Table,
|
||||
TypeBoolean,
|
||||
TypeNumber,
|
||||
TypeText,
|
||||
} from '@/components/emcn/icons'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import type {
|
||||
PreviewColumn,
|
||||
PreviewRow,
|
||||
} from '@/app/(home)/components/landing-preview/components/landing-preview-resource/landing-preview-resource'
|
||||
import {
|
||||
LandingPreviewResource,
|
||||
ownerCell,
|
||||
} from '@/app/(home)/components/landing-preview/components/landing-preview-resource/landing-preview-resource'
|
||||
|
||||
const CELL = 'border-[var(--border)] border-r border-b px-2 py-[7px] align-middle select-none'
|
||||
const CELL_CHECKBOX =
|
||||
'border-[var(--border)] border-r border-b px-1 py-[7px] align-middle select-none'
|
||||
const CELL_HEADER =
|
||||
'border-[var(--border)] border-r border-b bg-[var(--bg)] p-0 text-left align-middle'
|
||||
const CELL_HEADER_CHECKBOX =
|
||||
'border-[var(--border)] border-r border-b bg-[var(--bg)] px-1 py-[7px] text-center align-middle'
|
||||
const CELL_CONTENT =
|
||||
'relative min-h-[20px] min-w-0 overflow-clip text-ellipsis whitespace-nowrap text-small'
|
||||
const SELECTION_OVERLAY =
|
||||
'pointer-events-none absolute -top-px -right-px -bottom-px -left-px z-[5] border-[2px] border-[var(--selection)]'
|
||||
|
||||
const LIST_COLUMNS: PreviewColumn[] = [
|
||||
{ id: 'name', header: 'Name' },
|
||||
{ id: 'columns', header: 'Columns' },
|
||||
{ id: 'rows', header: 'Rows' },
|
||||
{ id: 'created', header: 'Created' },
|
||||
{ id: 'owner', header: 'Owner' },
|
||||
]
|
||||
|
||||
const TABLE_METAS: Record<string, string> = {
|
||||
'1': 'Customer Leads',
|
||||
'2': 'Product Catalog',
|
||||
'3': 'Campaign Analytics',
|
||||
'4': 'User Profiles',
|
||||
'5': 'Invoice Records',
|
||||
}
|
||||
|
||||
const TABLE_ICON = <Table className='h-[14px] w-[14px]' />
|
||||
const COLUMNS_ICON = <Columns3 className='h-[14px] w-[14px]' />
|
||||
const ROWS_ICON = <Rows3 className='h-[14px] w-[14px]' />
|
||||
|
||||
const LIST_ROWS: PreviewRow[] = [
|
||||
{
|
||||
id: '1',
|
||||
cells: {
|
||||
name: { icon: TABLE_ICON, label: 'Customer Leads' },
|
||||
columns: { icon: COLUMNS_ICON, label: '8' },
|
||||
rows: { icon: ROWS_ICON, label: '2,847' },
|
||||
created: { label: '2 days ago' },
|
||||
owner: ownerCell('S', 'Sarah K.'),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
cells: {
|
||||
name: { icon: TABLE_ICON, label: 'Product Catalog' },
|
||||
columns: { icon: COLUMNS_ICON, label: '12' },
|
||||
rows: { icon: ROWS_ICON, label: '1,203' },
|
||||
created: { label: '5 days ago' },
|
||||
owner: ownerCell('A', 'Alex M.'),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
cells: {
|
||||
name: { icon: TABLE_ICON, label: 'Campaign Analytics' },
|
||||
columns: { icon: COLUMNS_ICON, label: '6' },
|
||||
rows: { icon: ROWS_ICON, label: '534' },
|
||||
created: { label: '1 week ago' },
|
||||
owner: ownerCell('W', 'Emaan K.'),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
cells: {
|
||||
name: { icon: TABLE_ICON, label: 'User Profiles' },
|
||||
columns: { icon: COLUMNS_ICON, label: '15' },
|
||||
rows: { icon: ROWS_ICON, label: '18,492' },
|
||||
created: { label: '2 weeks ago' },
|
||||
owner: ownerCell('J', 'Jordan P.'),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
cells: {
|
||||
name: { icon: TABLE_ICON, label: 'Invoice Records' },
|
||||
columns: { icon: COLUMNS_ICON, label: '9' },
|
||||
rows: { icon: ROWS_ICON, label: '742' },
|
||||
created: { label: 'March 15th, 2026' },
|
||||
owner: ownerCell('S', 'Sarah K.'),
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
interface SpreadsheetColumn {
|
||||
id: string
|
||||
label: string
|
||||
type: 'text' | 'number' | 'boolean'
|
||||
width: number
|
||||
}
|
||||
|
||||
interface SpreadsheetRow {
|
||||
id: string
|
||||
cells: Record<string, string>
|
||||
}
|
||||
|
||||
const COLUMN_TYPE_ICONS = {
|
||||
text: TypeText,
|
||||
number: TypeNumber,
|
||||
boolean: TypeBoolean,
|
||||
} as const
|
||||
|
||||
const SPREADSHEET_DATA: Record<string, { columns: SpreadsheetColumn[]; rows: SpreadsheetRow[] }> = {
|
||||
'1': {
|
||||
columns: [
|
||||
{ id: 'name', label: 'Name', type: 'text', width: 160 },
|
||||
{ id: 'email', label: 'Email', type: 'text', width: 200 },
|
||||
{ id: 'company', label: 'Company', type: 'text', width: 160 },
|
||||
{ id: 'score', label: 'Score', type: 'number', width: 100 },
|
||||
{ id: 'qualified', label: 'Qualified', type: 'boolean', width: 120 },
|
||||
],
|
||||
rows: [
|
||||
{
|
||||
id: '1',
|
||||
cells: {
|
||||
name: 'Alice Johnson',
|
||||
email: 'alice@acme.com',
|
||||
company: 'Acme Corp',
|
||||
score: '87',
|
||||
qualified: 'true',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
cells: {
|
||||
name: 'Bob Williams',
|
||||
email: 'bob@techco.io',
|
||||
company: 'TechCo',
|
||||
score: '62',
|
||||
qualified: 'false',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
cells: {
|
||||
name: 'Carol Davis',
|
||||
email: 'carol@startup.co',
|
||||
company: 'StartupCo',
|
||||
score: '94',
|
||||
qualified: 'true',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
cells: {
|
||||
name: 'Dan Miller',
|
||||
email: 'dan@bigcorp.com',
|
||||
company: 'BigCorp',
|
||||
score: '71',
|
||||
qualified: 'true',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
cells: {
|
||||
name: 'Eva Chen',
|
||||
email: 'eva@design.io',
|
||||
company: 'Design IO',
|
||||
score: '45',
|
||||
qualified: 'false',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
cells: {
|
||||
name: 'Frank Lee',
|
||||
email: 'frank@ventures.co',
|
||||
company: 'Ventures',
|
||||
score: '88',
|
||||
qualified: 'true',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
'2': {
|
||||
columns: [
|
||||
{ id: 'sku', label: 'SKU', type: 'text', width: 120 },
|
||||
{ id: 'name', label: 'Product Name', type: 'text', width: 200 },
|
||||
{ id: 'price', label: 'Price', type: 'number', width: 100 },
|
||||
{ id: 'stock', label: 'In Stock', type: 'number', width: 120 },
|
||||
{ id: 'active', label: 'Active', type: 'boolean', width: 90 },
|
||||
],
|
||||
rows: [
|
||||
{
|
||||
id: '1',
|
||||
cells: {
|
||||
sku: 'PRD-001',
|
||||
name: 'Wireless Headphones',
|
||||
price: '79.99',
|
||||
stock: '234',
|
||||
active: 'true',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
cells: { sku: 'PRD-002', name: 'USB-C Hub', price: '49.99', stock: '89', active: 'true' },
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
cells: {
|
||||
sku: 'PRD-003',
|
||||
name: 'Laptop Stand',
|
||||
price: '39.99',
|
||||
stock: '0',
|
||||
active: 'false',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
cells: {
|
||||
sku: 'PRD-004',
|
||||
name: 'Mechanical Keyboard',
|
||||
price: '129.99',
|
||||
stock: '52',
|
||||
active: 'true',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
cells: { sku: 'PRD-005', name: 'Webcam HD', price: '89.99', stock: '17', active: 'true' },
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
cells: {
|
||||
sku: 'PRD-006',
|
||||
name: 'Mouse Pad XL',
|
||||
price: '24.99',
|
||||
stock: '0',
|
||||
active: 'false',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
'3': {
|
||||
columns: [
|
||||
{ id: 'campaign', label: 'Campaign', type: 'text', width: 180 },
|
||||
{ id: 'clicks', label: 'Clicks', type: 'number', width: 100 },
|
||||
{ id: 'conversions', label: 'Conversions', type: 'number', width: 140 },
|
||||
{ id: 'spend', label: 'Spend ($)', type: 'number', width: 130 },
|
||||
{ id: 'active', label: 'Active', type: 'boolean', width: 90 },
|
||||
],
|
||||
rows: [
|
||||
{
|
||||
id: '1',
|
||||
cells: {
|
||||
campaign: 'Spring Sale 2026',
|
||||
clicks: '12,847',
|
||||
conversions: '384',
|
||||
spend: '2,400',
|
||||
active: 'true',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
cells: {
|
||||
campaign: 'Email Reactivation',
|
||||
clicks: '3,201',
|
||||
conversions: '97',
|
||||
spend: '450',
|
||||
active: 'false',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
cells: {
|
||||
campaign: 'Referral Program',
|
||||
clicks: '8,923',
|
||||
conversions: '210',
|
||||
spend: '1,100',
|
||||
active: 'true',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
cells: {
|
||||
campaign: 'Product Launch',
|
||||
clicks: '24,503',
|
||||
conversions: '891',
|
||||
spend: '5,800',
|
||||
active: 'true',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
cells: {
|
||||
campaign: 'Retargeting Q1',
|
||||
clicks: '6,712',
|
||||
conversions: '143',
|
||||
spend: '980',
|
||||
active: 'false',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
'4': {
|
||||
columns: [
|
||||
{ id: 'username', label: 'Username', type: 'text', width: 140 },
|
||||
{ id: 'email', label: 'Email', type: 'text', width: 200 },
|
||||
{ id: 'plan', label: 'Plan', type: 'text', width: 120 },
|
||||
{ id: 'seats', label: 'Seats', type: 'number', width: 100 },
|
||||
{ id: 'active', label: 'Active', type: 'boolean', width: 100 },
|
||||
],
|
||||
rows: [
|
||||
{
|
||||
id: '1',
|
||||
cells: {
|
||||
username: 'alice_j',
|
||||
email: 'alice@acme.com',
|
||||
plan: 'Pro',
|
||||
seats: '5',
|
||||
active: 'true',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
cells: {
|
||||
username: 'bobw',
|
||||
email: 'bob@techco.io',
|
||||
plan: 'Starter',
|
||||
seats: '1',
|
||||
active: 'true',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
cells: {
|
||||
username: 'carol_d',
|
||||
email: 'carol@startup.co',
|
||||
plan: 'Enterprise',
|
||||
seats: '25',
|
||||
active: 'true',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
cells: {
|
||||
username: 'dan.m',
|
||||
email: 'dan@bigcorp.com',
|
||||
plan: 'Pro',
|
||||
seats: '10',
|
||||
active: 'false',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
cells: {
|
||||
username: 'eva_chen',
|
||||
email: 'eva@design.io',
|
||||
plan: 'Starter',
|
||||
seats: '1',
|
||||
active: 'true',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
cells: {
|
||||
username: 'frank_lee',
|
||||
email: 'frank@ventures.co',
|
||||
plan: 'Enterprise',
|
||||
seats: '50',
|
||||
active: 'true',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
'5': {
|
||||
columns: [
|
||||
{ id: 'invoice', label: 'Invoice #', type: 'text', width: 140 },
|
||||
{ id: 'client', label: 'Client', type: 'text', width: 160 },
|
||||
{ id: 'amount', label: 'Amount ($)', type: 'number', width: 130 },
|
||||
{ id: 'paid', label: 'Paid', type: 'boolean', width: 80 },
|
||||
],
|
||||
rows: [
|
||||
{
|
||||
id: '1',
|
||||
cells: { invoice: 'INV-2026-001', client: 'Acme Corp', amount: '4,800.00', paid: 'true' },
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
cells: { invoice: 'INV-2026-002', client: 'TechCo', amount: '1,200.00', paid: 'true' },
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
cells: { invoice: 'INV-2026-003', client: 'StartupCo', amount: '750.00', paid: 'false' },
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
cells: { invoice: 'INV-2026-004', client: 'BigCorp', amount: '12,500.00', paid: 'true' },
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
cells: { invoice: 'INV-2026-005', client: 'Design IO', amount: '3,300.00', paid: 'false' },
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
interface SpreadsheetViewProps {
|
||||
tableId: string
|
||||
tableName: string
|
||||
onBack: () => void
|
||||
}
|
||||
|
||||
function SpreadsheetView({ tableId, tableName, onBack }: SpreadsheetViewProps) {
|
||||
const data = SPREADSHEET_DATA[tableId] ?? SPREADSHEET_DATA['1']
|
||||
const [selectedCell, setSelectedCell] = useState<{ row: string; col: string } | null>(null)
|
||||
|
||||
return (
|
||||
<div className='flex h-full flex-1 flex-col overflow-hidden bg-[var(--bg)]'>
|
||||
{/* Breadcrumb header — matches real ResourceHeader breadcrumb layout */}
|
||||
<div className='border-[var(--border)] border-b px-4 py-[8.5px]'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<button
|
||||
type='button'
|
||||
onClick={onBack}
|
||||
className='inline-flex items-center px-2 py-1 font-medium text-[var(--text-secondary)] text-sm transition-colors hover-hover:text-[var(--text-body)]'
|
||||
>
|
||||
<Table className='mr-3 h-[14px] w-[14px] text-[var(--text-icon)]' />
|
||||
Tables
|
||||
</button>
|
||||
<span className='select-none text-[var(--text-icon)] text-sm'>/</span>
|
||||
<span className='inline-flex items-center px-2 py-1 font-medium text-[var(--text-body)] text-sm'>
|
||||
{tableName}
|
||||
<ChevronDown className='ml-2 h-[7px] w-[9px] shrink-0 text-[var(--text-muted)]' />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Spreadsheet — matches exact real table editor structure */}
|
||||
<div className='min-h-0 flex-1 overflow-auto overscroll-none'>
|
||||
<table className='table-fixed border-separate border-spacing-0 text-small'>
|
||||
<colgroup>
|
||||
<col style={{ width: 40 }} />
|
||||
{data.columns.map((col) => (
|
||||
<col key={col.id} style={{ width: col.width }} />
|
||||
))}
|
||||
</colgroup>
|
||||
<thead className='sticky top-0 z-10'>
|
||||
<tr>
|
||||
<th className={CELL_HEADER_CHECKBOX} />
|
||||
{data.columns.map((col) => {
|
||||
const Icon = COLUMN_TYPE_ICONS[col.type] ?? TypeText
|
||||
return (
|
||||
<th key={col.id} className={CELL_HEADER}>
|
||||
<div className='flex h-full w-full min-w-0 items-center px-2 py-[7px]'>
|
||||
<Icon className='h-3 w-3 shrink-0 text-[var(--text-icon)]' />
|
||||
<span className='ml-1.5 min-w-0 overflow-clip text-ellipsis whitespace-nowrap font-medium text-[var(--text-primary)] text-small'>
|
||||
{col.label}
|
||||
</span>
|
||||
<ChevronDown className='ml-auto h-[7px] w-[9px] shrink-0 text-[var(--text-muted)]' />
|
||||
</div>
|
||||
</th>
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.rows.map((row, rowIdx) => (
|
||||
<tr key={row.id}>
|
||||
<td className={cn(CELL_CHECKBOX, 'text-center')}>
|
||||
<span className='text-[var(--text-tertiary)] text-xs tabular-nums'>
|
||||
{rowIdx + 1}
|
||||
</span>
|
||||
</td>
|
||||
{data.columns.map((col) => {
|
||||
const isSelected = selectedCell?.row === row.id && selectedCell?.col === col.id
|
||||
const cellValue = row.cells[col.id] ?? ''
|
||||
return (
|
||||
<td
|
||||
key={col.id}
|
||||
onClick={() => setSelectedCell({ row: row.id, col: col.id })}
|
||||
className={cn(
|
||||
CELL,
|
||||
'relative cursor-default text-[var(--text-body)]',
|
||||
isSelected && 'bg-[rgba(37,99,235,0.06)]'
|
||||
)}
|
||||
>
|
||||
{isSelected && <div className={SELECTION_OVERLAY} />}
|
||||
<div className={CELL_CONTENT}>
|
||||
{col.type === 'boolean' ? (
|
||||
<div className='flex min-h-[20px] items-center justify-center'>
|
||||
<Checkbox
|
||||
size='sm'
|
||||
checked={cellValue === 'true'}
|
||||
className='pointer-events-none'
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
cellValue
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function LandingPreviewTables() {
|
||||
const [openTableId, setOpenTableId] = useState<string | null>(null)
|
||||
|
||||
if (openTableId !== null) {
|
||||
return (
|
||||
<SpreadsheetView
|
||||
tableId={openTableId}
|
||||
tableName={TABLE_METAS[openTableId] ?? 'Table'}
|
||||
onBack={() => setOpenTableId(null)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<LandingPreviewResource
|
||||
icon={Table}
|
||||
title='Tables'
|
||||
createLabel='New table'
|
||||
searchPlaceholder='Search tables...'
|
||||
columns={LIST_COLUMNS}
|
||||
rows={LIST_ROWS}
|
||||
onRowClick={(id) => setOpenTableId(id)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -228,13 +228,13 @@ export const PreviewBlockNode = memo(function PreviewBlockNode({
|
||||
{tools && tools.length > 0 && (
|
||||
<div className='flex items-center gap-2'>
|
||||
<span className='flex-shrink-0 font-normal text-[#b3b3b3] text-[14px]'>Tools</span>
|
||||
<div className='flex flex-1 flex-wrap items-center justify-end gap-2'>
|
||||
<div className='flex flex-1 flex-wrap items-center justify-end gap-[5px]'>
|
||||
{tools.map((tool) => {
|
||||
const ToolIcon = BLOCK_ICONS[tool.type]
|
||||
return (
|
||||
<div
|
||||
key={tool.type}
|
||||
className='flex items-center gap-2 rounded-[5px] border border-[#3d3d3d] bg-[#2a2a2a] px-2 py-1'
|
||||
className='flex items-center gap-[5px] rounded-[5px] border border-[#3d3d3d] bg-[#2a2a2a] px-[6px] py-[3px]'
|
||||
>
|
||||
<div
|
||||
className='flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center rounded-[4px]'
|
||||
|
||||
@@ -127,6 +127,60 @@ const SELF_HEALING_CRM_WORKFLOW: PreviewWorkflow = {
|
||||
edges: [{ id: 'e-3', source: 'schedule-1', target: 'mothership-1' }],
|
||||
}
|
||||
|
||||
/**
|
||||
* Customer Support Agent workflow — Gmail Trigger -> Agent (KB + Notion tools) -> Slack
|
||||
*/
|
||||
const CUSTOMER_SUPPORT_WORKFLOW: PreviewWorkflow = {
|
||||
id: 'wf-customer-support',
|
||||
name: 'Customer Support Agent',
|
||||
color: '#0EA5E9',
|
||||
blocks: [
|
||||
{
|
||||
id: 'gmail-1',
|
||||
name: 'Gmail',
|
||||
type: 'gmail',
|
||||
bgColor: '#E0E0E0',
|
||||
rows: [
|
||||
{ title: 'Event', value: 'New Email' },
|
||||
{ title: 'Label', value: 'Support' },
|
||||
],
|
||||
position: { x: 80, y: 140 },
|
||||
hideTargetHandle: true,
|
||||
},
|
||||
{
|
||||
id: 'agent-3',
|
||||
name: 'Support Agent',
|
||||
type: 'agent',
|
||||
bgColor: '#701ffc',
|
||||
rows: [
|
||||
{ title: 'Model', value: 'gpt-5.4' },
|
||||
{ title: 'System Prompt', value: 'Resolve customer issues...' },
|
||||
],
|
||||
tools: [
|
||||
{ name: 'Knowledge', type: 'knowledge_base', bgColor: '#10B981' },
|
||||
{ name: 'Notion', type: 'notion', bgColor: '#181C1E' },
|
||||
],
|
||||
position: { x: 420, y: 40 },
|
||||
},
|
||||
{
|
||||
id: 'slack-3',
|
||||
name: 'Slack',
|
||||
type: 'slack',
|
||||
bgColor: '#611f69',
|
||||
rows: [
|
||||
{ title: 'Channel', value: '#support' },
|
||||
{ title: 'Operation', value: 'Send Message' },
|
||||
],
|
||||
position: { x: 420, y: 260 },
|
||||
hideSourceHandle: true,
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{ id: 'e-cs-1', source: 'gmail-1', target: 'agent-3' },
|
||||
{ id: 'e-cs-2', source: 'gmail-1', target: 'slack-3' },
|
||||
],
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty "New Agent" workflow — a single note prompting the user to start building
|
||||
*/
|
||||
@@ -153,6 +207,7 @@ const NEW_AGENT_WORKFLOW: PreviewWorkflow = {
|
||||
export const PREVIEW_WORKFLOWS: PreviewWorkflow[] = [
|
||||
SELF_HEALING_CRM_WORKFLOW,
|
||||
IT_SERVICE_WORKFLOW,
|
||||
CUSTOMER_SUPPORT_WORKFLOW,
|
||||
NEW_AGENT_WORKFLOW,
|
||||
]
|
||||
|
||||
|
||||
@@ -2,9 +2,15 @@
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { motion, type Variants } from 'framer-motion'
|
||||
import { LandingPreviewFiles } from '@/app/(home)/components/landing-preview/components/landing-preview-files/landing-preview-files'
|
||||
import { LandingPreviewHome } from '@/app/(home)/components/landing-preview/components/landing-preview-home/landing-preview-home'
|
||||
import { LandingPreviewKnowledge } from '@/app/(home)/components/landing-preview/components/landing-preview-knowledge/landing-preview-knowledge'
|
||||
import { LandingPreviewLogs } from '@/app/(home)/components/landing-preview/components/landing-preview-logs/landing-preview-logs'
|
||||
import { LandingPreviewPanel } from '@/app/(home)/components/landing-preview/components/landing-preview-panel/landing-preview-panel'
|
||||
import { LandingPreviewScheduledTasks } from '@/app/(home)/components/landing-preview/components/landing-preview-scheduled-tasks/landing-preview-scheduled-tasks'
|
||||
import type { SidebarView } from '@/app/(home)/components/landing-preview/components/landing-preview-sidebar/landing-preview-sidebar'
|
||||
import { LandingPreviewSidebar } from '@/app/(home)/components/landing-preview/components/landing-preview-sidebar/landing-preview-sidebar'
|
||||
import { LandingPreviewTables } from '@/app/(home)/components/landing-preview/components/landing-preview-tables/landing-preview-tables'
|
||||
import { LandingPreviewWorkflow } from '@/app/(home)/components/landing-preview/components/landing-preview-workflow/landing-preview-workflow'
|
||||
import {
|
||||
EASE_OUT,
|
||||
@@ -46,18 +52,16 @@ const panelVariants: Variants = {
|
||||
* Interactive workspace preview for the hero section.
|
||||
*
|
||||
* Renders a lightweight replica of the Sim workspace with:
|
||||
* - A sidebar with two selectable workflows
|
||||
* - A sidebar with selectable workflows and workspace nav items
|
||||
* - A ReactFlow canvas showing the active workflow's blocks and edges
|
||||
* - Static previews of Tables, Files, Knowledge Base, Logs, and Scheduled Tasks
|
||||
* - A panel with a functional copilot input (stores prompt + redirects to /signup)
|
||||
*
|
||||
* Everything except the workflow items and the copilot input is non-interactive.
|
||||
* On mount the sidebar slides from left and the panel from right. The canvas
|
||||
* background stays fully opaque; individual block nodes animate in with a
|
||||
* staggered fade. Edges draw left-to-right. Animations only fire on initial
|
||||
* load — workflow switches render instantly.
|
||||
* Only workflow items, the home button, workspace nav items, and the copilot input
|
||||
* are interactive. Animations only fire on initial load.
|
||||
*/
|
||||
export function LandingPreview() {
|
||||
const [activeView, setActiveView] = useState<'home' | 'workflow'>('workflow')
|
||||
const [activeView, setActiveView] = useState<SidebarView>('workflow')
|
||||
const [activeWorkflowId, setActiveWorkflowId] = useState(PREVIEW_WORKFLOWS[0].id)
|
||||
const isInitialMount = useRef(true)
|
||||
|
||||
@@ -74,11 +78,34 @@ export function LandingPreview() {
|
||||
setActiveView('home')
|
||||
}, [])
|
||||
|
||||
const handleSelectNav = useCallback((id: SidebarView) => {
|
||||
setActiveView(id)
|
||||
}, [])
|
||||
|
||||
const activeWorkflow =
|
||||
PREVIEW_WORKFLOWS.find((w) => w.id === activeWorkflowId) ?? PREVIEW_WORKFLOWS[0]
|
||||
|
||||
const isWorkflowView = activeView === 'workflow'
|
||||
|
||||
function renderContent() {
|
||||
switch (activeView) {
|
||||
case 'workflow':
|
||||
return <LandingPreviewWorkflow workflow={activeWorkflow} animate={isInitialMount.current} />
|
||||
case 'home':
|
||||
return <LandingPreviewHome />
|
||||
case 'tables':
|
||||
return <LandingPreviewTables />
|
||||
case 'files':
|
||||
return <LandingPreviewFiles />
|
||||
case 'knowledge':
|
||||
return <LandingPreviewKnowledge />
|
||||
case 'logs':
|
||||
return <LandingPreviewLogs />
|
||||
case 'scheduled-tasks':
|
||||
return <LandingPreviewScheduledTasks />
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className='dark flex aspect-[1116/549] w-full overflow-hidden rounded bg-[var(--landing-bg-surface)] antialiased'
|
||||
@@ -93,6 +120,7 @@ export function LandingPreview() {
|
||||
activeView={activeView}
|
||||
onSelectWorkflow={handleSelectWorkflow}
|
||||
onSelectHome={handleSelectHome}
|
||||
onSelectNav={handleSelectNav}
|
||||
/>
|
||||
</motion.div>
|
||||
<div className='flex min-w-0 flex-1 flex-col py-2 pr-2 pl-2 lg:pl-0'>
|
||||
@@ -104,11 +132,7 @@ export function LandingPreview() {
|
||||
: 'relative flex min-w-0 flex-1 flex-col overflow-hidden'
|
||||
}
|
||||
>
|
||||
{isWorkflowView ? (
|
||||
<LandingPreviewWorkflow workflow={activeWorkflow} animate={isInitialMount.current} />
|
||||
) : (
|
||||
<LandingPreviewHome />
|
||||
)}
|
||||
{renderContent()}
|
||||
</div>
|
||||
<motion.div
|
||||
className={isWorkflowView ? 'hidden lg:flex' : 'hidden'}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { GithubOutlineIcon } from '@/components/icons'
|
||||
import { getFormattedGitHubStars } from '@/app/(landing)/actions/github'
|
||||
import { getFormattedGitHubStars } from '@/app/(home)/actions/github'
|
||||
|
||||
const logger = createLogger('github-stars')
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ const PRICING_TIERS: PricingTier[] = [
|
||||
'5GB file storage',
|
||||
'3 tables · 1,000 rows each',
|
||||
'5 min execution limit',
|
||||
'5 concurrent/workspace',
|
||||
'7-day log retention',
|
||||
'CLI/SDK/MCP Access',
|
||||
],
|
||||
@@ -42,6 +43,7 @@ const PRICING_TIERS: PricingTier[] = [
|
||||
'50GB file storage',
|
||||
'25 tables · 5,000 rows each',
|
||||
'50 min execution · 150 runs/min',
|
||||
'50 concurrent/workspace',
|
||||
'Unlimited log retention',
|
||||
'CLI/SDK/MCP Access',
|
||||
],
|
||||
@@ -59,6 +61,7 @@ const PRICING_TIERS: PricingTier[] = [
|
||||
'500GB file storage',
|
||||
'25 tables · 5,000 rows each',
|
||||
'50 min execution · 300 runs/min',
|
||||
'200 concurrent/workspace',
|
||||
'Unlimited log retention',
|
||||
'CLI/SDK/MCP Access',
|
||||
],
|
||||
@@ -75,8 +78,9 @@ const PRICING_TIERS: PricingTier[] = [
|
||||
'Custom file storage',
|
||||
'10,000 tables · 1M rows each',
|
||||
'Custom execution limits',
|
||||
'Custom concurrency limits',
|
||||
'Unlimited log retention',
|
||||
'SSO & SCIM · SOC2 & HIPAA',
|
||||
'SSO & SCIM · SOC2',
|
||||
'Self hosting · Dedicated support',
|
||||
],
|
||||
cta: { label: 'Book a demo', action: 'demo-request' },
|
||||
|
||||
@@ -93,7 +93,7 @@ export default function StructuredData() {
|
||||
url: 'https://sim.ai',
|
||||
name: 'Sim — Build AI Agents & Run Your Agentic Workforce',
|
||||
description:
|
||||
'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. Trusted by over 100,000 builders. SOC2 and HIPAA compliant.',
|
||||
'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. Trusted by over 100,000 builders. SOC2 compliant.',
|
||||
applicationCategory: 'DeveloperApplication',
|
||||
operatingSystem: 'Web',
|
||||
browserRequirements: 'Requires a modern browser with JavaScript enabled',
|
||||
@@ -179,7 +179,7 @@ export default function StructuredData() {
|
||||
name: 'What is Sim?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'Sim is the open-source platform to build AI agents and run your agentic workforce. Teams connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows. Create agents, workflows, knowledge bases, tables, and docs. Trusted by over 100,000 builders. SOC2 and HIPAA compliant.',
|
||||
text: 'Sim is the open-source platform to build AI agents and run your agentic workforce. Teams connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows. Create agents, workflows, knowledge bases, tables, and docs. Trusted by over 100,000 builders. SOC2 compliant.',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -211,7 +211,7 @@ export default function StructuredData() {
|
||||
name: 'What enterprise features does Sim offer?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'Sim offers SOC2 and HIPAA compliance, SSO/SAML authentication, role-based access control, audit logs, dedicated support, custom SLAs, and on-premise deployment options for enterprise customers.',
|
||||
text: 'Sim offers SOC2 compliance, SSO/SAML authentication, role-based access control, audit logs, dedicated support, custom SLAs, and on-premise deployment options for enterprise customers.',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/emcn'
|
||||
import { Copy } from '@/components/emcn/icons'
|
||||
import { LinkedInIcon, xIcon as XIcon } from '@/components/icons'
|
||||
|
||||
interface ShareButtonProps {
|
||||
url: string
|
||||
@@ -50,10 +52,17 @@ export function ShareButton({ url, title }: ShareButtonProps) {
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align='end'>
|
||||
<DropdownMenuItem onSelect={handleCopyLink}>
|
||||
<Copy className='h-4 w-4' />
|
||||
{copied ? 'Copied!' : 'Copy link'}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={handleShareTwitter}>Share on X</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={handleShareLinkedIn}>Share on LinkedIn</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={handleShareTwitter}>
|
||||
<XIcon className='h-4 w-4' />
|
||||
Share on X
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={handleShareLinkedIn}>
|
||||
<LinkedInIcon className='h-4 w-4' />
|
||||
Share on LinkedIn
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
|
||||
43
apps/sim/app/(landing)/blog/components/blog-image.tsx
Normal file
43
apps/sim/app/(landing)/blog/components/blog-image.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import NextImage from 'next/image'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { Lightbox } from '@/app/(landing)/blog/components/lightbox'
|
||||
|
||||
interface BlogImageProps {
|
||||
src: string
|
||||
alt?: string
|
||||
width?: number
|
||||
height?: number
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function BlogImage({ src, alt = '', width = 800, height = 450, className }: BlogImageProps) {
|
||||
const [isLightboxOpen, setIsLightboxOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<NextImage
|
||||
src={src}
|
||||
alt={alt}
|
||||
width={width}
|
||||
height={height}
|
||||
className={cn(
|
||||
'h-auto w-full cursor-pointer rounded-lg transition-opacity hover:opacity-95',
|
||||
className
|
||||
)}
|
||||
sizes='(max-width: 768px) 100vw, 800px'
|
||||
loading='lazy'
|
||||
unoptimized
|
||||
onClick={() => setIsLightboxOpen(true)}
|
||||
/>
|
||||
<Lightbox
|
||||
isOpen={isLightboxOpen}
|
||||
onClose={() => setIsLightboxOpen(false)}
|
||||
src={src}
|
||||
alt={alt}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
62
apps/sim/app/(landing)/blog/components/lightbox.tsx
Normal file
62
apps/sim/app/(landing)/blog/components/lightbox.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
interface LightboxProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
src: string
|
||||
alt: string
|
||||
}
|
||||
|
||||
export function Lightbox({ isOpen, onClose, src, alt }: LightboxProps) {
|
||||
const overlayRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (overlayRef.current && event.target === overlayRef.current) {
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown)
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
document.body.style.overflow = 'hidden'
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown)
|
||||
document.removeEventListener('click', handleClickOutside)
|
||||
document.body.style.overflow = 'unset'
|
||||
}
|
||||
}, [isOpen, onClose])
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={overlayRef}
|
||||
className='fixed inset-0 z-50 flex items-center justify-center bg-black/80 p-12 backdrop-blur-sm'
|
||||
role='dialog'
|
||||
aria-modal='true'
|
||||
aria-label='Image viewer'
|
||||
>
|
||||
<div className='relative max-h-full max-w-full overflow-hidden rounded-xl shadow-2xl'>
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
className='max-h-[75vh] max-w-[75vw] cursor-pointer rounded-xl object-contain'
|
||||
loading='lazy'
|
||||
onClick={onClose}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
export default function BackgroundSVG() {
|
||||
return (
|
||||
<svg
|
||||
aria-hidden='true'
|
||||
focusable='false'
|
||||
className='-translate-x-1/2 pointer-events-none absolute top-0 left-1/2 z-10 hidden h-full min-h-full w-[1308px] sm:block'
|
||||
width='1308'
|
||||
height='4970'
|
||||
viewBox='0 18 1308 4094'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
preserveAspectRatio='xMidYMin slice'
|
||||
>
|
||||
{/* Pricing section (extended by ~28 units) */}
|
||||
<path d='M6.71704 1236.22H1300.76' stroke='#E7E4EF' strokeWidth='2' />
|
||||
<circle cx='11.0557' cy='1236.48' r='8.07846' fill='white' stroke='#E7E4EF' strokeWidth='2' />
|
||||
<circle cx='1298.02' cy='1236.48' r='8.07846' fill='white' stroke='#E7E4EF' strokeWidth='2' />
|
||||
<path d='M10.7967 1245.42V1641.91' stroke='#E7E4EF' strokeWidth='2' />
|
||||
<path d='M1297.76 1245.96V1641.91' stroke='#E7E4EF' strokeWidth='2' />
|
||||
|
||||
{/* Integrations section (shifted down by 28 units) */}
|
||||
<path d='M6.71704 1642.89H1291.05' stroke='#E7E4EF' strokeWidth='2' />
|
||||
<circle cx='11.0557' cy='1643.15' r='8.07846' fill='white' stroke='#E7E4EF' strokeWidth='2' />
|
||||
<circle cx='1298.02' cy='1643.15' r='8.07846' fill='white' stroke='#E7E4EF' strokeWidth='2' />
|
||||
<path d='M10.7967 1652.61V2054.93' stroke='#E7E4EF' strokeWidth='2' />
|
||||
<path d='M1297.76 1652.61V2054.93' stroke='#E7E4EF' strokeWidth='2' />
|
||||
|
||||
{/* Testimonials section (shifted down by 28 units) */}
|
||||
<path d='M6.71704 2054.71H1300.76' stroke='#E7E4EF' strokeWidth='2' />
|
||||
<circle cx='11.0557' cy='2054.97' r='8.07846' fill='white' stroke='#E7E4EF' strokeWidth='2' />
|
||||
<circle cx='1298.02' cy='2054.97' r='8.07846' fill='white' stroke='#E7E4EF' strokeWidth='2' />
|
||||
<path d='M10.7967 2064.43V2205.43' stroke='#E7E4EF' strokeWidth='2' />
|
||||
<path d='M1297.76 2064.43V2205.43' stroke='#E7E4EF' strokeWidth='2' />
|
||||
|
||||
{/* Footer section line (shifted down by 28 units) */}
|
||||
<path d='M6.71704 2205.71H1300.76' stroke='#E7E4EF' strokeWidth='2' />
|
||||
<circle cx='11.0557' cy='2205.97' r='8.07846' fill='white' stroke='#E7E4EF' strokeWidth='2' />
|
||||
<circle cx='1298.02' cy='2205.97' r='8.07846' fill='white' stroke='#E7E4EF' strokeWidth='2' />
|
||||
<path d='M10.7967 2215.43V4118.25' stroke='#E7E4EF' strokeWidth='2' />
|
||||
<path d='M1297.76 2215.43V4118.25' stroke='#E7E4EF' strokeWidth='2' />
|
||||
<path
|
||||
d='M959.828 116.604C1064.72 187.189 1162.61 277.541 1293.45 536.597'
|
||||
stroke='#E7E4EF'
|
||||
strokeWidth='1.90903'
|
||||
/>
|
||||
<path d='M1118.77 612.174V88' stroke='#E7E4EF' strokeWidth='1.90903' />
|
||||
<path d='M1261.95 481.414L1289.13 481.533' stroke='#E7E4EF' strokeWidth='1.90903' />
|
||||
<path d='M960 109.049V88' stroke='#E7E4EF' strokeWidth='1.90903' />
|
||||
<circle
|
||||
cx='960.214'
|
||||
cy='115.214'
|
||||
r='6.25942'
|
||||
transform='rotate(90 960.214 115.214)'
|
||||
fill='white'
|
||||
stroke='#E7E4EF'
|
||||
strokeWidth='1.90903'
|
||||
/>
|
||||
<circle
|
||||
cx='1119.21'
|
||||
cy='258.214'
|
||||
r='6.25942'
|
||||
transform='rotate(90 1119.21 258.214)'
|
||||
fill='white'
|
||||
stroke='#E7E4EF'
|
||||
strokeWidth='1.90903'
|
||||
/>
|
||||
<circle
|
||||
cx='1265.19'
|
||||
cy='481.414'
|
||||
r='6.25942'
|
||||
transform='rotate(90 1265.19 481.414)'
|
||||
fill='white'
|
||||
stroke='#E7E4EF'
|
||||
strokeWidth='1.90903'
|
||||
/>
|
||||
<path
|
||||
d='M77 179C225.501 165.887 294.438 145.674 390 85'
|
||||
stroke='#E7E4EF'
|
||||
strokeWidth='1.90903'
|
||||
/>
|
||||
<path d='M214.855 521.491L215 75' stroke='#E7E4EF' strokeWidth='1.90903' />
|
||||
<path
|
||||
d='M76.6567 381.124C177.305 448.638 213.216 499.483 240.767 613.253'
|
||||
stroke='#E7E4EF'
|
||||
strokeWidth='1.90903'
|
||||
/>
|
||||
<path d='M76.5203 175.703V613.253' stroke='#E7E4EF' strokeWidth='1.90903' />
|
||||
<path d='M1.07967 179.225L76.6567 179.225' stroke='#E7E4EF' strokeWidth='1.90903' />
|
||||
<circle
|
||||
cx='76.3128'
|
||||
cy='178.882'
|
||||
r='6.25942'
|
||||
transform='rotate(90 76.3128 178.882)'
|
||||
fill='white'
|
||||
stroke='#E7E4EF'
|
||||
strokeWidth='1.90903'
|
||||
/>
|
||||
<circle
|
||||
cx='214.511'
|
||||
cy='528.695'
|
||||
r='6.25942'
|
||||
transform='rotate(90 214.511 528.695)'
|
||||
fill='white'
|
||||
stroke='#E7E4EF'
|
||||
strokeWidth='1.90903'
|
||||
/>
|
||||
<circle
|
||||
cx='76.3129'
|
||||
cy='380.78'
|
||||
r='6.25942'
|
||||
transform='rotate(90 76.3129 380.78)'
|
||||
fill='white'
|
||||
stroke='#E7E4EF'
|
||||
strokeWidth='1.90903'
|
||||
/>
|
||||
<path d='M10.7967 18V1226.51' stroke='#E7E4EF' strokeWidth='2' />
|
||||
<path d='M1297.76 18V1227.59' stroke='#E7E4EF' strokeWidth='2' />
|
||||
<path d='M6.71704 78.533H1300.76' stroke='#E7E4EF' strokeWidth='2' />
|
||||
<circle cx='10.7967' cy='78.792' r='8.07846' fill='white' stroke='#E7E4EF' strokeWidth='2' />
|
||||
<circle cx='214.976' cy='78.9761' r='8.07846' fill='white' stroke='#E7E4EF' strokeWidth='2' />
|
||||
<circle cx='396.976' cy='78.9761' r='8.07846' fill='white' stroke='#E7E4EF' strokeWidth='2' />
|
||||
<circle cx='1298.02' cy='78.792' r='8.07846' fill='white' stroke='#E7E4EF' strokeWidth='2' />
|
||||
<circle cx='1118.98' cy='78.9761' r='8.07846' fill='white' stroke='#E7E4EF' strokeWidth='2' />
|
||||
<circle cx='959.976' cy='78.9761' r='8.07846' fill='white' stroke='#E7E4EF' strokeWidth='2' />
|
||||
<path d='M16.4341 620.811H1292.13' stroke='#E7E4EF' strokeWidth='2' />
|
||||
<circle cx='11.0557' cy='621.07' r='8.07846' fill='white' stroke='#E7E4EF' strokeWidth='2' />
|
||||
<circle cx='76.3758' cy='621.07' r='8.07846' fill='white' stroke='#E7E4EF' strokeWidth='2' />
|
||||
<circle cx='244.805' cy='621.07' r='8.07846' fill='white' stroke='#E7E4EF' strokeWidth='2' />
|
||||
<circle cx='10.7967' cy='178.405' r='8.07846' fill='white' stroke='#E7E4EF' strokeWidth='2' />
|
||||
<circle cx='1298.02' cy='621.07' r='8.07846' fill='white' stroke='#E7E4EF' strokeWidth='2' />
|
||||
<circle cx='1119.23' cy='621.07' r='8.07846' fill='white' stroke='#E7E4EF' strokeWidth='2' />
|
||||
<circle cx='1298.02' cy='481.253' r='8.07846' fill='white' stroke='#E7E4EF' strokeWidth='2' />
|
||||
<circle cx='1298.02' cy='541.714' r='8.07846' fill='white' stroke='#E7E4EF' strokeWidth='2' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import dynamic from 'next/dynamic'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
|
||||
// Lazy load the SVG to reduce initial bundle size
|
||||
const BackgroundSVG = dynamic(() => import('./background-svg'), {
|
||||
ssr: true, // Enable SSR for SEO
|
||||
loading: () => null, // Don't show loading state
|
||||
})
|
||||
|
||||
type BackgroundProps = {
|
||||
className?: string
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
export default function Background({ className, children }: BackgroundProps) {
|
||||
return (
|
||||
<div className={cn('relative min-h-screen w-full', className)}>
|
||||
<div className='-z-50 pointer-events-none fixed inset-0 bg-white' />
|
||||
<BackgroundSVG />
|
||||
<div className='relative z-0 mx-auto w-full max-w-[1308px]'>{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { HIPAABadgeIcon } from '@/components/icons'
|
||||
|
||||
export default function ComplianceBadges() {
|
||||
return (
|
||||
<div className='mt-1.5 flex items-center gap-3'>
|
||||
{/* SOC2 badge */}
|
||||
<Link
|
||||
href='https://app.vanta.com/sim.ai/trust/v35ia0jil4l7dteqjgaktn'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
<Image
|
||||
src='/footer/soc2.png'
|
||||
alt='SOC2 Compliant'
|
||||
width={54}
|
||||
height={54}
|
||||
className='object-contain'
|
||||
loading='lazy'
|
||||
unoptimized
|
||||
/>
|
||||
</Link>
|
||||
{/* HIPAA badge */}
|
||||
<Link
|
||||
href='https://app.vanta.com/sim.ai/trust/v35ia0jil4l7dteqjgaktn'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
<HIPAABadgeIcon className='h-[54px] w-[54px]' />
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import ComplianceBadges from './compliance-badges'
|
||||
import Logo from './logo'
|
||||
import SocialLinks from './social-links'
|
||||
import StatusIndicator from './status-indicator'
|
||||
|
||||
export { ComplianceBadges, Logo, SocialLinks, StatusIndicator }
|
||||
@@ -1,17 +0,0 @@
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function Logo() {
|
||||
return (
|
||||
<Link href='/' aria-label='Sim home'>
|
||||
<Image
|
||||
src='/logo/b&w/text/b&w.svg'
|
||||
alt='Sim - Workflows for LLMs'
|
||||
width={49.78314}
|
||||
height={24.276}
|
||||
priority
|
||||
quality={90}
|
||||
/>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import { DiscordIcon, GithubIcon, LinkedInIcon, xIcon as XIcon } from '@/components/icons'
|
||||
|
||||
export default function SocialLinks() {
|
||||
return (
|
||||
<div className='flex items-center gap-3'>
|
||||
<a
|
||||
href='https://discord.gg/Hr4UWYEcTT'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='flex items-center text-md text-muted-foreground transition-colors hover:text-foreground'
|
||||
aria-label='Discord'
|
||||
>
|
||||
<DiscordIcon className='h-[20px] w-[20px]' aria-hidden='true' />
|
||||
</a>
|
||||
<a
|
||||
href='https://x.com/simdotai'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='flex items-center text-md text-muted-foreground transition-colors hover:text-foreground'
|
||||
aria-label='X (Twitter)'
|
||||
>
|
||||
<XIcon className='h-[18px] w-[18px]' aria-hidden='true' />
|
||||
</a>
|
||||
<a
|
||||
href='https://www.linkedin.com/company/simstudioai/'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='flex items-center text-md text-muted-foreground transition-colors hover:text-foreground'
|
||||
aria-label='LinkedIn'
|
||||
>
|
||||
<LinkedInIcon className='h-[18px] w-[18px]' aria-hidden='true' />
|
||||
</a>
|
||||
<a
|
||||
href='https://github.com/simstudioai/sim'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='flex items-center text-md text-muted-foreground transition-colors hover:text-foreground'
|
||||
aria-label='GitHub'
|
||||
>
|
||||
<GithubIcon className='h-[20px] w-[20px]' aria-hidden='true' />
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import type { SVGProps } from 'react'
|
||||
import Link from 'next/link'
|
||||
import type { StatusType } from '@/app/api/status/types'
|
||||
import { useStatus } from '@/hooks/queries/status'
|
||||
|
||||
interface StatusDotIconProps extends SVGProps<SVGSVGElement> {
|
||||
status: 'operational' | 'degraded' | 'outage' | 'maintenance' | 'loading' | 'error'
|
||||
}
|
||||
|
||||
export function StatusDotIcon({ status, className, ...props }: StatusDotIconProps) {
|
||||
const colors = {
|
||||
operational: '#10B981',
|
||||
degraded: '#F59E0B',
|
||||
outage: '#EF4444',
|
||||
maintenance: '#3B82F6',
|
||||
loading: '#9CA3AF',
|
||||
error: '#9CA3AF',
|
||||
}
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width={6}
|
||||
height={6}
|
||||
viewBox='0 0 6 6'
|
||||
fill='none'
|
||||
className={className}
|
||||
{...props}
|
||||
>
|
||||
<circle cx={3} cy={3} r={3} fill={colors[status]} />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
const STATUS_COLORS: Record<StatusType, string> = {
|
||||
operational: 'text-[#10B981] hover:text-[#059669]',
|
||||
degraded: 'text-[var(--caution)] hover:text-[#D97706]',
|
||||
outage: 'text-[var(--text-error)] hover:text-[var(--error)]',
|
||||
maintenance: 'text-[#3B82F6] hover:text-[#2563EB]',
|
||||
loading: 'text-muted-foreground hover:text-foreground',
|
||||
error: 'text-muted-foreground hover:text-foreground',
|
||||
}
|
||||
|
||||
export default function StatusIndicator() {
|
||||
const { data, isLoading, isError } = useStatus()
|
||||
|
||||
const status = isLoading ? 'loading' : isError ? 'error' : data?.status || 'error'
|
||||
const message = isLoading
|
||||
? 'Checking Status...'
|
||||
: isError
|
||||
? 'Status Unknown'
|
||||
: data?.message || 'Status Unknown'
|
||||
const statusUrl = data?.url || 'https://status.sim.ai'
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={statusUrl}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className={`flex min-w-[165px] items-center gap-1.5 whitespace-nowrap text-caption transition-colors ${STATUS_COLORS[status]}`}
|
||||
aria-label={`System status: ${message}`}
|
||||
>
|
||||
<StatusDotIcon status={status} className='h-[6px] w-[6px]' aria-hidden='true' />
|
||||
<span>{message}</span>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
export const FOOTER_BLOCKS = [
|
||||
'Agent',
|
||||
'API',
|
||||
'Condition',
|
||||
'Evaluator',
|
||||
'Function',
|
||||
'Guardrails',
|
||||
'Human In The Loop',
|
||||
'Loop',
|
||||
'Parallel',
|
||||
'Response',
|
||||
'Router',
|
||||
'Starter',
|
||||
'Webhook',
|
||||
'Workflow',
|
||||
]
|
||||
|
||||
export const FOOTER_TOOLS = [
|
||||
'Airtable',
|
||||
'Apify',
|
||||
'Apollo',
|
||||
'ArXiv',
|
||||
'Browser Use',
|
||||
'Calendly',
|
||||
'Clay',
|
||||
'Confluence',
|
||||
'Discord',
|
||||
'ElevenLabs',
|
||||
'Exa',
|
||||
'Firecrawl',
|
||||
'GitHub',
|
||||
'Gmail',
|
||||
'Google Drive',
|
||||
'HubSpot',
|
||||
'HuggingFace',
|
||||
'Hunter',
|
||||
'Incidentio',
|
||||
'Intercom',
|
||||
'Jina',
|
||||
'Jira',
|
||||
'Knowledge',
|
||||
'Linear',
|
||||
'LinkUp',
|
||||
'LinkedIn',
|
||||
'Mailchimp',
|
||||
'Mailgun',
|
||||
'MCP',
|
||||
'Mem0',
|
||||
'Microsoft Excel',
|
||||
'Microsoft Planner',
|
||||
'Microsoft Teams',
|
||||
'Mistral Parse',
|
||||
'MongoDB',
|
||||
'MySQL',
|
||||
'Neo4j',
|
||||
'Notion',
|
||||
'OneDrive',
|
||||
'OpenAI',
|
||||
'Outlook',
|
||||
'Parallel AI',
|
||||
'Perplexity',
|
||||
'Pinecone',
|
||||
'Pipedrive',
|
||||
'PostHog',
|
||||
'PostgreSQL',
|
||||
'Qdrant',
|
||||
'Reddit',
|
||||
'Resend',
|
||||
'S3',
|
||||
'Salesforce',
|
||||
'SendGrid',
|
||||
'Serper',
|
||||
'ServiceNow',
|
||||
'SharePoint',
|
||||
'Slack',
|
||||
'Smtp',
|
||||
'Stagehand',
|
||||
'Stripe',
|
||||
'Supabase',
|
||||
'Tavily',
|
||||
'Telegram',
|
||||
'Translate',
|
||||
'Trello',
|
||||
'Twilio',
|
||||
'Typeform',
|
||||
'Vision',
|
||||
'Wait',
|
||||
'Wealthbox',
|
||||
'Webflow',
|
||||
'WhatsApp',
|
||||
'Wikipedia',
|
||||
'X',
|
||||
'YouTube',
|
||||
'Zendesk',
|
||||
'Zep',
|
||||
]
|
||||
@@ -1,280 +0,0 @@
|
||||
import Link from 'next/link'
|
||||
import {
|
||||
ComplianceBadges,
|
||||
Logo,
|
||||
SocialLinks,
|
||||
StatusIndicator,
|
||||
} from '@/app/(landing)/components/footer/components'
|
||||
import { FOOTER_BLOCKS, FOOTER_TOOLS } from '@/app/(landing)/components/footer/consts'
|
||||
|
||||
interface FooterProps {
|
||||
fullWidth?: boolean
|
||||
}
|
||||
|
||||
export default function Footer({ fullWidth = false }: FooterProps) {
|
||||
return (
|
||||
<footer className='relative w-full overflow-hidden bg-white'>
|
||||
<div
|
||||
className={
|
||||
fullWidth
|
||||
? 'px-4 pt-10 pb-10 sm:px-4 sm:pt-[34px] sm:pb-[340px]'
|
||||
: 'px-4 pt-10 pb-10 sm:px-[50px] sm:pt-[34px] sm:pb-[340px]'
|
||||
}
|
||||
>
|
||||
<div className={`flex gap-20 ${fullWidth ? 'justify-center' : ''}`}>
|
||||
{/* Logo and social links */}
|
||||
<div className='flex flex-col gap-6'>
|
||||
<Logo />
|
||||
<SocialLinks />
|
||||
<ComplianceBadges />
|
||||
<StatusIndicator />
|
||||
</div>
|
||||
|
||||
{/* Links section */}
|
||||
<div>
|
||||
<h2 className='mb-4 font-medium text-foreground text-sm'>More Sim</h2>
|
||||
<div className='flex flex-col gap-3'>
|
||||
<Link
|
||||
href='https://docs.sim.ai'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-muted-foreground text-sm transition-colors hover:text-foreground'
|
||||
>
|
||||
Docs
|
||||
</Link>
|
||||
<Link
|
||||
href='#pricing'
|
||||
className='text-muted-foreground text-sm transition-colors hover:text-foreground'
|
||||
>
|
||||
Pricing
|
||||
</Link>
|
||||
<Link
|
||||
href='https://form.typeform.com/to/jqCO12pF'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-muted-foreground text-sm transition-colors hover:text-foreground'
|
||||
>
|
||||
Enterprise
|
||||
</Link>
|
||||
<Link
|
||||
href='/blog'
|
||||
className='text-muted-foreground text-sm transition-colors hover:text-foreground'
|
||||
>
|
||||
Blog
|
||||
</Link>
|
||||
<Link
|
||||
href='/changelog'
|
||||
className='text-muted-foreground text-sm transition-colors hover:text-foreground'
|
||||
>
|
||||
Changelog
|
||||
</Link>
|
||||
<Link
|
||||
href='https://status.sim.ai'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-muted-foreground text-sm transition-colors hover:text-foreground'
|
||||
>
|
||||
Status
|
||||
</Link>
|
||||
<a
|
||||
href='https://jobs.ashbyhq.com/sim'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-muted-foreground text-sm transition-colors hover:text-foreground'
|
||||
>
|
||||
Careers
|
||||
</a>
|
||||
<Link
|
||||
href='/privacy'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-muted-foreground text-sm transition-colors hover:text-foreground'
|
||||
>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
<Link
|
||||
href='/terms'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-muted-foreground text-sm transition-colors hover:text-foreground'
|
||||
>
|
||||
Terms of Service
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Blocks section */}
|
||||
<div className='hidden sm:block'>
|
||||
<h2 className='mb-4 font-medium text-foreground text-sm'>Blocks</h2>
|
||||
<div className='flex flex-col gap-3'>
|
||||
{FOOTER_BLOCKS.map((block) => (
|
||||
<Link
|
||||
key={block}
|
||||
href={`https://docs.sim.ai/blocks/${block.toLowerCase().replaceAll(' ', '-')}`}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-muted-foreground text-sm transition-colors hover:text-foreground'
|
||||
>
|
||||
{block}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tools section - split into columns */}
|
||||
<div className='hidden sm:block'>
|
||||
<h2 className='mb-4 font-medium text-foreground text-sm'>Tools</h2>
|
||||
<div className='flex gap-20'>
|
||||
{/* First column */}
|
||||
<div className='flex flex-col gap-3'>
|
||||
{FOOTER_TOOLS.slice(0, Math.ceil(FOOTER_TOOLS.length / 4)).map((tool) => (
|
||||
<Link
|
||||
key={tool}
|
||||
href={`https://docs.sim.ai/tools/${tool.toLowerCase().replace(/\s+/g, '_')}`}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='whitespace-nowrap text-muted-foreground text-sm transition-colors hover:text-foreground'
|
||||
>
|
||||
{tool}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
{/* Second column */}
|
||||
<div className='flex flex-col gap-3'>
|
||||
{FOOTER_TOOLS.slice(
|
||||
Math.ceil(FOOTER_TOOLS.length / 4),
|
||||
Math.ceil((FOOTER_TOOLS.length * 2) / 4)
|
||||
).map((tool) => (
|
||||
<Link
|
||||
key={tool}
|
||||
href={`https://docs.sim.ai/tools/${tool.toLowerCase().replace(/\s+/g, '_')}`}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='whitespace-nowrap text-muted-foreground text-sm transition-colors hover:text-foreground'
|
||||
>
|
||||
{tool}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
{/* Third column */}
|
||||
<div className='flex flex-col gap-3'>
|
||||
{FOOTER_TOOLS.slice(
|
||||
Math.ceil((FOOTER_TOOLS.length * 2) / 4),
|
||||
Math.ceil((FOOTER_TOOLS.length * 3) / 4)
|
||||
).map((tool) => (
|
||||
<Link
|
||||
key={tool}
|
||||
href={`https://docs.sim.ai/tools/${tool.toLowerCase().replace(/\s+/g, '_')}`}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='whitespace-nowrap text-muted-foreground text-sm transition-colors hover:text-foreground'
|
||||
>
|
||||
{tool}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
{/* Fourth column */}
|
||||
<div className='flex flex-col gap-3'>
|
||||
{FOOTER_TOOLS.slice(Math.ceil((FOOTER_TOOLS.length * 3) / 4)).map((tool) => (
|
||||
<Link
|
||||
key={tool}
|
||||
href={`https://docs.sim.ai/tools/${tool.toLowerCase().replace(/\s+/g, '_')}`}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='whitespace-nowrap text-muted-foreground text-sm transition-colors hover:text-foreground'
|
||||
>
|
||||
{tool}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Large SIM logo at bottom - half cut off */}
|
||||
<div className='-translate-x-1/2 pointer-events-none absolute bottom-[-240px] left-1/2 hidden sm:block'>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width='1128'
|
||||
height='550'
|
||||
viewBox='0 0 1128 550'
|
||||
fill='none'
|
||||
>
|
||||
<g filter='url(#filter0_dd_122_4989)'>
|
||||
<path
|
||||
d='M3 420.942H77.9115C77.9115 441.473 85.4027 457.843 100.385 470.051C115.367 481.704 135.621 487.53 161.147 487.53C188.892 487.53 210.255 482.258 225.238 471.715C240.22 460.617 247.711 445.913 247.711 427.601C247.711 414.283 243.549 403.185 235.226 394.307C227.457 385.428 213.03 378.215 191.943 372.666L120.361 356.019C84.2929 347.14 57.3802 333.545 39.6234 315.234C22.4215 296.922 13.8206 272.784 13.8206 242.819C13.8206 217.849 20.2019 196.208 32.9646 177.896C46.2822 159.584 64.3165 145.434 87.0674 135.446C110.373 125.458 137.008 120.464 166.973 120.464C196.938 120.464 222.74 125.735 244.382 136.278C266.578 146.821 283.779 161.526 295.987 180.393C308.75 199.259 315.409 221.733 315.964 247.813H241.052C240.497 226.727 233.561 210.357 220.243 198.705C206.926 187.052 188.337 181.225 164.476 181.225C140.06 181.225 121.194 186.497 107.876 197.04C94.5585 207.583 87.8997 222.01 87.8997 240.322C87.8997 267.512 107.876 286.101 147.829 296.09L219.411 313.569C253.815 321.337 279.618 334.1 296.82 351.857C314.022 369.059 322.622 392.642 322.622 422.607C322.622 448.132 315.686 470.606 301.814 490.027C287.941 508.894 268.797 523.599 244.382 534.142C220.521 544.13 192.221 549.124 159.482 549.124C111.76 549.124 73.7498 537.471 45.4499 514.165C17.15 490.86 3 459.785 3 420.942Z'
|
||||
fill='#DCDCDC'
|
||||
/>
|
||||
<path
|
||||
d='M377.713 539.136V132.117C408.911 143.439 422.667 143.439 455.954 132.117V539.136H377.713ZM416.001 105.211C402.129 105.211 389.921 100.217 379.378 90.2291C369.39 79.686 364.395 67.4782 364.395 53.6057C364.395 39.1783 369.39 26.9705 379.378 16.9823C389.921 6.9941 402.129 2 416.001 2C430.428 2 442.636 6.9941 452.625 16.9823C462.613 26.9705 467.607 39.1783 467.607 53.6057C467.607 67.4782 462.613 79.686 452.625 90.2291C442.636 100.217 430.428 105.211 416.001 105.211Z'
|
||||
fill='#DCDCDC'
|
||||
/>
|
||||
<path
|
||||
d='M593.961 539.136H515.72V132.117H585.637V200.792C593.961 178.041 610.053 158.752 632.249 143.769C655 128.232 682.467 120.464 714.651 120.464C750.72 120.464 780.685 130.174 804.545 149.596C822.01 163.812 835.016 181.446 843.562 202.5C851.434 181.446 864.509 163.812 882.786 149.596C907.757 130.174 938.554 120.464 975.177 120.464C1021.79 120.464 1058.41 134.059 1085.05 161.249C1111.68 188.439 1125 225.617 1125 272.784V539.136H1048.42V291.928C1048.42 259.744 1040.1 235.051 1023.45 217.849C1007.36 200.092 985.443 191.213 957.698 191.213C938.276 191.213 921.074 195.653 906.092 204.531C891.665 212.855 880.289 225.062 871.966 241.154C863.642 257.247 859.48 276.113 859.48 297.754V539.136H782.072V291.095C782.072 258.911 774.026 234.496 757.934 217.849C741.841 200.647 719.923 192.046 692.178 192.046C672.756 192.046 655.555 196.485 640.572 205.363C626.145 213.687 614.769 225.895 606.446 241.987C598.122 257.524 593.961 276.113 593.961 297.754V539.136Z'
|
||||
fill='#DCDCDC'
|
||||
/>
|
||||
<path
|
||||
d='M166.973 121.105C196.396 121.105 221.761 126.201 243.088 136.367L244.101 136.855L244.106 136.858C265.86 147.191 282.776 161.528 294.876 179.865L295.448 180.741L295.455 180.753C308.032 199.345 314.656 221.475 315.306 247.171H241.675C240.996 226.243 234.012 209.899 220.666 198.222C207.196 186.435 188.437 180.583 164.476 180.583C139.977 180.583 120.949 185.871 107.478 196.536C93.9928 207.212 87.2578 221.832 87.2578 240.322C87.2579 254.096 92.3262 265.711 102.444 275.127C112.542 284.524 127.641 291.704 147.673 296.712L147.677 296.713L219.259 314.192L219.27 314.195C253.065 321.827 278.469 334.271 295.552 351.48L296.358 352.304L296.365 352.311C313.42 369.365 321.98 392.77 321.98 422.606C321.98 448.005 315.082 470.343 301.297 489.646C287.502 508.408 268.456 523.046 244.134 533.55C220.369 543.498 192.157 548.482 159.481 548.482C111.864 548.482 74.0124 536.855 45.8584 513.67C17.8723 490.623 3.80059 459.948 3.64551 421.584H77.2734C77.4285 441.995 84.9939 458.338 99.9795 470.549L99.9854 470.553L99.9912 470.558C115.12 482.324 135.527 488.172 161.146 488.172C188.96 488.172 210.474 482.889 225.607 472.24L225.613 472.236L225.619 472.231C240.761 461.015 248.353 446.12 248.353 427.601C248.352 414.145 244.145 402.89 235.709 393.884C227.81 384.857 213.226 377.603 192.106 372.045L192.098 372.043L192.089 372.04L120.507 355.394C84.5136 346.533 57.7326 332.983 40.0908 314.794H40.0918C23.0227 296.624 14.4629 272.654 14.4629 242.819C14.4629 217.969 20.8095 196.463 33.4834 178.273C46.7277 160.063 64.6681 145.981 87.3252 136.034L87.3242 136.033C110.536 126.086 137.081 121.106 166.973 121.105ZM975.177 121.105C1021.66 121.105 1058.1 134.658 1084.59 161.698C1111.08 188.741 1124.36 225.743 1124.36 272.784V538.494H1049.07V291.928C1049.07 259.636 1040.71 234.76 1023.92 217.402H1023.91C1007.68 199.5 985.584 190.571 957.697 190.571C938.177 190.571 920.862 195.034 905.771 203.975C891.228 212.365 879.77 224.668 871.396 240.859C863.017 257.059 858.838 276.03 858.838 297.754V538.494H782.714V291.096C782.714 258.811 774.641 234.209 758.395 217.402C742.16 200.053 720.062 191.404 692.178 191.404C673.265 191.404 656.422 195.592 641.666 203.985L640.251 204.808C625.711 213.196 614.254 225.497 605.88 241.684C597.496 257.333 593.318 276.031 593.318 297.754V538.494H516.361V132.759H584.995V200.792L586.24 201.013C594.51 178.408 610.505 159.221 632.607 144.302L632.61 144.3C655.238 128.847 682.574 121.105 714.651 121.105C750.599 121.105 780.413 130.781 804.14 150.094C821.52 164.241 834.461 181.787 842.967 202.741L843.587 204.268L844.163 202.725C851.992 181.786 864.994 164.248 883.181 150.103C908.021 130.782 938.673 121.106 975.177 121.105ZM455.312 538.494H378.354V133.027C393.534 138.491 404.652 141.251 416.05 141.251C427.46 141.251 439.095 138.485 455.312 133.009V538.494ZM416.001 2.6416C430.262 2.6416 442.306 7.57157 452.171 17.4365C462.036 27.3014 466.965 39.3445 466.965 53.6055C466.965 67.3043 462.04 79.3548 452.16 89.7842C442.297 99.6427 430.258 104.569 416.001 104.569C402.303 104.569 390.254 99.6452 379.825 89.7676C369.957 79.3421 365.037 67.2967 365.037 53.6055C365.037 39.3444 369.966 27.3005 379.831 17.4355C390.258 7.56247 402.307 2.64163 416.001 2.6416Z'
|
||||
stroke='#C1C1C1'
|
||||
strokeWidth='1.28396'
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter
|
||||
id='filter0_dd_122_4989'
|
||||
x='0'
|
||||
y='0'
|
||||
width='1128'
|
||||
height='550'
|
||||
filterUnits='userSpaceOnUse'
|
||||
colorInterpolationFilters='sRGB'
|
||||
>
|
||||
<feFlood floodOpacity='0' result='BackgroundImageFix' />
|
||||
<feColorMatrix
|
||||
in='SourceAlpha'
|
||||
type='matrix'
|
||||
values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0'
|
||||
result='hardAlpha'
|
||||
/>
|
||||
<feMorphology
|
||||
radius='1'
|
||||
operator='erode'
|
||||
in='SourceAlpha'
|
||||
result='effect1_dropShadow_122_4989'
|
||||
/>
|
||||
<feOffset dy='1' />
|
||||
<feGaussianBlur stdDeviation='1' />
|
||||
<feColorMatrix type='matrix' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0' />
|
||||
<feBlend
|
||||
mode='normal'
|
||||
in2='BackgroundImageFix'
|
||||
result='effect1_dropShadow_122_4989'
|
||||
/>
|
||||
<feColorMatrix
|
||||
in='SourceAlpha'
|
||||
type='matrix'
|
||||
values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0'
|
||||
result='hardAlpha'
|
||||
/>
|
||||
<feOffset dy='1' />
|
||||
<feGaussianBlur stdDeviation='1.5' />
|
||||
<feColorMatrix type='matrix' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0' />
|
||||
<feBlend
|
||||
mode='normal'
|
||||
in2='effect1_dropShadow_122_4989'
|
||||
result='effect2_dropShadow_122_4989'
|
||||
/>
|
||||
<feBlend
|
||||
mode='normal'
|
||||
in='SourceGraphic'
|
||||
in2='effect2_dropShadow_122_4989'
|
||||
result='shape'
|
||||
/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import type React from 'react'
|
||||
|
||||
interface IconButtonProps {
|
||||
children: React.ReactNode
|
||||
onClick?: () => void
|
||||
onMouseEnter?: () => void
|
||||
style?: React.CSSProperties
|
||||
'aria-label': string
|
||||
isAutoHovered?: boolean
|
||||
}
|
||||
|
||||
export function IconButton({
|
||||
children,
|
||||
onClick,
|
||||
onMouseEnter,
|
||||
style,
|
||||
'aria-label': ariaLabel,
|
||||
isAutoHovered = false,
|
||||
}: IconButtonProps) {
|
||||
return (
|
||||
<button
|
||||
type='button'
|
||||
aria-label={ariaLabel}
|
||||
onClick={onClick}
|
||||
onMouseEnter={onMouseEnter}
|
||||
className={`flex items-center justify-center rounded-xl border p-2 outline-none transition-all duration-300 ${
|
||||
isAutoHovered
|
||||
? 'border-[#454545] shadow-subtle'
|
||||
: 'border-transparent hover:border-[#454545] hover:shadow-subtle'
|
||||
}`}
|
||||
style={style}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
export { IconButton } from './icon-button'
|
||||
export { DotPattern } from './landing-canvas/dot-pattern'
|
||||
export type {
|
||||
LandingBlockProps,
|
||||
LandingCardData,
|
||||
} from './landing-canvas/landing-block/landing-block'
|
||||
export { LandingBlock } from './landing-canvas/landing-block/landing-block'
|
||||
export type { LoopNodeData } from './landing-canvas/landing-block/landing-loop-node'
|
||||
export { LandingLoopNode } from './landing-canvas/landing-block/landing-loop-node'
|
||||
export { LandingNode } from './landing-canvas/landing-block/landing-node'
|
||||
export type { LoopBlockProps } from './landing-canvas/landing-block/loop-block'
|
||||
export { LoopBlock } from './landing-canvas/landing-block/loop-block'
|
||||
export type { SubBlockRowProps, TagProps } from './landing-canvas/landing-block/tag'
|
||||
export { SubBlockRow, Tag } from './landing-canvas/landing-block/tag'
|
||||
export type {
|
||||
LandingBlockNode,
|
||||
LandingCanvasProps,
|
||||
LandingEdgeData,
|
||||
LandingGroupData,
|
||||
LandingManualBlock,
|
||||
LandingViewportApi,
|
||||
} from './landing-canvas/landing-canvas'
|
||||
export { CARD_HEIGHT, CARD_WIDTH, LandingCanvas } from './landing-canvas/landing-canvas'
|
||||
export { LandingEdge } from './landing-canvas/landing-edge/landing-edge'
|
||||
export type { LandingFlowProps } from './landing-canvas/landing-flow'
|
||||
export { LandingFlow } from './landing-canvas/landing-flow'
|
||||
@@ -1,133 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import type React from 'react'
|
||||
import { useEffect, useId, useRef, useState } from 'react'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
|
||||
/**
|
||||
* DotPattern Component Props
|
||||
*
|
||||
* @param {number} [width=16] - The horizontal spacing between dots
|
||||
* @param {number} [height=16] - The vertical spacing between dots
|
||||
* @param {number} [x=0] - The x-offset of the entire pattern
|
||||
* @param {number} [y=0] - The y-offset of the entire pattern
|
||||
* @param {number} [cx=1] - The x-offset of individual dots
|
||||
* @param {number} [cy=1] - The y-offset of individual dots
|
||||
* @param {number} [cr=1] - The radius of each dot
|
||||
* @param {string} [className] - Additional CSS classes to apply to the SVG container
|
||||
* @param {boolean} [glow=false] - Whether dots should have a glowing animation effect
|
||||
*/
|
||||
interface DotPatternProps extends React.SVGProps<SVGSVGElement> {
|
||||
width?: number
|
||||
height?: number
|
||||
x?: number
|
||||
y?: number
|
||||
cx?: number
|
||||
cy?: number
|
||||
cr?: number
|
||||
className?: string
|
||||
glow?: boolean
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
/**
|
||||
* DotPattern Component
|
||||
*
|
||||
* A React component that creates an animated or static dot pattern background using SVG.
|
||||
* The pattern automatically adjusts to fill its container and can optionally display glowing dots.
|
||||
*
|
||||
* @component
|
||||
*
|
||||
* @see DotPatternProps for the props interface.
|
||||
*
|
||||
* @example
|
||||
* // Basic usage
|
||||
* <DotPattern />
|
||||
*
|
||||
* // With glowing effect and custom spacing
|
||||
* <DotPattern
|
||||
* width={20}
|
||||
* height={20}
|
||||
* glow={true}
|
||||
* className="opacity-50"
|
||||
* />
|
||||
*
|
||||
* @notes
|
||||
* - The component is client-side only ("use client")
|
||||
* - Automatically responds to container size changes
|
||||
* - When glow is enabled, dots will animate with random delays and durations
|
||||
* - Uses Motion for animations
|
||||
* - Dots color can be controlled via the text color utility classes
|
||||
*/
|
||||
|
||||
export function DotPattern({
|
||||
width = 16,
|
||||
height = 16,
|
||||
x = 0,
|
||||
y = 0,
|
||||
cx = 1,
|
||||
cy = 1,
|
||||
cr = 1,
|
||||
className,
|
||||
glow = false,
|
||||
...props
|
||||
}: DotPatternProps) {
|
||||
const id = useId()
|
||||
const containerRef = useRef<SVGSVGElement>(null)
|
||||
const [dimensions, setDimensions] = useState({ width: 0, height: 0 })
|
||||
|
||||
useEffect(() => {
|
||||
const updateDimensions = () => {
|
||||
if (containerRef.current) {
|
||||
const { width, height } = containerRef.current.getBoundingClientRect()
|
||||
setDimensions({ width, height })
|
||||
}
|
||||
}
|
||||
|
||||
updateDimensions()
|
||||
window.addEventListener('resize', updateDimensions)
|
||||
return () => window.removeEventListener('resize', updateDimensions)
|
||||
}, [])
|
||||
|
||||
const dots = Array.from(
|
||||
{
|
||||
length: Math.ceil(dimensions.width / width) * Math.ceil(dimensions.height / height),
|
||||
},
|
||||
(_, i) => {
|
||||
const col = i % Math.ceil(dimensions.width / width)
|
||||
const row = Math.floor(i / Math.ceil(dimensions.width / width))
|
||||
return {
|
||||
x: col * width + cx,
|
||||
y: row * height + cy,
|
||||
delay: Math.random() * 5,
|
||||
duration: Math.random() * 3 + 2,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return (
|
||||
<svg
|
||||
ref={containerRef}
|
||||
aria-hidden='true'
|
||||
className={cn('pointer-events-none absolute inset-0 h-full w-full', className)}
|
||||
{...props}
|
||||
>
|
||||
<defs>
|
||||
<radialGradient id={`${id}-gradient`}>
|
||||
<stop offset='0%' stopColor='currentColor' stopOpacity='1' />
|
||||
<stop offset='100%' stopColor='currentColor' stopOpacity='0' />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
{dots.map((dot, index) => (
|
||||
<circle
|
||||
key={`${dot.x}-${dot.y}`}
|
||||
cx={dot.x}
|
||||
cy={dot.y}
|
||||
r={cr}
|
||||
fill={glow ? `url(#${id}-gradient)` : 'currentColor'}
|
||||
className='text-neutral-400/80'
|
||||
/>
|
||||
))}
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
SubBlockRow,
|
||||
type SubBlockRowProps,
|
||||
} from '@/app/(landing)/components/hero/components/landing-canvas/landing-block/tag'
|
||||
|
||||
/**
|
||||
* Data structure for a landing card component
|
||||
* Matches the workflow block structure from the application
|
||||
*/
|
||||
export interface LandingCardData {
|
||||
/** Icon element to display in the card header */
|
||||
icon: React.ReactNode
|
||||
/** Background color for the icon container */
|
||||
color: string | '#f6f6f6'
|
||||
/** Name/title of the card */
|
||||
name: string
|
||||
/** Optional subblock rows to display below the header */
|
||||
tags?: SubBlockRowProps[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for the LandingBlock component
|
||||
*/
|
||||
export interface LandingBlockProps extends LandingCardData {
|
||||
/** Optional CSS class names */
|
||||
className?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Landing block component that displays a card with icon, name, and optional subblock rows
|
||||
* Styled to match the application's workflow blocks
|
||||
* @param props - Component properties including icon, color, name, tags, and className
|
||||
* @returns A styled block card component
|
||||
*/
|
||||
export const LandingBlock = React.memo(function LandingBlock({
|
||||
icon,
|
||||
color,
|
||||
name,
|
||||
tags,
|
||||
className,
|
||||
}: LandingBlockProps) {
|
||||
const hasContentBelowHeader = tags && tags.length > 0
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`z-10 flex w-[250px] flex-col rounded-lg border border-[var(--landing-border-light)] bg-white ${className ?? ''}`}
|
||||
>
|
||||
{/* Header - matches workflow-block.tsx header styling */}
|
||||
<div
|
||||
className={`flex items-center justify-between p-2 ${hasContentBelowHeader ? 'border-[var(--landing-border-light)] border-b' : ''}`}
|
||||
>
|
||||
<div className='flex min-w-0 flex-1 items-center gap-2.5'>
|
||||
<div
|
||||
className='flex h-[24px] w-[24px] flex-shrink-0 items-center justify-center rounded-md'
|
||||
style={{ background: color as string }}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
<span className='truncate font-medium text-[#171717] text-md' title={name}>
|
||||
{name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content - SubBlock Rows matching workflow-block.tsx */}
|
||||
{hasContentBelowHeader && (
|
||||
<div className='flex flex-col gap-2 p-2'>
|
||||
{tags.map((tag) => (
|
||||
<SubBlockRow key={tag.label} icon={tag.icon} label={tag.label} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
@@ -1,49 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import { LoopBlock } from '@/app/(landing)/components/hero/components/landing-canvas/landing-block/loop-block'
|
||||
|
||||
/**
|
||||
* Data structure for the loop node
|
||||
*/
|
||||
export interface LoopNodeData {
|
||||
/** Label for the loop block */
|
||||
label?: string
|
||||
/** Child content to render inside */
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
/**
|
||||
* React Flow node component for the loop block
|
||||
* Acts as a group node for subflow functionality
|
||||
* @param props - Component properties containing node data
|
||||
* @returns A React Flow compatible loop node component
|
||||
*/
|
||||
export const LandingLoopNode = React.memo(function LandingLoopNode({
|
||||
data,
|
||||
style,
|
||||
}: {
|
||||
data: LoopNodeData
|
||||
style?: React.CSSProperties
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className='nodrag nopan nowheel relative cursor-grab active:cursor-grabbing'
|
||||
style={{
|
||||
width: style?.width || 1198,
|
||||
height: style?.height || 528,
|
||||
backgroundColor: 'transparent',
|
||||
outline: 'none !important',
|
||||
boxShadow: 'none !important',
|
||||
border: 'none !important',
|
||||
}}
|
||||
>
|
||||
<LoopBlock style={{ width: '100%', height: '100%', pointerEvents: 'none' }}>
|
||||
<div className='flex items-start gap-3 px-6 py-4'>
|
||||
<span className='font-medium text-base text-blue-500'>Loop</span>
|
||||
</div>
|
||||
{data.children}
|
||||
</LoopBlock>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
@@ -1,95 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import { Handle, Position } from 'reactflow'
|
||||
import {
|
||||
LandingBlock,
|
||||
type LandingCardData,
|
||||
} from '@/app/(landing)/components/hero/components/landing-canvas/landing-block/landing-block'
|
||||
|
||||
/**
|
||||
* Handle Y offset from block top - matches HANDLE_POSITIONS.DEFAULT_Y_OFFSET
|
||||
*/
|
||||
const HANDLE_Y_OFFSET = 20
|
||||
|
||||
/**
|
||||
* React Flow node component for the landing canvas
|
||||
* Styled to match the application's workflow blocks
|
||||
* @param props - Component properties containing node data
|
||||
* @returns A React Flow compatible node component
|
||||
*/
|
||||
export const LandingNode = React.memo(function LandingNode({ data }: { data: LandingCardData }) {
|
||||
const wrapperRef = React.useRef<HTMLDivElement | null>(null)
|
||||
const innerRef = React.useRef<HTMLDivElement | null>(null)
|
||||
const [isAnimated, setIsAnimated] = React.useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
const delay = (data as any)?.delay ?? 0
|
||||
const timer = setTimeout(() => {
|
||||
setIsAnimated(true)
|
||||
}, delay * 1000)
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}, [data])
|
||||
|
||||
// Check if this node should have a target handle (schedule node shouldn't)
|
||||
const hideTargetHandle = (data as any)?.hideTargetHandle || false
|
||||
// Check if this node should have a source handle (agent and function nodes shouldn't)
|
||||
const hideSourceHandle = (data as any)?.hideSourceHandle || false
|
||||
|
||||
return (
|
||||
<div ref={wrapperRef} className='relative cursor-grab active:cursor-grabbing'>
|
||||
{!hideTargetHandle && (
|
||||
<Handle
|
||||
type='target'
|
||||
position={Position.Left}
|
||||
style={{
|
||||
width: '7px',
|
||||
height: '20px',
|
||||
background: '#D1D1D1',
|
||||
border: 'none',
|
||||
borderRadius: '2px 0 0 2px',
|
||||
top: `${HANDLE_Y_OFFSET}px`,
|
||||
left: '-7px',
|
||||
transform: 'translateY(-50%)',
|
||||
zIndex: 10,
|
||||
}}
|
||||
isConnectable={false}
|
||||
/>
|
||||
)}
|
||||
{!hideSourceHandle && (
|
||||
<Handle
|
||||
type='source'
|
||||
position={Position.Right}
|
||||
style={{
|
||||
width: '7px',
|
||||
height: '20px',
|
||||
background: '#D1D1D1',
|
||||
border: 'none',
|
||||
borderRadius: '0 2px 2px 0',
|
||||
top: `${HANDLE_Y_OFFSET}px`,
|
||||
right: '-7px',
|
||||
transform: 'translateY(-50%)',
|
||||
zIndex: 10,
|
||||
}}
|
||||
isConnectable={false}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
ref={innerRef}
|
||||
className={isAnimated ? 'landing-node-animated' : 'landing-node-initial'}
|
||||
style={{
|
||||
opacity: isAnimated ? 1 : 0,
|
||||
transform: isAnimated ? 'translateY(0) scale(1)' : 'translateY(8px) scale(0.98)',
|
||||
transition:
|
||||
'opacity 0.6s cubic-bezier(0.22, 1, 0.36, 1), transform 0.6s cubic-bezier(0.22, 1, 0.36, 1)',
|
||||
willChange: isAnimated ? 'auto' : 'transform, opacity',
|
||||
}}
|
||||
>
|
||||
<LandingBlock icon={data.icon} color={data.color} name={data.name} tags={data.tags} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
@@ -1,66 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
/**
|
||||
* Props for the LoopBlock component
|
||||
*/
|
||||
export interface LoopBlockProps {
|
||||
/** Child elements to render inside the loop block */
|
||||
children?: React.ReactNode
|
||||
/** Optional CSS class names */
|
||||
className?: string
|
||||
/** Optional inline styles */
|
||||
style?: React.CSSProperties
|
||||
}
|
||||
|
||||
/**
|
||||
* Loop block container component that provides a styled container
|
||||
* for grouping related elements with a dashed border
|
||||
* Styled to match the application's subflow containers
|
||||
* @param props - Component properties including children and styling
|
||||
* @returns A styled loop container component
|
||||
*/
|
||||
export const LoopBlock = React.memo(function LoopBlock({
|
||||
children,
|
||||
className,
|
||||
style,
|
||||
}: LoopBlockProps) {
|
||||
return (
|
||||
<div
|
||||
className={`flex flex-shrink-0 ${className ?? ''}`}
|
||||
style={{
|
||||
width: '1198px',
|
||||
height: '528px',
|
||||
borderRadius: '8px',
|
||||
background: 'rgba(59, 130, 246, 0.08)',
|
||||
position: 'relative',
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
{/* Custom dashed border with SVG - 8px border radius to match blocks */}
|
||||
<svg
|
||||
className='pointer-events-none absolute inset-0 h-full w-full'
|
||||
style={{ borderRadius: '8px' }}
|
||||
preserveAspectRatio='none'
|
||||
>
|
||||
<path
|
||||
className='landing-loop-animated-dash'
|
||||
d='M 1190 527.5
|
||||
L 8 527.5
|
||||
A 7.5 7.5 0 0 1 0.5 520
|
||||
L 0.5 8
|
||||
A 7.5 7.5 0 0 1 8 0.5
|
||||
L 1190 0.5
|
||||
A 7.5 7.5 0 0 1 1197.5 8
|
||||
L 1197.5 520
|
||||
A 7.5 7.5 0 0 1 1190 527.5 Z'
|
||||
fill='none'
|
||||
stroke='#3B82F6'
|
||||
strokeWidth='1'
|
||||
strokeDasharray='8 8'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
</svg>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
@@ -1,49 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
/**
|
||||
* Properties for a subblock row component
|
||||
* Matches the SubBlockRow pattern from workflow-block.tsx
|
||||
*/
|
||||
export interface SubBlockRowProps {
|
||||
/** Icon element to display (optional, for visual context) */
|
||||
icon?: React.ReactNode
|
||||
/** Text label for the row title */
|
||||
label: string
|
||||
/** Optional value to display on the right side */
|
||||
value?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Kept for backwards compatibility
|
||||
*/
|
||||
export type TagProps = SubBlockRowProps
|
||||
|
||||
/**
|
||||
* SubBlockRow component matching the workflow block's subblock row style
|
||||
* @param props - Row properties including label and optional value
|
||||
* @returns A styled row component
|
||||
*/
|
||||
export const SubBlockRow = React.memo(function SubBlockRow({ label, value }: SubBlockRowProps) {
|
||||
// Split label by colon to separate title and value if present
|
||||
const [title, displayValue] = label.includes(':')
|
||||
? label.split(':').map((s) => s.trim())
|
||||
: [label, value]
|
||||
|
||||
return (
|
||||
<div className='flex items-center gap-2'>
|
||||
<span className='min-w-0 truncate text-[#888888] text-sm capitalize' title={title}>
|
||||
{title}
|
||||
</span>
|
||||
{displayValue && (
|
||||
<span className='flex-1 truncate text-right text-[#171717] text-sm' title={displayValue}>
|
||||
{displayValue}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* Tag component - alias for SubBlockRow for backwards compatibility
|
||||
*/
|
||||
export const Tag = SubBlockRow
|
||||
@@ -1,153 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import type { Edge, Node } from 'reactflow'
|
||||
import { ReactFlowProvider } from 'reactflow'
|
||||
import { DotPattern } from '@/app/(landing)/components/hero/components/landing-canvas/dot-pattern'
|
||||
import type { LandingCardData } from '@/app/(landing)/components/hero/components/landing-canvas/landing-block/landing-block'
|
||||
import { LandingFlow } from '@/app/(landing)/components/hero/components/landing-canvas/landing-flow'
|
||||
|
||||
/**
|
||||
* Visual constants for landing node dimensions
|
||||
* Matches BLOCK_DIMENSIONS from the application
|
||||
*/
|
||||
export const CARD_WIDTH = 250
|
||||
export const CARD_HEIGHT = 100
|
||||
|
||||
/**
|
||||
* Landing block node with positioning information
|
||||
*/
|
||||
export interface LandingBlockNode extends LandingCardData {
|
||||
/** Unique identifier for the node */
|
||||
id: string
|
||||
/** X coordinate position */
|
||||
x: number
|
||||
/** Y coordinate position */
|
||||
y: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Data structure for edges connecting nodes
|
||||
*/
|
||||
export interface LandingEdgeData {
|
||||
/** Unique identifier for the edge */
|
||||
id: string
|
||||
/** Source node ID */
|
||||
from: string
|
||||
/** Target node ID */
|
||||
to: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Data structure for grouping visual elements
|
||||
*/
|
||||
export interface LandingGroupData {
|
||||
/** X coordinate of the group */
|
||||
x: number
|
||||
/** Y coordinate of the group */
|
||||
y: number
|
||||
/** Width of the group */
|
||||
w: number
|
||||
/** Height of the group */
|
||||
h: number
|
||||
/** Labels associated with the group */
|
||||
labels: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Manual block with responsive positioning
|
||||
*/
|
||||
export interface LandingManualBlock extends Omit<LandingCardData, 'x' | 'y'> {
|
||||
/** Unique identifier */
|
||||
id: string
|
||||
/** Responsive position configurations */
|
||||
positions: {
|
||||
/** Position for mobile devices */
|
||||
mobile: { x: number; y: number }
|
||||
/** Position for tablet devices */
|
||||
tablet: { x: number; y: number }
|
||||
/** Position for desktop devices */
|
||||
desktop: { x: number; y: number }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Public API for controlling the viewport
|
||||
*/
|
||||
export interface LandingViewportApi {
|
||||
/**
|
||||
* Pan the viewport to specific coordinates
|
||||
* @param x - X coordinate to pan to
|
||||
* @param y - Y coordinate to pan to
|
||||
* @param options - Optional configuration for the pan animation
|
||||
*/
|
||||
panTo: (x: number, y: number, options?: { duration?: number }) => void
|
||||
/**
|
||||
* Get the current viewport state
|
||||
* @returns Current viewport position and zoom level
|
||||
*/
|
||||
getViewport: () => { x: number; y: number; zoom: number }
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for the LandingCanvas component
|
||||
*/
|
||||
export interface LandingCanvasProps {
|
||||
/** Array of nodes to render */
|
||||
nodes: Node[]
|
||||
/** Array of edges connecting nodes */
|
||||
edges: Edge[]
|
||||
/** Optional group box for visual grouping */
|
||||
groupBox: LandingGroupData | null
|
||||
/** Total width of the world/canvas */
|
||||
worldWidth: number
|
||||
/** Ref to expose viewport control API */
|
||||
viewportApiRef: React.MutableRefObject<LandingViewportApi | null>
|
||||
}
|
||||
|
||||
/**
|
||||
* Main landing canvas component that provides the container and background
|
||||
* for the React Flow visualization
|
||||
* @param props - Component properties including nodes, edges, and viewport control
|
||||
* @returns A canvas component with dot pattern background and React Flow content
|
||||
*/
|
||||
export function LandingCanvas({
|
||||
nodes,
|
||||
edges,
|
||||
groupBox,
|
||||
worldWidth,
|
||||
viewportApiRef,
|
||||
}: LandingCanvasProps) {
|
||||
const flowWrapRef = React.useRef<HTMLDivElement | null>(null)
|
||||
|
||||
return (
|
||||
<div className='relative mx-auto flex h-[612px] w-full max-w-[1285px] border-none bg-background/80'>
|
||||
<DotPattern className='pointer-events-none absolute inset-0 z-0 h-full w-full opacity-20' />
|
||||
|
||||
{/* Use template button overlay */}
|
||||
{/* <button
|
||||
type='button'
|
||||
aria-label='Use template'
|
||||
className='absolute top-6 left-[50px] z-20 inline-flex items-center justify-center rounded-[10px] border border-[#343434] bg-gradient-to-b from-[#060606] to-[#323232] px-3 py-1.5 text-sm text-white shadow-brand-inset transition-all duration-200'
|
||||
onClick={() => {
|
||||
// Template usage logic will be implemented here
|
||||
}}
|
||||
>
|
||||
Use template
|
||||
</button> */}
|
||||
|
||||
<div ref={flowWrapRef} className='relative z-10 h-full w-full'>
|
||||
<ReactFlowProvider>
|
||||
<LandingFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
groupBox={groupBox}
|
||||
worldWidth={worldWidth}
|
||||
wrapperRef={flowWrapRef}
|
||||
viewportApiRef={viewportApiRef}
|
||||
/>
|
||||
</ReactFlowProvider>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import { type EdgeProps, getSmoothStepPath, Position } from 'reactflow'
|
||||
|
||||
/**
|
||||
* Custom edge component with animated dashed line
|
||||
* Styled to match the application's workflow edges with rectangular handles
|
||||
* @param props - React Flow edge properties
|
||||
* @returns An animated dashed edge component
|
||||
*/
|
||||
export const LandingEdge = React.memo(function LandingEdge(props: EdgeProps) {
|
||||
const { id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, style } = props
|
||||
|
||||
// Adjust the connection points to connect flush with rectangular handles
|
||||
// Handle width is 7px, positioned at -7px from edge
|
||||
let adjustedSourceX = sourceX
|
||||
let adjustedTargetX = targetX
|
||||
|
||||
if (sourcePosition === Position.Right) {
|
||||
adjustedSourceX = sourceX + 1
|
||||
} else if (sourcePosition === Position.Left) {
|
||||
adjustedSourceX = sourceX - 1
|
||||
}
|
||||
|
||||
if (targetPosition === Position.Left) {
|
||||
adjustedTargetX = targetX - 1
|
||||
} else if (targetPosition === Position.Right) {
|
||||
adjustedTargetX = targetX + 1
|
||||
}
|
||||
|
||||
const [path] = getSmoothStepPath({
|
||||
sourceX: adjustedSourceX,
|
||||
sourceY,
|
||||
targetX: adjustedTargetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
borderRadius: 8,
|
||||
offset: 16,
|
||||
})
|
||||
|
||||
return (
|
||||
<g style={{ zIndex: 1 }}>
|
||||
<style>
|
||||
{`
|
||||
@keyframes landing-edge-dash-${id} {
|
||||
from {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
to {
|
||||
stroke-dashoffset: -12;
|
||||
}
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<path
|
||||
id={id}
|
||||
d={path}
|
||||
fill='none'
|
||||
className='react-flow__edge-path'
|
||||
style={{
|
||||
stroke: '#D1D1D1',
|
||||
strokeWidth: 2,
|
||||
strokeDasharray: '6 6',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
pointerEvents: 'none',
|
||||
animation: `landing-edge-dash-${id} 1s linear infinite`,
|
||||
...style,
|
||||
}}
|
||||
/>
|
||||
</g>
|
||||
)
|
||||
})
|
||||
@@ -1,151 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import ReactFlow, { applyNodeChanges, type NodeChange, useReactFlow } from 'reactflow'
|
||||
import 'reactflow/dist/style.css'
|
||||
import { LandingLoopNode } from '@/app/(landing)/components/hero/components/landing-canvas/landing-block/landing-loop-node'
|
||||
import { LandingNode } from '@/app/(landing)/components/hero/components/landing-canvas/landing-block/landing-node'
|
||||
import {
|
||||
CARD_WIDTH,
|
||||
type LandingCanvasProps,
|
||||
} from '@/app/(landing)/components/hero/components/landing-canvas/landing-canvas'
|
||||
import { LandingEdge } from '@/app/(landing)/components/hero/components/landing-canvas/landing-edge/landing-edge'
|
||||
|
||||
/**
|
||||
* Props for the LandingFlow component
|
||||
*/
|
||||
export interface LandingFlowProps extends LandingCanvasProps {
|
||||
/** Reference to the wrapper element */
|
||||
wrapperRef: React.RefObject<HTMLDivElement | null>
|
||||
}
|
||||
|
||||
/**
|
||||
* React Flow wrapper component for the landing canvas
|
||||
* Handles viewport control, auto-panning, and node/edge rendering
|
||||
* @param props - Component properties including nodes, edges, and viewport control
|
||||
* @returns A configured React Flow instance
|
||||
*/
|
||||
export function LandingFlow({
|
||||
nodes,
|
||||
edges,
|
||||
groupBox,
|
||||
worldWidth,
|
||||
wrapperRef,
|
||||
viewportApiRef,
|
||||
}: LandingFlowProps) {
|
||||
const { setViewport, getViewport } = useReactFlow()
|
||||
const [rfReady, setRfReady] = React.useState(false)
|
||||
const [localNodes, setLocalNodes] = React.useState(nodes)
|
||||
|
||||
// Update local nodes when props change
|
||||
React.useEffect(() => {
|
||||
setLocalNodes(nodes)
|
||||
}, [nodes])
|
||||
|
||||
// Handle node changes (dragging)
|
||||
const onNodesChange = React.useCallback((changes: NodeChange[]) => {
|
||||
setLocalNodes((nds) => applyNodeChanges(changes, nds))
|
||||
}, [])
|
||||
|
||||
// Node and edge types map
|
||||
const nodeTypes = React.useMemo(
|
||||
() => ({
|
||||
landing: LandingNode,
|
||||
landingLoop: LandingLoopNode,
|
||||
group: LandingLoopNode, // Use our custom loop node for group type
|
||||
}),
|
||||
[]
|
||||
)
|
||||
const edgeTypes = React.useMemo(() => ({ landingEdge: LandingEdge }), [])
|
||||
|
||||
// Compose nodes with optional group overlay
|
||||
const flowNodes = localNodes
|
||||
|
||||
// Auto-pan to the right only if content overflows the wrapper
|
||||
React.useEffect(() => {
|
||||
const el = wrapperRef.current as HTMLDivElement | null
|
||||
if (!el || !rfReady || localNodes.length === 0) return
|
||||
|
||||
const containerWidth = el.clientWidth
|
||||
// Derive overflow from actual node positions for accuracy
|
||||
const PAD = 16
|
||||
const maxRight = localNodes.reduce((m, n) => Math.max(m, (n.position?.x ?? 0) + CARD_WIDTH), 0)
|
||||
const contentWidth = Math.max(worldWidth, maxRight + PAD)
|
||||
const overflow = Math.max(0, contentWidth - containerWidth)
|
||||
|
||||
// Delay pan so initial nodes are visible briefly
|
||||
const timer = window.setTimeout(() => {
|
||||
if (overflow > 12) {
|
||||
setViewport({ x: -overflow, y: 0, zoom: 1 }, { duration: 900 })
|
||||
}
|
||||
}, 1400)
|
||||
|
||||
return () => window.clearTimeout(timer)
|
||||
}, [worldWidth, wrapperRef, setViewport, rfReady, localNodes])
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
nodes={flowNodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
defaultEdgeOptions={{ type: 'smoothstep' }}
|
||||
elementsSelectable={true}
|
||||
selectNodesOnDrag={false}
|
||||
nodesDraggable={true}
|
||||
nodesConnectable={false}
|
||||
zoomOnScroll={false}
|
||||
zoomOnDoubleClick={false}
|
||||
panOnScroll={false}
|
||||
zoomOnPinch={false}
|
||||
panOnDrag={false}
|
||||
draggable={false}
|
||||
preventScrolling={false}
|
||||
autoPanOnNodeDrag={false}
|
||||
proOptions={{ hideAttribution: true }}
|
||||
fitView={false}
|
||||
defaultViewport={{ x: 0, y: 0, zoom: 1 }}
|
||||
onInit={(instance) => {
|
||||
setRfReady(true)
|
||||
// Expose limited viewport API for outer timeline to pan smoothly
|
||||
viewportApiRef.current = {
|
||||
panTo: (x: number, y: number, options?: { duration?: number }) => {
|
||||
setViewport({ x, y, zoom: 1 }, { duration: options?.duration ?? 0 })
|
||||
},
|
||||
getViewport: () => getViewport(),
|
||||
}
|
||||
}}
|
||||
className='h-full w-full'
|
||||
style={{
|
||||
// Override React Flow's default cursor styles
|
||||
cursor: 'default',
|
||||
}}
|
||||
>
|
||||
<style>
|
||||
{`
|
||||
/* Force default cursor on the canvas/pane */
|
||||
.react-flow__pane {
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
/* Force grab cursor on nodes */
|
||||
.react-flow__node {
|
||||
cursor: grab !important;
|
||||
}
|
||||
|
||||
/* Force grabbing cursor when dragging nodes */
|
||||
.react-flow__node.dragging {
|
||||
cursor: grabbing !important;
|
||||
}
|
||||
|
||||
/* Ensure viewport also has default cursor */
|
||||
.react-flow__viewport {
|
||||
cursor: default !important;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
{null}
|
||||
</ReactFlow>
|
||||
)
|
||||
}
|
||||
@@ -1,467 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import { ArrowUp, CodeIcon } from 'lucide-react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { type Edge, type Node, Position } from 'reactflow'
|
||||
import {
|
||||
AgentIcon,
|
||||
AirtableIcon,
|
||||
DiscordIcon,
|
||||
GmailIcon,
|
||||
GoogleDriveIcon,
|
||||
GoogleSheetsIcon,
|
||||
JiraIcon,
|
||||
LinearIcon,
|
||||
NotionIcon,
|
||||
OutlookIcon,
|
||||
PackageSearchIcon,
|
||||
PineconeIcon,
|
||||
ScheduleIcon,
|
||||
SlackIcon,
|
||||
StripeIcon,
|
||||
SupabaseIcon,
|
||||
} from '@/components/icons'
|
||||
import { LandingPromptStorage } from '@/lib/core/utils/browser-storage'
|
||||
import {
|
||||
CARD_WIDTH,
|
||||
IconButton,
|
||||
LandingCanvas,
|
||||
type LandingGroupData,
|
||||
type LandingManualBlock,
|
||||
type LandingViewportApi,
|
||||
} from '@/app/(landing)/components/hero/components'
|
||||
|
||||
/**
|
||||
* Service-specific template messages for the hero input
|
||||
*/
|
||||
const SERVICE_TEMPLATES = {
|
||||
slack: 'Summarizer agent that summarizes each new message in #general and sends me a DM',
|
||||
gmail: 'Alert agent that flags important Gmail messages in my inbox',
|
||||
outlook:
|
||||
'Auto-forwarding agent that classifies each new Outlook email and forwards to separate inboxes for further analysis',
|
||||
pinecone: 'RAG chat agent that uses memories stored in Pinecone',
|
||||
supabase: 'Natural language to SQL agent to query and update data in Supabase',
|
||||
linear: 'Agent that uses Linear to triage issues, assign owners, and draft updates',
|
||||
discord: 'Moderator agent that responds back to users in my Discord server',
|
||||
airtable: 'Alert agent that validates each new record in a table and prepares a weekly report',
|
||||
stripe: 'Agent that analyzes Stripe payment history to spot churn risks and generate summaries',
|
||||
notion: 'Support agent that appends new support tickets to my Notion workspace',
|
||||
googleSheets: 'Data science agent that analyzes Google Sheets data and generates insights',
|
||||
googleDrive: 'Drive reader agent that summarizes content in my Google Drive',
|
||||
jira: 'Engineering manager agent that uses Jira to update ticket statuses, generate sprint reports, and identify blockers',
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Landing blocks for the canvas preview
|
||||
* Styled to match the application's workflow blocks with subblock rows
|
||||
*/
|
||||
const LANDING_BLOCKS: LandingManualBlock[] = [
|
||||
{
|
||||
id: 'schedule',
|
||||
name: 'Schedule',
|
||||
color: '#7B68EE',
|
||||
icon: <ScheduleIcon className='h-[16px] w-[16px] text-white' />,
|
||||
positions: {
|
||||
mobile: { x: 8, y: 60 },
|
||||
tablet: { x: 40, y: 120 },
|
||||
desktop: { x: 60, y: 180 },
|
||||
},
|
||||
tags: [{ label: 'Time: 09:00AM Daily' }, { label: 'Timezone: PST' }],
|
||||
},
|
||||
{
|
||||
id: 'knowledge',
|
||||
name: 'Knowledge',
|
||||
color: '#00B0B0',
|
||||
icon: <PackageSearchIcon className='h-[16px] w-[16px] text-white' />,
|
||||
positions: {
|
||||
mobile: { x: 120, y: 140 },
|
||||
tablet: { x: 220, y: 200 },
|
||||
desktop: { x: 420, y: 241 },
|
||||
},
|
||||
tags: [{ label: 'Source: Product Vector DB' }, { label: 'Limit: 10' }],
|
||||
},
|
||||
{
|
||||
id: 'agent',
|
||||
name: 'Agent',
|
||||
color: '#802FFF',
|
||||
icon: <AgentIcon className='h-[16px] w-[16px] text-white' />,
|
||||
positions: {
|
||||
mobile: { x: 340, y: 60 },
|
||||
tablet: { x: 540, y: 120 },
|
||||
desktop: { x: 880, y: 142 },
|
||||
},
|
||||
tags: [{ label: 'Model: gpt-5' }, { label: 'Prompt: You are a support ag...' }],
|
||||
},
|
||||
{
|
||||
id: 'function',
|
||||
name: 'Function',
|
||||
color: '#FF402F',
|
||||
icon: <CodeIcon className='h-[16px] w-[16px] text-white' />,
|
||||
positions: {
|
||||
mobile: { x: 480, y: 220 },
|
||||
tablet: { x: 740, y: 280 },
|
||||
desktop: { x: 880, y: 340 },
|
||||
},
|
||||
tags: [{ label: 'Language: Python' }, { label: 'Code: time = "2025-09-01...' }],
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* Sample workflow edges for the canvas preview
|
||||
*/
|
||||
const SAMPLE_WORKFLOW_EDGES = [
|
||||
{ id: 'e1', from: 'schedule', to: 'knowledge' },
|
||||
{ id: 'e2', from: 'knowledge', to: 'agent' },
|
||||
{ id: 'e3', from: 'knowledge', to: 'function' },
|
||||
]
|
||||
|
||||
/**
|
||||
* Hero component for the landing page featuring service integrations and workflow preview
|
||||
*/
|
||||
export default function Hero() {
|
||||
const router = useRouter()
|
||||
|
||||
/**
|
||||
* State management for the text input
|
||||
*/
|
||||
const [textValue, setTextValue] = React.useState('')
|
||||
const isEmpty = textValue.trim().length === 0
|
||||
|
||||
/**
|
||||
* State for responsive icon display
|
||||
*/
|
||||
const [visibleIconCount, setVisibleIconCount] = React.useState(13)
|
||||
const [isMobile, setIsMobile] = React.useState(false)
|
||||
|
||||
/**
|
||||
* React Flow state for workflow preview canvas
|
||||
*/
|
||||
const [rfNodes, setRfNodes] = React.useState<Node[]>([])
|
||||
const [rfEdges, setRfEdges] = React.useState<Edge[]>([])
|
||||
const [groupBox, setGroupBox] = React.useState<LandingGroupData | null>(null)
|
||||
const [worldWidth, setWorldWidth] = React.useState<number>(1000)
|
||||
const viewportApiRef = React.useRef<LandingViewportApi | null>(null)
|
||||
|
||||
/**
|
||||
* Auto-hover animation state
|
||||
*/
|
||||
const [autoHoverIndex, setAutoHoverIndex] = React.useState(1)
|
||||
const [isUserHovering, setIsUserHovering] = React.useState(false)
|
||||
const [lastHoveredIndex, setLastHoveredIndex] = React.useState<number | null>(null)
|
||||
const intervalRef = React.useRef<NodeJS.Timeout | null>(null)
|
||||
|
||||
/**
|
||||
* Handle service icon click to populate textarea with template
|
||||
*/
|
||||
const handleServiceClick = (service: keyof typeof SERVICE_TEMPLATES) => {
|
||||
setTextValue(SERVICE_TEMPLATES[service])
|
||||
}
|
||||
|
||||
/**
|
||||
* Set visible icon count based on screen size
|
||||
*/
|
||||
React.useEffect(() => {
|
||||
const updateVisibleIcons = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const mobile = window.innerWidth < 640
|
||||
setVisibleIconCount(mobile ? 6 : 13)
|
||||
setIsMobile(mobile)
|
||||
}
|
||||
}
|
||||
|
||||
updateVisibleIcons()
|
||||
window.addEventListener('resize', updateVisibleIcons)
|
||||
|
||||
return () => window.removeEventListener('resize', updateVisibleIcons)
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* Service icons array for easier indexing
|
||||
*/
|
||||
const serviceIcons: Array<{
|
||||
key: string
|
||||
icon: React.ComponentType<{ className?: string }>
|
||||
label: string
|
||||
style?: React.CSSProperties
|
||||
}> = [
|
||||
{ key: 'slack', icon: SlackIcon, label: 'Slack' },
|
||||
{ key: 'gmail', icon: GmailIcon, label: 'Gmail' },
|
||||
{ key: 'outlook', icon: OutlookIcon, label: 'Outlook' },
|
||||
{ key: 'pinecone', icon: PineconeIcon, label: 'Pinecone' },
|
||||
{ key: 'supabase', icon: SupabaseIcon, label: 'Supabase' },
|
||||
{ key: 'linear', icon: LinearIcon, label: 'Linear', style: { color: '#5E6AD2' } },
|
||||
{ key: 'discord', icon: DiscordIcon, label: 'Discord', style: { color: '#5765F2' } },
|
||||
{ key: 'airtable', icon: AirtableIcon, label: 'Airtable' },
|
||||
{ key: 'stripe', icon: StripeIcon, label: 'Stripe', style: { color: '#635BFF' } },
|
||||
{ key: 'notion', icon: NotionIcon, label: 'Notion' },
|
||||
{ key: 'googleSheets', icon: GoogleSheetsIcon, label: 'Google Sheets' },
|
||||
{ key: 'googleDrive', icon: GoogleDriveIcon, label: 'Google Drive' },
|
||||
{ key: 'jira', icon: JiraIcon, label: 'Jira' },
|
||||
]
|
||||
|
||||
/**
|
||||
* Auto-hover animation effect
|
||||
*/
|
||||
React.useEffect(() => {
|
||||
// Start the interval when component mounts
|
||||
const startInterval = () => {
|
||||
intervalRef.current = setInterval(() => {
|
||||
setAutoHoverIndex((prev) => (prev + 1) % visibleIconCount)
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
// Only run interval when user is not hovering
|
||||
if (!isUserHovering) {
|
||||
startInterval()
|
||||
}
|
||||
|
||||
// Cleanup on unmount or when hovering state changes
|
||||
return () => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current)
|
||||
}
|
||||
}
|
||||
}, [isUserHovering, visibleIconCount])
|
||||
|
||||
/**
|
||||
* Handle mouse enter on icon container
|
||||
*/
|
||||
const handleIconContainerMouseEnter = () => {
|
||||
setIsUserHovering(true)
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle mouse leave on icon container
|
||||
*/
|
||||
const handleIconContainerMouseLeave = () => {
|
||||
setIsUserHovering(false)
|
||||
// Start from the next icon after the last hovered one
|
||||
if (lastHoveredIndex !== null) {
|
||||
setAutoHoverIndex((lastHoveredIndex + 1) % visibleIconCount)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle form submission
|
||||
*/
|
||||
const handleSubmit = () => {
|
||||
if (!isEmpty) {
|
||||
LandingPromptStorage.store(textValue)
|
||||
router.push('/signup')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle keyboard shortcuts (Enter to submit)
|
||||
*/
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
if (!isEmpty) {
|
||||
handleSubmit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize workflow preview with sample data
|
||||
*/
|
||||
React.useEffect(() => {
|
||||
// Determine breakpoint for responsive positioning
|
||||
const breakpoint =
|
||||
typeof window !== 'undefined' && window.innerWidth < 640
|
||||
? 'mobile'
|
||||
: typeof window !== 'undefined' && window.innerWidth < 1024
|
||||
? 'tablet'
|
||||
: 'desktop'
|
||||
|
||||
// Convert landing blocks to React Flow nodes
|
||||
const nodes: Node[] = [
|
||||
// Add the loop block node as a group with custom rendering
|
||||
{
|
||||
id: 'loop',
|
||||
type: 'group',
|
||||
position: { x: 720, y: 20 },
|
||||
data: {
|
||||
label: 'Loop',
|
||||
},
|
||||
draggable: false,
|
||||
selectable: false,
|
||||
focusable: false,
|
||||
connectable: false,
|
||||
// Group node properties for subflow functionality
|
||||
style: {
|
||||
width: 1198,
|
||||
height: 528,
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
padding: 0,
|
||||
},
|
||||
},
|
||||
// Convert blocks to nodes
|
||||
...LANDING_BLOCKS.map((block, index) => {
|
||||
// Make agent and function nodes children of the loop
|
||||
const isLoopChild = block.id === 'agent' || block.id === 'function'
|
||||
const baseNode = {
|
||||
id: block.id,
|
||||
type: 'landing',
|
||||
position: isLoopChild
|
||||
? {
|
||||
// Adjust positions relative to loop parent (original positions - loop position)
|
||||
x: block.id === 'agent' ? 160 : 160,
|
||||
y: block.id === 'agent' ? 122 : 320,
|
||||
}
|
||||
: block.positions[breakpoint],
|
||||
data: {
|
||||
icon: block.icon,
|
||||
color: block.color,
|
||||
name: block.name,
|
||||
tags: block.tags,
|
||||
delay: index * 0.18,
|
||||
hideTargetHandle: block.id === 'schedule', // Hide target handle for schedule node
|
||||
hideSourceHandle: block.id === 'agent' || block.id === 'function', // Hide source handle for agent and function nodes
|
||||
},
|
||||
sourcePosition: Position.Right,
|
||||
targetPosition: Position.Left,
|
||||
}
|
||||
|
||||
// Add parent properties for loop children
|
||||
if (isLoopChild) {
|
||||
return {
|
||||
...baseNode,
|
||||
parentId: 'loop',
|
||||
extent: 'parent',
|
||||
}
|
||||
}
|
||||
|
||||
return baseNode
|
||||
}),
|
||||
]
|
||||
|
||||
// Convert sample edges to React Flow edges
|
||||
const rfEdges: Edge[] = SAMPLE_WORKFLOW_EDGES.map((e) => ({
|
||||
id: e.id,
|
||||
source: e.from,
|
||||
target: e.to,
|
||||
type: 'landingEdge',
|
||||
animated: false,
|
||||
data: { delay: 0.6 },
|
||||
}))
|
||||
|
||||
setRfNodes(nodes)
|
||||
setRfEdges(rfEdges)
|
||||
|
||||
// Calculate world width for canvas
|
||||
const maxX = Math.max(...nodes.map((n) => n.position.x))
|
||||
setWorldWidth(maxX + CARD_WIDTH + 32)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<section
|
||||
id='hero'
|
||||
className='flex w-full flex-col items-center justify-center pt-9 sm:pt-20'
|
||||
aria-labelledby='hero-heading'
|
||||
>
|
||||
<h1
|
||||
id='hero-heading'
|
||||
className='text-balance px-4 text-center font-medium text-[36px] leading-none tracking-tight sm:px-0 sm:text-[74px]'
|
||||
>
|
||||
Workflows for LLMs
|
||||
</h1>
|
||||
<p className='px-4 pt-1.5 text-center text-lg opacity-70 sm:px-0 sm:pt-2.5 sm:text-[22px]'>
|
||||
Build and deploy AI agent workflows
|
||||
</p>
|
||||
<div
|
||||
className='flex items-center justify-center gap-0.5 pt-4.5 sm:pt-8'
|
||||
onMouseEnter={handleIconContainerMouseEnter}
|
||||
onMouseLeave={handleIconContainerMouseLeave}
|
||||
>
|
||||
{/* Service integration buttons */}
|
||||
{serviceIcons.slice(0, visibleIconCount).map((service, index) => {
|
||||
const Icon = service.icon
|
||||
return (
|
||||
<IconButton
|
||||
key={service.key}
|
||||
aria-label={service.label}
|
||||
onClick={() => handleServiceClick(service.key as keyof typeof SERVICE_TEMPLATES)}
|
||||
onMouseEnter={() => setLastHoveredIndex(index)}
|
||||
style={service.style}
|
||||
isAutoHovered={!isUserHovering && index === autoHoverIndex}
|
||||
>
|
||||
<Icon className='h-5 w-5 sm:h-6 sm:w-6' />
|
||||
</IconButton>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div className='flex w-full items-center justify-center px-4 pt-2 sm:px-8 sm:pt-3 md:px-[50px]'>
|
||||
<div className='relative w-full sm:w-[640px]'>
|
||||
<label htmlFor='agent-description' className='sr-only'>
|
||||
Describe the AI agent you want to build
|
||||
</label>
|
||||
<textarea
|
||||
id='agent-description'
|
||||
placeholder={
|
||||
isMobile ? 'Build an AI agent...' : 'Ask Sim to build an agent to read my emails...'
|
||||
}
|
||||
className='h-[100px] w-full resize-none px-3 py-2.5 text-sm sm:h-[120px] sm:px-4 sm:py-3 sm:text-base'
|
||||
value={textValue}
|
||||
onChange={(e) => setTextValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
style={{
|
||||
borderRadius: 16,
|
||||
border: 'var(--border-width-border, 1px) solid #E5E5E5',
|
||||
outline: 'none',
|
||||
background: '#FFFFFF',
|
||||
boxShadow:
|
||||
'var(--shadow-xs-offset-x, 0) var(--shadow-xs-offset-y, 2px) var(--shadow-xs-blur-radius, 4px) var(--shadow-xs-spread-radius, 0) var(--shadow-xs-color, rgba(0, 0, 0, 0.08))',
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
key={isEmpty ? 'empty' : 'filled'}
|
||||
type='button'
|
||||
aria-label='Submit description'
|
||||
className='absolute right-2.5 bottom-4 flex h-[30px] w-[30px] items-center justify-center transition-all duration-200 sm:right-[11px] sm:bottom-4 sm:h-[34px] sm:w-[34px]'
|
||||
disabled={isEmpty}
|
||||
onClick={handleSubmit}
|
||||
style={{
|
||||
padding: '3.75px 3.438px 3.75px 4.063px',
|
||||
borderRadius: 55,
|
||||
...(isEmpty
|
||||
? {
|
||||
border: '0.625px solid #E0E0E0',
|
||||
background: '#E5E5E5',
|
||||
boxShadow: 'none',
|
||||
cursor: 'not-allowed',
|
||||
}
|
||||
: {
|
||||
border: '0.625px solid #343434',
|
||||
background: 'linear-gradient(180deg, #060606 0%, #323232 100%)',
|
||||
boxShadow: '0 1.25px 2.5px 0 #9B77FF inset',
|
||||
cursor: 'pointer',
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<ArrowUp size={18} className='sm:h-5 sm:w-5' color={isEmpty ? '#999999' : '#FFFFFF'} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Canvas - hidden on mobile */}
|
||||
{!isMobile && (
|
||||
<div className='mt-[60px] w-full max-w-[1308px] sm:mt-[127.5px]'>
|
||||
<LandingCanvas
|
||||
nodes={rfNodes}
|
||||
edges={rfEdges}
|
||||
groupBox={groupBox}
|
||||
worldWidth={worldWidth}
|
||||
viewportApiRef={viewportApiRef}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -1,25 +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'
|
||||
import LandingPricing from '@/app/(landing)/components/landing-pricing/landing-pricing'
|
||||
import LandingTemplates from '@/app/(landing)/components/landing-templates/landing-templates'
|
||||
import LegalLayout from '@/app/(landing)/components/legal-layout'
|
||||
import Nav from '@/app/(landing)/components/nav/nav'
|
||||
import StructuredData from '@/app/(landing)/components/structured-data'
|
||||
import Testimonials from '@/app/(landing)/components/testimonials/testimonials'
|
||||
|
||||
export {
|
||||
Integrations,
|
||||
Testimonials,
|
||||
LandingTemplates,
|
||||
Nav,
|
||||
Background,
|
||||
Hero,
|
||||
LandingPricing,
|
||||
Footer,
|
||||
StructuredData,
|
||||
LegalLayout,
|
||||
ExternalRedirect,
|
||||
}
|
||||
export { LegalLayout, ExternalRedirect }
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
import * as Icons from '@/components/icons'
|
||||
|
||||
const modelProviderIcons = [
|
||||
{ icon: Icons.OpenAIIcon, label: 'OpenAI' },
|
||||
{ icon: Icons.AnthropicIcon, label: 'Anthropic' },
|
||||
{ icon: Icons.GeminiIcon, label: 'Gemini' },
|
||||
{ icon: Icons.MistralIcon, label: 'Mistral' },
|
||||
{ icon: Icons.PerplexityIcon, label: 'Perplexity' },
|
||||
{ icon: Icons.xAIIcon, label: 'xAI' },
|
||||
{ icon: Icons.GroqIcon, label: 'Groq' },
|
||||
{ icon: Icons.HuggingFaceIcon, label: 'HuggingFace' },
|
||||
{ icon: Icons.OllamaIcon, label: 'Ollama' },
|
||||
{ icon: Icons.DeepseekIcon, label: 'Deepseek' },
|
||||
{ icon: Icons.ElevenLabsIcon, label: 'ElevenLabs' },
|
||||
{ icon: Icons.VllmIcon, label: 'vLLM' },
|
||||
]
|
||||
|
||||
const communicationIcons = [
|
||||
{ icon: Icons.SlackIcon, label: 'Slack' },
|
||||
{ icon: Icons.GmailIcon, label: 'Gmail' },
|
||||
{ icon: Icons.OutlookIcon, label: 'Outlook' },
|
||||
{ icon: Icons.DiscordIcon, label: 'Discord', style: { color: '#5765F2' } },
|
||||
{ icon: Icons.LinearIcon, label: 'Linear', style: { color: '#5E6AD2' } },
|
||||
{ icon: Icons.NotionIcon, label: 'Notion' },
|
||||
{ icon: Icons.JiraIcon, label: 'Jira' },
|
||||
{ icon: Icons.ConfluenceIcon, label: 'Confluence' },
|
||||
{ icon: Icons.TelegramIcon, label: 'Telegram' },
|
||||
{ icon: Icons.GoogleCalendarIcon, label: 'Google Calendar' },
|
||||
{ icon: Icons.CalendlyIcon, label: 'Calendly' },
|
||||
{ icon: Icons.GoogleDocsIcon, label: 'Google Docs' },
|
||||
{ icon: Icons.BrowserUseIcon, label: 'BrowserUse' },
|
||||
{ icon: Icons.TypeformIcon, label: 'Typeform' },
|
||||
{ icon: Icons.GithubIcon, label: 'GitHub' },
|
||||
{ icon: Icons.GoogleSheetsIcon, label: 'Google Sheets' },
|
||||
{ icon: Icons.GoogleDriveIcon, label: 'Google Drive' },
|
||||
{ icon: Icons.AirtableIcon, label: 'Airtable' },
|
||||
]
|
||||
|
||||
const dataStorageIcons = [
|
||||
{ icon: Icons.PineconeIcon, label: 'Pinecone' },
|
||||
{ icon: Icons.SupabaseIcon, label: 'Supabase' },
|
||||
{ icon: Icons.PostgresIcon, label: 'PostgreSQL' },
|
||||
{ icon: Icons.MySQLIcon, label: 'MySQL' },
|
||||
{ icon: Icons.QdrantIcon, label: 'Qdrant' },
|
||||
{ icon: Icons.MicrosoftOneDriveIcon, label: 'OneDrive' },
|
||||
{ icon: Icons.MicrosoftSharepointIcon, label: 'SharePoint' },
|
||||
{ icon: Icons.SerperIcon, label: 'Serper' },
|
||||
{ icon: Icons.FirecrawlIcon, label: 'Firecrawl' },
|
||||
{ icon: Icons.StripeIcon, label: 'Stripe' },
|
||||
]
|
||||
|
||||
interface IntegrationBoxProps {
|
||||
icon?: React.ComponentType<{ className?: string }>
|
||||
style?: React.CSSProperties
|
||||
isVisible: boolean
|
||||
}
|
||||
|
||||
function IntegrationBox({ icon: Icon, style, isVisible }: IntegrationBoxProps) {
|
||||
return (
|
||||
<div
|
||||
className='flex h-[72px] w-[72px] items-center justify-center transition-all duration-300'
|
||||
style={{
|
||||
borderRadius: '12px',
|
||||
border: '1px solid var(--base-border, #E5E5E5)',
|
||||
background: 'var(--base-card, #FEFEFE)',
|
||||
opacity: isVisible ? 1 : 0.75,
|
||||
boxShadow: isVisible ? '0 2px 4px 0 rgba(0, 0, 0, 0.08)' : 'none',
|
||||
}}
|
||||
>
|
||||
{Icon && isVisible && (
|
||||
<div style={style}>
|
||||
<Icon className='h-8 w-8' />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface TickerRowProps {
|
||||
direction: 'left' | 'right'
|
||||
offset: number
|
||||
showOdd: boolean
|
||||
icons: Array<{
|
||||
icon: React.ComponentType<{ className?: string }>
|
||||
label: string
|
||||
style?: React.CSSProperties
|
||||
}>
|
||||
}
|
||||
|
||||
function TickerRow({ direction, offset, showOdd, icons }: TickerRowProps) {
|
||||
const extendedIcons = [...icons, ...icons, ...icons, ...icons]
|
||||
|
||||
return (
|
||||
<div className='relative h-[88px] w-full overflow-hidden'>
|
||||
<div
|
||||
className={`absolute flex items-center gap-4 ${
|
||||
direction === 'left' ? 'animate-slide-left' : 'animate-slide-right'
|
||||
}`}
|
||||
style={{
|
||||
animationDelay: `${offset}s`,
|
||||
}}
|
||||
>
|
||||
{extendedIcons.map((service, index) => {
|
||||
const isOdd = index % 2 === 1
|
||||
const shouldShow = showOdd ? isOdd : !isOdd
|
||||
return (
|
||||
<IntegrationBox
|
||||
key={`${service.label}-${index}`}
|
||||
icon={service.icon}
|
||||
style={service.style}
|
||||
isVisible={shouldShow}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Integrations() {
|
||||
return (
|
||||
<section
|
||||
id='integrations'
|
||||
className='flex flex-col pt-10 pb-[27px] sm:pt-6'
|
||||
aria-labelledby='integrations-heading'
|
||||
>
|
||||
<h2
|
||||
id='integrations-heading'
|
||||
className='mb-1 px-4 font-medium text-[28px] text-foreground tracking-tight sm:pl-[50px]'
|
||||
>
|
||||
Integrations
|
||||
</h2>
|
||||
<p className='mb-6 px-4 text-[#515151] text-lg sm:pl-[50px]'>
|
||||
Immediately connect to 100+ models and apps
|
||||
</p>
|
||||
|
||||
{/* Sliding tickers */}
|
||||
<div className='flex w-full flex-col sm:px-3'>
|
||||
<TickerRow direction='left' offset={0} showOdd={false} icons={modelProviderIcons} />
|
||||
<TickerRow direction='right' offset={0.5} showOdd={true} icons={communicationIcons} />
|
||||
<TickerRow direction='left' offset={1} showOdd={false} icons={dataStorageIcons} />
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
63
apps/sim/app/(landing)/components/landing-faq.tsx
Normal file
63
apps/sim/app/(landing)/components/landing-faq.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { ChevronDown } from '@/components/emcn'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
|
||||
export interface LandingFAQItem {
|
||||
question: string
|
||||
answer: string
|
||||
}
|
||||
|
||||
interface LandingFAQProps {
|
||||
faqs: LandingFAQItem[]
|
||||
}
|
||||
|
||||
export function LandingFAQ({ faqs }: LandingFAQProps) {
|
||||
const [openIndex, setOpenIndex] = useState<number | null>(0)
|
||||
|
||||
return (
|
||||
<div className='divide-y divide-[var(--landing-border)]'>
|
||||
{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-[var(--landing-text)]'
|
||||
: 'text-[var(--landing-text-muted)] hover:text-[var(--landing-text)]'
|
||||
)}
|
||||
>
|
||||
{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-[14px] text-[var(--landing-text-muted)] leading-[1.75]'>
|
||||
{answer}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,238 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import type { ComponentType, SVGProps } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import {
|
||||
ArrowRight,
|
||||
ChevronRight,
|
||||
Code2,
|
||||
Database,
|
||||
DollarSign,
|
||||
HardDrive,
|
||||
type LucideIcon,
|
||||
RefreshCw,
|
||||
Timer,
|
||||
Zap,
|
||||
} from 'lucide-react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { ENTERPRISE_PLAN_FEATURES } from '@/app/workspace/[workspaceId]/settings/components/subscription/plan-configs'
|
||||
|
||||
const logger = createLogger('LandingPricing')
|
||||
|
||||
interface PricingFeature {
|
||||
icon: LucideIcon | ComponentType<SVGProps<SVGSVGElement>>
|
||||
text: string
|
||||
}
|
||||
|
||||
interface PricingTier {
|
||||
name: string
|
||||
tier: string
|
||||
price: string
|
||||
features: PricingFeature[]
|
||||
ctaText: string
|
||||
featured?: boolean
|
||||
}
|
||||
|
||||
const FREE_PLAN_FEATURES: PricingFeature[] = [
|
||||
{ icon: DollarSign, text: '1,000 credits (trial)' },
|
||||
{ icon: HardDrive, text: '5GB file storage' },
|
||||
{ icon: Timer, text: '5 min execution limit' },
|
||||
{ icon: Database, text: 'Limited log retention' },
|
||||
{ icon: Code2, text: 'CLI/SDK Access' },
|
||||
]
|
||||
|
||||
const PRO_LANDING_FEATURES: PricingFeature[] = [
|
||||
{ icon: DollarSign, text: '6,000 credits/mo' },
|
||||
{ icon: RefreshCw, text: '+50 daily refresh credits' },
|
||||
{ icon: Zap, text: '150 runs/min (sync)' },
|
||||
{ icon: Timer, text: '50 min sync execution limit' },
|
||||
{ icon: HardDrive, text: '50GB file storage' },
|
||||
]
|
||||
|
||||
const MAX_LANDING_FEATURES: PricingFeature[] = [
|
||||
{ icon: DollarSign, text: '25,000 credits/mo' },
|
||||
{ icon: RefreshCw, text: '+200 daily refresh credits' },
|
||||
{ icon: Zap, text: '300 runs/min (sync)' },
|
||||
{ icon: Timer, text: '50 min sync execution limit' },
|
||||
{ icon: HardDrive, text: '500GB file storage' },
|
||||
]
|
||||
|
||||
const pricingTiers: PricingTier[] = [
|
||||
{
|
||||
name: 'COMMUNITY',
|
||||
tier: 'Free',
|
||||
price: 'Free',
|
||||
features: FREE_PLAN_FEATURES,
|
||||
ctaText: 'Get Started',
|
||||
},
|
||||
{
|
||||
name: 'PRO',
|
||||
tier: 'Pro',
|
||||
price: '$25/mo',
|
||||
features: PRO_LANDING_FEATURES,
|
||||
ctaText: 'Get Started',
|
||||
featured: true,
|
||||
},
|
||||
{
|
||||
name: 'MAX',
|
||||
tier: 'Max',
|
||||
price: '$100/mo',
|
||||
features: MAX_LANDING_FEATURES,
|
||||
ctaText: 'Get Started',
|
||||
},
|
||||
{
|
||||
name: 'ENTERPRISE',
|
||||
tier: 'Enterprise',
|
||||
price: 'Custom',
|
||||
features: ENTERPRISE_PLAN_FEATURES,
|
||||
ctaText: 'Contact Sales',
|
||||
},
|
||||
]
|
||||
|
||||
function PricingCard({
|
||||
tier,
|
||||
isBeforeFeatured,
|
||||
}: {
|
||||
tier: PricingTier
|
||||
isBeforeFeatured?: boolean
|
||||
}) {
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
const router = useRouter()
|
||||
|
||||
const handleCtaClick = () => {
|
||||
logger.info(`Pricing CTA clicked: ${tier.name}`)
|
||||
|
||||
if (tier.ctaText === 'Contact Sales') {
|
||||
window.open('https://form.typeform.com/to/jqCO12pF', '_blank')
|
||||
} else {
|
||||
router.push('/signup')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'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',
|
||||
tier.featured
|
||||
? 'sm:p-0'
|
||||
: isBeforeFeatured
|
||||
? 'sm:border-[#E7E4EF] sm:border-r-0'
|
||||
: 'sm:border-[#E7E4EF] sm:border-r-2 sm:last:border-r-0',
|
||||
!tier.featured && !isBeforeFeatured && 'lg:[&:nth-child(4n)]:border-r-0',
|
||||
!tier.featured &&
|
||||
!isBeforeFeatured &&
|
||||
'sm:[&:nth-child(2n)]:border-r-0 lg:[&:nth-child(2n)]:border-r-2',
|
||||
tier.featured ? 'z-10 bg-gradient-to-b from-[#8357FF] to-[#6F3DFA] text-white' : ''
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-full flex-col justify-between',
|
||||
tier.featured
|
||||
? 'border-2 border-[#6F3DFA] px-5 pt-4 pb-5 shadow-[inset_0_2px_4px_0_#9B77FF] sm:px-5 sm:pt-4 sm:pb-4'
|
||||
: ''
|
||||
)}
|
||||
>
|
||||
<div className='flex-1'>
|
||||
<div className='mb-1'>
|
||||
<span
|
||||
className={cn(
|
||||
'font-medium text-xs uppercase tracking-wider',
|
||||
tier.featured ? 'text-white/90' : 'text-gray-500'
|
||||
)}
|
||||
>
|
||||
{tier.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className='mb-6'>
|
||||
<span
|
||||
className={cn(
|
||||
'font-medium text-4xl leading-none',
|
||||
tier.featured ? 'text-white' : 'text-black'
|
||||
)}
|
||||
>
|
||||
{tier.price}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ul className='mb-0.5 space-y-3'>
|
||||
{tier.features.map((feature, idx) => (
|
||||
<li key={idx} className='flex items-start gap-2'>
|
||||
<feature.icon
|
||||
className={cn(
|
||||
'mt-0.5 h-4 w-4 flex-shrink-0',
|
||||
tier.featured ? 'text-white/90' : 'text-gray-600'
|
||||
)}
|
||||
/>
|
||||
<span className={cn('text-sm', tier.featured ? 'text-white' : 'text-gray-700')}>
|
||||
{feature.text}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className='mt-9'>
|
||||
{tier.featured ? (
|
||||
<button
|
||||
onClick={handleCtaClick}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
className='group inline-flex w-full items-center justify-center gap-2 rounded-[10px] border border-[#E8E8E8] bg-gradient-to-b from-[#F8F8F8] to-white px-3 py-1.5 font-medium text-[#6F3DFA] text-[14px] shadow-[inset_0_2px_4px_0_rgba(255,255,255,0.9)] transition-all'
|
||||
>
|
||||
<span className='flex items-center gap-1'>
|
||||
{tier.ctaText}
|
||||
<span className='inline-flex transition-transform duration-200 group-hover:translate-x-0.5'>
|
||||
{isHovered ? (
|
||||
<ArrowRight className='h-4 w-4' />
|
||||
) : (
|
||||
<ChevronRight className='h-4 w-4' />
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={handleCtaClick}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
className='group inline-flex w-full items-center justify-center gap-2 rounded-[10px] border border-[#343434] bg-gradient-to-b from-[#060606] to-[#323232] px-3 py-1.5 font-medium text-[14px] text-white shadow-[inset_0_1.25px_2.5px_0_#9B77FF] transition-all'
|
||||
>
|
||||
<span className='flex items-center gap-1'>
|
||||
{tier.ctaText}
|
||||
<span className='inline-flex transition-transform duration-200 group-hover:translate-x-0.5'>
|
||||
{isHovered ? (
|
||||
<ArrowRight className='h-4 w-4' />
|
||||
) : (
|
||||
<ChevronRight className='h-4 w-4' />
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Pricing grid with all tier cards. Rendered as a client component because
|
||||
* the tier data contains component references (icon functions) which are
|
||||
* not serializable across the RSC boundary.
|
||||
*/
|
||||
export function PricingGrid() {
|
||||
return (
|
||||
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2 sm:gap-0 lg:grid-cols-4'>
|
||||
{pricingTiers.map((tier, index) => {
|
||||
const nextTier = pricingTiers[index + 1]
|
||||
const isBeforeFeatured = nextTier?.featured
|
||||
return <PricingCard key={tier.name} tier={tier} isBeforeFeatured={isBeforeFeatured} />
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { PricingGrid } from '@/app/(landing)/components/landing-pricing/components/pricing-card'
|
||||
|
||||
/**
|
||||
* Landing page pricing section displaying tiered pricing plans
|
||||
*/
|
||||
export default function LandingPricing() {
|
||||
return (
|
||||
<section id='pricing' className='px-4 pt-[23px] sm:px-0 sm:pt-1' aria-label='Pricing plans'>
|
||||
<h2 className='sr-only'>Pricing Plans</h2>
|
||||
<div className='relative mx-auto w-full max-w-[1289px]'>
|
||||
<PricingGrid />
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user