mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-28 03:00:29 -04:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4309d0619a | ||
|
|
85f1d96859 | ||
|
|
bc31710c1c | ||
|
|
30c5e82ab0 | ||
|
|
6a4f5f2074 | ||
|
|
74d0a47525 | ||
|
|
c8525852d4 | ||
|
|
20cc0185bf | ||
|
|
cbfab1ceaa | ||
|
|
1acafe8763 | ||
|
|
c1d788ce94 | ||
|
|
bad78ccb59 | ||
|
|
8bbca9ba05 | ||
|
|
34f77e00bc | ||
|
|
fb5ebd3bed | ||
|
|
2e85361ed6 | ||
|
|
59de6bbb43 | ||
|
|
2b9fb19899 | ||
|
|
266bc2141d | ||
|
|
6099683e5a |
@@ -1,17 +1,17 @@
|
||||
---
|
||||
description: Create webhook triggers for a Sim integration using the generic trigger builder
|
||||
description: Create webhook or polling triggers for a Sim integration
|
||||
argument-hint: <service-name>
|
||||
---
|
||||
|
||||
# Add Trigger
|
||||
|
||||
You are an expert at creating webhook triggers for Sim. You understand the trigger system, the generic `buildTriggerSubBlocks` helper, and how triggers connect to blocks.
|
||||
You are an expert at creating webhook and polling triggers for Sim. You understand the trigger system, the generic `buildTriggerSubBlocks` helper, polling infrastructure, and how triggers connect to blocks.
|
||||
|
||||
## Your Task
|
||||
|
||||
1. Research what webhook events the service supports
|
||||
2. Create the trigger files using the generic builder
|
||||
3. Create a provider handler if custom auth, formatting, or subscriptions are needed
|
||||
1. Research what webhook events the service supports — if the service lacks reliable webhooks, use polling
|
||||
2. Create the trigger files using the generic builder (webhook) or manual config (polling)
|
||||
3. Create a provider handler (webhook) or polling handler (polling)
|
||||
4. Register triggers and connect them to the block
|
||||
|
||||
## Directory Structure
|
||||
@@ -146,23 +146,37 @@ export const TRIGGER_REGISTRY: TriggerRegistry = {
|
||||
|
||||
### Block file (`apps/sim/blocks/blocks/{service}.ts`)
|
||||
|
||||
Wire triggers into the block so the trigger UI appears and `generate-docs.ts` discovers them. Two changes are needed:
|
||||
|
||||
1. **Spread trigger subBlocks** at the end of the block's `subBlocks` array
|
||||
2. **Add `triggers` property** after `outputs` with `enabled: true` and `available: [...]`
|
||||
|
||||
```typescript
|
||||
import { getTrigger } from '@/triggers'
|
||||
|
||||
export const {Service}Block: BlockConfig = {
|
||||
// ...
|
||||
triggers: {
|
||||
enabled: true,
|
||||
available: ['{service}_event_a', '{service}_event_b'],
|
||||
},
|
||||
subBlocks: [
|
||||
// Regular tool subBlocks first...
|
||||
...getTrigger('{service}_event_a').subBlocks,
|
||||
...getTrigger('{service}_event_b').subBlocks,
|
||||
],
|
||||
// ... tools, inputs, outputs ...
|
||||
triggers: {
|
||||
enabled: true,
|
||||
available: ['{service}_event_a', '{service}_event_b'],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
**Versioned blocks (V1 + V2):** Many integrations have a hidden V1 block and a visible V2 block. Where you add the trigger wiring depends on how V2 inherits from V1:
|
||||
|
||||
- **V2 uses `...V1Block` spread** (e.g., Google Calendar): Add trigger to V1 — V2 inherits both `subBlocks` and `triggers` automatically.
|
||||
- **V2 defines its own `subBlocks`** (e.g., Google Sheets): Add trigger to V2 (the visible block). V1 is hidden and doesn't need it.
|
||||
- **Single block, no V2** (e.g., Google Drive): Add trigger directly.
|
||||
|
||||
`generate-docs.ts` deduplicates by base type (first match wins). If V1 is processed first without triggers, the V2 triggers won't appear in `integrations.json`. Always verify by checking the output after running the script.
|
||||
|
||||
## Provider Handler
|
||||
|
||||
All provider-specific webhook logic lives in a single handler file: `apps/sim/lib/webhooks/providers/{service}.ts`.
|
||||
@@ -327,6 +341,121 @@ export function buildOutputs(): Record<string, TriggerOutput> {
|
||||
}
|
||||
```
|
||||
|
||||
## Polling Triggers
|
||||
|
||||
Use polling when the service lacks reliable webhooks (e.g., Google Sheets, Google Drive, Google Calendar, Gmail, RSS, IMAP). Polling triggers do NOT use `buildTriggerSubBlocks` — they define subBlocks manually.
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
apps/sim/triggers/{service}/
|
||||
├── index.ts # Barrel export
|
||||
└── poller.ts # TriggerConfig with polling: true
|
||||
|
||||
apps/sim/lib/webhooks/polling/
|
||||
└── {service}.ts # PollingProviderHandler implementation
|
||||
```
|
||||
|
||||
### Polling Handler (`apps/sim/lib/webhooks/polling/{service}.ts`)
|
||||
|
||||
```typescript
|
||||
import { pollingIdempotency } from '@/lib/core/idempotency/service'
|
||||
import type { PollingProviderHandler, PollWebhookContext } from '@/lib/webhooks/polling/types'
|
||||
import { markWebhookFailed, markWebhookSuccess, resolveOAuthCredential, updateWebhookProviderConfig } from '@/lib/webhooks/polling/utils'
|
||||
import { processPolledWebhookEvent } from '@/lib/webhooks/processor'
|
||||
|
||||
export const {service}PollingHandler: PollingProviderHandler = {
|
||||
provider: '{service}',
|
||||
label: '{Service}',
|
||||
|
||||
async pollWebhook(ctx: PollWebhookContext): Promise<'success' | 'failure'> {
|
||||
const { webhookData, workflowData, requestId, logger } = ctx
|
||||
const webhookId = webhookData.id
|
||||
|
||||
try {
|
||||
// For OAuth services:
|
||||
const accessToken = await resolveOAuthCredential(webhookData, '{service}', requestId, logger)
|
||||
const config = webhookData.providerConfig as unknown as {Service}WebhookConfig
|
||||
|
||||
// First poll: seed state, emit nothing
|
||||
if (!config.lastCheckedTimestamp) {
|
||||
await updateWebhookProviderConfig(webhookId, { lastCheckedTimestamp: new Date().toISOString() }, logger)
|
||||
await markWebhookSuccess(webhookId, logger)
|
||||
return 'success'
|
||||
}
|
||||
|
||||
// Fetch changes since last poll, process with idempotency
|
||||
// ...
|
||||
|
||||
await markWebhookSuccess(webhookId, logger)
|
||||
return 'success'
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error processing {service} webhook ${webhookId}:`, error)
|
||||
await markWebhookFailed(webhookId, logger)
|
||||
return 'failure'
|
||||
}
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
**Key patterns:**
|
||||
- First poll seeds state and emits nothing (avoids flooding with existing data)
|
||||
- Use `pollingIdempotency.executeWithIdempotency(provider, key, callback)` for dedup
|
||||
- Use `processPolledWebhookEvent(webhookData, workflowData, payload, requestId)` to fire the workflow
|
||||
- Use `updateWebhookProviderConfig(webhookId, partialConfig, logger)` for read-merge-write on state
|
||||
- Use the latest server-side timestamp from API responses (not wall clock) to avoid clock skew
|
||||
|
||||
### Trigger Config (`apps/sim/triggers/{service}/poller.ts`)
|
||||
|
||||
```typescript
|
||||
import { {Service}Icon } from '@/components/icons'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
export const {service}PollingTrigger: TriggerConfig = {
|
||||
id: '{service}_poller',
|
||||
name: '{Service} Trigger',
|
||||
provider: '{service}',
|
||||
description: 'Triggers when ...',
|
||||
version: '1.0.0',
|
||||
icon: {Service}Icon,
|
||||
polling: true, // REQUIRED — routes to polling infrastructure
|
||||
|
||||
subBlocks: [
|
||||
{ id: 'triggerCredentials', type: 'oauth-input', title: 'Credentials', serviceId: '{service}', requiredScopes: [], required: true, mode: 'trigger', supportsCredentialSets: true },
|
||||
// ... service-specific config fields (dropdowns, inputs, switches) ...
|
||||
{ id: 'triggerInstructions', type: 'text', title: 'Setup Instructions', hideFromPreview: true, mode: 'trigger', defaultValue: '...' },
|
||||
],
|
||||
|
||||
outputs: {
|
||||
// Must match the payload shape from processPolledWebhookEvent
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Registration (3 places)
|
||||
|
||||
1. **`apps/sim/triggers/constants.ts`** — add provider to `POLLING_PROVIDERS` Set
|
||||
2. **`apps/sim/lib/webhooks/polling/registry.ts`** — import handler, add to `POLLING_HANDLERS`
|
||||
3. **`apps/sim/triggers/registry.ts`** — import trigger config, add to `TRIGGER_REGISTRY`
|
||||
|
||||
### Helm Cron Job
|
||||
|
||||
Add to `helm/sim/values.yaml` under the existing polling cron jobs:
|
||||
|
||||
```yaml
|
||||
{service}WebhookPoll:
|
||||
schedule: "*/1 * * * *"
|
||||
concurrencyPolicy: Forbid
|
||||
url: "http://sim:3000/api/webhooks/poll/{service}"
|
||||
```
|
||||
|
||||
### Reference Implementations
|
||||
|
||||
- Simple: `apps/sim/lib/webhooks/polling/rss.ts` + `apps/sim/triggers/rss/poller.ts`
|
||||
- Complex (OAuth, attachments): `apps/sim/lib/webhooks/polling/gmail.ts` + `apps/sim/triggers/gmail/poller.ts`
|
||||
- Cursor-based (changes API): `apps/sim/lib/webhooks/polling/google-drive.ts`
|
||||
- Timestamp-based: `apps/sim/lib/webhooks/polling/google-calendar.ts`
|
||||
|
||||
## Checklist
|
||||
|
||||
### Trigger Definition
|
||||
@@ -352,7 +481,17 @@ export function buildOutputs(): Record<string, TriggerOutput> {
|
||||
- [ ] NO changes to `route.ts`, `provider-subscriptions.ts`, or `deploy.ts`
|
||||
- [ ] API key field uses `password: true`
|
||||
|
||||
### Polling Trigger (if applicable)
|
||||
- [ ] Handler implements `PollingProviderHandler` at `lib/webhooks/polling/{service}.ts`
|
||||
- [ ] Trigger config has `polling: true` and defines subBlocks manually (no `buildTriggerSubBlocks`)
|
||||
- [ ] Provider string matches across: trigger config, handler, `POLLING_PROVIDERS`, polling registry
|
||||
- [ ] First poll seeds state and emits nothing
|
||||
- [ ] Added provider to `POLLING_PROVIDERS` in `triggers/constants.ts`
|
||||
- [ ] Added handler to `POLLING_HANDLERS` in `lib/webhooks/polling/registry.ts`
|
||||
- [ ] Added cron job to `helm/sim/values.yaml`
|
||||
- [ ] Payload shape matches trigger `outputs` schema
|
||||
|
||||
### Testing
|
||||
- [ ] `bun run type-check` passes
|
||||
- [ ] Manually verify `formatInput` output keys match trigger `outputs` keys
|
||||
- [ ] Manually verify output keys match trigger `outputs` keys
|
||||
- [ ] Trigger UI shows correctly in the block
|
||||
|
||||
71
.claude/rules/constitution.md
Normal file
71
.claude/rules/constitution.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# Sim — Language & Positioning
|
||||
|
||||
When editing user-facing copy (landing pages, docs, metadata, marketing), follow these rules.
|
||||
|
||||
## Identity
|
||||
|
||||
Sim is the **AI workspace** where teams build and run AI agents. Not a workflow tool, not an agent framework, not an automation platform.
|
||||
|
||||
**Short definition:** Sim is the open-source AI workspace where teams build, deploy, and manage AI agents.
|
||||
|
||||
**Full definition:** Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM to create agents that automate real work — visually, conversationally, or with code.
|
||||
|
||||
## Audience
|
||||
|
||||
**Primary:** Teams building AI agents for their organization — IT, operations, and technical teams who need governance, security, lifecycle management, and collaboration.
|
||||
|
||||
**Secondary:** Individual builders and developers who care about speed, flexibility, and open source.
|
||||
|
||||
## Required Language
|
||||
|
||||
| Concept | Use | Never use |
|
||||
|---------|-----|-----------|
|
||||
| The product | "AI workspace" | "workflow tool", "automation platform", "agent framework" |
|
||||
| Building | "build agents", "create agents" | "create workflows" (unless describing the workflow module specifically) |
|
||||
| Visual builder | "workflow builder" or "visual builder" | "canvas", "graph editor" |
|
||||
| Mothership | "Mothership" (capitalized) | "chat", "AI assistant", "copilot" |
|
||||
| Deployment | "deploy", "ship" | "publish", "activate" |
|
||||
| Audience | "teams", "builders" | "users", "customers" (in marketing copy) |
|
||||
| What agents do | "automate real work" | "automate tasks", "automate workflows" |
|
||||
| Our advantage | "open-source AI workspace" | "open-source platform" |
|
||||
|
||||
## Tone
|
||||
|
||||
- **Direct.** Short sentences. Active voice. Lead with what it does.
|
||||
- **Concrete.** Name specific things — "Slack bots, compliance agents, data pipelines" — not abstractions.
|
||||
- **Confident, not loud.** No exclamation marks or superlatives.
|
||||
- **Simple.** If a 16-year-old can't understand the sentence, rewrite it.
|
||||
|
||||
## Claim Hierarchy
|
||||
|
||||
When describing Sim, always lead with the most differentiated claim:
|
||||
|
||||
1. **What it is:** "The AI workspace for teams"
|
||||
2. **What you do:** "Build, deploy, and manage AI agents"
|
||||
3. **How:** "Visually, conversationally, or with code"
|
||||
4. **Scale:** "1,000+ integrations, every major LLM"
|
||||
5. **Trust:** "Open source. SOC2. Trusted by 100,000+ builders."
|
||||
|
||||
## Module Descriptions
|
||||
|
||||
| Module | One-liner |
|
||||
|--------|-----------|
|
||||
| **Mothership** | Your AI command center. Build and manage everything in natural language. |
|
||||
| **Workflows** | The visual builder. Connect blocks, models, and integrations into agent logic. |
|
||||
| **Knowledge Base** | Your agents' memory. Upload docs, sync sources, build vector databases. |
|
||||
| **Tables** | A database, built in. Store, query, and wire structured data into agent runs. |
|
||||
| **Files** | Upload, create, and share. One store for your team and every agent. |
|
||||
| **Logs** | Full visibility, every run. Trace execution block by block. |
|
||||
|
||||
## What We Never Say
|
||||
|
||||
- Never call Sim "just a workflow tool"
|
||||
- Never compare only on integration count — we win on AI-native capabilities
|
||||
- Never use "no-code" as the primary descriptor — say "visually, conversationally, or with code"
|
||||
- Never promise unshipped features
|
||||
- Never use jargon ("RAG", "vector database", "MCP") without plain-English explanation on public pages
|
||||
- Avoid "agentic workforce" as a primary term — use "AI agents"
|
||||
|
||||
## Vision
|
||||
|
||||
Sim becomes the default environment where teams build AI agents — not a tool you visit for one task, but a workspace you live in. Workflows are one module; Mothership is another. The workspace is the constant; the interface adapts.
|
||||
@@ -1,12 +1,12 @@
|
||||
# Add Trigger
|
||||
|
||||
You are an expert at creating webhook triggers for Sim. You understand the trigger system, the generic `buildTriggerSubBlocks` helper, and how triggers connect to blocks.
|
||||
You are an expert at creating webhook and polling triggers for Sim. You understand the trigger system, the generic `buildTriggerSubBlocks` helper, polling infrastructure, and how triggers connect to blocks.
|
||||
|
||||
## Your Task
|
||||
|
||||
1. Research what webhook events the service supports
|
||||
2. Create the trigger files using the generic builder
|
||||
3. Create a provider handler if custom auth, formatting, or subscriptions are needed
|
||||
1. Research what webhook events the service supports — if the service lacks reliable webhooks, use polling
|
||||
2. Create the trigger files using the generic builder (webhook) or manual config (polling)
|
||||
3. Create a provider handler (webhook) or polling handler (polling)
|
||||
4. Register triggers and connect them to the block
|
||||
|
||||
## Directory Structure
|
||||
@@ -141,23 +141,37 @@ export const TRIGGER_REGISTRY: TriggerRegistry = {
|
||||
|
||||
### Block file (`apps/sim/blocks/blocks/{service}.ts`)
|
||||
|
||||
Wire triggers into the block so the trigger UI appears and `generate-docs.ts` discovers them. Two changes are needed:
|
||||
|
||||
1. **Spread trigger subBlocks** at the end of the block's `subBlocks` array
|
||||
2. **Add `triggers` property** after `outputs` with `enabled: true` and `available: [...]`
|
||||
|
||||
```typescript
|
||||
import { getTrigger } from '@/triggers'
|
||||
|
||||
export const {Service}Block: BlockConfig = {
|
||||
// ...
|
||||
triggers: {
|
||||
enabled: true,
|
||||
available: ['{service}_event_a', '{service}_event_b'],
|
||||
},
|
||||
subBlocks: [
|
||||
// Regular tool subBlocks first...
|
||||
...getTrigger('{service}_event_a').subBlocks,
|
||||
...getTrigger('{service}_event_b').subBlocks,
|
||||
],
|
||||
// ... tools, inputs, outputs ...
|
||||
triggers: {
|
||||
enabled: true,
|
||||
available: ['{service}_event_a', '{service}_event_b'],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
**Versioned blocks (V1 + V2):** Many integrations have a hidden V1 block and a visible V2 block. Where you add the trigger wiring depends on how V2 inherits from V1:
|
||||
|
||||
- **V2 uses `...V1Block` spread** (e.g., Google Calendar): Add trigger to V1 — V2 inherits both `subBlocks` and `triggers` automatically.
|
||||
- **V2 defines its own `subBlocks`** (e.g., Google Sheets): Add trigger to V2 (the visible block). V1 is hidden and doesn't need it.
|
||||
- **Single block, no V2** (e.g., Google Drive): Add trigger directly.
|
||||
|
||||
`generate-docs.ts` deduplicates by base type (first match wins). If V1 is processed first without triggers, the V2 triggers won't appear in `integrations.json`. Always verify by checking the output after running the script.
|
||||
|
||||
## Provider Handler
|
||||
|
||||
All provider-specific webhook logic lives in a single handler file: `apps/sim/lib/webhooks/providers/{service}.ts`.
|
||||
@@ -322,6 +336,121 @@ export function buildOutputs(): Record<string, TriggerOutput> {
|
||||
}
|
||||
```
|
||||
|
||||
## Polling Triggers
|
||||
|
||||
Use polling when the service lacks reliable webhooks (e.g., Google Sheets, Google Drive, Google Calendar, Gmail, RSS, IMAP). Polling triggers do NOT use `buildTriggerSubBlocks` — they define subBlocks manually.
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
apps/sim/triggers/{service}/
|
||||
├── index.ts # Barrel export
|
||||
└── poller.ts # TriggerConfig with polling: true
|
||||
|
||||
apps/sim/lib/webhooks/polling/
|
||||
└── {service}.ts # PollingProviderHandler implementation
|
||||
```
|
||||
|
||||
### Polling Handler (`apps/sim/lib/webhooks/polling/{service}.ts`)
|
||||
|
||||
```typescript
|
||||
import { pollingIdempotency } from '@/lib/core/idempotency/service'
|
||||
import type { PollingProviderHandler, PollWebhookContext } from '@/lib/webhooks/polling/types'
|
||||
import { markWebhookFailed, markWebhookSuccess, resolveOAuthCredential, updateWebhookProviderConfig } from '@/lib/webhooks/polling/utils'
|
||||
import { processPolledWebhookEvent } from '@/lib/webhooks/processor'
|
||||
|
||||
export const {service}PollingHandler: PollingProviderHandler = {
|
||||
provider: '{service}',
|
||||
label: '{Service}',
|
||||
|
||||
async pollWebhook(ctx: PollWebhookContext): Promise<'success' | 'failure'> {
|
||||
const { webhookData, workflowData, requestId, logger } = ctx
|
||||
const webhookId = webhookData.id
|
||||
|
||||
try {
|
||||
// For OAuth services:
|
||||
const accessToken = await resolveOAuthCredential(webhookData, '{service}', requestId, logger)
|
||||
const config = webhookData.providerConfig as unknown as {Service}WebhookConfig
|
||||
|
||||
// First poll: seed state, emit nothing
|
||||
if (!config.lastCheckedTimestamp) {
|
||||
await updateWebhookProviderConfig(webhookId, { lastCheckedTimestamp: new Date().toISOString() }, logger)
|
||||
await markWebhookSuccess(webhookId, logger)
|
||||
return 'success'
|
||||
}
|
||||
|
||||
// Fetch changes since last poll, process with idempotency
|
||||
// ...
|
||||
|
||||
await markWebhookSuccess(webhookId, logger)
|
||||
return 'success'
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error processing {service} webhook ${webhookId}:`, error)
|
||||
await markWebhookFailed(webhookId, logger)
|
||||
return 'failure'
|
||||
}
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
**Key patterns:**
|
||||
- First poll seeds state and emits nothing (avoids flooding with existing data)
|
||||
- Use `pollingIdempotency.executeWithIdempotency(provider, key, callback)` for dedup
|
||||
- Use `processPolledWebhookEvent(webhookData, workflowData, payload, requestId)` to fire the workflow
|
||||
- Use `updateWebhookProviderConfig(webhookId, partialConfig, logger)` for read-merge-write on state
|
||||
- Use the latest server-side timestamp from API responses (not wall clock) to avoid clock skew
|
||||
|
||||
### Trigger Config (`apps/sim/triggers/{service}/poller.ts`)
|
||||
|
||||
```typescript
|
||||
import { {Service}Icon } from '@/components/icons'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
export const {service}PollingTrigger: TriggerConfig = {
|
||||
id: '{service}_poller',
|
||||
name: '{Service} Trigger',
|
||||
provider: '{service}',
|
||||
description: 'Triggers when ...',
|
||||
version: '1.0.0',
|
||||
icon: {Service}Icon,
|
||||
polling: true, // REQUIRED — routes to polling infrastructure
|
||||
|
||||
subBlocks: [
|
||||
{ id: 'triggerCredentials', type: 'oauth-input', title: 'Credentials', serviceId: '{service}', requiredScopes: [], required: true, mode: 'trigger', supportsCredentialSets: true },
|
||||
// ... service-specific config fields (dropdowns, inputs, switches) ...
|
||||
{ id: 'triggerInstructions', type: 'text', title: 'Setup Instructions', hideFromPreview: true, mode: 'trigger', defaultValue: '...' },
|
||||
],
|
||||
|
||||
outputs: {
|
||||
// Must match the payload shape from processPolledWebhookEvent
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Registration (3 places)
|
||||
|
||||
1. **`apps/sim/triggers/constants.ts`** — add provider to `POLLING_PROVIDERS` Set
|
||||
2. **`apps/sim/lib/webhooks/polling/registry.ts`** — import handler, add to `POLLING_HANDLERS`
|
||||
3. **`apps/sim/triggers/registry.ts`** — import trigger config, add to `TRIGGER_REGISTRY`
|
||||
|
||||
### Helm Cron Job
|
||||
|
||||
Add to `helm/sim/values.yaml` under the existing polling cron jobs:
|
||||
|
||||
```yaml
|
||||
{service}WebhookPoll:
|
||||
schedule: "*/1 * * * *"
|
||||
concurrencyPolicy: Forbid
|
||||
url: "http://sim:3000/api/webhooks/poll/{service}"
|
||||
```
|
||||
|
||||
### Reference Implementations
|
||||
|
||||
- Simple: `apps/sim/lib/webhooks/polling/rss.ts` + `apps/sim/triggers/rss/poller.ts`
|
||||
- Complex (OAuth, attachments): `apps/sim/lib/webhooks/polling/gmail.ts` + `apps/sim/triggers/gmail/poller.ts`
|
||||
- Cursor-based (changes API): `apps/sim/lib/webhooks/polling/google-drive.ts`
|
||||
- Timestamp-based: `apps/sim/lib/webhooks/polling/google-calendar.ts`
|
||||
|
||||
## Checklist
|
||||
|
||||
### Trigger Definition
|
||||
@@ -347,7 +476,17 @@ export function buildOutputs(): Record<string, TriggerOutput> {
|
||||
- [ ] NO changes to `route.ts`, `provider-subscriptions.ts`, or `deploy.ts`
|
||||
- [ ] API key field uses `password: true`
|
||||
|
||||
### Polling Trigger (if applicable)
|
||||
- [ ] Handler implements `PollingProviderHandler` at `lib/webhooks/polling/{service}.ts`
|
||||
- [ ] Trigger config has `polling: true` and defines subBlocks manually (no `buildTriggerSubBlocks`)
|
||||
- [ ] Provider string matches across: trigger config, handler, `POLLING_PROVIDERS`, polling registry
|
||||
- [ ] First poll seeds state and emits nothing
|
||||
- [ ] Added provider to `POLLING_PROVIDERS` in `triggers/constants.ts`
|
||||
- [ ] Added handler to `POLLING_HANDLERS` in `lib/webhooks/polling/registry.ts`
|
||||
- [ ] Added cron job to `helm/sim/values.yaml`
|
||||
- [ ] Payload shape matches trigger `outputs` schema
|
||||
|
||||
### Testing
|
||||
- [ ] `bun run type-check` passes
|
||||
- [ ] Manually verify `formatInput` output keys match trigger `outputs` keys
|
||||
- [ ] Manually verify output keys match trigger `outputs` keys
|
||||
- [ ] Trigger UI shows correctly in the block
|
||||
|
||||
76
.cursor/rules/constitution.mdc
Normal file
76
.cursor/rules/constitution.mdc
Normal file
@@ -0,0 +1,76 @@
|
||||
---
|
||||
description: Sim product language, positioning, and tone guidelines
|
||||
globs: ["apps/sim/app/(landing)/**", "apps/sim/app/(home)/**", "apps/docs/**", "apps/sim/app/manifest.ts", "apps/sim/app/sitemap.ts", "apps/sim/app/robots.ts", "apps/sim/app/llms.txt/**", "apps/sim/app/llms-full.txt/**", "apps/sim/app/(landing)/**/structured-data*", "apps/docs/**/structured-data*", "**/metadata*", "**/seo*"]
|
||||
---
|
||||
|
||||
# Sim — Language & Positioning
|
||||
|
||||
When editing user-facing copy (landing pages, docs, metadata, marketing), follow these rules.
|
||||
|
||||
## Identity
|
||||
|
||||
Sim is the **AI workspace** where teams build and run AI agents. Not a workflow tool, not an agent framework, not an automation platform.
|
||||
|
||||
**Short definition:** Sim is the open-source AI workspace where teams build, deploy, and manage AI agents.
|
||||
|
||||
**Full definition:** Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM to create agents that automate real work — visually, conversationally, or with code.
|
||||
|
||||
## Audience
|
||||
|
||||
**Primary:** Teams building AI agents for their organization — IT, operations, and technical teams who need governance, security, lifecycle management, and collaboration.
|
||||
|
||||
**Secondary:** Individual builders and developers who care about speed, flexibility, and open source.
|
||||
|
||||
## Required Language
|
||||
|
||||
| Concept | Use | Never use |
|
||||
|---------|-----|-----------|
|
||||
| The product | "AI workspace" | "workflow tool", "automation platform", "agent framework" |
|
||||
| Building | "build agents", "create agents" | "create workflows" (unless describing the workflow module specifically) |
|
||||
| Visual builder | "workflow builder" or "visual builder" | "canvas", "graph editor" |
|
||||
| Mothership | "Mothership" (capitalized) | "chat", "AI assistant", "copilot" |
|
||||
| Deployment | "deploy", "ship" | "publish", "activate" |
|
||||
| Audience | "teams", "builders" | "users", "customers" (in marketing copy) |
|
||||
| What agents do | "automate real work" | "automate tasks", "automate workflows" |
|
||||
| Our advantage | "open-source AI workspace" | "open-source platform" |
|
||||
|
||||
## Tone
|
||||
|
||||
- **Direct.** Short sentences. Active voice. Lead with what it does.
|
||||
- **Concrete.** Name specific things — "Slack bots, compliance agents, data pipelines" — not abstractions.
|
||||
- **Confident, not loud.** No exclamation marks or superlatives.
|
||||
- **Simple.** If a 16-year-old can't understand the sentence, rewrite it.
|
||||
|
||||
## Claim Hierarchy
|
||||
|
||||
When describing Sim, always lead with the most differentiated claim:
|
||||
|
||||
1. **What it is:** "The AI workspace for teams"
|
||||
2. **What you do:** "Build, deploy, and manage AI agents"
|
||||
3. **How:** "Visually, conversationally, or with code"
|
||||
4. **Scale:** "1,000+ integrations, every major LLM"
|
||||
5. **Trust:** "Open source. SOC2. Trusted by 100,000+ builders."
|
||||
|
||||
## Module Descriptions
|
||||
|
||||
| Module | One-liner |
|
||||
|--------|-----------|
|
||||
| **Mothership** | Your AI command center. Build and manage everything in natural language. |
|
||||
| **Workflows** | The visual builder. Connect blocks, models, and integrations into agent logic. |
|
||||
| **Knowledge Base** | Your agents' memory. Upload docs, sync sources, build vector databases. |
|
||||
| **Tables** | A database, built in. Store, query, and wire structured data into agent runs. |
|
||||
| **Files** | Upload, create, and share. One store for your team and every agent. |
|
||||
| **Logs** | Full visibility, every run. Trace execution block by block. |
|
||||
|
||||
## What We Never Say
|
||||
|
||||
- Never call Sim "just a workflow tool"
|
||||
- Never compare only on integration count — we win on AI-native capabilities
|
||||
- Never use "no-code" as the primary descriptor — say "visually, conversationally, or with code"
|
||||
- Never promise unshipped features
|
||||
- Never use jargon ("RAG", "vector database", "MCP") without plain-English explanation on public pages
|
||||
- Avoid "agentic workforce" as a primary term — use "AI agents"
|
||||
|
||||
## Vision
|
||||
|
||||
Sim becomes the default environment where teams build AI agents — not a tool you visit for one task, but a workspace you live in. Workflows are one module; Mothership is another. The workspace is the constant; the interface adapts.
|
||||
@@ -280,12 +280,12 @@ export async function generateMetadata(props: {
|
||||
title: data.title,
|
||||
description:
|
||||
data.description ||
|
||||
'Documentation for Sim — the open-source platform to build AI agents and run your agentic workforce.',
|
||||
'Documentation for Sim — the open-source AI workspace where teams build, deploy, and manage AI agents.',
|
||||
keywords: [
|
||||
'AI agents',
|
||||
'agentic workforce',
|
||||
'AI agent platform',
|
||||
'agentic workflows',
|
||||
'AI workspace',
|
||||
'AI agent builder',
|
||||
'build AI agents',
|
||||
'LLM orchestration',
|
||||
'AI automation',
|
||||
'knowledge base',
|
||||
@@ -300,7 +300,7 @@ export async function generateMetadata(props: {
|
||||
title: data.title,
|
||||
description:
|
||||
data.description ||
|
||||
'Documentation for Sim — the open-source platform to build AI agents and run your agentic workforce.',
|
||||
'Documentation for Sim — the open-source AI workspace where teams build, deploy, and manage AI agents.',
|
||||
url: fullUrl,
|
||||
siteName: 'Sim Documentation',
|
||||
type: 'article',
|
||||
@@ -322,7 +322,7 @@ export async function generateMetadata(props: {
|
||||
title: data.title,
|
||||
description:
|
||||
data.description ||
|
||||
'Documentation for Sim — the open-source platform to build AI agents and run your agentic workforce.',
|
||||
'Documentation for Sim — the open-source AI workspace where teams build, deploy, and manage AI agents.',
|
||||
images: [ogImageUrl],
|
||||
creator: '@simdotai',
|
||||
site: '@simdotai',
|
||||
|
||||
@@ -66,7 +66,7 @@ export default async function Layout({ children, params }: LayoutProps) {
|
||||
'@type': 'WebSite',
|
||||
name: 'Sim Documentation',
|
||||
description:
|
||||
'Documentation for Sim — the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows.',
|
||||
'Documentation for Sim — the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM.',
|
||||
url: 'https://docs.sim.ai',
|
||||
publisher: {
|
||||
'@type': 'Organization',
|
||||
|
||||
@@ -14,29 +14,27 @@ export const viewport: Viewport = {
|
||||
export const metadata = {
|
||||
metadataBase: new URL('https://docs.sim.ai'),
|
||||
title: {
|
||||
default: 'Sim Documentation — Build AI Agents & Run Your Agentic Workforce',
|
||||
default: 'Sim Documentation — The AI Workspace for Teams',
|
||||
template: '%s | Sim Docs',
|
||||
},
|
||||
description:
|
||||
'Documentation for Sim — the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows.',
|
||||
'Documentation for Sim — the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM.',
|
||||
applicationName: 'Sim Docs',
|
||||
generator: 'Next.js',
|
||||
referrer: 'origin-when-cross-origin' as const,
|
||||
keywords: [
|
||||
'AI workspace',
|
||||
'AI agent builder',
|
||||
'AI agents',
|
||||
'agentic workforce',
|
||||
'AI agent platform',
|
||||
'build AI agents',
|
||||
'open-source AI agents',
|
||||
'agentic workflows',
|
||||
'LLM orchestration',
|
||||
'AI integrations',
|
||||
'knowledge base',
|
||||
'AI automation',
|
||||
'workflow builder',
|
||||
'AI workflow orchestration',
|
||||
'visual workflow builder',
|
||||
'enterprise AI',
|
||||
'AI agent deployment',
|
||||
'intelligent automation',
|
||||
'AI tools',
|
||||
],
|
||||
authors: [{ name: 'Sim Team', url: 'https://sim.ai' }],
|
||||
@@ -65,9 +63,9 @@ export const metadata = {
|
||||
alternateLocale: ['es_ES', 'fr_FR', 'de_DE', 'ja_JP', 'zh_CN'],
|
||||
url: 'https://docs.sim.ai',
|
||||
siteName: 'Sim Documentation',
|
||||
title: 'Sim Documentation — Build AI Agents & Run Your Agentic Workforce',
|
||||
title: 'Sim Documentation — The AI Workspace for Teams',
|
||||
description:
|
||||
'Documentation for Sim — the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows.',
|
||||
'Documentation for Sim — the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM.',
|
||||
images: [
|
||||
{
|
||||
url: 'https://docs.sim.ai/api/og?title=Sim%20Documentation',
|
||||
@@ -79,9 +77,9 @@ export const metadata = {
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: 'Sim Documentation — Build AI Agents & Run Your Agentic Workforce',
|
||||
title: 'Sim Documentation — The AI Workspace for Teams',
|
||||
description:
|
||||
'Documentation for Sim — the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows.',
|
||||
'Documentation for Sim — the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM.',
|
||||
creator: '@simdotai',
|
||||
site: '@simdotai',
|
||||
images: ['https://docs.sim.ai/api/og?title=Sim%20Documentation'],
|
||||
|
||||
@@ -37,9 +37,9 @@ export async function GET() {
|
||||
|
||||
const manifest = `# Sim Documentation
|
||||
|
||||
> The open-source platform to build AI agents and run your agentic workforce.
|
||||
> The open-source AI workspace where teams build, deploy, and manage AI agents.
|
||||
|
||||
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.
|
||||
Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM to create agents that automate real work — visually, conversationally, or with code. Trusted by over 100,000 builders.
|
||||
|
||||
## Documentation Overview
|
||||
|
||||
|
||||
@@ -70,10 +70,11 @@ export function StructuredData({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'SoftwareApplication',
|
||||
name: 'Sim',
|
||||
applicationCategory: 'DeveloperApplication',
|
||||
applicationCategory: 'BusinessApplication',
|
||||
applicationSubCategory: 'AI Workspace',
|
||||
operatingSystem: 'Any',
|
||||
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.',
|
||||
'Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM to create agents that automate real work.',
|
||||
url: baseUrl,
|
||||
author: {
|
||||
'@type': 'Organization',
|
||||
@@ -84,8 +85,9 @@ export function StructuredData({
|
||||
category: 'Developer Tools',
|
||||
},
|
||||
featureList: [
|
||||
'AI agent creation',
|
||||
'Agentic workflow orchestration',
|
||||
'AI workspace for teams',
|
||||
'Mothership — natural language agent creation',
|
||||
'Visual workflow builder',
|
||||
'1,000+ integrations',
|
||||
'LLM orchestration (OpenAI, Anthropic, Google, xAI, Mistral, Perplexity)',
|
||||
'Knowledge base creation',
|
||||
|
||||
@@ -21,7 +21,17 @@ Verwenden Sie Ihre eigenen API-Schlüssel für KI-Modellanbieter anstelle der ge
|
||||
| OpenAI | Knowledge Base-Embeddings, Agent-Block |
|
||||
| Anthropic | Agent-Block |
|
||||
| Google | Agent-Block |
|
||||
| Mistral | Knowledge Base OCR |
|
||||
| Mistral | Knowledge Base OCR, Agent-Block |
|
||||
| Fireworks | Agent-Block |
|
||||
| Firecrawl | Web-Scraping, Crawling, Suche und Extraktion |
|
||||
| Exa | KI-gestützte Suche und Recherche |
|
||||
| Serper | Google-Such-API |
|
||||
| Linkup | Websuche und Inhaltsabruf |
|
||||
| Parallel AI | Websuche, Extraktion und tiefgehende Recherche |
|
||||
| Perplexity | KI-gestützter Chat und Websuche |
|
||||
| Jina AI | Web-Lesen und Suche |
|
||||
| Google Cloud | Translate, Maps, PageSpeed und Books APIs |
|
||||
| Brandfetch | Marken-Assets, Logos, Farben und Unternehmensinformationen |
|
||||
|
||||
### Einrichtung
|
||||
|
||||
|
||||
@@ -105,9 +105,108 @@ Die Modellaufschlüsselung zeigt:
|
||||
Die angezeigten Preise entsprechen den Tarifen vom 10. September 2025. Überprüfen Sie die Dokumentation der Anbieter für aktuelle Preise.
|
||||
</Callout>
|
||||
|
||||
## Gehostete Tool-Preise
|
||||
|
||||
Wenn Workflows Tool-Blöcke mit den gehosteten API-Schlüsseln von Sim verwenden, werden die Kosten pro Operation berechnet. Verwenden Sie Ihre eigenen Schlüssel über BYOK, um direkt an die Anbieter zu zahlen.
|
||||
|
||||
<Tabs items={['Firecrawl', 'Exa', 'Serper', 'Perplexity', 'Linkup', 'Parallel AI', 'Jina AI', 'Google Cloud', 'Brandfetch']}>
|
||||
<Tab>
|
||||
**Firecrawl** - Web-Scraping, Crawling, Suche und Extraktion
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Scrape | $0.001 per credit used |
|
||||
| Crawl | $0.001 per credit used |
|
||||
| Search | $0.001 per credit used |
|
||||
| Extract | $0.001 per credit used |
|
||||
| Map | $0.001 per credit used |
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Exa** - KI-gestützte Suche und Recherche
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Search | Dynamic (returned by API) |
|
||||
| Get Contents | Dynamic (returned by API) |
|
||||
| Find Similar Links | Dynamic (returned by API) |
|
||||
| Answer | Dynamic (returned by API) |
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Serper** - Google-Such-API
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Search (≤10 results) | $0.001 |
|
||||
| Search (>10 results) | $0.002 |
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Perplexity** - KI-gestützter Chat und Websuche
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Search | $0.005 per request |
|
||||
| Chat | Token-based (varies by model) |
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Linkup** - Websuche und Inhaltsabruf
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Standard search | ~$0.006 |
|
||||
| Deep search | ~$0.055 |
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Parallel AI** - Websuche, Extraktion und tiefgehende Recherche
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Search (≤10 results) | $0.005 |
|
||||
| Search (>10 results) | $0.005 + $0.001 per additional result |
|
||||
| Extract | $0.001 per URL |
|
||||
| Deep Research | $0.005–$2.40 (varies by processor tier) |
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Jina AI** - Web-Lesen und Suche
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Read URL | $0.20 per 1M tokens |
|
||||
| Search | $0.20 per 1M tokens (minimum 10K tokens) |
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Google Cloud** - Translate, Maps, PageSpeed und Books APIs
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Translate / Detect | $0.00002 per character |
|
||||
| Maps (Geocode, Directions, Distance Matrix, Elevation, Timezone, Reverse Geocode, Geolocate, Validate Address) | $0.005 per request |
|
||||
| Maps (Snap to Roads) | $0.01 per request |
|
||||
| Maps (Place Details) | $0.017 per request |
|
||||
| Maps (Places Search) | $0.032 per request |
|
||||
| PageSpeed | Free |
|
||||
| Books (Search, Details) | Free |
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Brandfetch** - Marken-Assets, Logos, Farben und Unternehmensinformationen
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Search | Free |
|
||||
| Get Brand | $0.04 per request |
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Bring Your Own Key (BYOK)
|
||||
|
||||
Sie können Ihre eigenen API-Schlüssel für gehostete Modelle (OpenAI, Anthropic, Google, Mistral) unter **Einstellungen → BYOK** verwenden, um Basispreise zu zahlen. Schlüssel werden verschlüsselt und gelten arbeitsbereichsweit.
|
||||
Sie können Ihre eigenen API-Schlüssel für unterstützte Anbieter (OpenAI, Anthropic, Google, Mistral, Fireworks, Firecrawl, Exa, Serper, Linkup, Parallel AI, Perplexity, Jina AI, Google Cloud, Brandfetch) unter **Einstellungen → BYOK** verwenden, um Basispreise zu zahlen. Schlüssel werden verschlüsselt und gelten arbeitsbereichsweit.
|
||||
|
||||
## Strategien zur Kostenoptimierung
|
||||
|
||||
|
||||
@@ -69,6 +69,9 @@ For self-hosted deployments, enterprise features can be enabled via environment
|
||||
| `ACCESS_CONTROL_ENABLED`, `NEXT_PUBLIC_ACCESS_CONTROL_ENABLED` | Permission groups for access restrictions |
|
||||
| `SSO_ENABLED`, `NEXT_PUBLIC_SSO_ENABLED` | Single Sign-On with SAML/OIDC |
|
||||
| `CREDENTIAL_SETS_ENABLED`, `NEXT_PUBLIC_CREDENTIAL_SETS_ENABLED` | Polling Groups for email triggers |
|
||||
| `INBOX_ENABLED`, `NEXT_PUBLIC_INBOX_ENABLED` | Sim Mailer inbox for outbound email |
|
||||
| `WHITELABELING_ENABLED`, `NEXT_PUBLIC_WHITELABELING_ENABLED` | Custom branding and white-labeling |
|
||||
| `AUDIT_LOGS_ENABLED`, `NEXT_PUBLIC_AUDIT_LOGS_ENABLED` | Audit logging for compliance and monitoring |
|
||||
| `DISABLE_INVITATIONS`, `NEXT_PUBLIC_DISABLE_INVITATIONS` | Globally disable workspace/organization invitations |
|
||||
|
||||
### Organization Management
|
||||
|
||||
@@ -110,9 +110,108 @@ The model breakdown shows:
|
||||
Pricing shown reflects rates as of September 10, 2025. Check provider documentation for current pricing.
|
||||
</Callout>
|
||||
|
||||
## Hosted Tool Pricing
|
||||
|
||||
When workflows use tool blocks with Sim's hosted API keys, costs are charged per operation. Use your own keys via BYOK to pay providers directly instead.
|
||||
|
||||
<Tabs items={['Firecrawl', 'Exa', 'Serper', 'Perplexity', 'Linkup', 'Parallel AI', 'Jina AI', 'Google Cloud', 'Brandfetch']}>
|
||||
<Tab>
|
||||
**Firecrawl** - Web scraping, crawling, search, and extraction
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Scrape | $0.001 per credit used |
|
||||
| Crawl | $0.001 per credit used |
|
||||
| Search | $0.001 per credit used |
|
||||
| Extract | $0.001 per credit used |
|
||||
| Map | $0.001 per credit used |
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Exa** - AI-powered search and research
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Search | Dynamic (returned by API) |
|
||||
| Get Contents | Dynamic (returned by API) |
|
||||
| Find Similar Links | Dynamic (returned by API) |
|
||||
| Answer | Dynamic (returned by API) |
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Serper** - Google search API
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Search (≤10 results) | $0.001 |
|
||||
| Search (>10 results) | $0.002 |
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Perplexity** - AI-powered chat and web search
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Search | $0.005 per request |
|
||||
| Chat | Token-based (varies by model) |
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Linkup** - Web search and content retrieval
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Standard search | ~$0.006 |
|
||||
| Deep search | ~$0.055 |
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Parallel AI** - Web search, extraction, and deep research
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Search (≤10 results) | $0.005 |
|
||||
| Search (>10 results) | $0.005 + $0.001 per additional result |
|
||||
| Extract | $0.001 per URL |
|
||||
| Deep Research | $0.005–$2.40 (varies by processor tier) |
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Jina AI** - Web reading and search
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Read URL | $0.20 per 1M tokens |
|
||||
| Search | $0.20 per 1M tokens (minimum 10K tokens) |
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Google Cloud** - Translate, Maps, PageSpeed, and Books APIs
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Translate / Detect | $0.00002 per character |
|
||||
| Maps (Geocode, Directions, Distance Matrix, Elevation, Timezone, Reverse Geocode, Geolocate, Validate Address) | $0.005 per request |
|
||||
| Maps (Snap to Roads) | $0.01 per request |
|
||||
| Maps (Place Details) | $0.017 per request |
|
||||
| Maps (Places Search) | $0.032 per request |
|
||||
| PageSpeed | Free |
|
||||
| Books (Search, Details) | Free |
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Brandfetch** - Brand assets, logos, colors, and company info
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Search | Free |
|
||||
| Get Brand | $0.04 per request |
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Bring Your Own Key (BYOK)
|
||||
|
||||
Use your own API keys for AI model providers instead of Sim's hosted keys to pay base prices with no markup.
|
||||
Use your own API keys for supported providers instead of Sim's hosted keys to pay base prices with no markup.
|
||||
|
||||
### Supported Providers
|
||||
|
||||
@@ -121,7 +220,17 @@ Use your own API keys for AI model providers instead of Sim's hosted keys to pay
|
||||
| OpenAI | Knowledge Base embeddings, Agent block |
|
||||
| Anthropic | Agent block |
|
||||
| Google | Agent block |
|
||||
| Mistral | Knowledge Base OCR |
|
||||
| Mistral | Knowledge Base OCR, Agent block |
|
||||
| Fireworks | Agent block |
|
||||
| Firecrawl | Web scraping, crawling, search, and extraction |
|
||||
| Exa | AI-powered search and research |
|
||||
| Serper | Google search API |
|
||||
| Linkup | Web search and content retrieval |
|
||||
| Parallel AI | Web search, extraction, and deep research |
|
||||
| Perplexity | AI-powered chat and web search |
|
||||
| Jina AI | Web reading and search |
|
||||
| Google Cloud | Translate, Maps, PageSpeed, and Books APIs |
|
||||
| Brandfetch | Brand assets, logos, colors, and company info |
|
||||
|
||||
### Setup
|
||||
|
||||
@@ -152,20 +261,20 @@ Each voice session is billed when it starts. In deployed chat voice mode, each c
|
||||
|
||||
## Plans
|
||||
|
||||
Sim has two paid plan tiers — **Pro** and **Max**. Either can be used individually or with a team. Team plans pool credits across all seats in the organization.
|
||||
Sim has two paid plan tiers - **Pro** and **Max**. Either can be used individually or with a team. Team plans pool credits across all seats in the organization.
|
||||
|
||||
| Plan | Price | Credits Included | Daily Refresh |
|
||||
|------|-------|------------------|---------------|
|
||||
| **Community** | $0 | 1,000 (one-time) | — |
|
||||
| **Community** | $0 | 1,000 (one-time) | - |
|
||||
| **Pro** | $25/mo | 6,000/mo | +50/day |
|
||||
| **Max** | $100/mo | 25,000/mo | +200/day |
|
||||
| **Enterprise** | Custom | Custom | — |
|
||||
| **Enterprise** | Custom | Custom | - |
|
||||
|
||||
To use Pro or Max with a team, select **Get For Team** in subscription settings and choose the tier and number of seats. Credits are pooled across the organization at the per-seat rate (e.g. Max for Teams with 3 seats = 75,000 credits/mo pooled).
|
||||
|
||||
### Daily Refresh Credits
|
||||
|
||||
Paid plans include a small daily credit allowance that does not count toward your plan limit. Each day, usage up to the daily refresh amount is excluded from billable usage. This allowance resets every 24 hours and does not carry over — use it or lose it.
|
||||
Paid plans include a small daily credit allowance that does not count toward your plan limit. Each day, usage up to the daily refresh amount is excluded from billable usage. This allowance resets every 24 hours and does not carry over - use it or lose it.
|
||||
|
||||
| Plan | Daily Refresh |
|
||||
|------|---------------|
|
||||
@@ -252,7 +361,7 @@ Sim uses a **base subscription + overage** billing model:
|
||||
|
||||
### How It Works
|
||||
|
||||
**Pro Plan ($25/month — 6,000 credits):**
|
||||
**Pro Plan ($25/month - 6,000 credits):**
|
||||
- Monthly subscription includes 6,000 credits of usage
|
||||
- Usage under 6,000 credits → No additional charges
|
||||
- Usage over 6,000 credits (with on-demand enabled) → Pay the overage at month end
|
||||
|
||||
@@ -170,17 +170,17 @@ Build, test, and refine workflows quickly with immediate feedback
|
||||
## Next Steps
|
||||
|
||||
<Cards>
|
||||
<Card title="Explore Workflow Blocks" href="/blocks">
|
||||
Discover API, Function, Condition, and other workflow blocks
|
||||
<Card title="Explore Blocks" href="/blocks">
|
||||
Discover API, Function, Condition, and other blocks
|
||||
</Card>
|
||||
<Card title="Browse Integrations" href="/tools">
|
||||
Connect 160+ services including Gmail, Slack, Notion, and more
|
||||
Connect 1,000+ services including Gmail, Slack, Notion, and more
|
||||
</Card>
|
||||
<Card title="Add Custom Logic" href="/blocks/function">
|
||||
Write custom functions for advanced data processing
|
||||
</Card>
|
||||
<Card title="Deploy Your Workflow" href="/execution">
|
||||
Make your workflow accessible via REST API or webhooks
|
||||
<Card title="Deploy Your Agent" href="/execution">
|
||||
Make your agent accessible via REST API or webhooks
|
||||
</Card>
|
||||
</Cards>
|
||||
|
||||
@@ -188,7 +188,7 @@ Build, test, and refine workflows quickly with immediate feedback
|
||||
|
||||
**Need detailed explanations?** Visit the [Blocks documentation](/blocks) for comprehensive guides on each component.
|
||||
|
||||
**Looking for integrations?** Explore the [Tools documentation](/tools) to see all 160+ available integrations.
|
||||
**Looking for integrations?** Explore the [Tools documentation](/tools) to see all 1,000+ available integrations.
|
||||
|
||||
**Ready to go live?** Learn about [Execution and Deployment](/execution) to make your workflows production-ready.
|
||||
|
||||
@@ -199,5 +199,5 @@ Build, test, and refine workflows quickly with immediate feedback
|
||||
{ question: "Can I use a different AI model instead of GPT-4o?", answer: "Yes. The Agent block supports models from OpenAI, Anthropic, Google, Groq, Cerebras, DeepSeek, Mistral, xAI, and more. You can select any available model from the dropdown. If you self-host, you can also use local models through Ollama." },
|
||||
{ question: "Can I import workflows from other tools?", answer: "Sim does not currently support importing workflows from other automation platforms. However, you can use the Copilot feature to describe what you want in natural language and have it build the workflow for you, which is often faster than manual recreation." },
|
||||
{ question: "What if my workflow does not produce the expected output?", answer: "Use the Chat panel to test iteratively and inspect outputs from each block. You can click the dropdown to view different block outputs and pinpoint where the issue is. The execution logs (accessible from the Logs tab) show detailed information about each step including token usage, costs, and any errors." },
|
||||
{ question: "Where do I go after completing this tutorial?", answer: "Explore the Blocks documentation to learn about Condition, Router, Function, and API blocks. Browse the Tools section to discover 160+ integrations you can add to your agents. When you are ready to deploy, check the Execution docs for REST API, webhook, and scheduled trigger options." },
|
||||
{ question: "Where do I go after completing this tutorial?", answer: "Explore the Blocks documentation to learn about Condition, Router, Function, and API blocks. Browse the Tools section to discover 1,000+ integrations you can add to your agents. When you are ready to deploy, check the Execution docs for REST API, webhook, and scheduled trigger options." },
|
||||
]} />
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Card, Cards } from 'fumadocs-ui/components/card'
|
||||
|
||||
# Sim Documentation
|
||||
|
||||
Welcome to Sim, a visual workflow builder for AI applications. Build powerful AI agents, automation workflows, and data processing pipelines by connecting blocks on a canvas.
|
||||
Welcome to Sim, the open-source AI workspace where teams build, deploy, and manage AI agents. Create agents visually with the workflow builder, conversationally through Mothership, or programmatically with the API — connected to 1,000+ integrations and every major LLM.
|
||||
|
||||
## Quick Start
|
||||
|
||||
@@ -15,13 +15,13 @@ Welcome to Sim, a visual workflow builder for AI applications. Build powerful AI
|
||||
Learn what you can build with Sim
|
||||
</Card>
|
||||
<Card title="Getting Started" href="/getting-started">
|
||||
Create your first workflow in 10 minutes
|
||||
Build your first agent in 10 minutes
|
||||
</Card>
|
||||
<Card title="Workflow Blocks" href="/blocks">
|
||||
<Card title="Blocks" href="/blocks">
|
||||
Learn about the building blocks
|
||||
</Card>
|
||||
<Card title="Tools & Integrations" href="/tools">
|
||||
Explore 80+ built-in integrations
|
||||
Explore 1,000+ integrations
|
||||
</Card>
|
||||
</Cards>
|
||||
|
||||
@@ -35,10 +35,10 @@ Welcome to Sim, a visual workflow builder for AI applications. Build powerful AI
|
||||
Work with workflow and environment variables
|
||||
</Card>
|
||||
<Card title="Execution" href="/execution">
|
||||
Monitor workflow runs and manage costs
|
||||
Monitor agent runs and manage costs
|
||||
</Card>
|
||||
<Card title="Triggers" href="/triggers">
|
||||
Start workflows via API, webhooks, or schedules
|
||||
Start agents via API, webhooks, or schedules
|
||||
</Card>
|
||||
</Cards>
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Image } from '@/components/ui/image'
|
||||
import { Video } from '@/components/ui/video'
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
Sim is an open-source visual workflow builder for building and deploying AI agent workflows. Design intelligent automation systems using a no-code interface—connect AI models, databases, APIs, and business tools through an intuitive drag-and-drop canvas. Whether you're building chatbots, automating business processes, or orchestrating complex data pipelines, Sim provides the tools to bring your AI workflows to life.
|
||||
Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Create agents visually with the workflow builder, conversationally through Mothership, or programmatically with the API. Connect AI models, databases, APIs, and 1,000+ business tools to build agents that automate real work — from chatbots and compliance agents to data pipelines and ITSM automation.
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
@@ -40,8 +40,8 @@ Orchestrate complex multi-service interactions. Create unified API endpoints, im
|
||||
|
||||
## How It Works
|
||||
|
||||
**Visual Workflow Editor**
|
||||
Design workflows using an intuitive drag-and-drop canvas. Connect AI models, databases, APIs, and third-party services through a visual, no-code interface that makes complex automation logic easy to understand and maintain.
|
||||
**Visual Workflow Builder**
|
||||
Design agent logic using an intuitive drag-and-drop canvas. Connect AI models, databases, APIs, and third-party services through a visual interface that makes complex automation easy to understand and maintain.
|
||||
|
||||
**Modular Block System**
|
||||
Build with specialized components: processing blocks (AI agents, API calls, custom functions), logic blocks (conditional branching, loops, routers), and output blocks (responses, evaluators). Each block handles a specific task in your workflow.
|
||||
@@ -58,7 +58,7 @@ Enable your team to build together. Multiple users can edit workflows simultaneo
|
||||
|
||||
## Integrations
|
||||
|
||||
Sim provides native integrations with 160+ services across multiple categories:
|
||||
Sim provides native integrations with 1,000+ services across multiple categories:
|
||||
|
||||
- **AI Models**: OpenAI, Anthropic, Google Gemini, Groq, Cerebras, local models via Ollama or VLLM
|
||||
- **Communication**: Gmail, Slack, Microsoft Teams, Telegram, WhatsApp
|
||||
@@ -100,17 +100,17 @@ Deploy on your own infrastructure using Docker Compose or Kubernetes. Maintain c
|
||||
|
||||
## Next Steps
|
||||
|
||||
Ready to build your first AI workflow?
|
||||
Ready to build your first AI agent?
|
||||
|
||||
<Cards>
|
||||
<Card title="Getting Started" href="/getting-started">
|
||||
Create your first workflow in 10 minutes
|
||||
Build your first agent in 10 minutes
|
||||
</Card>
|
||||
<Card title="Workflow Blocks" href="/blocks">
|
||||
<Card title="Blocks" href="/blocks">
|
||||
Learn about the building blocks
|
||||
</Card>
|
||||
<Card title="Tools & Integrations" href="/tools">
|
||||
Explore 160+ built-in integrations
|
||||
Explore 1,000+ integrations
|
||||
</Card>
|
||||
<Card title="Team Permissions" href="/permissions/roles-and-permissions">
|
||||
Set up workspace roles and permissions
|
||||
@@ -121,9 +121,9 @@ Ready to build your first AI workflow?
|
||||
{ question: "Is Sim free to use?", answer: "Sim offers a free Community plan with 1,000 one-time credits to get started. Paid plans start at $25/month (Pro) with 5,000 credits and go up to $100/month (Max) with 20,000 credits. Annual billing is available at a 15% discount. You can also self-host Sim for free on your own infrastructure." },
|
||||
{ question: "Is Sim open source?", answer: "Yes. Sim is open source under the Apache 2.0 license. The full source code is available on GitHub and you can self-host it, contribute to development, or modify it for your own needs. Enterprise features (SSO, access control) have a separate license that requires a subscription for production use." },
|
||||
{ question: "Which AI models and providers are supported?", answer: "Sim supports 15+ providers including OpenAI, Anthropic, Google Gemini, Groq, Cerebras, DeepSeek, Mistral, xAI, and OpenRouter. You can also run local models through Ollama or VLLM at no API cost. Bring Your Own Key (BYOK) is supported so you can use your own API keys at base provider pricing with no markup." },
|
||||
{ question: "Do I need coding experience to use Sim?", answer: "No. Sim is a no-code visual builder where you design workflows by dragging blocks onto a canvas and connecting them. For advanced use cases, the Function block lets you write custom JavaScript, but it is entirely optional." },
|
||||
{ question: "Do I need coding experience to use Sim?", answer: "No. Sim lets you build agents visually by dragging blocks onto a canvas and connecting them, or conversationally through Mothership using natural language. For advanced use cases, the Function block lets you write custom JavaScript, and the full API/SDK is available for programmatic access." },
|
||||
{ question: "Can I self-host Sim?", answer: "Yes. Sim provides Docker Compose configurations for self-hosted deployments. The stack includes the Sim application, a PostgreSQL database with pgvector, and a realtime collaboration server. You can also integrate local AI models via Ollama for a fully offline setup." },
|
||||
{ question: "Is there a limit on how many workflows I can create?", answer: "There is no limit on the number of workflows you can create on any plan. Usage limits apply to execution credits, rate limits, and file storage, which vary by plan tier." },
|
||||
{ question: "What integrations are available?", answer: "Sim offers 160+ native integrations across categories including AI models, communication tools (Gmail, Slack, Teams, Telegram), productivity apps (Notion, Google Workspace, Airtable), development tools (GitHub, Jira, Linear), search services (Google Search, Perplexity, Exa), and databases (PostgreSQL, Supabase, Pinecone). For anything not built in, you can use the MCP (Model Context Protocol) support to connect custom services." },
|
||||
{ question: "How does Sim compare to other workflow automation tools?", answer: "Sim is purpose-built for AI agent workflows rather than general task automation. It provides a visual canvas for orchestrating LLM-powered agents with built-in support for tool use, structured outputs, conditional branching, and real-time collaboration. The Copilot feature also lets you build and modify workflows using natural language." },
|
||||
{ question: "What integrations are available?", answer: "Sim offers 1,000+ native integrations across categories including AI models, communication tools (Gmail, Slack, Teams, Telegram), productivity apps (Notion, Google Workspace, Airtable), development tools (GitHub, Jira, Linear), search services (Google Search, Perplexity, Exa), and databases (PostgreSQL, Supabase, Pinecone). For anything not built in, you can use the MCP (Model Context Protocol) support to connect custom services." },
|
||||
{ question: "How does Sim compare to other AI agent builders?", answer: "Sim is an AI workspace — not just a workflow tool or an agent framework. It combines a visual workflow builder, Mothership for natural-language agent creation, knowledge bases, tables, and full observability in one environment. Teams build agents visually, conversationally, or with code, then deploy and manage them with enterprise governance, real-time collaboration, and staging-to-production workflows." },
|
||||
]} />
|
||||
|
||||
@@ -21,7 +21,17 @@ Usa tus propias claves API para proveedores de modelos de IA en lugar de las cla
|
||||
| OpenAI | Embeddings de base de conocimiento, bloque Agent |
|
||||
| Anthropic | Bloque Agent |
|
||||
| Google | Bloque Agent |
|
||||
| Mistral | OCR de base de conocimiento |
|
||||
| Mistral | OCR de base de conocimiento, bloque Agent |
|
||||
| Fireworks | Bloque Agent |
|
||||
| Firecrawl | Web scraping, crawling, búsqueda y extracción |
|
||||
| Exa | Búsqueda e investigación impulsada por IA |
|
||||
| Serper | API de búsqueda de Google |
|
||||
| Linkup | Búsqueda web y recuperación de contenido |
|
||||
| Parallel AI | Búsqueda web, extracción e investigación profunda |
|
||||
| Perplexity | Chat y búsqueda web impulsada por IA |
|
||||
| Jina AI | Lectura y búsqueda web |
|
||||
| Google Cloud | APIs de Translate, Maps, PageSpeed y Books |
|
||||
| Brandfetch | Activos de marca, logos, colores e información de empresas |
|
||||
|
||||
### Configuración
|
||||
|
||||
|
||||
@@ -105,9 +105,108 @@ El desglose del modelo muestra:
|
||||
Los precios mostrados reflejan las tarifas a partir del 10 de septiembre de 2025. Consulta la documentación del proveedor para conocer los precios actuales.
|
||||
</Callout>
|
||||
|
||||
## Precios de herramientas alojadas
|
||||
|
||||
Cuando los flujos de trabajo usan bloques de herramientas con las claves API alojadas de Sim, los costos se cobran por operación. Usa tus propias claves a través de BYOK para pagar directamente a los proveedores.
|
||||
|
||||
<Tabs items={['Firecrawl', 'Exa', 'Serper', 'Perplexity', 'Linkup', 'Parallel AI', 'Jina AI', 'Google Cloud', 'Brandfetch']}>
|
||||
<Tab>
|
||||
**Firecrawl** - Web scraping, crawling, búsqueda y extracción
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Scrape | $0.001 per credit used |
|
||||
| Crawl | $0.001 per credit used |
|
||||
| Search | $0.001 per credit used |
|
||||
| Extract | $0.001 per credit used |
|
||||
| Map | $0.001 per credit used |
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Exa** - Búsqueda e investigación impulsada por IA
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Search | Dynamic (returned by API) |
|
||||
| Get Contents | Dynamic (returned by API) |
|
||||
| Find Similar Links | Dynamic (returned by API) |
|
||||
| Answer | Dynamic (returned by API) |
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Serper** - API de búsqueda de Google
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Search (≤10 results) | $0.001 |
|
||||
| Search (>10 results) | $0.002 |
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Perplexity** - Chat y búsqueda web impulsada por IA
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Search | $0.005 per request |
|
||||
| Chat | Token-based (varies by model) |
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Linkup** - Búsqueda web y recuperación de contenido
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Standard search | ~$0.006 |
|
||||
| Deep search | ~$0.055 |
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Parallel AI** - Búsqueda web, extracción e investigación profunda
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Search (≤10 results) | $0.005 |
|
||||
| Search (>10 results) | $0.005 + $0.001 per additional result |
|
||||
| Extract | $0.001 per URL |
|
||||
| Deep Research | $0.005–$2.40 (varies by processor tier) |
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Jina AI** - Lectura y búsqueda web
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Read URL | $0.20 per 1M tokens |
|
||||
| Search | $0.20 per 1M tokens (minimum 10K tokens) |
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Google Cloud** - APIs de Translate, Maps, PageSpeed y Books
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Translate / Detect | $0.00002 per character |
|
||||
| Maps (Geocode, Directions, Distance Matrix, Elevation, Timezone, Reverse Geocode, Geolocate, Validate Address) | $0.005 per request |
|
||||
| Maps (Snap to Roads) | $0.01 per request |
|
||||
| Maps (Place Details) | $0.017 per request |
|
||||
| Maps (Places Search) | $0.032 per request |
|
||||
| PageSpeed | Free |
|
||||
| Books (Search, Details) | Free |
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Brandfetch** - Activos de marca, logos, colores e información de empresas
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Search | Free |
|
||||
| Get Brand | $0.04 per request |
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Trae tu propia clave (BYOK)
|
||||
|
||||
Puedes usar tus propias claves API para modelos alojados (OpenAI, Anthropic, Google, Mistral) en **Configuración → BYOK** para pagar precios base. Las claves están encriptadas y se aplican a todo el espacio de trabajo.
|
||||
Puedes usar tus propias claves API para proveedores compatibles (OpenAI, Anthropic, Google, Mistral, Fireworks, Firecrawl, Exa, Serper, Linkup, Parallel AI, Perplexity, Jina AI, Google Cloud, Brandfetch) en **Configuración → BYOK** para pagar precios base. Las claves están encriptadas y se aplican a todo el espacio de trabajo.
|
||||
|
||||
## Estrategias de optimización de costos
|
||||
|
||||
|
||||
@@ -21,7 +21,17 @@ Utilisez vos propres clés API pour les fournisseurs de modèles IA au lieu des
|
||||
| OpenAI | Embeddings de base de connaissances, bloc Agent |
|
||||
| Anthropic | Bloc Agent |
|
||||
| Google | Bloc Agent |
|
||||
| Mistral | OCR de base de connaissances |
|
||||
| Mistral | OCR de base de connaissances, bloc Agent |
|
||||
| Fireworks | Bloc Agent |
|
||||
| Firecrawl | Web scraping, crawling, recherche et extraction |
|
||||
| Exa | Recherche et investigation alimentées par l'IA |
|
||||
| Serper | API de recherche Google |
|
||||
| Linkup | Recherche web et récupération de contenu |
|
||||
| Parallel AI | Recherche web, extraction et recherche approfondie |
|
||||
| Perplexity | Chat et recherche web alimentés par l'IA |
|
||||
| Jina AI | Lecture et recherche web |
|
||||
| Google Cloud | APIs Translate, Maps, PageSpeed et Books |
|
||||
| Brandfetch | Ressources de marque, logos, couleurs et informations d'entreprise |
|
||||
|
||||
### Configuration
|
||||
|
||||
|
||||
@@ -105,9 +105,108 @@ La répartition des modèles montre :
|
||||
Les prix indiqués reflètent les tarifs en date du 10 septembre 2025. Consultez la documentation des fournisseurs pour les tarifs actuels.
|
||||
</Callout>
|
||||
|
||||
## Tarification des outils hébergés
|
||||
|
||||
Lorsque les workflows utilisent des blocs d'outils avec les clés API hébergées par Sim, les coûts sont facturés par opération. Utilisez vos propres clés via BYOK pour payer directement les fournisseurs.
|
||||
|
||||
<Tabs items={['Firecrawl', 'Exa', 'Serper', 'Perplexity', 'Linkup', 'Parallel AI', 'Jina AI', 'Google Cloud', 'Brandfetch']}>
|
||||
<Tab>
|
||||
**Firecrawl** - Web scraping, crawling, recherche et extraction
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Scrape | $0.001 per credit used |
|
||||
| Crawl | $0.001 per credit used |
|
||||
| Search | $0.001 per credit used |
|
||||
| Extract | $0.001 per credit used |
|
||||
| Map | $0.001 per credit used |
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Exa** - Recherche et investigation alimentées par l'IA
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Search | Dynamic (returned by API) |
|
||||
| Get Contents | Dynamic (returned by API) |
|
||||
| Find Similar Links | Dynamic (returned by API) |
|
||||
| Answer | Dynamic (returned by API) |
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Serper** - API de recherche Google
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Search (≤10 results) | $0.001 |
|
||||
| Search (>10 results) | $0.002 |
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Perplexity** - Chat et recherche web alimentés par l'IA
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Search | $0.005 per request |
|
||||
| Chat | Token-based (varies by model) |
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Linkup** - Recherche web et récupération de contenu
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Standard search | ~$0.006 |
|
||||
| Deep search | ~$0.055 |
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Parallel AI** - Recherche web, extraction et recherche approfondie
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Search (≤10 results) | $0.005 |
|
||||
| Search (>10 results) | $0.005 + $0.001 per additional result |
|
||||
| Extract | $0.001 per URL |
|
||||
| Deep Research | $0.005–$2.40 (varies by processor tier) |
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Jina AI** - Lecture et recherche web
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Read URL | $0.20 per 1M tokens |
|
||||
| Search | $0.20 per 1M tokens (minimum 10K tokens) |
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Google Cloud** - APIs Translate, Maps, PageSpeed et Books
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Translate / Detect | $0.00002 per character |
|
||||
| Maps (Geocode, Directions, Distance Matrix, Elevation, Timezone, Reverse Geocode, Geolocate, Validate Address) | $0.005 per request |
|
||||
| Maps (Snap to Roads) | $0.01 per request |
|
||||
| Maps (Place Details) | $0.017 per request |
|
||||
| Maps (Places Search) | $0.032 per request |
|
||||
| PageSpeed | Free |
|
||||
| Books (Search, Details) | Free |
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Brandfetch** - Ressources de marque, logos, couleurs et informations d'entreprise
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Search | Free |
|
||||
| Get Brand | $0.04 per request |
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Apportez votre propre clé (BYOK)
|
||||
|
||||
Vous pouvez utiliser vos propres clés API pour les modèles hébergés (OpenAI, Anthropic, Google, Mistral) dans **Paramètres → BYOK** pour payer les prix de base. Les clés sont chiffrées et s'appliquent à l'ensemble de l'espace de travail.
|
||||
Vous pouvez utiliser vos propres clés API pour les fournisseurs pris en charge (OpenAI, Anthropic, Google, Mistral, Fireworks, Firecrawl, Exa, Serper, Linkup, Parallel AI, Perplexity, Jina AI, Google Cloud, Brandfetch) dans **Paramètres → BYOK** pour payer les prix de base. Les clés sont chiffrées et s'appliquent à l'ensemble de l'espace de travail.
|
||||
|
||||
## Stratégies d'optimisation des coûts
|
||||
|
||||
|
||||
@@ -20,7 +20,17 @@ Simのホストキーの代わりに、AIモデルプロバイダー用の独自
|
||||
| OpenAI | ナレッジベースの埋め込み、エージェントブロック |
|
||||
| Anthropic | エージェントブロック |
|
||||
| Google | エージェントブロック |
|
||||
| Mistral | ナレッジベースOCR |
|
||||
| Mistral | ナレッジベースOCR、エージェントブロック |
|
||||
| Fireworks | エージェントブロック |
|
||||
| Firecrawl | Webスクレイピング、クローリング、検索、抽出 |
|
||||
| Exa | AI搭載の検索とリサーチ |
|
||||
| Serper | Google検索API |
|
||||
| Linkup | Web検索とコンテンツ取得 |
|
||||
| Parallel AI | Web検索、抽出、ディープリサーチ |
|
||||
| Perplexity | AI搭載のチャットとWeb検索 |
|
||||
| Jina AI | Web閲覧と検索 |
|
||||
| Google Cloud | Translate、Maps、PageSpeed、Books API |
|
||||
| Brandfetch | ブランドアセット、ロゴ、カラー、企業情報 |
|
||||
|
||||
### セットアップ
|
||||
|
||||
|
||||
@@ -105,9 +105,108 @@ AIブロックを使用するワークフローでは、ログで詳細なコス
|
||||
表示価格は2025年9月10日時点のレートを反映しています。最新の価格については各プロバイダーのドキュメントをご確認ください。
|
||||
</Callout>
|
||||
|
||||
## ホスティングツールの料金
|
||||
|
||||
ワークフローがSimのホスティングAPIキーを使用するツールブロックを利用する場合、操作ごとに料金が発生します。BYOKで独自のキーを使用すると、プロバイダーに直接支払うことができます。
|
||||
|
||||
<Tabs items={['Firecrawl', 'Exa', 'Serper', 'Perplexity', 'Linkup', 'Parallel AI', 'Jina AI', 'Google Cloud', 'Brandfetch']}>
|
||||
<Tab>
|
||||
**Firecrawl** - Webスクレイピング、クローリング、検索、抽出
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Scrape | $0.001 per credit used |
|
||||
| Crawl | $0.001 per credit used |
|
||||
| Search | $0.001 per credit used |
|
||||
| Extract | $0.001 per credit used |
|
||||
| Map | $0.001 per credit used |
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Exa** - AI搭載の検索とリサーチ
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Search | Dynamic (returned by API) |
|
||||
| Get Contents | Dynamic (returned by API) |
|
||||
| Find Similar Links | Dynamic (returned by API) |
|
||||
| Answer | Dynamic (returned by API) |
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Serper** - Google検索API
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Search (≤10 results) | $0.001 |
|
||||
| Search (>10 results) | $0.002 |
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Perplexity** - AI搭載のチャットとWeb検索
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Search | $0.005 per request |
|
||||
| Chat | Token-based (varies by model) |
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Linkup** - Web検索とコンテンツ取得
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Standard search | ~$0.006 |
|
||||
| Deep search | ~$0.055 |
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Parallel AI** - Web検索、抽出、ディープリサーチ
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Search (≤10 results) | $0.005 |
|
||||
| Search (>10 results) | $0.005 + $0.001 per additional result |
|
||||
| Extract | $0.001 per URL |
|
||||
| Deep Research | $0.005–$2.40 (varies by processor tier) |
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Jina AI** - Web閲覧と検索
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Read URL | $0.20 per 1M tokens |
|
||||
| Search | $0.20 per 1M tokens (minimum 10K tokens) |
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Google Cloud** - Translate、Maps、PageSpeed、Books API
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Translate / Detect | $0.00002 per character |
|
||||
| Maps (Geocode, Directions, Distance Matrix, Elevation, Timezone, Reverse Geocode, Geolocate, Validate Address) | $0.005 per request |
|
||||
| Maps (Snap to Roads) | $0.01 per request |
|
||||
| Maps (Place Details) | $0.017 per request |
|
||||
| Maps (Places Search) | $0.032 per request |
|
||||
| PageSpeed | Free |
|
||||
| Books (Search, Details) | Free |
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Brandfetch** - ブランドアセット、ロゴ、カラー、企業情報
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Search | Free |
|
||||
| Get Brand | $0.04 per request |
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Bring Your Own Key (BYOK)
|
||||
|
||||
ホストされたモデル(OpenAI、Anthropic、Google、Mistral)に対して、**設定 → BYOK**で独自のAPIキーを使用し、基本価格で支払うことができます。キーは暗号化され、ワークスペース全体に適用されます。
|
||||
対応プロバイダー(OpenAI、Anthropic、Google、Mistral、Fireworks、Firecrawl、Exa、Serper、Linkup、Parallel AI、Perplexity、Jina AI、Google Cloud、Brandfetch)に対して、**設定 → BYOK**で独自のAPIキーを使用し、基本価格で支払うことができます。キーは暗号化され、ワークスペース全体に適用されます。
|
||||
|
||||
## コスト最適化戦略
|
||||
|
||||
|
||||
@@ -20,7 +20,17 @@ Sim 企业版为需要更高安全性、合规性和管理能力的组织提供
|
||||
| OpenAI | 知识库嵌入、Agent 模块 |
|
||||
| Anthropic | Agent 模块 |
|
||||
| Google | Agent 模块 |
|
||||
| Mistral | 知识库 OCR |
|
||||
| Mistral | 知识库 OCR、Agent 模块 |
|
||||
| Fireworks | Agent 模块 |
|
||||
| Firecrawl | 网页抓取、爬取、搜索和提取 |
|
||||
| Exa | AI 驱动的搜索和研究 |
|
||||
| Serper | Google 搜索 API |
|
||||
| Linkup | 网络搜索和内容检索 |
|
||||
| Parallel AI | 网络搜索、提取和深度研究 |
|
||||
| Perplexity | AI 驱动的聊天和网络搜索 |
|
||||
| Jina AI | 网页阅读和搜索 |
|
||||
| Google Cloud | Translate、Maps、PageSpeed 和 Books API |
|
||||
| Brandfetch | 品牌资产、标志、颜色和公司信息 |
|
||||
|
||||
### 配置方法
|
||||
|
||||
|
||||
@@ -105,9 +105,108 @@ totalCost = baseExecutionCharge + modelCost
|
||||
显示的价格为截至 2025 年 9 月 10 日的费率。请查看提供商文档以获取最新价格。
|
||||
</Callout>
|
||||
|
||||
## 托管工具定价
|
||||
|
||||
当工作流使用 Sim 托管 API 密钥的工具模块时,费用按操作收取。通过 BYOK 使用你自己的密钥可直接向服务商付费。
|
||||
|
||||
<Tabs items={['Firecrawl', 'Exa', 'Serper', 'Perplexity', 'Linkup', 'Parallel AI', 'Jina AI', 'Google Cloud', 'Brandfetch']}>
|
||||
<Tab>
|
||||
**Firecrawl** - 网页抓取、爬取、搜索和提取
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Scrape | $0.001 per credit used |
|
||||
| Crawl | $0.001 per credit used |
|
||||
| Search | $0.001 per credit used |
|
||||
| Extract | $0.001 per credit used |
|
||||
| Map | $0.001 per credit used |
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Exa** - AI 驱动的搜索和研究
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Search | Dynamic (returned by API) |
|
||||
| Get Contents | Dynamic (returned by API) |
|
||||
| Find Similar Links | Dynamic (returned by API) |
|
||||
| Answer | Dynamic (returned by API) |
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Serper** - Google 搜索 API
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Search (≤10 results) | $0.001 |
|
||||
| Search (>10 results) | $0.002 |
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Perplexity** - AI 驱动的聊天和网络搜索
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Search | $0.005 per request |
|
||||
| Chat | Token-based (varies by model) |
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Linkup** - 网络搜索和内容检索
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Standard search | ~$0.006 |
|
||||
| Deep search | ~$0.055 |
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Parallel AI** - 网络搜索、提取和深度研究
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Search (≤10 results) | $0.005 |
|
||||
| Search (>10 results) | $0.005 + $0.001 per additional result |
|
||||
| Extract | $0.001 per URL |
|
||||
| Deep Research | $0.005–$2.40 (varies by processor tier) |
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Jina AI** - 网页阅读和搜索
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Read URL | $0.20 per 1M tokens |
|
||||
| Search | $0.20 per 1M tokens (minimum 10K tokens) |
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Google Cloud** - Translate、Maps、PageSpeed 和 Books API
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Translate / Detect | $0.00002 per character |
|
||||
| Maps (Geocode, Directions, Distance Matrix, Elevation, Timezone, Reverse Geocode, Geolocate, Validate Address) | $0.005 per request |
|
||||
| Maps (Snap to Roads) | $0.01 per request |
|
||||
| Maps (Place Details) | $0.017 per request |
|
||||
| Maps (Places Search) | $0.032 per request |
|
||||
| PageSpeed | Free |
|
||||
| Books (Search, Details) | Free |
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Brandfetch** - 品牌资产、标志、颜色和公司信息
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Search | Free |
|
||||
| Get Brand | $0.04 per request |
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## 自带密钥(BYOK)
|
||||
|
||||
你可以在 **设置 → BYOK** 中为托管模型(OpenAI、Anthropic、Google、Mistral)使用你自己的 API 密钥,以按基础价格计费。密钥会被加密,并在整个工作区范围内生效。
|
||||
你可以在 **设置 → BYOK** 中为支持的服务商(OpenAI、Anthropic、Google、Mistral、Fireworks、Firecrawl、Exa、Serper、Linkup、Parallel AI、Perplexity、Jina AI、Google Cloud、Brandfetch)使用你自己的 API 密钥,以按基础价格计费。密钥会被加密,并在整个工作区范围内生效。
|
||||
|
||||
## 成本优化策略
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "Sim Documentation — Build AI Agents & Run Your Agentic Workforce",
|
||||
"name": "Sim Documentation — The AI Workspace for Teams",
|
||||
"short_name": "Sim Docs",
|
||||
"description": "Documentation for Sim — the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows.",
|
||||
"description": "Documentation for Sim — the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM.",
|
||||
"start_url": "/",
|
||||
"scope": "/",
|
||||
"icons": [
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
# Sim Documentation
|
||||
|
||||
Sim is a visual workflow builder for AI applications that lets you build AI agent workflows visually. Create powerful AI agents, automation workflows, and data processing pipelines by connecting blocks on a canvas—no coding required.
|
||||
Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Create agents visually with the workflow builder, conversationally through Mothership, or programmatically with the API — connected to 1,000+ integrations and every major LLM.
|
||||
|
||||
## What is Sim?
|
||||
|
||||
Sim provides a complete ecosystem for AI workflow automation including:
|
||||
Sim provides a complete AI workspace including:
|
||||
- Mothership — natural language agent creation and workspace management
|
||||
- Visual workflow builder with drag-and-drop interface
|
||||
- AI agent creation and automation
|
||||
- 80+ built-in integrations (OpenAI, Slack, Gmail, GitHub, etc.)
|
||||
- 1,000+ built-in integrations (OpenAI, Anthropic, Slack, Gmail, GitHub, etc.)
|
||||
- Knowledge bases for retrieval-augmented generation
|
||||
- Built-in tables for structured data
|
||||
- Real-time team collaboration
|
||||
- Multiple deployment options (cloud-hosted or self-hosted)
|
||||
- Custom integrations via MCP protocol
|
||||
@@ -16,22 +18,22 @@ Sim provides a complete ecosystem for AI workflow automation including:
|
||||
|
||||
Here are the key areas covered in our documentation:
|
||||
|
||||
/introduction - Getting started with Sim visual workflow builder
|
||||
/getting-started - Quick start guide for building your first workflow
|
||||
/blocks - Understanding workflow blocks (AI agents, APIs, functions)
|
||||
/tools - 80+ built-in integrations and tools
|
||||
/introduction - Getting started with Sim AI workspace
|
||||
/getting-started - Quick start guide for building your first agent
|
||||
/blocks - Understanding blocks (AI agents, APIs, functions)
|
||||
/tools - 1,000+ integrations and tools
|
||||
/webhooks - Webhook triggers and handling
|
||||
/mcp - Custom integrations via MCP protocol
|
||||
/deployment - Cloud-hosted vs self-hosted deployment
|
||||
/permissions - Team collaboration and workspace management
|
||||
/collaboration - Real-time editing and team features
|
||||
/workflows - Building complex automation workflows
|
||||
/workflows - Building agent logic with the visual builder
|
||||
|
||||
## Technical Information
|
||||
|
||||
- Framework: Fumadocs (Next.js-based documentation platform)
|
||||
- Content: MDX files with interactive examples
|
||||
- Languages: English (primary), French, Chinese
|
||||
- Languages: English (primary), Spanish, French, German, Japanese, Chinese
|
||||
- Search: AI-powered search and assistance available
|
||||
|
||||
## Complete Documentation
|
||||
@@ -40,14 +42,10 @@ For the full documentation with all pages, examples, and interactive features, v
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- GitHub repository with workflow examples
|
||||
- GitHub repository with agent examples
|
||||
- Discord community for support and discussions
|
||||
- 80+ built-in integrations with detailed guides
|
||||
- 1,000+ built-in integrations with detailed guides
|
||||
- MCP protocol documentation for custom integrations
|
||||
- Self-hosting guides and Docker deployment
|
||||
|
||||
For the complete documentation with interactive examples and visual workflow builder guides, visit https://docs.sim.ai
|
||||
|
||||
---
|
||||
|
||||
Last updated: 2025-09-15
|
||||
For the complete documentation visit https://docs.sim.ai
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useRef, useState } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { Eye, EyeOff, Loader2 } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
@@ -20,6 +20,7 @@ import { validateCallbackUrl } from '@/lib/core/security/input-validation'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { quickValidateEmail } from '@/lib/messaging/email/validation'
|
||||
import { captureClientEvent } 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'
|
||||
@@ -113,6 +114,10 @@ export default function LoginPage({
|
||||
: null
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
captureClientEvent('login_page_viewed', {})
|
||||
}, [])
|
||||
|
||||
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newEmail = e.target.value
|
||||
setEmail(newEmail)
|
||||
|
||||
@@ -12,7 +12,7 @@ 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 { captureClientEvent, 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'
|
||||
@@ -71,15 +71,13 @@ const validateEmailField = (emailValue: string): string[] => {
|
||||
return errors
|
||||
}
|
||||
|
||||
function SignupFormContent({
|
||||
githubAvailable,
|
||||
googleAvailable,
|
||||
isProduction,
|
||||
}: {
|
||||
interface SignupFormProps {
|
||||
githubAvailable: boolean
|
||||
googleAvailable: boolean
|
||||
isProduction: boolean
|
||||
}) {
|
||||
}
|
||||
|
||||
function SignupFormContent({ githubAvailable, googleAvailable, isProduction }: SignupFormProps) {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const { refetch: refetchSession } = useSession()
|
||||
@@ -87,8 +85,8 @@ function SignupFormContent({
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
captureEvent(posthog, 'signup_page_viewed', {})
|
||||
}, [posthog])
|
||||
captureClientEvent('signup_page_viewed', {})
|
||||
}, [])
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const [password, setPassword] = useState('')
|
||||
const [passwordErrors, setPasswordErrors] = useState<string[]>([])
|
||||
@@ -243,8 +241,6 @@ function SignupFormContent({
|
||||
return
|
||||
}
|
||||
|
||||
const sanitizedName = trimmedName
|
||||
|
||||
let token: string | undefined
|
||||
const widget = turnstileRef.current
|
||||
if (turnstileSiteKey && widget) {
|
||||
@@ -267,7 +263,7 @@ function SignupFormContent({
|
||||
{
|
||||
email: emailValue,
|
||||
password: passwordValue,
|
||||
name: sanitizedName,
|
||||
name: trimmedName,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
@@ -629,11 +625,7 @@ export default function SignupPage({
|
||||
githubAvailable,
|
||||
googleAvailable,
|
||||
isProduction,
|
||||
}: {
|
||||
githubAvailable: boolean
|
||||
googleAvailable: boolean
|
||||
isProduction: boolean
|
||||
}) {
|
||||
}: SignupFormProps) {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={<div className='flex h-screen items-center justify-center'>Loading...</div>}
|
||||
|
||||
@@ -161,7 +161,7 @@ export default async function Page({ params }: { params: Promise<{ slug: string
|
||||
<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 className='line-clamp-2 text-[var(--landing-text-muted)] text-sm leading-[150%]'>
|
||||
{p.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@ export default async function StudioLayout({ children }: { children: React.React
|
||||
name: 'Sim',
|
||||
url: 'https://sim.ai',
|
||||
description:
|
||||
'Sim is an open-source platform for building, testing, and deploying AI agent workflows.',
|
||||
'Sim is the open-source AI workspace where teams build, deploy, and manage AI agents.',
|
||||
logo: 'https://sim.ai/logo/primary/small.png',
|
||||
sameAs: [
|
||||
'https://x.com/simdotai',
|
||||
|
||||
@@ -19,8 +19,8 @@ export async function generateMetadata({
|
||||
const title = titleParts.join(' — ')
|
||||
|
||||
const description = tag
|
||||
? `Sim blog posts tagged "${tag}" — insights and guides for building AI agent workflows.`
|
||||
: 'Announcements, insights, and guides for building AI agent workflows.'
|
||||
? `Sim blog posts tagged "${tag}" — insights and guides for building AI agents.`
|
||||
: 'Announcements, insights, and guides for building AI agents.'
|
||||
|
||||
const canonicalParams = new URLSearchParams()
|
||||
if (tag) canonicalParams.set('tag', tag)
|
||||
@@ -110,7 +110,7 @@ export default async function BlogIndex({
|
||||
<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'>
|
||||
<p className='max-w-[540px] font-[430] font-season text-[var(--landing-text-muted)] text-sm leading-[150%] tracking-[0.02em] lg:text-base'>
|
||||
Announcements, insights, and guides for building AI agent workflows.
|
||||
</p>
|
||||
</div>
|
||||
@@ -152,7 +152,7 @@ export default async function BlogIndex({
|
||||
<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 className='line-clamp-2 text-[var(--landing-text-muted)] text-sm leading-[150%]'>
|
||||
{p.description}
|
||||
</p>
|
||||
</div>
|
||||
@@ -191,7 +191,7 @@ export default async function BlogIndex({
|
||||
<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 className='line-clamp-2 text-[var(--landing-text-muted)] text-sm leading-[150%]'>
|
||||
{p.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
250
apps/sim/app/(landing)/components/auth-modal/auth-modal.tsx
Normal file
250
apps/sim/app/(landing)/components/auth-modal/auth-modal.tsx
Normal file
@@ -0,0 +1,250 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { Loader2, X } from 'lucide-react'
|
||||
import Image from 'next/image'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Modal, ModalClose, ModalContent, ModalTitle, ModalTrigger } from '@/components/emcn'
|
||||
import { GithubIcon, GoogleIcon } from '@/components/icons'
|
||||
import { client } from '@/lib/auth/auth-client'
|
||||
import { getEnv, isFalsy, isTruthy } from '@/lib/core/config/env'
|
||||
import { captureClientEvent } from '@/lib/posthog/client'
|
||||
import type { PostHogEventMap } from '@/lib/posthog/events'
|
||||
import { getBrandConfig } from '@/ee/whitelabeling'
|
||||
|
||||
const logger = createLogger('AuthModal')
|
||||
|
||||
type AuthView = 'login' | 'signup'
|
||||
|
||||
interface AuthModalProps {
|
||||
children: React.ReactNode
|
||||
defaultView?: AuthView
|
||||
source: PostHogEventMap['auth_modal_opened']['source']
|
||||
}
|
||||
|
||||
interface ProviderStatus {
|
||||
githubAvailable: boolean
|
||||
googleAvailable: boolean
|
||||
registrationDisabled: boolean
|
||||
}
|
||||
|
||||
let fetchPromise: Promise<ProviderStatus> | null = null
|
||||
|
||||
const FALLBACK_STATUS: ProviderStatus = {
|
||||
githubAvailable: false,
|
||||
googleAvailable: false,
|
||||
registrationDisabled: false,
|
||||
}
|
||||
|
||||
const SOCIAL_BTN =
|
||||
'relative flex h-[32px] w-full items-center justify-center rounded-[5px] border border-[var(--landing-border-strong)] text-[13.5px] text-[var(--landing-text)] transition-colors hover:bg-[var(--landing-bg-elevated)] disabled:cursor-not-allowed disabled:opacity-50'
|
||||
|
||||
function fetchProviderStatus(): Promise<ProviderStatus> {
|
||||
if (fetchPromise) return fetchPromise
|
||||
fetchPromise = fetch('/api/auth/providers')
|
||||
.then((r) => {
|
||||
if (!r.ok) throw new Error(`HTTP ${r.status}`)
|
||||
return r.json()
|
||||
})
|
||||
.then(({ githubAvailable, googleAvailable, registrationDisabled }: ProviderStatus) => ({
|
||||
githubAvailable,
|
||||
googleAvailable,
|
||||
registrationDisabled,
|
||||
}))
|
||||
.catch(() => {
|
||||
fetchPromise = null
|
||||
return FALLBACK_STATUS
|
||||
})
|
||||
return fetchPromise
|
||||
}
|
||||
|
||||
export function AuthModal({ children, defaultView = 'login', source }: AuthModalProps) {
|
||||
const router = useRouter()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [view, setView] = useState<AuthView>(defaultView)
|
||||
const [providerStatus, setProviderStatus] = useState<ProviderStatus | null>(null)
|
||||
const [socialLoading, setSocialLoading] = useState<'github' | 'google' | null>(null)
|
||||
const brand = useMemo(() => getBrandConfig(), [])
|
||||
|
||||
useEffect(() => {
|
||||
fetchProviderStatus().then(setProviderStatus)
|
||||
}, [])
|
||||
|
||||
const hasSocial = providerStatus?.githubAvailable || providerStatus?.googleAvailable
|
||||
const ssoEnabled = isTruthy(getEnv('NEXT_PUBLIC_SSO_ENABLED'))
|
||||
const emailEnabled = !isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED'))
|
||||
const hasModalContent = hasSocial || ssoEnabled
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !providerStatus) return
|
||||
if (!hasModalContent) {
|
||||
setOpen(false)
|
||||
router.push(defaultView === 'login' ? '/login' : '/signup')
|
||||
return
|
||||
}
|
||||
if (providerStatus.registrationDisabled && view === 'signup') {
|
||||
setView('login')
|
||||
}
|
||||
}, [open, providerStatus, hasModalContent, defaultView, router, view])
|
||||
|
||||
const handleOpenChange = useCallback(
|
||||
(nextOpen: boolean) => {
|
||||
if (nextOpen && providerStatus && !hasModalContent) {
|
||||
router.push(defaultView === 'login' ? '/login' : '/signup')
|
||||
return
|
||||
}
|
||||
setOpen(nextOpen)
|
||||
if (nextOpen) {
|
||||
const initialView =
|
||||
defaultView === 'signup' && providerStatus?.registrationDisabled ? 'login' : defaultView
|
||||
setView(initialView)
|
||||
captureClientEvent('auth_modal_opened', { view: initialView, source })
|
||||
}
|
||||
},
|
||||
[defaultView, hasModalContent, providerStatus, router, source]
|
||||
)
|
||||
|
||||
const handleSocialLogin = useCallback(async (provider: 'github' | 'google') => {
|
||||
setSocialLoading(provider)
|
||||
try {
|
||||
await client.signIn.social({ provider, callbackURL: '/workspace' })
|
||||
} catch (error) {
|
||||
logger.warn('Social sign-in did not complete', { provider, error })
|
||||
} finally {
|
||||
setSocialLoading(null)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleSSOLogin = useCallback(() => {
|
||||
setOpen(false)
|
||||
router.push('/sso')
|
||||
}, [router])
|
||||
|
||||
const handleEmailContinue = useCallback(() => {
|
||||
setOpen(false)
|
||||
router.push(view === 'login' ? '/login' : '/signup')
|
||||
}, [router, view])
|
||||
|
||||
return (
|
||||
<Modal open={open} onOpenChange={handleOpenChange}>
|
||||
<ModalTrigger asChild>{children}</ModalTrigger>
|
||||
<ModalContent
|
||||
size='sm'
|
||||
className='dark bg-[var(--landing-bg)] font-[430] font-season text-[var(--landing-text)]'
|
||||
>
|
||||
<ModalTitle className='sr-only'>
|
||||
{view === 'login' ? 'Log in' : 'Create account'}
|
||||
</ModalTitle>
|
||||
|
||||
<div className='relative px-6 pt-6 pb-6'>
|
||||
<ModalClose className='absolute top-6 right-6 rounded-sm opacity-70 transition-opacity hover:opacity-100'>
|
||||
<X className='h-5 w-5 text-[var(--landing-text-muted)]' />
|
||||
<span className='sr-only'>Close</span>
|
||||
</ModalClose>
|
||||
|
||||
{!providerStatus ? (
|
||||
<div className='flex items-center justify-center py-16'>
|
||||
<Loader2 className='h-5 w-5 animate-spin text-[var(--landing-text-muted)]' />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className='flex flex-col items-start gap-6 pe-10'>
|
||||
<Image
|
||||
src={brand.logoUrl || '/logo/sim-landing.svg'}
|
||||
alt={brand.name}
|
||||
width={71}
|
||||
height={22}
|
||||
unoptimized
|
||||
className='h-[22px] w-auto shrink-0 object-contain'
|
||||
/>
|
||||
<div className='flex flex-col gap-1 text-left'>
|
||||
<p className='text-[22px] text-[color-mix(in_srgb,var(--landing-text-subtle)_60%,transparent)] leading-[125%] tracking-[0.02em]'>
|
||||
Start building.
|
||||
</p>
|
||||
<h2 className='text-[22px] text-white leading-[110%] tracking-[-0.02em]'>
|
||||
{view === 'login' ? 'Log in to continue' : 'Create free account'}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mt-6 space-y-3'>
|
||||
{providerStatus.googleAvailable && (
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => handleSocialLogin('google')}
|
||||
disabled={!!socialLoading}
|
||||
className={SOCIAL_BTN}
|
||||
>
|
||||
<GoogleIcon className='absolute left-4 h-[18px] w-[18px] shrink-0' />
|
||||
<span>
|
||||
{socialLoading === 'google' ? 'Connecting...' : 'Continue with Google'}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
{providerStatus.githubAvailable && (
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => handleSocialLogin('github')}
|
||||
disabled={!!socialLoading}
|
||||
className={SOCIAL_BTN}
|
||||
>
|
||||
<GithubIcon className='absolute left-4 h-[18px] w-[18px] shrink-0' />
|
||||
<span>
|
||||
{socialLoading === 'github' ? 'Connecting...' : 'Continue with GitHub'}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
{ssoEnabled && (
|
||||
<button type='button' onClick={handleSSOLogin} className={SOCIAL_BTN}>
|
||||
Sign in with SSO
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{emailEnabled && (
|
||||
<>
|
||||
<div className='relative my-4'>
|
||||
<div className='absolute inset-0 flex items-center'>
|
||||
<div className='w-full border-[var(--landing-bg-elevated)] border-t' />
|
||||
</div>
|
||||
<div className='relative flex justify-center text-[13.5px]'>
|
||||
<span className='bg-[var(--landing-bg)] px-4 text-[var(--landing-text-muted)]'>
|
||||
Or
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type='button'
|
||||
onClick={handleEmailContinue}
|
||||
className='flex h-[32px] w-full items-center justify-center rounded-[5px] border border-[var(--auth-primary-btn-border)] bg-[var(--auth-primary-btn-bg)] text-[13.5px] text-[var(--auth-primary-btn-text)] transition-colors hover:border-[var(--auth-primary-btn-hover-border)] hover:bg-[var(--auth-primary-btn-hover-bg)]'
|
||||
>
|
||||
Continue with email
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className='mt-4 text-center text-[13.5px]'>
|
||||
<span className='text-[var(--landing-text-muted)]'>
|
||||
{view === 'login' ? "Don't have an account? " : 'Already have an account? '}
|
||||
</span>
|
||||
{view === 'login' && providerStatus.registrationDisabled ? (
|
||||
<span className='text-[var(--landing-text-muted)]'>Registration is disabled</span>
|
||||
) : (
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => setView(view === 'login' ? 'signup' : 'login')}
|
||||
className='text-[var(--landing-text)] underline-offset-4 transition hover:text-white hover:underline'
|
||||
>
|
||||
{view === 'login' ? 'Sign up' : 'Sign in'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { Badge } from '@/components/emcn'
|
||||
import { AuthModal } from '@/app/(landing)/components/auth-modal/auth-modal'
|
||||
import { trackLandingCta } from '@/app/(landing)/landing-analytics'
|
||||
|
||||
interface DotGridProps {
|
||||
className?: string
|
||||
@@ -250,10 +252,10 @@ export default function 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.
|
||||
Sim supports real-time multiplayer collaboration. Teams build AI agents together in a
|
||||
shared workspace with live cursors, presence indicators, and concurrent editing.
|
||||
Features include role-based access control, shared agents and 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'>
|
||||
@@ -261,45 +263,54 @@ export default function Collaboration() {
|
||||
in real-time inside your workspace.
|
||||
</p>
|
||||
|
||||
<Link
|
||||
href='/signup'
|
||||
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
|
||||
<svg
|
||||
className='h-[10px] w-[10px] shrink-0'
|
||||
viewBox='0 0 10 10'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
<AuthModal defaultView='signup' source='collaboration'>
|
||||
<button
|
||||
type='button'
|
||||
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]'
|
||||
onClick={() =>
|
||||
trackLandingCta({
|
||||
label: 'Build together',
|
||||
section: 'collaboration',
|
||||
destination: 'auth_modal',
|
||||
})
|
||||
}
|
||||
>
|
||||
<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'
|
||||
Build together
|
||||
<svg
|
||||
className='h-[10px] w-[10px] shrink-0'
|
||||
viewBox='0 0 10 10'
|
||||
fill='none'
|
||||
className='transition-transform duration-200 ease-out group-hover/cta:translate-x-[30%]'
|
||||
/>
|
||||
</svg>
|
||||
</Link>
|
||||
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'
|
||||
className='transition-transform duration-200 ease-out group-hover/cta:translate-x-[30%]'
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</AuthModal>
|
||||
</div>
|
||||
|
||||
<figure className='pointer-events-none relative h-[220px] w-full md:h-[600px]'>
|
||||
<div className='md:-left-[18%] -top-[10%] absolute inset-y-0 left-[7%] min-w-full md:top-0'>
|
||||
<Image
|
||||
src='/landing/collaboration-visual.svg'
|
||||
alt='Collaboration visual showing team workflows with real-time editing, shared cursors, and version control interface'
|
||||
alt='Collaboration visual showing teams building AI agents together with real-time editing, shared cursors, and version control'
|
||||
width={876}
|
||||
height={480}
|
||||
className='h-full w-auto object-left md:min-w-[100vw]'
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
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 } from '@/components/emcn'
|
||||
import { AuthModal } from '@/app/(landing)/components/auth-modal/auth-modal'
|
||||
import { FeaturesPreview } from '@/app/(landing)/components/features/components/features-preview'
|
||||
import { trackLandingCta } from '@/app/(landing)/landing-analytics'
|
||||
|
||||
function hexToRgba(hex: string, alpha: number): string {
|
||||
const r = Number.parseInt(hex.slice(1, 3), 16)
|
||||
@@ -110,7 +111,7 @@ const FEATURE_TABS: FeatureTab[] = [
|
||||
},
|
||||
]
|
||||
|
||||
const HEADING_TEXT = 'Everything you need to build, deploy, and manage AI agents. '
|
||||
const HEADING_TEXT = 'One workspace to build, deploy, and manage AI agents. '
|
||||
const HEADING_LETTERS = HEADING_TEXT.split('')
|
||||
|
||||
const LETTER_REVEAL_SPAN = 0.85
|
||||
@@ -189,8 +190,7 @@ export default function Features() {
|
||||
</ScrollLetter>
|
||||
))}
|
||||
<span className='text-[color-mix(in_srgb,var(--landing-text-dark)_40%,transparent)]'>
|
||||
Design powerful workflows, connect your data, and monitor every run — all in one
|
||||
platform.
|
||||
Build agents, connect your data, and monitor every run — all in one workspace.
|
||||
</span>
|
||||
</h2>
|
||||
</div>
|
||||
@@ -265,12 +265,21 @@ export default function Features() {
|
||||
{FEATURE_TABS[activeTab].description}
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href='/signup'
|
||||
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}
|
||||
</Link>
|
||||
<AuthModal defaultView='signup' source='features'>
|
||||
<button
|
||||
type='button'
|
||||
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)]'
|
||||
onClick={() =>
|
||||
trackLandingCta({
|
||||
label: FEATURE_TABS[activeTab].cta,
|
||||
section: 'features',
|
||||
destination: 'auth_modal',
|
||||
})
|
||||
}
|
||||
>
|
||||
{FEATURE_TABS[activeTab].cta}
|
||||
</button>
|
||||
</AuthModal>
|
||||
</div>
|
||||
|
||||
<FeaturesPreview activeTab={activeTab} />
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { ArrowUp } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { captureClientEvent } from '@/lib/posthog/client'
|
||||
import { AuthModal } from '@/app/(landing)/components/auth-modal/auth-modal'
|
||||
import { useLandingSubmit } from '@/app/(landing)/components/landing-preview/components/landing-preview-panel/landing-preview-panel'
|
||||
import { trackLandingCta } from '@/app/(landing)/landing-analytics'
|
||||
import { useAnimatedPlaceholder } from '@/hooks/use-animated-placeholder'
|
||||
@@ -70,8 +71,8 @@ export function FooterCTA() {
|
||||
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)] 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` }}
|
||||
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] caret-white outline-none placeholder:font-[380] placeholder:text-[var(--landing-text-muted)] focus-visible:ring-0'
|
||||
style={{ maxHeight: `${MAX_HEIGHT}px` }}
|
||||
/>
|
||||
<div className='flex items-center justify-end'>
|
||||
<button
|
||||
@@ -79,11 +80,10 @@ export function FooterCTA() {
|
||||
onClick={handleSubmit}
|
||||
disabled={isEmpty}
|
||||
aria-label='Submit message'
|
||||
className='flex h-[28px] w-[28px] items-center justify-center rounded-full border-0 p-0 transition-colors'
|
||||
style={{
|
||||
background: isEmpty ? '#555555' : '#FFFFFF',
|
||||
cursor: isEmpty ? 'not-allowed' : 'pointer',
|
||||
}}
|
||||
className={cn(
|
||||
'flex h-[28px] w-[28px] items-center justify-center rounded-full border-0 p-0 transition-colors',
|
||||
isEmpty ? 'cursor-not-allowed bg-[#555555]' : 'cursor-pointer bg-white'
|
||||
)}
|
||||
>
|
||||
<ArrowUp size={16} strokeWidth={2.25} color={isEmpty ? '#888888' : '#1C1C1C'} />
|
||||
</button>
|
||||
@@ -96,7 +96,10 @@ export function FooterCTA() {
|
||||
href='https://docs.sim.ai'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className={`${CTA_BUTTON} border-[var(--landing-border-strong)] text-[var(--landing-text)] transition-colors hover:bg-[var(--landing-bg-elevated)]`}
|
||||
className={cn(
|
||||
CTA_BUTTON,
|
||||
'border-[var(--landing-border-strong)] text-[var(--landing-text)] transition-colors hover:bg-[var(--landing-bg-elevated)]'
|
||||
)}
|
||||
onClick={() =>
|
||||
trackLandingCta({
|
||||
label: 'Docs',
|
||||
@@ -107,15 +110,24 @@ export function FooterCTA() {
|
||||
>
|
||||
Docs
|
||||
</a>
|
||||
<Link
|
||||
href='/signup'
|
||||
className={`${CTA_BUTTON} gap-2 border-white bg-white text-black transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]`}
|
||||
onClick={() =>
|
||||
trackLandingCta({ label: 'Get started', section: 'footer_cta', destination: '/signup' })
|
||||
}
|
||||
>
|
||||
Get started
|
||||
</Link>
|
||||
<AuthModal defaultView='signup' source='footer_cta'>
|
||||
<button
|
||||
type='button'
|
||||
className={cn(
|
||||
CTA_BUTTON,
|
||||
'gap-2 border-white bg-white text-black transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
|
||||
)}
|
||||
onClick={() =>
|
||||
trackLandingCta({
|
||||
label: 'Get started',
|
||||
section: 'footer_cta',
|
||||
destination: 'auth_modal',
|
||||
})
|
||||
}
|
||||
>
|
||||
Get started
|
||||
</button>
|
||||
</AuthModal>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { FooterCTA } from '@/app/(landing)/components/footer/footer-cta'
|
||||
|
||||
const LINK_CLASS =
|
||||
@@ -9,25 +10,24 @@ interface FooterItem {
|
||||
label: string
|
||||
href: string
|
||||
external?: boolean
|
||||
arrow?: boolean
|
||||
externalArrow?: boolean
|
||||
}
|
||||
|
||||
const PRODUCT_LINKS: FooterItem[] = [
|
||||
{ label: 'Self Hosting', href: 'https://docs.sim.ai/self-hosting', external: true },
|
||||
{ label: 'MCP', href: 'https://docs.sim.ai/mcp', external: true },
|
||||
{ label: 'Mothership', href: 'https://docs.sim.ai', external: true },
|
||||
{ label: 'Workflows', href: 'https://docs.sim.ai', external: true },
|
||||
{ label: 'Knowledge Base', href: 'https://docs.sim.ai/knowledgebase', external: true },
|
||||
{ label: 'Tables', href: 'https://docs.sim.ai/tables', external: true },
|
||||
{ label: 'MCP', href: 'https://docs.sim.ai/mcp', external: true },
|
||||
{ label: 'API', href: 'https://docs.sim.ai/api-reference/getting-started', external: true },
|
||||
{ label: 'Self Hosting', href: 'https://docs.sim.ai/self-hosting', 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, externalArrow: true },
|
||||
{ label: 'Changelog', href: '/changelog' },
|
||||
@@ -47,7 +47,7 @@ const BLOCK_LINKS: FooterItem[] = [
|
||||
]
|
||||
|
||||
const INTEGRATION_LINKS: FooterItem[] = [
|
||||
{ label: 'All Integrations', href: '/integrations', arrow: true },
|
||||
{ label: 'All Integrations', href: '/integrations' },
|
||||
{ label: 'Confluence', href: 'https://docs.sim.ai/tools/confluence', external: true },
|
||||
{ label: 'Slack', href: 'https://docs.sim.ai/tools/slack', external: true },
|
||||
{ label: 'GitHub', href: 'https://docs.sim.ai/tools/github', external: true },
|
||||
@@ -63,10 +63,8 @@ const INTEGRATION_LINKS: FooterItem[] = [
|
||||
{ label: 'Linear', href: 'https://docs.sim.ai/tools/linear', external: true },
|
||||
{ label: 'Airtable', href: 'https://docs.sim.ai/tools/airtable', external: true },
|
||||
{ label: 'Firecrawl', href: 'https://docs.sim.ai/tools/firecrawl', external: true },
|
||||
{ label: 'Pinecone', href: 'https://docs.sim.ai/tools/pinecone', external: true },
|
||||
{ label: 'Discord', href: 'https://docs.sim.ai/tools/discord', external: true },
|
||||
{ label: 'Microsoft Teams', href: 'https://docs.sim.ai/tools/microsoft_teams', external: true },
|
||||
{ label: 'Outlook', href: 'https://docs.sim.ai/tools/outlook', external: true },
|
||||
{ label: 'Telegram', href: 'https://docs.sim.ai/tools/telegram', external: true },
|
||||
]
|
||||
|
||||
@@ -95,7 +93,7 @@ const LEGAL_LINKS: FooterItem[] = [
|
||||
function ChevronArrow({ external }: { external?: boolean }) {
|
||||
return (
|
||||
<svg
|
||||
className={`h-3 w-3 shrink-0${external ? ' -rotate-45' : ''}`}
|
||||
className={cn('h-3 w-3 shrink-0', external && '-rotate-45')}
|
||||
viewBox='0 0 10 10'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
@@ -128,26 +126,24 @@ function FooterColumn({ title, items }: { title: string; items: FooterItem[] })
|
||||
<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, arrow, externalArrow }) =>
|
||||
{items.map(({ label, href, external, externalArrow }) =>
|
||||
external ? (
|
||||
<a
|
||||
key={label}
|
||||
href={href}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className={`${LINK_CLASS}${externalArrow ? ' group/link inline-flex items-center gap-1' : ''}`}
|
||||
className={cn(
|
||||
LINK_CLASS,
|
||||
externalArrow && 'group/link inline-flex items-center gap-1'
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
{externalArrow && <ChevronArrow external />}
|
||||
</a>
|
||||
) : (
|
||||
<Link
|
||||
key={label}
|
||||
href={href}
|
||||
className={`${LINK_CLASS}${arrow ? ' group/link inline-flex items-center gap-1.5' : ''}`}
|
||||
>
|
||||
<Link key={label} href={href} className={LINK_CLASS}>
|
||||
{label}
|
||||
{arrow && <ChevronArrow />}
|
||||
</Link>
|
||||
)
|
||||
)}
|
||||
@@ -164,7 +160,10 @@ export default function Footer({ hideCTA }: FooterProps) {
|
||||
return (
|
||||
<footer
|
||||
role='contentinfo'
|
||||
className={`bg-[var(--landing-bg)] pb-10 font-[430] font-season text-sm${hideCTA ? ' pt-10' : ''}`}
|
||||
className={cn(
|
||||
'bg-[var(--landing-bg)] pb-10 font-[430] font-season text-sm',
|
||||
hideCTA && 'pt-10'
|
||||
)}
|
||||
>
|
||||
{!hideCTA && <FooterCTA />}
|
||||
<div className='relative px-[1.6vw] sm:px-8 lg:px-16'>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
'use client'
|
||||
|
||||
import dynamic from 'next/dynamic'
|
||||
import Link from 'next/link'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { AuthModal } from '@/app/(landing)/components/auth-modal/auth-modal'
|
||||
import { DemoRequestModal } from '@/app/(landing)/components/demo-request/demo-request-modal'
|
||||
import { trackLandingCta } from '@/app/(landing)/landing-analytics'
|
||||
|
||||
@@ -16,7 +17,6 @@ const LandingPreview = dynamic(
|
||||
}
|
||||
)
|
||||
|
||||
/** 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'
|
||||
|
||||
@@ -30,11 +30,11 @@ export default function Hero() {
|
||||
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.
|
||||
Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect
|
||||
1,000+ integrations and every major LLM — including OpenAI, Anthropic Claude, Google Gemini,
|
||||
Mistral, and xAI Grok — to create agents that automate real work. Build agents visually with
|
||||
the workflow builder, conversationally through Mothership, or programmatically with the API.
|
||||
Trusted by over 100,000 builders at startups and Fortune 500 companies. SOC2 compliant.
|
||||
</p>
|
||||
|
||||
<div className='relative z-10 flex flex-col items-center gap-3'>
|
||||
@@ -56,7 +56,10 @@ export default function Hero() {
|
||||
<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)]`}
|
||||
className={cn(
|
||||
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'
|
||||
onClick={() =>
|
||||
trackLandingCta({ label: 'Get a demo', section: 'hero', destination: 'demo_modal' })
|
||||
@@ -65,16 +68,25 @@ export default function Hero() {
|
||||
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'
|
||||
onClick={() =>
|
||||
trackLandingCta({ label: 'Get started', section: 'hero', destination: '/signup' })
|
||||
}
|
||||
>
|
||||
Get started
|
||||
</Link>
|
||||
<AuthModal defaultView='signup' source='hero'>
|
||||
<button
|
||||
type='button'
|
||||
className={cn(
|
||||
CTA_BASE,
|
||||
'gap-2 border-white bg-white text-black transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
|
||||
)}
|
||||
aria-label='Get started with Sim'
|
||||
onClick={() =>
|
||||
trackLandingCta({
|
||||
label: 'Get started',
|
||||
section: 'hero',
|
||||
destination: 'auth_modal',
|
||||
})
|
||||
}
|
||||
>
|
||||
Get started
|
||||
</button>
|
||||
</AuthModal>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { ChevronDown } from '@/components/emcn'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
|
||||
@@ -15,46 +16,67 @@ interface LandingFAQProps {
|
||||
|
||||
export function LandingFAQ({ faqs }: LandingFAQProps) {
|
||||
const [openIndex, setOpenIndex] = useState<number | null>(0)
|
||||
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null)
|
||||
|
||||
return (
|
||||
<div className='divide-y divide-[var(--landing-border)]'>
|
||||
<div>
|
||||
{faqs.map(({ question, answer }, index) => {
|
||||
const isOpen = openIndex === index
|
||||
const isHovered = hoveredIndex === index
|
||||
const showDivider = index > 0 && hoveredIndex !== index && hoveredIndex !== index - 1
|
||||
|
||||
return (
|
||||
<div key={question}>
|
||||
<div
|
||||
className={cn(
|
||||
'h-px w-full bg-[var(--landing-bg-elevated)]',
|
||||
index === 0 || !showDivider ? 'invisible' : 'visible'
|
||||
)}
|
||||
/>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => setOpenIndex(isOpen ? null : index)}
|
||||
className='flex w-full items-start justify-between gap-4 py-5 text-left'
|
||||
onMouseEnter={() => setHoveredIndex(index)}
|
||||
onMouseLeave={() => setHoveredIndex(null)}
|
||||
className='-mx-6 flex w-[calc(100%+3rem)] items-center justify-between gap-4 px-6 py-4 text-left transition-colors hover:bg-[var(--landing-bg-elevated)]'
|
||||
aria-expanded={isOpen}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
'font-[500] text-[15px] leading-snug transition-colors',
|
||||
'text-[15px] leading-snug tracking-[-0.02em] transition-colors',
|
||||
isOpen
|
||||
? 'text-[var(--landing-text)]'
|
||||
: 'text-[var(--landing-text-muted)] hover:text-[var(--landing-text)]'
|
||||
: 'text-[var(--landing-text-body)] 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',
|
||||
'h-3 w-3 shrink-0 text-[var(--landing-text-subtle)] 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>
|
||||
)}
|
||||
<AnimatePresence initial={false}>
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.25, ease: [0.4, 0, 0.2, 1] }}
|
||||
className='overflow-hidden'
|
||||
>
|
||||
<div className='pt-2 pb-4'>
|
||||
<p className='text-[14px] text-[var(--landing-text-body)] leading-[1.75]'>
|
||||
{answer}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
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 { captureClientEvent } from '@/lib/posthog/client'
|
||||
import { AuthModal } from '@/app/(landing)/components/auth-modal/auth-modal'
|
||||
import {
|
||||
EASE_OUT,
|
||||
type EditorPromptData,
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
TYPE_INTERVAL_MS,
|
||||
TYPE_START_BUFFER_MS,
|
||||
} from '@/app/(landing)/components/landing-preview/components/landing-preview-workflow/workflow-data'
|
||||
import { trackLandingCta } from '@/app/(landing)/landing-analytics'
|
||||
|
||||
type PanelTab = 'copilot' | 'editor'
|
||||
|
||||
@@ -44,6 +45,11 @@ export function useLandingSubmit() {
|
||||
const trimmed = text.trim()
|
||||
if (!trimmed) return
|
||||
LandingPromptStorage.store(trimmed)
|
||||
trackLandingCta({
|
||||
label: 'Prompt submit',
|
||||
section: 'landing_preview',
|
||||
destination: '/signup',
|
||||
})
|
||||
router.push('/signup')
|
||||
},
|
||||
[router]
|
||||
@@ -175,20 +181,29 @@ export const LandingPreviewPanel = memo(function LandingPreviewPanel({
|
||||
<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>
|
||||
<AuthModal defaultView='signup' source='landing_preview'>
|
||||
<button
|
||||
type='button'
|
||||
className='flex gap-1.5'
|
||||
onMouseMove={(e) => setCursorPos({ x: e.clientX, y: e.clientY })}
|
||||
onMouseLeave={() => setCursorPos(null)}
|
||||
onClick={() =>
|
||||
trackLandingCta({
|
||||
label: 'Deploy',
|
||||
section: 'landing_preview',
|
||||
destination: 'auth_modal',
|
||||
})
|
||||
}
|
||||
>
|
||||
<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>
|
||||
</button>
|
||||
</AuthModal>
|
||||
{cursorPos &&
|
||||
createPortal(
|
||||
<div
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
import type { ComponentType, SVGProps } from 'react'
|
||||
import Link from 'next/link'
|
||||
import {
|
||||
AgentIcon,
|
||||
ApiIcon,
|
||||
McpIcon,
|
||||
PackageSearchIcon,
|
||||
TableIcon,
|
||||
WorkflowIcon,
|
||||
} from '@/components/icons'
|
||||
|
||||
interface ProductLink {
|
||||
label: string
|
||||
description: string
|
||||
href: string
|
||||
external?: boolean
|
||||
icon: ComponentType<SVGProps<SVGSVGElement>>
|
||||
}
|
||||
|
||||
interface SidebarLink {
|
||||
label: string
|
||||
href: string
|
||||
external?: boolean
|
||||
}
|
||||
|
||||
const WORKSPACE: ProductLink[] = [
|
||||
{
|
||||
label: 'Workflows',
|
||||
description: 'Visual AI automation builder',
|
||||
href: 'https://docs.sim.ai/getting-started',
|
||||
external: true,
|
||||
icon: WorkflowIcon,
|
||||
},
|
||||
{
|
||||
label: 'Agent',
|
||||
description: 'Build autonomous AI agents',
|
||||
href: 'https://docs.sim.ai/blocks/agent',
|
||||
external: true,
|
||||
icon: AgentIcon,
|
||||
},
|
||||
{
|
||||
label: 'MCP',
|
||||
description: 'Connect external tools',
|
||||
href: 'https://docs.sim.ai/mcp',
|
||||
external: true,
|
||||
icon: McpIcon,
|
||||
},
|
||||
{
|
||||
label: 'Knowledge Base',
|
||||
description: 'Retrieval-augmented context',
|
||||
href: 'https://docs.sim.ai/knowledgebase',
|
||||
external: true,
|
||||
icon: PackageSearchIcon,
|
||||
},
|
||||
{
|
||||
label: 'Tables',
|
||||
description: 'Structured data storage',
|
||||
href: 'https://docs.sim.ai/tables',
|
||||
external: true,
|
||||
icon: TableIcon,
|
||||
},
|
||||
{
|
||||
label: 'API',
|
||||
description: 'Deploy agents as endpoints',
|
||||
href: 'https://docs.sim.ai/api-reference/getting-started',
|
||||
external: true,
|
||||
icon: ApiIcon,
|
||||
},
|
||||
]
|
||||
|
||||
const EXPLORE: SidebarLink[] = [
|
||||
{ label: 'Models', href: '/models' },
|
||||
{ label: 'Integrations', href: '/integrations' },
|
||||
{ label: 'Changelog', href: '/changelog' },
|
||||
{ label: 'Self-hosting', href: 'https://docs.sim.ai/self-hosting', external: true },
|
||||
]
|
||||
|
||||
function DropdownLink({ link }: { link: ProductLink }) {
|
||||
const Icon = link.icon
|
||||
const Tag = link.external ? 'a' : Link
|
||||
const props = link.external
|
||||
? { href: link.href, target: '_blank' as const, rel: 'noopener noreferrer' }
|
||||
: { href: link.href }
|
||||
|
||||
return (
|
||||
<Tag
|
||||
{...props}
|
||||
className='group/item flex items-start gap-2.5 rounded-[5px] px-2.5 py-2 transition-colors hover:bg-[var(--landing-bg-elevated)]'
|
||||
>
|
||||
<Icon className='mt-0.5 h-[15px] w-[15px] shrink-0 text-[var(--landing-text-icon)]' />
|
||||
<div className='flex flex-col'>
|
||||
<span className='font-[430] font-season text-[13px] text-white leading-tight'>
|
||||
{link.label}
|
||||
</span>
|
||||
<span className='font-season text-[12px] text-[var(--landing-text-subtle)] leading-[150%]'>
|
||||
{link.description}
|
||||
</span>
|
||||
</div>
|
||||
</Tag>
|
||||
)
|
||||
}
|
||||
|
||||
export function ProductDropdown() {
|
||||
return (
|
||||
<div className='flex w-[560px] rounded-[5px] border border-[var(--landing-bg-elevated)] bg-[var(--landing-bg)] shadow-overlay'>
|
||||
<div className='flex-1 p-2'>
|
||||
<div className='mb-1 px-2.5 pt-1'>
|
||||
<span className='font-[430] font-season text-[11px] text-[var(--landing-text-subtle)] uppercase tracking-[0.08em]'>
|
||||
Workspace
|
||||
</span>
|
||||
<div className='mt-1.5 h-px bg-[var(--landing-bg-elevated)]' />
|
||||
</div>
|
||||
|
||||
<div className='grid grid-cols-2'>
|
||||
{WORKSPACE.map((link) => (
|
||||
<DropdownLink key={link.label} link={link} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='w-px self-stretch bg-[var(--landing-bg-elevated)]' />
|
||||
|
||||
<div className='w-[160px] p-2'>
|
||||
<div className='mb-1 px-2.5 pt-1'>
|
||||
<span className='font-[430] font-season text-[11px] text-[var(--landing-text-subtle)] uppercase tracking-[0.08em]'>
|
||||
Explore
|
||||
</span>
|
||||
<div className='mt-1.5 h-px bg-[var(--landing-bg-elevated)]' />
|
||||
</div>
|
||||
|
||||
{EXPLORE.map((link) => {
|
||||
const Tag = link.external ? 'a' : Link
|
||||
const props = link.external
|
||||
? { href: link.href, target: '_blank' as const, rel: 'noopener noreferrer' }
|
||||
: { href: link.href }
|
||||
return (
|
||||
<Tag
|
||||
key={link.label}
|
||||
{...props}
|
||||
className='block rounded-[5px] px-2.5 py-1.5 font-[430] font-season text-[13px] text-white transition-colors hover:bg-[var(--landing-bg-elevated)]'
|
||||
>
|
||||
{link.label}
|
||||
</Tag>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { useSearchParams } from 'next/navigation'
|
||||
import { GithubOutlineIcon } from '@/components/icons'
|
||||
import { useSession } from '@/lib/auth/auth-client'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { AuthModal } from '@/app/(landing)/components/auth-modal/auth-modal'
|
||||
import {
|
||||
BlogDropdown,
|
||||
type NavBlogPost,
|
||||
@@ -29,6 +30,8 @@ interface NavLink {
|
||||
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: 'Integrations', href: '/integrations' },
|
||||
{ label: 'Models', href: '/models' },
|
||||
{ label: 'Pricing', href: '/#pricing' },
|
||||
]
|
||||
|
||||
@@ -225,30 +228,38 @@ export default function Navbar({ logoOnly = false, blogPosts = [] }: NavbarProps
|
||||
</Link>
|
||||
) : (
|
||||
<>
|
||||
<Link
|
||||
href='/login'
|
||||
className='inline-flex h-[30px] items-center rounded-[5px] border border-[var(--landing-border-strong)] px-[9px] text-[13.5px] text-[var(--landing-text)] transition-colors hover:bg-[var(--landing-bg-elevated)]'
|
||||
aria-label='Log in'
|
||||
onClick={() =>
|
||||
trackLandingCta({ label: 'Log in', section: 'navbar', destination: '/login' })
|
||||
}
|
||||
>
|
||||
Log in
|
||||
</Link>
|
||||
<Link
|
||||
href='/signup'
|
||||
className='inline-flex h-[30px] items-center gap-[7px] rounded-[5px] border border-[var(--white)] bg-[var(--white)] px-2.5 text-[13.5px] text-black transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
|
||||
aria-label='Get started with Sim'
|
||||
onClick={() =>
|
||||
trackLandingCta({
|
||||
label: 'Get started',
|
||||
section: 'navbar',
|
||||
destination: '/signup',
|
||||
})
|
||||
}
|
||||
>
|
||||
Get started
|
||||
</Link>
|
||||
<AuthModal defaultView='login' source='navbar'>
|
||||
<button
|
||||
type='button'
|
||||
className='inline-flex h-[30px] items-center rounded-[5px] border border-[var(--landing-border-strong)] px-[9px] text-[13.5px] text-[var(--landing-text)] transition-colors hover:bg-[var(--landing-bg-elevated)]'
|
||||
aria-label='Log in'
|
||||
onClick={() =>
|
||||
trackLandingCta({
|
||||
label: 'Log in',
|
||||
section: 'navbar',
|
||||
destination: 'auth_modal',
|
||||
})
|
||||
}
|
||||
>
|
||||
Log in
|
||||
</button>
|
||||
</AuthModal>
|
||||
<AuthModal defaultView='signup' source='navbar'>
|
||||
<button
|
||||
type='button'
|
||||
className='inline-flex h-[30px] items-center gap-[7px] rounded-[5px] border border-[var(--white)] bg-[var(--white)] px-2.5 text-[13.5px] text-black transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
|
||||
aria-label='Get started with Sim'
|
||||
onClick={() =>
|
||||
trackLandingCta({
|
||||
label: 'Get started',
|
||||
section: 'navbar',
|
||||
destination: 'auth_modal',
|
||||
})
|
||||
}
|
||||
>
|
||||
Get started
|
||||
</button>
|
||||
</AuthModal>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -335,32 +346,38 @@ export default function Navbar({ logoOnly = false, blogPosts = [] }: NavbarProps
|
||||
</Link>
|
||||
) : (
|
||||
<>
|
||||
<Link
|
||||
href='/login'
|
||||
className='flex h-[32px] items-center justify-center rounded-[5px] border border-[var(--landing-border-strong)] text-[14px] text-[var(--landing-text)] transition-colors active:bg-[var(--landing-bg-elevated)]'
|
||||
onClick={() => {
|
||||
trackLandingCta({ label: 'Log in', section: 'navbar', destination: '/login' })
|
||||
setMobileMenuOpen(false)
|
||||
}}
|
||||
aria-label='Log in'
|
||||
>
|
||||
Log in
|
||||
</Link>
|
||||
<Link
|
||||
href='/signup'
|
||||
className='flex h-[32px] items-center justify-center rounded-[5px] border border-[var(--white)] bg-[var(--white)] text-[14px] text-black transition-colors active:bg-[#E0E0E0]'
|
||||
onClick={() => {
|
||||
trackLandingCta({
|
||||
label: 'Get started',
|
||||
section: 'navbar',
|
||||
destination: '/signup',
|
||||
})
|
||||
setMobileMenuOpen(false)
|
||||
}}
|
||||
aria-label='Get started with Sim'
|
||||
>
|
||||
Get started
|
||||
</Link>
|
||||
<AuthModal defaultView='login' source='mobile_navbar'>
|
||||
<button
|
||||
type='button'
|
||||
className='flex h-[32px] items-center justify-center rounded-[5px] border border-[var(--landing-border-strong)] text-[14px] text-[var(--landing-text)] transition-colors active:bg-[var(--landing-bg-elevated)]'
|
||||
onClick={() =>
|
||||
trackLandingCta({
|
||||
label: 'Log in',
|
||||
section: 'navbar',
|
||||
destination: 'auth_modal',
|
||||
})
|
||||
}
|
||||
aria-label='Log in'
|
||||
>
|
||||
Log in
|
||||
</button>
|
||||
</AuthModal>
|
||||
<AuthModal defaultView='signup' source='mobile_navbar'>
|
||||
<button
|
||||
type='button'
|
||||
className='flex h-[32px] items-center justify-center rounded-[5px] border border-[var(--white)] bg-[var(--white)] text-[14px] text-black transition-colors active:bg-[#E0E0E0]'
|
||||
onClick={() =>
|
||||
trackLandingCta({
|
||||
label: 'Get started',
|
||||
section: 'navbar',
|
||||
destination: 'auth_modal',
|
||||
})
|
||||
}
|
||||
aria-label='Get started with Sim'
|
||||
>
|
||||
Get started
|
||||
</button>
|
||||
</AuthModal>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { Badge } from '@/components/emcn'
|
||||
import { AuthModal } from '@/app/(landing)/components/auth-modal/auth-modal'
|
||||
import { DemoRequestModal } from '@/app/(landing)/components/demo-request/demo-request-modal'
|
||||
import { trackLandingCta } from '@/app/(landing)/landing-analytics'
|
||||
|
||||
@@ -37,7 +37,7 @@ const PRICING_TIERS: PricingTier[] = [
|
||||
{
|
||||
id: 'pro',
|
||||
name: 'Pro',
|
||||
description: 'For professionals building production workflows',
|
||||
description: 'For professionals deploying AI agents',
|
||||
price: '$25',
|
||||
billingPeriod: 'per month',
|
||||
color: '#00F701',
|
||||
@@ -55,7 +55,7 @@ const PRICING_TIERS: PricingTier[] = [
|
||||
{
|
||||
id: 'max',
|
||||
name: 'Max',
|
||||
description: 'For power users and teams building at scale',
|
||||
description: 'For teams building AI agents at scale',
|
||||
price: '$100',
|
||||
billingPeriod: 'per month',
|
||||
color: '#FA4EDF',
|
||||
@@ -163,33 +163,37 @@ function PricingCard({ tier }: PricingCardProps) {
|
||||
</button>
|
||||
</DemoRequestModal>
|
||||
) : isPro ? (
|
||||
<Link
|
||||
href={tier.cta.href || '/signup'}
|
||||
className='flex h-[32px] w-full items-center justify-center rounded-[5px] border border-[#1D1D1D] bg-[#1D1D1D] px-2.5 font-[430] font-season text-[14px] text-white transition-colors hover:border-[var(--landing-border)] hover:bg-[var(--landing-bg-elevated)]'
|
||||
onClick={() =>
|
||||
trackLandingCta({
|
||||
label: tier.cta.label,
|
||||
section: 'pricing',
|
||||
destination: tier.cta.href || '/signup',
|
||||
})
|
||||
}
|
||||
>
|
||||
{tier.cta.label}
|
||||
</Link>
|
||||
<AuthModal defaultView='signup' source='pricing'>
|
||||
<button
|
||||
type='button'
|
||||
className='flex h-[32px] w-full items-center justify-center rounded-[5px] border border-[#1D1D1D] bg-[#1D1D1D] px-2.5 font-[430] font-season text-[14px] text-white transition-colors hover:border-[var(--landing-border)] hover:bg-[var(--landing-bg-elevated)]'
|
||||
onClick={() =>
|
||||
trackLandingCta({
|
||||
label: tier.cta.label,
|
||||
section: 'pricing',
|
||||
destination: 'auth_modal',
|
||||
})
|
||||
}
|
||||
>
|
||||
{tier.cta.label}
|
||||
</button>
|
||||
</AuthModal>
|
||||
) : (
|
||||
<Link
|
||||
href={tier.cta.href || '/signup'}
|
||||
className='flex h-[32px] w-full items-center justify-center rounded-[5px] border border-[var(--landing-border-light)] px-2.5 font-[430] font-season text-[14px] text-[var(--landing-text-dark)] transition-colors hover:bg-[var(--landing-bg-hover)]'
|
||||
onClick={() =>
|
||||
trackLandingCta({
|
||||
label: tier.cta.label,
|
||||
section: 'pricing',
|
||||
destination: tier.cta.href || '/signup',
|
||||
})
|
||||
}
|
||||
>
|
||||
{tier.cta.label}
|
||||
</Link>
|
||||
<AuthModal defaultView='signup' source='pricing'>
|
||||
<button
|
||||
type='button'
|
||||
className='flex h-[32px] w-full items-center justify-center rounded-[5px] border border-[var(--landing-border-light)] px-2.5 font-[430] font-season text-[14px] text-[var(--landing-text-dark)] transition-colors hover:bg-[var(--landing-bg-hover)]'
|
||||
onClick={() =>
|
||||
trackLandingCta({
|
||||
label: tier.cta.label,
|
||||
section: 'pricing',
|
||||
destination: 'auth_modal',
|
||||
})
|
||||
}
|
||||
>
|
||||
{tier.cta.label}
|
||||
</button>
|
||||
</AuthModal>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -27,7 +27,7 @@ export default function StructuredData() {
|
||||
name: 'Sim',
|
||||
alternateName: 'Sim Studio',
|
||||
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.',
|
||||
'Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM to create agents that automate real work.',
|
||||
url: 'https://sim.ai',
|
||||
logo: {
|
||||
'@type': 'ImageObject',
|
||||
@@ -55,9 +55,9 @@ export default function StructuredData() {
|
||||
'@type': 'WebSite',
|
||||
'@id': 'https://sim.ai/#website',
|
||||
url: 'https://sim.ai',
|
||||
name: 'Sim — Build AI Agents & Run Your Agentic Workforce',
|
||||
name: 'Sim — The AI Workspace | Build, Deploy & Manage AI Agents',
|
||||
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. Join 100,000+ builders.',
|
||||
'Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM. Join 100,000+ builders.',
|
||||
publisher: { '@id': 'https://sim.ai/#organization' },
|
||||
inLanguage: 'en-US',
|
||||
},
|
||||
@@ -65,13 +65,13 @@ export default function StructuredData() {
|
||||
'@type': 'WebPage',
|
||||
'@id': 'https://sim.ai/#webpage',
|
||||
url: 'https://sim.ai',
|
||||
name: 'Sim — Build AI Agents & Run Your Agentic Workforce',
|
||||
name: 'Sim — The AI Workspace | Build, Deploy & Manage AI Agents',
|
||||
isPartOf: { '@id': 'https://sim.ai/#website' },
|
||||
about: { '@id': 'https://sim.ai/#software' },
|
||||
datePublished: '2024-01-01T00:00:00+00:00',
|
||||
dateModified: new Date().toISOString(),
|
||||
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.',
|
||||
'Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM to create agents that automate real work.',
|
||||
breadcrumb: { '@id': 'https://sim.ai/#breadcrumb' },
|
||||
inLanguage: 'en-US',
|
||||
speakable: {
|
||||
@@ -91,12 +91,14 @@ export default function StructuredData() {
|
||||
'@type': 'WebApplication',
|
||||
'@id': 'https://sim.ai/#software',
|
||||
url: 'https://sim.ai',
|
||||
name: 'Sim — Build AI Agents & Run Your Agentic Workforce',
|
||||
name: 'Sim — The AI Workspace',
|
||||
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 compliant.',
|
||||
applicationCategory: 'DeveloperApplication',
|
||||
'Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM to create agents that automate real work — visually, conversationally, or with code. Trusted by over 100,000 builders. SOC2 compliant.',
|
||||
applicationCategory: 'BusinessApplication',
|
||||
applicationSubCategory: 'AI Workspace',
|
||||
operatingSystem: 'Web',
|
||||
browserRequirements: 'Requires a modern browser with JavaScript enabled',
|
||||
installUrl: 'https://sim.ai/signup',
|
||||
offers: [
|
||||
{
|
||||
'@type': 'Offer',
|
||||
@@ -135,8 +137,9 @@ export default function StructuredData() {
|
||||
},
|
||||
],
|
||||
featureList: [
|
||||
'AI agent creation',
|
||||
'Agentic workflow orchestration',
|
||||
'AI workspace for teams',
|
||||
'Mothership — natural language agent creation',
|
||||
'Visual workflow builder',
|
||||
'1,000+ integrations',
|
||||
'LLM orchestration (OpenAI, Anthropic, Google, xAI, Mistral, Perplexity)',
|
||||
'Knowledge base creation',
|
||||
@@ -176,19 +179,27 @@ export default function StructuredData() {
|
||||
codeRepository: 'https://github.com/simstudioai/sim',
|
||||
programmingLanguage: ['TypeScript', 'Python'],
|
||||
runtimePlatform: 'Node.js',
|
||||
license: 'https://opensource.org/licenses/AGPL-3.0',
|
||||
license: 'https://opensource.org/licenses/Apache-2.0',
|
||||
isPartOf: { '@id': 'https://sim.ai/#software' },
|
||||
},
|
||||
{
|
||||
'@type': 'FAQPage',
|
||||
'@id': 'https://sim.ai/#faq',
|
||||
mainEntity: [
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'What is the best AI agent builder?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'Sim is the open-source AI workspace trusted by over 100,000 builders for creating, deploying, and managing AI agents. Build agents visually with the workflow builder, conversationally through Mothership, or programmatically with the API. Sim connects to 1,000+ integrations and all major LLMs (OpenAI, Anthropic, Google, xAI, Mistral), and includes knowledge bases, tables, real-time collaboration, and enterprise governance. Free tier available. SOC2 compliant. Self-hostable.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'Question',
|
||||
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 compliant.',
|
||||
text: 'Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM to create agents that automate real work — visually, conversationally, or with code. The workspace includes Mothership for natural-language creation, a visual workflow builder, knowledge bases, tables, and full observability. Trusted by over 100,000 builders. SOC2 compliant.',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -212,7 +223,7 @@ export default function StructuredData() {
|
||||
name: 'Do I need coding skills to use Sim?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'No coding skills are required. Sim provides a visual interface for building AI agents and agentic workflows. Developers can also use custom functions, the API, and the CLI/SDK for advanced use cases.',
|
||||
text: 'No coding skills are required. Sim provides multiple ways to build agents: a visual workflow builder for drag-and-drop creation, Mothership for building in natural language, and templates for common use cases. Developers can also use custom functions, the API, and the CLI/SDK for advanced use cases.',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -228,7 +239,7 @@ export default function StructuredData() {
|
||||
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.',
|
||||
text: 'Yes. Sim is fully open source under the Apache 2.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.',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -9,6 +9,7 @@ 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/(landing)/components/templates/template-workflows'
|
||||
import { trackLandingCta } from '@/app/(landing)/landing-analytics'
|
||||
|
||||
const logger = createLogger('LandingTemplates')
|
||||
|
||||
@@ -297,6 +298,11 @@ export default function Templates() {
|
||||
})
|
||||
} finally {
|
||||
setIsPreparingTemplate(false)
|
||||
trackLandingCta({
|
||||
label: activeWorkflow.name,
|
||||
section: 'templates',
|
||||
destination: '/signup',
|
||||
})
|
||||
router.push('/signup')
|
||||
}
|
||||
}, [
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
'use client'
|
||||
|
||||
import { AuthModal } from '@/app/(landing)/components/auth-modal/auth-modal'
|
||||
import { trackLandingCta } from '@/app/(landing)/landing-analytics'
|
||||
|
||||
interface IntegrationCtaButtonProps {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
label: string
|
||||
}
|
||||
|
||||
export function IntegrationCtaButton({ children, className, label }: IntegrationCtaButtonProps) {
|
||||
return (
|
||||
<AuthModal defaultView='signup' source='integrations'>
|
||||
<button
|
||||
type='button'
|
||||
className={className}
|
||||
onClick={() =>
|
||||
trackLandingCta({ label, section: 'integrations', destination: 'auth_modal' })
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
</AuthModal>
|
||||
)
|
||||
}
|
||||
@@ -2,26 +2,26 @@
|
||||
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { LandingPromptStorage } from '@/lib/core/utils/browser-storage'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { trackLandingCta } from '@/app/(landing)/landing-analytics'
|
||||
|
||||
interface TemplateCardButtonProps {
|
||||
prompt: string
|
||||
className?: string
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function TemplateCardButton({ prompt, children }: TemplateCardButtonProps) {
|
||||
export function TemplateCardButton({ prompt, className, children }: TemplateCardButtonProps) {
|
||||
const router = useRouter()
|
||||
|
||||
function handleClick() {
|
||||
LandingPromptStorage.store(prompt)
|
||||
trackLandingCta({ label: 'Template card', section: 'integrations', destination: '/signup' })
|
||||
router.push('/signup')
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type='button'
|
||||
onClick={handleClick}
|
||||
className='group flex w-full flex-col items-start rounded-lg border border-[var(--landing-border)] bg-[var(--landing-bg-card)] p-5 text-left transition-colors hover:border-[var(--landing-border-strong)] hover:bg-[var(--landing-bg-elevated)]'
|
||||
>
|
||||
<button type='button' onClick={handleClick} className={cn('w-full text-left', className)}>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
import type { ComponentType, SVGProps } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { Badge } from '@/components/emcn'
|
||||
import type { Integration } from '@/app/(landing)/integrations/data/types'
|
||||
import { ChevronArrow } from '@/app/(landing)/models/components/model-primitives'
|
||||
import { IntegrationIcon } from './integration-icon'
|
||||
|
||||
interface IntegrationCardProps {
|
||||
@@ -9,49 +9,76 @@ interface IntegrationCardProps {
|
||||
IconComponent?: ComponentType<SVGProps<SVGSVGElement>>
|
||||
}
|
||||
|
||||
/**
|
||||
* Featured integration card — matches blog featured post pattern.
|
||||
* Used in flex rows separated by border-l dividers.
|
||||
*/
|
||||
export function IntegrationCard({ integration, IconComponent }: IntegrationCardProps) {
|
||||
const { slug, name, description, bgColor, operationCount, triggerCount } = integration
|
||||
const { slug, name, description, bgColor } = integration
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={`/integrations/${slug}`}
|
||||
className='group flex flex-col rounded-lg border border-[var(--landing-border)] bg-[var(--landing-bg-card)] p-4 transition-colors hover:border-[var(--landing-border-strong)] hover:bg-[var(--landing-bg-elevated)]'
|
||||
aria-label={`${name} integration`}
|
||||
className='group/link flex flex-1 flex-col gap-4 border-[var(--landing-bg-elevated)] border-t p-6 transition-colors first:border-t-0 hover:bg-[var(--landing-bg-elevated)] sm:border-t-0 sm:border-l sm:first:border-l-0'
|
||||
>
|
||||
<IntegrationIcon
|
||||
bgColor={bgColor}
|
||||
name={name}
|
||||
Icon={IconComponent}
|
||||
className='mb-3 h-10 w-10 rounded-lg'
|
||||
className='h-10 w-10 rounded-[5px]'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
|
||||
{/* Name */}
|
||||
<h3 className='mb-1 font-[500] text-[14px] text-[var(--landing-text)] leading-snug'>
|
||||
{name}
|
||||
</h3>
|
||||
|
||||
{/* Description — clamped to 2 lines */}
|
||||
<p className='mb-3 line-clamp-2 flex-1 text-[12px] text-[var(--landing-text-muted)] leading-relaxed'>
|
||||
{description}
|
||||
</p>
|
||||
|
||||
{/* Footer row */}
|
||||
<div className='flex flex-wrap items-center gap-1.5'>
|
||||
{operationCount > 0 && (
|
||||
<Badge className='border-0 bg-[#333] text-[11px] text-[var(--landing-text-muted)]'>
|
||||
{operationCount} {operationCount === 1 ? 'tool' : 'tools'}
|
||||
</Badge>
|
||||
)}
|
||||
{triggerCount > 0 && (
|
||||
<Badge className='border-0 bg-[#333] text-[11px] text-[var(--landing-text-muted)]'>
|
||||
{triggerCount} {triggerCount === 1 ? 'trigger' : 'triggers'}
|
||||
</Badge>
|
||||
)}
|
||||
<span className='ml-auto text-[#555] text-[12px] transition-colors group-hover:text-[var(--landing-text-muted)]'>
|
||||
Learn more →
|
||||
</span>
|
||||
<div className='flex flex-col gap-2'>
|
||||
<h3 className='text-lg text-white leading-tight tracking-[-0.01em]'>{name}</h3>
|
||||
<p className='line-clamp-2 text-[var(--landing-text-muted)] text-sm leading-[150%]'>
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
interface IntegrationRowProps {
|
||||
integration: Integration
|
||||
IconComponent?: ComponentType<SVGProps<SVGSVGElement>>
|
||||
}
|
||||
|
||||
/**
|
||||
* Integration list row — matches blog remaining post pattern.
|
||||
* Each row followed by an h-px divider.
|
||||
*/
|
||||
export function IntegrationRow({ integration, IconComponent }: IntegrationRowProps) {
|
||||
const { slug, name, description, bgColor } = integration
|
||||
|
||||
return (
|
||||
<>
|
||||
<Link
|
||||
href={`/integrations/${slug}`}
|
||||
className='group/link flex items-center gap-4 px-6 py-4 transition-colors hover:bg-[var(--landing-bg-elevated)]'
|
||||
aria-label={`${name} integration`}
|
||||
>
|
||||
<IntegrationIcon
|
||||
bgColor={bgColor}
|
||||
name={name}
|
||||
Icon={IconComponent}
|
||||
className='h-8 w-8 shrink-0 rounded-[5px]'
|
||||
iconClassName='h-4 w-4'
|
||||
fallbackClassName='text-[13px]'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
|
||||
{/* Name + description */}
|
||||
<div className='flex min-w-0 flex-1 flex-col gap-0.5'>
|
||||
<h3 className='text-[14px] text-white leading-snug tracking-[-0.02em]'>{name}</h3>
|
||||
<p className='line-clamp-1 hidden text-[12px] text-[var(--landing-text-muted)] leading-[150%] sm:block'>
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Animated arrow */}
|
||||
<ChevronArrow />
|
||||
</Link>
|
||||
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,12 +4,11 @@ import { useMemo, useState } from 'react'
|
||||
import { Input } from '@/components/emcn'
|
||||
import { blockTypeToIconMap } from '@/app/(landing)/integrations/data/icon-mapping'
|
||||
import type { Integration } from '@/app/(landing)/integrations/data/types'
|
||||
import { IntegrationCard } from './integration-card'
|
||||
import { IntegrationRow } from './integration-card'
|
||||
|
||||
const CATEGORY_LABELS: Record<string, string> = {
|
||||
ai: 'AI',
|
||||
analytics: 'Analytics',
|
||||
automation: 'Automation',
|
||||
communication: 'Communication',
|
||||
crm: 'CRM',
|
||||
'customer-support': 'Customer Support',
|
||||
@@ -21,12 +20,10 @@ const CATEGORY_LABELS: Record<string, string> = {
|
||||
email: 'Email',
|
||||
'file-storage': 'File Storage',
|
||||
hr: 'HR',
|
||||
media: 'Media',
|
||||
productivity: 'Productivity',
|
||||
'sales-intelligence': 'Sales Intelligence',
|
||||
sales: 'Sales',
|
||||
search: 'Search',
|
||||
security: 'Security',
|
||||
social: 'Social',
|
||||
other: 'Other',
|
||||
} as const
|
||||
|
||||
@@ -41,8 +38,10 @@ export function IntegrationGrid({ integrations }: IntegrationGridProps) {
|
||||
const availableCategories = useMemo(() => {
|
||||
const counts = new Map<string, number>()
|
||||
for (const i of integrations) {
|
||||
if (i.integrationType) {
|
||||
counts.set(i.integrationType, (counts.get(i.integrationType) || 0) + 1)
|
||||
if (i.integrationTypes) {
|
||||
for (const t of i.integrationTypes) {
|
||||
counts.set(t, (counts.get(t) || 0) + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
return Array.from(counts.entries())
|
||||
@@ -54,7 +53,7 @@ export function IntegrationGrid({ integrations }: IntegrationGridProps) {
|
||||
let results = integrations
|
||||
|
||||
if (activeCategory) {
|
||||
results = results.filter((i) => i.integrationType === activeCategory)
|
||||
results = results.filter((i) => i.integrationTypes?.includes(activeCategory))
|
||||
}
|
||||
|
||||
const q = query.trim().toLowerCase()
|
||||
@@ -75,7 +74,7 @@ export function IntegrationGrid({ integrations }: IntegrationGridProps) {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='mb-6 flex flex-col gap-4 sm:flex-row sm:items-center'>
|
||||
<div className='mb-6 flex flex-col gap-4 px-6 sm:flex-row sm:items-center'>
|
||||
<div className='relative max-w-[480px] flex-1'>
|
||||
<svg
|
||||
aria-hidden='true'
|
||||
@@ -99,14 +98,14 @@ export function IntegrationGrid({ integrations }: IntegrationGridProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mb-8 flex flex-wrap gap-2'>
|
||||
<div className='mb-6 flex flex-wrap gap-2 px-6'>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => setActiveCategory(null)}
|
||||
className={`rounded-md border px-3 py-1 text-[12px] transition-colors ${
|
||||
className={`rounded-[5px] border px-[9px] py-0.5 text-[13.5px] transition-colors ${
|
||||
activeCategory === null
|
||||
? 'border-[#555] bg-[#333] text-[var(--landing-text)]'
|
||||
: 'border-[var(--landing-border)] bg-transparent text-[var(--landing-text-muted)] hover:border-[var(--landing-border-strong)] hover:text-[var(--landing-text)]'
|
||||
? 'border-[var(--landing-border-strong)] bg-[var(--landing-bg-elevated)] text-[var(--landing-text)]'
|
||||
: 'border-[var(--landing-border-strong)] text-[var(--landing-text)] hover:bg-[var(--landing-bg-elevated)]'
|
||||
}`}
|
||||
>
|
||||
All
|
||||
@@ -116,10 +115,10 @@ export function IntegrationGrid({ integrations }: IntegrationGridProps) {
|
||||
key={cat}
|
||||
type='button'
|
||||
onClick={() => setActiveCategory(activeCategory === cat ? null : cat)}
|
||||
className={`rounded-md border px-3 py-1 text-[12px] transition-colors ${
|
||||
className={`rounded-[5px] border px-[9px] py-0.5 text-[13.5px] transition-colors ${
|
||||
activeCategory === cat
|
||||
? 'border-[#555] bg-[#333] text-[var(--landing-text)]'
|
||||
: 'border-[var(--landing-border)] bg-transparent text-[var(--landing-text-muted)] hover:border-[var(--landing-border-strong)] hover:text-[var(--landing-text)]'
|
||||
? 'border-[var(--landing-border-strong)] bg-[var(--landing-bg-elevated)] text-[var(--landing-text)]'
|
||||
: 'border-[var(--landing-border-strong)] text-[var(--landing-text)] hover:bg-[var(--landing-bg-elevated)]'
|
||||
}`}
|
||||
>
|
||||
{CATEGORY_LABELS[cat] || cat}
|
||||
@@ -127,16 +126,18 @@ export function IntegrationGrid({ integrations }: IntegrationGridProps) {
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
|
||||
|
||||
{filtered.length === 0 ? (
|
||||
<p className='py-12 text-center text-[#555] text-[15px]'>
|
||||
<p className='py-12 text-center text-[15px] text-[var(--landing-text-subtle)]'>
|
||||
No integrations found
|
||||
{query ? <> for “{query}”</> : null}
|
||||
{activeCategory ? <> in {CATEGORY_LABELS[activeCategory] || activeCategory}</> : null}
|
||||
</p>
|
||||
) : (
|
||||
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4'>
|
||||
<div>
|
||||
{filtered.map((integration) => (
|
||||
<IntegrationCard
|
||||
<IntegrationRow
|
||||
key={integration.type}
|
||||
integration={integration}
|
||||
IconComponent={blockTypeToIconMap[integration.type]}
|
||||
|
||||
@@ -41,9 +41,7 @@ export function IntegrationIcon({
|
||||
{Icon ? (
|
||||
<Icon className={cn(iconClassName, 'text-white')} />
|
||||
) : (
|
||||
<span className={cn('font-[500] text-white leading-none', fallbackClassName)}>
|
||||
{name.charAt(0)}
|
||||
</span>
|
||||
<span className={cn('text-white leading-none', fallbackClassName)}>{name.charAt(0)}</span>
|
||||
)}
|
||||
</Tag>
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -34,6 +34,6 @@ export interface Integration {
|
||||
triggerCount: number
|
||||
authType: AuthType
|
||||
category: string
|
||||
integrationType?: string
|
||||
integrationTypes?: string[]
|
||||
tags?: string[]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { Metadata } from 'next'
|
||||
import { Badge } from '@/components/emcn'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { IntegrationCard } from './components/integration-card'
|
||||
import { IntegrationGrid } from './components/integration-grid'
|
||||
import { RequestIntegrationModal } from './components/request-integration-modal'
|
||||
import { blockTypeToIconMap } from './data/icon-mapping'
|
||||
@@ -18,19 +20,27 @@ const TOP_NAMES = [...new Set(POPULAR_WORKFLOWS.flatMap((p) => [p.from, p.to]))]
|
||||
|
||||
const baseUrl = getBaseUrl()
|
||||
|
||||
/** Curated featured integrations — high-recognition services shown as cards. */
|
||||
const FEATURED_SLUGS = ['slack', 'notion', 'github', 'gmail'] as const
|
||||
|
||||
const bySlug = new Map(allIntegrations.map((i) => [i.slug, i]))
|
||||
const featured = FEATURED_SLUGS.map((s) => bySlug.get(s)).filter(
|
||||
(i): i is Integration => i !== undefined
|
||||
)
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Integrations',
|
||||
description: `Connect ${INTEGRATION_COUNT}+ apps and services with Sim's AI workflow automation. Build intelligent pipelines with ${TOP_NAMES.join(', ')}, and more.`,
|
||||
description: `Connect ${INTEGRATION_COUNT}+ apps and services in Sim's AI workspace. Build agents that automate real work with ${TOP_NAMES.join(', ')}, and more.`,
|
||||
keywords: [
|
||||
'workflow automation integrations',
|
||||
'AI workflow automation',
|
||||
'no-code automation',
|
||||
'AI workspace integrations',
|
||||
'AI agent integrations',
|
||||
'AI agent builder integrations',
|
||||
...TOP_NAMES.flatMap((n) => [`${n} integration`, `${n} automation`]),
|
||||
...allIntegrations.slice(0, 20).map((i) => `${i.name} automation`),
|
||||
],
|
||||
openGraph: {
|
||||
title: 'Integrations for AI Workflow Automation | Sim',
|
||||
description: `Connect ${INTEGRATION_COUNT}+ apps with Sim. Build AI-powered pipelines that link ${TOP_NAMES.join(', ')}, and every tool your team uses.`,
|
||||
title: 'Integrations | Sim AI Workspace',
|
||||
description: `Connect ${INTEGRATION_COUNT}+ apps in Sim's AI workspace. Build agents that link ${TOP_NAMES.join(', ')}, and every tool your team uses.`,
|
||||
url: `${baseUrl}/integrations`,
|
||||
type: 'website',
|
||||
images: [
|
||||
@@ -45,7 +55,7 @@ export const metadata: Metadata = {
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: 'Integrations | Sim',
|
||||
description: `Connect ${INTEGRATION_COUNT}+ apps with Sim's AI workflow automation.`,
|
||||
description: `Connect ${INTEGRATION_COUNT}+ apps in Sim's AI workspace.`,
|
||||
images: [
|
||||
{ url: `${baseUrl}/opengraph-image.png`, alt: 'Sim Integrations for AI Workflow Automation' },
|
||||
],
|
||||
@@ -72,7 +82,7 @@ export default function IntegrationsPage() {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'ItemList',
|
||||
name: 'Sim AI Workflow Integrations',
|
||||
description: `Complete list of ${INTEGRATION_COUNT}+ integrations available in Sim for building AI-powered workflow automation.`,
|
||||
description: `Complete list of ${INTEGRATION_COUNT}+ integrations available in Sim's AI workspace for building and deploying AI agents.`,
|
||||
url: `${baseUrl}/integrations`,
|
||||
numberOfItems: INTEGRATION_COUNT,
|
||||
itemListElement: allIntegrations.map((integration, index) => ({
|
||||
@@ -90,7 +100,7 @@ export default function IntegrationsPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className='bg-[var(--landing-bg)]'>
|
||||
<script
|
||||
type='application/ld+json'
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbJsonLd) }}
|
||||
@@ -100,64 +110,81 @@ export default function IntegrationsPage() {
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(itemListJsonLd) }}
|
||||
/>
|
||||
|
||||
<div className='mx-auto max-w-[1200px] px-6 py-16 sm:px-8 md:px-12'>
|
||||
{/* Hero */}
|
||||
<section aria-labelledby='integrations-heading' className='mb-16'>
|
||||
{/* Hero */}
|
||||
<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]'
|
||||
>
|
||||
Integrations
|
||||
</Badge>
|
||||
|
||||
<div className='flex flex-col gap-4 xl:flex-row xl:items-end xl:justify-between'>
|
||||
<h1
|
||||
id='integrations-heading'
|
||||
className='mb-4 text-balance font-[500] text-[40px] text-[var(--landing-text)] leading-tight sm:text-[56px]'
|
||||
className='text-balance text-[28px] text-white leading-[100%] tracking-[-0.02em] lg:text-[40px]'
|
||||
>
|
||||
Integrations
|
||||
</h1>
|
||||
<p className='max-w-[640px] text-[18px] text-[var(--landing-text-muted)] leading-relaxed'>
|
||||
Connect every tool your team uses. Build AI-powered workflows that automate tasks across{' '}
|
||||
{TOP_NAMES.slice(0, 4).map((name, i, arr) => {
|
||||
const integration = allIntegrations.find((int) => int.name === name)
|
||||
const Icon = integration ? blockTypeToIconMap[integration.type] : undefined
|
||||
return (
|
||||
<span key={name} className='inline-flex items-center gap-[5px]'>
|
||||
{Icon && (
|
||||
<span
|
||||
aria-hidden='true'
|
||||
className='inline-flex shrink-0'
|
||||
style={{ opacity: 0.65 }}
|
||||
>
|
||||
<Icon className='h-[0.85em] w-[0.85em]' />
|
||||
</span>
|
||||
)}
|
||||
{name}
|
||||
{i < arr.length - 1 ? ', ' : ''}
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
{' and more.'}
|
||||
<p className='font-[430] font-season text-[var(--landing-text-muted)] text-sm leading-[150%] tracking-[0.02em] lg:text-base'>
|
||||
Connect every tool your team uses. Build agents that automate real work across{' '}
|
||||
{INTEGRATION_COUNT} apps and services.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Searchable grid — client component */}
|
||||
{/* Full-width divider */}
|
||||
<div className='mt-8 h-px w-full bg-[var(--landing-bg-elevated)]' />
|
||||
|
||||
{/* Border-railed content */}
|
||||
<div className='mx-5 border-[var(--landing-bg-elevated)] border-x lg:mx-16'>
|
||||
{/* Featured integrations — top */}
|
||||
{featured.length > 0 && (
|
||||
<>
|
||||
<nav aria-label='Featured integrations' className='flex flex-col sm:flex-row'>
|
||||
{featured.map((integration) => (
|
||||
<IntegrationCard
|
||||
key={integration.type}
|
||||
integration={integration}
|
||||
IconComponent={blockTypeToIconMap[integration.type]}
|
||||
/>
|
||||
))}
|
||||
</nav>
|
||||
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* All Integrations — search, filters, rows */}
|
||||
<section aria-labelledby='all-integrations-heading'>
|
||||
<h2
|
||||
id='all-integrations-heading'
|
||||
className='mb-8 font-[500] text-[24px] text-[var(--landing-text)]'
|
||||
>
|
||||
All Integrations
|
||||
</h2>
|
||||
<div className='px-6 pt-10 pb-4'>
|
||||
<h2
|
||||
id='all-integrations-heading'
|
||||
className='mb-2 text-[20px] text-white leading-[100%] tracking-[-0.02em] lg:text-[24px]'
|
||||
>
|
||||
All Integrations
|
||||
</h2>
|
||||
</div>
|
||||
<IntegrationGrid integrations={allIntegrations} />
|
||||
</section>
|
||||
|
||||
{/* Integration request */}
|
||||
<div className='mt-16 flex flex-col items-start gap-3 border-[var(--landing-border)] border-t pt-10 sm:flex-row sm:items-center sm:justify-between'>
|
||||
<div className='flex flex-col items-start gap-3 px-6 py-6 sm:flex-row sm:items-center sm:justify-between'>
|
||||
<div>
|
||||
<p className='font-[500] text-[15px] text-[var(--landing-text)]'>
|
||||
<p className='text-[15px] text-white tracking-[-0.02em]'>
|
||||
Don't see the integration you need?
|
||||
</p>
|
||||
<p className='mt-0.5 text-[#555] text-[13px]'>
|
||||
<p className='mt-0.5 font-martian-mono text-[var(--landing-text-subtle)] text-xs uppercase tracking-[0.1em]'>
|
||||
Let us know and we'll prioritize it.
|
||||
</p>
|
||||
</div>
|
||||
<RequestIntegrationModal />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
{/* Closing full-width divider */}
|
||||
<div className='-mt-px h-px w-full bg-[var(--landing-bg-elevated)]' />
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import { usePostHog } from 'posthog-js/react'
|
||||
import { captureClientEvent, captureEvent } from '@/lib/posthog/client'
|
||||
import { captureClientEvent } from '@/lib/posthog/client'
|
||||
import type { PostHogEventMap } from '@/lib/posthog/events'
|
||||
|
||||
export function LandingAnalytics() {
|
||||
const posthog = usePostHog()
|
||||
|
||||
useEffect(() => {
|
||||
captureEvent(posthog, 'landing_page_viewed', {})
|
||||
}, [posthog])
|
||||
captureClientEvent('landing_page_viewed', {})
|
||||
}, [])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { martianMono } from '@/app/_styles/fonts/martian-mono/martian-mono'
|
||||
import { season } from '@/app/_styles/fonts/season/season'
|
||||
import {
|
||||
Collaboration,
|
||||
// Enterprise,
|
||||
Features,
|
||||
Footer,
|
||||
Hero,
|
||||
@@ -31,7 +30,7 @@ import { LandingAnalytics } from '@/app/(landing)/landing-analytics'
|
||||
* - Section `id` attributes serve as fragment anchors for precise AI citations.
|
||||
* - Content ordering prioritizes answer-first patterns: definition (Hero) ->
|
||||
* examples (Templates) -> capabilities (Features) -> social proof (Collaboration) ->
|
||||
* enterprise (Enterprise) -> pricing (Pricing) -> testimonials (Testimonials).
|
||||
* pricing (Pricing) -> testimonials (Testimonials).
|
||||
*/
|
||||
export default async function Landing() {
|
||||
const blogPosts = await getNavBlogPosts()
|
||||
@@ -53,16 +52,18 @@ export default async function Landing() {
|
||||
</header>
|
||||
<main id='main-content'>
|
||||
<article itemScope itemType='https://schema.org/WebPage'>
|
||||
<meta itemProp='name' content='Sim — Build AI Agents & Run Your Agentic Workforce' />
|
||||
<meta
|
||||
itemProp='name'
|
||||
content='Sim — The AI Workspace | Build, Deploy & Manage AI Agents'
|
||||
/>
|
||||
<meta
|
||||
itemProp='description'
|
||||
content='Sim is the open-source platform to build AI agents and run your agentic workforce.'
|
||||
content='Sim is the open-source AI workspace where teams build, deploy, and manage AI agents.'
|
||||
/>
|
||||
<Hero />
|
||||
<Templates />
|
||||
<Features />
|
||||
<Collaboration />
|
||||
{/* <Enterprise /> */}
|
||||
<Pricing />
|
||||
<Testimonials />
|
||||
</article>
|
||||
|
||||
@@ -3,14 +3,7 @@ import Link from 'next/link'
|
||||
import { notFound } from 'next/navigation'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { LandingFAQ } from '@/app/(landing)/components/landing-faq'
|
||||
import {
|
||||
Breadcrumbs,
|
||||
CapabilityTags,
|
||||
DetailItem,
|
||||
ModelCard,
|
||||
ProviderIcon,
|
||||
StatCard,
|
||||
} from '@/app/(landing)/models/components/model-primitives'
|
||||
import { FeaturedModelCard, ProviderIcon } from '@/app/(landing)/models/components/model-primitives'
|
||||
import {
|
||||
ALL_CATALOG_MODELS,
|
||||
buildModelCapabilityFacts,
|
||||
@@ -165,66 +158,88 @@ export default async function ModelPage({
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqJsonLd) }}
|
||||
/>
|
||||
|
||||
<div className='mx-auto max-w-[1280px] px-6 py-12 sm:px-8 md:px-12'>
|
||||
<Breadcrumbs
|
||||
items={[
|
||||
{ label: 'Home', href: '/' },
|
||||
{ label: 'Models', href: '/models' },
|
||||
{ label: provider.name, href: provider.href },
|
||||
{ label: model.displayName },
|
||||
]}
|
||||
/>
|
||||
<section className='bg-[var(--landing-bg)]'>
|
||||
<div className='px-5 pt-[60px] lg:px-16 lg:pt-[100px]'>
|
||||
<div className='mb-6'>
|
||||
<Link
|
||||
href={provider.href}
|
||||
className='group/link inline-flex items-center gap-1.5 font-season text-[var(--landing-text-muted)] text-sm tracking-[0.02em] hover:text-[var(--landing-text)]'
|
||||
>
|
||||
<svg
|
||||
className='h-3 w-3 shrink-0'
|
||||
viewBox='0 0 10 10'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<line
|
||||
x1='1'
|
||||
y1='5'
|
||||
x2='10'
|
||||
y2='5'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.33'
|
||||
strokeLinecap='square'
|
||||
className='origin-right scale-x-0 transition-transform duration-200 ease-out [transform-box:fill-box] group-hover/link:scale-x-100'
|
||||
/>
|
||||
<path
|
||||
d='M6.5 2L3.5 5L6.5 8'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.33'
|
||||
strokeLinecap='square'
|
||||
strokeLinejoin='miter'
|
||||
fill='none'
|
||||
className='group-hover/link:-translate-x-[30%] transition-transform duration-200 ease-out'
|
||||
/>
|
||||
</svg>
|
||||
Back to {provider.name}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<section aria-labelledby='model-heading' className='mb-14'>
|
||||
<div className='mb-6 flex items-start gap-4'>
|
||||
<div className='mb-6 flex items-center gap-5'>
|
||||
<ProviderIcon
|
||||
provider={provider}
|
||||
className='h-16 w-16 rounded-3xl'
|
||||
className='h-16 w-16 rounded-[5px]'
|
||||
iconClassName='h-8 w-8'
|
||||
/>
|
||||
<div className='min-w-0'>
|
||||
<p className='text-[12px] text-[var(--landing-text-muted)] uppercase tracking-[0.12em]'>
|
||||
<div>
|
||||
<p className='mb-0.5 font-martian-mono text-[var(--landing-text-subtle)] text-xs uppercase tracking-[0.1em]'>
|
||||
{provider.name} model
|
||||
</p>
|
||||
<h1
|
||||
id='model-heading'
|
||||
className='font-[500] text-[38px] text-[var(--landing-text)] leading-tight sm:text-[48px]'
|
||||
className='text-[28px] text-white leading-[100%] tracking-[-0.02em] sm:text-[36px] lg:text-[44px]'
|
||||
>
|
||||
{model.displayName}
|
||||
</h1>
|
||||
<p className='mt-2 break-all text-[13px] text-[var(--landing-text-muted)]'>
|
||||
Model ID: {model.id}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className='max-w-[820px] text-[17px] text-[var(--landing-text-muted)] leading-relaxed'>
|
||||
<p className='mb-8 max-w-[700px] text-[var(--landing-text-body)] text-base leading-[150%] tracking-[0.02em]'>
|
||||
{model.summary}
|
||||
{model.bestFor ? ` ${model.bestFor}` : ''}
|
||||
</p>
|
||||
|
||||
<div className='mt-8 flex flex-wrap gap-3'>
|
||||
<Link
|
||||
href={provider.href}
|
||||
className='inline-flex h-[34px] items-center rounded-[6px] border border-[var(--landing-border-strong)] px-3 font-[430] text-[14px] text-[var(--landing-text)] transition-colors hover:bg-[var(--landing-bg-elevated)]'
|
||||
>
|
||||
Explore {provider.name} models
|
||||
</Link>
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
<a
|
||||
href='https://sim.ai'
|
||||
className='inline-flex h-[34px] items-center rounded-[6px] border border-[var(--white)] bg-[var(--white)] px-3 font-[430] text-[14px] text-[var(--landing-text-dark)] transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
|
||||
className='inline-flex h-[32px] items-center gap-2 rounded-[5px] border border-white bg-white px-2.5 font-season text-black text-sm transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
|
||||
>
|
||||
Build with this model
|
||||
</a>
|
||||
<Link
|
||||
href={provider.href}
|
||||
className='inline-flex h-[32px] items-center rounded-[5px] border border-[var(--landing-border-strong)] px-2.5 font-season text-[var(--landing-text)] text-sm transition-colors hover:bg-[var(--landing-bg-elevated)]'
|
||||
>
|
||||
All {provider.name} models
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section
|
||||
aria-label='Model stats'
|
||||
className='mb-16 grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4'
|
||||
>
|
||||
<StatCard label='Input price' value={`${formatPrice(model.pricing.input)}/1M`} />
|
||||
<StatCard
|
||||
<div className='mt-8 h-px w-full bg-[var(--landing-bg-elevated)]' />
|
||||
|
||||
<div className='mx-5 border-[var(--landing-bg-elevated)] border-x lg:mx-16'>
|
||||
<InfoRow label='Input price' value={`${formatPrice(model.pricing.input)}/1M`} />
|
||||
<InfoRow
|
||||
label='Cached input'
|
||||
value={
|
||||
model.pricing.cachedInput !== undefined
|
||||
@@ -232,158 +247,72 @@ export default async function ModelPage({
|
||||
: 'N/A'
|
||||
}
|
||||
/>
|
||||
<StatCard label='Output price' value={`${formatPrice(model.pricing.output)}/1M`} />
|
||||
<StatCard
|
||||
<InfoRow label='Output price' value={`${formatPrice(model.pricing.output)}/1M`} />
|
||||
<InfoRow
|
||||
label='Context window'
|
||||
value={model.contextWindow ? formatTokenCount(model.contextWindow) : 'Unknown'}
|
||||
/>
|
||||
</section>
|
||||
<InfoRow
|
||||
label='Max output'
|
||||
value={
|
||||
model.capabilities.maxOutputTokens
|
||||
? `${formatTokenCount(getEffectiveMaxOutputTokens(model.capabilities))} tokens`
|
||||
: 'Not published'
|
||||
}
|
||||
/>
|
||||
<InfoRow label='Provider' value={provider.name} />
|
||||
<InfoRow label='Updated' value={formatUpdatedAt(model.pricing.updatedAt)} />
|
||||
{model.bestFor ? <InfoRow label='Best for' value={model.bestFor} /> : null}
|
||||
|
||||
<div className='grid grid-cols-1 gap-16 lg:grid-cols-[1fr_320px]'>
|
||||
<div className='min-w-0 space-y-16'>
|
||||
<section aria-labelledby='pricing-heading'>
|
||||
<h2
|
||||
id='pricing-heading'
|
||||
className='mb-2 font-[500] text-[28px] text-[var(--landing-text)]'
|
||||
>
|
||||
Pricing and limits
|
||||
</h2>
|
||||
<p className='mb-6 max-w-[760px] text-[15px] text-[var(--landing-text-muted)] leading-relaxed'>
|
||||
Pricing below is generated directly from the provider registry in Sim. All amounts
|
||||
are listed per one million tokens.
|
||||
</p>
|
||||
{capabilityFacts.length > 0 && (
|
||||
<>
|
||||
{capabilityFacts.map((item) => (
|
||||
<InfoRow key={item.label} label={item.label} value={item.value} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className='grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4'>
|
||||
<DetailItem label='Input price' value={`${formatPrice(model.pricing.input)}/1M`} />
|
||||
<DetailItem
|
||||
label='Cached input'
|
||||
value={
|
||||
model.pricing.cachedInput !== undefined
|
||||
? `${formatPrice(model.pricing.cachedInput)}/1M`
|
||||
: 'N/A'
|
||||
}
|
||||
/>
|
||||
<DetailItem
|
||||
label='Output price'
|
||||
value={`${formatPrice(model.pricing.output)}/1M`}
|
||||
/>
|
||||
<DetailItem label='Updated' value={formatUpdatedAt(model.pricing.updatedAt)} />
|
||||
<DetailItem
|
||||
label='Context window'
|
||||
value={
|
||||
model.contextWindow
|
||||
? `${formatTokenCount(model.contextWindow)} tokens`
|
||||
: 'Unknown'
|
||||
}
|
||||
/>
|
||||
<DetailItem
|
||||
label='Max output'
|
||||
value={
|
||||
model.capabilities.maxOutputTokens
|
||||
? `${formatTokenCount(getEffectiveMaxOutputTokens(model.capabilities))} tokens`
|
||||
: 'Not published'
|
||||
}
|
||||
/>
|
||||
<DetailItem label='Provider' value={provider.name} />
|
||||
{model.bestFor ? <DetailItem label='Best for' value={model.bestFor} /> : null}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section aria-labelledby='capabilities-heading'>
|
||||
<h2
|
||||
id='capabilities-heading'
|
||||
className='mb-2 font-[500] text-[28px] text-[var(--landing-text)]'
|
||||
>
|
||||
Capabilities
|
||||
</h2>
|
||||
<p className='mb-6 max-w-[760px] text-[15px] text-[var(--landing-text-muted)] leading-relaxed'>
|
||||
These capability flags are generated from the provider and model definitions tracked
|
||||
in Sim.
|
||||
</p>
|
||||
<CapabilityTags tags={model.capabilityTags} />
|
||||
<div className='mt-8 grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3'>
|
||||
{capabilityFacts.map((item) => (
|
||||
<DetailItem key={item.label} label={item.label} value={item.value} />
|
||||
{relatedModels.length > 0 && (
|
||||
<>
|
||||
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
|
||||
<nav aria-label='Related models' className='flex flex-col sm:flex-row'>
|
||||
{relatedModels.slice(0, 3).map((entry) => (
|
||||
<FeaturedModelCard key={entry.id} provider={provider} model={entry} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</nav>
|
||||
</>
|
||||
)}
|
||||
|
||||
{relatedModels.length > 0 && (
|
||||
<section aria-labelledby='related-models-heading'>
|
||||
<h2
|
||||
id='related-models-heading'
|
||||
className='mb-2 font-[500] text-[28px] text-[var(--landing-text)]'
|
||||
>
|
||||
Related {provider.name} models
|
||||
</h2>
|
||||
<p className='mb-8 max-w-[760px] text-[15px] text-[var(--landing-text-muted)] leading-relaxed'>
|
||||
Browse comparable models from the same provider to compare pricing, context
|
||||
window, and capability coverage.
|
||||
</p>
|
||||
<div className='grid grid-cols-1 gap-4 xl:grid-cols-2'>
|
||||
{relatedModels.map((entry) => (
|
||||
<ModelCard key={entry.id} provider={provider} model={entry} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
|
||||
|
||||
<section
|
||||
aria-labelledby='model-faq-heading'
|
||||
className='rounded-3xl border border-[var(--landing-border)] bg-[var(--landing-bg-card)] p-6 sm:p-8'
|
||||
<section aria-labelledby='model-faq-heading' className='px-6 py-10'>
|
||||
<h2
|
||||
id='model-faq-heading'
|
||||
className='mb-8 text-[20px] text-white leading-[100%] tracking-[-0.02em] lg:text-[24px]'
|
||||
>
|
||||
<h2
|
||||
id='model-faq-heading'
|
||||
className='font-[500] text-[28px] text-[var(--landing-text)]'
|
||||
>
|
||||
Frequently asked questions
|
||||
</h2>
|
||||
<div className='mt-3'>
|
||||
<LandingFAQ faqs={faqs} />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<aside className='space-y-5' aria-label='Model details'>
|
||||
<div className='rounded-3xl border border-[var(--landing-border)] bg-[var(--landing-bg-card)] p-5'>
|
||||
<h2 className='mb-4 font-[500] text-[16px] text-[var(--landing-text)]'>
|
||||
Quick details
|
||||
</h2>
|
||||
<div className='space-y-3'>
|
||||
<DetailItem label='Display name' value={model.displayName} />
|
||||
<DetailItem label='Provider' value={provider.name} />
|
||||
<DetailItem
|
||||
label='Context tracked'
|
||||
value={model.contextWindow ? 'Yes' : 'Partial'}
|
||||
/>
|
||||
<DetailItem
|
||||
label='Pricing updated'
|
||||
value={formatUpdatedAt(model.pricing.updatedAt)}
|
||||
/>
|
||||
</div>
|
||||
Frequently asked questions
|
||||
</h2>
|
||||
<div>
|
||||
<LandingFAQ faqs={faqs} />
|
||||
</div>
|
||||
|
||||
<div className='rounded-3xl border border-[var(--landing-border)] bg-[var(--landing-bg-card)] p-5'>
|
||||
<h2 className='mb-4 font-[500] text-[16px] text-[var(--landing-text)]'>
|
||||
Browse more
|
||||
</h2>
|
||||
<div className='space-y-2'>
|
||||
<Link
|
||||
href={provider.href}
|
||||
className='block rounded-xl px-3 py-2 text-[14px] text-[var(--landing-text-muted)] transition-colors hover:bg-[var(--landing-bg-elevated)] hover:text-[var(--landing-text)]'
|
||||
>
|
||||
All {provider.name} models
|
||||
</Link>
|
||||
<Link
|
||||
href='/models'
|
||||
className='block rounded-xl px-3 py-2 text-[14px] text-[var(--landing-text-muted)] transition-colors hover:bg-[var(--landing-bg-elevated)] hover:text-[var(--landing-text)]'
|
||||
>
|
||||
Full models directory
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div className='-mt-px h-px w-full bg-[var(--landing-bg-elevated)]' />
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function InfoRow({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<>
|
||||
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
|
||||
<div className='flex items-baseline justify-between gap-4 px-6 py-4'>
|
||||
<span className='font-martian-mono text-[var(--landing-text-subtle)] text-xs uppercase tracking-[0.1em]'>
|
||||
{label}
|
||||
</span>
|
||||
<span className='text-right text-[14px] text-white leading-snug'>{value}</span>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import type { Metadata } from 'next'
|
||||
import Link from 'next/link'
|
||||
import { notFound } from 'next/navigation'
|
||||
import { Badge } from '@/components/emcn'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { LandingFAQ } from '@/app/(landing)/components/landing-faq'
|
||||
import {
|
||||
Breadcrumbs,
|
||||
CapabilityTags,
|
||||
ModelCard,
|
||||
ProviderCard,
|
||||
ChevronArrow,
|
||||
FeaturedModelCard,
|
||||
FeaturedProviderCard,
|
||||
ProviderIcon,
|
||||
StatCard,
|
||||
} from '@/app/(landing)/models/components/model-primitives'
|
||||
import { ModelTimelineChart } from '@/app/(landing)/models/components/model-timeline-chart'
|
||||
import {
|
||||
buildProviderFaqs,
|
||||
formatPrice,
|
||||
formatTokenCount,
|
||||
getProviderBySlug,
|
||||
getProviderCapabilitySummary,
|
||||
MODEL_PROVIDERS_WITH_CATALOGS,
|
||||
TOP_MODEL_PROVIDERS,
|
||||
} from '@/app/(landing)/models/utils'
|
||||
@@ -95,7 +97,6 @@ export default async function ProviderModelsPage({
|
||||
}
|
||||
|
||||
const faqs = buildProviderFaqs(provider)
|
||||
const capabilitySummary = getProviderCapabilitySummary(provider)
|
||||
const relatedProviders = MODEL_PROVIDERS_WITH_CATALOGS.filter(
|
||||
(entry) => entry.id !== provider.id && TOP_MODEL_PROVIDERS.includes(entry.name)
|
||||
).slice(0, 4)
|
||||
@@ -153,142 +154,149 @@ export default async function ProviderModelsPage({
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqJsonLd) }}
|
||||
/>
|
||||
|
||||
<div className='mx-auto max-w-[1280px] px-6 py-12 sm:px-8 md:px-12'>
|
||||
<Breadcrumbs
|
||||
items={[
|
||||
{ label: 'Home', href: '/' },
|
||||
{ label: 'Models', href: '/models' },
|
||||
{ label: provider.name },
|
||||
]}
|
||||
/>
|
||||
<section className='bg-[var(--landing-bg)]'>
|
||||
<div className='px-5 pt-[60px] lg:px-16 lg:pt-[100px]'>
|
||||
<div className='mb-6'>
|
||||
<Link
|
||||
href='/models'
|
||||
className='group/link inline-flex items-center gap-1.5 font-season text-[var(--landing-text-muted)] text-sm tracking-[0.02em] hover:text-[var(--landing-text)]'
|
||||
>
|
||||
<svg
|
||||
className='h-3 w-3 shrink-0'
|
||||
viewBox='0 0 10 10'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<line
|
||||
x1='1'
|
||||
y1='5'
|
||||
x2='10'
|
||||
y2='5'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.33'
|
||||
strokeLinecap='square'
|
||||
className='origin-right scale-x-0 transition-transform duration-200 ease-out [transform-box:fill-box] group-hover/link:scale-x-100'
|
||||
/>
|
||||
<path
|
||||
d='M6.5 2L3.5 5L6.5 8'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.33'
|
||||
strokeLinecap='square'
|
||||
strokeLinejoin='miter'
|
||||
fill='none'
|
||||
className='group-hover/link:-translate-x-[30%] transition-transform duration-200 ease-out'
|
||||
/>
|
||||
</svg>
|
||||
Back to Models
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<section aria-labelledby='provider-heading' className='mb-14'>
|
||||
<div className='mb-6 flex items-center gap-4'>
|
||||
<ProviderIcon
|
||||
provider={provider}
|
||||
className='h-16 w-16 rounded-3xl'
|
||||
iconClassName='h-8 w-8'
|
||||
/>
|
||||
<div>
|
||||
<p className='text-[12px] text-[var(--landing-text-muted)] uppercase tracking-[0.12em]'>
|
||||
Provider
|
||||
</p>
|
||||
<Badge
|
||||
variant='blue'
|
||||
size='md'
|
||||
dot
|
||||
className='mb-5 bg-white/10 font-season text-white uppercase tracking-[0.02em]'
|
||||
>
|
||||
Provider
|
||||
</Badge>
|
||||
|
||||
<div className='flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between'>
|
||||
<div className='flex items-center gap-4'>
|
||||
<ProviderIcon
|
||||
provider={provider}
|
||||
className='h-12 w-12 rounded-[5px]'
|
||||
iconClassName='h-6 w-6'
|
||||
/>
|
||||
<h1
|
||||
id='provider-heading'
|
||||
className='font-[500] text-[38px] text-[var(--landing-text)] leading-tight sm:text-[48px]'
|
||||
className='font-[430] font-season text-[28px] text-white leading-[100%] tracking-[-0.02em] lg:text-[40px]'
|
||||
>
|
||||
{provider.name} models
|
||||
</h1>
|
||||
</div>
|
||||
<span className='shrink-0 font-martian-mono text-[var(--landing-text-subtle)] text-xs uppercase tracking-[0.1em]'>
|
||||
{provider.modelCount} models
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className='max-w-[820px] text-[17px] text-[var(--landing-text-muted)] leading-relaxed'>
|
||||
{provider.summary} Browse every {provider.name} model page generated from Sim's
|
||||
provider registry with human-readable names, pricing, context windows, and capability
|
||||
metadata.
|
||||
</p>
|
||||
<div className='mt-8 h-px w-full bg-[var(--landing-bg-elevated)]' />
|
||||
|
||||
<div className='mt-8 grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4'>
|
||||
<StatCard label='Models tracked' value={provider.modelCount.toString()} />
|
||||
<StatCard
|
||||
label='Default model'
|
||||
value={provider.defaultModelDisplayName || 'Dynamic'}
|
||||
compact
|
||||
/>
|
||||
<StatCard
|
||||
label='Metadata coverage'
|
||||
value={provider.contextInformationAvailable ? 'Tracked' : 'Partial'}
|
||||
compact
|
||||
/>
|
||||
<StatCard
|
||||
label='Featured models'
|
||||
value={provider.featuredModels.length.toString()}
|
||||
compact
|
||||
/>
|
||||
</div>
|
||||
<div className='mx-5 border-[var(--landing-bg-elevated)] border-x lg:mx-16'>
|
||||
{provider.featuredModels.length > 0 && (
|
||||
<>
|
||||
<nav aria-label='Featured models' className='flex flex-col sm:flex-row'>
|
||||
{provider.featuredModels.slice(0, 3).map((model) => (
|
||||
<FeaturedModelCard key={model.id} provider={provider} model={model} />
|
||||
))}
|
||||
</nav>
|
||||
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className='mt-6'>
|
||||
<CapabilityTags tags={provider.providerCapabilityTags} />
|
||||
</div>
|
||||
</section>
|
||||
<ModelTimelineChart models={provider.models} providerId={provider.id} />
|
||||
|
||||
<section aria-labelledby='provider-models-heading' className='mb-16'>
|
||||
<h2
|
||||
id='provider-models-heading'
|
||||
className='mb-2 font-[500] text-[28px] text-[var(--landing-text)]'
|
||||
>
|
||||
All {provider.name} models
|
||||
</h2>
|
||||
<p className='mb-8 max-w-[760px] text-[15px] text-[var(--landing-text-muted)] leading-relaxed'>
|
||||
Every model below links to a dedicated SEO page with exact pricing, context window,
|
||||
capability support, and related model recommendations.
|
||||
</p>
|
||||
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
|
||||
|
||||
<div className='grid grid-cols-1 gap-4 xl:grid-cols-2'>
|
||||
{provider.models.map((model) => (
|
||||
<ModelCard key={model.id} provider={provider} model={model} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
{provider.models.map((model) => (
|
||||
<div key={model.id}>
|
||||
<Link
|
||||
href={model.href}
|
||||
className='group/link flex items-center gap-4 px-6 py-4 transition-colors hover:bg-[var(--landing-bg-elevated)]'
|
||||
>
|
||||
<div className='flex min-w-0 flex-1 flex-col gap-0.5'>
|
||||
<h3 className='text-[14px] text-white leading-snug tracking-[-0.02em]'>
|
||||
{model.displayName}
|
||||
</h3>
|
||||
<p className='line-clamp-1 hidden text-[12px] text-[var(--landing-text-muted)] leading-[150%] sm:block'>
|
||||
{model.id}
|
||||
</p>
|
||||
</div>
|
||||
<span className='hidden shrink-0 font-martian-mono text-[11px] text-[var(--landing-text-muted)] uppercase tracking-[0.1em] md:block'>
|
||||
{formatPrice(model.pricing.input)}/1M in
|
||||
</span>
|
||||
<span className='hidden shrink-0 font-martian-mono text-[11px] text-[var(--landing-text-muted)] uppercase tracking-[0.1em] md:block'>
|
||||
{formatPrice(model.pricing.output)}/1M out
|
||||
</span>
|
||||
{model.contextWindow ? (
|
||||
<span className='hidden shrink-0 font-martian-mono text-[11px] text-[var(--landing-text-muted)] uppercase tracking-[0.1em] lg:block'>
|
||||
{formatTokenCount(model.contextWindow)} ctx
|
||||
</span>
|
||||
) : null}
|
||||
<ChevronArrow />
|
||||
</Link>
|
||||
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
|
||||
</div>
|
||||
))}
|
||||
|
||||
<section
|
||||
aria-labelledby='lineup-snapshot-heading'
|
||||
className='mb-16 rounded-3xl border border-[var(--landing-border)] bg-[var(--landing-bg-card)] p-6 sm:p-8'
|
||||
>
|
||||
<h2
|
||||
id='lineup-snapshot-heading'
|
||||
className='mb-2 font-[500] text-[28px] text-[var(--landing-text)]'
|
||||
>
|
||||
Lineup snapshot
|
||||
</h2>
|
||||
<p className='mb-8 max-w-[760px] text-[15px] text-[var(--landing-text-muted)] leading-relaxed'>
|
||||
A quick view of the strongest differentiators in the {provider.name} model lineup based
|
||||
on the metadata currently tracked in Sim.
|
||||
</p>
|
||||
{relatedProviders.length > 0 && (
|
||||
<>
|
||||
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
|
||||
<nav aria-label='Related providers' className='flex flex-col sm:flex-row'>
|
||||
{relatedProviders.map((entry) => (
|
||||
<FeaturedProviderCard key={entry.id} provider={entry} />
|
||||
))}
|
||||
</nav>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className='grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3'>
|
||||
{capabilitySummary.map((item) => (
|
||||
<StatCard key={item.label} label={item.label} value={item.value} compact />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
|
||||
|
||||
{relatedProviders.length > 0 && (
|
||||
<section aria-labelledby='related-providers-heading' className='mb-16'>
|
||||
<section aria-labelledby='provider-faq-heading' className='px-6 py-10'>
|
||||
<h2
|
||||
id='related-providers-heading'
|
||||
className='mb-2 font-[500] text-[28px] text-[var(--landing-text)]'
|
||||
id='provider-faq-heading'
|
||||
className='mb-8 text-[20px] text-white leading-[100%] tracking-[-0.02em] lg:text-[24px]'
|
||||
>
|
||||
Compare with other providers
|
||||
Frequently asked questions
|
||||
</h2>
|
||||
<p className='mb-8 max-w-[760px] text-[15px] text-[var(--landing-text-muted)] leading-relaxed'>
|
||||
Explore similar provider hubs to compare model lineups, pricing surfaces, and
|
||||
long-context coverage across the broader AI ecosystem.
|
||||
</p>
|
||||
|
||||
<div className='grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4'>
|
||||
{relatedProviders.map((entry) => (
|
||||
<ProviderCard key={entry.id} provider={entry} />
|
||||
))}
|
||||
<div>
|
||||
<LandingFAQ faqs={faqs} />
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<section
|
||||
aria-labelledby='provider-faq-heading'
|
||||
className='rounded-3xl border border-[var(--landing-border)] bg-[var(--landing-bg-card)] p-6 sm:p-8'
|
||||
>
|
||||
<h2
|
||||
id='provider-faq-heading'
|
||||
className='font-[500] text-[28px] text-[var(--landing-text)]'
|
||||
>
|
||||
Frequently asked questions
|
||||
</h2>
|
||||
<div className='mt-3'>
|
||||
<LandingFAQ faqs={faqs} />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div className='-mt-px h-px w-full bg-[var(--landing-bg-elevated)]' />
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
9
apps/sim/app/(landing)/models/components/consts.ts
Normal file
9
apps/sim/app/(landing)/models/components/consts.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { MODEL_CATALOG_PROVIDERS } from '@/app/(landing)/models/utils'
|
||||
|
||||
const colorMap = new Map(
|
||||
MODEL_CATALOG_PROVIDERS.filter((p) => p.color).map((p) => [p.id, p.color as string])
|
||||
)
|
||||
|
||||
export function getProviderColor(providerId: string): string {
|
||||
return colorMap.get(providerId) ?? '#888888'
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
'use client'
|
||||
|
||||
import type { ComponentType } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { getProviderColor } from '@/app/(landing)/models/components/consts'
|
||||
import type { CatalogModel } from '@/app/(landing)/models/utils'
|
||||
import {
|
||||
formatPrice,
|
||||
formatTokenCount,
|
||||
MODEL_CATALOG_PROVIDERS,
|
||||
} from '@/app/(landing)/models/utils'
|
||||
|
||||
/** Providers that host other providers' models — deprioritized to avoid duplicates. */
|
||||
const RESELLER_PROVIDERS = new Set(
|
||||
MODEL_CATALOG_PROVIDERS.filter((p) => p.isReseller).map((p) => p.id)
|
||||
)
|
||||
|
||||
const PROVIDER_ICON_MAP: Record<string, ComponentType<{ className?: string }>> = (() => {
|
||||
const map: Record<string, ComponentType<{ className?: string }>> = {}
|
||||
for (const provider of MODEL_CATALOG_PROVIDERS) {
|
||||
if (provider.icon) {
|
||||
map[provider.id] = provider.icon
|
||||
}
|
||||
}
|
||||
return map
|
||||
})()
|
||||
|
||||
function selectComparisonModels(models: CatalogModel[]): CatalogModel[] {
|
||||
const seen = new Set<string>()
|
||||
const result: CatalogModel[] = []
|
||||
|
||||
const sorted = [...models].sort((a, b) => {
|
||||
const score = (m: CatalogModel) => {
|
||||
const reseller = RESELLER_PROVIDERS.has(m.providerId) ? -50 : 0
|
||||
const reasoning = m.capabilities.reasoningEffort || m.capabilities.thinking ? 10 : 0
|
||||
const context = (m.contextWindow ?? 0) / 100000
|
||||
return reseller + reasoning + context
|
||||
}
|
||||
return score(b) - score(a)
|
||||
})
|
||||
|
||||
for (const model of sorted) {
|
||||
if (result.length >= 10) break
|
||||
const nameKey = model.displayName.toLowerCase()
|
||||
if (seen.has(nameKey)) continue
|
||||
seen.add(nameKey)
|
||||
result.push(model)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
interface ModelLabelProps {
|
||||
model: CatalogModel
|
||||
}
|
||||
|
||||
function ModelLabel({ model }: ModelLabelProps) {
|
||||
const Icon = PROVIDER_ICON_MAP[model.providerId]
|
||||
|
||||
return (
|
||||
<div className='flex w-[140px] shrink-0 items-center justify-end gap-1.5 sm:w-[180px]'>
|
||||
{Icon && <Icon className='h-3.5 w-3.5 shrink-0' />}
|
||||
<span className='truncate font-medium text-[13px] text-[var(--landing-text)] leading-none tracking-[-0.01em]'>
|
||||
{model.displayName}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface ChartProps {
|
||||
models: CatalogModel[]
|
||||
}
|
||||
|
||||
function StackedCostChart({ models }: ChartProps) {
|
||||
const data = useMemo(() => {
|
||||
const entries = models
|
||||
.map((model) => ({
|
||||
model,
|
||||
input: model.pricing.input,
|
||||
output: model.pricing.output,
|
||||
total: model.pricing.input + model.pricing.output,
|
||||
}))
|
||||
.filter((e) => e.total > 0)
|
||||
.sort((a, b) => a.total - b.total)
|
||||
|
||||
const maxTotal = entries.length > 0 ? Math.max(...entries.map((e) => e.total)) : 0
|
||||
return { entries, maxTotal }
|
||||
}, [models])
|
||||
|
||||
if (data.entries.length === 0) return null
|
||||
|
||||
return (
|
||||
<div className='flex flex-col gap-3'>
|
||||
<div className='flex flex-col gap-1'>
|
||||
<h3 className='text-[20px] text-white leading-[100%] tracking-[-0.02em] lg:text-[24px]'>
|
||||
Cost
|
||||
</h3>
|
||||
<span className='font-[430] font-season text-[var(--landing-text-muted)] text-sm leading-[150%] tracking-[0.02em]'>
|
||||
Per 1M tokens
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col gap-1.5'>
|
||||
{data.entries.map(({ model, input, output, total }) => {
|
||||
const totalPct = data.maxTotal > 0 ? (total / data.maxTotal) * 100 : 0
|
||||
const inputPct = total > 0 ? (input / total) * 100 : 0
|
||||
const color = getProviderColor(model.providerId)
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={model.id}
|
||||
href={model.href}
|
||||
className='-mx-2 flex items-center gap-3 rounded-md px-2 transition-colors hover:bg-[var(--landing-bg-elevated)]'
|
||||
>
|
||||
<ModelLabel model={model} />
|
||||
<div className='relative flex h-7 min-w-0 flex-1 items-center'>
|
||||
<div
|
||||
className='flex h-full overflow-hidden rounded-r-[3px]'
|
||||
style={{ width: `${Math.max(totalPct, 3)}%` }}
|
||||
>
|
||||
<div
|
||||
className='h-full'
|
||||
style={{
|
||||
width: `${inputPct}%`,
|
||||
backgroundColor: color,
|
||||
opacity: 0.8,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className='h-full'
|
||||
style={{
|
||||
width: `${100 - inputPct}%`,
|
||||
backgroundColor: color,
|
||||
opacity: 0.35,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span className='ml-2.5 shrink-0 font-mono text-[var(--landing-text-muted)] text-xs'>
|
||||
{formatPrice(input)} input / {formatPrice(output)} output
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ContextWindowChart({ models }: ChartProps) {
|
||||
const data = useMemo(() => {
|
||||
const entries = models
|
||||
.map((model) => ({
|
||||
model,
|
||||
value: model.contextWindow,
|
||||
}))
|
||||
.filter((e): e is { model: CatalogModel; value: number } => e.value !== null && e.value > 0)
|
||||
.sort((a, b) => a.value - b.value)
|
||||
|
||||
const maxValue = entries.length > 0 ? Math.max(...entries.map((e) => e.value)) : 0
|
||||
return { entries, maxValue }
|
||||
}, [models])
|
||||
|
||||
if (data.entries.length === 0) return null
|
||||
|
||||
return (
|
||||
<div className='flex flex-col gap-3'>
|
||||
<div className='flex flex-col gap-1'>
|
||||
<h3 className='text-[20px] text-white leading-[100%] tracking-[-0.02em] lg:text-[24px]'>
|
||||
Context window
|
||||
</h3>
|
||||
<span className='font-[430] font-season text-[var(--landing-text-muted)] text-sm leading-[150%] tracking-[0.02em]'>
|
||||
Max tokens
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col gap-1.5'>
|
||||
{data.entries.map(({ model, value }) => {
|
||||
const pct = data.maxValue > 0 ? (value / data.maxValue) * 100 : 0
|
||||
const color = getProviderColor(model.providerId)
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={model.id}
|
||||
href={model.href}
|
||||
className='-mx-2 flex items-center gap-3 rounded-md px-2 transition-colors hover:bg-[var(--landing-bg-elevated)]'
|
||||
>
|
||||
<ModelLabel model={model} />
|
||||
<div className='relative flex h-7 min-w-0 flex-1 items-center'>
|
||||
<div
|
||||
className='h-full rounded-r-[3px]'
|
||||
style={{
|
||||
width: `${Math.max(pct, 3)}%`,
|
||||
backgroundColor: color,
|
||||
opacity: 0.8,
|
||||
}}
|
||||
/>
|
||||
<span className='ml-2.5 shrink-0 font-mono text-[var(--landing-text-muted)] text-xs'>
|
||||
{formatTokenCount(value)}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface ModelComparisonChartsProps {
|
||||
models: CatalogModel[]
|
||||
}
|
||||
|
||||
export function ModelComparisonCharts({ models }: ModelComparisonChartsProps) {
|
||||
const comparisonModels = useMemo(() => selectComparisonModels(models), [models])
|
||||
|
||||
return (
|
||||
<section aria-labelledby='comparison-heading'>
|
||||
<div className='px-6 pt-10 pb-4'>
|
||||
<h2
|
||||
id='comparison-heading'
|
||||
className='mb-2 text-[20px] text-white leading-[100%] tracking-[-0.02em] lg:text-[24px]'
|
||||
>
|
||||
Compare models
|
||||
</h2>
|
||||
<p className='font-[430] font-season text-[var(--landing-text-muted)] text-sm leading-[150%] tracking-[0.02em]'>
|
||||
Side-by-side comparison of top models across key metrics.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
|
||||
|
||||
<div className='flex flex-col sm:flex-row'>
|
||||
<div className='flex-1 p-6'>
|
||||
<StackedCostChart models={comparisonModels} />
|
||||
</div>
|
||||
<div className='h-px w-full bg-[var(--landing-bg-elevated)] sm:h-auto sm:w-px' />
|
||||
<div className='flex-1 p-6'>
|
||||
<ContextWindowChart models={comparisonModels} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -3,20 +3,14 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { Input } from '@/components/emcn'
|
||||
import { SearchIcon } from '@/components/icons'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import {
|
||||
CapabilityTags,
|
||||
DetailItem,
|
||||
ModelCard,
|
||||
ProviderIcon,
|
||||
StatCard,
|
||||
} from '@/app/(landing)/models/components/model-primitives'
|
||||
import { ChevronArrow, ProviderIcon } from '@/app/(landing)/models/components/model-primitives'
|
||||
import {
|
||||
type CatalogModel,
|
||||
type CatalogProvider,
|
||||
formatPrice,
|
||||
formatTokenCount,
|
||||
MODEL_PROVIDERS_WITH_CATALOGS,
|
||||
MODEL_PROVIDERS_WITH_DYNAMIC_CATALOGS,
|
||||
TOTAL_MODELS,
|
||||
} from '@/app/(landing)/models/utils'
|
||||
|
||||
export function ModelDirectory() {
|
||||
@@ -35,7 +29,7 @@ export function ModelDirectory() {
|
||||
|
||||
const normalizedQuery = query.trim().toLowerCase()
|
||||
|
||||
const { filteredProviders, filteredDynamicProviders, visibleModelCount } = useMemo(() => {
|
||||
const { filteredProviders, filteredDynamicProviders } = useMemo(() => {
|
||||
const filteredProviders = MODEL_PROVIDERS_WITH_CATALOGS.map((provider) => {
|
||||
const providerMatchesSearch =
|
||||
normalizedQuery.length > 0 && provider.searchText.includes(normalizedQuery)
|
||||
@@ -77,15 +71,9 @@ export function ModelDirectory() {
|
||||
return provider.searchText.includes(normalizedQuery)
|
||||
})
|
||||
|
||||
const visibleModelCount = filteredProviders.reduce(
|
||||
(count, provider) => count + provider.models.length,
|
||||
0
|
||||
)
|
||||
|
||||
return {
|
||||
filteredProviders,
|
||||
filteredDynamicProviders,
|
||||
visibleModelCount,
|
||||
}
|
||||
}, [activeProviderId, normalizedQuery])
|
||||
|
||||
@@ -93,170 +81,143 @@ export function ModelDirectory() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='mb-8 flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between'>
|
||||
<div className='relative max-w-[560px] flex-1'>
|
||||
<SearchIcon
|
||||
<div className='mb-6 flex flex-col gap-4 px-6 sm:flex-row sm:items-center'>
|
||||
<div className='relative max-w-[480px] flex-1'>
|
||||
<svg
|
||||
aria-hidden='true'
|
||||
className='-translate-y-1/2 pointer-events-none absolute top-1/2 left-3 h-4 w-4 text-[var(--landing-text-muted)]'
|
||||
/>
|
||||
className='-translate-y-1/2 pointer-events-none absolute top-1/2 left-3 h-4 w-4 text-[#555]'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth={2}
|
||||
viewBox='0 0 24 24'
|
||||
>
|
||||
<circle cx={11} cy={11} r={8} />
|
||||
<path d='m21 21-4.35-4.35' />
|
||||
</svg>
|
||||
<Input
|
||||
type='search'
|
||||
placeholder='Search models, providers, capabilities, or pricing details'
|
||||
placeholder='Search models, providers, or capabilities…'
|
||||
value={query}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
className='h-11 border-[var(--landing-border)] bg-[var(--landing-bg-card)] pl-10 text-[var(--landing-text)] placeholder:text-[var(--landing-text-muted)]'
|
||||
className='pl-9'
|
||||
aria-label='Search AI models'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p className='text-[13px] text-[var(--landing-text-muted)] leading-relaxed'>
|
||||
Showing {visibleModelCount.toLocaleString('en-US')} of{' '}
|
||||
{TOTAL_MODELS.toLocaleString('en-US')} models
|
||||
{activeProviderId ? ' in one provider' : ''}.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='mb-10 flex flex-wrap gap-2'>
|
||||
<FilterButton
|
||||
isActive={activeProviderId === null}
|
||||
<div className='mb-6 flex flex-wrap gap-2 px-6'>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => setActiveProviderId(null)}
|
||||
label={`All providers (${MODEL_PROVIDERS_WITH_CATALOGS.length})`}
|
||||
/>
|
||||
className={`rounded-[5px] border px-[9px] py-0.5 text-[13.5px] transition-colors ${
|
||||
activeProviderId === null
|
||||
? 'border-[var(--landing-border-strong)] bg-[var(--landing-bg-elevated)] text-[var(--landing-text)]'
|
||||
: 'border-[var(--landing-border-strong)] text-[var(--landing-text)] hover:bg-[var(--landing-bg-elevated)]'
|
||||
}`}
|
||||
>
|
||||
All
|
||||
</button>
|
||||
{providerOptions.map((provider) => (
|
||||
<FilterButton
|
||||
<button
|
||||
key={provider.id}
|
||||
isActive={activeProviderId === provider.id}
|
||||
type='button'
|
||||
onClick={() =>
|
||||
setActiveProviderId(activeProviderId === provider.id ? null : provider.id)
|
||||
}
|
||||
label={`${provider.name} (${provider.count})`}
|
||||
/>
|
||||
className={`rounded-[5px] border px-[9px] py-0.5 text-[13.5px] transition-colors ${
|
||||
activeProviderId === provider.id
|
||||
? 'border-[var(--landing-border-strong)] bg-[var(--landing-bg-elevated)] text-[var(--landing-text)]'
|
||||
: 'border-[var(--landing-border-strong)] text-[var(--landing-text)] hover:bg-[var(--landing-bg-elevated)]'
|
||||
}`}
|
||||
>
|
||||
{provider.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
|
||||
|
||||
{!hasResults ? (
|
||||
<div className='rounded-2xl border border-[var(--landing-border)] bg-[var(--landing-bg-card)] px-6 py-12 text-center'>
|
||||
<h3 className='font-[500] text-[18px] text-[var(--landing-text)]'>No matches found</h3>
|
||||
<p className='mt-2 text-[14px] text-[var(--landing-text-muted)] leading-relaxed'>
|
||||
<div className='px-6 py-12 text-center'>
|
||||
<h3 className='text-[18px] text-white'>No matches found</h3>
|
||||
<p className='mt-2 text-[var(--landing-text-muted)] text-sm leading-[150%]'>
|
||||
Try a provider name like OpenAI or Anthropic, or search for capabilities like
|
||||
structured outputs, reasoning, or deep research.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className='space-y-10'>
|
||||
{filteredProviders.map((provider) => (
|
||||
<section
|
||||
key={provider.id}
|
||||
aria-labelledby={`${provider.id}-heading`}
|
||||
className='rounded-3xl border border-[var(--landing-border)] bg-[var(--landing-bg-card)] p-6 sm:p-8'
|
||||
>
|
||||
<div className='mb-6 flex flex-col gap-5 border-[var(--landing-border)] border-b pb-6 lg:flex-row lg:items-start lg:justify-between'>
|
||||
<div className='min-w-0'>
|
||||
<div className='mb-3 flex items-center gap-3'>
|
||||
<ProviderIcon provider={provider} />
|
||||
<div>
|
||||
<p className='text-[12px] text-[var(--landing-text-muted)]'>Provider</p>
|
||||
<h2
|
||||
id={`${provider.id}-heading`}
|
||||
className='font-[500] text-[24px] text-[var(--landing-text)]'
|
||||
>
|
||||
{provider.name}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{filteredProviders.map((provider, index) => (
|
||||
<section key={provider.id} aria-labelledby={`${provider.id}-heading`}>
|
||||
{index > 0 && <div className='h-px w-full bg-[var(--landing-bg-elevated)]' />}
|
||||
|
||||
<p className='max-w-[720px] text-[15px] text-[var(--landing-text-muted)] leading-relaxed'>
|
||||
{provider.description}
|
||||
</p>
|
||||
<Link
|
||||
href={provider.href}
|
||||
className='mt-3 inline-flex text-[#555] text-[13px] transition-colors hover:text-[var(--landing-text-muted)]'
|
||||
<Link
|
||||
href={provider.href}
|
||||
className='group/link flex items-center gap-3 px-6 py-4 transition-colors hover:bg-[var(--landing-bg-elevated)]'
|
||||
>
|
||||
<ProviderIcon
|
||||
provider={provider}
|
||||
className='h-8 w-8 rounded-[5px]'
|
||||
iconClassName='h-4 w-4'
|
||||
/>
|
||||
<div className='min-w-0 flex-1'>
|
||||
<h2
|
||||
id={`${provider.id}-heading`}
|
||||
className='text-[14px] text-white leading-snug tracking-[-0.02em]'
|
||||
>
|
||||
View provider page →
|
||||
</Link>
|
||||
{provider.name}
|
||||
</h2>
|
||||
<p className='line-clamp-1 hidden text-[12px] text-[var(--landing-text-muted)] leading-[150%] sm:block'>
|
||||
{provider.modelCount} models · {provider.description}
|
||||
</p>
|
||||
</div>
|
||||
<ChevronArrow />
|
||||
</Link>
|
||||
|
||||
<div className='grid shrink-0 grid-cols-2 gap-3 sm:grid-cols-3'>
|
||||
<StatCard label='Models' value={provider.models.length.toString()} />
|
||||
<StatCard
|
||||
label='Default'
|
||||
value={provider.defaultModelDisplayName || 'Dynamic'}
|
||||
compact
|
||||
/>
|
||||
<StatCard
|
||||
label='Context info'
|
||||
value={provider.contextInformationAvailable ? 'Tracked' : 'Limited'}
|
||||
compact
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mb-6'>
|
||||
<CapabilityTags tags={provider.providerCapabilityTags} />
|
||||
</div>
|
||||
|
||||
<div className='grid grid-cols-1 gap-4 xl:grid-cols-2'>
|
||||
{provider.models.map((model) => (
|
||||
<ModelCard key={model.id} provider={provider} model={model} />
|
||||
))}
|
||||
</div>
|
||||
{provider.models.map((model) => (
|
||||
<ModelRow key={model.id} provider={provider} model={model} />
|
||||
))}
|
||||
</section>
|
||||
))}
|
||||
|
||||
{filteredDynamicProviders.length > 0 && (
|
||||
<section
|
||||
aria-labelledby='dynamic-catalogs-heading'
|
||||
className='rounded-3xl border border-[var(--landing-border)] bg-[var(--landing-bg-card)] p-6 sm:p-8'
|
||||
>
|
||||
<div className='mb-6'>
|
||||
<section aria-labelledby='dynamic-catalogs-heading'>
|
||||
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
|
||||
|
||||
<div className='px-6 pt-8 pb-6'>
|
||||
<h2
|
||||
id='dynamic-catalogs-heading'
|
||||
className='font-[500] text-[24px] text-[var(--landing-text)]'
|
||||
className='text-[18px] text-white leading-[100%] tracking-[-0.02em] lg:text-[20px]'
|
||||
>
|
||||
Dynamic model catalogs
|
||||
</h2>
|
||||
<p className='mt-2 max-w-[760px] text-[15px] text-[var(--landing-text-muted)] leading-relaxed'>
|
||||
These providers are supported by Sim, but their model lists are loaded dynamically
|
||||
at runtime rather than hard-coded into the public catalog.
|
||||
<p className='mt-2 text-[var(--landing-text-muted)] text-sm leading-[150%]'>
|
||||
These providers load their model lists dynamically at runtime.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4'>
|
||||
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
|
||||
|
||||
<nav aria-label='Dynamic catalog providers' className='flex flex-col lg:flex-row'>
|
||||
{filteredDynamicProviders.map((provider) => (
|
||||
<article
|
||||
<div
|
||||
key={provider.id}
|
||||
className='rounded-2xl border border-[var(--landing-border)] bg-[var(--landing-bg-elevated)] p-5'
|
||||
className='flex flex-1 items-center gap-3 border-[var(--landing-bg-elevated)] border-t px-6 py-4 first:border-t-0 lg:border-t-0 lg:border-l lg:first:border-l-0'
|
||||
>
|
||||
<div className='mb-4 flex items-center gap-3'>
|
||||
<ProviderIcon provider={provider} />
|
||||
<div className='min-w-0'>
|
||||
<h3 className='font-[500] text-[16px] text-[var(--landing-text)]'>
|
||||
{provider.name}
|
||||
</h3>
|
||||
<p className='text-[12px] text-[var(--landing-text-muted)]'>
|
||||
{provider.id}
|
||||
</p>
|
||||
</div>
|
||||
<ProviderIcon
|
||||
provider={provider}
|
||||
className='h-8 w-8 rounded-[5px]'
|
||||
iconClassName='h-4 w-4'
|
||||
/>
|
||||
<div className='min-w-0 flex-1'>
|
||||
<h3 className='text-[14px] text-white leading-snug'>{provider.name}</h3>
|
||||
<p className='line-clamp-1 text-[12px] text-[var(--landing-text-muted)] leading-[150%]'>
|
||||
{provider.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p className='text-[13px] text-[var(--landing-text-muted)] leading-relaxed'>
|
||||
{provider.description}
|
||||
</p>
|
||||
|
||||
<div className='mt-4 space-y-3 text-[13px]'>
|
||||
<DetailItem
|
||||
label='Default'
|
||||
value={provider.defaultModelDisplayName || 'Selected at runtime'}
|
||||
/>
|
||||
<DetailItem label='Catalog source' value='Loaded dynamically inside Sim' />
|
||||
</div>
|
||||
|
||||
<div className='mt-4'>
|
||||
<CapabilityTags tags={provider.providerCapabilityTags} />
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
@@ -265,27 +226,33 @@ export function ModelDirectory() {
|
||||
)
|
||||
}
|
||||
|
||||
function FilterButton({
|
||||
isActive,
|
||||
onClick,
|
||||
label,
|
||||
}: {
|
||||
isActive: boolean
|
||||
onClick: () => void
|
||||
label: string
|
||||
}) {
|
||||
function ModelRow({ provider, model }: { provider: CatalogProvider; model: CatalogModel }) {
|
||||
return (
|
||||
<button
|
||||
type='button'
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
'rounded-full border px-3 py-1.5 text-[12px] transition-colors',
|
||||
isActive
|
||||
? 'border-[#555] bg-[#333] text-[var(--landing-text)]'
|
||||
: 'border-[var(--landing-border)] bg-transparent text-[var(--landing-text-muted)] hover:border-[var(--landing-border-strong)] hover:text-[var(--landing-text)]'
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
<>
|
||||
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
|
||||
<Link
|
||||
href={model.href}
|
||||
className='group/link flex items-center gap-4 px-6 py-4 transition-colors hover:bg-[var(--landing-bg-elevated)]'
|
||||
>
|
||||
<ProviderIcon
|
||||
provider={provider}
|
||||
className='h-8 w-8 shrink-0 rounded-[5px]'
|
||||
iconClassName='h-4 w-4'
|
||||
/>
|
||||
|
||||
<div className='flex min-w-0 flex-1 flex-col gap-0.5'>
|
||||
<h3 className='text-[14px] text-white leading-snug tracking-[-0.02em]'>
|
||||
{model.displayName}
|
||||
</h3>
|
||||
<p className='line-clamp-1 hidden text-[12px] text-[var(--landing-text-muted)] leading-[150%] sm:block'>
|
||||
{model.id} · Input {formatPrice(model.pricing.input)}/1M · Output{' '}
|
||||
{formatPrice(model.pricing.output)}/1M
|
||||
{model.contextWindow ? ` · ${formatTokenCount(model.contextWindow)} context` : ''}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ChevronArrow />
|
||||
</Link>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ export function Breadcrumbs({ items }: { items: Array<{ label: string; href?: st
|
||||
return (
|
||||
<nav
|
||||
aria-label='Breadcrumb'
|
||||
className='mb-10 flex flex-wrap items-center gap-2 text-[#555] text-[13px]'
|
||||
className='mb-10 flex flex-wrap items-center gap-2 font-martian-mono text-[var(--landing-text-subtle)] text-xs uppercase tracking-[0.1em]'
|
||||
>
|
||||
{items.map((item, index) => (
|
||||
<span key={`${item.label}-${index}`} className='inline-flex items-center gap-2'>
|
||||
@@ -35,7 +35,7 @@ export function Breadcrumbs({ items }: { items: Array<{ label: string; href?: st
|
||||
|
||||
export function ProviderIcon({
|
||||
provider,
|
||||
className = 'h-12 w-12 rounded-2xl',
|
||||
className = 'h-12 w-12 rounded-[5px]',
|
||||
iconClassName = 'h-6 w-6',
|
||||
}: {
|
||||
provider: Pick<CatalogProvider, 'icon' | 'name'>
|
||||
@@ -51,7 +51,7 @@ export function ProviderIcon({
|
||||
{Icon ? (
|
||||
<Icon className={iconClassName} />
|
||||
) : (
|
||||
<span className='font-[500] text-[14px] text-[var(--landing-text)]'>
|
||||
<span className='font-[430] text-[14px] text-[var(--landing-text)]'>
|
||||
{provider.name.slice(0, 2).toUpperCase()}
|
||||
</span>
|
||||
)}
|
||||
@@ -69,12 +69,12 @@ export function StatCard({
|
||||
compact?: boolean
|
||||
}) {
|
||||
return (
|
||||
<div className='rounded-2xl border border-[var(--landing-border)] bg-[var(--landing-bg-elevated)] px-4 py-3'>
|
||||
<p className='text-[11px] text-[var(--landing-text-muted)] uppercase tracking-[0.08em]'>
|
||||
<div className='rounded-[5px] border border-[var(--landing-border)] bg-[var(--landing-bg-elevated)] px-4 py-3'>
|
||||
<p className='font-martian-mono text-[var(--landing-text-subtle)] text-xs uppercase tracking-[0.1em]'>
|
||||
{label}
|
||||
</p>
|
||||
<p
|
||||
className={`mt-1 font-[500] text-[var(--landing-text)] ${
|
||||
className={`mt-1 font-[430] text-[var(--landing-text)] ${
|
||||
compact ? 'break-all text-[12px] leading-snug' : 'text-[18px]'
|
||||
}`}
|
||||
>
|
||||
@@ -86,17 +86,49 @@ export function StatCard({
|
||||
|
||||
export function DetailItem({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div className='rounded-xl border border-[var(--landing-border)] bg-[var(--landing-bg-card)] px-3 py-2'>
|
||||
<p className='text-[11px] text-[var(--landing-text-muted)] uppercase tracking-[0.08em]'>
|
||||
<div className='rounded-[5px] border border-[var(--landing-border)] bg-[var(--landing-bg-card)] px-3 py-2'>
|
||||
<p className='font-martian-mono text-[var(--landing-text-subtle)] text-xs uppercase tracking-[0.1em]'>
|
||||
{label}
|
||||
</p>
|
||||
<p className='mt-1 break-words font-[500] text-[12px] text-[var(--landing-text)] leading-snug'>
|
||||
<p className='mt-1 break-words font-[430] text-[12px] text-[var(--landing-text)] leading-snug'>
|
||||
{value}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function ChevronArrow() {
|
||||
return (
|
||||
<svg
|
||||
className='h-3 w-3 shrink-0 text-[var(--landing-text-subtle)]'
|
||||
viewBox='0 0 10 10'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
aria-hidden='true'
|
||||
>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
export function CapabilityTags({ tags }: { tags: string[] }) {
|
||||
if (tags.length === 0) {
|
||||
return null
|
||||
@@ -116,23 +148,76 @@ export function CapabilityTags({ tags }: { tags: string[] }) {
|
||||
)
|
||||
}
|
||||
|
||||
export function FeaturedProviderCard({ provider }: { provider: CatalogProvider }) {
|
||||
return (
|
||||
<Link
|
||||
href={provider.href}
|
||||
className='group flex flex-1 flex-col gap-4 border-[var(--landing-bg-elevated)] border-t p-6 transition-colors first:border-t-0 hover:bg-[var(--landing-bg-elevated)] sm:border-t-0 sm:border-l sm:first:border-l-0'
|
||||
>
|
||||
<ProviderIcon
|
||||
provider={provider}
|
||||
className='h-10 w-10 rounded-[5px]'
|
||||
iconClassName='h-5 w-5'
|
||||
/>
|
||||
<div className='flex flex-col gap-2'>
|
||||
<h3 className='text-lg text-white leading-tight tracking-[-0.01em]'>{provider.name}</h3>
|
||||
<p className='line-clamp-2 text-[var(--landing-text-muted)] text-sm leading-[150%]'>
|
||||
{provider.description}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export function FeaturedModelCard({
|
||||
provider,
|
||||
model,
|
||||
}: {
|
||||
provider: CatalogProvider
|
||||
model: CatalogModel
|
||||
}) {
|
||||
return (
|
||||
<Link
|
||||
href={model.href}
|
||||
className='group flex flex-1 flex-col gap-4 border-[var(--landing-bg-elevated)] border-t p-6 transition-colors first:border-t-0 hover:bg-[var(--landing-bg-elevated)] sm:border-t-0 sm:border-l sm:first:border-l-0'
|
||||
>
|
||||
<ProviderIcon
|
||||
provider={provider}
|
||||
className='h-10 w-10 rounded-[5px]'
|
||||
iconClassName='h-5 w-5'
|
||||
/>
|
||||
<div className='flex flex-col gap-2'>
|
||||
<span className='font-martian-mono text-[var(--landing-text-subtle)] text-xs uppercase tracking-[0.1em]'>
|
||||
{provider.name}
|
||||
</span>
|
||||
<h3 className='text-lg text-white leading-tight tracking-[-0.01em]'>{model.displayName}</h3>
|
||||
<p className='line-clamp-2 text-[var(--landing-text-muted)] text-sm leading-[150%]'>
|
||||
{model.summary}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export function ProviderCard({ provider }: { provider: CatalogProvider }) {
|
||||
return (
|
||||
<Link
|
||||
href={provider.href}
|
||||
className='group flex h-full flex-col rounded-lg border border-[var(--landing-border)] bg-[var(--landing-bg-card)] p-4 transition-colors hover:border-[var(--landing-border-strong)] hover:bg-[var(--landing-bg-elevated)]'
|
||||
className='group flex h-full flex-col rounded-[5px] border border-[var(--landing-border)] bg-[var(--landing-bg-card)] p-4 transition-colors hover:border-[var(--landing-border-strong)] hover:bg-[var(--landing-bg-elevated)]'
|
||||
>
|
||||
<div className='mb-4 flex items-center gap-3'>
|
||||
<ProviderIcon provider={provider} />
|
||||
<div className='min-w-0'>
|
||||
<h3 className='font-[500] text-[18px] text-[var(--landing-text)]'>{provider.name}</h3>
|
||||
<p className='text-[12px] text-[var(--landing-text-muted)]'>
|
||||
<h3 className='font-[430] font-season text-base text-white tracking-[-0.01em]'>
|
||||
{provider.name}
|
||||
</h3>
|
||||
<p className='font-martian-mono text-[var(--landing-text-subtle)] text-xs uppercase tracking-[0.1em]'>
|
||||
{provider.modelCount} models tracked
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className='mb-4 flex-1 text-[14px] text-[var(--landing-text-muted)] leading-relaxed'>
|
||||
<p className='mb-4 flex-1 text-[var(--landing-text-muted)] text-sm leading-[150%]'>
|
||||
{provider.description}
|
||||
</p>
|
||||
|
||||
@@ -165,26 +250,30 @@ export function ModelCard({
|
||||
return (
|
||||
<Link
|
||||
href={model.href}
|
||||
className='group flex h-full flex-col rounded-lg border border-[var(--landing-border)] bg-[var(--landing-bg-card)] p-4 transition-colors hover:border-[var(--landing-border-strong)] hover:bg-[var(--landing-bg-elevated)]'
|
||||
className='group flex h-full flex-col rounded-[5px] border border-[var(--landing-border)] bg-[var(--landing-bg-card)] p-4 transition-colors hover:border-[var(--landing-border-strong)] hover:bg-[var(--landing-bg-elevated)]'
|
||||
>
|
||||
<div className='mb-4 flex items-start gap-3'>
|
||||
<ProviderIcon
|
||||
provider={provider}
|
||||
className='h-10 w-10 rounded-xl'
|
||||
className='h-10 w-10 rounded-[5px]'
|
||||
iconClassName='h-5 w-5'
|
||||
/>
|
||||
<div className='min-w-0 flex-1'>
|
||||
{showProvider ? (
|
||||
<p className='mb-1 text-[12px] text-[var(--landing-text-muted)]'>{provider.name}</p>
|
||||
<p className='mb-1 font-martian-mono text-[var(--landing-text-subtle)] text-xs uppercase tracking-[0.1em]'>
|
||||
{provider.name}
|
||||
</p>
|
||||
) : null}
|
||||
<h3 className='break-all font-[500] text-[16px] text-[var(--landing-text)] leading-snug'>
|
||||
<h3 className='break-all font-[430] font-season text-base text-white leading-snug tracking-[-0.01em]'>
|
||||
{model.displayName}
|
||||
</h3>
|
||||
<p className='mt-1 break-all text-[12px] text-[var(--landing-text-muted)]'>{model.id}</p>
|
||||
<p className='mt-1 break-all font-martian-mono text-[var(--landing-text-subtle)] text-xs tracking-[0.1em]'>
|
||||
{model.id}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className='mb-3 line-clamp-3 flex-1 text-[12px] text-[var(--landing-text-muted)] leading-relaxed'>
|
||||
<p className='mb-3 line-clamp-3 flex-1 text-[var(--landing-text-muted)] text-sm leading-[150%]'>
|
||||
{model.summary}
|
||||
</p>
|
||||
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
'use client'
|
||||
|
||||
import { useMemo } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { getProviderColor } from '@/app/(landing)/models/components/consts'
|
||||
import type { CatalogModel } from '@/app/(landing)/models/utils'
|
||||
|
||||
function formatShortDate(date: string): string {
|
||||
try {
|
||||
return new Intl.DateTimeFormat('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
timeZone: 'UTC',
|
||||
}).format(new Date(date))
|
||||
} catch {
|
||||
return date
|
||||
}
|
||||
}
|
||||
|
||||
interface ModelTimelineChartProps {
|
||||
models: CatalogModel[]
|
||||
providerId: string
|
||||
}
|
||||
|
||||
const ITEM_WIDTH = 150
|
||||
|
||||
export function ModelTimelineChart({ models, providerId }: ModelTimelineChartProps) {
|
||||
const entries = useMemo(() => {
|
||||
return models
|
||||
.filter((m) => m.releaseDate !== null)
|
||||
.map((m) => ({
|
||||
model: m,
|
||||
date: new Date(m.releaseDate as string),
|
||||
dateStr: m.releaseDate as string,
|
||||
}))
|
||||
.sort((a, b) => a.date.getTime() - b.date.getTime())
|
||||
}, [models])
|
||||
|
||||
if (entries.length === 0) return null
|
||||
|
||||
const color = getProviderColor(providerId)
|
||||
|
||||
return (
|
||||
<section aria-labelledby='timeline-heading'>
|
||||
<div className='px-6 pt-10 pb-4'>
|
||||
<h2
|
||||
id='timeline-heading'
|
||||
className='mb-2 text-[20px] text-white leading-[100%] tracking-[-0.02em] lg:text-[24px]'
|
||||
>
|
||||
Release timeline
|
||||
</h2>
|
||||
<p className='font-[430] font-season text-[var(--landing-text-muted)] text-sm leading-[150%] tracking-[0.02em]'>
|
||||
When each model was first publicly available.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='overflow-x-auto px-6 pb-8'>
|
||||
{/* Fixed height: top labels + line + bottom labels */}
|
||||
<div
|
||||
className='relative h-[140px]'
|
||||
style={{ minWidth: `${entries.length * ITEM_WIDTH}px` }}
|
||||
>
|
||||
{/* Horizontal line — vertically centered */}
|
||||
<div className='absolute top-[70px] right-0 left-0 h-px bg-[var(--landing-border-strong)]' />
|
||||
|
||||
{entries.map(({ model, dateStr }, i) => {
|
||||
const left = i * ITEM_WIDTH + ITEM_WIDTH / 2
|
||||
const isAbove = i % 2 === 0
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={model.id}
|
||||
href={model.href}
|
||||
className='group absolute flex flex-col items-center'
|
||||
style={{
|
||||
left: `${left}px`,
|
||||
width: `${ITEM_WIDTH}px`,
|
||||
marginLeft: `${-ITEM_WIDTH / 2}px`,
|
||||
top: 0,
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
{/* Dot — centered exactly on the line (70px - 4.5px) */}
|
||||
<div
|
||||
className='-translate-x-1/2 absolute top-[66px] left-1/2 h-[9px] w-[9px] rounded-full transition-[filter,transform] duration-150 group-hover:scale-150 group-hover:brightness-150'
|
||||
style={{ backgroundColor: color, opacity: 0.85 }}
|
||||
/>
|
||||
|
||||
{/* Stem + label above */}
|
||||
{isAbove && (
|
||||
<div className='-translate-x-1/2 absolute bottom-[74px] left-1/2 flex flex-col items-center'>
|
||||
<div className='flex flex-col items-center gap-0.5 pb-1.5'>
|
||||
<span className='whitespace-nowrap font-medium text-[12px] text-[var(--landing-text)] leading-none tracking-[-0.01em] transition-colors group-hover:text-white'>
|
||||
{model.displayName}
|
||||
</span>
|
||||
<span className='whitespace-nowrap font-mono text-[10px] text-[var(--landing-text-muted)] leading-none'>
|
||||
{formatShortDate(dateStr)}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className='w-px'
|
||||
style={{ height: '10px', backgroundColor: color, opacity: 0.2 }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Stem + label below */}
|
||||
{!isAbove && (
|
||||
<div className='-translate-x-1/2 absolute top-[75px] left-1/2 flex flex-col items-center'>
|
||||
<div
|
||||
className='w-px'
|
||||
style={{ height: '10px', backgroundColor: color, opacity: 0.2 }}
|
||||
/>
|
||||
<div className='flex flex-col items-center gap-0.5 pt-1.5'>
|
||||
<span className='whitespace-nowrap font-medium text-[12px] text-[var(--landing-text)] leading-none tracking-[-0.01em] transition-colors group-hover:text-white'>
|
||||
{model.displayName}
|
||||
</span>
|
||||
<span className='whitespace-nowrap font-mono text-[10px] text-[var(--landing-text-muted)] leading-none'>
|
||||
{formatShortDate(dateStr)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -1,10 +1,15 @@
|
||||
import type { Metadata } from 'next'
|
||||
import Link from 'next/link'
|
||||
import { Badge } from '@/components/emcn'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { LandingFAQ } from '@/app/(landing)/components/landing-faq'
|
||||
import { ModelComparisonCharts } from '@/app/(landing)/models/components/model-comparison-charts'
|
||||
import { ModelDirectory } from '@/app/(landing)/models/components/model-directory'
|
||||
import { ModelCard, ProviderCard } from '@/app/(landing)/models/components/model-primitives'
|
||||
import {
|
||||
FeaturedModelCard,
|
||||
FeaturedProviderCard,
|
||||
} from '@/app/(landing)/models/components/model-primitives'
|
||||
import {
|
||||
ALL_CATALOG_MODELS,
|
||||
getPricingBounds,
|
||||
MODEL_CATALOG_PROVIDERS,
|
||||
MODEL_PROVIDERS_WITH_CATALOGS,
|
||||
@@ -17,32 +22,38 @@ const baseUrl = getBaseUrl()
|
||||
|
||||
const faqItems = [
|
||||
{
|
||||
question: 'What is the Sim AI models directory?',
|
||||
question: 'Which AI models are best for building agents and automated workflows?',
|
||||
answer:
|
||||
'The Sim AI models directory is a public catalog of the language models and providers tracked inside Sim. It shows provider coverage, model IDs, pricing per one million tokens, context windows, and supported capabilities such as reasoning controls, structured outputs, and deep research.',
|
||||
'The most important factors for agent tasks are reliable tool use (function calling), a large enough context window to track conversation history and tool outputs, and consistent instruction following. In Sim, OpenAI GPT-4.1, Anthropic Claude Sonnet, and Google Gemini 2.5 Pro are popular choices — each supports tool use, structured outputs, and context windows of 128K tokens or more. For cost-sensitive or high-throughput agents, Groq and Cerebras offer significantly faster inference at lower cost.',
|
||||
},
|
||||
{
|
||||
question: 'Can I compare models from multiple providers in one place?',
|
||||
question: 'What does context window size mean when running an AI agent?',
|
||||
answer:
|
||||
'Yes. This page organizes every tracked model by provider and lets you search across providers, model names, and capabilities. You can quickly compare OpenAI, Anthropic, Google, xAI, Mistral, Groq, Cerebras, Fireworks, Bedrock, and more from a single directory.',
|
||||
'The context window is the total number of tokens a model can process in a single call, including your system prompt, conversation history, tool call results, and any documents you pass in. For agents running multi-step tasks, context fills up quickly — each tool result and each retrieved document adds tokens. A 128K-token context window fits roughly 300 pages of text; models like Gemini 2.5 Pro support up to 1M tokens, enough to hold an entire codebase in a single pass.',
|
||||
},
|
||||
{
|
||||
question: 'Are these model prices shown per million tokens?',
|
||||
question: 'Are model prices shown per million tokens?',
|
||||
answer:
|
||||
'Yes. Input, cached input, and output prices on this page are shown per one million tokens based on the provider metadata tracked in Sim.',
|
||||
'Yes. Input, cached input, and output prices are all listed per one million tokens, matching how providers bill through their APIs. For agents that chain multiple calls, costs compound quickly — an agent completing 100 turns at 10K tokens each consumes roughly 1M tokens per session. Cached input pricing applies when a provider supports prompt caching, where a repeated prefix like a system prompt is billed at a reduced rate.',
|
||||
},
|
||||
{
|
||||
question: 'Does Sim support providers with dynamic model catalogs too?',
|
||||
question: 'Which AI models support tool use and function calling?',
|
||||
answer:
|
||||
'Yes. Some providers such as OpenRouter, Fireworks, Ollama, and vLLM load their model lists dynamically at runtime. Those providers are still shown here even when their full public model list is not hard-coded into the catalog.',
|
||||
'Tool use — also called function calling — lets an agent invoke external APIs, query databases, run code, or take any action you define. In Sim, all first-party models from OpenAI, Anthropic, Google, Mistral, Groq, Cerebras, and xAI support tool use. Look for the Tool Use capability tag on any model card in this directory to confirm support.',
|
||||
},
|
||||
{
|
||||
question: 'How do I add a model to a Sim agent?',
|
||||
answer:
|
||||
'Open Sim, add an Agent block, and select your provider and model from the model picker inside that block. Every model listed in this directory is available in the Agent block. Swapping models takes one click and does not affect the rest of your agent, making it straightforward to test different models on the same task without rebuilding anything.',
|
||||
},
|
||||
]
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'AI Models Directory',
|
||||
description: `Browse ${TOTAL_MODELS}+ AI models across ${TOTAL_MODEL_PROVIDERS} providers. Compare pricing, context windows, and capabilities for OpenAI, Anthropic, Google, xAI, Mistral, Bedrock, Groq, and more.`,
|
||||
description: `Browse and compare ${TOTAL_MODELS}+ AI models across ${TOTAL_MODEL_PROVIDERS} providers in Sim's AI workspace. Compare pricing, context windows, and capabilities — then use any model directly in your agents.`,
|
||||
keywords: [
|
||||
'AI models directory',
|
||||
'AI model comparison',
|
||||
'LLM model list',
|
||||
'model pricing',
|
||||
'context window comparison',
|
||||
@@ -82,15 +93,15 @@ export default function ModelsPage() {
|
||||
const flatModels = MODEL_CATALOG_PROVIDERS.flatMap((provider) =>
|
||||
provider.models.map((model) => ({ provider, model }))
|
||||
)
|
||||
const featuredProviders = MODEL_PROVIDERS_WITH_CATALOGS.slice(0, 6)
|
||||
const featuredModels = MODEL_PROVIDERS_WITH_CATALOGS.flatMap((provider) =>
|
||||
provider.featuredModels[0] ? [{ provider, model: provider.featuredModels[0] }] : []
|
||||
).slice(0, 6)
|
||||
const heroProviders = ['openai', 'anthropic', 'azure-openai', 'google', 'bedrock']
|
||||
.map((providerId) => MODEL_CATALOG_PROVIDERS.find((provider) => provider.id === providerId))
|
||||
.filter(
|
||||
(provider): provider is (typeof MODEL_CATALOG_PROVIDERS)[number] => provider !== undefined
|
||||
const featuredProviderOrder = ['anthropic', 'openai', 'google']
|
||||
const featuredProviders = featuredProviderOrder
|
||||
.map((id) => MODEL_PROVIDERS_WITH_CATALOGS.find((p) => p.id === id))
|
||||
.filter((p): p is (typeof MODEL_PROVIDERS_WITH_CATALOGS)[number] => p !== undefined)
|
||||
const featuredModels = featuredProviders
|
||||
.map((provider) =>
|
||||
provider.featuredModels[0] ? { provider, model: provider.featuredModels[0] } : null
|
||||
)
|
||||
.filter((entry): entry is NonNullable<typeof entry> => entry !== null)
|
||||
|
||||
const breadcrumbJsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
@@ -159,135 +170,89 @@ export default function ModelsPage() {
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqJsonLd) }}
|
||||
/>
|
||||
|
||||
<div className='mx-auto max-w-[1280px] px-6 py-16 sm:px-8 md:px-12'>
|
||||
<section aria-labelledby='models-heading' className='mb-14'>
|
||||
<div className='max-w-[840px]'>
|
||||
<p className='mb-3 text-[12px] text-[var(--landing-text-muted)] uppercase tracking-[0.16em]'>
|
||||
Public model directory
|
||||
</p>
|
||||
<section className='bg-[var(--landing-bg)]'>
|
||||
<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]'
|
||||
>
|
||||
Models
|
||||
</Badge>
|
||||
|
||||
<div className='flex flex-col gap-4 xl:flex-row xl:items-end xl:justify-between'>
|
||||
<h1
|
||||
id='models-heading'
|
||||
className='text-balance font-[500] text-[40px] text-[var(--landing-text)] leading-tight sm:text-[56px]'
|
||||
className='text-balance text-[28px] text-white leading-[100%] tracking-[-0.02em] lg:text-[40px]'
|
||||
>
|
||||
Browse AI models by provider, pricing, and capabilities
|
||||
Compare AI Models
|
||||
</h1>
|
||||
<p className='mt-5 max-w-[760px] text-[18px] text-[var(--landing-text-muted)] leading-relaxed'>
|
||||
Explore every model tracked in Sim across providers like{' '}
|
||||
{heroProviders.map((provider, index, allProviders) => {
|
||||
const Icon = provider.icon
|
||||
|
||||
return (
|
||||
<span key={provider.id}>
|
||||
<span className='inline-flex items-center gap-1 whitespace-nowrap align-[0.02em]'>
|
||||
{Icon ? (
|
||||
<span
|
||||
aria-hidden='true'
|
||||
className='relative top-[0.02em] inline-flex shrink-0 text-[var(--landing-text)]'
|
||||
>
|
||||
<Icon className='h-[0.82em] w-[0.82em]' />
|
||||
</span>
|
||||
) : null}
|
||||
<span>{provider.name}</span>
|
||||
</span>
|
||||
{index < allProviders.length - 1 ? ', ' : ''}
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
{
|
||||
' and more. Compare model IDs, token pricing, context windows, and features such as reasoning, structured outputs, and deep research from one clean catalog.'
|
||||
}
|
||||
<p className='font-[430] font-season text-[var(--landing-text-muted)] text-sm leading-[150%] tracking-[0.02em] lg:text-base'>
|
||||
Browse {TOTAL_MODELS} AI models across {TOTAL_MODEL_PROVIDERS} providers. Compare
|
||||
pricing, context windows, and capabilities.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mt-8 flex flex-wrap gap-3'>
|
||||
<a
|
||||
href='https://sim.ai'
|
||||
className='inline-flex h-[34px] items-center rounded-[6px] border border-[var(--white)] bg-[var(--white)] px-3 font-[430] text-[14px] text-[var(--landing-text-dark)] transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
|
||||
>
|
||||
Start building free
|
||||
</a>
|
||||
<Link
|
||||
href='/integrations'
|
||||
className='inline-flex h-[34px] items-center rounded-[6px] border border-[var(--landing-border-strong)] px-3 font-[430] text-[14px] text-[var(--landing-text)] transition-colors hover:bg-[var(--landing-bg-elevated)]'
|
||||
>
|
||||
Explore integrations
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
<div className='mt-8 h-px w-full bg-[var(--landing-bg-elevated)]' />
|
||||
|
||||
<section aria-labelledby='providers-heading' className='mb-16'>
|
||||
<div className='mb-6'>
|
||||
<div className='mx-5 border-[var(--landing-bg-elevated)] border-x lg:mx-16'>
|
||||
{featuredProviders.length > 0 && (
|
||||
<>
|
||||
<nav aria-label='Featured providers' className='flex flex-col sm:flex-row'>
|
||||
{featuredProviders.map((provider) => (
|
||||
<FeaturedProviderCard key={provider.id} provider={provider} />
|
||||
))}
|
||||
</nav>
|
||||
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
|
||||
</>
|
||||
)}
|
||||
|
||||
{featuredModels.length > 0 && (
|
||||
<>
|
||||
<nav aria-label='Featured models' className='flex flex-col sm:flex-row'>
|
||||
{featuredModels.map(({ provider, model }) => (
|
||||
<FeaturedModelCard key={model.id} provider={provider} model={model} />
|
||||
))}
|
||||
</nav>
|
||||
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
|
||||
</>
|
||||
)}
|
||||
|
||||
<ModelComparisonCharts models={ALL_CATALOG_MODELS} />
|
||||
|
||||
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
|
||||
|
||||
<section aria-labelledby='all-models-heading'>
|
||||
<div className='px-6 pt-10 pb-4'>
|
||||
<h2
|
||||
id='all-models-heading'
|
||||
className='mb-2 text-[20px] text-white leading-[100%] tracking-[-0.02em] lg:text-[24px]'
|
||||
>
|
||||
All models
|
||||
</h2>
|
||||
</div>
|
||||
<ModelDirectory />
|
||||
</section>
|
||||
|
||||
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
|
||||
|
||||
<section aria-labelledby='faq-heading' className='px-6 py-10'>
|
||||
<h2
|
||||
id='providers-heading'
|
||||
className='font-[500] text-[28px] text-[var(--landing-text)]'
|
||||
id='faq-heading'
|
||||
className='mb-8 text-[20px] text-white leading-[100%] tracking-[-0.02em] lg:text-[24px]'
|
||||
>
|
||||
Browse by provider
|
||||
Frequently asked questions
|
||||
</h2>
|
||||
<p className='mt-2 max-w-[760px] text-[15px] text-[var(--landing-text-muted)] leading-relaxed'>
|
||||
Each provider has its own generated SEO page with model lineup details, featured
|
||||
models, provider FAQs, and internal links to individual model pages.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<LandingFAQ faqs={faqItems} />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div className='grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3'>
|
||||
{featuredProviders.map((provider) => (
|
||||
<ProviderCard key={provider.id} provider={provider} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section aria-labelledby='featured-models-heading' className='mb-16'>
|
||||
<div className='mb-6'>
|
||||
<h2
|
||||
id='featured-models-heading'
|
||||
className='font-[500] text-[28px] text-[var(--landing-text)]'
|
||||
>
|
||||
Featured model pages
|
||||
</h2>
|
||||
<p className='mt-2 max-w-[760px] text-[15px] text-[var(--landing-text-muted)] leading-relaxed'>
|
||||
These pages are generated directly from the model registry and target high-intent
|
||||
search queries around pricing, context windows, and model capabilities.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='grid grid-cols-1 gap-4 xl:grid-cols-2'>
|
||||
{featuredModels.map(({ provider, model }) => (
|
||||
<ModelCard key={model.id} provider={provider} model={model} showProvider />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section aria-labelledby='all-models-heading'>
|
||||
<div className='mb-6'>
|
||||
<h2
|
||||
id='all-models-heading'
|
||||
className='font-[500] text-[28px] text-[var(--landing-text)]'
|
||||
>
|
||||
All models
|
||||
</h2>
|
||||
<p className='mt-2 max-w-[760px] text-[15px] text-[var(--landing-text-muted)] leading-relaxed'>
|
||||
Search the full catalog by provider, model ID, or capability. Use it to compare
|
||||
providers, sanity-check pricing, and quickly understand which models fit the workflow
|
||||
you're building. All pricing is shown per one million tokens using the metadata
|
||||
currently tracked in Sim.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ModelDirectory />
|
||||
</section>
|
||||
|
||||
<section
|
||||
aria-labelledby='faq-heading'
|
||||
className='mt-16 rounded-3xl border border-[var(--landing-border)] bg-[var(--landing-bg-card)] p-6 sm:p-8'
|
||||
>
|
||||
<h2 id='faq-heading' className='font-[500] text-[28px] text-[var(--landing-text)]'>
|
||||
Frequently asked questions
|
||||
</h2>
|
||||
<div className='mt-3'>
|
||||
<LandingFAQ faqs={faqItems} />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div className='-mt-px h-px w-full bg-[var(--landing-bg-elevated)]' />
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,12 +13,6 @@ const PROVIDER_PREFIXES: Record<string, string[]> = {
|
||||
vllm: ['vllm/'],
|
||||
}
|
||||
|
||||
const PROVIDER_NAME_OVERRIDES: Record<string, string> = {
|
||||
deepseek: 'DeepSeek',
|
||||
vllm: 'vLLM',
|
||||
xai: 'xAI',
|
||||
}
|
||||
|
||||
const TOKEN_REPLACEMENTS: Record<string, string> = {
|
||||
ai: 'AI',
|
||||
aws: 'AWS',
|
||||
@@ -108,6 +102,7 @@ export interface CatalogModel {
|
||||
providerName: string
|
||||
providerSlug: string
|
||||
contextWindow: number | null
|
||||
releaseDate: string | null
|
||||
pricing: PricingInfo
|
||||
capabilities: ModelCapabilities
|
||||
capabilityTags: string[]
|
||||
@@ -126,6 +121,8 @@ export interface CatalogProvider {
|
||||
defaultModel: string
|
||||
defaultModelDisplayName: string
|
||||
icon?: ComponentType<{ className?: string }>
|
||||
color?: string
|
||||
isReseller: boolean
|
||||
contextInformationAvailable: boolean
|
||||
providerCapabilityTags: string[]
|
||||
modelCount: number
|
||||
@@ -418,10 +415,6 @@ function buildModelSummary(
|
||||
return parts.filter(Boolean).join(' ')
|
||||
}
|
||||
|
||||
function getProviderDisplayName(providerId: string, providerName: string): string {
|
||||
return PROVIDER_NAME_OVERRIDES[providerId] ?? providerName
|
||||
}
|
||||
|
||||
function computeModelRelevanceScore(model: CatalogModel): number {
|
||||
return (
|
||||
(model.capabilities.reasoningEffort ? 10 : 0) +
|
||||
@@ -438,7 +431,7 @@ function compareModelsByRelevance(a: CatalogModel, b: CatalogModel): number {
|
||||
|
||||
const rawProviders = Object.values(PROVIDER_DEFINITIONS).map((provider) => {
|
||||
const providerSlug = slugify(provider.id)
|
||||
const providerDisplayName = getProviderDisplayName(provider.id, provider.name)
|
||||
const providerDisplayName = provider.name
|
||||
const providerCapabilityTags = buildCapabilityTags(provider.capabilities ?? {})
|
||||
|
||||
const models: CatalogModel[] = provider.models.map((model) => {
|
||||
@@ -464,6 +457,7 @@ const rawProviders = Object.values(PROVIDER_DEFINITIONS).map((provider) => {
|
||||
providerName: providerDisplayName,
|
||||
providerSlug,
|
||||
contextWindow: model.contextWindow ?? null,
|
||||
releaseDate: model.releaseDate ?? null,
|
||||
pricing: model.pricing,
|
||||
capabilities: mergedCapabilities,
|
||||
capabilityTags,
|
||||
@@ -507,6 +501,8 @@ const rawProviders = Object.values(PROVIDER_DEFINITIONS).map((provider) => {
|
||||
defaultModel: provider.defaultModel,
|
||||
defaultModelDisplayName,
|
||||
icon: provider.icon,
|
||||
color: provider.color,
|
||||
isReseller: provider.isReseller ?? false,
|
||||
contextInformationAvailable: provider.contextInformationAvailable !== false,
|
||||
providerCapabilityTags,
|
||||
modelCount: models.length,
|
||||
@@ -514,7 +510,6 @@ const rawProviders = Object.values(PROVIDER_DEFINITIONS).map((provider) => {
|
||||
featuredModels,
|
||||
searchText: [
|
||||
provider.name,
|
||||
providerDisplayName,
|
||||
provider.id,
|
||||
provider.description,
|
||||
provider.defaultModel,
|
||||
@@ -631,7 +626,13 @@ export function buildProviderFaqs(provider: CatalogProvider): CatalogFaq[] {
|
||||
const cheapestModel = getCheapestProviderModel(provider)
|
||||
const largestContextModel = getLargestContextProviderModel(provider)
|
||||
|
||||
return [
|
||||
const toolUseModels = provider.models.filter(
|
||||
(m) =>
|
||||
m.capabilities.toolUsageControl !== undefined ||
|
||||
provider.providerCapabilityTags.includes('Tool Use')
|
||||
)
|
||||
|
||||
const faqs: CatalogFaq[] = [
|
||||
{
|
||||
question: `What ${provider.name} models are available in Sim?`,
|
||||
answer: `Sim currently tracks ${provider.modelCount} ${provider.name} model${provider.modelCount === 1 ? '' : 's'} including ${provider.models
|
||||
@@ -662,10 +663,27 @@ export function buildProviderFaqs(provider: CatalogProvider): CatalogFaq[] {
|
||||
: `Context window details are not fully available for every ${provider.name} model in the public catalog.`,
|
||||
},
|
||||
]
|
||||
|
||||
if (toolUseModels.length > 0) {
|
||||
faqs.push({
|
||||
question: `Which ${provider.name} models support tool use and function calling in Sim?`,
|
||||
answer:
|
||||
toolUseModels.length === provider.modelCount
|
||||
? `All ${provider.name} models in Sim support tool use and function calling, allowing agents to invoke external APIs, query databases, and run custom actions.`
|
||||
: `${toolUseModels
|
||||
.slice(0, 5)
|
||||
.map((m) => m.displayName)
|
||||
.join(
|
||||
', '
|
||||
)}${toolUseModels.length > 5 ? ', and others' : ''} support tool use and function calling in Sim, enabling agents to invoke external APIs and run custom actions.`,
|
||||
})
|
||||
}
|
||||
|
||||
return faqs
|
||||
}
|
||||
|
||||
export function buildModelFaqs(provider: CatalogProvider, model: CatalogModel): CatalogFaq[] {
|
||||
return [
|
||||
const faqs: CatalogFaq[] = [
|
||||
{
|
||||
question: `What is ${model.displayName}?`,
|
||||
answer: `${model.displayName} is a ${provider.name} model available in Sim. ${model.summary}`,
|
||||
@@ -677,17 +695,26 @@ export function buildModelFaqs(provider: CatalogProvider, model: CatalogModel):
|
||||
{
|
||||
question: `What is the context window for ${model.displayName}?`,
|
||||
answer: model.contextWindow
|
||||
? `${model.displayName} supports a listed context window of ${formatTokenCount(model.contextWindow)} tokens in Sim.`
|
||||
? `${model.displayName} supports a context window of ${formatTokenCount(model.contextWindow)} tokens in Sim. In an agent, this determines how much conversation history, tool outputs, and retrieved documents the model can hold in a single call.`
|
||||
: `A public context window value is not currently tracked for ${model.displayName}.`,
|
||||
},
|
||||
{
|
||||
question: `What capabilities does ${model.displayName} support?`,
|
||||
answer:
|
||||
model.capabilityTags.length > 0
|
||||
? `${model.displayName} supports ${model.capabilityTags.join(', ')}.`
|
||||
: `${model.displayName} is available in Sim, but no extra public capability flags are currently tracked for this model.`,
|
||||
? `${model.displayName} supports the following capabilities in Sim: ${model.capabilityTags.join(', ')}.`
|
||||
: `${model.displayName} supports standard text generation in Sim. No additional capability flags such as tool use or structured outputs are currently tracked for this model.`,
|
||||
},
|
||||
]
|
||||
|
||||
if (model.bestFor) {
|
||||
faqs.push({
|
||||
question: `What is ${model.displayName} best used for?`,
|
||||
answer: `${model.bestFor} When used in a Sim workflow, it can be selected in any Agent block from the model picker.`,
|
||||
})
|
||||
}
|
||||
|
||||
return faqs
|
||||
}
|
||||
|
||||
export function buildModelCapabilityFacts(model: CatalogModel): CapabilityFact[] {
|
||||
|
||||
@@ -8,7 +8,7 @@ import Navbar from '@/app/(landing)/components/navbar/navbar'
|
||||
export const metadata: Metadata = {
|
||||
title: 'Partner Program',
|
||||
description:
|
||||
'Join the Sim partner program. Build, deploy, and sell AI workflow solutions. Earn your certification through Sim Academy.',
|
||||
"Join the Sim partner program. Build, deploy, and sell AI agent solutions powered by Sim's AI workspace. Earn your certification through Sim Academy.",
|
||||
metadataBase: new URL('https://sim.ai'),
|
||||
openGraph: {
|
||||
title: 'Partner Program | Sim',
|
||||
@@ -22,7 +22,7 @@ const PARTNER_TIERS = [
|
||||
name: 'Certified Partner',
|
||||
badge: 'Entry',
|
||||
color: '#3A3A3A',
|
||||
requirements: ['Complete Sim Academy certification', 'Deploy at least 1 live workflow'],
|
||||
requirements: ['Complete Sim Academy certification', 'Deploy at least 1 live agent'],
|
||||
perks: [
|
||||
'Official partner badge',
|
||||
'Listed in partner directory',
|
||||
@@ -69,13 +69,13 @@ const HOW_IT_WORKS = [
|
||||
step: '01',
|
||||
title: 'Sign up & complete Sim Academy',
|
||||
description:
|
||||
'Create an account and work through the Sim Academy certification program. Learn to build, integrate, and deploy AI workflows through hands-on canvas exercises.',
|
||||
'Create an account and work through the Sim Academy certification program. Learn to build, integrate, and deploy AI agents through hands-on exercises.',
|
||||
},
|
||||
{
|
||||
step: '02',
|
||||
title: 'Build & deploy real solutions',
|
||||
description:
|
||||
'Put your skills to work. Build workflow automations for clients, integrate Sim into existing products, or create your own Sim-powered applications.',
|
||||
'Put your skills to work. Build AI agents for clients, integrate Sim into existing products, or create your own Sim-powered applications.',
|
||||
},
|
||||
{
|
||||
step: '03',
|
||||
@@ -119,7 +119,7 @@ const BENEFITS = [
|
||||
icon: '📣',
|
||||
title: 'Community',
|
||||
description:
|
||||
'Join a growing community of Sim builders. Share workflows, collaborate on solutions, and shape the product roadmap.',
|
||||
'Join a growing community of Sim builders. Share agents, collaborate on solutions, and shape the product roadmap.',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -144,11 +144,11 @@ export default async function PartnersPage() {
|
||||
<h1 className='mb-5 text-[64px] text-white leading-[105%] tracking-[-0.03em]'>
|
||||
Build the future
|
||||
<br />
|
||||
of AI automation
|
||||
of AI agents
|
||||
</h1>
|
||||
<p className='mb-10 max-w-xl text-[#F6F6F0]/60 text-[18px] leading-[160%] tracking-[0.01em]'>
|
||||
Become a certified Sim partner. Complete Sim Academy, deploy real solutions, and earn
|
||||
recognition in the growing ecosystem of AI workflow builders.
|
||||
recognition in the growing ecosystem of AI agent builders.
|
||||
</p>
|
||||
<div className='flex items-center gap-4'>
|
||||
{/* TODO: Uncomment when academy is public */}
|
||||
|
||||
71
apps/sim/app/api/audit-logs/route.ts
Normal file
71
apps/sim/app/api/audit-logs/route.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { validateEnterpriseAuditAccess } from '@/app/api/v1/audit-logs/auth'
|
||||
import { formatAuditLogEntry } from '@/app/api/v1/audit-logs/format'
|
||||
import {
|
||||
buildFilterConditions,
|
||||
buildOrgScopeCondition,
|
||||
queryAuditLogs,
|
||||
} from '@/app/api/v1/audit-logs/query'
|
||||
|
||||
const logger = createLogger('AuditLogsAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const authResult = await validateEnterpriseAuditAccess(session.user.id)
|
||||
if (!authResult.success) {
|
||||
return authResult.response
|
||||
}
|
||||
|
||||
const { orgMemberIds } = authResult.context
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const search = searchParams.get('search')?.trim() || undefined
|
||||
const startDate = searchParams.get('startDate') || undefined
|
||||
const endDate = searchParams.get('endDate') || undefined
|
||||
const includeDeparted = searchParams.get('includeDeparted') === 'true'
|
||||
const limit = Math.min(Math.max(Number(searchParams.get('limit')) || 50, 1), 100)
|
||||
const cursor = searchParams.get('cursor') || undefined
|
||||
|
||||
if (startDate && Number.isNaN(Date.parse(startDate))) {
|
||||
return NextResponse.json({ error: 'Invalid startDate format' }, { status: 400 })
|
||||
}
|
||||
if (endDate && Number.isNaN(Date.parse(endDate))) {
|
||||
return NextResponse.json({ error: 'Invalid endDate format' }, { status: 400 })
|
||||
}
|
||||
|
||||
const scopeCondition = await buildOrgScopeCondition(orgMemberIds, includeDeparted)
|
||||
const filterConditions = buildFilterConditions({
|
||||
action: searchParams.get('action') || undefined,
|
||||
resourceType: searchParams.get('resourceType') || undefined,
|
||||
actorId: searchParams.get('actorId') || undefined,
|
||||
search,
|
||||
startDate,
|
||||
endDate,
|
||||
})
|
||||
|
||||
const { data, nextCursor } = await queryAuditLogs(
|
||||
[scopeCondition, ...filterConditions],
|
||||
limit,
|
||||
cursor
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: data.map(formatAuditLogEntry),
|
||||
nextCursor,
|
||||
})
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
logger.error('Audit logs fetch error', { error: message })
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
import { db } from '@sim/db'
|
||||
import { user } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||
import { auth } from '@/lib/auth'
|
||||
import { isSameOrigin } from '@/lib/core/utils/validation'
|
||||
|
||||
@@ -51,6 +55,26 @@ export async function POST(request: NextRequest) {
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
const [existingUser] = await db
|
||||
.select({ id: user.id, name: user.name, email: user.email })
|
||||
.from(user)
|
||||
.where(eq(user.email, email))
|
||||
.limit(1)
|
||||
|
||||
if (existingUser) {
|
||||
recordAudit({
|
||||
actorId: existingUser.id,
|
||||
actorName: existingUser.name,
|
||||
actorEmail: existingUser.email,
|
||||
action: AuditAction.PASSWORD_RESET_REQUESTED,
|
||||
resourceType: AuditResourceType.PASSWORD,
|
||||
resourceId: existingUser.id,
|
||||
resourceName: existingUser.email ?? undefined,
|
||||
description: `Password reset requested for ${existingUser.email}`,
|
||||
request,
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
logger.error('Error requesting password reset:', { error })
|
||||
|
||||
14
apps/sim/app/api/auth/providers/route.ts
Normal file
14
apps/sim/app/api/auth/providers/route.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { isRegistrationDisabled } from '@/lib/core/config/feature-flags'
|
||||
import { getOAuthProviderStatus } from '@/app/(auth)/components/oauth-provider-checker'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function GET() {
|
||||
const { githubAvailable, googleAvailable } = await getOAuthProviderStatus()
|
||||
return NextResponse.json({
|
||||
githubAvailable,
|
||||
googleAvailable,
|
||||
registrationDisabled: isRegistrationDisabled,
|
||||
})
|
||||
}
|
||||
@@ -64,8 +64,12 @@ export async function POST(request: NextRequest) {
|
||||
actorEmail: session.user.email,
|
||||
action: AuditAction.CREDIT_PURCHASED,
|
||||
resourceType: AuditResourceType.BILLING,
|
||||
resourceId: validation.data.requestId,
|
||||
description: `Purchased $${validation.data.amount} in credits`,
|
||||
metadata: { amount: validation.data.amount, requestId: validation.data.requestId },
|
||||
metadata: {
|
||||
amountDollars: validation.data.amount,
|
||||
requestId: validation.data.requestId,
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -233,6 +233,12 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
||||
resourceId: chatId,
|
||||
resourceName: title || existingChatRecord.title,
|
||||
description: `Updated chat deployment "${title || existingChatRecord.title}"`,
|
||||
metadata: {
|
||||
identifier: updatedIdentifier,
|
||||
authType: updateData.authType || existingChatRecord.authType,
|
||||
workflowId: workflowId || existingChatRecord.workflowId,
|
||||
chatUrl,
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -159,7 +159,12 @@ export async function POST(
|
||||
resourceId: id,
|
||||
resourceName: result.set.name,
|
||||
description: `Resent credential set invitation to ${invitation.email}`,
|
||||
metadata: { invitationId, targetEmail: invitation.email },
|
||||
metadata: {
|
||||
invitationId,
|
||||
targetEmail: invitation.email,
|
||||
providerId: result.set.providerId,
|
||||
credentialSetName: result.set.name,
|
||||
},
|
||||
request: req,
|
||||
})
|
||||
|
||||
|
||||
@@ -187,7 +187,12 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
actorEmail: session.user.email ?? undefined,
|
||||
resourceName: result.set.name,
|
||||
description: `Created invitation for credential set "${result.set.name}"${email ? ` to ${email}` : ''}`,
|
||||
metadata: { targetEmail: email || undefined },
|
||||
metadata: {
|
||||
invitationId: invitation.id,
|
||||
targetEmail: email || undefined,
|
||||
providerId: result.set.providerId,
|
||||
credentialSetName: result.set.name,
|
||||
},
|
||||
request: req,
|
||||
})
|
||||
|
||||
|
||||
@@ -197,7 +197,12 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i
|
||||
actorEmail: session.user.email ?? undefined,
|
||||
resourceName: result.set.name,
|
||||
description: `Removed member from credential set "${result.set.name}"`,
|
||||
metadata: { targetEmail: memberToRemove.email ?? undefined },
|
||||
metadata: {
|
||||
memberId,
|
||||
memberUserId: memberToRemove.userId,
|
||||
targetEmail: memberToRemove.email ?? undefined,
|
||||
providerId: result.set.providerId,
|
||||
},
|
||||
request: req,
|
||||
})
|
||||
|
||||
|
||||
@@ -142,6 +142,13 @@ export async function PUT(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
actorEmail: session.user.email ?? undefined,
|
||||
resourceName: updated?.name ?? result.set.name,
|
||||
description: `Updated credential set "${updated?.name ?? result.set.name}"`,
|
||||
metadata: {
|
||||
organizationId: result.set.organizationId,
|
||||
providerId: result.set.providerId,
|
||||
updatedFields: Object.keys(updates).filter(
|
||||
(k) => updates[k as keyof typeof updates] !== undefined
|
||||
),
|
||||
},
|
||||
request: req,
|
||||
})
|
||||
|
||||
@@ -199,6 +206,7 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i
|
||||
actorEmail: session.user.email ?? undefined,
|
||||
resourceName: result.set.name,
|
||||
description: `Deleted credential set "${result.set.name}"`,
|
||||
metadata: { organizationId: result.set.organizationId, providerId: result.set.providerId },
|
||||
request: req,
|
||||
})
|
||||
|
||||
|
||||
@@ -192,7 +192,12 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ tok
|
||||
resourceId: invitation.credentialSetId,
|
||||
resourceName: invitation.credentialSetName,
|
||||
description: `Accepted credential set invitation`,
|
||||
metadata: { invitationId: invitation.id },
|
||||
metadata: {
|
||||
invitationId: invitation.id,
|
||||
credentialSetId: invitation.credentialSetId,
|
||||
providerId: invitation.providerId,
|
||||
credentialSetName: invitation.credentialSetName,
|
||||
},
|
||||
request: req,
|
||||
})
|
||||
|
||||
|
||||
@@ -116,6 +116,7 @@ export async function DELETE(req: NextRequest) {
|
||||
resourceType: AuditResourceType.CREDENTIAL_SET,
|
||||
resourceId: credentialSetId,
|
||||
description: `Left credential set`,
|
||||
metadata: { credentialSetId },
|
||||
request: req,
|
||||
})
|
||||
|
||||
|
||||
@@ -179,6 +179,7 @@ export async function POST(req: Request) {
|
||||
actorEmail: session.user.email ?? undefined,
|
||||
resourceName: name,
|
||||
description: `Created credential set "${name}"`,
|
||||
metadata: { organizationId, providerId, credentialSetName: name },
|
||||
request: req,
|
||||
})
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { encryptSecret } from '@/lib/core/security/encryption'
|
||||
import { generateId } from '@/lib/core/utils/uuid'
|
||||
@@ -166,6 +167,23 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
updates.updatedAt = new Date()
|
||||
await db.update(credential).set(updates).where(eq(credential.id, id))
|
||||
|
||||
recordAudit({
|
||||
workspaceId: access.credential.workspaceId,
|
||||
actorId: session.user.id,
|
||||
actorName: session.user.name,
|
||||
actorEmail: session.user.email,
|
||||
action: AuditAction.CREDENTIAL_UPDATED,
|
||||
resourceType: AuditResourceType.CREDENTIAL,
|
||||
resourceId: id,
|
||||
resourceName: access.credential.displayName,
|
||||
description: `Updated ${access.credential.type} credential "${access.credential.displayName}"`,
|
||||
metadata: {
|
||||
credentialType: access.credential.type,
|
||||
updatedFields: Object.keys(updates).filter((k) => k !== 'updatedAt'),
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
const row = await getCredentialResponse(id, session.user.id)
|
||||
return NextResponse.json({ credential: row }, { status: 200 })
|
||||
} catch (error) {
|
||||
@@ -249,6 +267,20 @@ export async function DELETE(
|
||||
{ groups: { workspace: access.credential.workspaceId } }
|
||||
)
|
||||
|
||||
recordAudit({
|
||||
workspaceId: access.credential.workspaceId,
|
||||
actorId: session.user.id,
|
||||
actorName: session.user.name,
|
||||
actorEmail: session.user.email,
|
||||
action: AuditAction.CREDENTIAL_DELETED,
|
||||
resourceType: AuditResourceType.CREDENTIAL,
|
||||
resourceId: id,
|
||||
resourceName: access.credential.displayName,
|
||||
description: `Deleted personal env credential "${access.credential.envKey}"`,
|
||||
metadata: { credentialType: 'env_personal', envKey: access.credential.envKey },
|
||||
request,
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true }, { status: 200 })
|
||||
}
|
||||
|
||||
@@ -302,6 +334,20 @@ export async function DELETE(
|
||||
{ groups: { workspace: access.credential.workspaceId } }
|
||||
)
|
||||
|
||||
recordAudit({
|
||||
workspaceId: access.credential.workspaceId,
|
||||
actorId: session.user.id,
|
||||
actorName: session.user.name,
|
||||
actorEmail: session.user.email,
|
||||
action: AuditAction.CREDENTIAL_DELETED,
|
||||
resourceType: AuditResourceType.CREDENTIAL,
|
||||
resourceId: id,
|
||||
resourceName: access.credential.displayName,
|
||||
description: `Deleted workspace env credential "${access.credential.envKey}"`,
|
||||
metadata: { credentialType: 'env_workspace', envKey: access.credential.envKey },
|
||||
request,
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true }, { status: 200 })
|
||||
}
|
||||
|
||||
@@ -318,6 +364,23 @@ export async function DELETE(
|
||||
{ groups: { workspace: access.credential.workspaceId } }
|
||||
)
|
||||
|
||||
recordAudit({
|
||||
workspaceId: access.credential.workspaceId,
|
||||
actorId: session.user.id,
|
||||
actorName: session.user.name,
|
||||
actorEmail: session.user.email,
|
||||
action: AuditAction.CREDENTIAL_DELETED,
|
||||
resourceType: AuditResourceType.CREDENTIAL,
|
||||
resourceId: id,
|
||||
resourceName: access.credential.displayName,
|
||||
description: `Deleted ${access.credential.type} credential "${access.credential.displayName}"`,
|
||||
metadata: {
|
||||
credentialType: access.credential.type,
|
||||
providerId: access.credential.providerId,
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true }, { status: 200 })
|
||||
} catch (error) {
|
||||
logger.error('Failed to delete credential', error)
|
||||
|
||||
@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { encryptSecret } from '@/lib/core/security/encryption'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
@@ -612,6 +613,23 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
)
|
||||
|
||||
recordAudit({
|
||||
workspaceId,
|
||||
actorId: session.user.id,
|
||||
actorName: session.user.name,
|
||||
actorEmail: session.user.email,
|
||||
action: AuditAction.CREDENTIAL_CREATED,
|
||||
resourceType: AuditResourceType.CREDENTIAL,
|
||||
resourceId: credentialId,
|
||||
resourceName: resolvedDisplayName,
|
||||
description: `Created ${type} credential "${resolvedDisplayName}"`,
|
||||
metadata: {
|
||||
credentialType: type,
|
||||
providerId: resolvedProviderId,
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
return NextResponse.json({ credential: created }, { status: 201 })
|
||||
} catch (error: any) {
|
||||
if (error?.code === '23505') {
|
||||
|
||||
@@ -67,8 +67,13 @@ export async function POST(req: NextRequest) {
|
||||
actorEmail: session.user.email,
|
||||
action: AuditAction.ENVIRONMENT_UPDATED,
|
||||
resourceType: AuditResourceType.ENVIRONMENT,
|
||||
description: 'Updated global environment variables',
|
||||
metadata: { variableCount: Object.keys(variables).length },
|
||||
resourceId: session.user.id,
|
||||
description: `Updated ${Object.keys(variables).length} personal environment variable(s)`,
|
||||
metadata: {
|
||||
variableCount: Object.keys(variables).length,
|
||||
updatedKeys: Object.keys(variables),
|
||||
scope: 'personal',
|
||||
},
|
||||
request: req,
|
||||
})
|
||||
|
||||
|
||||
@@ -168,7 +168,13 @@ export async function POST(request: NextRequest) {
|
||||
resourceId: id,
|
||||
resourceName: name.trim(),
|
||||
description: `Created folder "${name.trim()}"`,
|
||||
metadata: { name: name.trim() },
|
||||
metadata: {
|
||||
name: name.trim(),
|
||||
workspaceId,
|
||||
parentId: parentId || undefined,
|
||||
color: color || '#6B7280',
|
||||
sortOrder: newFolder.sortOrder,
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -197,8 +197,14 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
||||
resourceId: id,
|
||||
actorName: session.user.name ?? undefined,
|
||||
actorEmail: session.user.email ?? undefined,
|
||||
resourceName: formRecord.title ?? undefined,
|
||||
description: `Updated form "${formRecord.title}"`,
|
||||
resourceName: (title || formRecord.title) ?? undefined,
|
||||
description: `Updated form "${title || formRecord.title}"`,
|
||||
metadata: {
|
||||
identifier: identifier || formRecord.identifier,
|
||||
workflowId: formRecord.workflowId,
|
||||
authType: authType || formRecord.authType,
|
||||
updatedFields: Object.keys(updateData).filter((k) => k !== 'updatedAt'),
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
@@ -255,6 +261,7 @@ export async function DELETE(
|
||||
actorEmail: session.user.email ?? undefined,
|
||||
resourceName: formRecord.title ?? undefined,
|
||||
description: `Deleted form "${formRecord.title}"`,
|
||||
metadata: { identifier: formRecord.identifier, workflowId: formRecord.workflowId },
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -208,6 +208,7 @@ export async function POST(request: NextRequest) {
|
||||
actorEmail: session.user.email ?? undefined,
|
||||
resourceName: title,
|
||||
description: `Created form "${title}" for workflow ${workflowId}`,
|
||||
metadata: { identifier, workflowId, authType, formUrl, showBranding },
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -194,7 +194,13 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) {
|
||||
resourceType: AuditResourceType.CONNECTOR,
|
||||
resourceId: connectorId,
|
||||
description: `Restored ${updated.length} excluded document(s) for knowledge base "${writeCheck.knowledgeBase.name}"`,
|
||||
metadata: { knowledgeBaseId, documentCount: updated.length },
|
||||
metadata: {
|
||||
knowledgeBaseId,
|
||||
knowledgeBaseName: writeCheck.knowledgeBase.name,
|
||||
operation: 'restore',
|
||||
documentCount: updated.length,
|
||||
documentIds: updated.map((d) => d.id),
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
@@ -229,7 +235,13 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) {
|
||||
resourceType: AuditResourceType.CONNECTOR,
|
||||
resourceId: connectorId,
|
||||
description: `Excluded ${updated.length} document(s) from knowledge base "${writeCheck.knowledgeBase.name}"`,
|
||||
metadata: { knowledgeBaseId, documentCount: updated.length },
|
||||
metadata: {
|
||||
knowledgeBaseId,
|
||||
knowledgeBaseName: writeCheck.knowledgeBase.name,
|
||||
operation: 'exclude',
|
||||
documentCount: updated.length,
|
||||
documentIds: updated.map((d) => d.id),
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -268,7 +268,16 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) {
|
||||
resourceId: connectorId,
|
||||
resourceName: updatedData.connectorType,
|
||||
description: `Updated connector for knowledge base "${writeCheck.knowledgeBase.name}"`,
|
||||
metadata: { knowledgeBaseId, updatedFields: Object.keys(parsed.data) },
|
||||
metadata: {
|
||||
knowledgeBaseId,
|
||||
knowledgeBaseName: writeCheck.knowledgeBase.name,
|
||||
connectorType: updatedData.connectorType,
|
||||
updatedFields: Object.keys(parsed.data),
|
||||
...(parsed.data.syncIntervalMinutes !== undefined && {
|
||||
syncIntervalMinutes: parsed.data.syncIntervalMinutes,
|
||||
}),
|
||||
...(parsed.data.status !== undefined && { newStatus: parsed.data.status }),
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
@@ -399,6 +408,9 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) {
|
||||
description: `Deleted connector from knowledge base "${writeCheck.knowledgeBase.name}"`,
|
||||
metadata: {
|
||||
knowledgeBaseId,
|
||||
knowledgeBaseName: writeCheck.knowledgeBase.name,
|
||||
connectorType: existingConnector[0].connectorType,
|
||||
deleteDocuments,
|
||||
documentsDeleted: deleteDocuments ? docCount : 0,
|
||||
documentsKept: deleteDocuments ? 0 : docCount,
|
||||
},
|
||||
|
||||
@@ -78,7 +78,13 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
|
||||
resourceId: connectorId,
|
||||
resourceName: connectorRows[0].connectorType,
|
||||
description: `Triggered manual sync for connector on knowledge base "${writeCheck.knowledgeBase.name}"`,
|
||||
metadata: { knowledgeBaseId },
|
||||
metadata: {
|
||||
knowledgeBaseId,
|
||||
knowledgeBaseName: writeCheck.knowledgeBase.name,
|
||||
connectorType: connectorRows[0].connectorType,
|
||||
connectorStatus: connectorRows[0].status,
|
||||
syncType: 'manual',
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -286,7 +286,13 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
resourceId: connectorId,
|
||||
resourceName: connectorType,
|
||||
description: `Created ${connectorType} connector for knowledge base "${writeCheck.knowledgeBase.name}"`,
|
||||
metadata: { knowledgeBaseId, connectorType, syncIntervalMinutes },
|
||||
metadata: {
|
||||
knowledgeBaseId,
|
||||
knowledgeBaseName: writeCheck.knowledgeBase.name,
|
||||
connectorType,
|
||||
syncIntervalMinutes,
|
||||
authMode: connectorConfig.auth.mode,
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -208,7 +208,16 @@ export async function PUT(
|
||||
resourceType: AuditResourceType.DOCUMENT,
|
||||
resourceId: documentId,
|
||||
resourceName: validatedData.filename ?? accessCheck.document?.filename,
|
||||
description: `Updated document "${documentId}" in knowledge base "${knowledgeBaseId}"`,
|
||||
description: `Updated document "${validatedData.filename ?? accessCheck.document?.filename}" in knowledge base "${knowledgeBaseId}"`,
|
||||
metadata: {
|
||||
knowledgeBaseId,
|
||||
knowledgeBaseName: accessCheck.knowledgeBase?.name,
|
||||
fileName: validatedData.filename ?? accessCheck.document?.filename,
|
||||
updatedFields: Object.keys(validatedData).filter(
|
||||
(k) => validatedData[k as keyof typeof validatedData] !== undefined
|
||||
),
|
||||
...(validatedData.enabled !== undefined && { enabled: validatedData.enabled }),
|
||||
},
|
||||
request: req,
|
||||
})
|
||||
|
||||
@@ -281,8 +290,14 @@ export async function DELETE(
|
||||
resourceType: AuditResourceType.DOCUMENT,
|
||||
resourceId: documentId,
|
||||
resourceName: accessCheck.document?.filename,
|
||||
description: `Deleted document "${documentId}" from knowledge base "${knowledgeBaseId}"`,
|
||||
metadata: { fileName: accessCheck.document?.filename },
|
||||
description: `Deleted document "${accessCheck.document?.filename}" from knowledge base "${knowledgeBaseId}"`,
|
||||
metadata: {
|
||||
knowledgeBaseId,
|
||||
knowledgeBaseName: accessCheck.knowledgeBase?.name,
|
||||
fileName: accessCheck.document?.filename,
|
||||
fileSize: accessCheck.document?.fileSize,
|
||||
mimeType: accessCheck.document?.mimeType,
|
||||
},
|
||||
request: req,
|
||||
})
|
||||
|
||||
|
||||
@@ -278,8 +278,8 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
resourceName: `${createdDocuments.length} document(s)`,
|
||||
description: `Uploaded ${createdDocuments.length} document(s) to knowledge base "${knowledgeBaseId}"`,
|
||||
metadata: {
|
||||
knowledgeBaseName: accessCheck.knowledgeBase?.name,
|
||||
fileCount: createdDocuments.length,
|
||||
fileNames: createdDocuments.map((doc) => doc.filename),
|
||||
},
|
||||
request: req,
|
||||
})
|
||||
@@ -358,6 +358,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
resourceName: validatedData.filename,
|
||||
description: `Uploaded document "${validatedData.filename}" to knowledge base "${knowledgeBaseId}"`,
|
||||
metadata: {
|
||||
knowledgeBaseName: accessCheck.knowledgeBase?.name,
|
||||
fileName: validatedData.filename,
|
||||
fileType: validatedData.mimeType,
|
||||
fileSize: validatedData.fileSize,
|
||||
|
||||
@@ -196,7 +196,10 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
? `Upserted (replaced) document "${validatedData.filename}" in knowledge base "${knowledgeBaseId}"`
|
||||
: `Upserted (created) document "${validatedData.filename}" in knowledge base "${knowledgeBaseId}"`,
|
||||
metadata: {
|
||||
knowledgeBaseName: accessCheck.knowledgeBase?.name,
|
||||
fileName: validatedData.filename,
|
||||
fileType: validatedData.mimeType,
|
||||
fileSize: validatedData.fileSize,
|
||||
previousDocumentId: existingDocumentId,
|
||||
isUpdate,
|
||||
},
|
||||
|
||||
@@ -59,6 +59,9 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
resourceId: id,
|
||||
resourceName: kb.name,
|
||||
description: `Restored knowledge base "${kb.name}"`,
|
||||
metadata: {
|
||||
knowledgeBaseName: kb.name,
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -147,6 +147,20 @@ export async function PUT(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
resourceId: id,
|
||||
resourceName: validatedData.name ?? updatedKnowledgeBase.name,
|
||||
description: `Updated knowledge base "${validatedData.name ?? updatedKnowledgeBase.name}"`,
|
||||
metadata: {
|
||||
updatedFields: Object.keys(validatedData).filter(
|
||||
(k) => validatedData[k as keyof typeof validatedData] !== undefined
|
||||
),
|
||||
...(validatedData.name && { newName: validatedData.name }),
|
||||
...(validatedData.description !== undefined && {
|
||||
description: validatedData.description,
|
||||
}),
|
||||
...(validatedData.chunkingConfig && {
|
||||
chunkMaxSize: validatedData.chunkingConfig.maxSize,
|
||||
chunkMinSize: validatedData.chunkingConfig.minSize,
|
||||
chunkOverlap: validatedData.chunkingConfig.overlap,
|
||||
}),
|
||||
},
|
||||
request: req,
|
||||
})
|
||||
|
||||
@@ -226,6 +240,9 @@ export async function DELETE(
|
||||
resourceId: id,
|
||||
resourceName: accessCheck.knowledgeBase.name,
|
||||
description: `Deleted knowledge base "${accessCheck.knowledgeBase.name || id}"`,
|
||||
metadata: {
|
||||
knowledgeBaseName: accessCheck.knowledgeBase.name,
|
||||
},
|
||||
request: _request,
|
||||
})
|
||||
|
||||
|
||||
@@ -15,14 +15,6 @@ import { captureServerEvent } from '@/lib/posthog/server'
|
||||
|
||||
const logger = createLogger('KnowledgeBaseAPI')
|
||||
|
||||
/**
|
||||
* Schema for creating a knowledge base
|
||||
*
|
||||
* Chunking config units:
|
||||
* - maxSize: tokens (1 token ≈ 4 characters)
|
||||
* - minSize: characters
|
||||
* - overlap: tokens (1 token ≈ 4 characters)
|
||||
*/
|
||||
const CreateKnowledgeBaseSchema = z.object({
|
||||
name: z.string().min(1, 'Name is required'),
|
||||
description: z.string().optional(),
|
||||
@@ -31,12 +23,20 @@ const CreateKnowledgeBaseSchema = z.object({
|
||||
embeddingDimension: z.literal(1536).default(1536),
|
||||
chunkingConfig: z
|
||||
.object({
|
||||
/** Maximum chunk size in tokens (1 token ≈ 4 characters) */
|
||||
maxSize: z.number().min(100).max(4000).default(1024),
|
||||
/** Minimum chunk size in characters */
|
||||
minSize: z.number().min(1).max(2000).default(100),
|
||||
/** Overlap between chunks in tokens (1 token ≈ 4 characters) */
|
||||
overlap: z.number().min(0).max(500).default(200),
|
||||
strategy: z
|
||||
.enum(['auto', 'text', 'regex', 'recursive', 'sentence', 'token'])
|
||||
.default('auto')
|
||||
.optional(),
|
||||
strategyOptions: z
|
||||
.object({
|
||||
pattern: z.string().max(500).optional(),
|
||||
separators: z.array(z.string()).optional(),
|
||||
recipe: z.enum(['plain', 'markdown', 'code']).optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.default({
|
||||
maxSize: 1024,
|
||||
@@ -45,13 +45,31 @@ const CreateKnowledgeBaseSchema = z.object({
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
// Convert maxSize from tokens to characters for comparison (1 token ≈ 4 chars)
|
||||
const maxSizeInChars = data.maxSize * 4
|
||||
return data.minSize < maxSizeInChars
|
||||
},
|
||||
{
|
||||
message: 'Min chunk size (characters) must be less than max chunk size (tokens × 4)',
|
||||
}
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
return data.overlap < data.maxSize
|
||||
},
|
||||
{
|
||||
message: 'Overlap must be less than max chunk size',
|
||||
}
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.strategy === 'regex' && !data.strategyOptions?.pattern) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
{
|
||||
message: 'Regex pattern is required when using the regex chunking strategy',
|
||||
}
|
||||
),
|
||||
})
|
||||
|
||||
@@ -144,7 +162,16 @@ export async function POST(req: NextRequest) {
|
||||
resourceId: newKnowledgeBase.id,
|
||||
resourceName: validatedData.name,
|
||||
description: `Created knowledge base "${validatedData.name}"`,
|
||||
metadata: { name: validatedData.name },
|
||||
metadata: {
|
||||
name: validatedData.name,
|
||||
description: validatedData.description,
|
||||
embeddingModel: validatedData.embeddingModel,
|
||||
embeddingDimension: validatedData.embeddingDimension,
|
||||
chunkingStrategy: validatedData.chunkingConfig.strategy,
|
||||
chunkMaxSize: validatedData.chunkingConfig.maxSize,
|
||||
chunkMinSize: validatedData.chunkingConfig.minSize,
|
||||
chunkOverlap: validatedData.chunkingConfig.overlap,
|
||||
},
|
||||
request: req,
|
||||
})
|
||||
|
||||
|
||||
@@ -124,6 +124,14 @@ export const PATCH = withMcpAuth<{ id: string }>('write')(
|
||||
resourceId: serverId,
|
||||
resourceName: updatedServer.name || serverId,
|
||||
description: `Updated MCP server "${updatedServer.name || serverId}"`,
|
||||
metadata: {
|
||||
serverName: updatedServer.name,
|
||||
transport: updatedServer.transport,
|
||||
url: updatedServer.url,
|
||||
updatedFields: Object.keys(updateData).filter(
|
||||
(k) => k !== 'workspaceId' && k !== 'updatedAt'
|
||||
),
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -206,7 +206,14 @@ export const POST = withMcpAuth('write')(
|
||||
resourceId: serverId,
|
||||
resourceName: body.name,
|
||||
description: `Added MCP server "${body.name}"`,
|
||||
metadata: { serverName: body.name, transport: body.transport },
|
||||
metadata: {
|
||||
serverName: body.name,
|
||||
transport: body.transport,
|
||||
url: body.url,
|
||||
timeout: body.timeout || 30000,
|
||||
retries: body.retries || 3,
|
||||
source: source,
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
@@ -278,6 +285,12 @@ export const DELETE = withMcpAuth('admin')(
|
||||
resourceId: serverId!,
|
||||
resourceName: deletedServer.name,
|
||||
description: `Removed MCP server "${deletedServer.name}"`,
|
||||
metadata: {
|
||||
serverName: deletedServer.name,
|
||||
transport: deletedServer.transport,
|
||||
url: deletedServer.url,
|
||||
source,
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -135,6 +135,11 @@ export const PATCH = withMcpAuth<RouteParams>('write')(
|
||||
resourceId: serverId,
|
||||
resourceName: updatedServer.name,
|
||||
description: `Updated workflow MCP server "${updatedServer.name}"`,
|
||||
metadata: {
|
||||
serverName: updatedServer.name,
|
||||
isPublic: updatedServer.isPublic,
|
||||
updatedFields: Object.keys(updateData).filter((k) => k !== 'updatedAt'),
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
@@ -189,6 +194,7 @@ export const DELETE = withMcpAuth<RouteParams>('admin')(
|
||||
resourceId: serverId,
|
||||
resourceName: deletedServer.name,
|
||||
description: `Unpublished workflow MCP server "${deletedServer.name}"`,
|
||||
metadata: { serverName: deletedServer.name },
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -152,7 +152,12 @@ export const PATCH = withMcpAuth<RouteParams>('write')(
|
||||
resourceType: AuditResourceType.MCP_SERVER,
|
||||
resourceId: serverId,
|
||||
description: `Updated tool "${updatedTool.toolName}" in MCP server`,
|
||||
metadata: { toolId, toolName: updatedTool.toolName },
|
||||
metadata: {
|
||||
toolId,
|
||||
toolName: updatedTool.toolName,
|
||||
workflowId: updatedTool.workflowId,
|
||||
updatedFields: Object.keys(updateData).filter((k) => k !== 'updatedAt'),
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
@@ -220,7 +225,7 @@ export const DELETE = withMcpAuth<RouteParams>('write')(
|
||||
resourceType: AuditResourceType.MCP_SERVER,
|
||||
resourceId: serverId,
|
||||
description: `Removed tool "${deletedTool.toolName}" from MCP server`,
|
||||
metadata: { toolId, toolName: deletedTool.toolName },
|
||||
metadata: { toolId, toolName: deletedTool.toolName, workflowId: deletedTool.workflowId },
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -224,7 +224,13 @@ export const POST = withMcpAuth<RouteParams>('write')(
|
||||
resourceType: AuditResourceType.MCP_SERVER,
|
||||
resourceId: serverId,
|
||||
description: `Added tool "${toolName}" to MCP server`,
|
||||
metadata: { toolId, toolName, workflowId: body.workflowId },
|
||||
metadata: {
|
||||
toolId,
|
||||
toolName,
|
||||
toolDescription,
|
||||
workflowId: body.workflowId,
|
||||
workflowName: workflowRecord.name,
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -208,6 +208,13 @@ export const POST = withMcpAuth('write')(
|
||||
resourceId: serverId,
|
||||
resourceName: body.name.trim(),
|
||||
description: `Published workflow MCP server "${body.name.trim()}" with ${addedTools.length} tool(s)`,
|
||||
metadata: {
|
||||
serverName: body.name.trim(),
|
||||
isPublic: body.isPublic ?? false,
|
||||
toolCount: addedTools.length,
|
||||
toolNames: addedTools.map((t) => t.toolName),
|
||||
workflowIds: addedTools.map((t) => t.workflowId),
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -182,6 +182,20 @@ export async function POST(
|
||||
email: orgInvitation.email,
|
||||
})
|
||||
|
||||
recordAudit({
|
||||
workspaceId: null,
|
||||
actorId: session.user.id,
|
||||
action: AuditAction.ORG_INVITATION_RESENT,
|
||||
resourceType: AuditResourceType.ORGANIZATION,
|
||||
resourceId: organizationId,
|
||||
actorName: session.user.name ?? undefined,
|
||||
actorEmail: session.user.email ?? undefined,
|
||||
resourceName: org?.name ?? undefined,
|
||||
description: `Resent organization invitation to ${orgInvitation.email}`,
|
||||
metadata: { invitationId, targetEmail: orgInvitation.email, targetRole: orgInvitation.role },
|
||||
request: _request,
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Invitation resent successfully',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user