Compare commits
101 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d889f32697 | ||
|
|
7f1efcc798 | ||
|
|
a680cec78f | ||
|
|
235f0748ca | ||
|
|
ebc19484f2 | ||
|
|
33e6921954 | ||
|
|
adfcb67dc2 | ||
|
|
f9a7c4538e | ||
|
|
28af223a9f | ||
|
|
d0baf5b1df | ||
|
|
855c892f55 | ||
|
|
8ae4b88d80 | ||
|
|
a70ccddef5 | ||
|
|
b4d9b8c396 | ||
|
|
ce53275e9d | ||
|
|
7971a64e63 | ||
|
|
f39b4c74dc | ||
|
|
0ba8ab1ec7 | ||
|
|
039e57541e | ||
|
|
75f8c6ad7e | ||
|
|
a54dcbe949 | ||
|
|
c2b12cf21f | ||
|
|
4a9439e952 | ||
|
|
893e322a49 | ||
|
|
b0cb95be2f | ||
|
|
0b9019d9a2 | ||
|
|
6d00d6bf2c | ||
|
|
3267d8cc24 | ||
|
|
2e69f85364 | ||
|
|
57e5bac121 | ||
|
|
8ce0299400 | ||
|
|
a0796f088b | ||
|
|
98fe4cd40b | ||
|
|
34d210c66c | ||
|
|
2334f2dca4 | ||
|
|
65fc138bfc | ||
|
|
e8f7fe0989 | ||
|
|
ace87791d8 | ||
|
|
74af452175 | ||
|
|
ec51f73596 | ||
|
|
6866da590c | ||
|
|
b0c0ee29a8 | ||
|
|
20c05644ab | ||
|
|
f9d73db65c | ||
|
|
e2e53aba76 | ||
|
|
f0d1950477 | ||
|
|
727bb1cadb | ||
|
|
e2e29cefd7 | ||
|
|
0fdd8ffb55 | ||
|
|
45f053a383 | ||
|
|
225d5d551a | ||
|
|
a78f3f9c2e | ||
|
|
080a0a6123 | ||
|
|
fc6fe193fa | ||
|
|
bbc704fe05 | ||
|
|
c016537564 | ||
|
|
4c94f3cf78 | ||
|
|
27a11a269d | ||
|
|
2c174ca4f6 | ||
|
|
ac831b85b2 | ||
|
|
8527ae5d3b | ||
|
|
076c835ba2 | ||
|
|
df6ceb61a4 | ||
|
|
2ede12aa0e | ||
|
|
42fb434354 | ||
|
|
d581009099 | ||
|
|
dcebe3ae97 | ||
|
|
e39c534ee3 | ||
|
|
b95a0491a0 | ||
|
|
a79c8a75ce | ||
|
|
282ec8c58c | ||
|
|
e45fbe0184 | ||
|
|
512558dcb3 | ||
|
|
35411e465e | ||
|
|
72e28baa07 | ||
|
|
d99dd86bf2 | ||
|
|
7898e5d75f | ||
|
|
df62502903 | ||
|
|
1a2aa6949e | ||
|
|
4544fd4519 | ||
|
|
019630bdc8 | ||
|
|
7d0fdefb22 | ||
|
|
90f592797a | ||
|
|
d091441e39 | ||
|
|
7d4dd26760 | ||
|
|
0abeac77e1 | ||
|
|
e9c94fa462 | ||
|
|
72eea64bf6 | ||
|
|
27460f847c | ||
|
|
c7643198dc | ||
|
|
e5aef6184a | ||
|
|
4ae5b1b620 | ||
|
|
5c334874eb | ||
|
|
d3d58a9615 | ||
|
|
1d59eca90a | ||
|
|
e1359b09d6 | ||
|
|
35b3646330 | ||
|
|
73e00f53e1 | ||
|
|
5c47ea58f8 | ||
|
|
1d7ae906bc | ||
|
|
c4f4e6b48c |
@@ -9,5 +9,26 @@ Use TSDoc for documentation. No `====` separators. No non-TSDoc comments.
|
||||
## Styling
|
||||
Never update global styles. Keep all styling local to components.
|
||||
|
||||
## ID Generation
|
||||
Never use `crypto.randomUUID()`, `nanoid`, or the `uuid` package directly. Use the utilities from `@/lib/core/utils/uuid`:
|
||||
|
||||
- `generateId()` — UUID v4, use by default
|
||||
- `generateShortId(size?)` — short URL-safe ID (default 21 chars), for compact identifiers
|
||||
|
||||
Both use `crypto.getRandomValues()` under the hood and work in all contexts including non-secure (HTTP) browsers.
|
||||
|
||||
```typescript
|
||||
// ✗ Bad
|
||||
import { nanoid } from 'nanoid'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
const id = crypto.randomUUID()
|
||||
|
||||
// ✓ Good
|
||||
import { generateId, generateShortId } from '@/lib/core/utils/uuid'
|
||||
const uuid = generateId()
|
||||
const shortId = generateShortId()
|
||||
const tiny = generateShortId(8)
|
||||
```
|
||||
|
||||
## Package Manager
|
||||
Use `bun` and `bunx`, not `npm` and `npx`.
|
||||
|
||||
@@ -16,5 +16,26 @@ Use TSDoc for documentation. No `====` separators. No non-TSDoc comments.
|
||||
## Styling
|
||||
Never update global styles. Keep all styling local to components.
|
||||
|
||||
## ID Generation
|
||||
Never use `crypto.randomUUID()`, `nanoid`, or the `uuid` package directly. Use the utilities from `@/lib/core/utils/uuid`:
|
||||
|
||||
- `generateId()` — UUID v4, use by default
|
||||
- `generateShortId(size?)` — short URL-safe ID (default 21 chars), for compact identifiers
|
||||
|
||||
Both use `crypto.getRandomValues()` under the hood and work in all contexts including non-secure (HTTP) browsers.
|
||||
|
||||
```typescript
|
||||
// ✗ Bad
|
||||
import { nanoid } from 'nanoid'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
const id = crypto.randomUUID()
|
||||
|
||||
// ✓ Good
|
||||
import { generateId, generateShortId } from '@/lib/core/utils/uuid'
|
||||
const uuid = generateId()
|
||||
const shortId = generateShortId()
|
||||
const tiny = generateShortId(8)
|
||||
```
|
||||
|
||||
## Package Manager
|
||||
Use `bun` and `bunx`, not `npm` and `npx`.
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -7,6 +7,7 @@ You are a professional software engineer. All code must follow best practices: a
|
||||
- **Logging**: Import `createLogger` from `@sim/logger`. Use `logger.info`, `logger.warn`, `logger.error` instead of `console.log`
|
||||
- **Comments**: Use TSDoc for documentation. No `====` separators. No non-TSDoc comments
|
||||
- **Styling**: Never update global styles. Keep all styling local to components
|
||||
- **ID Generation**: Never use `crypto.randomUUID()`, `nanoid`, or `uuid` package. Use `generateId()` (UUID v4) or `generateShortId()` (compact) from `@/lib/core/utils/uuid`
|
||||
- **Package Manager**: Use `bun` and `bunx`, not `npm` and `npx`
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -7,6 +7,7 @@ You are a professional software engineer. All code must follow best practices: a
|
||||
- **Logging**: Import `createLogger` from `@sim/logger`. Use `logger.info`, `logger.warn`, `logger.error` instead of `console.log`
|
||||
- **Comments**: Use TSDoc for documentation. No `====` separators. No non-TSDoc comments
|
||||
- **Styling**: Never update global styles. Keep all styling local to components
|
||||
- **ID Generation**: Never use `crypto.randomUUID()`, `nanoid`, or `uuid` package. Use `generateId()` (UUID v4) or `generateShortId()` (compact) from `@/lib/core/utils/uuid`
|
||||
- **Package Manager**: Use `bun` and `bunx`, not `npm` and `npx`
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -90,6 +90,7 @@ Sim also supports local models via [Ollama](https://ollama.ai) and [vLLM](https:
|
||||
git clone https://github.com/simstudioai/sim.git
|
||||
cd sim
|
||||
bun install
|
||||
bun run prepare # Set up pre-commit hooks
|
||||
```
|
||||
|
||||
2. Set up PostgreSQL with pgvector:
|
||||
@@ -104,6 +105,11 @@ Or install manually via the [pgvector guide](https://github.com/pgvector/pgvecto
|
||||
|
||||
```bash
|
||||
cp apps/sim/.env.example apps/sim/.env
|
||||
# Create your secrets
|
||||
perl -i -pe "s/your_encryption_key/$(openssl rand -hex 32)/" apps/sim/.env
|
||||
perl -i -pe "s/your_internal_api_secret/$(openssl rand -hex 32)/" apps/sim/.env
|
||||
perl -i -pe "s/your_api_encryption_key/$(openssl rand -hex 32)/" apps/sim/.env
|
||||
# DB configs for migration
|
||||
cp packages/db/.env.example packages/db/.env
|
||||
# Edit both .env files to set DATABASE_URL="postgresql://postgres:your_password@localhost:5432/simstudio"
|
||||
```
|
||||
@@ -111,7 +117,7 @@ cp packages/db/.env.example packages/db/.env
|
||||
4. Run migrations:
|
||||
|
||||
```bash
|
||||
cd packages/db && bunx drizzle-kit migrate --config=./drizzle.config.ts
|
||||
cd packages/db && bun run db:migrate
|
||||
```
|
||||
|
||||
5. Start development servers:
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import type { ComponentType, SVGProps } from 'react'
|
||||
import {
|
||||
A2AIcon,
|
||||
AgentMailIcon,
|
||||
AhrefsIcon,
|
||||
AirtableIcon,
|
||||
AirweaveIcon,
|
||||
@@ -26,7 +27,9 @@ import {
|
||||
CirclebackIcon,
|
||||
ClayIcon,
|
||||
ClerkIcon,
|
||||
CloudFormationIcon,
|
||||
CloudflareIcon,
|
||||
CloudWatchIcon,
|
||||
ConfluenceIcon,
|
||||
CursorIcon,
|
||||
DatabricksIcon,
|
||||
@@ -45,6 +48,7 @@ import {
|
||||
EnrichSoIcon,
|
||||
EvernoteIcon,
|
||||
ExaAIIcon,
|
||||
ExtendIcon,
|
||||
EyeIcon,
|
||||
FathomIcon,
|
||||
FirecrawlIcon,
|
||||
@@ -91,6 +95,7 @@ import {
|
||||
KalshiIcon,
|
||||
KetchIcon,
|
||||
LangsmithIcon,
|
||||
LaunchDarklyIcon,
|
||||
LemlistIcon,
|
||||
LinearIcon,
|
||||
LinkedInIcon,
|
||||
@@ -126,6 +131,7 @@ import {
|
||||
PolymarketIcon,
|
||||
PostgresIcon,
|
||||
PosthogIcon,
|
||||
ProfoundIcon,
|
||||
PulseIcon,
|
||||
QdrantIcon,
|
||||
QuiverIcon,
|
||||
@@ -136,9 +142,11 @@ import {
|
||||
ResendIcon,
|
||||
RevenueCatIcon,
|
||||
RipplingIcon,
|
||||
RootlyIcon,
|
||||
S3Icon,
|
||||
SalesforceIcon,
|
||||
SearchIcon,
|
||||
SecretsManagerIcon,
|
||||
SendgridIcon,
|
||||
SentryIcon,
|
||||
SerperIcon,
|
||||
@@ -154,6 +162,7 @@ import {
|
||||
StagehandIcon,
|
||||
StripeIcon,
|
||||
SupabaseIcon,
|
||||
TailscaleIcon,
|
||||
TavilyIcon,
|
||||
TelegramIcon,
|
||||
TextractIcon,
|
||||
@@ -183,6 +192,7 @@ type IconComponent = ComponentType<SVGProps<SVGSVGElement>>
|
||||
|
||||
export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
a2a: A2AIcon,
|
||||
agentmail: AgentMailIcon,
|
||||
ahrefs: AhrefsIcon,
|
||||
airtable: AirtableIcon,
|
||||
airweave: AirweaveIcon,
|
||||
@@ -203,6 +213,8 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
clay: ClayIcon,
|
||||
clerk: ClerkIcon,
|
||||
cloudflare: CloudflareIcon,
|
||||
cloudformation: CloudFormationIcon,
|
||||
cloudwatch: CloudWatchIcon,
|
||||
confluence_v2: ConfluenceIcon,
|
||||
cursor_v2: CursorIcon,
|
||||
databricks: DatabricksIcon,
|
||||
@@ -220,6 +232,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
enrich: EnrichSoIcon,
|
||||
evernote: EvernoteIcon,
|
||||
exa: ExaAIIcon,
|
||||
extend_v2: ExtendIcon,
|
||||
fathom: FathomIcon,
|
||||
file_v3: DocumentIcon,
|
||||
firecrawl: FirecrawlIcon,
|
||||
@@ -268,6 +281,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
ketch: KetchIcon,
|
||||
knowledge: PackageSearchIcon,
|
||||
langsmith: LangsmithIcon,
|
||||
launchdarkly: LaunchDarklyIcon,
|
||||
lemlist: LemlistIcon,
|
||||
linear: LinearIcon,
|
||||
linkedin: LinkedInIcon,
|
||||
@@ -302,6 +316,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
polymarket: PolymarketIcon,
|
||||
postgresql: PostgresIcon,
|
||||
posthog: PosthogIcon,
|
||||
profound: ProfoundIcon,
|
||||
pulse_v2: PulseIcon,
|
||||
qdrant: QdrantIcon,
|
||||
quiver: QuiverIcon,
|
||||
@@ -312,9 +327,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,
|
||||
@@ -331,6 +348,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
|
||||
|
||||
150
apps/docs/content/docs/en/blocks/credential.mdx
Normal file
@@ -0,0 +1,150 @@
|
||||
---
|
||||
title: Credential
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
|
||||
import { Image } from '@/components/ui/image'
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
The Credential block has two operations: **Select Credential** picks a single OAuth credential and outputs its ID reference for downstream blocks; **List Credentials** returns all OAuth credentials in the workspace (optionally filtered by provider) as an array for iteration.
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/blocks/credential.png"
|
||||
alt="Credential Block"
|
||||
width={400}
|
||||
height={300}
|
||||
className="my-6"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Callout>
|
||||
The Credential block outputs credential **ID references**, not secrets. Downstream blocks receive the ID and resolve the actual OAuth token securely during their own execution.
|
||||
</Callout>
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Operation
|
||||
|
||||
| Value | Description |
|
||||
|---|---|
|
||||
| **Select Credential** | Pick one OAuth credential and output its reference — use this to wire a single credential into downstream blocks |
|
||||
| **List Credentials** | Return all OAuth credentials in the workspace as an array — use this with a ForEach loop |
|
||||
|
||||
### Credential (Select operation)
|
||||
|
||||
Select an OAuth credential from your workspace. The dropdown shows all connected OAuth accounts (Google, GitHub, Slack, etc.).
|
||||
|
||||
In advanced mode, paste a credential ID directly. You can copy a credential ID from your workspace's Credentials settings page.
|
||||
|
||||
### Provider (List operation)
|
||||
|
||||
Filter the returned OAuth credentials by provider. Select one or more providers from the dropdown — only providers you have credentials for will appear. Leave empty to return all OAuth credentials.
|
||||
|
||||
| Example | Returns |
|
||||
|---|---|
|
||||
| Gmail | Gmail credentials only |
|
||||
| Slack | Slack credentials only |
|
||||
| Gmail + Slack | Gmail and Slack credentials |
|
||||
|
||||
## Outputs
|
||||
|
||||
<Tabs items={['Select Credential', 'List Credentials']}>
|
||||
<Tab>
|
||||
| Output | Type | Description |
|
||||
|---|---|---|
|
||||
| `credentialId` | `string` | The credential ID — pipe this into other blocks' credential fields |
|
||||
| `displayName` | `string` | Human-readable name (e.g. "waleed@company.com") |
|
||||
| `providerId` | `string` | OAuth provider ID (e.g. `google-email`, `slack`) |
|
||||
</Tab>
|
||||
<Tab>
|
||||
| Output | Type | Description |
|
||||
|---|---|---|
|
||||
| `credentials` | `json` | Array of OAuth credential objects (see shape below) |
|
||||
| `count` | `number` | Number of credentials returned |
|
||||
|
||||
Each object in the `credentials` array:
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `credentialId` | `string` | The credential ID |
|
||||
| `displayName` | `string` | Human-readable name |
|
||||
| `providerId` | `string` | OAuth provider ID |
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Example Use Cases
|
||||
|
||||
**Shared credential across multiple blocks** — Define once, use everywhere
|
||||
```
|
||||
Credential (Select, Google) → Gmail (Send) & Google Drive (Upload) & Google Calendar (Create)
|
||||
```
|
||||
|
||||
**Multi-account workflows** — Route to different credentials based on logic
|
||||
```
|
||||
Agent (Determine account) → Condition → Credential A or Credential B → Slack (Post)
|
||||
```
|
||||
|
||||
**Iterate over all Gmail accounts**
|
||||
```
|
||||
Credential (List, Provider: Gmail) → ForEach Loop → Gmail (Send) using <loop.currentItem.credentialId>
|
||||
```
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/blocks/credential-loop.png"
|
||||
alt="Credential List wired into a ForEach Loop"
|
||||
width={900}
|
||||
height={400}
|
||||
className="my-6"
|
||||
/>
|
||||
</div>
|
||||
|
||||
## How to wire a Credential block
|
||||
|
||||
### Select Credential
|
||||
|
||||
1. Drop a **Credential** block and select your OAuth credential from the picker
|
||||
2. In the downstream block, switch to **advanced mode** on its credential field
|
||||
3. Enter `<credentialBlockName.credentialId>` as the value
|
||||
|
||||
<Tabs items={['Gmail', 'Slack']}>
|
||||
<Tab>
|
||||
In the Gmail block's credential field (advanced mode):
|
||||
```
|
||||
<myCredential.credentialId>
|
||||
```
|
||||
</Tab>
|
||||
<Tab>
|
||||
In the Slack block's credential field (advanced mode):
|
||||
```
|
||||
<myCredential.credentialId>
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### List Credentials
|
||||
|
||||
1. Drop a **Credential** block, set Operation to **List Credentials**
|
||||
2. Optionally select one or more **Providers** to narrow results (only your connected providers appear)
|
||||
3. Wire `<credentialBlockName.credentials>` into a **ForEach Loop** as the items source
|
||||
4. Inside the loop, reference `<loop.currentItem.credentialId>` in downstream blocks' credential fields
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Define once, reference many times**: When five blocks use the same Google account, use one Credential block and wire all five to `<credential.credentialId>` instead of selecting the account five times
|
||||
- **Outputs are safe to log**: The `credentialId` output is a UUID reference, not a secret. It is safe to inspect in execution logs
|
||||
- **Use for environment switching**: Pair with a Condition block to route to a production or staging OAuth credential based on a workflow variable
|
||||
- **Advanced mode is required**: Downstream blocks must be in advanced mode on their credential field to accept a dynamic reference
|
||||
- **Use List + ForEach for fan-out**: When you need to run the same action across all accounts of a provider, List Credentials feeds naturally into a ForEach loop
|
||||
- **Narrow by provider**: Use the Provider multiselect to filter to specific services — only providers you have credentials for are shown
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "Does the Credential block expose my secret or token?", answer: "No. The block outputs a credential ID (a UUID), not the actual OAuth token. Downstream blocks receive the ID and resolve the token securely in their own execution context. Secrets never appear in workflow state, logs, or the canvas." },
|
||||
{ question: "What credential types does it support?", answer: "OAuth connected accounts only (Google, GitHub, Slack, etc.). Environment variables and service accounts cannot be resolved by ID in downstream blocks, so they are not supported." },
|
||||
{ question: "How is Select different from just copying a credential ID into advanced mode?", answer: "Functionally identical — both pass the same credential ID to the downstream block. The Credential block adds value when you need to use one credential in many blocks (change it once), or when you want to select between credentials dynamically using a Condition block." },
|
||||
{ question: "Can I list all OAuth credentials in my workspace?", answer: "Yes. Set the Operation to 'List Credentials'. Optionally filter by provider using the Provider multiselect. Wire the credentials output into a ForEach loop to process each credential individually." },
|
||||
{ question: "Can I use a Credential block output in a Function block?", answer: "Yes. Reference <credential.credentialId> in your Function block's code. Note that the function will receive the raw UUID string — if you need the resolved token, the downstream block must handle the resolution (as integration blocks do). The Function block does not automatically resolve credential IDs." },
|
||||
{ question: "What happens if the credential is deleted?", answer: "The Select operation will throw an error at execution time: 'Credential not found'. The List operation will simply omit the deleted credential from the results. Update the Credential block to select a valid credential before re-running." },
|
||||
]} />
|
||||
@@ -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
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"agent",
|
||||
"api",
|
||||
"condition",
|
||||
"credential",
|
||||
"evaluator",
|
||||
"function",
|
||||
"guardrails",
|
||||
|
||||
@@ -20,7 +20,7 @@ The Response block formats and sends structured HTTP responses back to API calle
|
||||
</div>
|
||||
|
||||
<Callout type="info">
|
||||
Response blocks are terminal blocks - they end workflow execution and cannot connect to other blocks.
|
||||
Response blocks are exit points — when a Response block executes, it ends the workflow and sends the HTTP response immediately. Multiple Response blocks can be placed on different branches (e.g. after a Router or Condition), but only the first one to execute determines the API response.
|
||||
</Callout>
|
||||
|
||||
## Configuration Options
|
||||
@@ -77,7 +77,11 @@ Condition (Error Detected) → Router → Response (400/500, Error Details)
|
||||
|
||||
## Outputs
|
||||
|
||||
Response blocks are terminal — no downstream blocks execute after them. However, the block does define outputs (`data`, `status`, `headers`) which are used to construct the HTTP response sent back to the API caller.
|
||||
Response blocks are exit points — when one executes, no further blocks run. The block defines outputs (`data`, `status`, `headers`) which are used to construct the HTTP response sent back to the API caller.
|
||||
|
||||
<Callout type="warning">
|
||||
If a Response block is placed on a parallel branch, there are no guarantees about whether other parallel blocks will run or not. Execution order across parallel branches is non-deterministic, so a parallel block may execute before or after the Response block on any given run. Avoid placing Response blocks in parallel with blocks that have important side effects.
|
||||
</Callout>
|
||||
|
||||
## Variable References
|
||||
|
||||
@@ -110,10 +114,10 @@ Use the `<variable.name>` syntax to dynamically insert workflow variables into y
|
||||
- **Validate variable references**: Ensure all referenced variables exist and contain the expected data types before the Response block executes
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "Can I have multiple Response blocks in a workflow?", answer: "No. The Response block is a single-instance block — only one is allowed per workflow. If you need different responses for different conditions, use a Condition or Router block upstream to determine what data reaches the single Response block." },
|
||||
{ question: "Can I have multiple Response blocks in a workflow?", answer: "Yes. You can place multiple Response blocks on different branches (e.g. after a Router or Condition block). The first Response block to execute determines the API response and ends the workflow. This is useful for returning different responses based on conditions — for example, a 200 on the success branch and a 500 on the error branch." },
|
||||
{ question: "What triggers require a Response block?", answer: "The Response block is designed for use with the API Trigger. When your workflow is invoked via the API, the Response block sends the structured HTTP response back to the caller. Other trigger types (like webhooks or schedules) do not require a Response block." },
|
||||
{ question: "What is the difference between Builder and Editor mode?", answer: "Builder mode provides a visual interface for constructing your response structure with fields and types. Editor mode gives you a raw JSON code editor where you can write the response body directly. Builder mode is recommended for most use cases." },
|
||||
{ question: "What is the default status code?", answer: "If you do not specify a status code, the Response block defaults to 200 (OK). You can set any valid HTTP status code including error codes like 400, 404, or 500." },
|
||||
{ question: "Can the Response block connect to downstream blocks?", answer: "No. Response blocks are terminal — they end workflow execution and send the HTTP response. No further blocks can be connected after a Response block." },
|
||||
{ question: "Can the Response block connect to downstream blocks?", answer: "No. Response blocks are exit points — they end workflow execution and send the HTTP response. No further blocks can execute after a Response block." },
|
||||
]} />
|
||||
|
||||
|
||||
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
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"title": "Credentials",
|
||||
"pages": ["index", "google-service-account"],
|
||||
"defaultOpen": false
|
||||
}
|
||||
@@ -96,8 +96,9 @@ Understanding these core principles will help you build better workflows:
|
||||
2. **Automatic Parallelization**: Independent blocks run concurrently without configuration
|
||||
3. **Smart Data Flow**: Outputs flow automatically to connected blocks
|
||||
4. **Error Handling**: Failed blocks stop their execution path but don't affect independent paths
|
||||
5. **State Persistence**: All block outputs and execution details are preserved for debugging
|
||||
6. **Cycle Protection**: Workflows that call other workflows (via Workflow blocks, MCP tools, or API blocks) are tracked with a call chain. If the chain exceeds 25 hops, execution is stopped to prevent infinite loops
|
||||
5. **Response Blocks as Exit Points**: When a Response block executes, the entire workflow stops and the API response is sent immediately. Multiple Response blocks can exist on different branches — the first one to execute wins
|
||||
6. **State Persistence**: All block outputs and execution details are preserved for debugging
|
||||
7. **Cycle Protection**: Workflows that call other workflows (via Workflow blocks, MCP tools, or API blocks) are tracked with a call chain. If the chain exceeds 25 hops, execution is stopped to prevent infinite loops
|
||||
|
||||
## Next Steps
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
183
apps/docs/content/docs/en/tools/cloudformation.mdx
Normal file
@@ -0,0 +1,183 @@
|
||||
---
|
||||
title: CloudFormation
|
||||
description: Manage and inspect AWS CloudFormation stacks, resources, and drift
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="cloudformation"
|
||||
color="linear-gradient(45deg, #B0084D 0%, #FF4F8B 100%)"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[AWS CloudFormation](https://aws.amazon.com/cloudformation/) is an infrastructure-as-code service that lets you model, provision, and manage AWS resources by treating infrastructure as code. CloudFormation uses templates to describe the resources you need and their dependencies, so you can launch and configure them together as a stack.
|
||||
|
||||
With the CloudFormation integration, you can:
|
||||
|
||||
- **Describe Stacks**: List all stacks in a region or get detailed information about a specific stack, including its status, outputs, tags, and drift information
|
||||
- **List Stack Resources**: Enumerate every resource in a stack with its logical ID, physical ID, type, status, and drift status
|
||||
- **Describe Stack Events**: View the full event history for a stack to understand what happened during create, update, or delete operations
|
||||
- **Detect Stack Drift**: Initiate drift detection to check whether any resources in a stack have been modified outside of CloudFormation
|
||||
- **Drift Detection Status**: Poll the results of a drift detection operation to see which resources have drifted and how many
|
||||
- **Get Template**: Retrieve the original template body (JSON or YAML) used to create or update a stack
|
||||
- **Validate Template**: Check a CloudFormation template for syntax errors, required capabilities, parameters, and declared transforms before deploying
|
||||
|
||||
In Sim, the CloudFormation integration enables your agents to monitor infrastructure state, detect configuration drift, audit stack resources, and validate templates as part of automated SRE and DevOps workflows. This is especially powerful when combined with CloudWatch for observability and SNS for alerting, creating end-to-end infrastructure monitoring pipelines.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate AWS CloudFormation into workflows. Describe stacks, list resources, detect drift, view stack events, retrieve templates, and validate templates. Requires AWS access key and secret access key.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `cloudformation_describe_stacks`
|
||||
|
||||
List and describe CloudFormation stacks
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `awsAccessKeyId` | string | Yes | AWS access key ID |
|
||||
| `awsSecretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `stackName` | string | No | Stack name or ID to describe \(omit to list all stacks\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `stacks` | array | List of CloudFormation stacks with status, outputs, and tags |
|
||||
|
||||
### `cloudformation_list_stack_resources`
|
||||
|
||||
List all resources in a CloudFormation stack
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `awsAccessKeyId` | string | Yes | AWS access key ID |
|
||||
| `awsSecretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `stackName` | string | Yes | Stack name or ID |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `resources` | array | List of stack resources with type, status, and drift information |
|
||||
|
||||
### `cloudformation_detect_stack_drift`
|
||||
|
||||
Initiate drift detection on a CloudFormation stack
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `awsAccessKeyId` | string | Yes | AWS access key ID |
|
||||
| `awsSecretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `stackName` | string | Yes | Stack name or ID to detect drift on |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `stackDriftDetectionId` | string | ID to use with Describe Stack Drift Detection Status to check results |
|
||||
|
||||
### `cloudformation_describe_stack_drift_detection_status`
|
||||
|
||||
Check the status of a stack drift detection operation
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `awsAccessKeyId` | string | Yes | AWS access key ID |
|
||||
| `awsSecretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `stackDriftDetectionId` | string | Yes | The drift detection ID returned by Detect Stack Drift |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `stackId` | string | The stack ID |
|
||||
| `stackDriftDetectionId` | string | The drift detection ID |
|
||||
| `stackDriftStatus` | string | Drift status \(DRIFTED, IN_SYNC, NOT_CHECKED\) |
|
||||
| `detectionStatus` | string | Detection status \(DETECTION_IN_PROGRESS, DETECTION_COMPLETE, DETECTION_FAILED\) |
|
||||
| `detectionStatusReason` | string | Reason if detection failed |
|
||||
| `driftedStackResourceCount` | number | Number of resources that have drifted |
|
||||
| `timestamp` | number | Timestamp of the detection |
|
||||
|
||||
### `cloudformation_describe_stack_events`
|
||||
|
||||
Get the event history for a CloudFormation stack
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `awsAccessKeyId` | string | Yes | AWS access key ID |
|
||||
| `awsSecretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `stackName` | string | Yes | Stack name or ID |
|
||||
| `limit` | number | No | Maximum number of events to return \(default: 50\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `events` | array | List of stack events with resource status and timestamps |
|
||||
|
||||
### `cloudformation_get_template`
|
||||
|
||||
Retrieve the template body for a CloudFormation stack
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `awsAccessKeyId` | string | Yes | AWS access key ID |
|
||||
| `awsSecretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `stackName` | string | Yes | Stack name or ID |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `templateBody` | string | The template body as a JSON or YAML string |
|
||||
| `stagesAvailable` | array | Available template stages |
|
||||
|
||||
### `cloudformation_validate_template`
|
||||
|
||||
Validate a CloudFormation template for syntax and structural correctness
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `awsAccessKeyId` | string | Yes | AWS access key ID |
|
||||
| `awsSecretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `templateBody` | string | Yes | The CloudFormation template body \(JSON or YAML\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `description` | string | Template description |
|
||||
| `parameters` | array | Template parameters with defaults and descriptions |
|
||||
| `capabilities` | array | Required capabilities \(e.g., CAPABILITY_IAM\) |
|
||||
| `capabilitiesReason` | string | Reason capabilities are required |
|
||||
| `declaredTransforms` | array | Transforms used in the template \(e.g., AWS::Serverless-2016-10-31\) |
|
||||
|
||||
|
||||
180
apps/docs/content/docs/en/tools/cloudwatch.mdx
Normal file
@@ -0,0 +1,180 @@
|
||||
---
|
||||
title: CloudWatch
|
||||
description: Query and monitor AWS CloudWatch logs, metrics, and alarms
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="cloudwatch"
|
||||
color="linear-gradient(45deg, #B0084D 0%, #FF4F8B 100%)"
|
||||
/>
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate AWS CloudWatch into workflows. Run Log Insights queries, list log groups, retrieve log events, list and get metrics, and monitor alarms. Requires AWS access key and secret access key.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `cloudwatch_query_logs`
|
||||
|
||||
Run a CloudWatch Log Insights query against one or more log groups
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `awsAccessKeyId` | string | Yes | AWS access key ID |
|
||||
| `awsSecretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `logGroupNames` | array | Yes | Log group names to query |
|
||||
| `queryString` | string | Yes | CloudWatch Log Insights query string |
|
||||
| `startTime` | number | Yes | Start time as Unix epoch seconds |
|
||||
| `endTime` | number | Yes | End time as Unix epoch seconds |
|
||||
| `limit` | number | No | Maximum number of results to return |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `results` | array | Query result rows |
|
||||
| `statistics` | object | Query statistics \(bytesScanned, recordsMatched, recordsScanned\) |
|
||||
| `status` | string | Query completion status |
|
||||
|
||||
### `cloudwatch_describe_log_groups`
|
||||
|
||||
List available CloudWatch log groups
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `awsAccessKeyId` | string | Yes | AWS access key ID |
|
||||
| `awsSecretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `prefix` | string | No | Filter log groups by name prefix |
|
||||
| `limit` | number | No | Maximum number of log groups to return |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `logGroups` | array | List of CloudWatch log groups with metadata |
|
||||
|
||||
### `cloudwatch_get_log_events`
|
||||
|
||||
Retrieve log events from a specific CloudWatch log stream
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `awsAccessKeyId` | string | Yes | AWS access key ID |
|
||||
| `awsSecretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `logGroupName` | string | Yes | CloudWatch log group name |
|
||||
| `logStreamName` | string | Yes | CloudWatch log stream name |
|
||||
| `startTime` | number | No | Start time as Unix epoch seconds |
|
||||
| `endTime` | number | No | End time as Unix epoch seconds |
|
||||
| `limit` | number | No | Maximum number of events to return |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `events` | array | Log events with timestamp, message, and ingestion time |
|
||||
|
||||
### `cloudwatch_describe_log_streams`
|
||||
|
||||
List log streams within a CloudWatch log group
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `awsAccessKeyId` | string | Yes | AWS access key ID |
|
||||
| `awsSecretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `logGroupName` | string | Yes | CloudWatch log group name |
|
||||
| `prefix` | string | No | Filter log streams by name prefix |
|
||||
| `limit` | number | No | Maximum number of log streams to return |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `logStreams` | array | List of log streams with metadata |
|
||||
|
||||
### `cloudwatch_list_metrics`
|
||||
|
||||
List available CloudWatch metrics
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `awsAccessKeyId` | string | Yes | AWS access key ID |
|
||||
| `awsSecretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `namespace` | string | No | Filter by namespace \(e.g., AWS/EC2, AWS/Lambda\) |
|
||||
| `metricName` | string | No | Filter by metric name |
|
||||
| `recentlyActive` | boolean | No | Only show metrics active in the last 3 hours |
|
||||
| `limit` | number | No | Maximum number of metrics to return |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `metrics` | array | List of metrics with namespace, name, and dimensions |
|
||||
|
||||
### `cloudwatch_get_metric_statistics`
|
||||
|
||||
Get statistics for a CloudWatch metric over a time range
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `awsAccessKeyId` | string | Yes | AWS access key ID |
|
||||
| `awsSecretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `namespace` | string | Yes | Metric namespace \(e.g., AWS/EC2, AWS/Lambda\) |
|
||||
| `metricName` | string | Yes | Metric name \(e.g., CPUUtilization, Invocations\) |
|
||||
| `startTime` | number | Yes | Start time as Unix epoch seconds |
|
||||
| `endTime` | number | Yes | End time as Unix epoch seconds |
|
||||
| `period` | number | Yes | Granularity in seconds \(e.g., 60, 300, 3600\) |
|
||||
| `statistics` | array | Yes | Statistics to retrieve \(Average, Sum, Minimum, Maximum, SampleCount\) |
|
||||
| `dimensions` | string | No | Dimensions as JSON \(e.g., \{"InstanceId": "i-1234"\}\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `label` | string | Metric label |
|
||||
| `datapoints` | array | Datapoints with timestamp and statistics values |
|
||||
|
||||
### `cloudwatch_describe_alarms`
|
||||
|
||||
List and filter CloudWatch alarms
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `awsAccessKeyId` | string | Yes | AWS access key ID |
|
||||
| `awsSecretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `alarmNamePrefix` | string | No | Filter alarms by name prefix |
|
||||
| `stateValue` | string | No | Filter by alarm state \(OK, ALARM, INSUFFICIENT_DATA\) |
|
||||
| `alarmType` | string | No | Filter by alarm type \(MetricAlarm, CompositeAlarm\) |
|
||||
| `limit` | number | No | Maximum number of alarms to return |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `alarms` | array | List of CloudWatch alarms with state and configuration |
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ List all cloud agents for the authenticated user with optional pagination. Retur
|
||||
| `apiKey` | string | Yes | Cursor API key |
|
||||
| `limit` | number | No | Number of agents to return \(default: 20, max: 100\) |
|
||||
| `cursor` | string | No | Pagination cursor from previous response |
|
||||
| `prUrl` | string | No | Filter agents by pull request URL |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -173,4 +174,41 @@ Permanently delete a cloud agent. Returns API-aligned fields only.
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Agent ID |
|
||||
|
||||
### `cursor_list_artifacts`
|
||||
|
||||
List generated artifact files for a cloud agent. Returns API-aligned fields only.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Cursor API key |
|
||||
| `agentId` | string | Yes | Unique identifier for the cloud agent \(e.g., bc_abc123\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `artifacts` | array | List of artifact files |
|
||||
| ↳ `path` | string | Artifact file path |
|
||||
| ↳ `size` | number | File size in bytes |
|
||||
|
||||
### `cursor_download_artifact`
|
||||
|
||||
Download a generated artifact file from a cloud agent. Returns the file for execution storage.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Cursor API key |
|
||||
| `agentId` | string | Yes | Unique identifier for the cloud agent \(e.g., bc_abc123\) |
|
||||
| `path` | string | Yes | Absolute path of the artifact to download \(e.g., /src/index.ts\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `file` | file | Downloaded artifact file stored in execution files |
|
||||
|
||||
|
||||
|
||||
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 |
|
||||
|
||||
|
||||
|
||||
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",
|
||||
@@ -22,6 +23,8 @@
|
||||
"clay",
|
||||
"clerk",
|
||||
"cloudflare",
|
||||
"cloudformation",
|
||||
"cloudwatch",
|
||||
"confluence",
|
||||
"cursor",
|
||||
"databricks",
|
||||
@@ -39,6 +42,7 @@
|
||||
"enrich",
|
||||
"evernote",
|
||||
"exa",
|
||||
"extend",
|
||||
"fathom",
|
||||
"file",
|
||||
"firecrawl",
|
||||
@@ -87,6 +91,7 @@
|
||||
"ketch",
|
||||
"knowledge",
|
||||
"langsmith",
|
||||
"launchdarkly",
|
||||
"lemlist",
|
||||
"linear",
|
||||
"linkedin",
|
||||
@@ -121,6 +126,7 @@
|
||||
"polymarket",
|
||||
"postgresql",
|
||||
"posthog",
|
||||
"profound",
|
||||
"pulse",
|
||||
"qdrant",
|
||||
"quiver",
|
||||
@@ -131,9 +137,11 @@
|
||||
"resend",
|
||||
"revenuecat",
|
||||
"rippling",
|
||||
"rootly",
|
||||
"s3",
|
||||
"salesforce",
|
||||
"search",
|
||||
"secrets_manager",
|
||||
"sendgrid",
|
||||
"sentry",
|
||||
"serper",
|
||||
@@ -151,6 +159,7 @@
|
||||
"stt",
|
||||
"supabase",
|
||||
"table",
|
||||
"tailscale",
|
||||
"tavily",
|
||||
"telegram",
|
||||
"textract",
|
||||
|
||||
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 |
|
||||
|
||||
|
||||
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
@@ -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
@@ -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/blocks/credential-loop.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
apps/docs/public/static/blocks/credential.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
apps/docs/public/static/credentials/add-service-account.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
apps/docs/public/static/credentials/gcp-add-client-id.png
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
apps/docs/public/static/credentials/gcp-create-private-key.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 47 KiB |
@@ -1,8 +1,5 @@
|
||||
# Database (Required)
|
||||
DATABASE_URL="postgresql://postgres:password@localhost:5432/postgres"
|
||||
|
||||
# PostgreSQL Port (Optional) - defaults to 5432 if not specified
|
||||
# POSTGRES_PORT=5432
|
||||
DATABASE_URL="postgresql://postgres:your_password@localhost:5432/simstudio"
|
||||
|
||||
# Authentication (Required unless DISABLE_AUTH=true)
|
||||
BETTER_AUTH_SECRET=your_secret_key # Use `openssl rand -hex 32` to generate, or visit https://www.better-auth.com/docs/installation
|
||||
@@ -28,6 +25,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.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import AuthBackground from '@/app/(auth)/components/auth-background'
|
||||
import Navbar from '@/app/(home)/components/navbar/navbar'
|
||||
import Navbar from '@/app/(landing)/components/navbar/navbar'
|
||||
|
||||
export default function AuthLayoutClient({ children }: { children: React.ReactNode }) {
|
||||
useEffect(() => {
|
||||
|
||||
@@ -81,7 +81,7 @@ export function SocialLoginButtons({
|
||||
const githubButton = (
|
||||
<Button
|
||||
variant='outline'
|
||||
className='w-full rounded-[10px]'
|
||||
className='w-full rounded-sm border-[var(--landing-border-strong)] py-1.5 text-sm'
|
||||
disabled={!githubAvailable || isGithubLoading}
|
||||
onClick={signInWithGithub}
|
||||
>
|
||||
@@ -93,7 +93,7 @@ export function SocialLoginButtons({
|
||||
const googleButton = (
|
||||
<Button
|
||||
variant='outline'
|
||||
className='w-full rounded-[10px]'
|
||||
className='w-full rounded-sm border-[var(--landing-border-strong)] py-1.5 text-sm'
|
||||
disabled={!googleAvailable || isGoogleLoading}
|
||||
onClick={signInWithGoogle}
|
||||
>
|
||||
|
||||
@@ -28,7 +28,9 @@ export function SSOLoginButton({
|
||||
router.push(ssoUrl)
|
||||
}
|
||||
|
||||
const outlineBtnClasses = cn('w-full rounded-[10px]')
|
||||
const outlineBtnClasses = cn(
|
||||
'w-full rounded-sm border-[var(--landing-border-strong)] py-1.5 text-sm'
|
||||
)
|
||||
|
||||
return (
|
||||
<Button
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import AuthBackground from '@/app/(auth)/components/auth-background'
|
||||
import Navbar from '@/app/(home)/components/navbar/navbar'
|
||||
import Navbar from '@/app/(landing)/components/navbar/navbar'
|
||||
import { SupportFooter } from './support-footer'
|
||||
|
||||
export interface StatusPageLayoutProps {
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
'use client'
|
||||
|
||||
import { Suspense, useMemo, useRef, useState } from 'react'
|
||||
import { Suspense, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Turnstile, type TurnstileInstance } from '@marsidev/react-turnstile'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { Eye, EyeOff, Loader2 } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { usePostHog } from 'posthog-js/react'
|
||||
import { Input, Label } from '@/components/emcn'
|
||||
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 { captureEvent } from '@/lib/posthog/client'
|
||||
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'
|
||||
@@ -81,7 +83,12 @@ function SignupFormContent({
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const { refetch: refetchSession } = useSession()
|
||||
const posthog = usePostHog()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
captureEvent(posthog, 'signup_page_viewed', {})
|
||||
}, [posthog])
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const [password, setPassword] = useState('')
|
||||
const [passwordErrors, setPasswordErrors] = useState<string[]>([])
|
||||
@@ -92,8 +99,6 @@ function SignupFormContent({
|
||||
const [showEmailValidationError, setShowEmailValidationError] = useState(false)
|
||||
const [formError, setFormError] = useState<string | null>(null)
|
||||
const turnstileRef = useRef<TurnstileInstance>(null)
|
||||
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 redirectUrl = useMemo(
|
||||
() => searchParams.get('redirect') || searchParams.get('callbackUrl') || '',
|
||||
@@ -251,27 +256,14 @@ function SignupFormContent({
|
||||
let token: string | undefined
|
||||
const widget = turnstileRef.current
|
||||
if (turnstileSiteKey && widget) {
|
||||
let timeoutId: ReturnType<typeof setTimeout> | undefined
|
||||
try {
|
||||
widget.reset()
|
||||
token = await Promise.race([
|
||||
new Promise<string>((resolve, reject) => {
|
||||
captchaResolveRef.current = resolve
|
||||
captchaRejectRef.current = reject
|
||||
widget.execute()
|
||||
}),
|
||||
new Promise<string>((_, reject) => {
|
||||
timeoutId = setTimeout(() => reject(new Error('Captcha timed out')), 15_000)
|
||||
}),
|
||||
])
|
||||
widget.execute()
|
||||
token = await widget.getResponsePromise()
|
||||
} catch {
|
||||
setFormError('Captcha verification failed. Please try again.')
|
||||
setIsLoading(false)
|
||||
return
|
||||
} finally {
|
||||
clearTimeout(timeoutId)
|
||||
captchaResolveRef.current = null
|
||||
captchaRejectRef.current = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -528,10 +520,7 @@ function SignupFormContent({
|
||||
<Turnstile
|
||||
ref={turnstileRef}
|
||||
siteKey={turnstileSiteKey}
|
||||
onSuccess={(token) => captchaResolveRef.current?.(token)}
|
||||
onError={() => captchaRejectRef.current?.(new Error('Captcha verification failed'))}
|
||||
onExpire={() => captchaRejectRef.current?.(new Error('Captcha token expired'))}
|
||||
options={{ execution: 'execute' }}
|
||||
options={{ execution: 'execute', appearance: 'execute' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,584 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { motion, type Variants } from 'framer-motion'
|
||||
|
||||
/** Stagger between each block appearing (seconds). */
|
||||
const ENTER_STAGGER = 0.06
|
||||
|
||||
/** Duration of each block's fade-in (seconds). */
|
||||
const ENTER_DURATION = 0.3
|
||||
|
||||
/** Stagger between each block disappearing (seconds). */
|
||||
const EXIT_STAGGER = 0.12
|
||||
|
||||
/** Duration of each block's fade-out (seconds). */
|
||||
const EXIT_DURATION = 0.5
|
||||
|
||||
/** Shared corner radius for all decorative rects. */
|
||||
const RX = '2.59574'
|
||||
|
||||
/** Hold time after the initial enter animation before cycling starts (ms). */
|
||||
const INITIAL_HOLD_MS = 2500
|
||||
|
||||
/** Pause between an exit completing and the next enter starting (ms). */
|
||||
const TRANSITION_PAUSE_MS = 400
|
||||
|
||||
/** Hold time between successive transitions (ms). */
|
||||
const HOLD_BETWEEN_MS = 2500
|
||||
|
||||
/** Animation state for a block group. */
|
||||
export type BlockAnimState = 'entering' | 'visible' | 'exiting' | 'hidden'
|
||||
|
||||
/** Positions around the hero where block groups can appear. */
|
||||
export type BlockPosition = 'topRight' | 'left' | 'rightEdge' | 'rightSide' | 'topLeft'
|
||||
|
||||
/** Attributes for a single animated SVG rect. */
|
||||
interface BlockRect {
|
||||
opacity: number
|
||||
width: string
|
||||
height: string
|
||||
fill: string
|
||||
x?: string
|
||||
y?: string
|
||||
transform?: string
|
||||
}
|
||||
|
||||
const containerVariants: Variants = {
|
||||
hidden: {},
|
||||
visible: { transition: { staggerChildren: ENTER_STAGGER } },
|
||||
exit: { transition: { staggerChildren: EXIT_STAGGER } },
|
||||
}
|
||||
|
||||
const blockVariants: Variants = {
|
||||
hidden: { opacity: 0, transition: { duration: 0 } },
|
||||
visible: (targetOpacity: number) => ({
|
||||
opacity: targetOpacity,
|
||||
transition: { duration: ENTER_DURATION },
|
||||
}),
|
||||
exit: {
|
||||
opacity: 0,
|
||||
transition: { duration: EXIT_DURATION },
|
||||
},
|
||||
}
|
||||
|
||||
/** Maps a BlockAnimState to the framer-motion animate value. */
|
||||
function toAnimateValue(state: BlockAnimState): string {
|
||||
if (state === 'entering' || state === 'visible') return 'visible'
|
||||
if (state === 'exiting') return 'exit'
|
||||
return 'hidden'
|
||||
}
|
||||
|
||||
/** Shared SVG wrapper that staggers child rects in and out. */
|
||||
function AnimatedBlocksSvg({
|
||||
width,
|
||||
height,
|
||||
viewBox,
|
||||
rects,
|
||||
animState = 'entering',
|
||||
}: {
|
||||
width: number
|
||||
height: number
|
||||
viewBox: string
|
||||
rects: readonly BlockRect[]
|
||||
animState?: BlockAnimState
|
||||
}) {
|
||||
return (
|
||||
<motion.svg
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox={viewBox}
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
className='h-auto w-full'
|
||||
initial='hidden'
|
||||
animate={toAnimateValue(animState)}
|
||||
variants={containerVariants}
|
||||
>
|
||||
{rects.map((r, i) => (
|
||||
<motion.rect
|
||||
key={i}
|
||||
variants={blockVariants}
|
||||
custom={r.opacity}
|
||||
x={r.x}
|
||||
y={r.y}
|
||||
width={r.width}
|
||||
height={r.height}
|
||||
rx={RX}
|
||||
fill={r.fill}
|
||||
transform={r.transform}
|
||||
/>
|
||||
))}
|
||||
</motion.svg>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Rect data for the top-right position.
|
||||
* Two-row horizontal strip, ordered left-to-right.
|
||||
*/
|
||||
const TOP_RIGHT_RECTS: readonly BlockRect[] = [
|
||||
{ opacity: 1, x: '0', y: '0', width: '16.8626', height: '33.7252', fill: '#2ABBF8' },
|
||||
{ opacity: 0.6, x: '0', y: '0', width: '85.3433', height: '16.8626', fill: '#2ABBF8' },
|
||||
{ opacity: 1, x: '0', y: '0', width: '16.8626', height: '16.8626', fill: '#2ABBF8' },
|
||||
{ opacity: 0.6, x: '34.2403', y: '0', width: '34.2403', height: '33.7252', fill: '#2ABBF8' },
|
||||
{ opacity: 1, x: '34.2403', y: '0', width: '16.8626', height: '16.8626', fill: '#2ABBF8' },
|
||||
{ opacity: 1, x: '51.6188', y: '16.8626', width: '16.8626', height: '16.8626', fill: '#2ABBF8' },
|
||||
{ opacity: 1, x: '68.4812', y: '0', width: '54.6502', height: '16.8626', fill: '#00F701' },
|
||||
{ opacity: 0.6, x: '106.268', y: '0', width: '34.2403', height: '33.7252', fill: '#00F701' },
|
||||
{ opacity: 0.6, x: '106.268', y: '0', width: '51.103', height: '16.8626', fill: '#00F701' },
|
||||
{ opacity: 1, x: '123.6484', y: '16.8626', width: '16.8626', height: '16.8626', fill: '#00F701' },
|
||||
{ opacity: 0.6, x: '157.371', y: '0', width: '34.2403', height: '16.8626', fill: '#FFCC02' },
|
||||
{ opacity: 1, x: '157.371', y: '0', width: '16.8626', height: '16.8626', fill: '#FFCC02' },
|
||||
{ opacity: 0.6, x: '208.993', y: '0', width: '68.4805', height: '16.8626', fill: '#FA4EDF' },
|
||||
{ opacity: 0.6, x: '209.137', y: '0', width: '16.8626', height: '33.7252', fill: '#FA4EDF' },
|
||||
{ opacity: 0.6, x: '243.233', y: '0', width: '34.2403', height: '33.7252', fill: '#FA4EDF' },
|
||||
{ opacity: 1, x: '243.233', y: '0', width: '16.8626', height: '16.8626', fill: '#FA4EDF' },
|
||||
{ opacity: 0.6, x: '260.096', y: '0', width: '34.04', height: '16.8626', fill: '#FA4EDF' },
|
||||
{ opacity: 1, x: '260.611', y: '16.8626', width: '16.8626', height: '16.8626', fill: '#FA4EDF' },
|
||||
]
|
||||
|
||||
/**
|
||||
* Rect data for the top-left position.
|
||||
* Same two-row structure as top-right with rotated colour palette:
|
||||
* blue→green, green→yellow, yellow→pink, pink→blue.
|
||||
*/
|
||||
const TOP_LEFT_RECTS: readonly BlockRect[] = [
|
||||
{ opacity: 1, x: '0', y: '0', width: '16.8626', height: '33.7252', fill: '#00F701' },
|
||||
{ opacity: 0.6, x: '0', y: '0', width: '85.3433', height: '16.8626', fill: '#00F701' },
|
||||
{ opacity: 1, x: '0', y: '0', width: '16.8626', height: '16.8626', fill: '#00F701' },
|
||||
{ opacity: 0.6, x: '34.2403', y: '0', width: '34.2403', height: '33.7252', fill: '#00F701' },
|
||||
{ opacity: 1, x: '34.2403', y: '0', width: '16.8626', height: '16.8626', fill: '#00F701' },
|
||||
{ opacity: 1, x: '51.6188', y: '16.8626', width: '16.8626', height: '16.8626', fill: '#00F701' },
|
||||
{ opacity: 1, x: '68.4812', y: '0', width: '54.6502', height: '16.8626', fill: '#FFCC02' },
|
||||
{ opacity: 0.6, x: '106.268', y: '0', width: '34.2403', height: '33.7252', fill: '#FFCC02' },
|
||||
{ opacity: 0.6, x: '106.268', y: '0', width: '51.103', height: '16.8626', fill: '#FFCC02' },
|
||||
{ opacity: 1, x: '123.6484', y: '16.8626', width: '16.8626', height: '16.8626', fill: '#FFCC02' },
|
||||
{ opacity: 0.6, x: '157.371', y: '0', width: '34.2403', height: '16.8626', fill: '#FA4EDF' },
|
||||
{ opacity: 1, x: '157.371', y: '0', width: '16.8626', height: '16.8626', fill: '#FA4EDF' },
|
||||
{ opacity: 0.6, x: '208.993', y: '0', width: '68.4805', height: '16.8626', fill: '#2ABBF8' },
|
||||
{ opacity: 0.6, x: '209.137', y: '0', width: '16.8626', height: '33.7252', fill: '#2ABBF8' },
|
||||
{ opacity: 0.6, x: '243.233', y: '0', width: '34.2403', height: '33.7252', fill: '#2ABBF8' },
|
||||
{ opacity: 1, x: '243.233', y: '0', width: '16.8626', height: '16.8626', fill: '#2ABBF8' },
|
||||
{ opacity: 0.6, x: '260.096', y: '0', width: '34.04', height: '16.8626', fill: '#2ABBF8' },
|
||||
{ opacity: 1, x: '260.611', y: '16.8626', width: '16.8626', height: '16.8626', fill: '#2ABBF8' },
|
||||
]
|
||||
|
||||
/**
|
||||
* Rect data for the left position.
|
||||
* Two-column vertical strip, ordered top-to-bottom.
|
||||
*/
|
||||
const LEFT_RECTS: readonly BlockRect[] = [
|
||||
{
|
||||
opacity: 0.6,
|
||||
width: '34.240',
|
||||
height: '33.725',
|
||||
fill: '#FA4EDF',
|
||||
transform: 'matrix(0 1 1 0 0 0)',
|
||||
},
|
||||
{
|
||||
opacity: 0.6,
|
||||
width: '16.8626',
|
||||
height: '68.480',
|
||||
fill: '#FA4EDF',
|
||||
transform: 'matrix(-1 0 0 1 33.727 0)',
|
||||
},
|
||||
{
|
||||
opacity: 1,
|
||||
width: '16.8626',
|
||||
height: '16.8626',
|
||||
fill: '#FA4EDF',
|
||||
transform: 'matrix(-1 0 0 1 33.727 17.378)',
|
||||
},
|
||||
{
|
||||
opacity: 0.6,
|
||||
width: '16.8626',
|
||||
height: '33.986',
|
||||
fill: '#FA4EDF',
|
||||
transform: 'matrix(0 1 1 0 0 51.616)',
|
||||
},
|
||||
{
|
||||
opacity: 0.6,
|
||||
width: '16.8626',
|
||||
height: '140.507',
|
||||
fill: '#00F701',
|
||||
transform: 'matrix(-1 0 0 1 33.986 85.335)',
|
||||
},
|
||||
{
|
||||
opacity: 0.4,
|
||||
x: '17.119',
|
||||
y: '136.962',
|
||||
width: '34.240',
|
||||
height: '16.8626',
|
||||
fill: '#FFCC02',
|
||||
transform: 'rotate(-90 17.119 136.962)',
|
||||
},
|
||||
{
|
||||
opacity: 1,
|
||||
x: '17.119',
|
||||
y: '136.962',
|
||||
width: '16.8626',
|
||||
height: '16.8626',
|
||||
fill: '#FFCC02',
|
||||
transform: 'rotate(-90 17.119 136.962)',
|
||||
},
|
||||
{
|
||||
opacity: 0.5,
|
||||
width: '34.240',
|
||||
height: '33.725',
|
||||
fill: '#00F701',
|
||||
transform: 'matrix(0 1 1 0 0.257 153.825)',
|
||||
},
|
||||
{
|
||||
opacity: 1,
|
||||
width: '16.8626',
|
||||
height: '16.8626',
|
||||
fill: '#00F701',
|
||||
transform: 'matrix(0 1 1 0 0.257 153.825)',
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* Rect data for the right-side position (right edge of screenshot).
|
||||
* Same two-column structure as left with rotated colours:
|
||||
* pink→blue, green→pink, yellow→green.
|
||||
*/
|
||||
const RIGHT_SIDE_RECTS: readonly BlockRect[] = [
|
||||
{
|
||||
opacity: 0.6,
|
||||
width: '34.240',
|
||||
height: '33.725',
|
||||
fill: '#2ABBF8',
|
||||
transform: 'matrix(0 1 1 0 0 0)',
|
||||
},
|
||||
{
|
||||
opacity: 0.6,
|
||||
width: '16.8626',
|
||||
height: '68.480',
|
||||
fill: '#2ABBF8',
|
||||
transform: 'matrix(-1 0 0 1 33.727 0)',
|
||||
},
|
||||
{
|
||||
opacity: 1,
|
||||
width: '16.8626',
|
||||
height: '16.8626',
|
||||
fill: '#2ABBF8',
|
||||
transform: 'matrix(-1 0 0 1 33.727 17.378)',
|
||||
},
|
||||
{
|
||||
opacity: 0.6,
|
||||
width: '16.8626',
|
||||
height: '33.986',
|
||||
fill: '#2ABBF8',
|
||||
transform: 'matrix(0 1 1 0 0 51.616)',
|
||||
},
|
||||
{
|
||||
opacity: 0.6,
|
||||
width: '16.8626',
|
||||
height: '140.507',
|
||||
fill: '#FA4EDF',
|
||||
transform: 'matrix(-1 0 0 1 33.986 85.335)',
|
||||
},
|
||||
{
|
||||
opacity: 0.4,
|
||||
x: '17.119',
|
||||
y: '136.962',
|
||||
width: '34.240',
|
||||
height: '16.8626',
|
||||
fill: '#00F701',
|
||||
transform: 'rotate(-90 17.119 136.962)',
|
||||
},
|
||||
{
|
||||
opacity: 1,
|
||||
x: '17.119',
|
||||
y: '136.962',
|
||||
width: '16.8626',
|
||||
height: '16.8626',
|
||||
fill: '#00F701',
|
||||
transform: 'rotate(-90 17.119 136.962)',
|
||||
},
|
||||
{
|
||||
opacity: 0.5,
|
||||
width: '34.240',
|
||||
height: '33.725',
|
||||
fill: '#FA4EDF',
|
||||
transform: 'matrix(0 1 1 0 0.257 153.825)',
|
||||
},
|
||||
{
|
||||
opacity: 1,
|
||||
width: '16.8626',
|
||||
height: '16.8626',
|
||||
fill: '#FA4EDF',
|
||||
transform: 'matrix(0 1 1 0 0.257 153.825)',
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* Rect data for the right-edge position (far right of screen).
|
||||
* Two-column vertical strip, ordered top-to-bottom.
|
||||
*/
|
||||
const RIGHT_RECTS: readonly BlockRect[] = [
|
||||
{
|
||||
opacity: 0.6,
|
||||
width: '16.8626',
|
||||
height: '33.726',
|
||||
fill: '#FA4EDF',
|
||||
transform: 'matrix(0 1 1 0 0 0)',
|
||||
},
|
||||
{
|
||||
opacity: 0.6,
|
||||
width: '34.241',
|
||||
height: '16.8626',
|
||||
fill: '#FA4EDF',
|
||||
transform: 'matrix(0 1 1 0 16.891 0)',
|
||||
},
|
||||
{
|
||||
opacity: 0.6,
|
||||
width: '16.8626',
|
||||
height: '68.482',
|
||||
fill: '#FA4EDF',
|
||||
transform: 'matrix(-1 0 0 1 33.739 16.888)',
|
||||
},
|
||||
{
|
||||
opacity: 0.6,
|
||||
width: '16.8626',
|
||||
height: '33.726',
|
||||
fill: '#FA4EDF',
|
||||
transform: 'matrix(0 1 1 0 0 33.776)',
|
||||
},
|
||||
{
|
||||
opacity: 1,
|
||||
width: '16.8626',
|
||||
height: '16.8626',
|
||||
fill: '#FA4EDF',
|
||||
transform: 'matrix(-1 0 0 1 33.739 34.272)',
|
||||
},
|
||||
{
|
||||
opacity: 0.6,
|
||||
width: '16.8626',
|
||||
height: '33.726',
|
||||
fill: '#FA4EDF',
|
||||
transform: 'matrix(0 1 1 0 0.012 68.510)',
|
||||
},
|
||||
{
|
||||
opacity: 0.6,
|
||||
width: '16.8626',
|
||||
height: '102.384',
|
||||
fill: '#2ABBF8',
|
||||
transform: 'matrix(-1 0 0 1 33.787 102.384)',
|
||||
},
|
||||
{
|
||||
opacity: 0.4,
|
||||
x: '17.131',
|
||||
y: '153.859',
|
||||
width: '34.241',
|
||||
height: '16.8626',
|
||||
fill: '#00F701',
|
||||
transform: 'rotate(-90 17.131 153.859)',
|
||||
},
|
||||
{
|
||||
opacity: 1,
|
||||
x: '17.131',
|
||||
y: '153.859',
|
||||
width: '16.8626',
|
||||
height: '16.8626',
|
||||
fill: '#00F701',
|
||||
transform: 'rotate(-90 17.131 153.859)',
|
||||
},
|
||||
]
|
||||
|
||||
/** Number of rects per position, used to compute animation durations. */
|
||||
const RECT_COUNTS: Record<BlockPosition, number> = {
|
||||
topRight: TOP_RIGHT_RECTS.length,
|
||||
topLeft: TOP_LEFT_RECTS.length,
|
||||
left: LEFT_RECTS.length,
|
||||
rightSide: RIGHT_SIDE_RECTS.length,
|
||||
rightEdge: RIGHT_RECTS.length,
|
||||
}
|
||||
|
||||
/** Total enter animation time for a position (seconds). */
|
||||
function enterTime(pos: BlockPosition): number {
|
||||
return (RECT_COUNTS[pos] - 1) * ENTER_STAGGER + ENTER_DURATION
|
||||
}
|
||||
|
||||
/** Total exit animation time for a position (seconds). */
|
||||
function exitTime(pos: BlockPosition): number {
|
||||
return (RECT_COUNTS[pos] - 1) * EXIT_STAGGER + EXIT_DURATION
|
||||
}
|
||||
|
||||
/** A single step in the repeating animation cycle. */
|
||||
type CycleStep =
|
||||
| { action: 'exit'; position: BlockPosition }
|
||||
| { action: 'enter'; position: BlockPosition }
|
||||
| { action: 'hold'; ms: number }
|
||||
|
||||
/**
|
||||
* The repeating cycle sequence. After all steps, the layout returns to its
|
||||
* initial state (topRight + left + rightEdge) so the loop is seamless.
|
||||
*
|
||||
* Order: exit top → exit right-edge → enter right-side-of-preview →
|
||||
* exit left → enter top-left → exit right-side → enter left →
|
||||
* exit top-left → enter top-right → enter right-edge → back to initial.
|
||||
*/
|
||||
const CYCLE_STEPS: readonly CycleStep[] = [
|
||||
{ action: 'exit', position: 'topRight' },
|
||||
{ action: 'exit', position: 'rightEdge' },
|
||||
{ action: 'enter', position: 'rightSide' },
|
||||
{ action: 'hold', ms: HOLD_BETWEEN_MS },
|
||||
{ action: 'exit', position: 'left' },
|
||||
{ action: 'enter', position: 'topLeft' },
|
||||
{ action: 'hold', ms: HOLD_BETWEEN_MS },
|
||||
{ action: 'exit', position: 'rightSide' },
|
||||
{ action: 'enter', position: 'left' },
|
||||
{ action: 'hold', ms: HOLD_BETWEEN_MS },
|
||||
{ action: 'exit', position: 'topLeft' },
|
||||
{ action: 'enter', position: 'topRight' },
|
||||
{ action: 'hold', ms: HOLD_BETWEEN_MS },
|
||||
{ action: 'enter', position: 'rightEdge' },
|
||||
{ action: 'hold', ms: HOLD_BETWEEN_MS },
|
||||
]
|
||||
|
||||
/**
|
||||
* Drives the block-cycling animation loop. Returns the current animation
|
||||
* state for every position so each component can be driven declaratively.
|
||||
*
|
||||
* Lifecycle:
|
||||
* 1. All three initial groups (topRight, left, rightEdge) enter together.
|
||||
* 2. After a hold period the cycle begins, processing each step in order.
|
||||
* 3. Repeats indefinitely, returning to the initial layout every cycle.
|
||||
*/
|
||||
export function useBlockCycle(): Record<BlockPosition, BlockAnimState> {
|
||||
const [states, setStates] = useState<Record<BlockPosition, BlockAnimState>>({
|
||||
topRight: 'entering',
|
||||
left: 'entering',
|
||||
rightEdge: 'entering',
|
||||
rightSide: 'hidden',
|
||||
topLeft: 'hidden',
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const cancelled = { current: false }
|
||||
const delay = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))
|
||||
|
||||
const run = async () => {
|
||||
const longestEnter = Math.max(
|
||||
enterTime('topRight'),
|
||||
enterTime('left'),
|
||||
enterTime('rightEdge')
|
||||
)
|
||||
await delay(longestEnter * 1000)
|
||||
if (cancelled.current) return
|
||||
|
||||
setStates({
|
||||
topRight: 'visible',
|
||||
left: 'visible',
|
||||
rightEdge: 'visible',
|
||||
rightSide: 'hidden',
|
||||
topLeft: 'hidden',
|
||||
})
|
||||
|
||||
await delay(INITIAL_HOLD_MS)
|
||||
if (cancelled.current) return
|
||||
|
||||
while (!cancelled.current) {
|
||||
for (const step of CYCLE_STEPS) {
|
||||
if (cancelled.current) return
|
||||
|
||||
if (step.action === 'exit') {
|
||||
setStates((prev) => ({ ...prev, [step.position]: 'exiting' }))
|
||||
await delay(exitTime(step.position) * 1000)
|
||||
if (cancelled.current) return
|
||||
setStates((prev) => ({ ...prev, [step.position]: 'hidden' }))
|
||||
await delay(TRANSITION_PAUSE_MS)
|
||||
} else if (step.action === 'enter') {
|
||||
setStates((prev) => ({ ...prev, [step.position]: 'entering' }))
|
||||
await delay(enterTime(step.position) * 1000)
|
||||
if (cancelled.current) return
|
||||
setStates((prev) => ({ ...prev, [step.position]: 'visible' }))
|
||||
await delay(TRANSITION_PAUSE_MS)
|
||||
} else {
|
||||
await delay(step.ms)
|
||||
}
|
||||
|
||||
if (cancelled.current) return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
run()
|
||||
return () => {
|
||||
cancelled.current = true
|
||||
}
|
||||
}, [])
|
||||
|
||||
return states
|
||||
}
|
||||
|
||||
interface AnimatedBlockProps {
|
||||
animState?: BlockAnimState
|
||||
}
|
||||
|
||||
/** Two-row horizontal strip at the top-right of the hero. */
|
||||
export function BlocksTopRightAnimated({ animState = 'entering' }: AnimatedBlockProps) {
|
||||
return (
|
||||
<AnimatedBlocksSvg
|
||||
width={295}
|
||||
height={34}
|
||||
viewBox='0 0 295 34'
|
||||
rects={TOP_RIGHT_RECTS}
|
||||
animState={animState}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/** Two-row horizontal strip at the top-left of the hero. */
|
||||
export function BlocksTopLeftAnimated({ animState = 'entering' }: AnimatedBlockProps) {
|
||||
return (
|
||||
<AnimatedBlocksSvg
|
||||
width={295}
|
||||
height={34}
|
||||
viewBox='0 0 295 34'
|
||||
rects={TOP_LEFT_RECTS}
|
||||
animState={animState}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/** Two-column vertical strip on the left edge of the screenshot. */
|
||||
export function BlocksLeftAnimated({ animState = 'entering' }: AnimatedBlockProps) {
|
||||
return (
|
||||
<AnimatedBlocksSvg
|
||||
width={34}
|
||||
height={226}
|
||||
viewBox='0 0 34 226.021'
|
||||
rects={LEFT_RECTS}
|
||||
animState={animState}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/** Two-column vertical strip on the right edge of the screenshot. */
|
||||
export function BlocksRightSideAnimated({ animState = 'entering' }: AnimatedBlockProps) {
|
||||
return (
|
||||
<AnimatedBlocksSvg
|
||||
width={34}
|
||||
height={226}
|
||||
viewBox='0 0 34 226.021'
|
||||
rects={RIGHT_SIDE_RECTS}
|
||||
animState={animState}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/** Two-column vertical strip at the far-right edge of the screen. */
|
||||
export function BlocksRightAnimated({ animState = 'entering' }: AnimatedBlockProps) {
|
||||
return (
|
||||
<AnimatedBlocksSvg
|
||||
width={34}
|
||||
height={205}
|
||||
viewBox='0 0 34 204.769'
|
||||
rects={RIGHT_RECTS}
|
||||
animState={animState}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import dynamic from 'next/dynamic'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { DemoRequestModal } from '@/app/(home)/components/demo-request/demo-request-modal'
|
||||
import {
|
||||
BlocksLeftAnimated,
|
||||
BlocksRightAnimated,
|
||||
BlocksRightSideAnimated,
|
||||
BlocksTopLeftAnimated,
|
||||
BlocksTopRightAnimated,
|
||||
useBlockCycle,
|
||||
} from '@/app/(home)/components/hero/components/animated-blocks'
|
||||
|
||||
const LandingPreview = dynamic(
|
||||
() =>
|
||||
import('@/app/(home)/components/landing-preview/landing-preview').then(
|
||||
(mod) => mod.LandingPreview
|
||||
),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => <div className='aspect-[1116/549] w-full rounded bg-[var(--landing-bg)]' />,
|
||||
}
|
||||
)
|
||||
|
||||
/** Shared base classes for CTA link buttons — matches Deploy/Run button styling in the preview panel. */
|
||||
const CTA_BASE =
|
||||
'inline-flex items-center h-[32px] rounded-[5px] border px-2.5 font-[430] font-season text-sm'
|
||||
|
||||
export default function Hero() {
|
||||
const blockStates = useBlockCycle()
|
||||
|
||||
return (
|
||||
<section
|
||||
id='hero'
|
||||
aria-labelledby='hero-heading'
|
||||
className='relative flex flex-col items-center overflow-hidden bg-[var(--landing-bg)] pt-[60px] pb-3 lg:pt-[100px]'
|
||||
>
|
||||
<p className='sr-only'>
|
||||
Sim is the open-source platform to build AI agents and run your agentic workforce. Connect
|
||||
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.
|
||||
</p>
|
||||
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='pointer-events-none absolute top-[-0.7vw] left-[-2.8vw] z-0 aspect-[344/328] w-[23.9vw]'
|
||||
>
|
||||
<Image src='/landing/card-left.svg' alt='' fill className='object-contain' />
|
||||
</div>
|
||||
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='pointer-events-none absolute top-[-2.8vw] right-[-4vw] z-0 aspect-[471/470] w-[32.7vw]'
|
||||
>
|
||||
<Image src='/landing/card-right.svg' alt='' fill className='object-contain' />
|
||||
</div>
|
||||
|
||||
<div className='relative z-10 flex flex-col items-center gap-3'>
|
||||
<h1
|
||||
id='hero-heading'
|
||||
className='text-balance font-[430] font-season text-[36px] text-white leading-[100%] tracking-[-0.02em] sm:text-[48px] lg:text-[72px]'
|
||||
>
|
||||
Build AI Agents
|
||||
</h1>
|
||||
<p className='font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-subtle)_60%,transparent)] text-base leading-[125%] tracking-[0.02em] lg:text-lg'>
|
||||
Sim is the AI Workspace for Agent Builders.
|
||||
</p>
|
||||
|
||||
<div className='mt-3 flex items-center gap-2'>
|
||||
<DemoRequestModal>
|
||||
<button
|
||||
type='button'
|
||||
className={`${CTA_BASE} border-[var(--landing-border-strong)] bg-transparent text-[var(--landing-text)] transition-colors hover:bg-[var(--landing-bg-elevated)]`}
|
||||
aria-label='Get a demo'
|
||||
>
|
||||
Get a demo
|
||||
</button>
|
||||
</DemoRequestModal>
|
||||
<Link
|
||||
href='/signup'
|
||||
className={`${CTA_BASE} gap-2 border-white bg-white text-black transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]`}
|
||||
aria-label='Get started with Sim'
|
||||
>
|
||||
Get started
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='pointer-events-none absolute top-0 right-[13.1vw] z-20 w-[calc(140px_+_10.76vw)] max-w-[295px]'
|
||||
>
|
||||
<BlocksTopRightAnimated animState={blockStates.topRight} />
|
||||
</div>
|
||||
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='pointer-events-none absolute top-0 left-[16vw] z-20 w-[calc(140px_+_10.76vw)] max-w-[295px]'
|
||||
>
|
||||
<BlocksTopLeftAnimated animState={blockStates.topLeft} />
|
||||
</div>
|
||||
|
||||
<div className='relative z-10 mx-auto mt-[3.2vw] w-[78.9vw] px-[1.4vw]'>
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='-translate-y-1/2 pointer-events-none absolute top-[50%] right-[calc(100%-1.41vw)] z-20 w-[calc(16px_+_1.25vw)] max-w-[34px]'
|
||||
>
|
||||
<BlocksLeftAnimated animState={blockStates.left} />
|
||||
</div>
|
||||
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='-translate-y-1/2 pointer-events-none absolute top-[50%] left-[calc(100%-1.41vw)] z-20 w-[calc(16px_+_1.25vw)] max-w-[34px] scale-x-[-1]'
|
||||
>
|
||||
<BlocksRightSideAnimated animState={blockStates.rightSide} />
|
||||
</div>
|
||||
|
||||
<div className='relative z-10 overflow-hidden rounded border border-[var(--landing-bg-elevated)]'>
|
||||
<LandingPreview />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='-translate-y-1/2 pointer-events-none absolute top-[50%] right-0 z-20 w-[calc(16px_+_1.25vw)] max-w-[34px]'
|
||||
>
|
||||
<BlocksRightAnimated animState={blockStates.rightEdge} />
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import Collaboration from '@/app/(home)/components/collaboration/collaboration'
|
||||
import Enterprise from '@/app/(home)/components/enterprise/enterprise'
|
||||
import Features from '@/app/(home)/components/features/features'
|
||||
import Footer from '@/app/(home)/components/footer/footer'
|
||||
import Hero from '@/app/(home)/components/hero/hero'
|
||||
import Navbar from '@/app/(home)/components/navbar/navbar'
|
||||
import Pricing from '@/app/(home)/components/pricing/pricing'
|
||||
import StructuredData from '@/app/(home)/components/structured-data'
|
||||
import Templates from '@/app/(home)/components/templates/templates'
|
||||
import Testimonials from '@/app/(home)/components/testimonials/testimonials'
|
||||
|
||||
export {
|
||||
Collaboration,
|
||||
Enterprise,
|
||||
Features,
|
||||
Footer,
|
||||
Hero,
|
||||
Navbar,
|
||||
Pricing,
|
||||
StructuredData,
|
||||
Templates,
|
||||
Testimonials,
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { memo, useCallback, useRef, useState } from 'react'
|
||||
import { ArrowUp } from 'lucide-react'
|
||||
import { useLandingSubmit } from '@/app/(home)/components/landing-preview/components/landing-preview-panel/landing-preview-panel'
|
||||
import { useAnimatedPlaceholder } from '@/hooks/use-animated-placeholder'
|
||||
|
||||
const C = {
|
||||
SURFACE: '#292929',
|
||||
BORDER: '#3d3d3d',
|
||||
TEXT_PRIMARY: '#e6e6e6',
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Landing preview replica of the workspace Home initial view.
|
||||
* Shows a greeting heading and a minimal chat input (no + or mic).
|
||||
* On submit, stores the prompt and redirects to /signup.
|
||||
*/
|
||||
export const LandingPreviewHome = memo(function LandingPreviewHome() {
|
||||
const landingSubmit = useLandingSubmit()
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
const animatedPlaceholder = useAnimatedPlaceholder()
|
||||
|
||||
const isEmpty = inputValue.trim().length === 0
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
if (isEmpty) return
|
||||
landingSubmit(inputValue)
|
||||
}, [isEmpty, inputValue, landingSubmit])
|
||||
|
||||
const MAX_HEIGHT = 200
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
handleSubmit()
|
||||
}
|
||||
},
|
||||
[handleSubmit]
|
||||
)
|
||||
|
||||
const handleInput = useCallback((e: React.FormEvent<HTMLTextAreaElement>) => {
|
||||
const target = e.target as HTMLTextAreaElement
|
||||
target.style.height = 'auto'
|
||||
target.style.height = `${Math.min(target.scrollHeight, MAX_HEIGHT)}px`
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className='flex min-w-0 flex-1 flex-col items-center justify-center px-6 pb-[2vh]'>
|
||||
<p
|
||||
role='presentation'
|
||||
className='mb-6 max-w-[42rem] font-[430] font-season text-[32px] tracking-[-0.02em]'
|
||||
style={{ color: C.TEXT_PRIMARY }}
|
||||
>
|
||||
What should we get done?
|
||||
</p>
|
||||
|
||||
<div className='w-full max-w-[32rem]'>
|
||||
<div
|
||||
className='cursor-text rounded-[20px] border px-2.5 py-2'
|
||||
style={{ borderColor: C.BORDER, backgroundColor: C.SURFACE }}
|
||||
onClick={() => textareaRef.current?.focus()}
|
||||
>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onInput={handleInput}
|
||||
placeholder={animatedPlaceholder}
|
||||
rows={1}
|
||||
className='m-0 box-border min-h-[24px] w-full resize-none overflow-y-auto border-0 bg-transparent px-1 py-1 font-body text-[15px] leading-[24px] tracking-[-0.015em] outline-none placeholder:font-[380] placeholder:text-[#787878] focus-visible:ring-0'
|
||||
style={{
|
||||
color: C.TEXT_PRIMARY,
|
||||
caretColor: C.TEXT_PRIMARY,
|
||||
maxHeight: `${MAX_HEIGHT}px`,
|
||||
}}
|
||||
/>
|
||||
<div className='flex items-center justify-end'>
|
||||
<button
|
||||
type='button'
|
||||
onClick={handleSubmit}
|
||||
disabled={isEmpty}
|
||||
className='flex h-[28px] w-[28px] items-center justify-center rounded-full border-0 p-0 transition-colors'
|
||||
style={{
|
||||
background: isEmpty ? '#808080' : '#e0e0e0',
|
||||
cursor: isEmpty ? 'not-allowed' : 'pointer',
|
||||
}}
|
||||
>
|
||||
<ArrowUp size={16} strokeWidth={2.25} color='#1b1b1b' />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
@@ -1,169 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { memo, useCallback, useRef, useState } from 'react'
|
||||
import { ArrowUp } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { BubbleChatPreview, ChevronDown, MoreHorizontal, Play } from '@/components/emcn'
|
||||
import { LandingPromptStorage } from '@/lib/core/utils/browser-storage'
|
||||
|
||||
/**
|
||||
* Stores the prompt in browser storage and redirects to /signup.
|
||||
* Shared by both the copilot panel and the landing home view.
|
||||
*/
|
||||
export function useLandingSubmit() {
|
||||
const router = useRouter()
|
||||
return useCallback(
|
||||
(text: string) => {
|
||||
const trimmed = text.trim()
|
||||
if (!trimmed) return
|
||||
LandingPromptStorage.store(trimmed)
|
||||
router.push('/signup')
|
||||
},
|
||||
[router]
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Lightweight static panel replicating the real workspace panel styling.
|
||||
* The copilot tab is active with a functional user input.
|
||||
* When submitted, stores the prompt and redirects to /signup (same as landing hero).
|
||||
*
|
||||
* Structure mirrors the real Panel component:
|
||||
* aside > div.border-l.pt-[14px] > Header(px-8) > Tabs(px-8,pt-14) > Content(pt-12)
|
||||
* inside Content > Copilot > header-bar(mx-[-1px]) > UserInput(p-8)
|
||||
*/
|
||||
export const LandingPreviewPanel = memo(function LandingPreviewPanel() {
|
||||
const landingSubmit = useLandingSubmit()
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
const [cursorPos, setCursorPos] = useState<{ x: number; y: number } | null>(null)
|
||||
|
||||
const isEmpty = inputValue.trim().length === 0
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
if (isEmpty) return
|
||||
landingSubmit(inputValue)
|
||||
}, [isEmpty, inputValue, landingSubmit])
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
handleSubmit()
|
||||
}
|
||||
},
|
||||
[handleSubmit]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className='flex h-full w-[280px] flex-shrink-0 flex-col bg-[#1e1e1e]'>
|
||||
<div className='flex h-full flex-col border-[#2c2c2c] border-l pt-3.5'>
|
||||
{/* Header — More + Chat | Deploy + Run */}
|
||||
<div className='flex flex-shrink-0 items-center justify-between px-2'>
|
||||
<div className='pointer-events-none flex gap-1.5'>
|
||||
<div className='flex h-[30px] w-[30px] items-center justify-center rounded-[5px] border border-[#3d3d3d] bg-[#363636]'>
|
||||
<MoreHorizontal className='h-[14px] w-[14px] text-[#e6e6e6]' />
|
||||
</div>
|
||||
<div className='flex h-[30px] w-[30px] items-center justify-center rounded-[5px] border border-[#3d3d3d] bg-[#363636]'>
|
||||
<BubbleChatPreview className='h-[14px] w-[14px] text-[#e6e6e6]' />
|
||||
</div>
|
||||
</div>
|
||||
<Link
|
||||
href='/signup'
|
||||
className='flex gap-1.5'
|
||||
onMouseMove={(e) => setCursorPos({ x: e.clientX, y: e.clientY })}
|
||||
onMouseLeave={() => setCursorPos(null)}
|
||||
>
|
||||
<div className='flex h-[30px] items-center rounded-[5px] bg-[#33C482] px-2.5 transition-colors hover:bg-[#2DAC72]'>
|
||||
<span className='font-medium text-[#1b1b1b] text-[12px]'>Deploy</span>
|
||||
</div>
|
||||
<div className='flex h-[30px] items-center gap-2 rounded-[5px] bg-[#33C482] px-2.5 transition-colors hover:bg-[#2DAC72]'>
|
||||
<Play className='h-[11.5px] w-[11.5px] text-[#1b1b1b]' />
|
||||
<span className='font-medium text-[#1b1b1b] text-[12px]'>Run</span>
|
||||
</div>
|
||||
</Link>
|
||||
{cursorPos &&
|
||||
createPortal(
|
||||
<div
|
||||
className='pointer-events-none fixed z-[9999]'
|
||||
style={{ left: cursorPos.x + 14, top: cursorPos.y + 14 }}
|
||||
>
|
||||
{/* Decorative color bars — mirrors hero top-right block sequence */}
|
||||
<div className='flex h-[4px]'>
|
||||
<div className='h-full w-[8px] bg-[#2ABBF8]' />
|
||||
<div className='h-full w-[14px] bg-[#2ABBF8] opacity-60' />
|
||||
<div className='h-full w-[8px] bg-[#00F701]' />
|
||||
<div className='h-full w-[16px] bg-[#00F701] opacity-60' />
|
||||
<div className='h-full w-[8px] bg-[#FFCC02]' />
|
||||
<div className='h-full w-[10px] bg-[#FFCC02] opacity-60' />
|
||||
<div className='h-full w-[8px] bg-[#FA4EDF]' />
|
||||
<div className='h-full w-[14px] bg-[#FA4EDF] opacity-60' />
|
||||
</div>
|
||||
<div className='flex items-center gap-[5px] bg-white px-1.5 py-1 font-medium text-[#1C1C1C] text-[11px]'>
|
||||
Get started
|
||||
<ChevronDown className='-rotate-90 h-[7px] w-[7px] text-[#1C1C1C]' />
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className='flex flex-shrink-0 items-center px-2 pt-3.5'>
|
||||
<div className='pointer-events-none flex gap-1'>
|
||||
<div className='flex h-[28px] items-center rounded-[6px] border border-[#3d3d3d] bg-[#363636] px-2 py-[5px]'>
|
||||
<span className='font-medium text-[#e6e6e6] text-[12.5px]'>Copilot</span>
|
||||
</div>
|
||||
<div className='flex h-[28px] items-center rounded-[6px] border border-transparent px-2 py-[5px]'>
|
||||
<span className='font-medium text-[#787878] text-[12.5px]'>Toolbar</span>
|
||||
</div>
|
||||
<div className='flex h-[28px] items-center rounded-[6px] border border-transparent px-2 py-[5px]'>
|
||||
<span className='font-medium text-[#787878] text-[12.5px]'>Editor</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab content — copilot */}
|
||||
<div className='flex flex-1 flex-col overflow-hidden pt-3'>
|
||||
<div className='flex h-full flex-col'>
|
||||
{/* Copilot header bar — matches mx-[-1px] in real copilot */}
|
||||
<div className='pointer-events-none mx-[-1px] flex flex-shrink-0 items-center rounded-[4px] border border-[#2c2c2c] bg-[#292929] px-3 py-1.5'>
|
||||
<span className='truncate font-medium text-[#e6e6e6] text-[14px]'>New Chat</span>
|
||||
</div>
|
||||
|
||||
{/* User input — matches real UserInput at p-[8px] inside copilot welcome state */}
|
||||
<div className='px-2 pt-3 pb-2'>
|
||||
<div className='rounded-[4px] border border-[#3d3d3d] bg-[#292929] px-1.5 py-1.5'>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder='Build an AI agent...'
|
||||
rows={2}
|
||||
className='mb-1.5 min-h-[48px] w-full cursor-text resize-none border-0 bg-transparent px-0.5 py-1 font-base text-[#e6e6e6] text-sm leading-[1.25rem] placeholder-[#787878] caret-[#e6e6e6] outline-none'
|
||||
/>
|
||||
<div className='flex items-center justify-end'>
|
||||
<button
|
||||
type='button'
|
||||
onClick={handleSubmit}
|
||||
disabled={isEmpty}
|
||||
className='flex h-[22px] w-[22px] items-center justify-center rounded-full border-0 p-0 transition-colors'
|
||||
style={{
|
||||
background: isEmpty ? '#808080' : '#e0e0e0',
|
||||
cursor: isEmpty ? 'not-allowed' : 'pointer',
|
||||
}}
|
||||
>
|
||||
<ArrowUp size={14} strokeWidth={2.25} color='#1b1b1b' />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
@@ -1,123 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { motion, type Variants } from 'framer-motion'
|
||||
import { LandingPreviewHome } from '@/app/(home)/components/landing-preview/components/landing-preview-home/landing-preview-home'
|
||||
import { LandingPreviewPanel } from '@/app/(home)/components/landing-preview/components/landing-preview-panel/landing-preview-panel'
|
||||
import { LandingPreviewSidebar } from '@/app/(home)/components/landing-preview/components/landing-preview-sidebar/landing-preview-sidebar'
|
||||
import { LandingPreviewWorkflow } from '@/app/(home)/components/landing-preview/components/landing-preview-workflow/landing-preview-workflow'
|
||||
import {
|
||||
EASE_OUT,
|
||||
PREVIEW_WORKFLOWS,
|
||||
} from '@/app/(home)/components/landing-preview/components/landing-preview-workflow/workflow-data'
|
||||
|
||||
const containerVariants: Variants = {
|
||||
hidden: {},
|
||||
visible: {
|
||||
transition: { staggerChildren: 0.15 },
|
||||
},
|
||||
}
|
||||
|
||||
const sidebarVariants: Variants = {
|
||||
hidden: { opacity: 0, x: -12 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
transition: {
|
||||
x: { duration: 0.25, ease: EASE_OUT },
|
||||
opacity: { duration: 0.25, ease: EASE_OUT },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const panelVariants: Variants = {
|
||||
hidden: { opacity: 0, x: 12 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
transition: {
|
||||
x: { duration: 0.25, ease: EASE_OUT },
|
||||
opacity: { duration: 0.25, ease: EASE_OUT },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Interactive workspace preview for the hero section.
|
||||
*
|
||||
* Renders a lightweight replica of the Sim workspace with:
|
||||
* - A sidebar with two selectable workflows
|
||||
* - A ReactFlow canvas showing the active workflow's blocks and edges
|
||||
* - 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.
|
||||
*/
|
||||
export function LandingPreview() {
|
||||
const [activeView, setActiveView] = useState<'home' | 'workflow'>('workflow')
|
||||
const [activeWorkflowId, setActiveWorkflowId] = useState(PREVIEW_WORKFLOWS[0].id)
|
||||
const isInitialMount = useRef(true)
|
||||
|
||||
useEffect(() => {
|
||||
isInitialMount.current = false
|
||||
}, [])
|
||||
|
||||
const handleSelectWorkflow = useCallback((id: string) => {
|
||||
setActiveWorkflowId(id)
|
||||
setActiveView('workflow')
|
||||
}, [])
|
||||
|
||||
const handleSelectHome = useCallback(() => {
|
||||
setActiveView('home')
|
||||
}, [])
|
||||
|
||||
const activeWorkflow =
|
||||
PREVIEW_WORKFLOWS.find((w) => w.id === activeWorkflowId) ?? PREVIEW_WORKFLOWS[0]
|
||||
|
||||
const isWorkflowView = activeView === 'workflow'
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className='dark flex aspect-[1116/549] w-full overflow-hidden rounded bg-[var(--landing-bg-surface)] antialiased'
|
||||
initial='hidden'
|
||||
animate='visible'
|
||||
variants={containerVariants}
|
||||
>
|
||||
<motion.div className='hidden lg:flex' variants={sidebarVariants}>
|
||||
<LandingPreviewSidebar
|
||||
workflows={PREVIEW_WORKFLOWS}
|
||||
activeWorkflowId={activeWorkflowId}
|
||||
activeView={activeView}
|
||||
onSelectWorkflow={handleSelectWorkflow}
|
||||
onSelectHome={handleSelectHome}
|
||||
/>
|
||||
</motion.div>
|
||||
<div className='flex min-w-0 flex-1 flex-col py-2 pr-2 pl-2 lg:pl-0'>
|
||||
<div className='flex flex-1 overflow-hidden rounded-[8px] border border-[#2c2c2c] bg-[var(--landing-bg)]'>
|
||||
<div
|
||||
className={
|
||||
isWorkflowView
|
||||
? 'relative min-w-0 flex-1 overflow-hidden'
|
||||
: 'relative flex min-w-0 flex-1 flex-col overflow-hidden'
|
||||
}
|
||||
>
|
||||
{isWorkflowView ? (
|
||||
<LandingPreviewWorkflow workflow={activeWorkflow} animate={isInitialMount.current} />
|
||||
) : (
|
||||
<LandingPreviewHome />
|
||||
)}
|
||||
</div>
|
||||
<motion.div
|
||||
className={isWorkflowView ? 'hidden lg:flex' : 'hidden'}
|
||||
variants={panelVariants}
|
||||
>
|
||||
<LandingPreviewPanel />
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { martianMono } from '@/app/_styles/fonts/martian-mono/martian-mono'
|
||||
import { season } from '@/app/_styles/fonts/season/season'
|
||||
|
||||
/**
|
||||
* Landing page route-group layout.
|
||||
*
|
||||
* Applies landing-specific font CSS variables to the subtree:
|
||||
* - `--font-season` (Season Sans): Headings and display text
|
||||
* - `--font-martian-mono` (Martian Mono): Code snippets and technical accents
|
||||
*
|
||||
* Available to child components via Tailwind (`font-season`, `font-martian-mono`).
|
||||
*
|
||||
* SEO metadata for the `/` route is exported from `app/page.tsx` — not here.
|
||||
* This layout only applies when a `page.tsx` exists inside the `(home)/` route group.
|
||||
*/
|
||||
export default function HomeLayout({ children }: { children: React.ReactNode }) {
|
||||
return <div className={`${season.variable} ${martianMono.variable}`}>{children}</div>
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getNavBlogPosts } from '@/lib/blog/registry'
|
||||
import Footer from '@/app/(home)/components/footer/footer'
|
||||
import Navbar from '@/app/(home)/components/navbar/navbar'
|
||||
import Footer from '@/app/(landing)/components/footer/footer'
|
||||
import Navbar from '@/app/(landing)/components/navbar/navbar'
|
||||
|
||||
export default async function StudioLayout({ children }: { children: React.ReactNode }) {
|
||||
const blogPosts = await getNavBlogPosts()
|
||||
|
||||
@@ -1,32 +1,55 @@
|
||||
import { Skeleton } from '@/components/emcn'
|
||||
|
||||
const SKELETON_CARD_COUNT = 6
|
||||
|
||||
export default function BlogLoading() {
|
||||
return (
|
||||
<main className='mx-auto max-w-[1200px] px-6 py-12 sm:px-8 md:px-12'>
|
||||
<Skeleton className='h-[48px] w-[100px] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
|
||||
<Skeleton className='mt-3 h-[18px] w-[420px] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
|
||||
<div className='mt-10 grid grid-cols-1 gap-4 md:grid-cols-2 md:gap-6 lg:grid-cols-3'>
|
||||
{Array.from({ length: SKELETON_CARD_COUNT }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className='flex flex-col overflow-hidden rounded-xl border border-[var(--landing-border)]'
|
||||
>
|
||||
<Skeleton className='aspect-video w-full rounded-none bg-[var(--landing-bg-elevated)]' />
|
||||
<div className='flex flex-1 flex-col p-4'>
|
||||
<Skeleton className='mb-2 h-[12px] w-[80px] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
|
||||
<Skeleton className='mb-1 h-[20px] w-[85%] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
|
||||
<Skeleton className='mb-3 h-[14px] w-full rounded-[4px] bg-[var(--landing-bg-elevated)]' />
|
||||
<Skeleton className='h-[14px] w-[70%] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
|
||||
<div className='mt-3 flex items-center gap-2'>
|
||||
<Skeleton className='h-[16px] w-[16px] rounded-full bg-[var(--landing-bg-elevated)]' />
|
||||
<Skeleton className='h-[12px] w-[80px] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
|
||||
<section className='bg-[var(--landing-bg)]'>
|
||||
{/* Header skeleton */}
|
||||
<div className='px-5 pt-[60px] lg:px-16 lg:pt-[100px]'>
|
||||
<Skeleton className='mb-5 h-[20px] w-[60px] rounded-md bg-[var(--landing-bg-elevated)]' />
|
||||
<div className='flex flex-col gap-4 md:flex-row md:items-end md:justify-between'>
|
||||
<Skeleton className='h-[40px] w-[240px] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
|
||||
<Skeleton className='h-[18px] w-[320px] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content area with vertical border rails */}
|
||||
<div className='mx-5 mt-8 border-[var(--landing-bg-elevated)] border-x lg:mx-16'>
|
||||
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
|
||||
|
||||
{/* Featured skeleton */}
|
||||
<div className='flex'>
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className='flex flex-1 flex-col gap-4 border-[var(--landing-bg-elevated)] p-6 md:border-l md:first:border-l-0'
|
||||
>
|
||||
<Skeleton className='aspect-video w-full rounded-[5px] bg-[var(--landing-bg-elevated)]' />
|
||||
<div className='flex flex-col gap-2'>
|
||||
<Skeleton className='h-[12px] w-[60px] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
|
||||
<Skeleton className='h-[20px] w-[80%] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
|
||||
<Skeleton className='h-[14px] w-full rounded-[4px] bg-[var(--landing-bg-elevated)]' />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
|
||||
|
||||
{/* List skeleton */}
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<div key={i}>
|
||||
<div className='flex items-center gap-6 px-6 py-6'>
|
||||
<Skeleton className='hidden h-[14px] w-[120px] rounded-[4px] bg-[var(--landing-bg-elevated)] md:block' />
|
||||
<div className='flex min-w-0 flex-1 flex-col gap-1'>
|
||||
<Skeleton className='h-[18px] w-[70%] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
|
||||
<Skeleton className='h-[14px] w-[90%] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
|
||||
</div>
|
||||
<Skeleton className='hidden h-[80px] w-[140px] rounded-[5px] bg-[var(--landing-bg-elevated)] sm:block' />
|
||||
</div>
|
||||
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</main>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Metadata } from 'next'
|
||||
import Link from 'next/link'
|
||||
import { Badge } from '@/components/emcn'
|
||||
import { getAllPostMeta } from '@/lib/blog/registry'
|
||||
import { PostGrid } from '@/app/(landing)/blog/post-grid'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Blog',
|
||||
@@ -34,8 +34,9 @@ export default async function BlogIndex({
|
||||
const totalPages = Math.max(1, Math.ceil(sorted.length / perPage))
|
||||
const start = (pageNum - 1) * perPage
|
||||
const posts = sorted.slice(start, start + perPage)
|
||||
// Tag filter chips are intentionally disabled for now.
|
||||
// const tags = await getAllTags()
|
||||
const featured = pageNum === 1 ? posts.slice(0, 3) : []
|
||||
const remaining = pageNum === 1 ? posts.slice(3) : posts
|
||||
|
||||
const blogJsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Blog',
|
||||
@@ -45,54 +46,154 @@ export default async function BlogIndex({
|
||||
}
|
||||
|
||||
return (
|
||||
<main className='mx-auto max-w-[1200px] px-6 py-12 sm:px-8 md:px-12'>
|
||||
<section className='bg-[var(--landing-bg)]'>
|
||||
<script
|
||||
type='application/ld+json'
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(blogJsonLd) }}
|
||||
/>
|
||||
<h1 className='mb-3 text-balance font-[500] text-[40px] text-[var(--landing-text)] leading-tight sm:text-[56px]'>
|
||||
Blog
|
||||
</h1>
|
||||
<p className='mb-10 text-[var(--landing-text-muted)] text-lg'>
|
||||
Announcements, insights, and guides for building AI agent workflows.
|
||||
</p>
|
||||
|
||||
{/* Tag filter chips hidden until we have more posts */}
|
||||
{/* <div className='mb-10 flex flex-wrap gap-3'>
|
||||
<Link href='/blog' className={`rounded-full border px-3 py-1 text-sm ${!tag ? 'border-black bg-black text-white' : 'border-gray-300'}`}>All</Link>
|
||||
{tags.map((t) => (
|
||||
<Link key={t.tag} href={`/blog?tag=${encodeURIComponent(t.tag)}`} className={`rounded-full border px-3 py-1 text-sm ${tag === t.tag ? 'border-black bg-black text-white' : 'border-gray-300'}`}>
|
||||
{t.tag} ({t.count})
|
||||
</Link>
|
||||
))}
|
||||
</div> */}
|
||||
{/* Section header */}
|
||||
<div className='px-5 pt-[60px] lg:px-16 lg:pt-[100px]'>
|
||||
<Badge
|
||||
variant='blue'
|
||||
size='md'
|
||||
dot
|
||||
className='mb-5 bg-white/10 font-season text-white uppercase tracking-[0.02em]'
|
||||
>
|
||||
Blog
|
||||
</Badge>
|
||||
|
||||
{/* Grid layout for consistent rows */}
|
||||
<PostGrid posts={posts} />
|
||||
|
||||
{totalPages > 1 && (
|
||||
<div className='mt-10 flex items-center justify-center gap-3'>
|
||||
{pageNum > 1 && (
|
||||
<Link
|
||||
href={`/blog?page=${pageNum - 1}${tag ? `&tag=${encodeURIComponent(tag)}` : ''}`}
|
||||
className='rounded-[5px] border border-[var(--landing-border-strong)] px-3 py-1 text-[var(--landing-text)] text-sm transition-colors hover:bg-[var(--landing-bg-elevated)]'
|
||||
>
|
||||
Previous
|
||||
</Link>
|
||||
)}
|
||||
<span className='text-[var(--landing-text-muted)] text-sm'>
|
||||
Page {pageNum} of {totalPages}
|
||||
</span>
|
||||
{pageNum < totalPages && (
|
||||
<Link
|
||||
href={`/blog?page=${pageNum + 1}${tag ? `&tag=${encodeURIComponent(tag)}` : ''}`}
|
||||
className='rounded-[5px] border border-[var(--landing-border-strong)] px-3 py-1 text-[var(--landing-text)] text-sm transition-colors hover:bg-[var(--landing-bg-elevated)]'
|
||||
>
|
||||
Next
|
||||
</Link>
|
||||
)}
|
||||
<div className='flex flex-col gap-4 md:flex-row md:items-end md:justify-between'>
|
||||
<h1 className='text-balance font-[430] font-season text-[28px] text-white leading-[100%] tracking-[-0.02em] lg:text-[40px]'>
|
||||
Latest from Sim
|
||||
</h1>
|
||||
<p className='max-w-[360px] font-[430] font-season text-[#F6F6F0]/50 text-sm leading-[150%] tracking-[0.02em] lg:text-base'>
|
||||
Announcements, insights, and guides for building AI agent workflows.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* Full-width top line */}
|
||||
<div className='mt-8 h-px w-full bg-[var(--landing-bg-elevated)]' />
|
||||
|
||||
{/* Content area with vertical border rails */}
|
||||
<div className='mx-5 border-[var(--landing-bg-elevated)] border-x lg:mx-16'>
|
||||
{/* Featured posts */}
|
||||
{featured.length > 0 && (
|
||||
<>
|
||||
<div className='flex'>
|
||||
{featured.map((p, index) => (
|
||||
<Link
|
||||
key={p.slug}
|
||||
href={`/blog/${p.slug}`}
|
||||
className='group flex flex-1 flex-col gap-4 border-[var(--landing-bg-elevated)] p-6 transition-colors hover:bg-[var(--landing-bg-elevated)] md:border-l md:first:border-l-0'
|
||||
>
|
||||
<div className='relative aspect-video w-full overflow-hidden rounded-[5px]'>
|
||||
<img
|
||||
src={p.ogImage}
|
||||
alt={p.title}
|
||||
className='h-full w-full object-cover'
|
||||
loading={index < 3 ? 'eager' : 'lazy'}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-col gap-2'>
|
||||
<span className='font-martian-mono text-[var(--landing-text-subtle)] text-xs uppercase tracking-[0.1em]'>
|
||||
{new Date(p.date).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
year: '2-digit',
|
||||
})}
|
||||
</span>
|
||||
<h3 className='font-[430] font-season text-lg text-white leading-tight tracking-[-0.01em]'>
|
||||
{p.title}
|
||||
</h3>
|
||||
<p className='line-clamp-2 text-[#F6F6F0]/50 text-sm leading-[150%]'>
|
||||
{p.description}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
|
||||
</>
|
||||
)}
|
||||
|
||||
{remaining.map((p) => (
|
||||
<div key={p.slug}>
|
||||
<Link
|
||||
href={`/blog/${p.slug}`}
|
||||
className='group flex items-start gap-6 px-6 py-6 transition-colors hover:bg-[var(--landing-bg-elevated)] md:items-center'
|
||||
>
|
||||
{/* Date */}
|
||||
<span className='hidden w-[120px] shrink-0 pt-1 font-martian-mono text-[var(--landing-text-subtle)] text-xs uppercase tracking-[0.1em] md:block'>
|
||||
{new Date(p.date).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</span>
|
||||
|
||||
{/* Title + description */}
|
||||
<div className='flex min-w-0 flex-1 flex-col gap-1'>
|
||||
<span className='font-martian-mono text-[var(--landing-text-subtle)] text-xs uppercase tracking-[0.1em] md:hidden'>
|
||||
{new Date(p.date).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</span>
|
||||
<h3 className='font-[430] font-season text-base text-white leading-tight tracking-[-0.01em] lg:text-lg'>
|
||||
{p.title}
|
||||
</h3>
|
||||
<p className='line-clamp-2 text-[#F6F6F0]/40 text-sm leading-[150%]'>
|
||||
{p.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Image */}
|
||||
<div className='hidden h-[80px] w-[140px] shrink-0 overflow-hidden rounded-[5px] sm:block'>
|
||||
<img
|
||||
src={p.ogImage}
|
||||
alt={p.title}
|
||||
className='h-full w-full object-cover'
|
||||
loading='lazy'
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className='px-6 py-8'>
|
||||
<div className='flex items-center justify-center gap-3'>
|
||||
{pageNum > 1 && (
|
||||
<Link
|
||||
href={`/blog?page=${pageNum - 1}${tag ? `&tag=${encodeURIComponent(tag)}` : ''}`}
|
||||
className='rounded-[5px] border border-[var(--landing-border-strong)] px-3 py-1 text-[var(--landing-text)] text-sm transition-colors hover:bg-[var(--landing-bg-elevated)]'
|
||||
>
|
||||
Previous
|
||||
</Link>
|
||||
)}
|
||||
<span className='text-[var(--landing-text-muted)] text-sm'>
|
||||
Page {pageNum} of {totalPages}
|
||||
</span>
|
||||
{pageNum < totalPages && (
|
||||
<Link
|
||||
href={`/blog?page=${pageNum + 1}${tag ? `&tag=${encodeURIComponent(tag)}` : ''}`}
|
||||
className='rounded-[5px] border border-[var(--landing-border-strong)] px-3 py-1 text-[var(--landing-text)] text-sm transition-colors hover:bg-[var(--landing-bg-elevated)]'
|
||||
>
|
||||
Next
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Full-width bottom line — overlaps last inner divider to avoid double border */}
|
||||
<div className='-mt-px h-px w-full bg-[var(--landing-bg-elevated)]' />
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/emcn'
|
||||
|
||||
interface Author {
|
||||
id: string
|
||||
name: string
|
||||
avatarUrl?: string
|
||||
url?: string
|
||||
}
|
||||
|
||||
interface Post {
|
||||
slug: string
|
||||
title: string
|
||||
description: string
|
||||
date: string
|
||||
ogImage: string
|
||||
author: Author
|
||||
authors?: Author[]
|
||||
featured?: boolean
|
||||
}
|
||||
|
||||
export function PostGrid({ posts }: { posts: Post[] }) {
|
||||
return (
|
||||
<div className='grid grid-cols-1 gap-4 md:grid-cols-2 md:gap-6 lg:grid-cols-3'>
|
||||
{posts.map((p, index) => (
|
||||
<Link key={p.slug} href={`/blog/${p.slug}`} className='group flex flex-col'>
|
||||
<div className='flex h-full flex-col overflow-hidden rounded-xl border border-[var(--landing-bg-elevated)] transition-colors duration-300 hover:border-[var(--landing-border-strong)]'>
|
||||
{/* Image container with fixed aspect ratio to prevent layout shift */}
|
||||
<div className='relative aspect-video w-full overflow-hidden'>
|
||||
<Image
|
||||
src={p.ogImage}
|
||||
alt={p.title}
|
||||
sizes='(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw'
|
||||
unoptimized
|
||||
priority={index < 6}
|
||||
loading={index < 6 ? undefined : 'lazy'}
|
||||
fill
|
||||
style={{ objectFit: 'cover' }}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-1 flex-col p-4'>
|
||||
<div className='mb-2 text-[var(--landing-text-muted)] text-xs'>
|
||||
{new Date(p.date).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</div>
|
||||
<h3 className='mb-1 font-[500] text-[var(--landing-text)] text-lg leading-tight'>
|
||||
{p.title}
|
||||
</h3>
|
||||
<p className='mb-3 line-clamp-3 flex-1 text-[var(--landing-text-muted)] text-sm'>
|
||||
{p.description}
|
||||
</p>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='-space-x-1.5 flex'>
|
||||
{(p.authors && p.authors.length > 0 ? p.authors : [p.author])
|
||||
.slice(0, 3)
|
||||
.map((author, idx) => (
|
||||
<Avatar key={idx} className='size-4 border border-[var(--landing-text)]'>
|
||||
<AvatarImage src={author?.avatarUrl} alt={author?.name} />
|
||||
<AvatarFallback className='border border-[var(--landing-text)] bg-[var(--landing-bg-elevated)] text-[var(--landing-text-muted)] text-micro'>
|
||||
{author?.name.slice(0, 2)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
))}
|
||||
</div>
|
||||
<span className='text-[var(--landing-text-muted)] text-xs'>
|
||||
{(p.authors && p.authors.length > 0 ? p.authors : [p.author])
|
||||
.slice(0, 2)
|
||||
.map((a) => a?.name)
|
||||
.join(', ')}
|
||||
{(p.authors && p.authors.length > 0 ? p.authors : [p.author]).length > 2 && (
|
||||
<>
|
||||
{' '}
|
||||
and {(p.authors && p.authors.length > 0 ? p.authors : [p.author]).length - 2}{' '}
|
||||
other
|
||||
{(p.authors && p.authors.length > 0 ? p.authors : [p.author]).length - 2 > 1
|
||||
? 's'
|
||||
: ''}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { Badge, ChevronDown } from '@/components/emcn'
|
||||
import { Badge } from '@/components/emcn'
|
||||
|
||||
interface DotGridProps {
|
||||
className?: string
|
||||
@@ -230,7 +230,7 @@ export default function Collaboration() {
|
||||
|
||||
<div className='relative overflow-hidden'>
|
||||
<div className='grid grid-cols-1 md:grid-cols-[auto_1fr]'>
|
||||
<div className='flex flex-col items-start gap-3 px-4 pt-[60px] pb-8 sm:gap-4 sm:px-8 md:gap-5 md:px-20 md:pt-[100px]'>
|
||||
<div className='flex flex-col items-start gap-3 px-4 pt-[60px] pb-8 sm:gap-4 sm:px-8 md:gap-5 md:px-16 md:pt-[100px]'>
|
||||
<Badge
|
||||
variant='blue'
|
||||
size='md'
|
||||
@@ -249,6 +249,13 @@ export default function Collaboration() {
|
||||
collaboration
|
||||
</h2>
|
||||
|
||||
<p className='sr-only'>
|
||||
Sim supports real-time multiplayer collaboration. Teams can build AI agents together
|
||||
in a shared workspace with live cursors, presence indicators, and concurrent editing.
|
||||
Features include role-based access control, shared workflows, and team workspace
|
||||
management.
|
||||
</p>
|
||||
|
||||
<p className='font-[430] font-season text-[#F6F6F0]/50 text-base leading-[150%] tracking-[0.02em] md:text-lg'>
|
||||
Grab your team. Build agents together <br className='hidden md:block' />
|
||||
in real-time inside your workspace.
|
||||
@@ -259,24 +266,32 @@ export default function Collaboration() {
|
||||
className='group/cta mt-3 inline-flex h-[32px] cursor-none items-center gap-1.5 rounded-[5px] border border-white bg-white px-2.5 font-[430] font-season text-black text-sm transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
|
||||
>
|
||||
Build together
|
||||
<span className='relative h-[10px] w-[10px] shrink-0'>
|
||||
<ChevronDown className='-rotate-90 absolute inset-0 h-[10px] w-[10px] transition-opacity duration-150 group-hover/cta:opacity-0' />
|
||||
<svg
|
||||
className='absolute inset-0 h-[10px] w-[10px] opacity-0 transition-opacity duration-150 group-hover/cta:opacity-100'
|
||||
viewBox='0 0 10 10'
|
||||
<svg
|
||||
className='h-[10px] w-[10px] shrink-0'
|
||||
viewBox='0 0 10 10'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<line
|
||||
x1='0'
|
||||
y1='5'
|
||||
x2='9'
|
||||
y2='5'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.33'
|
||||
strokeLinecap='square'
|
||||
className='origin-left scale-x-0 transition-transform duration-200 ease-out [transform-box:fill-box] group-hover/cta:scale-x-100'
|
||||
/>
|
||||
<path
|
||||
d='M3.5 2L6.5 5L3.5 8'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.33'
|
||||
strokeLinecap='square'
|
||||
strokeLinejoin='miter'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<path
|
||||
d='M1 5H8M5.5 2L8.5 5L5.5 8'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.33'
|
||||
strokeLinecap='square'
|
||||
strokeLinejoin='miter'
|
||||
fill='none'
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
className='transition-transform duration-200 ease-out group-hover/cta:translate-x-[30%]'
|
||||
/>
|
||||
</svg>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -305,7 +320,7 @@ export default function Collaboration() {
|
||||
href='/blog/multiplayer'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='relative mx-4 mb-6 flex cursor-none items-center gap-3.5 rounded-[5px] border border-[var(--landing-bg-elevated)] bg-[var(--landing-bg)] px-3 py-2.5 transition-colors hover:border-[var(--landing-border-strong)] hover:bg-[var(--landing-bg-card)] sm:mx-8 md:absolute md:bottom-10 md:left-20 md:z-20 md:mx-0 md:mb-0'
|
||||
className='relative mx-4 mb-6 flex cursor-none items-center gap-3.5 rounded-[5px] border border-[var(--landing-bg-elevated)] bg-[var(--landing-bg)] px-3 py-2.5 transition-colors hover:border-[var(--landing-border-strong)] hover:bg-[var(--landing-bg-card)] sm:mx-8 md:absolute md:bottom-10 md:left-16 md:z-20 md:mx-0 md:mb-0'
|
||||
>
|
||||
<div className='relative h-7 w-11 shrink-0'>
|
||||
<Image src='/landing/multiplayer-cursors.svg' alt='' fill className='object-contain' />
|
||||
@@ -5,15 +5,6 @@ import { quickValidateEmail } from '@/lib/messaging/email/validation'
|
||||
|
||||
const FREE_EMAIL_DOMAINS = new Set(freeEmailDomains)
|
||||
|
||||
export const DEMO_REQUEST_REGION_VALUES = [
|
||||
'north_america',
|
||||
'europe',
|
||||
'asia_pacific',
|
||||
'latin_america',
|
||||
'middle_east_africa',
|
||||
'other',
|
||||
] as const
|
||||
|
||||
export const DEMO_REQUEST_COMPANY_SIZE_VALUES = [
|
||||
'1_10',
|
||||
'11_50',
|
||||
@@ -24,15 +15,6 @@ export const DEMO_REQUEST_COMPANY_SIZE_VALUES = [
|
||||
'10000_plus',
|
||||
] as const
|
||||
|
||||
export const DEMO_REQUEST_REGION_OPTIONS = [
|
||||
{ value: 'north_america', label: 'North America' },
|
||||
{ value: 'europe', label: 'Europe' },
|
||||
{ value: 'asia_pacific', label: 'Asia Pacific' },
|
||||
{ value: 'latin_america', label: 'Latin America' },
|
||||
{ value: 'middle_east_africa', label: 'Middle East & Africa' },
|
||||
{ value: 'other', label: 'Other' },
|
||||
] as const
|
||||
|
||||
export const DEMO_REQUEST_COMPANY_SIZE_OPTIONS = [
|
||||
{ value: '1_10', label: '1–10' },
|
||||
{ value: '11_50', label: '11–50' },
|
||||
@@ -73,9 +55,6 @@ export const demoRequestSchema = z.object({
|
||||
.max(50, 'Phone number must be 50 characters or less')
|
||||
.optional()
|
||||
.transform((value) => (value && value.length > 0 ? value : undefined)),
|
||||
region: z.enum(DEMO_REQUEST_REGION_VALUES, {
|
||||
errorMap: () => ({ message: 'Please select a region' }),
|
||||
}),
|
||||
companySize: z.enum(DEMO_REQUEST_COMPANY_SIZE_VALUES, {
|
||||
errorMap: () => ({ message: 'Please select company size' }),
|
||||
}),
|
||||
@@ -84,10 +63,6 @@ export const demoRequestSchema = z.object({
|
||||
|
||||
export type DemoRequestPayload = z.infer<typeof demoRequestSchema>
|
||||
|
||||
export function getDemoRequestRegionLabel(value: DemoRequestPayload['region']): string {
|
||||
return DEMO_REQUEST_REGION_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
|
||||
}
|
||||
@@ -2,9 +2,7 @@
|
||||
|
||||
import { useCallback, useState } from 'react'
|
||||
import {
|
||||
Button,
|
||||
Combobox,
|
||||
FormField,
|
||||
Input,
|
||||
Modal,
|
||||
ModalBody,
|
||||
@@ -17,10 +15,9 @@ import {
|
||||
import { Check } from '@/components/emcn/icons'
|
||||
import {
|
||||
DEMO_REQUEST_COMPANY_SIZE_OPTIONS,
|
||||
DEMO_REQUEST_REGION_OPTIONS,
|
||||
type DemoRequestPayload,
|
||||
demoRequestSchema,
|
||||
} from '@/app/(home)/components/demo-request/consts'
|
||||
} from '@/app/(landing)/components/demo-request/consts'
|
||||
|
||||
interface DemoRequestModalProps {
|
||||
children: React.ReactNode
|
||||
@@ -35,13 +32,11 @@ interface DemoRequestFormState {
|
||||
lastName: string
|
||||
companyEmail: string
|
||||
phoneNumber: string
|
||||
region: DemoRequestPayload['region'] | ''
|
||||
companySize: DemoRequestPayload['companySize'] | ''
|
||||
details: string
|
||||
}
|
||||
|
||||
const SUBMIT_SUCCESS_MESSAGE = "We'll be in touch soon!"
|
||||
const COMBOBOX_REGIONS = [...DEMO_REQUEST_REGION_OPTIONS]
|
||||
const COMBOBOX_COMPANY_SIZES = [...DEMO_REQUEST_COMPANY_SIZE_OPTIONS]
|
||||
|
||||
const INITIAL_FORM_STATE: DemoRequestFormState = {
|
||||
@@ -49,11 +44,37 @@ const INITIAL_FORM_STATE: DemoRequestFormState = {
|
||||
lastName: '',
|
||||
companyEmail: '',
|
||||
phoneNumber: '',
|
||||
region: '',
|
||||
companySize: '',
|
||||
details: '',
|
||||
}
|
||||
|
||||
interface LandingFieldProps {
|
||||
label: string
|
||||
htmlFor: string
|
||||
optional?: boolean
|
||||
error?: string
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
function LandingField({ label, htmlFor, optional, error, children }: LandingFieldProps) {
|
||||
return (
|
||||
<div className='flex flex-col gap-1.5'>
|
||||
<label
|
||||
htmlFor={htmlFor}
|
||||
className='font-[430] font-season text-[13px] text-[var(--text-secondary)] tracking-[0.02em]'
|
||||
>
|
||||
{label}
|
||||
{optional ? <span className='ml-1 text-[var(--text-muted)]'>(optional)</span> : null}
|
||||
</label>
|
||||
{children}
|
||||
{error ? <p className='text-[12px] text-[var(--text-error)]'>{error}</p> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const LANDING_INPUT =
|
||||
'h-[32px] rounded-[5px] border border-[var(--border-1)] bg-[var(--surface-5)] px-2.5 font-[430] font-season text-[13.5px] text-[var(--text-primary)] transition-colors placeholder:text-[var(--text-muted)] outline-none'
|
||||
|
||||
export function DemoRequestModal({ children, theme = 'dark' }: DemoRequestModalProps) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [form, setForm] = useState<DemoRequestFormState>(INITIAL_FORM_STATE)
|
||||
@@ -117,7 +138,6 @@ export function DemoRequestModal({ children, theme = 'dark' }: DemoRequestModalP
|
||||
lastName: fieldErrors.lastName?.[0],
|
||||
companyEmail: fieldErrors.companyEmail?.[0],
|
||||
phoneNumber: fieldErrors.phoneNumber?.[0],
|
||||
region: fieldErrors.region?.[0],
|
||||
companySize: fieldErrors.companySize?.[0],
|
||||
details: fieldErrors.details?.[0],
|
||||
})
|
||||
@@ -162,7 +182,9 @@ export function DemoRequestModal({ children, theme = 'dark' }: DemoRequestModalP
|
||||
<ModalContent size='lg' className={theme === 'dark' ? 'dark' : undefined}>
|
||||
<ModalHeader>
|
||||
<span className={submitSuccess ? 'sr-only' : undefined}>
|
||||
{submitSuccess ? 'Demo request submitted' : 'Nearly there!'}
|
||||
<span className='font-[430] font-season text-[15px] tracking-[-0.02em]'>
|
||||
{submitSuccess ? 'Demo request submitted' : 'Talk to sales'}
|
||||
</span>
|
||||
</span>
|
||||
</ModalHeader>
|
||||
<div className='relative flex-1'>
|
||||
@@ -176,37 +198,44 @@ export function DemoRequestModal({ children, theme = 'dark' }: DemoRequestModalP
|
||||
}
|
||||
>
|
||||
<ModalBody>
|
||||
<div className='space-y-4'>
|
||||
<div className='grid gap-4 sm:grid-cols-2'>
|
||||
<FormField htmlFor='firstName' label='First name' error={errors.firstName}>
|
||||
<div className='space-y-3'>
|
||||
<div className='grid gap-3 sm:grid-cols-2'>
|
||||
<LandingField htmlFor='firstName' label='First name' error={errors.firstName}>
|
||||
<Input
|
||||
id='firstName'
|
||||
value={form.firstName}
|
||||
onChange={(event) => updateField('firstName', event.target.value)}
|
||||
placeholder='First'
|
||||
className={LANDING_INPUT}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField htmlFor='lastName' label='Last name' error={errors.lastName}>
|
||||
</LandingField>
|
||||
<LandingField htmlFor='lastName' label='Last name' error={errors.lastName}>
|
||||
<Input
|
||||
id='lastName'
|
||||
value={form.lastName}
|
||||
onChange={(event) => updateField('lastName', event.target.value)}
|
||||
placeholder='Last'
|
||||
className={LANDING_INPUT}
|
||||
/>
|
||||
</FormField>
|
||||
</LandingField>
|
||||
</div>
|
||||
|
||||
<FormField htmlFor='companyEmail' label='Company email' error={errors.companyEmail}>
|
||||
<LandingField
|
||||
htmlFor='companyEmail'
|
||||
label='Company email'
|
||||
error={errors.companyEmail}
|
||||
>
|
||||
<Input
|
||||
id='companyEmail'
|
||||
type='email'
|
||||
value={form.companyEmail}
|
||||
onChange={(event) => updateField('companyEmail', event.target.value)}
|
||||
placeholder='Your work email'
|
||||
className={LANDING_INPUT}
|
||||
/>
|
||||
</FormField>
|
||||
</LandingField>
|
||||
|
||||
<FormField
|
||||
<LandingField
|
||||
htmlFor='phoneNumber'
|
||||
label='Phone number'
|
||||
optional
|
||||
@@ -218,54 +247,48 @@ export function DemoRequestModal({ children, theme = 'dark' }: DemoRequestModalP
|
||||
value={form.phoneNumber}
|
||||
onChange={(event) => updateField('phoneNumber', event.target.value)}
|
||||
placeholder='Your phone number'
|
||||
className={LANDING_INPUT}
|
||||
/>
|
||||
</FormField>
|
||||
</LandingField>
|
||||
|
||||
<div className='grid gap-4 sm:grid-cols-2'>
|
||||
<FormField htmlFor='region' label='Region' error={errors.region}>
|
||||
<Combobox
|
||||
options={COMBOBOX_REGIONS}
|
||||
value={form.region}
|
||||
selectedValue={form.region}
|
||||
onChange={(value) =>
|
||||
updateField('region', value as DemoRequestPayload['region'])
|
||||
}
|
||||
placeholder='Select'
|
||||
editable={false}
|
||||
filterOptions={false}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField htmlFor='companySize' label='Company size' error={errors.companySize}>
|
||||
<Combobox
|
||||
options={COMBOBOX_COMPANY_SIZES}
|
||||
value={form.companySize}
|
||||
selectedValue={form.companySize}
|
||||
onChange={(value) =>
|
||||
updateField('companySize', value as DemoRequestPayload['companySize'])
|
||||
}
|
||||
placeholder='Select'
|
||||
editable={false}
|
||||
filterOptions={false}
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
<LandingField htmlFor='companySize' label='Company size' error={errors.companySize}>
|
||||
<Combobox
|
||||
options={COMBOBOX_COMPANY_SIZES}
|
||||
value={form.companySize}
|
||||
selectedValue={form.companySize}
|
||||
onChange={(value) =>
|
||||
updateField('companySize', value as DemoRequestPayload['companySize'])
|
||||
}
|
||||
placeholder='Select'
|
||||
editable={false}
|
||||
filterOptions={false}
|
||||
className='h-[32px] rounded-[5px] px-2.5 font-[430] font-season text-[13.5px]'
|
||||
/>
|
||||
</LandingField>
|
||||
|
||||
<FormField htmlFor='details' label='Details' error={errors.details}>
|
||||
<LandingField htmlFor='details' label='Details' error={errors.details}>
|
||||
<Textarea
|
||||
id='details'
|
||||
value={form.details}
|
||||
onChange={(event) => updateField('details', event.target.value)}
|
||||
placeholder='Tell us about your needs and questions'
|
||||
className='min-h-[80px] rounded-[5px] border border-[var(--border-1)] bg-[var(--surface-5)] px-2.5 py-2 font-[430] font-season text-[13.5px] text-[var(--text-primary)] outline-none transition-colors placeholder:text-[var(--text-muted)]'
|
||||
/>
|
||||
</FormField>
|
||||
</LandingField>
|
||||
</div>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter className='flex-col items-stretch gap-3'>
|
||||
{submitError && <p className='text-[13px] text-[var(--text-error)]'>{submitError}</p>}
|
||||
<Button type='submit' variant='primary' disabled={isSubmitting}>
|
||||
<ModalFooter className='flex-col items-stretch gap-3 border-t-0 bg-transparent pt-0'>
|
||||
{submitError && (
|
||||
<p className='font-season text-[13px] text-[var(--text-error)]'>{submitError}</p>
|
||||
)}
|
||||
<button
|
||||
type='submit'
|
||||
disabled={isSubmitting}
|
||||
className='flex h-[32px] w-full items-center justify-center rounded-[5px] bg-[var(--text-primary)] font-[430] font-season text-[13.5px] text-[var(--bg)] transition-colors hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50'
|
||||
>
|
||||
{isSubmitting ? 'Submitting...' : 'Submit'}
|
||||
</Button>
|
||||
</button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
|
||||
@@ -275,10 +298,10 @@ export function DemoRequestModal({ children, theme = 'dark' }: DemoRequestModalP
|
||||
<div className='flex h-20 w-20 items-center justify-center rounded-full border border-[var(--border)] bg-[var(--bg-subtle)] text-[var(--text-primary)]'>
|
||||
<Check className='h-10 w-10' />
|
||||
</div>
|
||||
<h2 className='mt-8 font-medium text-[34px] text-[var(--text-primary)] leading-[1.1] tracking-[-0.03em]'>
|
||||
<h2 className='mt-8 font-[430] font-season text-[34px] text-[var(--text-primary)] leading-[1.1] tracking-[-0.03em]'>
|
||||
{SUBMIT_SUCCESS_MESSAGE}
|
||||
</h2>
|
||||
<p className='mt-4 text-[17px] text-[var(--text-secondary)] leading-7'>
|
||||
<p className='mt-4 font-season text-[15px] text-[var(--text-secondary)] leading-7'>
|
||||
Our team will be in touch soon. If you have any questions, please email us at{' '}
|
||||
<a
|
||||
href='mailto:enterprise@sim.ai'
|
||||
@@ -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,23 +4,23 @@
|
||||
* 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?".
|
||||
*/
|
||||
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { Badge, ChevronDown } from '@/components/emcn'
|
||||
import { Badge } from '@/components/emcn'
|
||||
import { Lock } from '@/components/emcn/icons'
|
||||
import { GithubIcon } from '@/components/icons'
|
||||
import { DemoRequestModal } from '@/app/(home)/components/demo-request/demo-request-modal'
|
||||
import { AccessControlPanel } from '@/app/(home)/components/enterprise/components/access-control-panel'
|
||||
import { AuditLogPreview } from '@/app/(home)/components/enterprise/components/audit-log-preview'
|
||||
import { DemoRequestModal } from '@/app/(landing)/components/demo-request/demo-request-modal'
|
||||
import { AccessControlPanel } from '@/app/(landing)/components/enterprise/components/access-control-panel'
|
||||
import { AuditLogPreview } from '@/app/(landing)/components/enterprise/components/audit-log-preview'
|
||||
|
||||
const ENTERPRISE_FEATURE_MARQUEE_STYLES = `
|
||||
@keyframes enterprise-feature-marquee {
|
||||
@@ -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)_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 · PHI protected →
|
||||
Type II →
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
@@ -136,7 +136,7 @@ export default function Enterprise() {
|
||||
aria-labelledby='enterprise-heading'
|
||||
className='bg-[var(--landing-bg-section)]'
|
||||
>
|
||||
<div className='px-4 pt-[60px] pb-10 sm:px-8 sm:pt-20 sm:pb-0 md:px-20 md:pt-[100px]'>
|
||||
<div className='px-4 pt-[60px] pb-10 sm:px-8 sm:pt-20 sm:pb-0 md:px-16 md:pt-[100px]'>
|
||||
<div className='flex flex-col items-start gap-3 sm:gap-4 md:gap-5'>
|
||||
<Badge
|
||||
variant='blue'
|
||||
@@ -230,23 +230,32 @@ export default function Enterprise() {
|
||||
className='group/cta inline-flex h-[32px] cursor-pointer items-center gap-1.5 rounded-[5px] border border-white bg-white px-2.5 font-[430] font-season text-[14px] text-black transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
|
||||
>
|
||||
Book a demo
|
||||
<span className='relative h-[10px] w-[10px] shrink-0'>
|
||||
<ChevronDown className='-rotate-90 absolute inset-0 h-[10px] w-[10px] transition-opacity duration-150 group-hover/cta:opacity-0' />
|
||||
<svg
|
||||
className='absolute inset-0 h-[10px] w-[10px] opacity-0 transition-opacity duration-150 group-hover/cta:opacity-100'
|
||||
viewBox='0 0 10 10'
|
||||
<svg
|
||||
className='h-[10px] w-[10px] shrink-0'
|
||||
viewBox='0 0 10 10'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<line
|
||||
x1='0'
|
||||
y1='5'
|
||||
x2='9'
|
||||
y2='5'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.33'
|
||||
strokeLinecap='square'
|
||||
className='origin-left scale-x-0 transition-transform duration-200 ease-out [transform-box:fill-box] group-hover/cta:scale-x-100'
|
||||
/>
|
||||
<path
|
||||
d='M3.5 2L6.5 5L3.5 8'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.33'
|
||||
strokeLinecap='square'
|
||||
strokeLinejoin='miter'
|
||||
fill='none'
|
||||
>
|
||||
<path
|
||||
d='M1 5H8M5.5 2L8.5 5L5.5 8'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.33'
|
||||
strokeLinecap='square'
|
||||
strokeLinejoin='miter'
|
||||
fill='none'
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
className='transition-transform duration-200 ease-out group-hover/cta:translate-x-[30%]'
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</DemoRequestModal>
|
||||
</div>
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
SlackIcon,
|
||||
xAIIcon,
|
||||
} from '@/components/icons'
|
||||
import { CsvIcon, JsonIcon, MarkdownIcon, PdfIcon } from '@/components/icons/document-icons'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
|
||||
interface FeaturesPreviewProps {
|
||||
@@ -25,7 +24,7 @@ interface FeaturesPreviewProps {
|
||||
}
|
||||
|
||||
export function FeaturesPreview({ activeTab }: FeaturesPreviewProps) {
|
||||
const isWorkspaceTab = activeTab <= 4
|
||||
const isWorkspaceTab = activeTab <= 3
|
||||
|
||||
return (
|
||||
<div className='relative h-[350px] w-full md:h-[560px]'>
|
||||
@@ -66,7 +65,7 @@ const CARD_GAP = 8
|
||||
const GRID_STEP = CARD_SIZE + CARD_GAP
|
||||
const GRID_PAD = 8
|
||||
|
||||
type CardVariant = 'prompt' | 'table' | 'workflow' | 'knowledge' | 'logs' | 'file'
|
||||
type CardVariant = 'prompt' | 'table' | 'workflow' | 'logs' | 'file'
|
||||
|
||||
interface CardDef {
|
||||
row: number
|
||||
@@ -80,19 +79,19 @@ const MOTHERSHIP_CARDS: CardDef[] = [
|
||||
{ row: 0, col: 0, variant: 'prompt', label: 'prompt.md' },
|
||||
{ row: 1, col: 0, variant: 'table', label: 'Leads' },
|
||||
{ row: 0, col: 1, variant: 'workflow', label: 'Email Bot', color: '#7C3AED' },
|
||||
{ row: 1, col: 1, variant: 'knowledge', label: 'Company KB' },
|
||||
{ row: 1, col: 1, variant: 'file', label: 'handbook.md' },
|
||||
{ row: 2, col: 0, variant: 'logs', label: 'Run Logs' },
|
||||
{ row: 0, col: 2, variant: 'file', label: 'notes.md' },
|
||||
{ row: 2, col: 1, variant: 'workflow', label: 'Onboarding', color: '#2563EB' },
|
||||
{ row: 1, col: 2, variant: 'table', label: 'Contacts' },
|
||||
{ row: 2, col: 2, variant: 'file', label: 'report.pdf' },
|
||||
{ row: 3, col: 0, variant: 'table', label: 'Tickets' },
|
||||
{ row: 0, col: 3, variant: 'knowledge', label: 'Product Wiki' },
|
||||
{ row: 0, col: 3, variant: 'file', label: 'wiki.md' },
|
||||
{ row: 3, col: 1, variant: 'logs', label: 'Audit Trail' },
|
||||
{ row: 1, col: 3, variant: 'workflow', label: 'Support', color: '#059669' },
|
||||
{ row: 2, col: 3, variant: 'file', label: 'data.csv' },
|
||||
{ row: 3, col: 2, variant: 'table', label: 'Users' },
|
||||
{ row: 3, col: 3, variant: 'knowledge', label: 'HR Docs' },
|
||||
{ row: 3, col: 3, variant: 'file', label: 'policies.pdf' },
|
||||
{ row: 0, col: 4, variant: 'workflow', label: 'Pipeline', color: '#DC2626' },
|
||||
{ row: 1, col: 4, variant: 'logs', label: 'API Logs' },
|
||||
{ row: 2, col: 4, variant: 'table', label: 'Orders' },
|
||||
@@ -100,7 +99,7 @@ const MOTHERSHIP_CARDS: CardDef[] = [
|
||||
{ row: 0, col: 5, variant: 'logs', label: 'Deploys' },
|
||||
{ row: 1, col: 5, variant: 'table', label: 'Campaigns' },
|
||||
{ row: 2, col: 5, variant: 'workflow', label: 'Intake', color: '#D97706' },
|
||||
{ row: 3, col: 5, variant: 'knowledge', label: 'Research' },
|
||||
{ row: 3, col: 5, variant: 'file', label: 'research.pdf' },
|
||||
{ row: 4, col: 0, variant: 'file', label: 'readme.md' },
|
||||
{ row: 4, col: 1, variant: 'table', label: 'Revenue' },
|
||||
{ row: 4, col: 2, variant: 'workflow', label: 'Sync', color: '#0891B2' },
|
||||
@@ -110,27 +109,25 @@ const MOTHERSHIP_CARDS: CardDef[] = [
|
||||
{ row: 0, col: 6, variant: 'table', label: 'Analytics' },
|
||||
{ row: 1, col: 6, variant: 'workflow', label: 'Digest', color: '#6366F1' },
|
||||
{ row: 0, col: 7, variant: 'file', label: 'brief.md' },
|
||||
{ row: 2, col: 6, variant: 'knowledge', label: 'Playbooks' },
|
||||
{ row: 2, col: 6, variant: 'file', label: 'playbook.md' },
|
||||
{ row: 1, col: 7, variant: 'logs', label: 'Webhooks' },
|
||||
{ row: 3, col: 6, variant: 'file', label: 'export.csv' },
|
||||
{ row: 2, col: 7, variant: 'workflow', label: 'Alerts', color: '#E11D48' },
|
||||
{ row: 4, col: 6, variant: 'logs', label: 'Metrics' },
|
||||
{ row: 3, col: 7, variant: 'table', label: 'Feedback' },
|
||||
{ row: 4, col: 7, variant: 'knowledge', label: 'Runbooks' },
|
||||
{ row: 4, col: 7, variant: 'file', label: 'runbook.md' },
|
||||
]
|
||||
|
||||
const EXPAND_TARGETS: Record<number, { row: number; col: number }> = {
|
||||
1: { row: 1, col: 0 },
|
||||
2: { row: 0, col: 2 },
|
||||
3: { row: 1, col: 1 },
|
||||
4: { row: 2, col: 0 },
|
||||
3: { row: 2, col: 0 },
|
||||
}
|
||||
|
||||
const EXPAND_ROW_COUNTS: Record<number, number> = {
|
||||
1: 8,
|
||||
2: 10,
|
||||
3: 10,
|
||||
4: 7,
|
||||
3: 7,
|
||||
}
|
||||
|
||||
function WorkspacePreview({ activeTab, isActive }: { activeTab: number; isActive: boolean }) {
|
||||
@@ -146,7 +143,7 @@ function WorkspacePreview({ activeTab, isActive }: { activeTab: number; isActive
|
||||
const [revealedRows, setRevealedRows] = useState(0)
|
||||
|
||||
const isMothership = activeTab === 0 && isActive
|
||||
const isExpandTab = activeTab >= 1 && activeTab <= 4 && isActive
|
||||
const isExpandTab = activeTab >= 1 && activeTab <= 3 && isActive
|
||||
const expandTarget = EXPAND_TARGETS[activeTab] ?? null
|
||||
|
||||
useEffect(() => {
|
||||
@@ -292,8 +289,7 @@ function WorkspacePreview({ activeTab, isActive }: { activeTab: number; isActive
|
||||
>
|
||||
{expandedTab === 1 && <MockFullTable revealedRows={revealedRows} />}
|
||||
{expandedTab === 2 && <MockFullFiles />}
|
||||
{expandedTab === 3 && <MockFullKnowledgeBase revealedRows={revealedRows} />}
|
||||
{expandedTab === 4 && <MockFullLogs revealedRows={revealedRows} />}
|
||||
{expandedTab === 3 && <MockFullLogs revealedRows={revealedRows} />}
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
@@ -393,8 +389,6 @@ function MiniCardIcon({ variant, color }: { variant: CardVariant; color?: string
|
||||
/>
|
||||
)
|
||||
}
|
||||
case 'knowledge':
|
||||
return <Database className={cls} />
|
||||
case 'logs':
|
||||
return <Library className={cls} />
|
||||
}
|
||||
@@ -410,8 +404,6 @@ function MiniCardBody({ variant, color }: { variant: CardVariant; color?: string
|
||||
return <TableCardBody />
|
||||
case 'workflow':
|
||||
return <WorkflowCardBody color={color ?? '#7C3AED'} />
|
||||
case 'knowledge':
|
||||
return <KnowledgeCardBody />
|
||||
case 'logs':
|
||||
return <LogsCardBody />
|
||||
}
|
||||
@@ -498,21 +490,6 @@ function WorkflowCardBody({ color }: { color: string }) {
|
||||
)
|
||||
}
|
||||
|
||||
const KB_WIDTHS = [70, 85, 55, 80, 48] as const
|
||||
|
||||
function KnowledgeCardBody() {
|
||||
return (
|
||||
<div className='flex flex-col gap-[5px] px-2 py-1.5'>
|
||||
{KB_WIDTHS.map((w, i) => (
|
||||
<div key={i} className='flex items-center gap-1'>
|
||||
<div className='h-[3px] w-[3px] flex-shrink-0 rounded-full bg-[#D4D4D4]' />
|
||||
<div className='h-[1.5px] rounded-full bg-[#E8E8E8]' style={{ width: `${w}%` }} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const LOG_ENTRIES = [
|
||||
{ color: '#22C55E', width: 65 },
|
||||
{ color: '#22C55E', width: 78 },
|
||||
@@ -579,33 +556,6 @@ The team agreed to prioritize the new onboarding flow. Key decisions:
|
||||
|
||||
Follow up with engineering on the timeline for the API v2 migration. Draft the proposal for the board meeting next week.`
|
||||
|
||||
const MOCK_KB_COLUMNS = ['Name', 'Size', 'Tokens', 'Chunks', 'Status'] as const
|
||||
|
||||
const KB_FILE_ICONS: Record<string, React.ComponentType<SVGProps<SVGSVGElement>>> = {
|
||||
pdf: PdfIcon,
|
||||
md: MarkdownIcon,
|
||||
csv: CsvIcon,
|
||||
json: JsonIcon,
|
||||
}
|
||||
|
||||
function getKBFileIcon(filename: string) {
|
||||
const ext = filename.split('.').pop()?.toLowerCase() ?? ''
|
||||
return KB_FILE_ICONS[ext] ?? File
|
||||
}
|
||||
|
||||
const MOCK_KB_DATA = [
|
||||
['product-specs.pdf', '4.2 MB', '12.4k', '86', 'enabled'],
|
||||
['eng-handbook.md', '1.8 MB', '8.2k', '54', 'enabled'],
|
||||
['api-reference.json', '920 KB', '4.1k', '32', 'enabled'],
|
||||
['release-notes.md', '340 KB', '2.8k', '18', 'enabled'],
|
||||
['onboarding-guide.pdf', '2.1 MB', '6.5k', '42', 'processing'],
|
||||
['data-export.csv', '560 KB', '3.4k', '24', 'enabled'],
|
||||
['runbook.md', '280 KB', '1.9k', '14', 'enabled'],
|
||||
['compliance.pdf', '180 KB', '1.2k', '8', 'disabled'],
|
||||
['style-guide.md', '410 KB', '2.6k', '20', 'enabled'],
|
||||
['metrics.csv', '1.4 MB', '5.8k', '38', 'enabled'],
|
||||
] as const
|
||||
|
||||
const MD_COMPONENTS: Components = {
|
||||
h1: ({ children }) => (
|
||||
<p
|
||||
@@ -677,106 +627,6 @@ function MockFullFiles() {
|
||||
)
|
||||
}
|
||||
|
||||
const KB_STATUS_STYLES: Record<string, { bg: string; text: string; label: string }> = {
|
||||
enabled: { bg: '#DCFCE7', text: '#166534', label: 'Enabled' },
|
||||
disabled: { bg: '#F3F4F6', text: '#6B7280', label: 'Disabled' },
|
||||
processing: { bg: '#F3E8FF', text: '#7C3AED', label: 'Processing' },
|
||||
}
|
||||
|
||||
function MockFullKnowledgeBase({ revealedRows }: { revealedRows: number }) {
|
||||
return (
|
||||
<div className='flex h-full flex-col'>
|
||||
<div className='flex h-[44px] shrink-0 items-center border-[#E5E5E5] border-b px-6'>
|
||||
<div className='flex items-center gap-1.5'>
|
||||
<Database className='h-[14px] w-[14px] text-[#999]' />
|
||||
<span className='text-[#999] text-[13px]'>Knowledge Base</span>
|
||||
<span className='text-[#D4D4D4] text-[13px]'>/</span>
|
||||
<span className='font-medium text-[#1C1C1C] text-[13px]'>Company KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex h-[36px] shrink-0 items-center border-[#E5E5E5] border-b px-6'>
|
||||
<div className='flex items-center gap-1.5'>
|
||||
<div className='flex h-[24px] items-center gap-1 rounded-[6px] border border-[#E5E5E5] px-2 text-[#999] text-[12px]'>
|
||||
Sort
|
||||
</div>
|
||||
<div className='flex h-[24px] items-center gap-1 rounded-[6px] border border-[#E5E5E5] px-2 text-[#999] text-[12px]'>
|
||||
Filter
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex-1 overflow-hidden'>
|
||||
<table className='w-full table-fixed border-separate border-spacing-0 text-[13px]'>
|
||||
<colgroup>
|
||||
<col style={{ width: 40 }} />
|
||||
{MOCK_KB_COLUMNS.map((col) => (
|
||||
<col key={col} />
|
||||
))}
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className='border-[#E5E5E5] border-r border-b bg-[#FAFAFA] px-1 py-[7px] text-center align-middle'>
|
||||
<div className='flex items-center justify-center'>
|
||||
<div className='h-[13px] w-[13px] rounded-[2px] border border-[#D4D4D4]' />
|
||||
</div>
|
||||
</th>
|
||||
{MOCK_KB_COLUMNS.map((col) => (
|
||||
<th
|
||||
key={col}
|
||||
className='border-[#E5E5E5] border-r border-b bg-[#FAFAFA] px-2 py-[7px] text-left align-middle'
|
||||
>
|
||||
<span className='font-base text-[#999] text-[13px]'>{col}</span>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{MOCK_KB_DATA.slice(0, revealedRows).map((row, i) => {
|
||||
const status = KB_STATUS_STYLES[row[4]] ?? KB_STATUS_STYLES.enabled
|
||||
const DocIcon = getKBFileIcon(row[0])
|
||||
return (
|
||||
<motion.tr
|
||||
key={i}
|
||||
initial={{ opacity: 0, y: 4 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.2, ease: 'easeOut' }}
|
||||
>
|
||||
<td className='border-[#E5E5E5] border-r border-b px-1 py-[7px] text-center align-middle'>
|
||||
<span className='text-[#999] text-[11px] tabular-nums'>{i + 1}</span>
|
||||
</td>
|
||||
<td className='border-[#E5E5E5] border-r border-b px-2 py-[7px] align-middle'>
|
||||
<span className='flex items-center gap-2 text-[#1C1C1C] text-[13px]'>
|
||||
<DocIcon className='h-[14px] w-[14px] shrink-0' />
|
||||
<span className='truncate'>{row[0]}</span>
|
||||
</span>
|
||||
</td>
|
||||
{row.slice(1, 4).map((cell, j) => (
|
||||
<td
|
||||
key={j}
|
||||
className='border-[#E5E5E5] border-r border-b px-2 py-[7px] align-middle'
|
||||
>
|
||||
<span className='text-[#999] text-[13px]'>{cell}</span>
|
||||
</td>
|
||||
))}
|
||||
<td className='border-[#E5E5E5] border-r border-b px-2 py-[7px] align-middle'>
|
||||
<span
|
||||
className='inline-flex items-center rounded-full px-2 py-0.5 font-medium text-[11px]'
|
||||
style={{ backgroundColor: status.bg, color: status.text }}
|
||||
>
|
||||
{status.label}
|
||||
</span>
|
||||
</td>
|
||||
</motion.tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const MOCK_LOG_COLORS = [
|
||||
'#7C3AED',
|
||||
'#2563EB',
|
||||
@@ -4,8 +4,8 @@ import { useRef, useState } from 'react'
|
||||
import { type MotionValue, motion, useScroll, useTransform } from 'framer-motion'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { Badge, ChevronDown } from '@/components/emcn'
|
||||
import { FeaturesPreview } from '@/app/(home)/components/features/components/features-preview'
|
||||
import { Badge } from '@/components/emcn'
|
||||
import { FeaturesPreview } from '@/app/(landing)/components/features/components/features-preview'
|
||||
|
||||
function hexToRgba(hex: string, alpha: number): string {
|
||||
const r = Number.parseInt(hex.slice(1, 3), 16)
|
||||
@@ -14,7 +14,19 @@ function hexToRgba(hex: string, alpha: number): string {
|
||||
return `rgba(${r},${g},${b},${alpha})`
|
||||
}
|
||||
|
||||
const FEATURE_TABS = [
|
||||
interface FeatureTab {
|
||||
label: string
|
||||
mobileLabel?: string
|
||||
color: string
|
||||
badgeColor?: string
|
||||
title: string
|
||||
description: string
|
||||
cta: string
|
||||
segments: number[][]
|
||||
hideOnMobile?: boolean
|
||||
}
|
||||
|
||||
const FEATURE_TABS: FeatureTab[] = [
|
||||
{
|
||||
label: 'Mothership',
|
||||
color: '#FA4EDF',
|
||||
@@ -75,27 +87,6 @@ const FEATURE_TABS = [
|
||||
[1, 10],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Knowledge Base',
|
||||
mobileLabel: 'Knowledge',
|
||||
color: '#8B5CF6',
|
||||
title: 'Your context engine',
|
||||
description:
|
||||
'Sync institutional knowledge from 30+ live connectors — Notion, Drive, Slack, Confluence, and more — so every agent draws from the same truth across your entire organization.',
|
||||
cta: 'Explore knowledge base',
|
||||
segments: [
|
||||
[0.3, 10],
|
||||
[0.25, 8],
|
||||
[0.4, 10],
|
||||
[0.5, 10],
|
||||
[0.65, 10],
|
||||
[0.8, 10],
|
||||
[0.9, 12],
|
||||
[1, 10],
|
||||
[0.95, 10],
|
||||
[1, 10],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Logs',
|
||||
hideOnMobile: true,
|
||||
@@ -138,36 +129,6 @@ function ScrollLetter({ scrollYProgress, charIndex, children }: ScrollLetterProp
|
||||
return <motion.span style={{ opacity }}>{children}</motion.span>
|
||||
}
|
||||
|
||||
function DotGrid({
|
||||
cols,
|
||||
rows,
|
||||
width,
|
||||
borderLeft,
|
||||
}: {
|
||||
cols: number
|
||||
rows: number
|
||||
width?: number
|
||||
borderLeft?: boolean
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className={`h-full shrink-0 bg-[var(--landing-bg-section)] p-1.5 ${borderLeft ? 'border-[var(--divider)] border-l' : ''}`}
|
||||
style={{
|
||||
width: width ? `${width}px` : undefined,
|
||||
display: 'grid',
|
||||
gridTemplateColumns: `repeat(${cols}, 1fr)`,
|
||||
gap: 4,
|
||||
placeItems: 'center',
|
||||
}}
|
||||
>
|
||||
{Array.from({ length: cols * rows }, (_, i) => (
|
||||
<div key={i} className='h-[1.5px] w-[1.5px] rounded-full bg-[#DEDEDE]' />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Features() {
|
||||
const sectionRef = useRef<HTMLDivElement>(null)
|
||||
const [activeTab, setActiveTab] = useState(0)
|
||||
@@ -183,7 +144,7 @@ export default function Features() {
|
||||
aria-labelledby='features-heading'
|
||||
className='relative overflow-hidden bg-[var(--landing-bg-section)]'
|
||||
>
|
||||
<div aria-hidden='true' className='absolute top-0 left-0 w-full'>
|
||||
<div aria-hidden='true' className='absolute top-0 left-0 hidden w-full lg:block'>
|
||||
<Image
|
||||
src='/landing/features-transition.svg'
|
||||
alt=''
|
||||
@@ -194,7 +155,7 @@ export default function Features() {
|
||||
</div>
|
||||
|
||||
<div className='relative z-10 pt-[60px] lg:pt-[100px]'>
|
||||
<div ref={sectionRef} className='flex flex-col items-start gap-5 px-6 lg:px-20'>
|
||||
<div ref={sectionRef} className='flex flex-col items-start gap-5 px-6 lg:px-16'>
|
||||
<Badge
|
||||
variant='blue'
|
||||
size='md'
|
||||
@@ -210,9 +171,17 @@ export default function Features() {
|
||||
>
|
||||
Workspace
|
||||
</Badge>
|
||||
<p className='sr-only'>
|
||||
Sim's workspace includes four core features: Mothership, an AI command center for
|
||||
natural-language control of your entire workspace; Tables, a built-in database for
|
||||
filtering, sorting, and wiring data directly into workflows; Files, a shared document
|
||||
store for uploading, creating, and sharing documents, spreadsheets, and media across
|
||||
teams and agents; and Logs, full execution tracing with inputs, outputs, cost, and
|
||||
duration for every run.
|
||||
</p>
|
||||
<h2
|
||||
id='features-heading'
|
||||
className='max-w-[900px] text-balance font-[430] font-season text-[28px] text-[var(--landing-text-dark)] leading-[110%] tracking-[-0.02em] md:text-[40px]'
|
||||
className='max-w-[900px] text-balance font-[430] font-season text-[24px] text-[var(--landing-text-dark)] leading-[110%] tracking-[-0.02em] md:text-[36px]'
|
||||
>
|
||||
{HEADING_LETTERS.map((char, i) => (
|
||||
<ScrollLetter key={i} scrollYProgress={scrollYProgress} charIndex={i}>
|
||||
@@ -226,45 +195,36 @@ export default function Features() {
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className='relative mt-10 pb-10 lg:mt-[73px] lg:pb-20'>
|
||||
<div className='relative mt-10 pb-[60px] lg:mt-[73px] lg:pb-[100px]'>
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='absolute top-0 bottom-0 left-[80px] z-20 hidden w-px bg-[var(--divider)] lg:block'
|
||||
className='absolute top-0 bottom-0 left-16 z-20 hidden w-px bg-[var(--divider)] lg:block'
|
||||
/>
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='absolute top-0 right-[80px] bottom-0 z-20 hidden w-px bg-[var(--divider)] lg:block'
|
||||
className='absolute top-0 right-16 bottom-0 z-20 hidden w-px bg-[var(--divider)] lg:block'
|
||||
/>
|
||||
|
||||
<div className='flex h-[68px] border border-[var(--divider)] lg:overflow-hidden'>
|
||||
<div className='h-full shrink-0'>
|
||||
<div className='h-full lg:hidden'>
|
||||
<DotGrid cols={3} rows={8} width={24} />
|
||||
</div>
|
||||
<div className='hidden h-full lg:block'>
|
||||
<DotGrid cols={10} rows={8} width={80} />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='h-full w-[24px] shrink-0 bg-[var(--landing-bg-section)] lg:w-16'
|
||||
/>
|
||||
|
||||
<div role='tablist' aria-label='Feature categories' className='flex flex-1'>
|
||||
{FEATURE_TABS.map((tab, index) => (
|
||||
<button
|
||||
key={tab.label}
|
||||
id={`feature-tab-${index}`}
|
||||
type='button'
|
||||
role='tab'
|
||||
aria-selected={index === activeTab}
|
||||
aria-controls='features-panel'
|
||||
onClick={() => setActiveTab(index)}
|
||||
className={`relative h-full flex-1 items-center justify-center whitespace-nowrap px-3 font-medium font-season text-[var(--landing-text-dark)] text-caption uppercase lg:px-0 lg:text-sm${tab.hideOnMobile ? ' hidden lg:flex' : ' flex'}${index > 0 ? ' border-[var(--divider)] border-l' : ''}`}
|
||||
className={`relative h-full min-w-0 flex-1 items-center justify-center px-2 font-medium font-season text-[var(--landing-text-dark)] text-caption uppercase lg:px-0 lg:text-sm${tab.hideOnMobile ? ' hidden lg:flex' : ' flex'}${index > 0 ? ' border-[var(--divider)] border-l' : ''}`}
|
||||
style={{ backgroundColor: index === activeTab ? '#FDFDFD' : '#F6F6F6' }}
|
||||
>
|
||||
{tab.mobileLabel ? (
|
||||
<>
|
||||
<span className='lg:hidden'>{tab.mobileLabel}</span>
|
||||
<span className='hidden lg:inline'>{tab.label}</span>
|
||||
</>
|
||||
) : (
|
||||
tab.label
|
||||
)}
|
||||
<span className='truncate'>{tab.label}</span>
|
||||
{index === activeTab && (
|
||||
<div className='absolute right-0 bottom-0 left-0 flex h-[6px]'>
|
||||
{tab.segments.map(([opacity, width], i) => (
|
||||
@@ -284,17 +244,18 @@ export default function Features() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className='h-full shrink-0'>
|
||||
<div className='h-full lg:hidden'>
|
||||
<DotGrid cols={3} rows={8} width={24} />
|
||||
</div>
|
||||
<div className='hidden h-full lg:block'>
|
||||
<DotGrid cols={10} rows={8} width={80} />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='h-full w-[24px] shrink-0 border-[var(--divider)] border-l bg-[var(--landing-bg-section)] lg:w-16'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='mt-8 flex flex-col gap-6 px-6 lg:mt-[60px] lg:grid lg:grid-cols-[1fr_2.8fr] lg:gap-[60px] lg:px-[120px]'>
|
||||
<div
|
||||
id='features-panel'
|
||||
role='tabpanel'
|
||||
aria-labelledby={`feature-tab-${activeTab}`}
|
||||
className='mt-8 flex flex-col gap-6 px-6 lg:mt-[60px] lg:grid lg:grid-cols-[1fr_2.8fr] lg:gap-[60px] lg:px-[104px]'
|
||||
>
|
||||
<div className='flex flex-col items-start justify-between gap-6 pt-5 lg:h-[560px] lg:gap-0'>
|
||||
<div className='flex flex-col items-start gap-4'>
|
||||
<h3 className='font-[430] font-season text-[24px] text-[var(--landing-text-dark)] leading-[120%] tracking-[-0.02em] lg:text-[28px]'>
|
||||
@@ -306,26 +267,9 @@ export default function Features() {
|
||||
</div>
|
||||
<Link
|
||||
href='/signup'
|
||||
className='group/cta inline-flex h-[32px] items-center gap-1.5 rounded-[5px] border border-[#1D1D1D] bg-[#1D1D1D] px-2.5 font-[430] font-season text-sm text-white transition-colors hover:border-[var(--landing-bg-elevated)] hover:bg-[var(--landing-bg-elevated)]'
|
||||
className='inline-flex h-[32px] items-center rounded-[5px] border border-[#1D1D1D] bg-[#1D1D1D] px-2.5 font-[430] font-season text-sm text-white transition-colors hover:border-[var(--landing-bg-elevated)] hover:bg-[var(--landing-bg-elevated)]'
|
||||
>
|
||||
{FEATURE_TABS[activeTab].cta}
|
||||
<span className='relative h-[10px] w-[10px] shrink-0'>
|
||||
<ChevronDown className='-rotate-90 absolute inset-0 h-[10px] w-[10px] transition-opacity duration-150 group-hover/cta:opacity-0' />
|
||||
<svg
|
||||
className='absolute inset-0 h-[10px] w-[10px] opacity-0 transition-opacity duration-150 group-hover/cta:opacity-100'
|
||||
viewBox='0 0 10 10'
|
||||
fill='none'
|
||||
>
|
||||
<path
|
||||
d='M1 5H8M5.5 2L8.5 5L5.5 8'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.33'
|
||||
strokeLinecap='square'
|
||||
strokeLinejoin='miter'
|
||||
fill='none'
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,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 { useLandingSubmit } from '@/app/(landing)/components/landing-preview/components/landing-preview-panel/landing-preview-panel'
|
||||
import { useAnimatedPlaceholder } from '@/hooks/use-animated-placeholder'
|
||||
|
||||
const MAX_HEIGHT = 120
|
||||
@@ -41,14 +41,21 @@ export function FooterCTA() {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className='flex flex-col items-center px-4 pt-[120px] pb-[100px] sm:px-8 md:px-20'>
|
||||
<h2 className='text-balance text-center font-[430] font-season text-[28px] text-[var(--landing-text-dark)] leading-[100%] tracking-[-0.02em] sm:text-[32px] md:text-[36px]'>
|
||||
<section
|
||||
id='cta'
|
||||
aria-labelledby='cta-heading'
|
||||
className='flex flex-col items-center px-4 pt-[90px] pb-[90px] sm:px-8 sm:pt-[120px] sm:pb-[120px] md:px-16 md:pt-[150px] md:pb-[150px]'
|
||||
>
|
||||
<h2
|
||||
id='cta-heading'
|
||||
className='text-balance text-center font-[430] font-season text-[28px] text-white leading-[100%] tracking-[-0.02em] sm:text-[32px] md:text-[36px]'
|
||||
>
|
||||
What should we get done?
|
||||
</h2>
|
||||
|
||||
<div className='mt-8 w-full max-w-[42rem]'>
|
||||
<div
|
||||
className='cursor-text rounded-[20px] border border-[var(--landing-bg-skeleton)] bg-white px-2.5 py-2 shadow-sm'
|
||||
className='cursor-text rounded-[20px] border border-[var(--landing-bg-elevated)] bg-[var(--landing-bg-surface)] px-2.5 py-2'
|
||||
onClick={() => textareaRef.current?.focus()}
|
||||
>
|
||||
<textarea
|
||||
@@ -57,10 +64,11 @@ export function FooterCTA() {
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onInput={handleInput}
|
||||
aria-label='Describe what you want to build'
|
||||
placeholder={animatedPlaceholder}
|
||||
rows={2}
|
||||
className='m-0 box-border min-h-[48px] w-full resize-none border-0 bg-transparent px-1 py-1 font-body text-[var(--landing-text-dark)] text-base leading-[24px] tracking-[-0.015em] outline-none placeholder:font-[380] placeholder:text-[var(--landing-text-muted)] focus-visible:ring-0'
|
||||
style={{ caretColor: '#1C1C1C', maxHeight: `${MAX_HEIGHT}px` }}
|
||||
className='m-0 box-border min-h-[48px] w-full resize-none border-0 bg-transparent px-1 py-1 font-body text-[var(--landing-text)] text-base leading-[24px] tracking-[-0.015em] outline-none placeholder:font-[380] placeholder:text-[var(--landing-text-muted)] focus-visible:ring-0'
|
||||
style={{ caretColor: '#FFFFFF', maxHeight: `${MAX_HEIGHT}px` }}
|
||||
/>
|
||||
<div className='flex items-center justify-end'>
|
||||
<button
|
||||
@@ -70,11 +78,11 @@ export function FooterCTA() {
|
||||
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',
|
||||
background: isEmpty ? '#555555' : '#FFFFFF',
|
||||
cursor: isEmpty ? 'not-allowed' : 'pointer',
|
||||
}}
|
||||
>
|
||||
<ArrowUp size={16} strokeWidth={2.25} color='#FFFFFF' />
|
||||
<ArrowUp size={16} strokeWidth={2.25} color={isEmpty ? '#888888' : '#1C1C1C'} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -85,17 +93,17 @@ export function FooterCTA() {
|
||||
href='https://docs.sim.ai'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className={`${CTA_BUTTON} border-[var(--landing-border-subtle)] text-[var(--landing-text-dark)] transition-colors hover:bg-[var(--landing-bg-skeleton)]`}
|
||||
className={`${CTA_BUTTON} border-[var(--landing-border-strong)] text-[var(--landing-text)] transition-colors hover:bg-[var(--landing-bg-elevated)]`}
|
||||
>
|
||||
Docs
|
||||
</a>
|
||||
<Link
|
||||
href='/signup'
|
||||
className={`${CTA_BUTTON} gap-2 border-[var(--landing-bg)] bg-[var(--landing-bg)] text-white transition-colors hover:border-[var(--landing-bg-elevated)] hover:bg-[var(--landing-bg-elevated)]`}
|
||||
className={`${CTA_BUTTON} gap-2 border-white bg-white text-black transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]`}
|
||||
>
|
||||
Get started
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { FooterCTA } from '@/app/(home)/components/footer/footer-cta'
|
||||
import { FooterCTA } from '@/app/(landing)/components/footer/footer-cta'
|
||||
|
||||
const LINK_CLASS =
|
||||
'text-sm text-[var(--landing-text-muted)] transition-colors hover:text-[var(--landing-text)]'
|
||||
@@ -9,26 +9,27 @@ interface FooterItem {
|
||||
label: string
|
||||
href: string
|
||||
external?: boolean
|
||||
arrow?: boolean
|
||||
externalArrow?: boolean
|
||||
}
|
||||
|
||||
const PRODUCT_LINKS: FooterItem[] = [
|
||||
{ label: 'Pricing', href: '/#pricing' },
|
||||
{ label: 'Enterprise', href: 'https://form.typeform.com/to/jqCO12pF', external: true },
|
||||
{ label: 'Self Hosting', href: 'https://docs.sim.ai/self-hosting', external: true },
|
||||
{ label: 'MCP', href: 'https://docs.sim.ai/mcp', external: true },
|
||||
{ label: 'Knowledge Base', href: 'https://docs.sim.ai/knowledgebase', external: true },
|
||||
{ label: 'Tables', href: 'https://docs.sim.ai/tables', external: true },
|
||||
{ label: 'API', href: 'https://docs.sim.ai/api-reference/getting-started', external: true },
|
||||
{ label: 'Status', href: 'https://status.sim.ai', external: true },
|
||||
{ label: 'Status', href: 'https://status.sim.ai', external: true, externalArrow: true },
|
||||
]
|
||||
|
||||
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: 'Careers', href: 'https://jobs.ashbyhq.com/sim', external: true, externalArrow: true },
|
||||
{ label: 'Changelog', href: '/changelog' },
|
||||
]
|
||||
|
||||
@@ -46,7 +47,7 @@ const BLOCK_LINKS: FooterItem[] = [
|
||||
]
|
||||
|
||||
const INTEGRATION_LINKS: FooterItem[] = [
|
||||
{ label: 'All Integrations →', href: '/integrations' },
|
||||
{ label: 'All Integrations', href: '/integrations', arrow: true },
|
||||
{ label: 'Confluence', href: 'https://docs.sim.ai/tools/confluence', external: true },
|
||||
{ label: 'Slack', href: 'https://docs.sim.ai/tools/slack', external: true },
|
||||
{ label: 'GitHub', href: 'https://docs.sim.ai/tools/github', external: true },
|
||||
@@ -70,10 +71,20 @@ const INTEGRATION_LINKS: FooterItem[] = [
|
||||
]
|
||||
|
||||
const SOCIAL_LINKS: FooterItem[] = [
|
||||
{ label: 'X (Twitter)', href: 'https://x.com/simdotai', external: true },
|
||||
{ label: 'LinkedIn', href: 'https://www.linkedin.com/company/simstudioai/', external: true },
|
||||
{ label: 'Discord', href: 'https://discord.gg/Hr4UWYEcTT', external: true },
|
||||
{ label: 'GitHub', href: 'https://github.com/simstudioai/sim', external: true },
|
||||
{ label: 'X (Twitter)', href: 'https://x.com/simdotai', external: true, externalArrow: true },
|
||||
{
|
||||
label: 'LinkedIn',
|
||||
href: 'https://www.linkedin.com/company/simstudioai/',
|
||||
external: true,
|
||||
externalArrow: true,
|
||||
},
|
||||
{ label: 'Discord', href: 'https://discord.gg/Hr4UWYEcTT', external: true, externalArrow: true },
|
||||
{
|
||||
label: 'GitHub',
|
||||
href: 'https://github.com/simstudioai/sim',
|
||||
external: true,
|
||||
externalArrow: true,
|
||||
},
|
||||
]
|
||||
|
||||
const LEGAL_LINKS: FooterItem[] = [
|
||||
@@ -81,25 +92,62 @@ const LEGAL_LINKS: FooterItem[] = [
|
||||
{ label: 'Privacy Policy', href: '/privacy' },
|
||||
]
|
||||
|
||||
function ChevronArrow({ external }: { external?: boolean }) {
|
||||
return (
|
||||
<svg
|
||||
className={`h-3 w-3 shrink-0${external ? ' -rotate-45' : ''}`}
|
||||
viewBox='0 0 10 10'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<line
|
||||
x1='0'
|
||||
y1='5'
|
||||
x2='9'
|
||||
y2='5'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.33'
|
||||
strokeLinecap='square'
|
||||
className='origin-left scale-x-0 transition-transform duration-200 ease-out [transform-box:fill-box] group-hover/link:scale-x-100'
|
||||
/>
|
||||
<path
|
||||
d='M3.5 2L6.5 5L3.5 8'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.33'
|
||||
strokeLinecap='square'
|
||||
strokeLinejoin='miter'
|
||||
fill='none'
|
||||
className='transition-transform duration-200 ease-out group-hover/link:translate-x-[30%]'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function FooterColumn({ title, items }: { title: string; items: FooterItem[] }) {
|
||||
return (
|
||||
<div>
|
||||
<h3 className='mb-4 font-medium text-[var(--landing-text)] text-sm'>{title}</h3>
|
||||
<div className='flex flex-col gap-2.5'>
|
||||
{items.map(({ label, href, external }) =>
|
||||
{items.map(({ label, href, external, arrow, externalArrow }) =>
|
||||
external ? (
|
||||
<a
|
||||
key={label}
|
||||
href={href}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className={LINK_CLASS}
|
||||
className={`${LINK_CLASS}${externalArrow ? ' group/link inline-flex items-center gap-1' : ''}`}
|
||||
>
|
||||
{label}
|
||||
{externalArrow && <ChevronArrow external />}
|
||||
</a>
|
||||
) : (
|
||||
<Link key={label} href={href} className={LINK_CLASS}>
|
||||
<Link
|
||||
key={label}
|
||||
href={href}
|
||||
className={`${LINK_CLASS}${arrow ? ' group/link inline-flex items-center gap-1.5' : ''}`}
|
||||
>
|
||||
{label}
|
||||
{arrow && <ChevronArrow />}
|
||||
</Link>
|
||||
)
|
||||
)}
|
||||
@@ -116,13 +164,31 @@ export default function Footer({ hideCTA }: FooterProps) {
|
||||
return (
|
||||
<footer
|
||||
role='contentinfo'
|
||||
className={`bg-[var(--landing-bg-section)] pb-10 font-[430] font-season text-sm${hideCTA ? ' pt-10' : ''}`}
|
||||
className={`bg-[var(--landing-bg)] pb-10 font-[430] font-season text-sm${hideCTA ? ' pt-10' : ''}`}
|
||||
>
|
||||
{!hideCTA && <FooterCTA />}
|
||||
<div className='px-4 sm:px-8 md:px-20'>
|
||||
<div className='relative overflow-hidden rounded-lg bg-[var(--landing-bg)] px-6 pt-10 pb-8 sm:px-10 sm:pt-12 sm:pb-10'>
|
||||
<div className='relative px-[1.6vw] sm:px-8 lg:px-16'>
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='absolute top-0 left-0 z-20 hidden h-px w-[calc(4rem+4px)] bg-[var(--landing-bg-elevated)] lg:block'
|
||||
/>
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='absolute top-0 right-0 z-20 hidden h-px w-[calc(4rem+4px)] bg-[var(--landing-bg-elevated)] lg:block'
|
||||
/>
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='absolute bottom-0 left-0 z-20 hidden h-px w-[calc(4rem+4px)] bg-[var(--landing-bg-elevated)] lg:block'
|
||||
/>
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='absolute right-0 bottom-0 z-20 hidden h-px w-[calc(4rem+4px)] bg-[var(--landing-bg-elevated)] lg:block'
|
||||
/>
|
||||
<div className='relative z-10 border border-[var(--landing-bg-elevated)] px-6 pt-10 pb-8 sm:px-10 sm:pt-12 sm:pb-10'>
|
||||
<nav
|
||||
aria-label='Footer navigation'
|
||||
itemScope
|
||||
itemType='https://schema.org/SiteNavigationElement'
|
||||
className='relative z-[1] grid grid-cols-2 gap-x-8 gap-y-10 sm:grid-cols-3 lg:grid-cols-7'
|
||||
>
|
||||
<div className='col-span-2 flex flex-col gap-6 sm:col-span-1'>
|
||||
@@ -144,29 +210,6 @@ export default function Footer({ hideCTA }: FooterProps) {
|
||||
<FooterColumn title='Socials' items={SOCIAL_LINKS} />
|
||||
<FooterColumn title='Legal' items={LEGAL_LINKS} />
|
||||
</nav>
|
||||
|
||||
{/* <svg
|
||||
aria-hidden='true'
|
||||
className='pointer-events-none absolute bottom-0 left-[-60px] hidden w-[85%] sm:block'
|
||||
viewBox='0 0 1800 316'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<path
|
||||
d='M18.3562 305V48.95A30.594 30.594 0 0 1 48.95 18.356H917.05A30.594 30.594 0 0 1 947.644 48.95V273H1768C1777.11 273 1784.5 280.387 1784.5 289.5C1784.5 298.613 1777.11 306 1768 306H96.8603C78.635 306 63.8604 310 63.8604 305H18.3562'
|
||||
stroke='#2A2A2A'
|
||||
strokeWidth='2'
|
||||
/>
|
||||
<rect
|
||||
x='58'
|
||||
y='58'
|
||||
width='849.288'
|
||||
height='199.288'
|
||||
rx='14'
|
||||
stroke='#2A2A2A'
|
||||
strokeWidth='2'
|
||||
/>
|
||||
</svg> */}
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
97
apps/sim/app/(landing)/components/hero/hero.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
'use client'
|
||||
|
||||
import dynamic from 'next/dynamic'
|
||||
import Link from 'next/link'
|
||||
import { DemoRequestModal } from '@/app/(landing)/components/demo-request/demo-request-modal'
|
||||
|
||||
const LandingPreview = dynamic(
|
||||
() =>
|
||||
import('@/app/(landing)/components/landing-preview/landing-preview').then(
|
||||
(mod) => mod.LandingPreview
|
||||
),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => <div className='aspect-[1116/615] w-full rounded bg-[var(--landing-bg)]' />,
|
||||
}
|
||||
)
|
||||
|
||||
/** Shared base classes for CTA link buttons — matches Deploy/Run button styling in the preview panel. */
|
||||
const CTA_BASE =
|
||||
'inline-flex items-center h-[32px] rounded-[5px] border px-2.5 font-[430] font-season text-sm'
|
||||
|
||||
export default function Hero() {
|
||||
return (
|
||||
<section
|
||||
id='hero'
|
||||
aria-labelledby='hero-heading'
|
||||
itemScope
|
||||
itemType='https://schema.org/WebApplication'
|
||||
className='relative flex flex-col items-center overflow-hidden bg-[var(--landing-bg)] pt-[60px] lg:pt-[100px]'
|
||||
>
|
||||
<p className='sr-only'>
|
||||
Sim is an open-source AI agent platform. Sim lets teams build AI agents and run an agentic
|
||||
workforce by connecting 1,000+ integrations and LLMs — including OpenAI, Anthropic Claude,
|
||||
Google Gemini, Mistral, and xAI Grok — to deploy and orchestrate agentic workflows. Users
|
||||
create agents, workflows, knowledge bases, tables, and docs. Sim is trusted by over 100,000
|
||||
builders at startups and Fortune 500 companies. Sim is SOC2 compliant.
|
||||
</p>
|
||||
|
||||
<div className='relative z-10 flex flex-col items-center gap-3'>
|
||||
<h1
|
||||
id='hero-heading'
|
||||
itemProp='name'
|
||||
className='text-balance font-[430] font-season text-[36px] text-white leading-[100%] tracking-[-0.02em] sm:text-[48px] lg:text-[72px]'
|
||||
>
|
||||
Build AI Agents
|
||||
</h1>
|
||||
<p
|
||||
itemProp='description'
|
||||
className='whitespace-nowrap text-center font-[430] font-season text-[4.4vw] text-[color-mix(in_srgb,var(--landing-text-subtle)_60%,transparent)] leading-[125%] tracking-[0.02em] sm:whitespace-normal sm:text-lg lg:text-xl'
|
||||
>
|
||||
Sim is the AI Workspace for Agent Builders
|
||||
</p>
|
||||
|
||||
<div className='mt-3 flex items-center gap-2'>
|
||||
<DemoRequestModal theme='light'>
|
||||
<button
|
||||
type='button'
|
||||
className={`${CTA_BASE} border-[var(--landing-border-strong)] bg-transparent text-[var(--landing-text)] transition-colors hover:bg-[var(--landing-bg-elevated)]`}
|
||||
aria-label='Get a demo'
|
||||
>
|
||||
Get a demo
|
||||
</button>
|
||||
</DemoRequestModal>
|
||||
<Link
|
||||
href='/signup'
|
||||
className={`${CTA_BASE} gap-2 border-white bg-white text-black transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]`}
|
||||
aria-label='Get started with Sim'
|
||||
>
|
||||
Get started
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='relative z-10 mx-auto mt-6 w-[92vw] px-[1.6vw] lg:mt-[3.2vw] lg:w-full lg:px-16'>
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='absolute top-0 left-0 z-20 hidden h-px w-[calc(4rem+4px)] bg-[var(--landing-bg-elevated)] lg:block'
|
||||
/>
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='absolute top-0 right-0 z-20 hidden h-px w-[calc(4rem+4px)] bg-[var(--landing-bg-elevated)] lg:block'
|
||||
/>
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='absolute bottom-0 left-0 z-20 hidden h-px w-[calc(4rem+4px)] bg-[var(--landing-bg-elevated)] lg:block'
|
||||
/>
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='absolute right-0 bottom-0 z-20 hidden h-px w-[calc(4rem+4px)] bg-[var(--landing-bg-elevated)] lg:block'
|
||||
/>
|
||||
<div className='relative z-10 overflow-hidden rounded border border-[var(--landing-bg-elevated)]'>
|
||||
<LandingPreview />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,27 @@
|
||||
import Collaboration from '@/app/(landing)/components/collaboration/collaboration'
|
||||
import Enterprise from '@/app/(landing)/components/enterprise/enterprise'
|
||||
import ExternalRedirect from '@/app/(landing)/components/external-redirect'
|
||||
import Features from '@/app/(landing)/components/features/features'
|
||||
import Footer from '@/app/(landing)/components/footer/footer'
|
||||
import Hero from '@/app/(landing)/components/hero/hero'
|
||||
import LegalLayout from '@/app/(landing)/components/legal-layout'
|
||||
import Navbar from '@/app/(landing)/components/navbar/navbar'
|
||||
import Pricing from '@/app/(landing)/components/pricing/pricing'
|
||||
import StructuredData from '@/app/(landing)/components/structured-data'
|
||||
import Templates from '@/app/(landing)/components/templates/templates'
|
||||
import Testimonials from '@/app/(landing)/components/testimonials/testimonials'
|
||||
|
||||
export { LegalLayout, ExternalRedirect }
|
||||
export {
|
||||
Collaboration,
|
||||
Enterprise,
|
||||
ExternalRedirect,
|
||||
Features,
|
||||
Footer,
|
||||
Hero,
|
||||
LegalLayout,
|
||||
Navbar,
|
||||
Pricing,
|
||||
StructuredData,
|
||||
Templates,
|
||||
Testimonials,
|
||||
}
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
import { File } from '@/components/emcn/icons'
|
||||
import { DocxIcon, PdfIcon } from '@/components/icons/document-icons'
|
||||
import type {
|
||||
PreviewColumn,
|
||||
PreviewRow,
|
||||
} from '@/app/(landing)/components/landing-preview/components/landing-preview-resource/landing-preview-resource'
|
||||
import {
|
||||
LandingPreviewResource,
|
||||
ownerCell,
|
||||
} from '@/app/(landing)/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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,479 @@
|
||||
'use client'
|
||||
|
||||
import { memo, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { ArrowUp, Table } from 'lucide-react'
|
||||
import { Blimp, Checkbox, ChevronDown } from '@/components/emcn'
|
||||
import { TypeBoolean, TypeNumber, TypeText } from '@/components/emcn/icons'
|
||||
import { useLandingSubmit } from '@/app/(landing)/components/landing-preview/components/landing-preview-panel/landing-preview-panel'
|
||||
import { EASE_OUT } from '@/app/(landing)/components/landing-preview/components/landing-preview-workflow/workflow-data'
|
||||
import { useAnimatedPlaceholder } from '@/hooks/use-animated-placeholder'
|
||||
|
||||
const C = {
|
||||
SURFACE: '#292929',
|
||||
BORDER: '#3d3d3d',
|
||||
TEXT_PRIMARY: '#e6e6e6',
|
||||
TEXT_BODY: '#cdcdcd',
|
||||
TEXT_SECONDARY: '#b3b3b3',
|
||||
TEXT_TERTIARY: '#939393',
|
||||
TEXT_ICON: '#939393',
|
||||
} as const
|
||||
|
||||
const AUTO_PROMPT = 'Analyze our customer leads and identify the top prospects'
|
||||
|
||||
const MOCK_RESPONSE =
|
||||
'I analyzed your **Customer Leads** table and found **3 top prospects** with the highest lead scores:\n\n1. **Carol Davis** (StartupCo) — Score: 94\n2. **Frank Lee** (Ventures) — Score: 88\n3. **Alice Johnson** (Acme Corp) — Score: 87\n\nAll three are qualified leads. Want me to draft outreach emails?'
|
||||
|
||||
const HOME_TYPE_MS = 40
|
||||
const HOME_TYPE_START_MS = 600
|
||||
const TOOL_CALL_DELAY_MS = 500
|
||||
const RESPONSE_DELAY_MS = 800
|
||||
const RESOURCE_PANEL_DELAY_MS = 600
|
||||
|
||||
const MINI_TABLE_COLUMNS = [
|
||||
{ id: 'name', label: 'Name', type: 'text' as const, width: '32%' },
|
||||
{ id: 'company', label: 'Company', type: 'text' as const, width: '30%' },
|
||||
{ id: 'score', label: 'Score', type: 'number' as const, width: '18%' },
|
||||
{ id: 'qualified', label: 'Qualified', type: 'boolean' as const, width: '20%' },
|
||||
]
|
||||
|
||||
const MINI_TABLE_ROWS = [
|
||||
{ name: 'Alice Johnson', company: 'Acme Corp', score: '87', qualified: 'true' },
|
||||
{ name: 'Bob Williams', company: 'TechCo', score: '62', qualified: 'false' },
|
||||
{ name: 'Carol Davis', company: 'StartupCo', score: '94', qualified: 'true' },
|
||||
{ name: 'Dan Miller', company: 'BigCorp', score: '71', qualified: 'true' },
|
||||
{ name: 'Eva Chen', company: 'Design IO', score: '45', qualified: 'false' },
|
||||
{ name: 'Frank Lee', company: 'Ventures', score: '88', qualified: 'true' },
|
||||
]
|
||||
|
||||
const COLUMN_TYPE_ICONS = {
|
||||
text: TypeText,
|
||||
number: TypeNumber,
|
||||
boolean: TypeBoolean,
|
||||
} as const
|
||||
|
||||
interface LandingPreviewHomeProps {
|
||||
autoType?: boolean
|
||||
}
|
||||
|
||||
type ChatPhase = 'input' | 'sent' | 'tool-call' | 'responding' | 'done'
|
||||
|
||||
/**
|
||||
* Landing preview replica of the workspace Home view.
|
||||
*
|
||||
* When `autoType` is true, automatically types a prompt, sends it,
|
||||
* shows a mothership agent group with tool calls, types a response,
|
||||
* and opens a resource panel — matching the real workspace chat UI.
|
||||
*/
|
||||
export const LandingPreviewHome = memo(function LandingPreviewHome({
|
||||
autoType = false,
|
||||
}: LandingPreviewHomeProps) {
|
||||
const landingSubmit = useLandingSubmit()
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
const animatedPlaceholder = useAnimatedPlaceholder()
|
||||
|
||||
const [chatPhase, setChatPhase] = useState<ChatPhase>('input')
|
||||
const [responseTypedLength, setResponseTypedLength] = useState(0)
|
||||
const [showResourcePanel, setShowResourcePanel] = useState(false)
|
||||
const [toolsExpanded, setToolsExpanded] = useState(true)
|
||||
|
||||
const typeIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||
const responseIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||
const timersRef = useRef<ReturnType<typeof setTimeout>[]>([])
|
||||
|
||||
const clearAllTimers = useCallback(() => {
|
||||
for (const t of timersRef.current) clearTimeout(t)
|
||||
timersRef.current = []
|
||||
if (typeIntervalRef.current) clearInterval(typeIntervalRef.current)
|
||||
if (responseIntervalRef.current) clearInterval(responseIntervalRef.current)
|
||||
typeIntervalRef.current = null
|
||||
responseIntervalRef.current = null
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoType) return
|
||||
|
||||
setChatPhase('input')
|
||||
setResponseTypedLength(0)
|
||||
setShowResourcePanel(false)
|
||||
setToolsExpanded(true)
|
||||
setInputValue('')
|
||||
|
||||
const t1 = setTimeout(() => {
|
||||
let idx = 0
|
||||
typeIntervalRef.current = setInterval(() => {
|
||||
idx++
|
||||
setInputValue(AUTO_PROMPT.slice(0, idx))
|
||||
if (idx >= AUTO_PROMPT.length) {
|
||||
if (typeIntervalRef.current) clearInterval(typeIntervalRef.current)
|
||||
typeIntervalRef.current = null
|
||||
|
||||
const t2 = setTimeout(() => {
|
||||
setChatPhase('sent')
|
||||
|
||||
const t3 = setTimeout(() => {
|
||||
setChatPhase('tool-call')
|
||||
|
||||
const t4 = setTimeout(() => {
|
||||
setShowResourcePanel(true)
|
||||
}, RESOURCE_PANEL_DELAY_MS)
|
||||
timersRef.current.push(t4)
|
||||
|
||||
const t5 = setTimeout(() => {
|
||||
setToolsExpanded(false)
|
||||
setChatPhase('responding')
|
||||
let rIdx = 0
|
||||
responseIntervalRef.current = setInterval(() => {
|
||||
rIdx++
|
||||
setResponseTypedLength(rIdx)
|
||||
if (rIdx >= MOCK_RESPONSE.length) {
|
||||
if (responseIntervalRef.current) clearInterval(responseIntervalRef.current)
|
||||
responseIntervalRef.current = null
|
||||
setChatPhase('done')
|
||||
}
|
||||
}, 8)
|
||||
}, TOOL_CALL_DELAY_MS + RESPONSE_DELAY_MS)
|
||||
timersRef.current.push(t5)
|
||||
}, TOOL_CALL_DELAY_MS)
|
||||
timersRef.current.push(t3)
|
||||
}, 400)
|
||||
timersRef.current.push(t2)
|
||||
}
|
||||
}, HOME_TYPE_MS)
|
||||
}, HOME_TYPE_START_MS)
|
||||
timersRef.current.push(t1)
|
||||
|
||||
return clearAllTimers
|
||||
}, [autoType, clearAllTimers])
|
||||
|
||||
const isEmpty = inputValue.trim().length === 0
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
if (isEmpty) return
|
||||
landingSubmit(inputValue)
|
||||
}, [isEmpty, inputValue, landingSubmit])
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
handleSubmit()
|
||||
}
|
||||
},
|
||||
[handleSubmit]
|
||||
)
|
||||
|
||||
const handleInput = useCallback((e: React.FormEvent<HTMLTextAreaElement>) => {
|
||||
const target = e.target as HTMLTextAreaElement
|
||||
target.style.height = 'auto'
|
||||
target.style.height = `${Math.min(target.scrollHeight, 200)}px`
|
||||
}, [])
|
||||
|
||||
if (chatPhase !== 'input') {
|
||||
const isResponding = chatPhase === 'responding' || chatPhase === 'done'
|
||||
const showToolCall = chatPhase === 'tool-call' || isResponding
|
||||
|
||||
return (
|
||||
<div className='flex min-w-0 flex-1 overflow-hidden'>
|
||||
{/* Chat area — matches mothership-view layout */}
|
||||
<div className='min-h-0 flex-1 overflow-y-auto overflow-x-hidden px-6 pt-4 pb-8'>
|
||||
<div className='mx-auto max-w-[42rem] space-y-6'>
|
||||
{/* User message — rounded bubble, right-aligned */}
|
||||
<motion.div
|
||||
className='flex flex-col items-end gap-[6px] pt-3'
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, ease: EASE_OUT }}
|
||||
>
|
||||
<div className='max-w-[70%] overflow-hidden rounded-[16px] bg-[#363636] px-3.5 py-2'>
|
||||
<p
|
||||
className='font-body text-[14px] leading-[1.5]'
|
||||
style={{ color: C.TEXT_PRIMARY }}
|
||||
>
|
||||
{AUTO_PROMPT}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Assistant — no bubble, full-width prose */}
|
||||
<AnimatePresence>
|
||||
{showToolCall && (
|
||||
<motion.div
|
||||
className='space-y-2.5'
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, ease: EASE_OUT }}
|
||||
>
|
||||
{/* Agent group header — icon + label + chevron */}
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => setToolsExpanded((p) => !p)}
|
||||
className='flex cursor-pointer items-center gap-2'
|
||||
>
|
||||
<div className='flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center'>
|
||||
<Blimp className='h-[16px] w-[16px]' style={{ color: C.TEXT_ICON }} />
|
||||
</div>
|
||||
<span className='font-base text-sm' style={{ color: C.TEXT_BODY }}>
|
||||
Mothership
|
||||
</span>
|
||||
<ChevronDown
|
||||
className='h-[7px] w-[9px] transition-transform duration-150'
|
||||
style={{
|
||||
color: C.TEXT_ICON,
|
||||
transform: toolsExpanded ? 'rotate(0deg)' : 'rotate(-90deg)',
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{/* Tool call items — collapsible */}
|
||||
<div
|
||||
className='grid transition-[grid-template-rows] duration-200 ease-out'
|
||||
style={{
|
||||
gridTemplateRows: toolsExpanded ? '1fr' : '0fr',
|
||||
}}
|
||||
>
|
||||
<div className='overflow-hidden'>
|
||||
<div className='flex flex-col gap-1.5 pt-0.5'>
|
||||
<ToolCallRow
|
||||
icon={
|
||||
<Table
|
||||
className='h-[15px] w-[15px]'
|
||||
style={{ color: C.TEXT_TERTIARY }}
|
||||
/>
|
||||
}
|
||||
title='Read Customer Leads'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Response prose — full width, no card */}
|
||||
{isResponding && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.2, ease: EASE_OUT }}
|
||||
>
|
||||
<ChatMarkdown
|
||||
content={MOCK_RESPONSE}
|
||||
visibleLength={responseTypedLength}
|
||||
isTyping={chatPhase === 'responding'}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Resource panel — slides in from right */}
|
||||
<AnimatePresence>
|
||||
{showResourcePanel && (
|
||||
<motion.div
|
||||
className='hidden h-full flex-shrink-0 overflow-hidden border-[#2c2c2c] border-l lg:flex'
|
||||
initial={{ width: 0, opacity: 0 }}
|
||||
animate={{ width: '55%', opacity: 1 }}
|
||||
transition={{ duration: 0.35, ease: EASE_OUT }}
|
||||
>
|
||||
<MiniTablePanel />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex min-w-0 flex-1 flex-col items-center justify-center px-6 pb-[2vh]'>
|
||||
<motion.p
|
||||
role='presentation'
|
||||
className='mb-6 max-w-[42rem] font-[430] font-season text-[32px] tracking-[-0.02em]'
|
||||
style={{ color: C.TEXT_PRIMARY }}
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4, ease: EASE_OUT }}
|
||||
>
|
||||
What should we get done?
|
||||
</motion.p>
|
||||
|
||||
<motion.div
|
||||
className='w-full max-w-[32rem]'
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4, delay: 0.1, ease: EASE_OUT }}
|
||||
>
|
||||
<div
|
||||
className='cursor-text rounded-[20px] border px-2.5 py-2'
|
||||
style={{ borderColor: C.BORDER, backgroundColor: C.SURFACE }}
|
||||
onClick={() => textareaRef.current?.focus()}
|
||||
>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={inputValue}
|
||||
onChange={(e) => {
|
||||
if (!autoType) setInputValue(e.target.value)
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
onInput={handleInput}
|
||||
placeholder={animatedPlaceholder}
|
||||
rows={1}
|
||||
readOnly={autoType}
|
||||
className='m-0 box-border min-h-[24px] w-full resize-none overflow-y-auto border-0 bg-transparent px-1 py-1 font-body text-[15px] leading-[24px] tracking-[-0.015em] outline-none placeholder:font-[380] placeholder:text-[#787878] focus-visible:ring-0'
|
||||
style={{
|
||||
color: C.TEXT_PRIMARY,
|
||||
caretColor: autoType ? 'transparent' : C.TEXT_PRIMARY,
|
||||
maxHeight: '200px',
|
||||
}}
|
||||
/>
|
||||
<div className='flex items-center justify-end'>
|
||||
<button
|
||||
type='button'
|
||||
onClick={handleSubmit}
|
||||
disabled={isEmpty}
|
||||
className='flex h-[28px] w-[28px] items-center justify-center rounded-full border-0 p-0 transition-colors'
|
||||
style={{
|
||||
background: isEmpty ? '#808080' : '#e0e0e0',
|
||||
cursor: isEmpty ? 'not-allowed' : 'pointer',
|
||||
}}
|
||||
>
|
||||
<ArrowUp size={16} strokeWidth={2.25} color='#1b1b1b' />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* Single tool call row matching the real `ToolCallItem` layout:
|
||||
* indented icon + display title.
|
||||
*/
|
||||
function ToolCallRow({ icon, title }: { icon: React.ReactNode; title: string }) {
|
||||
return (
|
||||
<div className='flex items-center gap-[8px] pl-[24px]'>
|
||||
<div className='flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center'>{icon}</div>
|
||||
<span className='font-base text-[13px]' style={{ color: C.TEXT_SECONDARY }}>
|
||||
{title}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders chat response as full-width prose with bold markdown
|
||||
* and progressive reveal for the typing effect.
|
||||
*/
|
||||
function ChatMarkdown({
|
||||
content,
|
||||
visibleLength,
|
||||
isTyping,
|
||||
}: {
|
||||
content: string
|
||||
visibleLength: number
|
||||
isTyping: boolean
|
||||
}) {
|
||||
const visible = content.slice(0, visibleLength)
|
||||
const rendered = visible.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>').replace(/\n/g, '<br />')
|
||||
|
||||
return (
|
||||
<div className='font-body text-[14px] leading-[1.6]' style={{ color: C.TEXT_PRIMARY }}>
|
||||
<span dangerouslySetInnerHTML={{ __html: rendered }} />
|
||||
{isTyping && (
|
||||
<motion.span
|
||||
className='inline-block h-[14px] w-[1.5px] translate-y-[2px] bg-[#e6e6e6]'
|
||||
animate={{ opacity: [1, 0] }}
|
||||
transition={{
|
||||
duration: 0.6,
|
||||
repeat: Number.POSITIVE_INFINITY,
|
||||
repeatType: 'reverse',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Mini Customer Leads table panel matching the resource panel pattern.
|
||||
*/
|
||||
function MiniTablePanel() {
|
||||
return (
|
||||
<div className='flex h-full w-full flex-col bg-[var(--landing-bg)]'>
|
||||
<div className='flex items-center gap-2 border-[#2c2c2c] border-b px-3 py-2'>
|
||||
<Table className='h-[14px] w-[14px]' style={{ color: C.TEXT_ICON }} />
|
||||
<span className='font-medium text-sm' style={{ color: C.TEXT_PRIMARY }}>
|
||||
Customer Leads
|
||||
</span>
|
||||
</div>
|
||||
<div className='min-h-0 flex-1 overflow-auto'>
|
||||
<table className='w-full table-fixed border-separate border-spacing-0 text-[12px]'>
|
||||
<colgroup>
|
||||
{MINI_TABLE_COLUMNS.map((col) => (
|
||||
<col key={col.id} style={{ width: col.width }} />
|
||||
))}
|
||||
</colgroup>
|
||||
<thead className='sticky top-0 z-10'>
|
||||
<tr>
|
||||
{MINI_TABLE_COLUMNS.map((col) => {
|
||||
const Icon = COLUMN_TYPE_ICONS[col.type]
|
||||
return (
|
||||
<th
|
||||
key={col.id}
|
||||
className='border-[#2c2c2c] border-r border-b bg-[#1e1e1e] p-0 text-left'
|
||||
>
|
||||
<div className='flex items-center gap-1 px-2 py-1.5'>
|
||||
<Icon className='h-3 w-3 shrink-0' style={{ color: C.TEXT_ICON }} />
|
||||
<span className='font-medium text-[11px]' style={{ color: C.TEXT_PRIMARY }}>
|
||||
{col.label}
|
||||
</span>
|
||||
<ChevronDown
|
||||
className='ml-auto h-[6px] w-[8px]'
|
||||
style={{ color: '#636363' }}
|
||||
/>
|
||||
</div>
|
||||
</th>
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{MINI_TABLE_ROWS.map((row, i) => (
|
||||
<motion.tr
|
||||
key={i}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.2, delay: i * 0.04, ease: EASE_OUT }}
|
||||
>
|
||||
{MINI_TABLE_COLUMNS.map((col) => {
|
||||
const val = row[col.id as keyof typeof row]
|
||||
return (
|
||||
<td
|
||||
key={col.id}
|
||||
className='border-[#2c2c2c] border-r border-b px-2 py-1.5'
|
||||
style={{ color: C.TEXT_BODY }}
|
||||
>
|
||||
{col.type === 'boolean' ? (
|
||||
<div className='flex items-center justify-center'>
|
||||
<Checkbox
|
||||
size='sm'
|
||||
checked={val === 'true'}
|
||||
className='pointer-events-none'
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<span className='block truncate'>{val}</span>
|
||||
)}
|
||||
</td>
|
||||
)
|
||||
})}
|
||||
</motion.tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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/(landing)/components/landing-preview/components/landing-preview-resource/landing-preview-resource'
|
||||
import { LandingPreviewResource } from '@/app/(landing)/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: '1 credit',
|
||||
trigger: 'api',
|
||||
triggerLabel: 'API',
|
||||
duration: '2.7s',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
workflowName: 'Email Campaign',
|
||||
workflowColor: '#a855f7',
|
||||
date: 'Apr 1 08:30 AM',
|
||||
status: 'completed',
|
||||
cost: '2 credits',
|
||||
trigger: 'schedule',
|
||||
triggerLabel: 'Schedule',
|
||||
duration: '0.8s',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
workflowName: 'Data Pipeline',
|
||||
workflowColor: '#f97316',
|
||||
date: 'Mar 31 10:14 PM',
|
||||
status: 'completed',
|
||||
cost: '7 credits',
|
||||
trigger: 'webhook',
|
||||
triggerLabel: 'Webhook',
|
||||
duration: '4.1s',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
workflowName: 'Invoice Processing',
|
||||
workflowColor: '#ec4899',
|
||||
date: 'Mar 31 08:45 PM',
|
||||
status: 'completed',
|
||||
cost: '2 credits',
|
||||
trigger: 'manual',
|
||||
triggerLabel: 'Manual',
|
||||
duration: '0.9s',
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
workflowName: 'Support Triage',
|
||||
workflowColor: '#0ea5e9',
|
||||
date: 'Mar 31 07:22 PM',
|
||||
status: 'completed',
|
||||
cost: '3 credits',
|
||||
trigger: 'api',
|
||||
triggerLabel: 'API',
|
||||
duration: '1.6s',
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
workflowName: 'Content Moderator',
|
||||
workflowColor: '#f59e0b',
|
||||
date: 'Mar 31 06:11 PM',
|
||||
status: 'error',
|
||||
cost: '1 credit',
|
||||
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,474 @@
|
||||
'use client'
|
||||
|
||||
import { memo, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { ArrowUp } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { Blimp, BubbleChatPreview, ChevronDown, MoreHorizontal, Play } from '@/components/emcn'
|
||||
import { AgentIcon, HubspotIcon, OpenAIIcon, SalesforceIcon } from '@/components/icons'
|
||||
import { LandingPromptStorage } from '@/lib/core/utils/browser-storage'
|
||||
import {
|
||||
EASE_OUT,
|
||||
type EditorPromptData,
|
||||
getEditorPrompt,
|
||||
getWorkflowAnimationTiming,
|
||||
type PreviewWorkflow,
|
||||
TYPE_INTERVAL_MS,
|
||||
TYPE_START_BUFFER_MS,
|
||||
} from '@/app/(landing)/components/landing-preview/components/landing-preview-workflow/workflow-data'
|
||||
|
||||
type PanelTab = 'copilot' | 'editor'
|
||||
|
||||
const EDITOR_BLOCK_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {
|
||||
agent: AgentIcon,
|
||||
mothership: Blimp,
|
||||
}
|
||||
|
||||
const TABS_WITH_TOOLBAR: { id: PanelTab | 'toolbar'; label: string; disabled?: boolean }[] = [
|
||||
{ id: 'copilot', label: 'Copilot' },
|
||||
{ id: 'toolbar', label: 'Toolbar', disabled: true },
|
||||
{ id: 'editor', label: 'Editor' },
|
||||
]
|
||||
|
||||
/**
|
||||
* Stores the prompt in browser storage and redirects to /signup.
|
||||
* Shared by both the copilot panel and the landing home view.
|
||||
*/
|
||||
export function useLandingSubmit() {
|
||||
const router = useRouter()
|
||||
return useCallback(
|
||||
(text: string) => {
|
||||
const trimmed = text.trim()
|
||||
if (!trimmed) return
|
||||
LandingPromptStorage.store(trimmed)
|
||||
router.push('/signup')
|
||||
},
|
||||
[router]
|
||||
)
|
||||
}
|
||||
|
||||
interface LandingPreviewPanelProps {
|
||||
activeWorkflow?: PreviewWorkflow
|
||||
animationKey?: number
|
||||
onHighlightBlock?: (blockId: string | null) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Workspace panel replica with switchable Copilot / Editor tabs.
|
||||
*
|
||||
* On every workflow switch (`animationKey` change):
|
||||
* 1. Resets to Copilot tab.
|
||||
* 2. Waits for blocks + edges to finish animating.
|
||||
* 3. Slides the tab indicator to Editor and types the agent's prompt.
|
||||
* 4. Highlights the agent block with the blue ring on the canvas.
|
||||
*/
|
||||
export const LandingPreviewPanel = memo(function LandingPreviewPanel({
|
||||
activeWorkflow,
|
||||
animationKey = 0,
|
||||
onHighlightBlock,
|
||||
}: LandingPreviewPanelProps) {
|
||||
const landingSubmit = useLandingSubmit()
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
const [cursorPos, setCursorPos] = useState<{ x: number; y: number } | null>(null)
|
||||
|
||||
const [activeTab, setActiveTab] = useState<PanelTab>('copilot')
|
||||
const [typedLength, setTypedLength] = useState(0)
|
||||
|
||||
const workflowRef = useRef(activeWorkflow)
|
||||
workflowRef.current = activeWorkflow
|
||||
const typeIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||
|
||||
const editorPrompt = activeWorkflow ? getEditorPrompt(activeWorkflow) : null
|
||||
|
||||
const userSwitchedTabRef = useRef(false)
|
||||
|
||||
const handleTabSwitch = useCallback(
|
||||
(tab: PanelTab) => {
|
||||
userSwitchedTabRef.current = true
|
||||
setActiveTab(tab)
|
||||
if (tab === 'editor' && editorPrompt) {
|
||||
onHighlightBlock?.(editorPrompt.blockId)
|
||||
} else {
|
||||
onHighlightBlock?.(null)
|
||||
}
|
||||
},
|
||||
[editorPrompt, onHighlightBlock]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (userSwitchedTabRef.current) return
|
||||
|
||||
setActiveTab('copilot')
|
||||
setTypedLength(0)
|
||||
onHighlightBlock?.(null)
|
||||
if (typeIntervalRef.current) clearInterval(typeIntervalRef.current)
|
||||
|
||||
const workflow = workflowRef.current
|
||||
if (!workflow) return
|
||||
|
||||
const prompt = workflow ? getEditorPrompt(workflow) : null
|
||||
if (!prompt) return
|
||||
|
||||
const { editorDelay } = getWorkflowAnimationTiming(workflow)
|
||||
|
||||
const switchTimer = setTimeout(() => {
|
||||
if (userSwitchedTabRef.current) return
|
||||
setActiveTab('editor')
|
||||
onHighlightBlock?.(prompt.blockId)
|
||||
}, editorDelay)
|
||||
|
||||
const typeTimer = setTimeout(() => {
|
||||
if (userSwitchedTabRef.current) return
|
||||
let charIndex = 0
|
||||
typeIntervalRef.current = setInterval(() => {
|
||||
charIndex++
|
||||
setTypedLength(charIndex)
|
||||
if (charIndex >= prompt.prompt.length) {
|
||||
if (typeIntervalRef.current) clearInterval(typeIntervalRef.current)
|
||||
typeIntervalRef.current = null
|
||||
}
|
||||
}, TYPE_INTERVAL_MS)
|
||||
}, editorDelay + TYPE_START_BUFFER_MS)
|
||||
|
||||
return () => {
|
||||
clearTimeout(switchTimer)
|
||||
clearTimeout(typeTimer)
|
||||
if (typeIntervalRef.current) {
|
||||
clearInterval(typeIntervalRef.current)
|
||||
typeIntervalRef.current = null
|
||||
}
|
||||
}
|
||||
}, [animationKey, onHighlightBlock])
|
||||
|
||||
const isEmpty = inputValue.trim().length === 0
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
if (isEmpty) return
|
||||
landingSubmit(inputValue)
|
||||
}, [isEmpty, inputValue, landingSubmit])
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
handleSubmit()
|
||||
}
|
||||
},
|
||||
[handleSubmit]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className='flex h-full w-[280px] flex-shrink-0 flex-col bg-[#1e1e1e]'>
|
||||
<div className='flex h-full flex-col border-[#2c2c2c] border-l pt-3.5'>
|
||||
{/* Header */}
|
||||
<div className='flex flex-shrink-0 items-center justify-between px-2'>
|
||||
<div className='pointer-events-none flex gap-1.5'>
|
||||
<div className='flex h-[30px] w-[30px] items-center justify-center rounded-[5px] border border-[#3d3d3d] bg-[#363636]'>
|
||||
<MoreHorizontal className='h-[14px] w-[14px] text-[#e6e6e6]' />
|
||||
</div>
|
||||
<div className='flex h-[30px] w-[30px] items-center justify-center rounded-[5px] border border-[#3d3d3d] bg-[#363636]'>
|
||||
<BubbleChatPreview className='h-[14px] w-[14px] text-[#e6e6e6]' />
|
||||
</div>
|
||||
</div>
|
||||
<Link
|
||||
href='/signup'
|
||||
className='flex gap-1.5'
|
||||
onMouseMove={(e) => setCursorPos({ x: e.clientX, y: e.clientY })}
|
||||
onMouseLeave={() => setCursorPos(null)}
|
||||
>
|
||||
<div className='flex h-[30px] items-center rounded-[5px] bg-[#33C482] px-2.5 transition-colors hover:bg-[#2DAC72]'>
|
||||
<span className='font-medium text-[#1b1b1b] text-[12px]'>Deploy</span>
|
||||
</div>
|
||||
<div className='flex h-[30px] items-center gap-2 rounded-[5px] bg-[#33C482] px-2.5 transition-colors hover:bg-[#2DAC72]'>
|
||||
<Play className='h-[11.5px] w-[11.5px] text-[#1b1b1b]' />
|
||||
<span className='font-medium text-[#1b1b1b] text-[12px]'>Run</span>
|
||||
</div>
|
||||
</Link>
|
||||
{cursorPos &&
|
||||
createPortal(
|
||||
<div
|
||||
className='pointer-events-none fixed z-[9999]'
|
||||
style={{ left: cursorPos.x + 14, top: cursorPos.y + 14 }}
|
||||
>
|
||||
<div className='flex h-[4px]'>
|
||||
<div className='h-full w-[8px] bg-[#2ABBF8]' />
|
||||
<div className='h-full w-[14px] bg-[#2ABBF8] opacity-60' />
|
||||
<div className='h-full w-[8px] bg-[#00F701]' />
|
||||
<div className='h-full w-[16px] bg-[#00F701] opacity-60' />
|
||||
<div className='h-full w-[8px] bg-[#FFCC02]' />
|
||||
<div className='h-full w-[10px] bg-[#FFCC02] opacity-60' />
|
||||
<div className='h-full w-[8px] bg-[#FA4EDF]' />
|
||||
<div className='h-full w-[14px] bg-[#FA4EDF] opacity-60' />
|
||||
</div>
|
||||
<div className='flex items-center gap-[5px] bg-white px-1.5 py-1 font-medium text-[#1C1C1C] text-[11px]'>
|
||||
Get started
|
||||
<ChevronDown className='-rotate-90 h-[7px] w-[7px] text-[#1C1C1C]' />
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tabs with sliding active indicator */}
|
||||
<div className='flex flex-shrink-0 items-center px-2 pt-3.5'>
|
||||
<div className='flex gap-1'>
|
||||
{TABS_WITH_TOOLBAR.map((tab) => {
|
||||
if (tab.disabled) {
|
||||
return (
|
||||
<div
|
||||
key={tab.id}
|
||||
className='pointer-events-none flex h-[28px] items-center rounded-md border border-transparent px-2 py-[5px]'
|
||||
>
|
||||
<span className='font-medium text-[#787878] text-[12.5px]'>{tab.label}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const isActive = activeTab === tab.id
|
||||
return (
|
||||
<button
|
||||
key={tab.id}
|
||||
type='button'
|
||||
onClick={() => handleTabSwitch(tab.id as PanelTab)}
|
||||
className='relative flex h-[28px] items-center rounded-md border border-transparent px-2 py-[5px] font-medium text-[12.5px] transition-colors hover:border-[#3d3d3d] hover:bg-[#363636] hover:text-[#e6e6e6]'
|
||||
style={{ color: isActive ? '#e6e6e6' : '#787878' }}
|
||||
>
|
||||
{isActive && (
|
||||
<motion.div
|
||||
layoutId='panel-tab-indicator'
|
||||
className='absolute inset-0 rounded-md border border-[#3d3d3d] bg-[#363636]'
|
||||
transition={{ type: 'spring', stiffness: 400, damping: 30 }}
|
||||
/>
|
||||
)}
|
||||
<span className='relative z-10'>{tab.label}</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab content with cross-fade */}
|
||||
<div className='flex flex-1 flex-col overflow-hidden pt-3'>
|
||||
<AnimatePresence mode='wait'>
|
||||
{activeTab === 'copilot' && (
|
||||
<motion.div
|
||||
key='copilot'
|
||||
className='flex h-full flex-col'
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.15, ease: EASE_OUT }}
|
||||
>
|
||||
<div className='pointer-events-none mx-[-1px] flex flex-shrink-0 items-center justify-between gap-2 border border-[#2c2c2c] bg-[#292929] px-3 py-1.5'>
|
||||
<span className='min-w-0 flex-1 truncate font-medium text-[#e6e6e6] text-[14px]'>
|
||||
New Chat
|
||||
</span>
|
||||
</div>
|
||||
<div className='px-2 pt-3 pb-2'>
|
||||
<div className='rounded-[4px] border border-[#3d3d3d] bg-[#292929] px-1.5 py-1.5'>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder='Build an AI agent...'
|
||||
rows={2}
|
||||
className='mb-1.5 min-h-[48px] w-full cursor-text resize-none border-0 bg-transparent px-0.5 py-1 font-base text-[#e6e6e6] text-sm leading-[1.25rem] placeholder-[#787878] caret-[#e6e6e6] outline-none'
|
||||
/>
|
||||
<div className='flex items-center justify-end'>
|
||||
<button
|
||||
type='button'
|
||||
onClick={handleSubmit}
|
||||
disabled={isEmpty}
|
||||
className='flex h-[22px] w-[22px] items-center justify-center rounded-full border-0 p-0 transition-colors'
|
||||
style={{
|
||||
background: isEmpty ? '#808080' : '#e0e0e0',
|
||||
cursor: isEmpty ? 'not-allowed' : 'pointer',
|
||||
}}
|
||||
>
|
||||
<ArrowUp size={14} strokeWidth={2.25} color='#1b1b1b' />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{activeTab === 'editor' && (
|
||||
<motion.div
|
||||
key='editor'
|
||||
className='flex h-full flex-col'
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.15, ease: EASE_OUT }}
|
||||
>
|
||||
<EditorTabContent editorPrompt={editorPrompt} typedLength={typedLength} />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
const TOOL_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {
|
||||
hubspot: HubspotIcon,
|
||||
salesforce: SalesforceIcon,
|
||||
}
|
||||
|
||||
const MODEL_ICON_MAP: Record<string, React.ComponentType<{ className?: string }>> = {
|
||||
'gpt-': OpenAIIcon,
|
||||
}
|
||||
|
||||
function getModelIcon(model: string) {
|
||||
const lower = model.toLowerCase()
|
||||
for (const [prefix, icon] of Object.entries(MODEL_ICON_MAP)) {
|
||||
if (lower.startsWith(prefix)) return icon
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
interface EditorTabContentProps {
|
||||
editorPrompt: EditorPromptData | null
|
||||
typedLength: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Editor tab replicating the real agent editor layout:
|
||||
* header bar, then scrollable sub-block fields.
|
||||
*/
|
||||
function EditorTabContent({ editorPrompt, typedLength }: EditorTabContentProps) {
|
||||
if (!editorPrompt) {
|
||||
return (
|
||||
<div className='flex flex-1 items-center justify-center'>
|
||||
<span className='font-medium text-[#787878] text-[13px]'>Select a block to edit</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const { blockName, blockType, bgColor, prompt, model, tools } = editorPrompt
|
||||
const visibleText = prompt.slice(0, typedLength)
|
||||
const isTyping = typedLength < prompt.length
|
||||
const BlockIcon = EDITOR_BLOCK_ICONS[blockType]
|
||||
const ModelIcon = model ? getModelIcon(model) : null
|
||||
|
||||
return (
|
||||
<div className='flex h-full flex-col'>
|
||||
{/* Editor header */}
|
||||
<div className='mx-[-1px] flex flex-shrink-0 items-center gap-2 border border-[#2c2c2c] bg-[#292929] px-3 py-1.5'>
|
||||
{BlockIcon && (
|
||||
<div
|
||||
className='flex h-[18px] w-[18px] flex-shrink-0 items-center justify-center rounded-sm'
|
||||
style={{ background: bgColor }}
|
||||
>
|
||||
<BlockIcon className='h-[12px] w-[12px] text-white' />
|
||||
</div>
|
||||
)}
|
||||
<span className='min-w-0 flex-1 truncate font-medium text-[#e6e6e6] text-sm'>
|
||||
{blockName}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Sub-block fields */}
|
||||
<div className='flex-1 overflow-y-auto overflow-x-hidden px-2 pt-3 pb-2'>
|
||||
<div className='flex flex-col gap-4'>
|
||||
{/* System Prompt */}
|
||||
<div className='flex flex-col gap-2.5'>
|
||||
<div className='flex items-center pl-0.5'>
|
||||
<span className='font-medium text-[#e6e6e6] text-small'>System Prompt</span>
|
||||
</div>
|
||||
<div className='rounded-[4px] border border-[#3d3d3d] bg-[#292929] px-2 py-2'>
|
||||
<p className='min-h-[48px] whitespace-pre-wrap break-words font-medium font-sans text-[#e6e6e6] text-sm leading-[1.5]'>
|
||||
{visibleText}
|
||||
{isTyping && (
|
||||
<motion.span
|
||||
className='inline-block h-[14px] w-[1.5px] translate-y-[2px] bg-[#e6e6e6]'
|
||||
animate={{ opacity: [1, 0] }}
|
||||
transition={{
|
||||
duration: 0.6,
|
||||
repeat: Number.POSITIVE_INFINITY,
|
||||
repeatType: 'reverse',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Model */}
|
||||
{model && (
|
||||
<div className='flex flex-col gap-2.5'>
|
||||
<div className='flex items-center pl-0.5'>
|
||||
<span className='font-medium text-[#e6e6e6] text-small'>Model</span>
|
||||
</div>
|
||||
<div className='flex h-[32px] items-center gap-2 rounded-[4px] border border-[#3d3d3d] bg-[#292929] px-2'>
|
||||
{ModelIcon && <ModelIcon className='h-[14px] w-[14px] text-[#e6e6e6]' />}
|
||||
<span className='flex-1 truncate font-medium text-[#e6e6e6] text-sm'>{model}</span>
|
||||
<ChevronDown className='h-[7px] w-[9px] text-[#636363]' />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tools */}
|
||||
{tools.length > 0 && (
|
||||
<div className='flex flex-col gap-2.5'>
|
||||
<div className='flex items-center pl-0.5'>
|
||||
<span className='font-medium text-[#e6e6e6] text-small'>Tools</span>
|
||||
</div>
|
||||
<div className='flex flex-wrap gap-[5px]'>
|
||||
{tools.map((tool) => {
|
||||
const ToolIcon = TOOL_ICONS[tool.type]
|
||||
return (
|
||||
<div
|
||||
key={tool.type}
|
||||
className='flex items-center gap-[5px] rounded-[5px] border border-[#3d3d3d] bg-[#2a2a2a] px-[6px] py-[3px]'
|
||||
>
|
||||
{ToolIcon && (
|
||||
<div
|
||||
className='flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center rounded-[4px]'
|
||||
style={{ background: tool.bgColor }}
|
||||
>
|
||||
<ToolIcon className='h-[10px] w-[10px] text-white' />
|
||||
</div>
|
||||
)}
|
||||
<span className='font-normal text-[#e6e6e6] text-[12px]'>{tool.name}</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Temperature */}
|
||||
<div className='flex flex-col gap-2.5'>
|
||||
<div className='flex items-center justify-between pl-0.5'>
|
||||
<span className='font-medium text-[#e6e6e6] text-small'>Temperature</span>
|
||||
<span className='font-medium text-[#787878] text-small'>0.7</span>
|
||||
</div>
|
||||
<div className='relative h-[6px] rounded-full bg-[#3d3d3d]'>
|
||||
<div className='h-full w-[70%] rounded-full bg-[#e6e6e6]' />
|
||||
<div
|
||||
className='-translate-y-1/2 absolute top-1/2 h-[14px] w-[14px] rounded-full border-[#e6e6e6] border-[2px] bg-[#292929]'
|
||||
style={{ left: 'calc(70% - 7px)' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Response Format */}
|
||||
<div className='flex flex-col gap-2.5'>
|
||||
<div className='flex items-center pl-0.5'>
|
||||
<span className='font-medium text-[#e6e6e6] text-small'>Response Format</span>
|
||||
</div>
|
||||
<div className='rounded-[4px] border border-[#3d3d3d] bg-[#292929] px-2 py-2'>
|
||||
<span className='font-mono text-[#787878] text-[12px]'>plain text</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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/(landing)/components/landing-preview/components/landing-preview-resource/landing-preview-resource'
|
||||
import { LandingPreviewResource } from '@/app/(landing)/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 type { PreviewWorkflow } from '@/app/(home)/components/landing-preview/components/landing-preview-workflow/workflow-data'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import type { PreviewWorkflow } from '@/app/(landing)/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,584 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
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/(landing)/components/landing-preview/components/landing-preview-resource/landing-preview-resource'
|
||||
import {
|
||||
LandingPreviewResource,
|
||||
ownerCell,
|
||||
} from '@/app/(landing)/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>
|
||||
)
|
||||
}
|
||||
|
||||
interface LandingPreviewTablesProps {
|
||||
autoOpenTableId?: string | null
|
||||
}
|
||||
|
||||
const tableViewTransition = {
|
||||
initial: { opacity: 0, x: 20 },
|
||||
animate: { opacity: 1, x: 0 },
|
||||
exit: { opacity: 0, x: -20 },
|
||||
transition: { duration: 0.25, ease: [0.16, 1, 0.3, 1] as const },
|
||||
} as const
|
||||
|
||||
export function LandingPreviewTables({ autoOpenTableId }: LandingPreviewTablesProps = {}) {
|
||||
const [openTableId, setOpenTableId] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoOpenTableId) return
|
||||
const timer = setTimeout(() => {
|
||||
setOpenTableId(autoOpenTableId)
|
||||
}, 800)
|
||||
return () => clearTimeout(timer)
|
||||
}, [autoOpenTableId])
|
||||
|
||||
return (
|
||||
<AnimatePresence mode='wait'>
|
||||
{openTableId !== null ? (
|
||||
<motion.div
|
||||
key={`spreadsheet-${openTableId}`}
|
||||
className='flex h-full flex-1 flex-col'
|
||||
{...tableViewTransition}
|
||||
>
|
||||
<SpreadsheetView
|
||||
tableId={openTableId}
|
||||
tableName={TABLE_METAS[openTableId] ?? 'Table'}
|
||||
onBack={() => setOpenTableId(null)}
|
||||
/>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key='table-list'
|
||||
className='flex h-full flex-1 flex-col'
|
||||
{...tableViewTransition}
|
||||
>
|
||||
<LandingPreviewResource
|
||||
icon={Table}
|
||||
title='Tables'
|
||||
createLabel='New table'
|
||||
searchPlaceholder='Search tables...'
|
||||
columns={LIST_COLUMNS}
|
||||
rows={LIST_ROWS}
|
||||
onRowClick={(id) => setOpenTableId(id)}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import ReactFlow, {
|
||||
applyEdgeChanges,
|
||||
@@ -16,22 +16,24 @@ import ReactFlow, {
|
||||
ReactFlowProvider,
|
||||
} from 'reactflow'
|
||||
import 'reactflow/dist/style.css'
|
||||
import { PreviewBlockNode } from '@/app/(home)/components/landing-preview/components/landing-preview-workflow/preview-block-node'
|
||||
import { PreviewBlockNode } from '@/app/(landing)/components/landing-preview/components/landing-preview-workflow/preview-block-node'
|
||||
import {
|
||||
EASE_OUT,
|
||||
type PreviewWorkflow,
|
||||
toReactFlowElements,
|
||||
} from '@/app/(home)/components/landing-preview/components/landing-preview-workflow/workflow-data'
|
||||
} from '@/app/(landing)/components/landing-preview/components/landing-preview-workflow/workflow-data'
|
||||
|
||||
interface FitViewOptions {
|
||||
padding?: number
|
||||
maxZoom?: number
|
||||
minZoom?: number
|
||||
}
|
||||
|
||||
interface LandingPreviewWorkflowProps {
|
||||
workflow: PreviewWorkflow
|
||||
animate?: boolean
|
||||
fitViewOptions?: FitViewOptions
|
||||
highlightedBlockId?: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,21 +90,35 @@ function PreviewEdge({
|
||||
const NODE_TYPES: NodeTypes = { previewBlock: PreviewBlockNode }
|
||||
const EDGE_TYPES: EdgeTypes = { previewEdge: PreviewEdge }
|
||||
const PRO_OPTIONS = { hideAttribution: true }
|
||||
const DEFAULT_FIT_VIEW_OPTIONS = { padding: 0.3, maxZoom: 1 } as const
|
||||
const DEFAULT_FIT_VIEW_OPTIONS = { padding: 0.5, maxZoom: 1 } as const
|
||||
|
||||
/**
|
||||
* Inner flow component. Keyed on workflow ID by the parent so it remounts
|
||||
* cleanly on workflow switch — fitView fires on mount with zero delay.
|
||||
*/
|
||||
function PreviewFlow({ workflow, animate = false, fitViewOptions }: LandingPreviewWorkflowProps) {
|
||||
function PreviewFlow({
|
||||
workflow,
|
||||
animate = false,
|
||||
fitViewOptions,
|
||||
highlightedBlockId,
|
||||
}: LandingPreviewWorkflowProps) {
|
||||
const { nodes: initialNodes, edges: initialEdges } = useMemo(
|
||||
() => toReactFlowElements(workflow, animate),
|
||||
[workflow, animate]
|
||||
() => toReactFlowElements(workflow, animate, highlightedBlockId),
|
||||
[workflow, animate, highlightedBlockId]
|
||||
)
|
||||
|
||||
const [nodes, setNodes] = useState<Node[]>(initialNodes)
|
||||
const [edges, setEdges] = useState<Edge[]>(initialEdges)
|
||||
|
||||
useEffect(() => {
|
||||
setNodes((prev) =>
|
||||
prev.map((node) => ({
|
||||
...node,
|
||||
data: { ...node.data, isHighlighted: highlightedBlockId === node.id },
|
||||
}))
|
||||
)
|
||||
}, [highlightedBlockId])
|
||||
|
||||
const onNodesChange: OnNodesChange = useCallback(
|
||||
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
|
||||
[]
|
||||
@@ -114,6 +130,7 @@ function PreviewFlow({ workflow, animate = false, fitViewOptions }: LandingPrevi
|
||||
)
|
||||
|
||||
const resolvedFitViewOptions = fitViewOptions ?? DEFAULT_FIT_VIEW_OPTIONS
|
||||
const minZoom = fitViewOptions?.minZoom ?? 0.5
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
@@ -135,6 +152,7 @@ function PreviewFlow({ workflow, animate = false, fitViewOptions }: LandingPrevi
|
||||
preventScrolling={false}
|
||||
autoPanOnNodeDrag={false}
|
||||
proOptions={PRO_OPTIONS}
|
||||
minZoom={minZoom}
|
||||
fitView
|
||||
fitViewOptions={resolvedFitViewOptions}
|
||||
className='h-full w-full bg-[var(--landing-bg)]'
|
||||
@@ -151,11 +169,17 @@ export function LandingPreviewWorkflow({
|
||||
workflow,
|
||||
animate = false,
|
||||
fitViewOptions,
|
||||
highlightedBlockId,
|
||||
}: LandingPreviewWorkflowProps) {
|
||||
return (
|
||||
<div className='h-full w-full'>
|
||||
<ReactFlowProvider key={workflow.id}>
|
||||
<PreviewFlow workflow={workflow} animate={animate} fitViewOptions={fitViewOptions} />
|
||||
<PreviewFlow
|
||||
workflow={workflow}
|
||||
animate={animate}
|
||||
fitViewOptions={fitViewOptions}
|
||||
highlightedBlockId={highlightedBlockId}
|
||||
/>
|
||||
</ReactFlowProvider>
|
||||
</div>
|
||||
)
|
||||
@@ -39,7 +39,7 @@ import {
|
||||
BLOCK_STAGGER,
|
||||
EASE_OUT,
|
||||
type PreviewTool,
|
||||
} from '@/app/(home)/components/landing-preview/components/landing-preview-workflow/workflow-data'
|
||||
} from '@/app/(landing)/components/landing-preview/components/landing-preview-workflow/workflow-data'
|
||||
|
||||
/** Map block type strings to their icon components. */
|
||||
const BLOCK_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {
|
||||
@@ -105,6 +105,7 @@ interface PreviewBlockData {
|
||||
hideSourceHandle?: boolean
|
||||
index?: number
|
||||
animate?: boolean
|
||||
isHighlighted?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -137,6 +138,7 @@ export const PreviewBlockNode = memo(function PreviewBlockNode({
|
||||
hideSourceHandle,
|
||||
index = 0,
|
||||
animate = false,
|
||||
isHighlighted = false,
|
||||
} = data
|
||||
const Icon = BLOCK_ICONS[blockType]
|
||||
const delay = animate ? index * BLOCK_STAGGER : 0
|
||||
@@ -228,13 +230,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]'
|
||||
@@ -264,6 +266,10 @@ export const PreviewBlockNode = memo(function PreviewBlockNode({
|
||||
isConnectableEnd={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isHighlighted && (
|
||||
<div className='pointer-events-none absolute inset-0 z-40 rounded-lg ring-[#33b4ff] ring-[1.75px]' />
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
@@ -66,7 +66,11 @@ const IT_SERVICE_WORKFLOW: PreviewWorkflow = {
|
||||
bgColor: '#701ffc',
|
||||
rows: [
|
||||
{ title: 'Model', value: 'claude-sonnet-4.6' },
|
||||
{ title: 'System Prompt', value: 'Triage incoming IT...' },
|
||||
{
|
||||
title: 'System Prompt',
|
||||
value:
|
||||
'Triage incoming IT support requests from Slack, categorize by severity, and create Jira tickets for the appropriate team.',
|
||||
},
|
||||
],
|
||||
tools: [{ name: 'Knowledge Base', type: 'knowledge_base', bgColor: '#10B981' }],
|
||||
position: { x: 420, y: 40 },
|
||||
@@ -91,7 +95,7 @@ const IT_SERVICE_WORKFLOW: PreviewWorkflow = {
|
||||
}
|
||||
|
||||
/**
|
||||
* Self-healing CRM workflow — Schedule -> Mothership
|
||||
* Self-healing CRM workflow — Schedule -> Agent
|
||||
*/
|
||||
const SELF_HEALING_CRM_WORKFLOW: PreviewWorkflow = {
|
||||
id: 'wf-self-healing-crm',
|
||||
@@ -111,20 +115,85 @@ const SELF_HEALING_CRM_WORKFLOW: PreviewWorkflow = {
|
||||
hideTargetHandle: true,
|
||||
},
|
||||
{
|
||||
id: 'mothership-1',
|
||||
id: 'agent-crm',
|
||||
name: 'CRM Agent',
|
||||
type: 'mothership',
|
||||
bgColor: '#33C482',
|
||||
rows: [{ title: 'Prompt', value: 'Audit CRM records, fix...' }],
|
||||
type: 'agent',
|
||||
bgColor: '#701ffc',
|
||||
rows: [
|
||||
{ title: 'Model', value: 'gpt-5.4' },
|
||||
{
|
||||
title: 'System Prompt',
|
||||
value:
|
||||
'Audit CRM records, identify data inconsistencies, and fix duplicate contacts, missing fields, and stale pipeline entries across HubSpot and Salesforce.',
|
||||
},
|
||||
],
|
||||
tools: [
|
||||
{ name: 'HubSpot', type: 'hubspot', bgColor: '#FF7A59' },
|
||||
{ name: 'Salesforce', type: 'salesforce', bgColor: '#E0E0E0' },
|
||||
],
|
||||
position: { x: 420, y: 180 },
|
||||
position: { x: 420, y: 140 },
|
||||
hideSourceHandle: true,
|
||||
},
|
||||
],
|
||||
edges: [{ id: 'e-3', source: 'schedule-1', target: 'mothership-1' }],
|
||||
edges: [{ id: 'e-3', source: 'schedule-1', target: 'agent-crm' }],
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 support issues using the knowledge base, draft a response, and notify the team in Slack.',
|
||||
},
|
||||
],
|
||||
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' },
|
||||
],
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -153,6 +222,7 @@ const NEW_AGENT_WORKFLOW: PreviewWorkflow = {
|
||||
export const PREVIEW_WORKFLOWS: PreviewWorkflow[] = [
|
||||
SELF_HEALING_CRM_WORKFLOW,
|
||||
IT_SERVICE_WORKFLOW,
|
||||
CUSTOMER_SUPPORT_WORKFLOW,
|
||||
NEW_AGENT_WORKFLOW,
|
||||
]
|
||||
|
||||
@@ -173,7 +243,8 @@ const EDGE_STYLE = { stroke: '#454545', strokeWidth: 1.5 } as const
|
||||
*/
|
||||
export function toReactFlowElements(
|
||||
workflow: PreviewWorkflow,
|
||||
animate = false
|
||||
animate = false,
|
||||
highlightedBlockId?: string | null
|
||||
): {
|
||||
nodes: Node[]
|
||||
edges: Edge[]
|
||||
@@ -195,6 +266,7 @@ export function toReactFlowElements(
|
||||
hideSourceHandle: block.hideSourceHandle,
|
||||
index,
|
||||
animate,
|
||||
isHighlighted: highlightedBlockId === block.id,
|
||||
},
|
||||
draggable: true,
|
||||
selectable: false,
|
||||
@@ -223,3 +295,74 @@ export function toReactFlowElements(
|
||||
|
||||
return { nodes, edges }
|
||||
}
|
||||
|
||||
/** Block types that carry an editable prompt suitable for the Editor tab. */
|
||||
const AGENT_BLOCK_TYPES = new Set(['agent', 'mothership'])
|
||||
|
||||
export interface EditorPromptData {
|
||||
blockId: string
|
||||
blockName: string
|
||||
blockType: string
|
||||
bgColor: string
|
||||
prompt: string
|
||||
model: string | null
|
||||
tools: PreviewTool[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the editor-facing prompt from the first agent/mothership block.
|
||||
*
|
||||
* @returns Block metadata + prompt + model + tools, or `null` when the workflow has no agent.
|
||||
*/
|
||||
export function getEditorPrompt(workflow: PreviewWorkflow): EditorPromptData | null {
|
||||
for (const block of workflow.blocks) {
|
||||
if (!AGENT_BLOCK_TYPES.has(block.type)) continue
|
||||
const promptRow = block.rows.find((r) => r.title === 'Prompt' || r.title === 'System Prompt')
|
||||
if (promptRow) {
|
||||
const modelRow = block.rows.find((r) => r.title === 'Model')
|
||||
return {
|
||||
blockId: block.id,
|
||||
blockName: block.name,
|
||||
blockType: block.type,
|
||||
bgColor: block.bgColor,
|
||||
prompt: promptRow.value,
|
||||
model: modelRow?.value ?? null,
|
||||
tools: block.tools ?? [],
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the delay (ms) before the Editor tab should activate.
|
||||
* Accounts for all block staggers + edge draw durations + a small buffer.
|
||||
*/
|
||||
export function getWorkflowAnimationTiming(workflow: PreviewWorkflow): { editorDelay: number } {
|
||||
const maxBlockIndex = Math.max(0, workflow.blocks.length - 1)
|
||||
const hasEdges = workflow.edges.length > 0
|
||||
const edgeDuration = hasEdges ? 0.4 : 0
|
||||
const buffer = 0.15
|
||||
const total = maxBlockIndex * BLOCK_STAGGER + BLOCK_STAGGER + edgeDuration + buffer
|
||||
return { editorDelay: Math.round(total * 1000) }
|
||||
}
|
||||
|
||||
/** Milliseconds between each character typed in the Editor prompt animation. */
|
||||
export const TYPE_INTERVAL_MS = 30
|
||||
|
||||
/** Extra pause (ms) after switching to the Editor tab before typing begins. */
|
||||
export const TYPE_START_BUFFER_MS = 150
|
||||
|
||||
/** How long to dwell on a completed step before advancing (ms). */
|
||||
export const STEP_DWELL_MS = 2500
|
||||
|
||||
/**
|
||||
* Computes the total time (ms) a workflow step occupies, including
|
||||
* canvas animation, editor typing, and a dwell period.
|
||||
*/
|
||||
export function getWorkflowStepDuration(workflow: PreviewWorkflow): number {
|
||||
const { editorDelay } = getWorkflowAnimationTiming(workflow)
|
||||
const prompt = getEditorPrompt(workflow)
|
||||
const typingTime = prompt ? prompt.prompt.length * TYPE_INTERVAL_MS : 0
|
||||
return editorDelay + TYPE_START_BUFFER_MS + typingTime + STEP_DWELL_MS
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { AnimatePresence, motion, type Variants } from 'framer-motion'
|
||||
import { LandingPreviewFiles } from '@/app/(landing)/components/landing-preview/components/landing-preview-files/landing-preview-files'
|
||||
import { LandingPreviewHome } from '@/app/(landing)/components/landing-preview/components/landing-preview-home/landing-preview-home'
|
||||
import { LandingPreviewKnowledge } from '@/app/(landing)/components/landing-preview/components/landing-preview-knowledge/landing-preview-knowledge'
|
||||
import { LandingPreviewLogs } from '@/app/(landing)/components/landing-preview/components/landing-preview-logs/landing-preview-logs'
|
||||
import { LandingPreviewPanel } from '@/app/(landing)/components/landing-preview/components/landing-preview-panel/landing-preview-panel'
|
||||
import { LandingPreviewScheduledTasks } from '@/app/(landing)/components/landing-preview/components/landing-preview-scheduled-tasks/landing-preview-scheduled-tasks'
|
||||
import type { SidebarView } from '@/app/(landing)/components/landing-preview/components/landing-preview-sidebar/landing-preview-sidebar'
|
||||
import { LandingPreviewSidebar } from '@/app/(landing)/components/landing-preview/components/landing-preview-sidebar/landing-preview-sidebar'
|
||||
import { LandingPreviewTables } from '@/app/(landing)/components/landing-preview/components/landing-preview-tables/landing-preview-tables'
|
||||
import { LandingPreviewWorkflow } from '@/app/(landing)/components/landing-preview/components/landing-preview-workflow/landing-preview-workflow'
|
||||
import {
|
||||
EASE_OUT,
|
||||
getWorkflowStepDuration,
|
||||
PREVIEW_WORKFLOWS,
|
||||
} from '@/app/(landing)/components/landing-preview/components/landing-preview-workflow/workflow-data'
|
||||
|
||||
const containerVariants: Variants = {
|
||||
hidden: {},
|
||||
visible: {
|
||||
transition: { staggerChildren: 0.15 },
|
||||
},
|
||||
}
|
||||
|
||||
const sidebarVariants: Variants = {
|
||||
hidden: { opacity: 0, x: -12 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
transition: {
|
||||
x: { duration: 0.25, ease: EASE_OUT },
|
||||
opacity: { duration: 0.25, ease: EASE_OUT },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const panelVariants: Variants = {
|
||||
hidden: { opacity: 0, x: 12 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
transition: {
|
||||
x: { duration: 0.25, ease: EASE_OUT },
|
||||
opacity: { duration: 0.25, ease: EASE_OUT },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const viewTransition = {
|
||||
initial: { opacity: 0 },
|
||||
animate: { opacity: 1 },
|
||||
exit: { opacity: 0 },
|
||||
transition: { duration: 0.2, ease: EASE_OUT },
|
||||
} as const
|
||||
|
||||
interface DemoStep {
|
||||
type: 'workflow' | 'tables' | 'home' | 'logs'
|
||||
workflowId?: string
|
||||
tableId?: string
|
||||
duration: number
|
||||
}
|
||||
|
||||
const WORKFLOW_MAP = new Map(PREVIEW_WORKFLOWS.map((w) => [w.id, w]))
|
||||
|
||||
const HOME_STEP_MS = 12000
|
||||
const LOGS_STEP_MS = 5000
|
||||
|
||||
/** Full desktop sequence: CRM -> home -> logs -> ITSM -> support -> repeat */
|
||||
const DESKTOP_STEPS: DemoStep[] = [
|
||||
{
|
||||
type: 'workflow',
|
||||
workflowId: 'wf-self-healing-crm',
|
||||
duration: getWorkflowStepDuration(WORKFLOW_MAP.get('wf-self-healing-crm')!),
|
||||
},
|
||||
{ type: 'home', duration: HOME_STEP_MS },
|
||||
{ type: 'logs', duration: LOGS_STEP_MS },
|
||||
{
|
||||
type: 'workflow',
|
||||
workflowId: 'wf-it-service',
|
||||
duration: getWorkflowStepDuration(WORKFLOW_MAP.get('wf-it-service')!),
|
||||
},
|
||||
{
|
||||
type: 'workflow',
|
||||
workflowId: 'wf-customer-support',
|
||||
duration: getWorkflowStepDuration(WORKFLOW_MAP.get('wf-customer-support')!),
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* Interactive workspace preview for the hero section.
|
||||
*
|
||||
* Desktop: auto-cycles CRM -> home -> logs -> ITSM -> support -> repeat.
|
||||
* Mobile: static workflow canvas (no animation, no cycling).
|
||||
* User interaction permanently stops the auto-cycle.
|
||||
*/
|
||||
export function LandingPreview() {
|
||||
const [activeView, setActiveView] = useState<SidebarView>('workflow')
|
||||
const [activeWorkflowId, setActiveWorkflowId] = useState(PREVIEW_WORKFLOWS[0].id)
|
||||
const animationKeyRef = useRef(0)
|
||||
const [animationKey, setAnimationKey] = useState(0)
|
||||
const [highlightedBlockId, setHighlightedBlockId] = useState<string | null>(null)
|
||||
const [autoTableId, setAutoTableId] = useState<string | null>(null)
|
||||
const [autoTypeHome, setAutoTypeHome] = useState(false)
|
||||
const [isDesktop, setIsDesktop] = useState(true)
|
||||
|
||||
const demoIndexRef = useRef(0)
|
||||
const demoTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const autoCycleActiveRef = useRef(true)
|
||||
const isDesktopRef = useRef(true)
|
||||
|
||||
const clearDemoTimer = useCallback(() => {
|
||||
if (demoTimerRef.current) {
|
||||
clearTimeout(demoTimerRef.current)
|
||||
demoTimerRef.current = null
|
||||
}
|
||||
}, [])
|
||||
|
||||
const applyDemoStep = useCallback((step: DemoStep) => {
|
||||
setAutoTableId(null)
|
||||
setAutoTypeHome(false)
|
||||
|
||||
if (step.type === 'workflow' && step.workflowId) {
|
||||
setActiveWorkflowId(step.workflowId)
|
||||
setActiveView('workflow')
|
||||
animationKeyRef.current += 1
|
||||
setAnimationKey(animationKeyRef.current)
|
||||
} else if (step.type === 'tables') {
|
||||
setActiveView('tables')
|
||||
setAutoTableId(step.tableId ?? null)
|
||||
} else if (step.type === 'home') {
|
||||
setActiveView('home')
|
||||
setAutoTypeHome(true)
|
||||
} else if (step.type === 'logs') {
|
||||
setActiveView('logs')
|
||||
}
|
||||
}, [])
|
||||
|
||||
const scheduleNextStep = useCallback(() => {
|
||||
if (!autoCycleActiveRef.current) return
|
||||
const steps = DESKTOP_STEPS
|
||||
const currentStep = steps[demoIndexRef.current]
|
||||
demoTimerRef.current = setTimeout(() => {
|
||||
if (!autoCycleActiveRef.current) return
|
||||
demoIndexRef.current = (demoIndexRef.current + 1) % steps.length
|
||||
applyDemoStep(steps[demoIndexRef.current])
|
||||
scheduleNextStep()
|
||||
}, currentStep.duration)
|
||||
}, [applyDemoStep])
|
||||
|
||||
useEffect(() => {
|
||||
const desktop = window.matchMedia('(min-width: 1024px)').matches
|
||||
isDesktopRef.current = desktop
|
||||
setIsDesktop(desktop)
|
||||
if (!desktop) return
|
||||
applyDemoStep(DESKTOP_STEPS[0])
|
||||
scheduleNextStep()
|
||||
return clearDemoTimer
|
||||
}, [applyDemoStep, scheduleNextStep, clearDemoTimer])
|
||||
|
||||
const stopAutoCycle = useCallback(() => {
|
||||
autoCycleActiveRef.current = false
|
||||
clearDemoTimer()
|
||||
}, [clearDemoTimer])
|
||||
|
||||
const handleSelectWorkflow = useCallback(
|
||||
(id: string) => {
|
||||
stopAutoCycle()
|
||||
setAutoTableId(null)
|
||||
setAutoTypeHome(false)
|
||||
setHighlightedBlockId(null)
|
||||
setActiveWorkflowId(id)
|
||||
setActiveView('workflow')
|
||||
animationKeyRef.current += 1
|
||||
setAnimationKey(animationKeyRef.current)
|
||||
},
|
||||
[stopAutoCycle]
|
||||
)
|
||||
|
||||
const handleSelectHome = useCallback(() => {
|
||||
stopAutoCycle()
|
||||
setAutoTableId(null)
|
||||
setAutoTypeHome(false)
|
||||
setHighlightedBlockId(null)
|
||||
setActiveView('home')
|
||||
}, [stopAutoCycle])
|
||||
|
||||
const handleSelectNav = useCallback(
|
||||
(id: SidebarView) => {
|
||||
stopAutoCycle()
|
||||
setAutoTableId(null)
|
||||
setAutoTypeHome(false)
|
||||
setHighlightedBlockId(null)
|
||||
setActiveView(id)
|
||||
},
|
||||
[stopAutoCycle]
|
||||
)
|
||||
|
||||
const handleHighlightBlock = useCallback((blockId: string | null) => {
|
||||
setHighlightedBlockId(blockId)
|
||||
}, [])
|
||||
|
||||
const activeWorkflow =
|
||||
PREVIEW_WORKFLOWS.find((w) => w.id === activeWorkflowId) ?? PREVIEW_WORKFLOWS[0]
|
||||
|
||||
const isWorkflowView = activeView === 'workflow'
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className='dark flex aspect-[1116/615] w-full overflow-hidden rounded bg-[var(--landing-bg-surface)] antialiased'
|
||||
initial={isDesktop ? 'hidden' : false}
|
||||
animate='visible'
|
||||
variants={containerVariants}
|
||||
>
|
||||
<motion.div className='hidden lg:flex' variants={sidebarVariants}>
|
||||
<LandingPreviewSidebar
|
||||
workflows={PREVIEW_WORKFLOWS}
|
||||
activeWorkflowId={activeWorkflowId}
|
||||
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'>
|
||||
<div className='flex flex-1 overflow-hidden rounded-[5px] border border-[#2c2c2c] bg-[var(--landing-bg)]'>
|
||||
<div
|
||||
className={
|
||||
isWorkflowView
|
||||
? 'relative min-w-0 flex-1 overflow-hidden'
|
||||
: 'relative flex min-w-0 flex-1 flex-col overflow-hidden'
|
||||
}
|
||||
>
|
||||
{isDesktop ? (
|
||||
<AnimatePresence mode='wait'>
|
||||
{activeView === 'workflow' && (
|
||||
<motion.div
|
||||
key={`wf-${activeWorkflow.id}-${animationKey}`}
|
||||
className='h-full w-full'
|
||||
{...viewTransition}
|
||||
>
|
||||
<LandingPreviewWorkflow
|
||||
workflow={activeWorkflow}
|
||||
animate
|
||||
highlightedBlockId={highlightedBlockId}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
{activeView === 'home' && (
|
||||
<motion.div
|
||||
key={`home-${animationKey}`}
|
||||
className='flex h-full w-full flex-col'
|
||||
{...viewTransition}
|
||||
>
|
||||
<LandingPreviewHome autoType={autoTypeHome} />
|
||||
</motion.div>
|
||||
)}
|
||||
{activeView === 'tables' && (
|
||||
<motion.div
|
||||
key={`tables-${animationKey}`}
|
||||
className='flex h-full w-full flex-col'
|
||||
{...viewTransition}
|
||||
>
|
||||
<LandingPreviewTables autoOpenTableId={autoTableId} />
|
||||
</motion.div>
|
||||
)}
|
||||
{activeView === 'files' && (
|
||||
<motion.div
|
||||
key='files'
|
||||
className='flex h-full w-full flex-col'
|
||||
{...viewTransition}
|
||||
>
|
||||
<LandingPreviewFiles />
|
||||
</motion.div>
|
||||
)}
|
||||
{activeView === 'knowledge' && (
|
||||
<motion.div
|
||||
key='knowledge'
|
||||
className='flex h-full w-full flex-col'
|
||||
{...viewTransition}
|
||||
>
|
||||
<LandingPreviewKnowledge />
|
||||
</motion.div>
|
||||
)}
|
||||
{activeView === 'logs' && (
|
||||
<motion.div key='logs' className='flex h-full w-full flex-col' initial={false}>
|
||||
<LandingPreviewLogs />
|
||||
</motion.div>
|
||||
)}
|
||||
{activeView === 'scheduled-tasks' && (
|
||||
<motion.div
|
||||
key='scheduled-tasks'
|
||||
className='flex h-full w-full flex-col'
|
||||
{...viewTransition}
|
||||
>
|
||||
<LandingPreviewScheduledTasks />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
) : (
|
||||
<div className='h-full w-full'>
|
||||
<LandingPreviewWorkflow workflow={activeWorkflow} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<motion.div
|
||||
className={isWorkflowView ? 'hidden lg:flex' : 'hidden'}
|
||||
variants={panelVariants}
|
||||
>
|
||||
<LandingPreviewPanel
|
||||
activeWorkflow={activeWorkflow}
|
||||
animationKey={animationKey}
|
||||
onHighlightBlock={handleHighlightBlock}
|
||||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getNavBlogPosts } from '@/lib/blog/registry'
|
||||
import { isHosted } from '@/lib/core/config/feature-flags'
|
||||
import Footer from '@/app/(home)/components/footer/footer'
|
||||
import Navbar from '@/app/(home)/components/navbar/navbar'
|
||||
import Footer from '@/app/(landing)/components/footer/footer'
|
||||
import Navbar from '@/app/(landing)/components/navbar/navbar'
|
||||
|
||||
interface LegalLayoutProps {
|
||||
title: string
|
||||
|
||||
@@ -44,9 +44,9 @@ function BlogCard({
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
<div className='flex-shrink-0 px-2.5 py-1.5'>
|
||||
<div className='flex-shrink-0 px-2.5 py-2'>
|
||||
<span
|
||||
className='font-[430] font-season text-[var(--landing-text-body)] leading-[140%]'
|
||||
className='block truncate font-[430] font-season text-[var(--landing-text-body)] leading-[140%]'
|
||||
style={{ fontSize: titleSize }}
|
||||
>
|
||||
{title}
|
||||
@@ -66,7 +66,7 @@ export function BlogDropdown({ posts }: BlogDropdownProps) {
|
||||
if (!featured) return null
|
||||
|
||||
return (
|
||||
<div className='w-[560px] rounded-[5px] border border-[var(--landing-bg-elevated)] bg-[var(--landing-bg)] p-4 shadow-overlay'>
|
||||
<div className='w-[560px] rounded-[5px] border border-[var(--landing-bg-elevated)] bg-[var(--landing-bg)] p-2 shadow-overlay'>
|
||||
<div className='grid grid-cols-3 gap-2'>
|
||||
<BlogCard
|
||||
slug={featured.slug}
|
||||
@@ -37,8 +37,8 @@ const RESOURCE_CARDS = [
|
||||
|
||||
export function DocsDropdown() {
|
||||
return (
|
||||
<div className='w-[480px] rounded-[5px] border border-[var(--landing-bg-elevated)] bg-[var(--landing-bg)] p-4 shadow-overlay'>
|
||||
<div className='grid grid-cols-2 gap-2.5'>
|
||||
<div className='w-[480px] rounded-[5px] border border-[var(--landing-bg-elevated)] bg-[var(--landing-bg)] p-2 shadow-overlay'>
|
||||
<div className='grid grid-cols-2 gap-2'>
|
||||
{PREVIEW_CARDS.map((card) => (
|
||||
<a
|
||||
key={card.title}
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { GithubOutlineIcon } from '@/components/icons'
|
||||
import { getFormattedGitHubStars } from '@/app/(home)/actions/github'
|
||||
import { getFormattedGitHubStars } from '@/app/(landing)/actions/github'
|
||||
|
||||
const logger = createLogger('github-stars')
|
||||
|
||||
@@ -31,7 +31,7 @@ export function GitHubStars() {
|
||||
href='https://github.com/simstudioai/sim'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='flex items-center gap-2 px-3'
|
||||
className='flex h-[30px] items-center gap-2 self-center rounded-[5px] px-3 transition-colors duration-200 group-hover:bg-[var(--landing-bg-elevated)]'
|
||||
aria-label={`GitHub repository — ${stars} stars`}
|
||||
>
|
||||
<GithubOutlineIcon className='h-[14px] w-[14px]' />
|
||||
@@ -10,9 +10,9 @@ import { cn } from '@/lib/core/utils/cn'
|
||||
import {
|
||||
BlogDropdown,
|
||||
type NavBlogPost,
|
||||
} from '@/app/(home)/components/navbar/components/blog-dropdown'
|
||||
import { DocsDropdown } from '@/app/(home)/components/navbar/components/docs-dropdown'
|
||||
import { GitHubStars } from '@/app/(home)/components/navbar/components/github-stars'
|
||||
} from '@/app/(landing)/components/navbar/components/blog-dropdown'
|
||||
import { DocsDropdown } from '@/app/(landing)/components/navbar/components/docs-dropdown'
|
||||
import { GitHubStars } from '@/app/(landing)/components/navbar/components/github-stars'
|
||||
import { getBrandConfig } from '@/ee/whitelabeling'
|
||||
|
||||
type DropdownId = 'docs' | 'blog' | null
|
||||
@@ -29,10 +29,9 @@ const NAV_LINKS: NavLink[] = [
|
||||
{ label: 'Docs', href: 'https://docs.sim.ai', external: true, icon: 'chevron', dropdown: 'docs' },
|
||||
{ label: 'Blog', href: '/blog', icon: 'chevron', dropdown: 'blog' },
|
||||
{ label: 'Pricing', href: '/#pricing' },
|
||||
{ label: 'Enterprise', href: 'https://form.typeform.com/to/jqCO12pF', external: true },
|
||||
]
|
||||
|
||||
const LOGO_CELL = 'flex items-center pl-5 lg:pl-20 pr-5'
|
||||
const LOGO_CELL = 'flex items-center pl-5 lg:pl-16 pr-5'
|
||||
const LINK_CELL = 'flex items-center px-3.5'
|
||||
|
||||
interface NavbarProps {
|
||||
@@ -49,7 +48,6 @@ export default function Navbar({ logoOnly = false, blogPosts = [] }: NavbarProps
|
||||
const useHomeLinks = isAuthenticated || isBrowsingHome
|
||||
const logoHref = useHomeLinks ? '/?home' : '/'
|
||||
const [activeDropdown, setActiveDropdown] = useState<DropdownId>(null)
|
||||
const [hoveredLink, setHoveredLink] = useState<string | null>(null)
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
||||
const closeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
|
||||
@@ -91,12 +89,10 @@ export default function Navbar({ logoOnly = false, blogPosts = [] }: NavbarProps
|
||||
return () => mq.removeEventListener('change', handler)
|
||||
}, [])
|
||||
|
||||
const anyHighlighted = activeDropdown !== null || hoveredLink !== null
|
||||
|
||||
return (
|
||||
<nav
|
||||
aria-label='Primary navigation'
|
||||
className='relative flex h-[52px] border-[var(--landing-bg-elevated)] border-b-[1px] bg-[var(--landing-bg)] font-[430] font-season text-[var(--landing-text)] text-sm'
|
||||
className='relative flex h-[58px] border-[var(--landing-bg-elevated)] border-b-[1px] bg-[var(--landing-bg)] font-[430] font-season text-[var(--landing-text)] text-sm'
|
||||
itemScope
|
||||
itemType='https://schema.org/SiteNavigationElement'
|
||||
>
|
||||
@@ -134,13 +130,9 @@ export default function Navbar({ logoOnly = false, blogPosts = [] }: NavbarProps
|
||||
useHomeLinks && rawHref.startsWith('/#') ? `/?home${rawHref.slice(1)}` : rawHref
|
||||
const hasDropdown = !!dropdown
|
||||
const isActive = hasDropdown && activeDropdown === dropdown
|
||||
const isThisHovered = hoveredLink === label
|
||||
const isHighlighted = isActive || isThisHovered
|
||||
const isDimmed = anyHighlighted && !isHighlighted
|
||||
const linkClass = cn(
|
||||
icon ? `${LINK_CELL} gap-2` : LINK_CELL,
|
||||
'transition-colors duration-200',
|
||||
isDimmed && 'text-[color-mix(in_srgb,var(--landing-text-subtle)_60%,transparent)]'
|
||||
'h-[30px] self-center rounded-[5px] transition-colors duration-200 group-hover:bg-[var(--landing-bg-elevated)]'
|
||||
)
|
||||
const chevron = icon === 'chevron' && <NavChevron open={isActive} />
|
||||
|
||||
@@ -148,7 +140,7 @@ export default function Navbar({ logoOnly = false, blogPosts = [] }: NavbarProps
|
||||
return (
|
||||
<li
|
||||
key={label}
|
||||
className='relative flex'
|
||||
className='group relative flex'
|
||||
onMouseEnter={() => openDropdown(dropdown)}
|
||||
onMouseLeave={scheduleClose}
|
||||
>
|
||||
@@ -157,51 +149,44 @@ export default function Navbar({ logoOnly = false, blogPosts = [] }: NavbarProps
|
||||
href={href}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className={cn(linkClass, 'h-full cursor-pointer')}
|
||||
itemProp='url'
|
||||
className={cn(linkClass, 'cursor-pointer')}
|
||||
>
|
||||
{label}
|
||||
{chevron}
|
||||
</a>
|
||||
) : (
|
||||
<Link href={href} className={cn(linkClass, 'h-full cursor-pointer')}>
|
||||
<Link href={href} itemProp='url' className={cn(linkClass, 'cursor-pointer')}>
|
||||
{label}
|
||||
{chevron}
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'-mt-0.5 absolute top-full left-0 z-50',
|
||||
isActive
|
||||
? 'pointer-events-auto opacity-100'
|
||||
: 'pointer-events-none opacity-0'
|
||||
)}
|
||||
style={{
|
||||
transform: isActive ? 'translateY(0)' : 'translateY(-6px)',
|
||||
transition: 'opacity 200ms ease, transform 200ms ease',
|
||||
}}
|
||||
>
|
||||
{dropdown === 'docs' && <DocsDropdown />}
|
||||
{dropdown === 'blog' && <BlogDropdown posts={blogPosts} />}
|
||||
</div>
|
||||
{isActive && (
|
||||
<div className='-mt-0.5 pointer-events-auto absolute top-full left-0 z-50'>
|
||||
{dropdown === 'docs' && <DocsDropdown />}
|
||||
{dropdown === 'blog' && <BlogDropdown posts={blogPosts} />}
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<li
|
||||
key={label}
|
||||
className='flex'
|
||||
onMouseEnter={() => setHoveredLink(label)}
|
||||
onMouseLeave={() => setHoveredLink(null)}
|
||||
>
|
||||
<li key={label} className='group flex'>
|
||||
{external ? (
|
||||
<a href={href} target='_blank' rel='noopener noreferrer' className={linkClass}>
|
||||
<a
|
||||
href={href}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
itemProp='url'
|
||||
className={linkClass}
|
||||
>
|
||||
{label}
|
||||
{chevron}
|
||||
</a>
|
||||
) : (
|
||||
<Link href={href} className={linkClass} aria-label={label}>
|
||||
<Link href={href} itemProp='url' className={linkClass} aria-label={label}>
|
||||
{label}
|
||||
{chevron}
|
||||
</Link>
|
||||
@@ -209,14 +194,7 @@ export default function Navbar({ logoOnly = false, blogPosts = [] }: NavbarProps
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
<li
|
||||
className={cn(
|
||||
'flex transition-opacity duration-200',
|
||||
anyHighlighted && hoveredLink !== 'github' && 'opacity-60'
|
||||
)}
|
||||
onMouseEnter={() => setHoveredLink('github')}
|
||||
onMouseLeave={() => setHoveredLink(null)}
|
||||
>
|
||||
<li className='group flex'>
|
||||
<GitHubStars />
|
||||
</li>
|
||||
</ul>
|
||||
@@ -225,7 +203,7 @@ export default function Navbar({ logoOnly = false, blogPosts = [] }: NavbarProps
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'hidden items-center gap-2 pr-20 pl-5 lg:flex',
|
||||
'hidden items-center gap-2 pr-16 pl-5 lg:flex',
|
||||
isSessionPending && 'invisible'
|
||||
)}
|
||||
>
|
||||
@@ -271,7 +249,7 @@ export default function Navbar({ logoOnly = false, blogPosts = [] }: NavbarProps
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'fixed inset-x-0 top-[52px] bottom-0 z-50 flex flex-col overflow-y-auto bg-[var(--landing-bg)] font-[430] font-season text-sm transition-all duration-200 lg:hidden',
|
||||
'fixed inset-x-0 top-[58px] bottom-0 z-50 flex flex-col overflow-y-auto bg-[var(--landing-bg)] font-[430] font-season text-sm transition-all duration-200 lg:hidden',
|
||||
mobileMenuOpen ? 'visible opacity-100' : 'invisible opacity-0'
|
||||
)}
|
||||
>
|
||||
@@ -1,6 +1,8 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { Badge } from '@/components/emcn'
|
||||
import { DemoRequestModal } from '@/app/(home)/components/demo-request/demo-request-modal'
|
||||
import { DemoRequestModal } from '@/app/(landing)/components/demo-request/demo-request-modal'
|
||||
|
||||
interface PricingTier {
|
||||
id: string
|
||||
@@ -80,10 +82,10 @@ const PRICING_TIERS: PricingTier[] = [
|
||||
'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' },
|
||||
cta: { label: 'Get a demo', action: 'demo-request' },
|
||||
},
|
||||
]
|
||||
|
||||
@@ -110,7 +112,21 @@ function PricingCard({ tier }: PricingCardProps) {
|
||||
const isPro = tier.id === 'pro'
|
||||
|
||||
return (
|
||||
<article className='flex flex-1 flex-col' aria-labelledby={`${tier.id}-heading`}>
|
||||
<article
|
||||
className='flex flex-1 flex-col'
|
||||
aria-labelledby={`${tier.id}-heading`}
|
||||
itemScope
|
||||
itemType='https://schema.org/Offer'
|
||||
>
|
||||
<meta itemProp='name' content={`${tier.name} Plan`} />
|
||||
<meta
|
||||
itemProp='price'
|
||||
content={
|
||||
tier.price === 'Free' ? '0' : tier.price === 'Custom' ? '' : tier.price.replace('$', '')
|
||||
}
|
||||
/>
|
||||
<meta itemProp='priceCurrency' content='USD' />
|
||||
<meta itemProp='availability' content='https://schema.org/InStock' />
|
||||
<div className='flex flex-1 flex-col gap-6 rounded-t-lg border border-[var(--landing-border-light)] border-b-0 bg-white p-5'>
|
||||
<div className='flex flex-col'>
|
||||
<h3
|
||||
@@ -196,7 +212,7 @@ export default function Pricing() {
|
||||
aria-labelledby='pricing-heading'
|
||||
className='bg-[var(--landing-bg-section)]'
|
||||
>
|
||||
<div className='px-4 pt-[60px] pb-10 sm:px-8 sm:pt-20 sm:pb-0 md:px-20 md:pt-[100px]'>
|
||||
<div className='px-4 pt-[60px] pb-[60px] sm:px-8 sm:pt-20 sm:pb-20 md:px-16 md:pt-[100px] md:pb-[100px]'>
|
||||
<div className='flex flex-col items-start gap-3 sm:gap-4 md:gap-5'>
|
||||
<Badge
|
||||
variant='blue'
|
||||
@@ -213,6 +229,12 @@ export default function Pricing() {
|
||||
>
|
||||
Pricing
|
||||
</h2>
|
||||
<p className='sr-only'>
|
||||
Sim pricing: Community plan is free with 1,000 credits and 5GB storage. Pro plan is $25
|
||||
per month with 6,000 credits and 50GB storage. Max plan is $100 per month with 25,000
|
||||
credits and 500GB storage. Enterprise pricing is custom with SSO, SCIM, SOC2 compliance,
|
||||
self-hosting, and dedicated support. All plans include CLI, SDK, and MCP access.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='mt-8 grid grid-cols-1 gap-4 sm:mt-10 sm:grid-cols-2 md:mt-12 lg:grid-cols-4'>
|
||||
@@ -4,7 +4,7 @@
|
||||
* Renders a `<script type="application/ld+json">` with Schema.org markup.
|
||||
* Single source of truth for machine-readable page metadata.
|
||||
*
|
||||
* Schemas: Organization, WebSite, WebPage, BreadcrumbList, WebApplication, FAQPage.
|
||||
* Schemas: Organization, WebSite, WebPage, BreadcrumbList, WebApplication, SoftwareSourceCode, FAQPage.
|
||||
*
|
||||
* AI crawler behavior (2025-2026):
|
||||
* - Google AI Overviews / Bing Copilot parse JSON-LD from their search indexes.
|
||||
@@ -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',
|
||||
@@ -170,6 +170,15 @@ export default function StructuredData() {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'@type': 'SoftwareSourceCode',
|
||||
'@id': 'https://sim.ai/#source',
|
||||
codeRepository: 'https://github.com/simstudioai/sim',
|
||||
programmingLanguage: ['TypeScript', 'Python'],
|
||||
runtimePlatform: 'Node.js',
|
||||
license: 'https://opensource.org/licenses/AGPL-3.0',
|
||||
isPartOf: { '@id': 'https://sim.ai/#software' },
|
||||
},
|
||||
{
|
||||
'@type': 'FAQPage',
|
||||
'@id': 'https://sim.ai/#faq',
|
||||
@@ -179,7 +188,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 +220,31 @@ 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.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'Is Sim open source?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'Yes. Sim is fully open source under the AGPL-3.0 license. The source code is available on GitHub at github.com/simstudioai/sim. You can self-host Sim or use the hosted version at sim.ai.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'What integrations does Sim support?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'Sim supports 1,000+ integrations including Slack, Gmail, GitHub, Notion, Airtable, Supabase, HubSpot, Salesforce, Jira, Linear, Google Drive, Google Sheets, Confluence, Discord, Microsoft Teams, Outlook, Telegram, Stripe, Pinecone, and Firecrawl. New integrations are added regularly.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'Can I self-host Sim?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'Yes. Sim can be self-hosted using Docker. Documentation is available at docs.sim.ai/self-hosting. Enterprise customers can also get dedicated infrastructure and on-premise deployment.',
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { PreviewWorkflow } from '@/app/(home)/components/landing-preview/components/landing-preview-workflow/workflow-data'
|
||||
import type { PreviewWorkflow } from '@/app/(landing)/components/landing-preview/components/landing-preview-workflow/workflow-data'
|
||||
|
||||
/**
|
||||
* OCR Invoice to DB — Start → Agent (Textract) → Supabase
|
||||
@@ -1,21 +1,21 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { AnimatePresence, type MotionValue, motion, useScroll, useTransform } from 'framer-motion'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Badge, ChevronDown } from '@/components/emcn'
|
||||
import { LandingWorkflowSeedStorage } from '@/lib/core/utils/browser-storage'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { TEMPLATE_WORKFLOWS } from '@/app/(home)/components/templates/template-workflows'
|
||||
import { TEMPLATE_WORKFLOWS } from '@/app/(landing)/components/templates/template-workflows'
|
||||
|
||||
const logger = createLogger('LandingTemplates')
|
||||
|
||||
const LandingPreviewWorkflow = dynamic(
|
||||
() =>
|
||||
import(
|
||||
'@/app/(home)/components/landing-preview/components/landing-preview-workflow/landing-preview-workflow'
|
||||
'@/app/(landing)/components/landing-preview/components/landing-preview-workflow/landing-preview-workflow'
|
||||
).then((mod) => mod.LandingPreviewWorkflow),
|
||||
{
|
||||
ssr: false,
|
||||
@@ -236,73 +236,6 @@ const DEPTH_CONFIGS: Record<string, DepthConfig> = {
|
||||
},
|
||||
}
|
||||
|
||||
const SCROLL_BLOCK_RX = '2.59574'
|
||||
|
||||
/**
|
||||
* Two-row horizontal block strip for the scroll-driven reveal in the templates section.
|
||||
* Same structural pattern as the hero's top-right blocks with matching colours:
|
||||
* blue (left) → pink (middle) → green (right).
|
||||
*/
|
||||
const SCROLL_BLOCK_RECTS = [
|
||||
{ opacity: 0.6, x: '-34.24', y: '0', width: '34.24', height: '16.86', fill: '#2ABBF8' },
|
||||
{ opacity: 1, x: '-17.38', y: '0', width: '16.86', height: '16.86', fill: '#2ABBF8' },
|
||||
{ opacity: 1, x: '0', y: '0', width: '16.86', height: '33.73', fill: '#2ABBF8' },
|
||||
{ opacity: 0.6, x: '0', y: '0', width: '85.34', height: '16.86', fill: '#2ABBF8' },
|
||||
{ opacity: 1, x: '0', y: '0', width: '16.86', height: '16.86', fill: '#2ABBF8' },
|
||||
{ opacity: 0.6, x: '34.24', y: '0', width: '34.24', height: '33.73', fill: '#2ABBF8' },
|
||||
{ opacity: 1, x: '34.24', y: '0', width: '16.86', height: '16.86', fill: '#2ABBF8' },
|
||||
{ opacity: 1, x: '51.62', y: '16.86', width: '16.86', height: '16.86', fill: '#2ABBF8' },
|
||||
{ opacity: 1, x: '68.48', y: '0', width: '54.65', height: '16.86', fill: '#FA4EDF' },
|
||||
{ opacity: 0.6, x: '106.27', y: '0', width: '34.24', height: '33.73', fill: '#FA4EDF' },
|
||||
{ opacity: 0.6, x: '106.27', y: '0', width: '51.10', height: '16.86', fill: '#FA4EDF' },
|
||||
{ opacity: 1, x: '123.65', y: '16.86', width: '16.86', height: '16.86', fill: '#FA4EDF' },
|
||||
{ opacity: 0.6, x: '157.37', y: '0', width: '34.24', height: '16.86', fill: '#FA4EDF' },
|
||||
{ opacity: 1, x: '157.37', y: '0', width: '16.86', height: '16.86', fill: '#FA4EDF' },
|
||||
{ opacity: 0.6, x: '209.0', y: '0', width: '68.48', height: '16.86', fill: '#00F701' },
|
||||
{ opacity: 0.6, x: '209.14', y: '0', width: '16.86', height: '33.73', fill: '#00F701' },
|
||||
{ opacity: 0.6, x: '243.23', y: '0', width: '34.24', height: '33.73', fill: '#00F701' },
|
||||
{ opacity: 1, x: '243.23', y: '0', width: '16.86', height: '16.86', fill: '#00F701' },
|
||||
{ opacity: 0.6, x: '260.10', y: '0', width: '34.04', height: '16.86', fill: '#00F701' },
|
||||
{ opacity: 1, x: '260.61', y: '16.86', width: '16.86', height: '16.86', fill: '#00F701' },
|
||||
] as const
|
||||
|
||||
const SCROLL_BLOCK_MAX_X = Math.max(...SCROLL_BLOCK_RECTS.map((r) => Number.parseFloat(r.x)))
|
||||
const SCROLL_REVEAL_START = 0.05
|
||||
const SCROLL_REVEAL_SPAN = 0.7
|
||||
const SCROLL_FADE_IN = 0.03
|
||||
|
||||
function getScrollBlockThreshold(x: string): number {
|
||||
const normalized = Number.parseFloat(x) / SCROLL_BLOCK_MAX_X
|
||||
return SCROLL_REVEAL_START + (1 - normalized) * SCROLL_REVEAL_SPAN
|
||||
}
|
||||
|
||||
interface ScrollBlockRectProps {
|
||||
scrollYProgress: MotionValue<number>
|
||||
rect: (typeof SCROLL_BLOCK_RECTS)[number]
|
||||
}
|
||||
|
||||
/** Renders a single SVG rect whose opacity is driven by scroll progress. */
|
||||
function ScrollBlockRect({ scrollYProgress, rect }: ScrollBlockRectProps) {
|
||||
const threshold = getScrollBlockThreshold(rect.x)
|
||||
const opacity = useTransform(
|
||||
scrollYProgress,
|
||||
[threshold, threshold + SCROLL_FADE_IN],
|
||||
[0, rect.opacity]
|
||||
)
|
||||
|
||||
return (
|
||||
<motion.rect
|
||||
x={rect.x}
|
||||
y={rect.y}
|
||||
width={rect.width}
|
||||
height={rect.height}
|
||||
rx={SCROLL_BLOCK_RX}
|
||||
fill={rect.fill}
|
||||
style={{ opacity }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function buildBottomWallStyle(config: DepthConfig) {
|
||||
let pos = 0
|
||||
const stops: string[] = []
|
||||
@@ -317,36 +250,9 @@ function buildBottomWallStyle(config: DepthConfig) {
|
||||
}
|
||||
}
|
||||
|
||||
interface DotGridProps {
|
||||
className?: string
|
||||
cols: number
|
||||
rows: number
|
||||
gap?: number
|
||||
}
|
||||
|
||||
function DotGrid({ className, cols, rows, gap = 0 }: DotGridProps) {
|
||||
return (
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className={className}
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: `repeat(${cols}, 1fr)`,
|
||||
gap,
|
||||
placeItems: 'center',
|
||||
}}
|
||||
>
|
||||
{Array.from({ length: cols * rows }, (_, i) => (
|
||||
<div key={i} className='h-[1.5px] w-[1.5px] rounded-full bg-[var(--landing-bg-elevated)]' />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const TEMPLATES_PANEL_ID = 'templates-panel'
|
||||
|
||||
export default function Templates() {
|
||||
const sectionRef = useRef<HTMLDivElement>(null)
|
||||
const [activeIndex, setActiveIndex] = useState(0)
|
||||
const [isPreparingTemplate, setIsPreparingTemplate] = useState(false)
|
||||
const [isMobile, setIsMobile] = useState(false)
|
||||
@@ -360,11 +266,6 @@ export default function Templates() {
|
||||
return () => mq.removeEventListener('change', handler)
|
||||
}, [])
|
||||
|
||||
const { scrollYProgress } = useScroll({
|
||||
target: sectionRef,
|
||||
offset: ['start 0.9', 'start 0.2'],
|
||||
})
|
||||
|
||||
const activeWorkflow = TEMPLATE_WORKFLOWS[activeIndex]
|
||||
const activeDepth = DEPTH_CONFIGS[activeWorkflow.id]
|
||||
|
||||
@@ -408,12 +309,7 @@ export default function Templates() {
|
||||
])
|
||||
|
||||
return (
|
||||
<section
|
||||
ref={sectionRef}
|
||||
id='templates'
|
||||
aria-labelledby='templates-heading'
|
||||
className='mt-10 mb-20'
|
||||
>
|
||||
<section id='templates' aria-labelledby='templates-heading' className='pt-[60px] lg:pt-[100px]'>
|
||||
<p className='sr-only'>
|
||||
Sim includes {TEMPLATE_WORKFLOWS.length} pre-built workflow templates covering OCR
|
||||
processing, release management, meeting follow-ups, resume scanning, email triage,
|
||||
@@ -421,35 +317,15 @@ export default function Templates() {
|
||||
and knowledge base Q&A. Each template connects real integrations and LLMs — pick one,
|
||||
customise it, and deploy in minutes.
|
||||
</p>
|
||||
<ul className='sr-only'>
|
||||
{TEMPLATE_WORKFLOWS.map((workflow) => (
|
||||
<li key={workflow.id}>{workflow.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div className='bg-[var(--landing-bg)]'>
|
||||
<DotGrid
|
||||
className='overflow-hidden border-[var(--landing-bg-elevated)] border-y bg-[var(--landing-bg)] p-1.5'
|
||||
cols={160}
|
||||
rows={1}
|
||||
gap={6}
|
||||
/>
|
||||
|
||||
<div className='relative overflow-hidden'>
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='pointer-events-none absolute top-0 right-0 z-20 hidden lg:block'
|
||||
>
|
||||
<svg
|
||||
width={329}
|
||||
height={34}
|
||||
viewBox='-34 0 329 34'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
className='h-auto w-full'
|
||||
>
|
||||
{SCROLL_BLOCK_RECTS.map((r, i) => (
|
||||
<ScrollBlockRect key={i} scrollYProgress={scrollYProgress} rect={r} />
|
||||
))}
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div className='px-5 pt-[60px] lg:px-20 lg:pt-[100px]'>
|
||||
<div className='px-5 lg:px-16'>
|
||||
<div className='flex flex-col items-start gap-5'>
|
||||
<Badge
|
||||
variant='blue'
|
||||
@@ -480,24 +356,10 @@ export default function Templates() {
|
||||
</div>
|
||||
|
||||
<div className='mt-10 flex border-[var(--landing-bg-elevated)] border-y lg:mt-[73px]'>
|
||||
<div className='shrink-0'>
|
||||
<div className='h-full lg:hidden'>
|
||||
<DotGrid
|
||||
className='h-full w-[24px] overflow-hidden border-[var(--landing-bg-elevated)] border-r p-1'
|
||||
cols={2}
|
||||
rows={55}
|
||||
gap={4}
|
||||
/>
|
||||
</div>
|
||||
<div className='hidden h-full lg:block'>
|
||||
<DotGrid
|
||||
className='h-full w-[80px] overflow-hidden border-[var(--landing-bg-elevated)] border-r p-1.5'
|
||||
cols={8}
|
||||
rows={55}
|
||||
gap={6}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='w-[24px] shrink-0 border-[var(--landing-bg-elevated)] border-r lg:w-16'
|
||||
/>
|
||||
|
||||
<div className='flex min-w-0 flex-1 flex-col lg:flex-row'>
|
||||
<div
|
||||
@@ -575,7 +437,7 @@ export default function Templates() {
|
||||
<LandingPreviewWorkflow
|
||||
workflow={workflow}
|
||||
animate
|
||||
fitViewOptions={{ padding: 0.15, maxZoom: 1.3 }}
|
||||
fitViewOptions={{ padding: 0.15, minZoom: 0.1, maxZoom: 0.8 }}
|
||||
/>
|
||||
</div>
|
||||
<div className='p-3'>
|
||||
@@ -617,46 +479,55 @@ export default function Templates() {
|
||||
className='group/cta absolute top-4 right-[16px] z-10 inline-flex h-[32px] cursor-pointer items-center gap-1.5 rounded-[5px] border border-white bg-white px-2.5 font-[430] font-season text-black text-sm transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
|
||||
>
|
||||
{isPreparingTemplate ? 'Preparing...' : 'Use template'}
|
||||
<span className='relative h-[10px] w-[10px] shrink-0'>
|
||||
<ChevronDown className='-rotate-90 absolute inset-0 h-[10px] w-[10px] transition-opacity duration-150 group-hover/cta:opacity-0' />
|
||||
<svg
|
||||
className='absolute inset-0 h-[10px] w-[10px] opacity-0 transition-opacity duration-150 group-hover/cta:opacity-100'
|
||||
viewBox='0 0 10 10'
|
||||
<svg
|
||||
className='h-[10px] w-[10px] shrink-0'
|
||||
viewBox='0 0 10 10'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<line
|
||||
x1='0'
|
||||
y1='5'
|
||||
x2='9'
|
||||
y2='5'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.33'
|
||||
strokeLinecap='square'
|
||||
className='origin-left scale-x-0 transition-transform duration-200 ease-out [transform-box:fill-box] group-hover/cta:scale-x-100'
|
||||
/>
|
||||
<path
|
||||
d='M3.5 2L6.5 5L3.5 8'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.33'
|
||||
strokeLinecap='square'
|
||||
strokeLinejoin='miter'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<path
|
||||
d='M1 5H8M5.5 2L8.5 5L5.5 8'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.33'
|
||||
strokeLinecap='square'
|
||||
strokeLinejoin='miter'
|
||||
fill='none'
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
className='transition-transform duration-200 ease-out group-hover/cta:translate-x-[30%]'
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='shrink-0'>
|
||||
<div className='h-full lg:hidden'>
|
||||
<DotGrid
|
||||
className='h-full w-[24px] overflow-hidden border-[var(--landing-bg-elevated)] border-l p-1'
|
||||
cols={2}
|
||||
rows={55}
|
||||
gap={4}
|
||||
/>
|
||||
</div>
|
||||
<div className='hidden h-full lg:block'>
|
||||
<DotGrid
|
||||
className='h-full w-[80px] overflow-hidden border-[var(--landing-bg-elevated)] border-l p-1.5'
|
||||
cols={8}
|
||||
rows={55}
|
||||
gap={6}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='w-[24px] shrink-0 border-[var(--landing-bg-elevated)] border-l lg:w-16'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='relative pb-[60px] lg:pb-[100px]'>
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='absolute top-0 bottom-0 left-[calc(4rem-1px)] hidden w-px bg-[var(--landing-bg-elevated)] lg:block'
|
||||
/>
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='absolute top-0 right-[calc(4rem-1px)] bottom-0 hidden w-px bg-[var(--landing-bg-elevated)] lg:block'
|
||||
/>
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='absolute right-16 bottom-0 left-16 hidden h-px bg-[var(--landing-bg-elevated)] lg:block'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,8 +1,4 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { ChevronDown } from '@/components/emcn'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { LandingFAQ } from '@/app/(landing)/components/landing-faq'
|
||||
import type { FAQItem } from '@/app/(landing)/integrations/data/types'
|
||||
|
||||
interface IntegrationFAQProps {
|
||||
@@ -10,49 +6,5 @@ interface IntegrationFAQProps {
|
||||
}
|
||||
|
||||
export function IntegrationFAQ({ faqs }: IntegrationFAQProps) {
|
||||
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>
|
||||
)
|
||||
return <LandingFAQ faqs={faqs} />
|
||||
}
|
||||
|
||||