Compare commits

..

6 Commits

Author SHA1 Message Date
Waleed Latif
11d5b23d0a update docker publish yml 2025-05-08 08:08:05 -07:00
Waleed Latif
4687f5dd0f added additional error for cli script 2025-05-07 22:58:33 -07:00
Waleed Latif
7f948a3a48 updated README.md 2025-05-07 22:40:09 -07:00
Waleed Latif
b538e5d884 updated README.md 2025-05-07 18:24:55 -07:00
Waleed Latif
1606f19d12 updated dockerfile for devcontainer 2025-05-07 17:10:03 -07:00
Waleed Latif
a9c755c5d4 feat(docker): added docker image & npm pkg to install that image 2025-05-07 17:03:02 -07:00
6408 changed files with 187821 additions and 1694376 deletions

View File

@@ -1,591 +0,0 @@
---
description: Create a block configuration for a Sim Studio integration with proper subBlocks, conditions, and tool wiring
argument-hint: <service-name>
---
# Add Block Skill
You are an expert at creating block configurations for Sim Studio. You understand the serializer, subBlock types, conditions, dependsOn, modes, and all UI patterns.
## Your Task
When the user asks you to create a block:
1. Create the block file in `apps/sim/blocks/blocks/{service}.ts`
2. Configure all subBlocks with proper types, conditions, and dependencies
3. Wire up tools correctly
## Block Configuration Structure
```typescript
import { {ServiceName}Icon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
export const {ServiceName}Block: BlockConfig = {
type: '{service}', // snake_case identifier
name: '{Service Name}', // Human readable
description: 'Brief description', // One sentence
longDescription: 'Detailed description for docs',
docsLink: 'https://docs.sim.ai/tools/{service}',
category: 'tools', // 'tools' | 'blocks' | 'triggers'
bgColor: '#HEXCOLOR', // Brand color
icon: {ServiceName}Icon,
// Auth mode
authMode: AuthMode.OAuth, // or AuthMode.ApiKey
subBlocks: [
// Define all UI fields here
],
tools: {
access: ['tool_id_1', 'tool_id_2'], // Array of tool IDs this block can use
config: {
tool: (params) => `{service}_${params.operation}`, // Tool selector function
params: (params) => ({
// Transform subBlock values to tool params
}),
},
},
inputs: {
// Optional: define expected inputs from other blocks
},
outputs: {
// Define outputs available to downstream blocks
},
}
```
## SubBlock Types Reference
**Critical:** Every subblock `id` must be unique within the block. Duplicate IDs cause conflicts even with different conditions.
### Text Inputs
```typescript
// Single-line input
{ id: 'field', title: 'Label', type: 'short-input', placeholder: '...' }
// Multi-line input
{ id: 'field', title: 'Label', type: 'long-input', placeholder: '...', rows: 6 }
// Password input
{ id: 'apiKey', title: 'API Key', type: 'short-input', password: true }
```
### Selection Inputs
```typescript
// Dropdown (static options)
{
id: 'operation',
title: 'Operation',
type: 'dropdown',
options: [
{ label: 'Create', id: 'create' },
{ label: 'Update', id: 'update' },
],
value: () => 'create', // Default value function
}
// Combobox (searchable dropdown)
{
id: 'field',
title: 'Label',
type: 'combobox',
options: [...],
searchable: true,
}
```
### Code/JSON Inputs
```typescript
{
id: 'code',
title: 'Code',
type: 'code',
language: 'javascript', // 'javascript' | 'json' | 'python'
placeholder: '// Enter code...',
}
```
### OAuth/Credentials
```typescript
{
id: 'credential',
title: 'Account',
type: 'oauth-input',
serviceId: '{service}', // Must match OAuth provider
placeholder: 'Select account',
required: true,
}
```
### Selectors (with dynamic options)
```typescript
// Channel selector (Slack, Discord, etc.)
{
id: 'channel',
title: 'Channel',
type: 'channel-selector',
serviceId: '{service}',
placeholder: 'Select channel',
dependsOn: ['credential'],
}
// Project selector (Jira, etc.)
{
id: 'project',
title: 'Project',
type: 'project-selector',
serviceId: '{service}',
dependsOn: ['credential'],
}
// File selector (Google Drive, etc.)
{
id: 'file',
title: 'File',
type: 'file-selector',
serviceId: '{service}',
mimeType: 'application/pdf',
dependsOn: ['credential'],
}
// User selector
{
id: 'user',
title: 'User',
type: 'user-selector',
serviceId: '{service}',
dependsOn: ['credential'],
}
```
### Other Types
```typescript
// Switch/toggle
{ id: 'enabled', type: 'switch' }
// Slider
{ id: 'temperature', title: 'Temperature', type: 'slider', min: 0, max: 2, step: 0.1 }
// Table (key-value pairs)
{ id: 'headers', title: 'Headers', type: 'table', columns: ['Key', 'Value'] }
// File upload
{
id: 'files',
title: 'Attachments',
type: 'file-upload',
multiple: true,
acceptedTypes: 'image/*,application/pdf',
}
```
## Condition Syntax
Controls when a field is shown based on other field values.
### Simple Condition
```typescript
condition: { field: 'operation', value: 'create' }
// Shows when operation === 'create'
```
### Multiple Values (OR)
```typescript
condition: { field: 'operation', value: ['create', 'update'] }
// Shows when operation is 'create' OR 'update'
```
### Negation
```typescript
condition: { field: 'operation', value: 'delete', not: true }
// Shows when operation !== 'delete'
```
### Compound (AND)
```typescript
condition: {
field: 'operation',
value: 'send',
and: {
field: 'type',
value: 'dm',
not: true,
}
}
// Shows when operation === 'send' AND type !== 'dm'
```
### Complex Example
```typescript
condition: {
field: 'operation',
value: ['list', 'search'],
not: true,
and: {
field: 'authMethod',
value: 'oauth',
}
}
// Shows when operation NOT in ['list', 'search'] AND authMethod === 'oauth'
```
## DependsOn Pattern
Controls when a field is enabled and when its options are refetched.
### Simple Array (all must be set)
```typescript
dependsOn: ['credential']
// Enabled only when credential has a value
// Options refetch when credential changes
dependsOn: ['credential', 'projectId']
// Enabled only when BOTH have values
```
### Complex (all + any)
```typescript
dependsOn: {
all: ['authMethod'], // All must be set
any: ['credential', 'apiKey'] // At least one must be set
}
// Enabled when authMethod is set AND (credential OR apiKey is set)
```
## Required Pattern
Can be boolean or condition-based.
### Simple Boolean
```typescript
required: true
required: false
```
### Conditional Required
```typescript
required: { field: 'operation', value: 'create' }
// Required only when operation === 'create'
required: { field: 'operation', value: ['create', 'update'] }
// Required when operation is 'create' OR 'update'
```
## Mode Pattern (Basic vs Advanced)
Controls which UI view shows the field.
### Mode Options
- `'basic'` - Only in basic view (default UI)
- `'advanced'` - Only in advanced view
- `'both'` - Both views (default if not specified)
- `'trigger'` - Only in trigger configuration
### canonicalParamId Pattern
Maps multiple UI fields to a single serialized parameter:
```typescript
// Basic mode: Visual selector
{
id: 'channel',
title: 'Channel',
type: 'channel-selector',
mode: 'basic',
canonicalParamId: 'channel', // Both map to 'channel' param
dependsOn: ['credential'],
}
// Advanced mode: Manual input
{
id: 'channelId',
title: 'Channel ID',
type: 'short-input',
mode: 'advanced',
canonicalParamId: 'channel', // Both map to 'channel' param
placeholder: 'Enter channel ID manually',
}
```
**How it works:**
- In basic mode: `channel` selector value → `params.channel`
- In advanced mode: `channelId` input value → `params.channel`
- The serializer consolidates based on current mode
**Critical constraints:**
- `canonicalParamId` must NOT match any other subblock's `id` in the same block (causes conflicts)
- `canonicalParamId` must be unique per block (only one basic/advanced pair per canonicalParamId)
- ONLY use `canonicalParamId` to link basic/advanced mode alternatives for the same logical parameter
- Do NOT use it for any other purpose
## WandConfig Pattern
Enables AI-assisted field generation.
```typescript
{
id: 'query',
title: 'Query',
type: 'code',
language: 'json',
wandConfig: {
enabled: true,
prompt: 'Generate a query based on the user request. Return ONLY the JSON.',
placeholder: 'Describe what you want to query...',
generationType: 'json-object', // Optional: affects AI behavior
maintainHistory: true, // Optional: keeps conversation context
},
}
```
### Generation Types
- `'javascript-function-body'` - JS code generation
- `'json-object'` - Raw JSON (adds "no markdown" instruction)
- `'json-schema'` - JSON Schema definitions
- `'sql-query'` - SQL statements
- `'timestamp'` - Adds current date/time context
## Tools Configuration
### Simple Tool Selector
```typescript
tools: {
access: ['service_create', 'service_read', 'service_update'],
config: {
tool: (params) => `service_${params.operation}`,
},
}
```
### With Parameter Transformation
```typescript
tools: {
access: ['service_action'],
config: {
tool: (params) => 'service_action',
params: (params) => ({
id: params.resourceId,
data: typeof params.data === 'string' ? JSON.parse(params.data) : params.data,
}),
},
}
```
### V2 Versioned Tool Selector
```typescript
import { createVersionedToolSelector } from '@/blocks/utils'
tools: {
access: [
'service_create_v2',
'service_read_v2',
'service_update_v2',
],
config: {
tool: createVersionedToolSelector({
baseToolSelector: (params) => `service_${params.operation}`,
suffix: '_v2',
fallbackToolId: 'service_create_v2',
}),
},
}
```
## Outputs Definition
**IMPORTANT:** Block outputs have a simpler schema than tool outputs. Block outputs do NOT support:
- `optional: true` - This is only for tool outputs
- `items` property - This is only for tool outputs with array types
Block outputs only support:
- `type` - The data type ('string', 'number', 'boolean', 'json', 'array')
- `description` - Human readable description
- Nested object structure (for complex types)
```typescript
outputs: {
// Simple outputs
id: { type: 'string', description: 'Resource ID' },
success: { type: 'boolean', description: 'Whether operation succeeded' },
// Use type: 'json' for complex objects or arrays (NOT type: 'array' with items)
items: { type: 'json', description: 'List of items' },
metadata: { type: 'json', description: 'Response metadata' },
// Nested outputs (for structured data)
user: {
id: { type: 'string', description: 'User ID' },
name: { type: 'string', description: 'User name' },
email: { type: 'string', description: 'User email' },
},
}
```
## V2 Block Pattern
When creating V2 blocks (alongside legacy V1):
```typescript
// V1 Block - mark as legacy
export const ServiceBlock: BlockConfig = {
type: 'service',
name: 'Service (Legacy)',
hideFromToolbar: true, // Hide from toolbar
// ... rest of config
}
// V2 Block - visible, uses V2 tools
export const ServiceV2Block: BlockConfig = {
type: 'service_v2',
name: 'Service', // Clean name
hideFromToolbar: false, // Visible
subBlocks: ServiceBlock.subBlocks, // Reuse UI
tools: {
access: ServiceBlock.tools?.access?.map(id => `${id}_v2`) || [],
config: {
tool: createVersionedToolSelector({
baseToolSelector: (params) => (ServiceBlock.tools?.config as any)?.tool(params),
suffix: '_v2',
fallbackToolId: 'service_default_v2',
}),
params: ServiceBlock.tools?.config?.params,
},
},
outputs: {
// Flat, API-aligned outputs (not wrapped in content/metadata)
},
}
```
## Registering Blocks
After creating the block, remind the user to:
1. Import in `apps/sim/blocks/registry.ts`
2. Add to the `registry` object (alphabetically):
```typescript
import { ServiceBlock } from '@/blocks/blocks/service'
export const registry: Record<string, BlockConfig> = {
// ... existing blocks ...
service: ServiceBlock,
}
```
## Complete Example
```typescript
import { ServiceIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
export const ServiceBlock: BlockConfig = {
type: 'service',
name: 'Service',
description: 'Integrate with Service API',
longDescription: 'Full description for documentation...',
docsLink: 'https://docs.sim.ai/tools/service',
category: 'tools',
bgColor: '#FF6B6B',
icon: ServiceIcon,
authMode: AuthMode.OAuth,
subBlocks: [
{
id: 'operation',
title: 'Operation',
type: 'dropdown',
options: [
{ label: 'Create', id: 'create' },
{ label: 'Read', id: 'read' },
{ label: 'Update', id: 'update' },
{ label: 'Delete', id: 'delete' },
],
value: () => 'create',
},
{
id: 'credential',
title: 'Service Account',
type: 'oauth-input',
serviceId: 'service',
placeholder: 'Select account',
required: true,
},
{
id: 'resourceId',
title: 'Resource ID',
type: 'short-input',
placeholder: 'Enter resource ID',
condition: { field: 'operation', value: ['read', 'update', 'delete'] },
required: { field: 'operation', value: ['read', 'update', 'delete'] },
},
{
id: 'name',
title: 'Name',
type: 'short-input',
placeholder: 'Resource name',
condition: { field: 'operation', value: ['create', 'update'] },
required: { field: 'operation', value: 'create' },
},
],
tools: {
access: ['service_create', 'service_read', 'service_update', 'service_delete'],
config: {
tool: (params) => `service_${params.operation}`,
},
},
outputs: {
id: { type: 'string', description: 'Resource ID' },
name: { type: 'string', description: 'Resource name' },
createdAt: { type: 'string', description: 'Creation timestamp' },
},
}
```
## Connecting Blocks with Triggers
If the service supports webhooks, connect the block to its triggers.
```typescript
import { getTrigger } from '@/triggers'
export const ServiceBlock: BlockConfig = {
// ... basic config ...
triggers: {
enabled: true,
available: ['service_event_a', 'service_event_b', 'service_webhook'],
},
subBlocks: [
// Tool subBlocks first...
{ id: 'operation', /* ... */ },
// Then spread trigger subBlocks
...getTrigger('service_event_a').subBlocks,
...getTrigger('service_event_b').subBlocks,
...getTrigger('service_webhook').subBlocks,
],
}
```
See the `/add-trigger` skill for creating triggers.
## Checklist Before Finishing
- [ ] All subBlocks have `id`, `title` (except switch), and `type`
- [ ] Conditions use correct syntax (field, value, not, and)
- [ ] DependsOn set for fields that need other values
- [ ] Required fields marked correctly (boolean or condition)
- [ ] OAuth inputs have correct `serviceId`
- [ ] Tools.access lists all tool IDs
- [ ] Tools.config.tool returns correct tool ID
- [ ] Outputs match tool outputs
- [ ] Block registered in registry.ts
- [ ] If triggers exist: `triggers` config set, trigger subBlocks spread

View File

@@ -1,450 +0,0 @@
---
description: Add a complete integration to Sim Studio (tools, block, icon, registration)
argument-hint: <service-name> [api-docs-url]
---
# Add Integration Skill
You are an expert at adding complete integrations to Sim Studio. This skill orchestrates the full process of adding a new service integration.
## Overview
Adding an integration involves these steps in order:
1. **Research** - Read the service's API documentation
2. **Create Tools** - Build tool configurations for each API operation
3. **Create Block** - Build the block UI configuration
4. **Add Icon** - Add the service's brand icon
5. **Create Triggers** (optional) - If the service supports webhooks
6. **Register** - Register tools, block, and triggers in their registries
7. **Generate Docs** - Run the docs generation script
## Step 1: Research the API
Before writing any code:
1. Use Context7 to find official documentation: `mcp__plugin_context7_context7__resolve-library-id`
2. Or use WebFetch to read API docs directly
3. Identify:
- Authentication method (OAuth, API Key, both)
- Available operations (CRUD, search, etc.)
- Required vs optional parameters
- Response structures
## Step 2: Create Tools
### Directory Structure
```
apps/sim/tools/{service}/
├── index.ts # Barrel exports
├── types.ts # TypeScript interfaces
├── {action1}.ts # Tool for action 1
├── {action2}.ts # Tool for action 2
└── ...
```
### Key Patterns
**types.ts:**
```typescript
import type { ToolResponse } from '@/tools/types'
export interface {Service}{Action}Params {
accessToken: string // For OAuth services
// OR
apiKey: string // For API key services
requiredParam: string
optionalParam?: string
}
export interface {Service}Response extends ToolResponse {
output: {
// Define output structure
}
}
```
**Tool file pattern:**
```typescript
export const {service}{Action}Tool: ToolConfig<Params, Response> = {
id: '{service}_{action}',
name: '{Service} {Action}',
description: '...',
version: '1.0.0',
oauth: { required: true, provider: '{service}' }, // If OAuth
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden', description: '...' },
// ... other params
},
request: { url, method, headers, body },
transformResponse: async (response) => {
const data = await response.json()
return {
success: true,
output: {
field: data.field ?? null, // Always handle nullables
},
}
},
outputs: { /* ... */ },
}
```
### Critical Rules
- `visibility: 'hidden'` for OAuth tokens
- `visibility: 'user-only'` for API keys and user credentials
- `visibility: 'user-or-llm'` for operation parameters
- Always use `?? null` for nullable API response fields
- Always use `?? []` for optional array fields
- Set `optional: true` for outputs that may not exist
- Never output raw JSON dumps - extract meaningful fields
## Step 3: Create Block
### File Location
`apps/sim/blocks/blocks/{service}.ts`
### Block Structure
```typescript
import { {Service}Icon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
export const {Service}Block: BlockConfig = {
type: '{service}',
name: '{Service}',
description: '...',
longDescription: '...',
docsLink: 'https://docs.sim.ai/tools/{service}',
category: 'tools',
bgColor: '#HEXCOLOR',
icon: {Service}Icon,
authMode: AuthMode.OAuth, // or AuthMode.ApiKey
subBlocks: [
// Operation dropdown
{
id: 'operation',
title: 'Operation',
type: 'dropdown',
options: [
{ label: 'Operation 1', id: 'action1' },
{ label: 'Operation 2', id: 'action2' },
],
value: () => 'action1',
},
// Credential field
{
id: 'credential',
title: '{Service} Account',
type: 'oauth-input',
serviceId: '{service}',
required: true,
},
// Conditional fields per operation
// ...
],
tools: {
access: ['{service}_action1', '{service}_action2'],
config: {
tool: (params) => `{service}_${params.operation}`,
},
},
outputs: { /* ... */ },
}
```
### Key SubBlock Patterns
**Condition-based visibility:**
```typescript
{
id: 'resourceId',
title: 'Resource ID',
type: 'short-input',
condition: { field: 'operation', value: ['read', 'update', 'delete'] },
required: { field: 'operation', value: ['read', 'update', 'delete'] },
}
```
**DependsOn for cascading selectors:**
```typescript
{
id: 'project',
type: 'project-selector',
dependsOn: ['credential'],
},
{
id: 'issue',
type: 'file-selector',
dependsOn: ['credential', 'project'],
}
```
**Basic/Advanced mode for dual UX:**
```typescript
// Basic: Visual selector
{
id: 'channel',
type: 'channel-selector',
mode: 'basic',
canonicalParamId: 'channel',
dependsOn: ['credential'],
},
// Advanced: Manual input
{
id: 'channelId',
type: 'short-input',
mode: 'advanced',
canonicalParamId: 'channel',
}
```
**Critical:**
- `canonicalParamId` must NOT match any other subblock's `id`, must be unique per block, and should only be used to link basic/advanced alternatives for the same parameter.
- `mode` only controls UI visibility, NOT serialization. Without `canonicalParamId`, both basic and advanced field values would be sent.
- Every subblock `id` must be unique within the block. Duplicate IDs cause conflicts even with different conditions.
## Step 4: Add Icon
### File Location
`apps/sim/components/icons.tsx`
### Pattern
```typescript
export function {Service}Icon(props: SVGProps<SVGSVGElement>) {
return (
<svg
{...props}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
{/* SVG paths from brand assets */}
</svg>
)
}
```
### Finding Icons
1. Check the service's brand/press kit page
2. Download SVG logo
3. Convert to React component
4. Ensure it accepts and spreads props
## Step 5: Create Triggers (Optional)
If the service supports webhooks, create triggers using the generic `buildTriggerSubBlocks` helper.
### Directory Structure
```
apps/sim/triggers/{service}/
├── index.ts # Barrel exports
├── utils.ts # Trigger options, setup instructions, extra fields
├── {event_a}.ts # Primary trigger (includes dropdown)
├── {event_b}.ts # Secondary triggers (no dropdown)
└── webhook.ts # Generic webhook (optional)
```
### Key Pattern
```typescript
import { buildTriggerSubBlocks } from '@/triggers'
import { {service}TriggerOptions, {service}SetupInstructions, build{Service}ExtraFields } from './utils'
// Primary trigger - includeDropdown: true
export const {service}EventATrigger: TriggerConfig = {
id: '{service}_event_a',
subBlocks: buildTriggerSubBlocks({
triggerId: '{service}_event_a',
triggerOptions: {service}TriggerOptions,
includeDropdown: true, // Only for primary trigger!
setupInstructions: {service}SetupInstructions('Event A'),
extraFields: build{Service}ExtraFields('{service}_event_a'),
}),
// ...
}
// Secondary triggers - no dropdown
export const {service}EventBTrigger: TriggerConfig = {
id: '{service}_event_b',
subBlocks: buildTriggerSubBlocks({
triggerId: '{service}_event_b',
triggerOptions: {service}TriggerOptions,
// No includeDropdown!
setupInstructions: {service}SetupInstructions('Event B'),
extraFields: build{Service}ExtraFields('{service}_event_b'),
}),
// ...
}
```
### Connect to Block
```typescript
import { getTrigger } from '@/triggers'
export const {Service}Block: BlockConfig = {
triggers: {
enabled: true,
available: ['{service}_event_a', '{service}_event_b'],
},
subBlocks: [
// Tool fields...
...getTrigger('{service}_event_a').subBlocks,
...getTrigger('{service}_event_b').subBlocks,
],
}
```
See `/add-trigger` skill for complete documentation.
## Step 6: Register Everything
### Tools Registry (`apps/sim/tools/registry.ts`)
```typescript
// Add import (alphabetically)
import {
{service}Action1Tool,
{service}Action2Tool,
} from '@/tools/{service}'
// Add to tools object (alphabetically)
export const tools: Record<string, ToolConfig> = {
// ... existing tools ...
{service}_action1: {service}Action1Tool,
{service}_action2: {service}Action2Tool,
}
```
### Block Registry (`apps/sim/blocks/registry.ts`)
```typescript
// Add import (alphabetically)
import { {Service}Block } from '@/blocks/blocks/{service}'
// Add to registry (alphabetically)
export const registry: Record<string, BlockConfig> = {
// ... existing blocks ...
{service}: {Service}Block,
}
```
### Trigger Registry (`apps/sim/triggers/registry.ts`) - If triggers exist
```typescript
// Add import (alphabetically)
import {
{service}EventATrigger,
{service}EventBTrigger,
{service}WebhookTrigger,
} from '@/triggers/{service}'
// Add to TRIGGER_REGISTRY (alphabetically)
export const TRIGGER_REGISTRY: TriggerRegistry = {
// ... existing triggers ...
{service}_event_a: {service}EventATrigger,
{service}_event_b: {service}EventBTrigger,
{service}_webhook: {service}WebhookTrigger,
}
```
## Step 7: Generate Docs
Run the documentation generator:
```bash
bun run scripts/generate-docs.ts
```
This creates `apps/docs/content/docs/en/tools/{service}.mdx`
## V2 Integration Pattern
If creating V2 versions (API-aligned outputs):
1. **V2 Tools** - Add `_v2` suffix, version `2.0.0`, flat outputs
2. **V2 Block** - Add `_v2` type, use `createVersionedToolSelector`
3. **V1 Block** - Add `(Legacy)` to name, set `hideFromToolbar: true`
4. **Registry** - Register both versions
```typescript
// In registry
{service}: {Service}Block, // V1 (legacy, hidden)
{service}_v2: {Service}V2Block, // V2 (visible)
```
## Complete Checklist
### Tools
- [ ] Created `tools/{service}/` directory
- [ ] Created `types.ts` with all interfaces
- [ ] Created tool file for each operation
- [ ] All params have correct visibility
- [ ] All nullable fields use `?? null`
- [ ] All optional outputs have `optional: true`
- [ ] Created `index.ts` barrel export
- [ ] Registered all tools in `tools/registry.ts`
### Block
- [ ] Created `blocks/blocks/{service}.ts`
- [ ] Defined operation dropdown with all operations
- [ ] Added credential field (oauth-input or short-input)
- [ ] Added conditional fields per operation
- [ ] Set up dependsOn for cascading selectors
- [ ] Configured tools.access with all tool IDs
- [ ] Configured tools.config.tool selector
- [ ] Defined outputs matching tool outputs
- [ ] Registered block in `blocks/registry.ts`
- [ ] If triggers: set `triggers.enabled` and `triggers.available`
- [ ] If triggers: spread trigger subBlocks with `getTrigger()`
### Icon
- [ ] Added icon to `components/icons.tsx`
- [ ] Icon spreads props correctly
### Triggers (if service supports webhooks)
- [ ] Created `triggers/{service}/` directory
- [ ] Created `utils.ts` with options, instructions, and extra fields helpers
- [ ] Primary trigger uses `includeDropdown: true`
- [ ] Secondary triggers do NOT have `includeDropdown`
- [ ] All triggers use `buildTriggerSubBlocks` helper
- [ ] Created `index.ts` barrel export
- [ ] Registered all triggers in `triggers/registry.ts`
### Docs
- [ ] Ran `bun run scripts/generate-docs.ts`
- [ ] Verified docs file created
## Example Command
When the user asks to add an integration:
```
User: Add a Stripe integration
You: I'll add the Stripe integration. Let me:
1. First, research the Stripe API using Context7
2. Create the tools for key operations (payments, subscriptions, etc.)
3. Create the block with operation dropdown
4. Add the Stripe icon
5. Register everything
6. Generate docs
[Proceed with implementation...]
```
## Common Gotchas
1. **OAuth serviceId must match** - The `serviceId` in oauth-input must match the OAuth provider configuration
2. **Tool IDs are snake_case** - `stripe_create_payment`, not `stripeCreatePayment`
3. **Block type is snake_case** - `type: 'stripe'`, not `type: 'Stripe'`
4. **Alphabetical ordering** - Keep imports and registry entries alphabetically sorted
5. **Required can be conditional** - Use `required: { field: 'op', value: 'create' }` instead of always true
6. **DependsOn clears options** - When a dependency changes, selector options are refetched

View File

@@ -1,284 +0,0 @@
---
description: Create tool configurations for a Sim Studio integration by reading API docs
argument-hint: <service-name> [api-docs-url]
---
# Add Tools Skill
You are an expert at creating tool configurations for Sim Studio integrations. Your job is to read API documentation and create properly structured tool files.
## Your Task
When the user asks you to create tools for a service:
1. Use Context7 or WebFetch to read the service's API documentation
2. Create the tools directory structure
3. Generate properly typed tool configurations
## Directory Structure
Create files in `apps/sim/tools/{service}/`:
```
tools/{service}/
├── index.ts # Barrel export
├── types.ts # Parameter & response types
└── {action}.ts # Individual tool files (one per operation)
```
## Tool Configuration Structure
Every tool MUST follow this exact structure:
```typescript
import type { {ServiceName}{Action}Params } from '@/tools/{service}/types'
import type { ToolConfig } from '@/tools/types'
interface {ServiceName}{Action}Response {
success: boolean
output: {
// Define output structure here
}
}
export const {serviceName}{Action}Tool: ToolConfig<
{ServiceName}{Action}Params,
{ServiceName}{Action}Response
> = {
id: '{service}_{action}', // snake_case, matches tool name
name: '{Service} {Action}', // Human readable
description: 'Brief description', // One sentence
version: '1.0.0',
// OAuth config (if service uses OAuth)
oauth: {
required: true,
provider: '{service}', // Must match OAuth provider ID
},
params: {
// Hidden params (system-injected)
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token',
},
// User-only params (credentials, IDs user must provide)
someId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'The ID of the resource',
},
// User-or-LLM params (can be provided by user OR computed by LLM)
query: {
type: 'string',
required: false, // Use false for optional
visibility: 'user-or-llm',
description: 'Search query',
},
},
request: {
url: (params) => `https://api.service.com/v1/resource/${params.id}`,
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => ({
// Request body - only for POST/PUT/PATCH
// Trim ID fields to prevent copy-paste whitespace errors:
// userId: params.userId?.trim(),
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
// Map API response to output
// Use ?? null for nullable fields
// Use ?? [] for optional arrays
},
}
},
outputs: {
// Define each output field
},
}
```
## Critical Rules for Parameters
### Visibility Options
- `'hidden'` - System-injected (OAuth tokens, internal params). User never sees.
- `'user-only'` - User must provide (credentials, account-specific IDs)
- `'user-or-llm'` - User provides OR LLM can compute (search queries, content, filters)
### Parameter Types
- `'string'` - Text values
- `'number'` - Numeric values
- `'boolean'` - True/false
- `'json'` - Complex objects (NOT 'object', use 'json')
- `'file'` - Single file
- `'file[]'` - Multiple files
### Required vs Optional
- Always explicitly set `required: true` or `required: false`
- Optional params should have `required: false`
## Critical Rules for Outputs
### Output Types
- `'string'`, `'number'`, `'boolean'` - Primitives
- `'json'` - Complex objects (use this, NOT 'object')
- `'array'` - Arrays with `items` property
- `'object'` - Objects with `properties` property
### Optional Outputs
Add `optional: true` for fields that may not exist in the response:
```typescript
closedAt: {
type: 'string',
description: 'When the issue was closed',
optional: true,
},
```
### Nested Properties
For complex outputs, define nested structure:
```typescript
metadata: {
type: 'json',
description: 'Response metadata',
properties: {
id: { type: 'string', description: 'Unique ID' },
status: { type: 'string', description: 'Current status' },
count: { type: 'number', description: 'Total count' },
},
},
items: {
type: 'array',
description: 'List of items',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Item ID' },
name: { type: 'string', description: 'Item name' },
},
},
},
```
## Critical Rules for transformResponse
### Handle Nullable Fields
ALWAYS use `?? null` for fields that may be undefined:
```typescript
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
id: data.id,
title: data.title,
body: data.body ?? null, // May be undefined
assignee: data.assignee ?? null, // May be undefined
labels: data.labels ?? [], // Default to empty array
closedAt: data.closed_at ?? null, // May be undefined
},
}
}
```
### Never Output Raw JSON Dumps
DON'T do this:
```typescript
output: {
data: data, // BAD - raw JSON dump
}
```
DO this instead - extract meaningful fields:
```typescript
output: {
id: data.id,
name: data.name,
status: data.status,
metadata: {
createdAt: data.created_at,
updatedAt: data.updated_at,
},
}
```
## Types File Pattern
Create `types.ts` with interfaces for all params and responses:
```typescript
import type { ToolResponse } from '@/tools/types'
// Parameter interfaces
export interface {Service}{Action}Params {
accessToken: string
requiredField: string
optionalField?: string
}
// Response interfaces (extend ToolResponse)
export interface {Service}{Action}Response extends ToolResponse {
output: {
field1: string
field2: number
optionalField?: string | null
}
}
```
## Index.ts Barrel Export Pattern
```typescript
// Export all tools
export { serviceTool1 } from './{action1}'
export { serviceTool2 } from './{action2}'
// Export types
export * from './types'
```
## Registering Tools
After creating tools, remind the user to:
1. Import tools in `apps/sim/tools/registry.ts`
2. Add to the `tools` object with snake_case keys:
```typescript
import { serviceActionTool } from '@/tools/{service}'
export const tools = {
// ... existing tools ...
{service}_{action}: serviceActionTool,
}
```
## V2 Tool Pattern
If creating V2 tools (API-aligned outputs), use `_v2` suffix:
- Tool ID: `{service}_{action}_v2`
- Variable name: `{action}V2Tool`
- Version: `'2.0.0'`
- Outputs: Flat, API-aligned (no content/metadata wrapper)
## Checklist Before Finishing
- [ ] All params have explicit `required: true` or `required: false`
- [ ] All params have appropriate `visibility`
- [ ] All nullable response fields use `?? null`
- [ ] All optional outputs have `optional: true`
- [ ] No raw JSON dumps in outputs
- [ ] Types file has all interfaces
- [ ] Index.ts exports all tools
- [ ] Tool IDs use snake_case

View File

@@ -1,656 +0,0 @@
---
description: Create webhook triggers for a Sim Studio integration using the generic trigger builder
argument-hint: <service-name>
---
# Add Trigger Skill
You are an expert at creating webhook triggers for Sim Studio. You understand the trigger system, the generic `buildTriggerSubBlocks` helper, and how triggers connect to blocks.
## Your Task
When the user asks you to create triggers for a service:
1. Research what webhook events the service supports
2. Create the trigger files using the generic builder
3. Register triggers and connect them to the block
## Directory Structure
```
apps/sim/triggers/{service}/
├── index.ts # Barrel exports
├── utils.ts # Service-specific helpers (trigger options, setup instructions, extra fields)
├── {event_a}.ts # Primary trigger (includes dropdown)
├── {event_b}.ts # Secondary trigger (no dropdown)
├── {event_c}.ts # Secondary trigger (no dropdown)
└── webhook.ts # Generic webhook trigger (optional, for "all events")
```
## Step 1: Create utils.ts
This file contains service-specific helpers used by all triggers.
```typescript
import type { SubBlockConfig } from '@/blocks/types'
import type { TriggerOutput } from '@/triggers/types'
/**
* Dropdown options for the trigger type selector.
* These appear in the primary trigger's dropdown.
*/
export const {service}TriggerOptions = [
{ label: 'Event A', id: '{service}_event_a' },
{ label: 'Event B', id: '{service}_event_b' },
{ label: 'Event C', id: '{service}_event_c' },
{ label: 'Generic Webhook (All Events)', id: '{service}_webhook' },
]
/**
* Generates HTML setup instructions for the trigger.
* Displayed to users to help them configure webhooks in the external service.
*/
export function {service}SetupInstructions(eventType: string): string {
const instructions = [
'Copy the <strong>Webhook URL</strong> above',
'Go to <strong>{Service} Settings > Webhooks</strong>',
'Click <strong>Add Webhook</strong>',
'Paste the webhook URL',
`Select the <strong>${eventType}</strong> event type`,
'Save the webhook configuration',
'Click "Save" above to activate your trigger',
]
return instructions
.map((instruction, index) =>
`<div class="mb-3"><strong>${index + 1}.</strong> ${instruction}</div>`
)
.join('')
}
/**
* Service-specific extra fields to add to triggers.
* These are inserted between webhookUrl and triggerSave.
*/
export function build{Service}ExtraFields(triggerId: string): SubBlockConfig[] {
return [
{
id: 'projectId',
title: 'Project ID (Optional)',
type: 'short-input',
placeholder: 'Leave empty for all projects',
description: 'Optionally filter to a specific project',
mode: 'trigger',
condition: { field: 'selectedTriggerId', value: triggerId },
},
]
}
/**
* Build outputs for this trigger type.
* Outputs define what data is available to downstream blocks.
*/
export function build{Service}Outputs(): Record<string, TriggerOutput> {
return {
eventType: { type: 'string', description: 'The type of event that triggered this workflow' },
resourceId: { type: 'string', description: 'ID of the affected resource' },
timestamp: { type: 'string', description: 'When the event occurred (ISO 8601)' },
// Nested outputs for complex data
resource: {
id: { type: 'string', description: 'Resource ID' },
name: { type: 'string', description: 'Resource name' },
status: { type: 'string', description: 'Current status' },
},
webhook: { type: 'json', description: 'Full webhook payload' },
}
}
```
## Step 2: Create the Primary Trigger
The **primary trigger** is the first one listed. It MUST include `includeDropdown: true` so users can switch between trigger types.
```typescript
import { {Service}Icon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
build{Service}ExtraFields,
build{Service}Outputs,
{service}SetupInstructions,
{service}TriggerOptions,
} from '@/triggers/{service}/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
* {Service} Event A Trigger
*
* This is the PRIMARY trigger - it includes the dropdown for selecting trigger type.
*/
export const {service}EventATrigger: TriggerConfig = {
id: '{service}_event_a',
name: '{Service} Event A',
provider: '{service}',
description: 'Trigger workflow when Event A occurs',
version: '1.0.0',
icon: {Service}Icon,
subBlocks: buildTriggerSubBlocks({
triggerId: '{service}_event_a',
triggerOptions: {service}TriggerOptions,
includeDropdown: true, // PRIMARY TRIGGER - includes dropdown
setupInstructions: {service}SetupInstructions('Event A'),
extraFields: build{Service}ExtraFields('{service}_event_a'),
}),
outputs: build{Service}Outputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
},
}
```
## Step 3: Create Secondary Triggers
Secondary triggers do NOT include the dropdown (it's already in the primary trigger).
```typescript
import { {Service}Icon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
build{Service}ExtraFields,
build{Service}Outputs,
{service}SetupInstructions,
{service}TriggerOptions,
} from '@/triggers/{service}/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
* {Service} Event B Trigger
*/
export const {service}EventBTrigger: TriggerConfig = {
id: '{service}_event_b',
name: '{Service} Event B',
provider: '{service}',
description: 'Trigger workflow when Event B occurs',
version: '1.0.0',
icon: {Service}Icon,
subBlocks: buildTriggerSubBlocks({
triggerId: '{service}_event_b',
triggerOptions: {service}TriggerOptions,
// NO includeDropdown - secondary trigger
setupInstructions: {service}SetupInstructions('Event B'),
extraFields: build{Service}ExtraFields('{service}_event_b'),
}),
outputs: build{Service}Outputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
},
}
```
## Step 4: Create index.ts Barrel Export
```typescript
export { {service}EventATrigger } from './event_a'
export { {service}EventBTrigger } from './event_b'
export { {service}EventCTrigger } from './event_c'
export { {service}WebhookTrigger } from './webhook'
```
## Step 5: Register Triggers
### Trigger Registry (`apps/sim/triggers/registry.ts`)
```typescript
// Add import
import {
{service}EventATrigger,
{service}EventBTrigger,
{service}EventCTrigger,
{service}WebhookTrigger,
} from '@/triggers/{service}'
// Add to TRIGGER_REGISTRY
export const TRIGGER_REGISTRY: TriggerRegistry = {
// ... existing triggers ...
{service}_event_a: {service}EventATrigger,
{service}_event_b: {service}EventBTrigger,
{service}_event_c: {service}EventCTrigger,
{service}_webhook: {service}WebhookTrigger,
}
```
## Step 6: Connect Triggers to Block
In the block file (`apps/sim/blocks/blocks/{service}.ts`):
```typescript
import { {Service}Icon } from '@/components/icons'
import { getTrigger } from '@/triggers'
import type { BlockConfig } from '@/blocks/types'
export const {Service}Block: BlockConfig = {
type: '{service}',
name: '{Service}',
// ... other config ...
// Enable triggers and list available trigger IDs
triggers: {
enabled: true,
available: [
'{service}_event_a',
'{service}_event_b',
'{service}_event_c',
'{service}_webhook',
],
},
subBlocks: [
// Regular tool subBlocks first
{ id: 'operation', /* ... */ },
{ id: 'credential', /* ... */ },
// ... other tool fields ...
// Then spread ALL trigger subBlocks
...getTrigger('{service}_event_a').subBlocks,
...getTrigger('{service}_event_b').subBlocks,
...getTrigger('{service}_event_c').subBlocks,
...getTrigger('{service}_webhook').subBlocks,
],
// ... tools config ...
}
```
## Automatic Webhook Registration (Preferred)
If the service's API supports programmatic webhook creation, implement automatic webhook registration instead of requiring users to manually configure webhooks. This provides a much better user experience.
### When to Use Automatic Registration
Check the service's API documentation for endpoints like:
- `POST /webhooks` or `POST /hooks` - Create webhook
- `DELETE /webhooks/{id}` - Delete webhook
Services that support this pattern include: Grain, Lemlist, Calendly, Airtable, Webflow, Typeform, etc.
### Implementation Steps
#### 1. Add API Key to Extra Fields
Update your `build{Service}ExtraFields` function to include an API key field:
```typescript
export function build{Service}ExtraFields(triggerId: string): SubBlockConfig[] {
return [
{
id: 'apiKey',
title: 'API Key',
type: 'short-input',
placeholder: 'Enter your {Service} API key',
description: 'Required to create the webhook in {Service}.',
password: true,
required: true,
mode: 'trigger',
condition: { field: 'selectedTriggerId', value: triggerId },
},
// Other optional fields (e.g., campaign filter, project filter)
{
id: 'projectId',
title: 'Project ID (Optional)',
type: 'short-input',
placeholder: 'Leave empty for all projects',
mode: 'trigger',
condition: { field: 'selectedTriggerId', value: triggerId },
},
]
}
```
#### 2. Update Setup Instructions for Automatic Creation
Change instructions to indicate automatic webhook creation:
```typescript
export function {service}SetupInstructions(eventType: string): string {
const instructions = [
'Enter your {Service} API Key above.',
'You can find your API key in {Service} at <strong>Settings > API</strong>.',
`Click <strong>"Save Configuration"</strong> to automatically create the webhook in {Service} for <strong>${eventType}</strong> events.`,
'The webhook will be automatically deleted when you remove this trigger.',
]
return instructions
.map((instruction, index) =>
`<div class="mb-3"><strong>${index + 1}.</strong> ${instruction}</div>`
)
.join('')
}
```
#### 3. Add Webhook Creation to API Route
In `apps/sim/app/api/webhooks/route.ts`, add provider-specific logic after the database save:
```typescript
// --- {Service} specific logic ---
if (savedWebhook && provider === '{service}') {
logger.info(`[${requestId}] {Service} provider detected. Creating webhook subscription.`)
try {
const result = await create{Service}WebhookSubscription(
{
id: savedWebhook.id,
path: savedWebhook.path,
providerConfig: savedWebhook.providerConfig,
},
requestId
)
if (result) {
// Update the webhook record with the external webhook ID
const updatedConfig = {
...(savedWebhook.providerConfig as Record<string, any>),
externalId: result.id,
}
await db
.update(webhook)
.set({
providerConfig: updatedConfig,
updatedAt: new Date(),
})
.where(eq(webhook.id, savedWebhook.id))
savedWebhook.providerConfig = updatedConfig
logger.info(`[${requestId}] Successfully created {Service} webhook`, {
externalHookId: result.id,
webhookId: savedWebhook.id,
})
}
} catch (err) {
logger.error(
`[${requestId}] Error creating {Service} webhook subscription, rolling back webhook`,
err
)
await db.delete(webhook).where(eq(webhook.id, savedWebhook.id))
return NextResponse.json(
{
error: 'Failed to create webhook in {Service}',
details: err instanceof Error ? err.message : 'Unknown error',
},
{ status: 500 }
)
}
}
// --- End {Service} specific logic ---
```
Then add the helper function at the end of the file:
```typescript
async function create{Service}WebhookSubscription(
webhookData: any,
requestId: string
): Promise<{ id: string } | undefined> {
try {
const { path, providerConfig } = webhookData
const { apiKey, triggerId, projectId } = providerConfig || {}
if (!apiKey) {
throw new Error('{Service} API Key is required.')
}
// Map trigger IDs to service event types
const eventTypeMap: Record<string, string | undefined> = {
{service}_event_a: 'eventA',
{service}_event_b: 'eventB',
{service}_webhook: undefined, // Generic - no filter
}
const eventType = eventTypeMap[triggerId]
const notificationUrl = `${getBaseUrl()}/api/webhooks/trigger/${path}`
const requestBody: Record<string, any> = {
url: notificationUrl,
}
if (eventType) {
requestBody.eventType = eventType
}
if (projectId) {
requestBody.projectId = projectId
}
const response = await fetch('https://api.{service}.com/webhooks', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
})
const responseBody = await response.json()
if (!response.ok) {
const errorMessage = responseBody.message || 'Unknown API error'
let userFriendlyMessage = 'Failed to create webhook in {Service}'
if (response.status === 401) {
userFriendlyMessage = 'Invalid API Key. Please verify and try again.'
} else if (errorMessage) {
userFriendlyMessage = `{Service} error: ${errorMessage}`
}
throw new Error(userFriendlyMessage)
}
return { id: responseBody.id }
} catch (error: any) {
logger.error(`Exception during {Service} webhook creation`, { error: error.message })
throw error
}
}
```
#### 4. Add Webhook Deletion to Provider Subscriptions
In `apps/sim/lib/webhooks/provider-subscriptions.ts`:
1. Add a logger:
```typescript
const {service}Logger = createLogger('{Service}Webhook')
```
2. Add the delete function:
```typescript
export async function delete{Service}Webhook(webhook: any, requestId: string): Promise<void> {
try {
const config = getProviderConfig(webhook)
const apiKey = config.apiKey as string | undefined
const externalId = config.externalId as string | undefined
if (!apiKey || !externalId) {
{service}Logger.warn(`[${requestId}] Missing apiKey or externalId, skipping cleanup`)
return
}
const response = await fetch(`https://api.{service}.com/webhooks/${externalId}`, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${apiKey}`,
},
})
if (!response.ok && response.status !== 404) {
{service}Logger.warn(`[${requestId}] Failed to delete webhook (non-fatal): ${response.status}`)
} else {
{service}Logger.info(`[${requestId}] Successfully deleted webhook ${externalId}`)
}
} catch (error) {
{service}Logger.warn(`[${requestId}] Error deleting webhook (non-fatal)`, error)
}
}
```
3. Add to `cleanupExternalWebhook`:
```typescript
export async function cleanupExternalWebhook(...): Promise<void> {
// ... existing providers ...
} else if (webhook.provider === '{service}') {
await delete{Service}Webhook(webhook, requestId)
}
}
```
### Key Points for Automatic Registration
- **API Key visibility**: Always use `password: true` for API key fields
- **Error handling**: Roll back the database webhook if external creation fails
- **External ID storage**: Save the external webhook ID in `providerConfig.externalId`
- **Graceful cleanup**: Don't fail webhook deletion if cleanup fails (use non-fatal logging)
- **User-friendly errors**: Map HTTP status codes to helpful error messages
## The buildTriggerSubBlocks Helper
This is the generic helper from `@/triggers` that creates consistent trigger subBlocks.
### Function Signature
```typescript
interface BuildTriggerSubBlocksOptions {
triggerId: string // e.g., 'service_event_a'
triggerOptions: Array<{ label: string; id: string }> // Dropdown options
includeDropdown?: boolean // true only for primary trigger
setupInstructions: string // HTML instructions
extraFields?: SubBlockConfig[] // Service-specific fields
webhookPlaceholder?: string // Custom placeholder text
}
function buildTriggerSubBlocks(options: BuildTriggerSubBlocksOptions): SubBlockConfig[]
```
### What It Creates
The helper creates this structure:
1. **Dropdown** (only if `includeDropdown: true`) - Trigger type selector
2. **Webhook URL** - Read-only field with copy button
3. **Extra Fields** - Your service-specific fields (filters, options, etc.)
4. **Save Button** - Activates the trigger
5. **Instructions** - Setup guide for users
All fields automatically have:
- `mode: 'trigger'` - Only shown in trigger mode
- `condition: { field: 'selectedTriggerId', value: triggerId }` - Only shown when this trigger is selected
## Trigger Outputs
Trigger outputs use the same schema as block outputs (NOT tool outputs).
**Supported:**
- `type` and `description` for simple fields
- Nested object structure for complex data
**NOT Supported:**
- `optional: true` (tool outputs only)
- `items` property (tool outputs only)
```typescript
export function buildOutputs(): Record<string, TriggerOutput> {
return {
// Simple fields
eventType: { type: 'string', description: 'Event type' },
timestamp: { type: 'string', description: 'When it occurred' },
// Complex data - use type: 'json'
payload: { type: 'json', description: 'Full event payload' },
// Nested structure
resource: {
id: { type: 'string', description: 'Resource ID' },
name: { type: 'string', description: 'Resource name' },
},
}
}
```
## Generic Webhook Trigger Pattern
For services with many event types, create a generic webhook that accepts all events:
```typescript
export const {service}WebhookTrigger: TriggerConfig = {
id: '{service}_webhook',
name: '{Service} Webhook (All Events)',
// ...
subBlocks: buildTriggerSubBlocks({
triggerId: '{service}_webhook',
triggerOptions: {service}TriggerOptions,
setupInstructions: {service}SetupInstructions('All Events'),
extraFields: [
// Event type filter (optional)
{
id: 'eventTypes',
title: 'Event Types',
type: 'dropdown',
multiSelect: true,
options: [
{ label: 'Event A', id: 'event_a' },
{ label: 'Event B', id: 'event_b' },
],
placeholder: 'Leave empty for all events',
mode: 'trigger',
condition: { field: 'selectedTriggerId', value: '{service}_webhook' },
},
// Plus any other service-specific fields
...build{Service}ExtraFields('{service}_webhook'),
],
}),
}
```
## Checklist Before Finishing
### Utils
- [ ] Created `{service}TriggerOptions` array with all trigger IDs
- [ ] Created `{service}SetupInstructions` function with clear steps
- [ ] Created `build{Service}ExtraFields` for service-specific fields
- [ ] Created output builders for each trigger type
### Triggers
- [ ] Primary trigger has `includeDropdown: true`
- [ ] Secondary triggers do NOT have `includeDropdown`
- [ ] All triggers use `buildTriggerSubBlocks` helper
- [ ] All triggers have proper outputs defined
- [ ] Created `index.ts` barrel export
### Registration
- [ ] All triggers imported in `triggers/registry.ts`
- [ ] All triggers added to `TRIGGER_REGISTRY`
- [ ] Block has `triggers.enabled: true`
- [ ] Block has all trigger IDs in `triggers.available`
- [ ] Block spreads all trigger subBlocks: `...getTrigger('id').subBlocks`
### Automatic Webhook Registration (if supported)
- [ ] Added API key field to `build{Service}ExtraFields` with `password: true`
- [ ] Updated setup instructions for automatic webhook creation
- [ ] Added provider-specific logic to `apps/sim/app/api/webhooks/route.ts`
- [ ] Added `create{Service}WebhookSubscription` helper function
- [ ] Added `delete{Service}Webhook` function to `provider-subscriptions.ts`
- [ ] Added provider to `cleanupExternalWebhook` function
### Testing
- [ ] Run `bun run type-check` to verify no TypeScript errors
- [ ] Restart dev server to pick up new triggers
- [ ] Test trigger UI shows correctly in the block
- [ ] Test automatic webhook creation works (if applicable)

View File

@@ -1,35 +0,0 @@
---
description: EMCN component library patterns
globs: ["apps/sim/components/emcn/**"]
---
# EMCN Components
Import from `@/components/emcn`, never from subpaths (except CSS files).
## CVA vs Direct Styles
**Use CVA when:** 2+ variants (primary/secondary, sm/md/lg)
```tsx
const buttonVariants = cva('base-classes', {
variants: { variant: { default: '...', primary: '...' } }
})
export { Button, buttonVariants }
```
**Use direct className when:** Single consistent style, no variations
```tsx
function Label({ className, ...props }) {
return <Primitive className={cn('style-classes', className)} {...props} />
}
```
## Rules
- Use Radix UI primitives for accessibility
- Export component and variants (if using CVA)
- TSDoc with usage examples
- Consistent tokens: `font-medium`, `text-[12px]`, `rounded-[4px]`
- `transition-colors` for hover states

View File

@@ -1,20 +0,0 @@
---
description: Global coding standards that apply to all files
alwaysApply: true
---
# Global Standards
You are a professional software engineer. All code must follow best practices: accurate, readable, clean, and efficient.
## Logging
Import `createLogger` from `sim/logger`. Use `logger.info`, `logger.warn`, `logger.error` instead of `console.log`.
## Comments
Use TSDoc for documentation. No `====` separators. No non-TSDoc comments.
## Styling
Never update global styles. Keep all styling local to components.
## Package Manager
Use `bun` and `bunx`, not `npm` and `npx`.

View File

@@ -1,56 +0,0 @@
---
description: Core architecture principles for the Sim app
globs: ["apps/sim/**"]
---
# Sim App Architecture
## Core Principles
1. **Single Responsibility**: Each component, hook, store has one clear purpose
2. **Composition Over Complexity**: Break down complex logic into smaller pieces
3. **Type Safety First**: TypeScript interfaces for all props, state, return types
4. **Predictable State**: Zustand for global state, useState for UI-only concerns
## Root-Level Structure
```
apps/sim/
├── app/ # Next.js app router (pages, API routes)
├── blocks/ # Block definitions and registry
├── components/ # Shared UI (emcn/, ui/)
├── executor/ # Workflow execution engine
├── hooks/ # Shared hooks (queries/, selectors/)
├── lib/ # App-wide utilities
├── providers/ # LLM provider integrations
├── stores/ # Zustand stores
├── tools/ # Tool definitions
└── triggers/ # Trigger definitions
```
## Feature Organization
Features live under `app/workspace/[workspaceId]/`:
```
feature/
├── components/ # Feature components
├── hooks/ # Feature-scoped hooks
├── utils/ # Feature-scoped utilities (2+ consumers)
├── feature.tsx # Main component
└── page.tsx # Next.js page entry
```
## Naming Conventions
- **Components**: PascalCase (`WorkflowList`)
- **Hooks**: `use` prefix (`useWorkflowOperations`)
- **Files**: kebab-case (`workflow-list.tsx`)
- **Stores**: `stores/feature/store.ts`
- **Constants**: SCREAMING_SNAKE_CASE
- **Interfaces**: PascalCase with suffix (`WorkflowListProps`)
## Utils Rules
- **Never create `utils.ts` for single consumer** - inline it
- **Create `utils.ts` when** 2+ files need the same helper
- **Check existing sources** before duplicating (`lib/` has many utilities)
- **Location**: `lib/` (app-wide) → `feature/utils/` (feature-scoped) → inline (single-use)

View File

@@ -1,48 +0,0 @@
---
description: Component patterns and structure for React components
globs: ["apps/sim/**/*.tsx"]
---
# Component Patterns
## Structure Order
```typescript
'use client' // Only if using hooks
// Imports (external → internal)
// Constants at module level
const CONFIG = { SPACING: 8 } as const
// Props interface
interface ComponentProps {
requiredProp: string
optionalProp?: boolean
}
export function Component({ requiredProp, optionalProp = false }: ComponentProps) {
// a. Refs
// b. External hooks (useParams, useRouter)
// c. Store hooks
// d. Custom hooks
// e. Local state
// f. useMemo
// g. useCallback
// h. useEffect
// i. Return JSX
}
```
## Rules
1. `'use client'` only when using React hooks
2. Always define props interface
3. Extract constants with `as const`
4. Semantic HTML (`aside`, `nav`, `article`)
5. Optional chain callbacks: `onAction?.(id)`
## Component Extraction
**Extract when:** 50+ lines, used in 2+ files, or has own state/logic
**Keep inline when:** < 10 lines, single use, purely presentational

View File

@@ -1,54 +0,0 @@
---
description: Custom hook patterns and best practices
globs: ["apps/sim/**/use-*.ts", "apps/sim/**/hooks/**/*.ts"]
---
# Hook Patterns
## Structure
```typescript
interface UseFeatureProps {
id: string
onSuccess?: (result: Result) => void
}
export function useFeature({ id, onSuccess }: UseFeatureProps) {
// 1. Refs for stable dependencies
const idRef = useRef(id)
const onSuccessRef = useRef(onSuccess)
// 2. State
const [data, setData] = useState<Data | null>(null)
const [isLoading, setIsLoading] = useState(false)
// 3. Sync refs
useEffect(() => {
idRef.current = id
onSuccessRef.current = onSuccess
}, [id, onSuccess])
// 4. Operations (useCallback with empty deps when using refs)
const fetchData = useCallback(async () => {
setIsLoading(true)
try {
const result = await fetch(`/api/${idRef.current}`).then(r => r.json())
setData(result)
onSuccessRef.current?.(result)
} finally {
setIsLoading(false)
}
}, [])
return { data, isLoading, fetchData }
}
```
## Rules
1. Single responsibility per hook
2. Props interface required
3. Refs for stable callback dependencies
4. Wrap returned functions in useCallback
5. Always try/catch async operations
6. Track loading/error states

View File

@@ -1,61 +0,0 @@
---
description: Import patterns for the Sim application
globs: ["apps/sim/**/*.ts", "apps/sim/**/*.tsx"]
---
# Import Patterns
## Absolute Imports
**Always use absolute imports.** Never use relative imports.
```typescript
// ✓ Good
import { useWorkflowStore } from '@/stores/workflows/store'
import { Button } from '@/components/ui/button'
// ✗ Bad
import { useWorkflowStore } from '../../../stores/workflows/store'
```
## Barrel Exports
Use barrel exports (`index.ts`) when a folder has 3+ exports. Import from barrel, not individual files.
```typescript
// ✓ Good
import { Dashboard, Sidebar } from '@/app/workspace/[workspaceId]/logs/components'
// ✗ Bad
import { Dashboard } from '@/app/workspace/[workspaceId]/logs/components/dashboard/dashboard'
```
## No Re-exports
Do not re-export from non-barrel files. Import directly from the source.
```typescript
// ✓ Good - import from where it's declared
import { CORE_TRIGGER_TYPES } from '@/stores/logs/filters/types'
// ✗ Bad - re-exporting in utils.ts then importing from there
import { CORE_TRIGGER_TYPES } from '@/app/workspace/.../utils'
```
## Import Order
1. React/core libraries
2. External libraries
3. UI components (`@/components/emcn`, `@/components/ui`)
4. Utilities (`@/lib/...`)
5. Stores (`@/stores/...`)
6. Feature imports
7. CSS imports
## Type Imports
Use `type` keyword for type-only imports:
```typescript
import type { WorkflowLog } from '@/stores/logs/types'
```

View File

@@ -1,207 +0,0 @@
---
description: Adding new integrations (tools, blocks, triggers)
globs: ["apps/sim/tools/**", "apps/sim/blocks/**", "apps/sim/triggers/**"]
---
# Adding Integrations
## Overview
Adding a new integration typically requires:
1. **Tools** - API operations (`tools/{service}/`)
2. **Block** - UI component (`blocks/blocks/{service}.ts`)
3. **Icon** - SVG icon (`components/icons.tsx`)
4. **Trigger** (optional) - Webhooks/polling (`triggers/{service}/`)
Always look up the service's API docs first.
## 1. Tools (`tools/{service}/`)
```
tools/{service}/
├── index.ts # Export all tools
├── types.ts # Params/response types
├── {action}.ts # Individual tool (e.g., send_message.ts)
└── ...
```
**Tool file structure:**
```typescript
// tools/{service}/{action}.ts
import type { {Service}Params, {Service}Response } from '@/tools/{service}/types'
import type { ToolConfig } from '@/tools/types'
export const {service}{Action}Tool: ToolConfig<{Service}Params, {Service}Response> = {
id: '{service}_{action}',
name: '{Service} {Action}',
description: 'What this tool does',
version: '1.0.0',
oauth: { required: true, provider: '{service}' }, // if OAuth
params: { /* param definitions */ },
request: {
url: '/api/tools/{service}/{action}',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({ ...params }),
},
transformResponse: async (response) => {
const data = await response.json()
if (!data.success) throw new Error(data.error)
return { success: true, output: data.output }
},
outputs: { /* output definitions */ },
}
```
**Register in `tools/registry.ts`:**
```typescript
import { {service}{Action}Tool } from '@/tools/{service}'
// Add to registry object
{service}_{action}: {service}{Action}Tool,
```
## 2. Block (`blocks/blocks/{service}.ts`)
```typescript
import { {Service}Icon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import type { {Service}Response } from '@/tools/{service}/types'
export const {Service}Block: BlockConfig<{Service}Response> = {
type: '{service}',
name: '{Service}',
description: 'Short description',
longDescription: 'Detailed description',
category: 'tools',
bgColor: '#hexcolor',
icon: {Service}Icon,
subBlocks: [ /* see SubBlock Properties below */ ],
tools: {
access: ['{service}_{action}', ...],
config: {
tool: (params) => `{service}_${params.operation}`,
params: (params) => ({ ...params }),
},
},
inputs: { /* input definitions */ },
outputs: { /* output definitions */ },
}
```
### SubBlock Properties
```typescript
{
id: 'fieldName', // Unique identifier
title: 'Field Label', // UI label
type: 'short-input', // See SubBlock Types below
placeholder: 'Hint text',
required: true, // See Required below
condition: { ... }, // See Condition below
dependsOn: ['otherField'], // See DependsOn below
mode: 'basic', // 'basic' | 'advanced' | 'both' | 'trigger'
}
```
**SubBlock Types:** `short-input`, `long-input`, `dropdown`, `code`, `switch`, `slider`, `oauth-input`, `channel-selector`, `user-selector`, `file-upload`, etc.
### `condition` - Show/hide based on another field
```typescript
// Show when operation === 'send'
condition: { field: 'operation', value: 'send' }
// Show when operation is 'send' OR 'read'
condition: { field: 'operation', value: ['send', 'read'] }
// Show when operation !== 'send'
condition: { field: 'operation', value: 'send', not: true }
// Complex: NOT in list AND another condition
condition: {
field: 'operation',
value: ['list_channels', 'list_users'],
not: true,
and: { field: 'destinationType', value: 'dm', not: true }
}
```
### `required` - Field validation
```typescript
// Always required
required: true
// Conditionally required (same syntax as condition)
required: { field: 'operation', value: 'send' }
```
### `dependsOn` - Clear field when dependencies change
```typescript
// Clear when credential changes
dependsOn: ['credential']
// Clear when authMethod changes AND (credential OR botToken) changes
dependsOn: { all: ['authMethod'], any: ['credential', 'botToken'] }
```
### `mode` - When to show field
- `'basic'` - Only in basic mode (default UI)
- `'advanced'` - Only in advanced mode (manual input)
- `'both'` - Show in both modes (default)
- `'trigger'` - Only when block is used as trigger
**Register in `blocks/registry.ts`:**
```typescript
import { {Service}Block } from '@/blocks/blocks/{service}'
// Add to registry object (alphabetically)
{service}: {Service}Block,
```
## 3. Icon (`components/icons.tsx`)
```typescript
export function {Service}Icon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
{/* SVG path from service's brand assets */}
</svg>
)
}
```
## 4. Trigger (`triggers/{service}/`) - Optional
```
triggers/{service}/
├── index.ts # Export all triggers
├── webhook.ts # Webhook handler
├── utils.ts # Shared utilities
└── {event}.ts # Specific event handlers
```
**Register in `triggers/registry.ts`:**
```typescript
import { {service}WebhookTrigger } from '@/triggers/{service}'
// Add to TRIGGER_REGISTRY
{service}_webhook: {service}WebhookTrigger,
```
## Checklist
- [ ] Look up API docs for the service
- [ ] Create `tools/{service}/types.ts` with proper types
- [ ] Create tool files for each operation
- [ ] Create `tools/{service}/index.ts` barrel export
- [ ] Register tools in `tools/registry.ts`
- [ ] Add icon to `components/icons.tsx`
- [ ] Create block in `blocks/blocks/{service}.ts`
- [ ] Register block in `blocks/registry.ts`
- [ ] (Optional) Create triggers in `triggers/{service}/`
- [ ] (Optional) Register triggers in `triggers/registry.ts`

View File

@@ -1,66 +0,0 @@
---
description: React Query patterns for the Sim application
globs: ["apps/sim/hooks/queries/**/*.ts"]
---
# React Query Patterns
All React Query hooks live in `hooks/queries/`.
## Query Key Factory
Every query file defines a keys factory:
```typescript
export const entityKeys = {
all: ['entity'] as const,
list: (workspaceId?: string) => [...entityKeys.all, 'list', workspaceId ?? ''] as const,
detail: (id?: string) => [...entityKeys.all, 'detail', id ?? ''] as const,
}
```
## File Structure
```typescript
// 1. Query keys factory
// 2. Types (if needed)
// 3. Private fetch functions
// 4. Exported hooks
```
## Query Hook
```typescript
export function useEntityList(workspaceId?: string, options?: { enabled?: boolean }) {
return useQuery({
queryKey: entityKeys.list(workspaceId),
queryFn: () => fetchEntities(workspaceId as string),
enabled: Boolean(workspaceId) && (options?.enabled ?? true),
staleTime: 60 * 1000,
placeholderData: keepPreviousData,
})
}
```
## Mutation Hook
```typescript
export function useCreateEntity() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (variables) => { /* fetch POST */ },
onSuccess: () => queryClient.invalidateQueries({ queryKey: entityKeys.all }),
})
}
```
## Optimistic Updates
For optimistic mutations syncing with Zustand, use `createOptimisticMutationHandlers` from `@/hooks/queries/utils/optimistic-mutation`.
## Naming
- **Keys**: `entityKeys`
- **Query hooks**: `useEntity`, `useEntityList`
- **Mutation hooks**: `useCreateEntity`, `useUpdateEntity`
- **Fetch functions**: `fetchEntity` (private)

View File

@@ -1,70 +0,0 @@
---
description: Zustand store patterns
globs: ["apps/sim/**/store.ts", "apps/sim/**/stores/**/*.ts"]
---
# Zustand Store Patterns
Stores live in `stores/`. Complex stores split into `store.ts` + `types.ts`.
## Basic Store
```typescript
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
import type { FeatureState } from '@/stores/feature/types'
const initialState = { items: [] as Item[], activeId: null as string | null }
export const useFeatureStore = create<FeatureState>()(
devtools(
(set, get) => ({
...initialState,
setItems: (items) => set({ items }),
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
reset: () => set(initialState),
}),
{ name: 'feature-store' }
)
)
```
## Persisted Store
```typescript
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
export const useFeatureStore = create<FeatureState>()(
persist(
(set) => ({
width: 300,
setWidth: (width) => set({ width }),
_hasHydrated: false,
setHasHydrated: (v) => set({ _hasHydrated: v }),
}),
{
name: 'feature-state',
partialize: (state) => ({ width: state.width }),
onRehydrateStorage: () => (state) => state?.setHasHydrated(true),
}
)
)
```
## Rules
1. Use `devtools` middleware (named stores)
2. Use `persist` only when data should survive reload
3. `partialize` to persist only necessary state
4. `_hasHydrated` pattern for persisted stores needing hydration tracking
5. Immutable updates only
6. `set((state) => ...)` when depending on previous state
7. Provide `reset()` action
## Outside React
```typescript
const items = useFeatureStore.getState().items
useFeatureStore.setState({ items: newItems })
```

View File

@@ -1,40 +0,0 @@
---
description: Tailwind CSS and styling conventions
globs: ["apps/sim/**/*.tsx", "apps/sim/**/*.css"]
---
# Styling Rules
## Tailwind
1. **No inline styles** - Use Tailwind classes
2. **No duplicate dark classes** - Skip `dark:` when value matches light mode
3. **Exact values** - `text-[14px]`, `h-[26px]`
4. **Transitions** - `transition-colors` for interactive states
## Conditional Classes
```typescript
import { cn } from '@/lib/utils'
<div className={cn(
'base-classes',
isActive && 'active-classes',
disabled ? 'opacity-60' : 'hover:bg-accent'
)} />
```
## CSS Variables
For dynamic values (widths, heights) synced with stores:
```typescript
// In store
setWidth: (width) => {
set({ width })
document.documentElement.style.setProperty('--sidebar-width', `${width}px`)
}
// In component
<aside style={{ width: 'var(--sidebar-width)' }} />
```

View File

@@ -1,57 +0,0 @@
---
description: Testing patterns with Vitest and @sim/testing
globs: ["apps/sim/**/*.test.ts", "apps/sim/**/*.test.tsx"]
---
# Testing Patterns
Use Vitest. Test files: `feature.ts` → `feature.test.ts`
## Structure
```typescript
/**
* @vitest-environment node
*/
import { databaseMock, loggerMock } from '@sim/testing'
import { describe, expect, it, vi } from 'vitest'
vi.mock('@sim/db', () => databaseMock)
vi.mock('@sim/logger', () => loggerMock)
import { myFunction } from '@/lib/feature'
describe('myFunction', () => {
beforeEach(() => vi.clearAllMocks())
it.concurrent('isolated tests run in parallel', () => { ... })
})
```
## @sim/testing Package
Always prefer over local mocks.
| Category | Utilities |
|----------|-----------|
| **Mocks** | `loggerMock`, `databaseMock`, `setupGlobalFetchMock()` |
| **Factories** | `createSession()`, `createWorkflowRecord()`, `createBlock()`, `createExecutorContext()` |
| **Builders** | `WorkflowBuilder`, `ExecutionContextBuilder` |
| **Assertions** | `expectWorkflowAccessGranted()`, `expectBlockExecuted()` |
## Rules
1. `@vitest-environment node` directive at file top
2. `vi.mock()` calls before importing mocked modules
3. `@sim/testing` utilities over local mocks
4. `it.concurrent` for isolated tests (no shared mutable state)
5. `beforeEach(() => vi.clearAllMocks())` to reset state
## Hoisted Mocks
For mutable mock references:
```typescript
const mockFn = vi.hoisted(() => vi.fn())
vi.mock('@/lib/module', () => ({ myFunction: mockFn }))
mockFn.mockResolvedValue({ data: 'test' })
```

View File

@@ -1,20 +0,0 @@
---
description: TypeScript conventions and type safety
globs: ["apps/sim/**/*.ts", "apps/sim/**/*.tsx"]
---
# TypeScript Rules
1. **No `any`** - Use proper types or `unknown` with type guards
2. **Props interface** - Always define for components
3. **Const assertions** - `as const` for constant objects/arrays
4. **Ref types** - Explicit: `useRef<HTMLDivElement>(null)`
5. **Type imports** - `import type { X }` for type-only imports
```typescript
// ✗ Bad
const handleClick = (e: any) => {}
// ✓ Good
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {}
```

45
.devcontainer/.bashrc Normal file
View File

@@ -0,0 +1,45 @@
# Sim Studio Development Environment Bashrc
# This gets sourced by post-create.sh
# Enhanced prompt with git branch info
parse_git_branch() {
git branch 2> /dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/ (\1)/'
}
export PS1="\[\033[01;32m\]\u@simstudio\[\033[00m\]:\[\033[01;34m\]\w\[\033[33m\]\$(parse_git_branch)\[\033[00m\]\$ "
# Helpful aliases
alias ll="ls -la"
alias ..="cd .."
alias ...="cd ../.."
# Database aliases
alias pgc="PGPASSWORD=postgres psql -h db -U postgres -d simstudio"
alias check-db="PGPASSWORD=postgres psql -h db -U postgres -c '\l'"
# Sim Studio specific aliases
alias logs="cd /workspace/sim && tail -f logs/*.log 2>/dev/null || echo 'No log files found'"
alias sim-start="cd /workspace/sim && npm run dev"
alias sim-migrate="cd /workspace/sim && npx drizzle-kit push"
alias sim-generate="cd /workspace/sim && npx drizzle-kit generate"
alias sim-rebuild="cd /workspace/sim && npm run build && npm start"
# Default to sim directory
cd /workspace/sim 2>/dev/null || true
# Welcome message - only show once per session
if [ -z "$SIM_WELCOME_SHOWN" ]; then
export SIM_WELCOME_SHOWN=1
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🚀 Welcome to Sim Studio development environment!"
echo ""
echo "Available commands:"
echo " sim-start - Start the development server"
echo " sim-migrate - Push schema changes to the database"
echo " sim-generate - Generate new migrations"
echo " sim-rebuild - Build and start the production server"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
fi

View File

@@ -1,47 +1,37 @@
FROM oven/bun:1.3.3-alpine
FROM node:20-bullseye
# Avoid warnings by switching to noninteractive
ENV DEBIAN_FRONTEND=noninteractive
# Set Node.js memory limit
ENV NODE_OPTIONS="--max-old-space-size=4096"
# Install necessary packages for development
RUN apk add --no-cache \
git \
curl \
wget \
jq \
sudo \
postgresql-client \
vim \
nano \
bash \
bash-completion \
zsh \
zsh-vcs \
ca-certificates \
shadow
RUN apt-get update \
&& apt-get -y install --no-install-recommends \
git curl wget jq sudo postgresql-client \
&& apt-get clean -y \
&& rm -rf /var/lib/apt/lists/*
# Create a non-root user with matching UID/GID
ARG USERNAME=bun
# Create a non-root user
ARG USERNAME=node
ARG USER_UID=1000
ARG USER_GID=$USER_UID
# Create user group if it doesn't exist
RUN if ! getent group $USER_GID >/dev/null; then \
addgroup -g $USER_GID $USERNAME; \
fi
# Create user if it doesn't exist
RUN if ! getent passwd $USER_UID >/dev/null; then \
adduser -D -u $USER_UID -G $(getent group $USER_GID | cut -d: -f1) $USERNAME; \
fi
# Add sudo support
RUN echo "$USERNAME ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/$USERNAME \
&& chmod 0440 /etc/sudoers.d/$USERNAME
# Set up shell environment
RUN echo "export PATH=\$PATH:/home/$USERNAME/.bun/bin" >> /etc/profile
# Make sure we have the latest npm
RUN npm install -g npm@latest
# Install global packages
RUN npm install -g drizzle-kit
# Switch back to dialog for any ad-hoc use of apt-get
ENV DEBIAN_FRONTEND=dialog
WORKDIR /workspace
# Expose the ports we're interested in
EXPOSE 3000
EXPOSE 3001
EXPOSE 3002
EXPOSE 3000

View File

@@ -1,75 +1,78 @@
# Sim Development Container
# Sim Studio Development Container
Development container configuration for VS Code Dev Containers and GitHub Codespaces.
This directory contains configuration files for Visual Studio Code Dev Containers / GitHub Codespaces. Dev containers provide a consistent, isolated development environment for this project.
## Prerequisites
## Contents
- `devcontainer.json` - The main configuration file that defines the development container settings
- `Dockerfile` - Defines the container image and development environment
- `docker-compose.yml` - Sets up the application and database containers
- `post-create.sh` - Script that runs when the container is created
- `.bashrc` - Custom shell configuration with helpful aliases
## Usage
### Prerequisites
- Visual Studio Code
- Docker Desktop or Podman Desktop
- VS Code Dev Containers extension
- Docker installation:
- Docker Desktop (Windows/macOS)
- Docker Engine (Linux)
- VS Code Remote - Containers extension
## Getting Started
### Getting Started
1. Open this project in VS Code
2. Click "Reopen in Container" when prompted (or press `F1` → "Dev Containers: Reopen in Container")
1. Open this project in Visual Studio Code
2. When prompted, click "Reopen in Container"
- Alternatively, press `F1` and select "Remote-Containers: Reopen in Container"
3. Wait for the container to build and initialize
4. Start developing with `sim-start`
4. The post-creation script will automatically:
The setup script will automatically install dependencies and run migrations.
- Install dependencies
- Set up environment variables
- Run database migrations
- Configure helpful aliases
## Development Commands
5. Start the application with `sim-start` (alias for `npm run dev`)
### Running Services
### Development Commands
You have two options for running the development environment:
**Option 1: Run everything together (recommended for most development)**
```bash
sim-start # Runs both app and socket server using concurrently
```
**Option 2: Run services separately (useful for debugging individual services)**
- In the **app** container terminal: `sim-app` (starts Next.js app on port 3000)
- In the **realtime** container terminal: `sim-sockets` (starts socket server on port 3002)
### Other Commands
The development environment includes these helpful aliases:
- `sim-start` - Start the development server
- `sim-migrate` - Push schema changes to the database
- `sim-generate` - Generate new migrations
- `build` - Build the application
- `pgc` - Connect to PostgreSQL database
- `sim-rebuild` - Build and start the production version
- `pgc` - Connect to the PostgreSQL database
- `check-db` - List all databases
### Using GitHub Codespaces
This project is also configured for GitHub Codespaces. To use it:
1. Go to the GitHub repository
2. Click the "Code" button
3. Select the "Codespaces" tab
4. Click "Create codespace on main"
This will start a new Codespace with the development environment already set up.
## Customization
You can customize the development environment by:
- Modifying `devcontainer.json` to add VS Code extensions or settings
- Updating the `Dockerfile` to install additional packages
- Editing `docker-compose.yml` to add services or change configuration
- Modifying `.bashrc` to add custom aliases or configurations
## Troubleshooting
**Build errors**: Rebuild the container with `F1` → "Dev Containers: Rebuild Container"
If you encounter issues:
**Port conflicts**: Ensure ports 3000, 3002, and 5432 are available
1. Rebuild the container: `F1` → "Remote-Containers: Rebuild Container"
2. Check Docker logs for build errors
3. Verify Docker Desktop is running
4. Ensure all prerequisites are installed
**Container runtime issues**: Verify Docker Desktop or Podman Desktop is running
## Technical Details
Services:
- **App container** (8GB memory limit) - Main Next.js application
- **Realtime container** (4GB memory limit) - Socket.io server for real-time features
- **Database** - PostgreSQL with pgvector extension
- **Migrations** - Runs automatically on container creation
You can develop with services running together or independently.
### Personalization
**Project commands** (`sim-start`, `sim-app`, etc.) are automatically available via `/workspace/.devcontainer/sim-commands.sh`.
**Personal shell customization** (aliases, prompts, etc.) should use VS Code's dotfiles feature:
1. Create a dotfiles repository (e.g., `github.com/youruser/dotfiles`)
2. Add your `.bashrc`, `.zshrc`, or other configs
3. Configure in VS Code Settings:
```json
{
"dotfiles.repository": "youruser/dotfiles",
"dotfiles.installCommand": "install.sh"
}
```
This separates project-specific commands from personal preferences, following VS Code best practices.
For more information, see the [VS Code Remote Development documentation](https://code.visualstudio.com/docs/remote/containers).

View File

@@ -1,5 +1,5 @@
{
"name": "Sim Dev Environment",
"name": "Sim Studio Dev Environment",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspace",
@@ -10,28 +10,52 @@
"settings": {
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.biome": "explicit",
"source.organizeImports.biome": "explicit"
"source.fixAll.eslint": true
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"terminal.integrated.defaultProfile.linux": "bash",
"terminal.integrated.profiles.linux": {
"bash": {
"path": "/bin/bash",
"args": ["--login"]
}
},
"terminal.integrated.shellIntegration.enabled": true
},
"extensions": [
"biomejs.biome",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"bradlc.vscode-tailwindcss",
"ms-vscode.vscode-typescript-next",
"github.copilot",
"github.copilot-chat",
"rvest.vs-code-prettier-eslint",
"mikestead.dotenv",
"dsznajder.es7-react-js-snippets",
"steoates.autoimport",
"oven.bun-vscode"
"steoates.autoimport"
]
}
},
"forwardPorts": [3000, 3002, 5432],
"forwardPorts": [3000, 5432],
"postCreateCommand": "bash -c 'bash .devcontainer/post-create.sh || true'",
"remoteUser": "bun"
"postStartCommand": "bash -c 'if [ ! -f ~/.bashrc ] || ! grep -q \"sim-start\" ~/.bashrc; then cp .devcontainer/.bashrc ~/.bashrc; fi'",
"remoteUser": "node",
"features": {
"ghcr.io/devcontainers/features/git:1": {},
"ghcr.io/devcontainers-contrib/features/npm-package:1": {
"package": "typescript",
"version": "latest"
}
}
}

View File

@@ -1,3 +1,5 @@
version: '3.8'
services:
app:
build:
@@ -5,73 +7,22 @@ services:
dockerfile: .devcontainer/Dockerfile
volumes:
- ..:/workspace:cached
- bun-cache:/home/bun/.bun/cache:delegated
command: sleep infinity
deploy:
resources:
limits:
memory: 8G
environment:
- NODE_ENV=development
- DATABASE_URL=postgresql://postgres:postgres@db:5432/simstudio
- POSTGRES_URL=postgresql://postgres:postgres@db:5432/simstudio
- BETTER_AUTH_URL=http://localhost:3000
- NEXT_PUBLIC_APP_URL=http://localhost:3000
- BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET:-your_auth_secret_here}
- ENCRYPTION_KEY=${ENCRYPTION_KEY:-your_encryption_key_here}
- COPILOT_API_KEY=${COPILOT_API_KEY}
- SIM_AGENT_API_URL=${SIM_AGENT_API_URL}
- OLLAMA_URL=${OLLAMA_URL:-http://localhost:11434}
- NEXT_PUBLIC_SOCKET_URL=${NEXT_PUBLIC_SOCKET_URL:-http://localhost:3002}
- BUN_INSTALL_CACHE_DIR=/home/bun/.bun/cache
depends_on:
db:
condition: service_healthy
migrations:
condition: service_completed_successfully
ports:
- "3000:3000"
- "3001:3001"
working_dir: /workspace
realtime:
build:
context: ..
dockerfile: .devcontainer/Dockerfile
volumes:
- ..:/workspace:cached
- bun-cache:/home/bun/.bun/cache:delegated
command: sleep infinity
deploy:
resources:
limits:
memory: 4G
environment:
- NODE_ENV=development
- DATABASE_URL=postgresql://postgres:postgres@db:5432/simstudio
- BETTER_AUTH_URL=http://localhost:3000
- NEXT_PUBLIC_APP_URL=http://localhost:3000
- BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET:-your_auth_secret_here}
depends_on:
db:
condition: service_healthy
ports:
- "3002:3002"
working_dir: /workspace
migrations:
build:
context: ..
dockerfile: docker/db.Dockerfile
environment:
- DATABASE_URL=postgresql://postgres:postgres@db:5432/simstudio
depends_on:
db:
condition: service_healthy
command: ['bun', 'run', 'db:migrate']
restart: 'no'
- "3000:3000"
working_dir: /workspace/sim
db:
image: pgvector/pgvector:pg17
image: postgres:16
restart: unless-stopped
volumes:
- postgres-data:/var/lib/postgresql/data
@@ -80,7 +31,7 @@ services:
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=simstudio
ports:
- "${POSTGRES_PORT:-5432}:5432"
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
@@ -88,5 +39,4 @@ services:
retries: 5
volumes:
postgres-data:
bun-cache:
postgres-data:

View File

@@ -3,88 +3,39 @@
# Exit on error, but with some error handling
set -e
echo "🔧 Setting up Sim development environment..."
echo "🔧 Setting up Sim Studio development environment..."
# Change to the workspace root directory
cd /workspace
# Change to the sim directory
cd /workspace/sim
# Install global packages for development (done at runtime, not build time)
echo "📦 Installing global development tools..."
bun install -g turbo drizzle-kit typescript @types/node 2>/dev/null || {
echo "⚠️ Some global packages may already be installed, continuing..."
}
# Set up bun completions (with proper shell detection)
echo "🔧 Setting up shell completions..."
if [ -n "$SHELL" ] && [ -f "$SHELL" ]; then
SHELL=/bin/bash bun completions 2>/dev/null | sudo tee /etc/bash_completion.d/bun > /dev/null || {
echo "⚠️ Could not install bun completions, but continuing..."
}
fi
# Add project commands to shell profile
echo "📄 Setting up project commands..."
# Add sourcing of sim-commands.sh to user's shell config files if they exist
for rcfile in ~/.bashrc ~/.zshrc; do
if [ -f "$rcfile" ]; then
# Check if already added
if ! grep -q "sim-commands.sh" "$rcfile"; then
echo "" >> "$rcfile"
echo "# Sim project commands" >> "$rcfile"
echo "if [ -f /workspace/.devcontainer/sim-commands.sh ]; then" >> "$rcfile"
echo " source /workspace/.devcontainer/sim-commands.sh" >> "$rcfile"
echo "fi" >> "$rcfile"
fi
fi
done
# If no rc files exist yet, create a minimal one
if [ ! -f ~/.bashrc ] && [ ! -f ~/.zshrc ]; then
echo "# Source Sim project commands" > ~/.bashrc
echo "if [ -f /workspace/.devcontainer/sim-commands.sh ]; then" >> ~/.bashrc
echo " source /workspace/.devcontainer/sim-commands.sh" >> ~/.bashrc
echo "fi" >> ~/.bashrc
fi
# Setup .bashrc
echo "📄 Setting up .bashrc with aliases..."
cp /workspace/.devcontainer/.bashrc ~/.bashrc
# Add to .profile to ensure .bashrc is sourced in non-interactive shells
echo 'if [ -f ~/.bashrc ]; then . ~/.bashrc; fi' >> ~/.profile
# Clean and reinstall dependencies to ensure platform compatibility
echo "📦 Cleaning and reinstalling dependencies..."
echo "📦 Cleaning and reinstalling npm dependencies..."
if [ -d "node_modules" ]; then
echo "Removing existing node_modules to ensure platform compatibility..."
rm -rf node_modules
rm -rf apps/sim/node_modules
rm -rf apps/docs/node_modules
fi
# Ensure Bun cache directory exists and has correct permissions
mkdir -p ~/.bun/cache
chmod 700 ~/.bun ~/.bun/cache
# Install dependencies with platform-specific binaries
echo "Installing dependencies with Bun..."
bun install
npm install || {
echo "⚠️ npm install had issues but continuing setup..."
}
# Check for native dependencies
echo "Checking for native dependencies compatibility..."
if grep -q '"trustedDependencies"' apps/sim/package.json 2>/dev/null; then
echo "⚠️ Native dependencies detected. Bun will handle compatibility during install."
fi
# Set up environment variables if .env doesn't exist for the sim app
if [ ! -f "apps/sim/.env" ]; then
# Set up environment variables if .env doesn't exist
if [ ! -f ".env" ]; then
echo "📄 Creating .env file from template..."
if [ -f "apps/sim/.env.example" ]; then
cp apps/sim/.env.example apps/sim/.env
else
echo "DATABASE_URL=postgresql://postgres:postgres@db:5432/simstudio" > apps/sim/.env
fi
cp .env.example .env 2>/dev/null || echo "DATABASE_URL=postgresql://postgres:postgres@db:5432/simstudio" > .env
fi
# Generate schema and run database migrations
echo "🗃️ Running database schema generation and migrations..."
echo "Generating schema..."
cd apps/sim
bunx drizzle-kit generate
cd ../..
npx drizzle-kit generate
echo "Waiting for database to be ready..."
# Try to connect to the database, but don't fail the script if it doesn't work
@@ -93,9 +44,7 @@ echo "Waiting for database to be ready..."
while [ $timeout -gt 0 ]; do
if PGPASSWORD=postgres psql -h db -U postgres -c '\q' 2>/dev/null; then
echo "Database is ready!"
cd apps/sim
DATABASE_URL=postgresql://postgres:postgres@db:5432/simstudio bunx drizzle-kit push
cd ../..
DATABASE_URL=postgresql://postgres:postgres@db:5432/simstudio npx drizzle-kit push
break
fi
echo "Database is unavailable - sleeping (${timeout}s remaining)"
@@ -108,12 +57,28 @@ echo "Waiting for database to be ready..."
fi
) || echo "⚠️ Database setup had issues but continuing..."
# Add additional helpful aliases to .bashrc
cat << EOF >> ~/.bashrc
# Additional Sim Studio Development Aliases
alias migrate="cd /workspace/sim && DATABASE_URL=postgresql://postgres:postgres@db:5432/simstudio npx drizzle-kit push"
alias generate="cd /workspace/sim && npx drizzle-kit generate"
alias dev="cd /workspace/sim && npm run dev"
alias build="cd /workspace/sim && npm run build"
alias start="cd /workspace/sim && npm run start"
alias lint="cd /workspace/sim && npm run lint"
alias test="cd /workspace/sim && npm run test"
EOF
# Source the .bashrc to make aliases available immediately
. ~/.bashrc
# Clear the welcome message flag to ensure it shows after setup
unset SIM_WELCOME_SHOWN
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "✅ Sim development environment setup complete!"
echo "✅ Sim Studio development environment setup complete!"
echo ""
echo "Your environment is now ready. A new terminal session will show"
echo "available commands. You can start the development server with:"

View File

@@ -1,42 +0,0 @@
#!/bin/bash
# Sim Project Commands
# Source this file to add project-specific commands to your shell
# Add to your ~/.bashrc or ~/.zshrc: source /workspace/.devcontainer/sim-commands.sh
# Project-specific aliases for Sim development
alias sim-start="cd /workspace && bun run dev:full"
alias sim-app="cd /workspace && bun run dev"
alias sim-sockets="cd /workspace && bun run dev:sockets"
alias sim-migrate="cd /workspace/apps/sim && bunx drizzle-kit push"
alias sim-generate="cd /workspace/apps/sim && bunx drizzle-kit generate"
alias sim-rebuild="cd /workspace && bun run build && bun run start"
alias docs-dev="cd /workspace/apps/docs && bun run dev"
# Database connection helpers
alias pgc="PGPASSWORD=postgres psql -h db -U postgres -d simstudio"
alias check-db="PGPASSWORD=postgres psql -h db -U postgres -c '\l'"
# Default to workspace directory
cd /workspace 2>/dev/null || true
# Welcome message - show once per session
if [ -z "$SIM_WELCOME_SHOWN" ]; then
export SIM_WELCOME_SHOWN=1
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🚀 Sim Development Environment"
echo ""
echo "Project commands:"
echo " sim-start - Start app + socket server"
echo " sim-app - Start only main app"
echo " sim-sockets - Start only socket server"
echo " sim-migrate - Push schema changes"
echo " sim-generate - Generate migrations"
echo ""
echo "Database:"
echo " pgc - Connect to PostgreSQL"
echo " check-db - List databases"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
fi

View File

@@ -1,67 +1,12 @@
# Git
# Exclude files from Docker build
.git
.gitignore
# Documentation
LICENSE
NOTICE
README.md
*.md
docs/
# IDE and editor
.vscode
.idea
*.swp
*.swo
# Environment and config
.env*
!.env.example
.prettierrc
.prettierignore
.eslintrc*
.eslintignore
# CI/CD and DevOps
.github
.devcontainer
.husky
docker-compose*.yml
Dockerfile*
# Build artifacts and caches
.next
.turbo
.cache
dist
build
out
coverage
*.log
# Dependencies (will be installed fresh in container)
node_modules
.bun
# Test files
**/*.test.ts
**/*.test.tsx
**/*.spec.ts
**/*.spec.tsx
__tests__
__mocks__
jest.config.*
vitest.config.*
# TypeScript build info
*.tsbuildinfo
# OS files
.DS_Store
Thumbs.db
# Temporary files
tmp
temp
*.tmp
.next
.vercel
.husky
.env
.env.*
npm-debug.log
README.md
.devcontainer

34
.gitattributes vendored
View File

@@ -1,34 +0,0 @@
# Set default behavior to automatically normalize line endings
* text=auto eol=lf
# Explicitly declare text files you want to always be normalized and converted
# to native line endings on checkout
*.ts text eol=lf
*.tsx text eol=lf
*.js text eol=lf
*.jsx text eol=lf
*.json text eol=lf
*.md text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
*.toml text eol=lf
*.css text eol=lf
*.scss text eol=lf
*.sh text eol=lf
*.bash text eol=lf
Dockerfile* text eol=lf
.dockerignore text eol=lf
.gitignore text eol=lf
.gitattributes text eol=lf
# Denote all files that are truly binary and should not be modified
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.woff binary
*.woff2 binary
*.ttf binary
*.eot binary
*.pdf binary

View File

@@ -1,4 +1,4 @@
# Code of Conduct - Sim
# Code of Conduct - Sim Studio
## Our Pledge
@@ -14,22 +14,22 @@ appearance, race, religion, or sexual identity and orientation.
Examples of behaviour that contributes to a positive environment for our
community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologising to those affected by our mistakes,
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologising to those affected by our mistakes,
and learning from the experience
- Focusing on what is best not just for us as individuals, but for the
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behaviour include:
- The use of sexualised language or imagery, and sexual attention or advances
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
* The use of sexualised language or imagery, and sexual attention or advances
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
@@ -55,7 +55,7 @@ representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behaviour may be
reported to the community leaders responsible for enforcement at <waleed@sim.ai>.
reported to the community leaders responsible for enforcement at <waleed@simstudio.ai>.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
@@ -112,4 +112,4 @@ the community.
This Code of Conduct is adapted from the [Contributor Covenant](https://contributor-covenant.org/), version
[1.4](https://www.contributor-covenant.org/version/1/4/code-of-conduct/code_of_conduct.md) and
[2.0](https://www.contributor-covenant.org/version/2/0/code_of_conduct/code_of_conduct.md),
and was generated by [contributing.md](https://contributing.md/generator).
and was generated by [contributing.md](https://contributing.md/generator).

View File

@@ -1,9 +1,9 @@
# Contributing to Sim
# Contributing to Sim Studio
Thank you for your interest in contributing to Sim! Our goal is to provide developers with a powerful, user-friendly platform for building, testing, and optimizing agentic workflows. We welcome contributions in all forms—from bug fixes and design improvements to brand-new features.
Thank you for your interest in contributing to Sim Studio! Our goal is to provide developers with a powerful, user-friendly platform for building, testing, and optimizing agentic workflows. We welcome contributions in all forms—from bug fixes and design improvements to brand-new features.
> **Project Overview:**
> Sim is a monorepo using Turborepo, containing the main application (`apps/sim/`), documentation (`apps/docs/`), and shared packages (`packages/`). The main application is built with Next.js (app router), ReactFlow, Zustand, Shadcn, and Tailwind CSS. Please ensure your contributions follow our best practices for clarity, maintainability, and consistency.
> Sim Studio is a monorepo containing the main application (`sim/`) and documentation (`docs/`). The main application is built with Next.js (app router), ReactFlow, Zustand, Shadcn, and Tailwind CSS. Please ensure your contributions follow our best practices for clarity, maintainability, and consistency.
---
@@ -15,6 +15,8 @@ Thank you for your interest in contributing to Sim! Our goal is to provide devel
- [Commit Message Guidelines](#commit-message-guidelines)
- [Local Development Setup](#local-development-setup)
- [Adding New Blocks and Tools](#adding-new-blocks-and-tools)
- [Local Storage Mode](#local-storage-mode)
- [Standalone Build](#standalone-build)
- [License](#license)
- [Contributor License Agreement (CLA)](#contributor-license-agreement-cla)
@@ -55,7 +57,7 @@ We strive to keep our workflow as simple as possible. To contribute:
```
7. **Create a Pull Request**
Open a pull request against the `staging` branch on GitHub. Please provide a clear description of the changes and reference any relevant issues (e.g., `fixes #123`).
Open a pull request against the `main` branch on GitHub. Please provide a clear description of the changes and reference any relevant issues (e.g., `fixes #123`).
---
@@ -83,7 +85,7 @@ If you discover a bug or have a feature request, please open an issue in our Git
Before creating a pull request:
- **Ensure Your Branch Is Up-to-Date:**
Rebase your branch onto the latest `staging` branch to prevent merge conflicts.
Rebase your branch onto the latest `main` branch to prevent merge conflicts.
- **Follow the Guidelines:**
Make sure your changes are well-tested, follow our coding standards, and include relevant documentation if necessary.
@@ -128,73 +130,54 @@ Using clear and consistent commit messages makes it easier for everyone to under
To set up your local development environment:
### Option 1: Using NPM Package (Simplest)
### Option 1: Using Docker (Recommended)
The easiest way to run Sim locally is using our NPM package:
Docker provides a consistent development environment with all dependencies pre-configured.
```bash
npx simstudio
```
1. **Clone the Repository:**
After running this command, open [http://localhost:3000/](http://localhost:3000/) in your browser.
```bash
git clone https://github.com/<your-username>/sim.git
cd sim
```
#### Options
2. **Start the Docker Environment:**
- `-p, --port <port>`: Specify the port to run Sim on (default: 3000)
- `--no-pull`: Skip pulling the latest Docker images
```bash
docker compose up -d
```
#### Requirements
Or use the convenience script which handles environment setup and migrations:
- Docker must be installed and running on your machine
```bash
chmod +x scripts/start_simstudio_docker.sh
./scripts/start_simstudio_docker.sh
```
### Option 2: Using Docker Compose
This will:
```bash
# Clone the repository
git clone https://github.com/<your-username>/sim.git
cd sim
- Start a PostgreSQL database container
- Build and run the Next.js application with hot-reloading
- Set up all necessary environment variables
- Apply database migrations automatically
# Start Sim
docker compose -f docker-compose.prod.yml up -d
```
3. **View Logs:**
Access the application at [http://localhost:3000/](http://localhost:3000/)
```bash
docker compose logs -f simstudio
```
#### Using Local Models
4. **Make Your Changes:**
- Edit files in your local directory
- Changes will be automatically reflected thanks to hot-reloading
To use local models with Sim:
1. Install Ollama and pull models:
```bash
# Install Ollama (if not already installed)
curl -fsSL https://ollama.ai/install.sh | sh
# Pull a model (e.g., gemma3:4b)
ollama pull gemma3:4b
```
2. Start Sim with local model support:
```bash
# With NVIDIA GPU support
docker compose --profile local-gpu -f docker-compose.ollama.yml up -d
# Without GPU (CPU only)
docker compose --profile local-cpu -f docker-compose.ollama.yml up -d
# If hosting on a server, update the environment variables in the docker-compose.prod.yml file
# to include the server's public IP then start again (OLLAMA_URL to i.e. http://1.1.1.1:11434)
docker compose -f docker-compose.prod.yml up -d
```
### Option 3: Using VS Code / Cursor Dev Containers
### Option 2: Using VS Code / Cursor Dev Containers
Dev Containers provide a consistent and easy-to-use development environment:
1. **Prerequisites:**
- Visual Studio Code or Cursor
- Visual Studio Code
- Docker Desktop
- [Remote - Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension for VS Code
@@ -205,56 +188,58 @@ Dev Containers provide a consistent and easy-to-use development environment:
git clone https://github.com/<your-username>/sim.git
cd sim
```
- Open the project in VS Code/Cursor
- Open the project in VS Code
- When prompted, click "Reopen in Container" (or press F1 and select "Remote-Containers: Reopen in Container")
- Wait for the container to build and initialize
- The development environment will be set up in the `sim/` directory
3. **Start Developing:**
- Run `bun run dev:full` in the terminal or use the `sim-start` alias
- This starts both the main application and the realtime socket server
- All dependencies and configurations are automatically set up
- Use the provided aliases (like `sim-start`) to run common commands
- Your changes will be automatically hot-reloaded
4. **GitHub Codespaces:**
- This setup also works with GitHub Codespaces if you prefer development in the browser
- Just click "Code" → "Codespaces" → "Create codespace on staging"
- Just click "Code" → "Codespaces" → "Create codespace on main"
### Option 4: Manual Setup
### Option 3: Manual Setup
If you prefer not to use Docker or Dev Containers:
1. **Clone the Repository:**
```bash
git clone https://github.com/<your-username>/sim.git
cd sim
bun install
cd sim/sim
```
2. **Install Dependencies:**
2. **Set Up Environment:**
- Navigate to the app directory:
- Using NPM:
```bash
cd apps/sim
npm install
```
3. **Set Up Environment:**
- Copy `.env.example` to `.env`
- Configure required variables (DATABASE_URL, BETTER_AUTH_SECRET, BETTER_AUTH_URL)
- Configure database connection and other required authentication variables
3. **Set Up Database:**
4. **Set Up Database:**
```bash
bunx drizzle-kit push
```
- You need a PostgreSQL instance running
- Run migrations:
```bash
npm run db:push
```
4. **Run the Development Server:**
5. **Run the Development Server:**
```bash
bun run dev:full
```
- With NPM:
```bash
npm run dev
```
This command starts both the main application and the realtime socket server required for full functionality.
5. **Make Your Changes and Test Locally.**
6. **Make Your Changes and Test Locally.**
### Email Template Development
@@ -263,7 +248,7 @@ When working on email templates, you can preview them using a local email previe
1. **Run the Email Preview Server:**
```bash
bun run email:dev
npm run email:dev
```
2. **Access the Preview:**
@@ -280,33 +265,33 @@ When working on email templates, you can preview them using a local email previe
## Adding New Blocks and Tools
Sim is built in a modular fashion where blocks and tools extend the platform's functionality. To maintain consistency and quality, please follow the guidelines below when adding a new block or tool.
Sim Studio is built in a modular fashion where blocks and tools extend the platform's functionality. To maintain consistency and quality, please follow the guidelines below when adding a new block or tool.
### Where to Add Your Code
- **Blocks:** Create your new block file under the `/apps/sim/blocks/blocks` directory. The name of the file should match the provider name (e.g., `pinecone.ts`).
- **Tools:** Create a new directory under `/apps/sim/tools` with the same name as the provider (e.g., `/apps/sim/tools/pinecone`).
- **Blocks:** Create your new block file under the `/sim/blocks/blocks` directory. The name of the file should match the provider name (e.g., `pinecone.ts`).
- **Tools:** Create a new directory under `/sim/tools` with the same name as the provider (e.g., `/sim/tools/pinecone`).
In addition, you will need to update the registries:
- **Block Registry:** Update the blocks index (`/apps/sim/blocks/index.ts`) to include your new block.
- **Tool Registry:** Update the tools registry (`/apps/sim/tools/index.ts`) to add your new tool.
- **Block Registry:** Update the blocks index (`/sim/blocks/index.ts`) to include your new block.
- **Tool Registry:** Update the tools registry (`/sim/tools/index.ts`) to add your new tool.
### How to Create a New Block
1. **Create a New File:**
Create a file for your block named after the provider (e.g., `pinecone.ts`) in the `/apps/sim/blocks/blocks` directory.
Create a file for your block named after the provider (e.g., `pinecone.ts`) in the `/sim/blocks/blocks` directory.
2. **Create a New Icon:**
Create a new icon for your block in the `/apps/sim/components/icons.tsx` file. The icon should follow the same naming convention as the block (e.g., `PineconeIcon`).
Create a new icon for your block in the `/sim/components/icons.tsx` file. The icon should follow the same naming convention as the block (e.g., `PineconeIcon`).
3. **Define the Block Configuration:**
Your block should export a constant of type `BlockConfig`. For example:
```typescript:/apps/sim/blocks/blocks/pinecone.ts
```typescript:/sim/blocks/blocks/pinecone.ts
import { PineconeIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import type { PineconeResponse } from '@/tools/pinecone/types'
import { PineconeResponse } from '@/tools/pinecone/types'
import { BlockConfig } from '../types'
export const PineconeBlock: BlockConfig<PineconeResponse> = {
type: 'pinecone',
@@ -317,65 +302,22 @@ In addition, you will need to update the registries:
bgColor: '#123456',
icon: PineconeIcon,
// If this block requires OAuth authentication
provider: 'pinecone',
// Define subBlocks for the UI configuration
subBlocks: [
{
id: 'operation',
title: 'Operation',
type: 'dropdown'
required: true,
options: [
{ label: 'Generate Embeddings', id: 'generate' },
{ label: 'Search Text', id: 'search_text' },
],
value: () => 'generate',
},
{
id: 'apiKey',
title: 'API Key',
type: 'short-input'
placeholder: 'Your Pinecone API key',
password: true,
required: true,
},
// Block configuration options
],
tools: {
access: ['pinecone_generate_embeddings', 'pinecone_search_text'],
config: {
tool: (params: Record<string, any>) => {
switch (params.operation) {
case 'generate':
return 'pinecone_generate_embeddings'
case 'search_text':
return 'pinecone_search_text'
default:
throw new Error('Invalid operation selected')
}
},
},
},
inputs: {
operation: { type: 'string', description: 'Operation to perform' },
apiKey: { type: 'string', description: 'Pinecone API key' },
searchQuery: { type: 'string', description: 'Search query text' },
topK: { type: 'string', description: 'Number of results to return' },
},
outputs: {
matches: { type: 'any', description: 'Search results or generated embeddings' },
data: { type: 'any', description: 'Response data from Pinecone' },
usage: { type: 'any', description: 'API usage statistics' },
},
}
```
4. **Register Your Block:**
Add your block to the blocks registry (`/apps/sim/blocks/registry.ts`):
```typescript:/apps/sim/blocks/registry.ts
import { PineconeBlock } from '@/blocks/blocks/pinecone'
Add your block to the blocks registry (`/sim/blocks/registry.ts`):
```typescript:/sim/blocks/registry.ts
import { PineconeBlock } from './blocks/pinecone'
// Registry of all available blocks
export const registry: Record<string, BlockConfig> = {
// ... existing blocks
@@ -391,7 +333,7 @@ In addition, you will need to update the registries:
### How to Create a New Tool
1. **Create a New Directory:**
Create a directory under `/apps/sim/tools` with the same name as the provider (e.g., `/apps/sim/tools/pinecone`).
Create a directory under `/sim/tools` with the same name as the provider (e.g., `/sim/tools/pinecone`).
2. **Create Tool Files:**
Create separate files for each tool functionality with descriptive names (e.g., `fetch.ts`, `generate_embeddings.ts`, `search_text.ts`) in your tool directory.
@@ -402,7 +344,7 @@ In addition, you will need to update the registries:
4. **Create an Index File:**
Create an `index.ts` file in your tool directory that imports and exports all tools:
```typescript:/apps/sim/tools/pinecone/index.ts
```typescript:/sim/tools/pinecone/index.ts
import { fetchTool } from './fetch'
import { generateEmbeddingsTool } from './generate_embeddings'
import { searchTextTool } from './search_text'
@@ -413,9 +355,9 @@ In addition, you will need to update the registries:
5. **Define the Tool Configuration:**
Your tool should export a constant with a naming convention of `{toolName}Tool`. The tool ID should follow the format `{provider}_{tool_name}`. For example:
```typescript:/apps/sim/tools/pinecone/fetch.ts
import { ToolConfig, ToolResponse } from '@/tools/types'
import { PineconeParams, PineconeResponse } from '@/tools/pinecone/types'
```typescript:/sim/tools/pinecone/fetch.ts
import { ToolConfig, ToolResponse } from '../types'
import { PineconeParams, PineconeResponse } from './types'
export const fetchTool: ToolConfig<PineconeParams, PineconeResponse> = {
id: 'pinecone_fetch', // Follow the {provider}_{tool_name} format
@@ -427,18 +369,7 @@ In addition, you will need to update the registries:
provider: 'pinecone', // ID of the OAuth provider
params: {
parameterName: {
type: 'string',
required: true,
visibility: 'user-or-llm', // Controls parameter visibility
description: 'Description of the parameter',
},
optionalParam: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Optional parameter only user can set',
},
// Tool parameters
},
request: {
// Request configuration
@@ -446,14 +377,17 @@ In addition, you will need to update the registries:
transformResponse: async (response: Response) => {
// Transform response
},
transformError: (error) => {
// Handle errors
},
}
```
6. **Register Your Tool:**
Update the tools registry in `/apps/sim/tools/index.ts` to include your new tool:
Update the tools registry in `/sim/tools/index.ts` to include your new tool:
```typescript:/apps/sim/tools/index.ts
import { fetchTool, generateEmbeddingsTool, searchTextTool } from '/@tools/pinecone'
```typescript:/sim/tools/index.ts
import { fetchTool, generateEmbeddingsTool, searchTextTool } from './pinecone'
// ... other imports
export const tools: Record<string, ToolConfig> = {
@@ -467,12 +401,6 @@ In addition, you will need to update the registries:
7. **Test Your Tool:**
Ensure that your tool functions correctly by making test requests and verifying the responses.
8. **Generate Documentation:**
Run the documentation generator to create docs for your new tool:
```bash
./scripts/generate-docs.sh
```
### Naming Conventions
Maintaining consistent naming across the codebase is critical for auto-generation of tools and documentation. Follow these naming guidelines:
@@ -485,57 +413,11 @@ Maintaining consistent naming across the codebase is critical for auto-generatio
- **Tool Exports:** Should be named `{toolName}Tool` (e.g., `fetchTool`)
- **Tool IDs:** Should follow the format `{provider}_{tool_name}` (e.g., `pinecone_fetch`)
### Parameter Visibility System
Sim implements a sophisticated parameter visibility system that controls how parameters are exposed to users and LLMs in agent workflows. Each parameter can have one of four visibility levels:
| Visibility | User Sees | LLM Sees | How It Gets Set |
|-------------|-----------|----------|--------------------------------|
| `user-only` | ✅ Yes | ❌ No | User provides in UI |
| `user-or-llm` | ✅ Yes | ✅ Yes | User provides OR LLM generates |
| `llm-only` | ❌ No | ✅ Yes | LLM generates only |
| `hidden` | ❌ No | ❌ No | Application injects at runtime |
#### Visibility Guidelines
- **`user-or-llm`**: Use for core parameters that can be provided by users or intelligently filled by the LLM (e.g., search queries, email subjects)
- **`user-only`**: Use for configuration parameters, API keys, and settings that only users should control (e.g., number of results, authentication credentials)
- **`llm-only`**: Use for computed values that the LLM should handle internally (e.g., dynamic calculations, contextual data)
- **`hidden`**: Use for system-level parameters injected at runtime (e.g., OAuth tokens, internal identifiers)
#### Example Implementation
```typescript
params: {
query: {
type: 'string',
required: true,
visibility: 'user-or-llm', // User can provide or LLM can generate
description: 'Search query to execute',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only', // Only user provides this
description: 'API key for authentication',
},
internalId: {
type: 'string',
required: false,
visibility: 'hidden', // System provides this at runtime
description: 'Internal tracking identifier',
},
}
```
This visibility system ensures clean user interfaces while maintaining full flexibility for LLM-driven workflows.
### Guidelines & Best Practices
- **Code Style:** Follow the project's Biome configurations. Use meaningful variable names and small, focused functions.
- **Code Style:** Follow the project's ESLint and Prettier configurations. Use meaningful variable names and small, focused functions.
- **Documentation:** Clearly document the purpose, inputs, outputs, and any special behavior for your block/tool.
- **Error Handling:** Implement robust error handling and provide user-friendly error messages.
- **Parameter Visibility:** Always specify the appropriate visibility level for each parameter to ensure proper UI behavior and LLM integration.
- **Testing:** Add unit or integration tests to verify your changes when possible.
- **Commit Changes:** Update all related components and registries, and describe your changes in your pull request.
@@ -553,7 +435,7 @@ This project is licensed under the Apache License 2.0. By contributing, you agre
By contributing to this repository, you agree that your contributions are provided under the terms of the Apache License Version 2.0, as included in the LICENSE file of this repository.
In addition, by submitting your contributions, you grant Sim, Inc. ("The Licensor") a perpetual, irrevocable, worldwide, royalty-free, sublicensable right and license to:
In addition, by submitting your contributions, you grant Sim Studio, Inc. ("The Licensor") a perpetual, irrevocable, worldwide, royalty-free, sublicensable right and license to:
- Use, copy, modify, distribute, publicly display, publicly perform, and prepare derivative works of your contributions.
- Incorporate your contributions into other works or products.
@@ -565,4 +447,4 @@ If you do not agree with these terms, you must not contribute your work to this
---
Thank you for taking the time to contribute to Sim. We truly appreciate your efforts and look forward to collaborating with you!
Thank you for taking the time to contribute to Sim Studio. We truly appreciate your efforts and look forward to collaborating with you!

View File

@@ -1,7 +1,7 @@
---
name: Bug report
about: Create a report to help us improve
title: '[BUG]'
title: "[BUG]"
labels: bug
assignees: ''
---
@@ -11,7 +11,6 @@ A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
@@ -24,4 +23,4 @@ A clear and concise description of what you expected to happen.
If applicable, add screenshots to help explain your problem.
**Additional context**
Add any other context about the problem here.
Add any other context about the problem here.

View File

@@ -1,7 +1,7 @@
---
name: Feature request
about: Suggest an idea for this project
title: '[REQUEST]'
title: "[REQUEST]"
labels: feature
assignees: ''
---
@@ -16,4 +16,4 @@ A clear and concise description of what you want to happen.
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
Add any other context or screenshots about the feature request here.

View File

@@ -1,25 +1,42 @@
## Summary
Brief description of what this PR does and why.
## Description
Fixes #(issue)
Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context.
## Type of Change
- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation
- [ ] Other: ___________
Fixes # (issue)
## Testing
How has this been tested? What should reviewers focus on?
## Type of change
## Checklist
- [ ] Code follows project style guidelines
- [ ] Self-reviewed my changes
- [ ] Tests added/updated and passing
- [ ] No new warnings introduced
Please delete options that are not relevant.
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Documentation update
- [ ] Security enhancement
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## How Has This Been Tested?
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration.
## Checklist:
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] All tests pass locally and in CI (`npm test`)
- [ ] My changes generate no new warnings
- [ ] Any dependent changes have been merged and published in downstream modules
- [ ] I have updated version numbers as needed (if needed)
- [ ] I confirm that I have read and agree to the terms outlined in the [Contributor License Agreement (CLA)](./CONTRIBUTING.md#contributor-license-agreement-cla)
## Screenshots/Videos
<!-- If applicable, add screenshots or videos to help explain your changes -->
<!-- For UI changes, before/after screenshots are especially helpful -->
## Security Considerations:
- [ ] My changes do not introduce any new security vulnerabilities
- [ ] I have considered the security implications of my changes
## Additional Information:
Any additional information, configuration or data that might be necessary to reproduce the issue or use the feature.

8
.github/SECURITY.md vendored
View File

@@ -6,16 +6,16 @@
| ------- | ------------------ |
| 0.1.x | :white_check_mark: |
## Reporting a Vulnerability
We take the security of Sim seriously. If you believe you've found a security vulnerability, please follow these steps:
We take the security of Sim Studio seriously. If you believe you've found a security vulnerability, please follow these steps:
1. **Do not disclose the vulnerability publicly** or to any third parties.
2. **Email us directly** at security@sim.ai with details of the vulnerability.
2. **Email us directly** at security@simstudio.ai with details of the vulnerability.
3. **Include the following information** in your report:
- Description of the vulnerability
- Steps to reproduce
- Potential impact
@@ -23,4 +23,4 @@ We take the security of Sim seriously. If you believe you've found a security vu
4. We will acknowledge receipt of your vulnerability report within 48 hours and provide an estimated timeline for a fix.
5. Once the vulnerability is fixed, we will notify you and publicly acknowledge your contribution (unless you prefer to remain anonymous).
5. Once the vulnerability is fixed, we will notify you and publicly acknowledge your contribution (unless you prefer to remain anonymous).

98
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,98 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/sim"
schedule:
interval: "weekly"
day: "monday"
time: "09:00"
# Disable version updates
open-pull-requests-limit: 0
labels:
- "dependencies"
- "security"
commit-message:
prefix: "fix(deps)"
prefix-development: "chore(deps)"
include: "scope"
groups:
dependencies:
applies-to: security-updates
patterns:
- "*"
# Documentation site dependencies (/docs)
- package-ecosystem: "npm"
directory: "/docs"
schedule:
interval: "weekly"
day: "wednesday"
# Disable version updates
open-pull-requests-limit: 0
labels:
- "dependencies"
- "security"
commit-message:
prefix: "docs(deps)"
include: "scope"
groups:
docs-dependencies:
applies-to: security-updates
patterns:
- "*"
# Root-level dependencies (if any)
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
day: "friday"
# Disable version updates
open-pull-requests-limit: 0
labels:
- "dependencies"
- "security"
commit-message:
prefix: "chore(deps)"
include: "scope"
groups:
root-dependencies:
applies-to: security-updates
patterns:
- "*"
# GitHub Actions workflows
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
# Disable version updates
open-pull-requests-limit: 0
labels:
- "dependencies"
- "security"
commit-message:
prefix: "ci(deps)"
groups:
actions:
applies-to: security-updates
patterns:
- "*"
# Docker containers (if applicable)
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "monthly"
# Disable version updates
open-pull-requests-limit: 0
labels:
- "dependencies"
- "security"
commit-message:
prefix: "docker(deps)"
groups:
docker:
applies-to: security-updates
patterns:
- "*"

View File

@@ -2,278 +2,82 @@ name: CI
on:
push:
branches: [main, staging]
branches: [main]
pull_request:
branches: [main, staging]
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: false
branches: [main]
jobs:
test-build:
test:
name: Test and Build
uses: ./.github/workflows/test-build.yml
secrets: inherit
# Detect if this is a version release commit (e.g., "v0.5.24: ...")
detect-version:
name: Detect Version
runs-on: blacksmith-4vcpu-ubuntu-2404
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging')
outputs:
version: ${{ steps.extract.outputs.version }}
is_release: ${{ steps.extract.outputs.is_release }}
steps:
- name: Extract version from commit message
id: extract
run: |
COMMIT_MSG="${{ github.event.head_commit.message }}"
# Only tag versions on main branch
if [ "${{ github.ref }}" = "refs/heads/main" ] && [[ "$COMMIT_MSG" =~ ^(v[0-9]+\.[0-9]+\.[0-9]+): ]]; then
VERSION="${BASH_REMATCH[1]}"
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "is_release=true" >> $GITHUB_OUTPUT
echo "✅ Detected release commit: ${VERSION}"
else
echo "version=" >> $GITHUB_OUTPUT
echo "is_release=false" >> $GITHUB_OUTPUT
echo " Not a release commit"
fi
# Build AMD64 images and push to ECR immediately (+ GHCR for main)
build-amd64:
name: Build AMD64
needs: [test-build, detect-version]
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging')
runs-on: blacksmith-8vcpu-ubuntu-2404
permissions:
contents: read
packages: write
id-token: write
strategy:
fail-fast: false
matrix:
include:
- dockerfile: ./docker/app.Dockerfile
ghcr_image: ghcr.io/simstudioai/simstudio
ecr_repo_secret: ECR_APP
- dockerfile: ./docker/db.Dockerfile
ghcr_image: ghcr.io/simstudioai/migrations
ecr_repo_secret: ECR_MIGRATIONS
- dockerfile: ./docker/realtime.Dockerfile
ghcr_image: ghcr.io/simstudioai/realtime
ecr_repo_secret: ECR_REALTIME
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
role-to-assume: ${{ github.ref == 'refs/heads/main' && secrets.AWS_ROLE_TO_ASSUME || secrets.STAGING_AWS_ROLE_TO_ASSUME }}
aws-region: ${{ github.ref == 'refs/heads/main' && secrets.AWS_REGION || secrets.STAGING_AWS_REGION }}
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
if: github.ref == 'refs/heads/main'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: useblacksmith/setup-docker-builder@v1
- name: Generate tags
id: meta
node-version: '20'
cache: 'npm'
cache-dependency-path: './sim/package-lock.json'
- name: Install dependencies
working-directory: ./sim
run: npm ci
- name: Fix Rollup module issue
working-directory: ./sim
run: |
ECR_REGISTRY="${{ steps.login-ecr.outputs.registry }}"
ECR_REPO="${{ secrets[matrix.ecr_repo_secret] }}"
GHCR_IMAGE="${{ matrix.ghcr_image }}"
# ECR tags (always build for ECR)
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
ECR_TAG="latest"
else
ECR_TAG="staging"
fi
ECR_IMAGE="${ECR_REGISTRY}/${ECR_REPO}:${ECR_TAG}"
# Build tags list
TAGS="${ECR_IMAGE}"
# Add GHCR tags only for main branch
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
GHCR_AMD64="${GHCR_IMAGE}:latest-amd64"
GHCR_SHA="${GHCR_IMAGE}:${{ github.sha }}-amd64"
TAGS="${TAGS},$GHCR_AMD64,$GHCR_SHA"
# Add version tag if this is a release commit
if [ "${{ needs.detect-version.outputs.is_release }}" = "true" ]; then
VERSION="${{ needs.detect-version.outputs.version }}"
GHCR_VERSION="${GHCR_IMAGE}:${VERSION}-amd64"
TAGS="${TAGS},$GHCR_VERSION"
echo "📦 Adding version tag: ${VERSION}-amd64"
fi
fi
echo "tags=${TAGS}" >> $GITHUB_OUTPUT
- name: Build and push images
uses: useblacksmith/build-push-action@v2
rm -rf node_modules package-lock.json
npm install
- name: Run tests with coverage
working-directory: ./sim
env:
NODE_OPTIONS: "--no-warnings"
run: npm run test:coverage
- name: Build application
working-directory: ./sim
env:
NODE_OPTIONS: "--no-warnings"
NEXT_PUBLIC_APP_URL: "https://www.simstudio.ai"
STRIPE_SECRET_KEY: "dummy_key_for_ci_only"
STRIPE_WEBHOOK_SECRET: "dummy_secret_for_ci_only"
RESEND_API_KEY: "dummy_key_for_ci_only"
AWS_REGION: "us-west-2"
run: npm run build
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
context: .
file: ${{ matrix.dockerfile }}
platforms: linux/amd64
push: true
tags: ${{ steps.meta.outputs.tags }}
provenance: false
sbom: false
directory: ./sim/coverage
fail_ci_if_error: false
verbose: true
# Build ARM64 images for GHCR (main branch only, runs in parallel)
build-ghcr-arm64:
name: Build ARM64 (GHCR Only)
needs: [test-build, detect-version]
runs-on: blacksmith-8vcpu-ubuntu-2404-arm
migrations:
name: Apply Database Migrations
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
include:
- dockerfile: ./docker/app.Dockerfile
image: ghcr.io/simstudioai/simstudio
- dockerfile: ./docker/db.Dockerfile
image: ghcr.io/simstudioai/migrations
- dockerfile: ./docker/realtime.Dockerfile
image: ghcr.io/simstudioai/realtime
needs: test
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Login to GHCR
uses: docker/login-action@v3
- name: Setup Node.js
uses: actions/setup-node@v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: useblacksmith/setup-docker-builder@v1
- name: Generate ARM64 tags
id: meta
run: |
IMAGE="${{ matrix.image }}"
TAGS="${IMAGE}:latest-arm64,${IMAGE}:${{ github.sha }}-arm64"
# Add version tag if this is a release commit
if [ "${{ needs.detect-version.outputs.is_release }}" = "true" ]; then
VERSION="${{ needs.detect-version.outputs.version }}"
TAGS="${TAGS},${IMAGE}:${VERSION}-arm64"
echo "📦 Adding version tag: ${VERSION}-arm64"
fi
echo "tags=${TAGS}" >> $GITHUB_OUTPUT
- name: Build and push ARM64 to GHCR
uses: useblacksmith/build-push-action@v2
with:
context: .
file: ${{ matrix.dockerfile }}
platforms: linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
provenance: false
sbom: false
# Create GHCR multi-arch manifests (only for main, after both builds)
create-ghcr-manifests:
name: Create GHCR Manifests
runs-on: blacksmith-2vcpu-ubuntu-2404
needs: [build-amd64, build-ghcr-arm64, detect-version]
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
permissions:
packages: write
strategy:
matrix:
include:
- image: ghcr.io/simstudioai/simstudio
- image: ghcr.io/simstudioai/migrations
- image: ghcr.io/simstudioai/realtime
steps:
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create and push manifests
run: |
IMAGE_BASE="${{ matrix.image }}"
# Create latest manifest
docker manifest create "${IMAGE_BASE}:latest" \
"${IMAGE_BASE}:latest-amd64" \
"${IMAGE_BASE}:latest-arm64"
docker manifest push "${IMAGE_BASE}:latest"
# Create SHA manifest
docker manifest create "${IMAGE_BASE}:${{ github.sha }}" \
"${IMAGE_BASE}:${{ github.sha }}-amd64" \
"${IMAGE_BASE}:${{ github.sha }}-arm64"
docker manifest push "${IMAGE_BASE}:${{ github.sha }}"
# Create version manifest if this is a release commit
if [ "${{ needs.detect-version.outputs.is_release }}" = "true" ]; then
VERSION="${{ needs.detect-version.outputs.version }}"
echo "📦 Creating version manifest: ${VERSION}"
docker manifest create "${IMAGE_BASE}:${VERSION}" \
"${IMAGE_BASE}:${VERSION}-amd64" \
"${IMAGE_BASE}:${VERSION}-arm64"
docker manifest push "${IMAGE_BASE}:${VERSION}"
fi
# Check if docs changed
check-docs-changes:
name: Check Docs Changes
runs-on: blacksmith-4vcpu-ubuntu-2404
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
outputs:
docs_changed: ${{ steps.filter.outputs.docs }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2 # Need at least 2 commits to detect changes
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
docs:
- 'apps/docs/content/docs/en/**'
- 'apps/sim/scripts/process-docs.ts'
- 'apps/sim/lib/chunkers/**'
# Process docs embeddings (only when docs change, after ECR images are pushed)
process-docs:
name: Process Docs
needs: [build-amd64, check-docs-changes]
if: needs.check-docs-changes.outputs.docs_changed == 'true'
uses: ./.github/workflows/docs-embeddings.yml
secrets: inherit
node-version: '20'
cache: 'npm'
cache-dependency-path: './sim/package-lock.json'
- name: Install dependencies
working-directory: ./sim
run: npm ci
- name: Apply migrations
working-directory: ./sim
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
run: npx drizzle-kit push

58
.github/workflows/docker-publish.yml vendored Normal file
View File

@@ -0,0 +1,58 @@
name: Docker Build and Publish
on:
push:
branches: [ main ]
tags: [ 'v*' ]
pull_request:
branches: [ main ]
env:
REGISTRY: ghcr.io
IMAGE_NAME: simstudioai/sim
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,format=short
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver: docker
buildkitd-flags: --debug
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -1,46 +0,0 @@
name: Process Docs Embeddings
on:
workflow_call:
workflow_dispatch: # Allow manual triggering
jobs:
process-docs-embeddings:
name: Process Documentation Embeddings
runs-on: blacksmith-8vcpu-ubuntu-2404
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.3
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: latest
- name: Cache Bun dependencies
uses: actions/cache@v4
with:
path: |
~/.bun/install/cache
node_modules
**/node_modules
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
restore-keys: |
${{ runner.os }}-bun-
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Process docs embeddings
working-directory: ./apps/sim
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: bun run scripts/process-docs.ts --clear

View File

@@ -1,180 +0,0 @@
name: 'Auto-translate Documentation'
on:
push:
branches: [ staging ]
paths:
- 'apps/docs/content/docs/en/**'
- 'apps/docs/i18n.json'
permissions:
contents: write
pull-requests: write
jobs:
translate:
runs-on: blacksmith-4vcpu-ubuntu-2404
if: github.actor != 'github-actions[bot]' # Prevent infinite loops
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ secrets.GH_PAT }}
fetch-depth: 0
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.3
- name: Cache Bun dependencies
uses: actions/cache@v4
with:
path: |
~/.bun/install/cache
node_modules
**/node_modules
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
restore-keys: |
${{ runner.os }}-bun-
- name: Run Lingo.dev translations
env:
LINGODOTDEV_API_KEY: ${{ secrets.LINGODOTDEV_API_KEY }}
run: |
cd apps/docs
bunx lingo.dev@latest i18n
- name: Check for translation changes
id: changes
run: |
cd apps/docs
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
if [ -n "$(git status --porcelain content/docs)" ]; then
echo "changes=true" >> $GITHUB_OUTPUT
else
echo "changes=false" >> $GITHUB_OUTPUT
fi
- name: Create Pull Request with translations
if: steps.changes.outputs.changes == 'true'
uses: peter-evans/create-pull-request@v5
with:
token: ${{ secrets.GH_PAT }}
commit-message: "feat(i18n): update translations"
title: "feat(i18n): update translations"
body: |
## Summary
Automated translation updates triggered by changes to documentation.
This PR was automatically created after content changes were made, updating translations for all supported languages using Lingo.dev AI translation engine.
**Original trigger**: ${{ github.event.head_commit.message }}
**Commit**: ${{ github.sha }}
**Workflow**: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
## Type of Change
- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change
- [x] Documentation
- [ ] Other: ___________
## Testing
This PR includes automated translations for modified English documentation content:
- 🇪🇸 Spanish (es) translations
- 🇫🇷 French (fr) translations
- 🇨🇳 Chinese (zh) translations
- 🇯🇵 Japanese (ja) translations
- 🇩🇪 German (de) translations
**What reviewers should focus on:**
- Verify translated content accuracy and context
- Check that all links and references work correctly in translated versions
- Ensure formatting, code blocks, and structure are preserved
- Validate that technical terms are appropriately translated
## Checklist
- [x] Code follows project style guidelines (automated translation)
- [x] Self-reviewed my changes (automated process)
- [ ] Tests added/updated and passing
- [x] No new warnings introduced
- [x] I confirm that I have read and agree to the terms outlined in the [Contributor License Agreement (CLA)](./CONTRIBUTING.md#contributor-license-agreement-cla)
## Screenshots/Videos
<!-- Translation changes are text-based - no visual changes expected -->
<!-- Reviewers should check the documentation site renders correctly for all languages -->
branch: auto-translate/staging-merge-${{ github.run_id }}
base: staging
labels: |
i18n
verify-translations:
needs: translate
runs-on: blacksmith-4vcpu-ubuntu-2404
if: always() # Run even if translation fails
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: staging
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.3
- name: Cache Bun dependencies
uses: actions/cache@v4
with:
path: |
~/.bun/install/cache
node_modules
**/node_modules
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
restore-keys: |
${{ runner.os }}-bun-
- name: Install dependencies
run: |
cd apps/docs
bun install --frozen-lockfile
- name: Build documentation to verify translations
run: |
cd apps/docs
bun run build
- name: Report translation status
run: |
cd apps/docs
echo "## Translation Status Report" >> $GITHUB_STEP_SUMMARY
echo "**Triggered by merge to staging branch**" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
en_count=$(find content/docs/en -name "*.mdx" | wc -l)
es_count=$(find content/docs/es -name "*.mdx" 2>/dev/null | wc -l || echo 0)
fr_count=$(find content/docs/fr -name "*.mdx" 2>/dev/null | wc -l || echo 0)
zh_count=$(find content/docs/zh -name "*.mdx" 2>/dev/null | wc -l || echo 0)
ja_count=$(find content/docs/ja -name "*.mdx" 2>/dev/null | wc -l || echo 0)
de_count=$(find content/docs/de -name "*.mdx" 2>/dev/null | wc -l || echo 0)
es_percentage=$((es_count * 100 / en_count))
fr_percentage=$((fr_count * 100 / en_count))
zh_percentage=$((zh_count * 100 / en_count))
ja_percentage=$((ja_count * 100 / en_count))
de_percentage=$((de_count * 100 / en_count))
echo "### Coverage Statistics" >> $GITHUB_STEP_SUMMARY
echo "- **🇬🇧 English**: $en_count files (source)" >> $GITHUB_STEP_SUMMARY
echo "- **🇪🇸 Spanish**: $es_count/$en_count files ($es_percentage%)" >> $GITHUB_STEP_SUMMARY
echo "- **🇫🇷 French**: $fr_count/$en_count files ($fr_percentage%)" >> $GITHUB_STEP_SUMMARY
echo "- **🇨🇳 Chinese**: $zh_count/$en_count files ($zh_percentage%)" >> $GITHUB_STEP_SUMMARY
echo "- **🇯🇵 Japanese**: $ja_count/$en_count files ($ja_percentage%)" >> $GITHUB_STEP_SUMMARY
echo "- **🇩🇪 German**: $de_count/$en_count files ($de_percentage%)" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "🔄 **Auto-translation PR**: Check for new pull request with updated translations" >> $GITHUB_STEP_SUMMARY

View File

@@ -1,181 +0,0 @@
name: Build and Push Images
on:
workflow_call:
workflow_dispatch:
permissions:
contents: read
packages: write
id-token: write
jobs:
build-amd64:
name: Build AMD64
runs-on: blacksmith-8vcpu-ubuntu-2404
strategy:
fail-fast: false
matrix:
include:
- dockerfile: ./docker/app.Dockerfile
ghcr_image: ghcr.io/simstudioai/simstudio
ecr_repo_secret: ECR_APP
- dockerfile: ./docker/db.Dockerfile
ghcr_image: ghcr.io/simstudioai/migrations
ecr_repo_secret: ECR_MIGRATIONS
- dockerfile: ./docker/realtime.Dockerfile
ghcr_image: ghcr.io/simstudioai/realtime
ecr_repo_secret: ECR_REALTIME
outputs:
registry: ${{ steps.login-ecr.outputs.registry }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ github.ref == 'refs/heads/main' && secrets.AWS_ROLE_TO_ASSUME || secrets.STAGING_AWS_ROLE_TO_ASSUME }}
aws-region: ${{ github.ref == 'refs/heads/main' && secrets.AWS_REGION || secrets.STAGING_AWS_REGION }}
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
if: github.ref == 'refs/heads/main'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: useblacksmith/setup-docker-builder@v1
- name: Generate tags
id: meta
run: |
ECR_REGISTRY="${{ steps.login-ecr.outputs.registry }}"
ECR_REPO="${{ secrets[matrix.ecr_repo_secret] }}"
GHCR_IMAGE="${{ matrix.ghcr_image }}"
# ECR tags (always build for ECR)
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
ECR_TAG="latest"
else
ECR_TAG="staging"
fi
ECR_IMAGE="${ECR_REGISTRY}/${ECR_REPO}:${ECR_TAG}"
# Build tags list
TAGS="${ECR_IMAGE}"
# Add GHCR tags only for main branch
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
GHCR_AMD64="${GHCR_IMAGE}:latest-amd64"
GHCR_SHA="${GHCR_IMAGE}:${{ github.sha }}-amd64"
TAGS="${TAGS},$GHCR_AMD64,$GHCR_SHA"
fi
echo "tags=${TAGS}" >> $GITHUB_OUTPUT
- name: Build and push images
uses: useblacksmith/build-push-action@v2
with:
context: .
file: ${{ matrix.dockerfile }}
platforms: linux/amd64
push: true
tags: ${{ steps.meta.outputs.tags }}
provenance: false
sbom: false
build-ghcr-arm64:
name: Build ARM64 (GHCR Only)
runs-on: blacksmith-8vcpu-ubuntu-2404-arm
if: github.ref == 'refs/heads/main'
strategy:
fail-fast: false
matrix:
include:
- dockerfile: ./docker/app.Dockerfile
image: ghcr.io/simstudioai/simstudio
- dockerfile: ./docker/db.Dockerfile
image: ghcr.io/simstudioai/migrations
- dockerfile: ./docker/realtime.Dockerfile
image: ghcr.io/simstudioai/realtime
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: useblacksmith/setup-docker-builder@v1
- name: Generate ARM64 tags
id: meta
run: |
IMAGE="${{ matrix.image }}"
echo "tags=${IMAGE}:latest-arm64,${IMAGE}:${{ github.sha }}-arm64" >> $GITHUB_OUTPUT
- name: Build and push ARM64 to GHCR
uses: useblacksmith/build-push-action@v2
with:
context: .
file: ${{ matrix.dockerfile }}
platforms: linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
provenance: false
sbom: false
create-ghcr-manifests:
name: Create GHCR Manifests
runs-on: blacksmith-8vcpu-ubuntu-2404
needs: [build-amd64, build-ghcr-arm64]
if: github.ref == 'refs/heads/main'
strategy:
matrix:
include:
- image: ghcr.io/simstudioai/simstudio
- image: ghcr.io/simstudioai/migrations
- image: ghcr.io/simstudioai/realtime
steps:
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create and push manifests
run: |
IMAGE_BASE="${{ matrix.image }}"
# Create latest manifest
docker manifest create "${IMAGE_BASE}:latest" \
"${IMAGE_BASE}:latest-amd64" \
"${IMAGE_BASE}:latest-arm64"
docker manifest push "${IMAGE_BASE}:latest"
# Create SHA manifest
docker manifest create "${IMAGE_BASE}:${{ github.sha }}" \
"${IMAGE_BASE}:${{ github.sha }}-amd64" \
"${IMAGE_BASE}:${{ github.sha }}-arm64"
docker manifest push "${IMAGE_BASE}:${{ github.sha }}"

View File

@@ -1,39 +0,0 @@
name: Database Migrations
on:
workflow_call:
workflow_dispatch:
jobs:
migrate:
name: Apply Database Migrations
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.3
- name: Cache Bun dependencies
uses: actions/cache@v4
with:
path: |
~/.bun/install/cache
node_modules
**/node_modules
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
restore-keys: |
${{ runner.os }}-bun-
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Apply migrations
working-directory: ./packages/db
env:
DATABASE_URL: ${{ github.ref == 'refs/heads/main' && secrets.DATABASE_URL || secrets.STAGING_DATABASE_URL }}
run: bunx drizzle-kit migrate --config=./drizzle.config.ts

View File

@@ -1,69 +1,93 @@
name: Publish CLI Package
name: Publish Sim Studio CLI
on:
push:
branches: [main]
branches:
- main
paths:
- 'packages/cli/**'
- 'packages/simstudio/**'
- '.github/workflows/publish-cli.yml'
jobs:
publish-npm:
runs-on: blacksmith-4vcpu-ubuntu-2404
publish:
runs-on: ubuntu-latest
permissions:
contents: write
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.3
fetch-depth: 0
- name: Setup Node.js for npm publishing
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
registry-url: 'https://registry.npmjs.org/'
- name: Cache Bun dependencies
uses: actions/cache@v4
with:
path: |
~/.bun/install/cache
node_modules
**/node_modules
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
restore-keys: |
${{ runner.os }}-bun-
node-version: '20'
registry-url: 'https://registry.npmjs.org'
cache: 'npm'
cache-dependency-path: 'packages/simstudio/package-lock.json'
- name: Install dependencies
working-directory: packages/cli
run: bun install --frozen-lockfile
working-directory: packages/simstudio
run: npm ci
- name: Build package
working-directory: packages/cli
run: bun run build
working-directory: packages/simstudio
run: npm run build
- name: Get package version
id: package_version
working-directory: packages/cli
- name: Get version
working-directory: packages/simstudio
id: get_version
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
- name: Check if version already exists
id: version_check
- name: Check version update
working-directory: packages/simstudio
run: |
if npm view simstudio@${{ steps.package_version.outputs.version }} version &> /dev/null; then
echo "exists=true" >> $GITHUB_OUTPUT
if ! git diff ${{ github.event.before }} ${{ github.sha }} packages/simstudio/package.json | grep -q '"version":'; then
echo "::error::Version not updated in package.json"
exit 1
fi
- name: Run tests
working-directory: packages/simstudio
run: npm test
- name: Check for changes
working-directory: packages/simstudio
id: check_changes
run: |
if git diff --quiet ${{ github.event.before }} ${{ github.sha }} packages/simstudio/; then
echo "changes=false" >> $GITHUB_OUTPUT
else
echo "exists=false" >> $GITHUB_OUTPUT
echo "changes=true" >> $GITHUB_OUTPUT
fi
- name: Publish to npm
if: steps.version_check.outputs.exists == 'false'
working-directory: packages/cli
run: npm publish --access=public
if: steps.check_changes.outputs.changes == 'true'
working-directory: packages/simstudio
run: |
if ! npm publish; then
echo "::error::Failed to publish package"
exit 1
fi
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Log skipped publish
if: steps.version_check.outputs.exists == 'true'
run: echo "Skipped publishing because version ${{ steps.package_version.outputs.version }} already exists on npm"
- name: Create Git tag
if: steps.check_changes.outputs.changes == 'true'
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git tag -a "v${{ steps.get_version.outputs.version }}" -m "Release v${{ steps.get_version.outputs.version }}"
git push origin "v${{ steps.get_version.outputs.version }}"
- name: Create GitHub Release
if: steps.check_changes.outputs.changes == 'true'
uses: softprops/action-gh-release@v1
with:
name: "v${{ steps.get_version.outputs.version }}"
tag_name: "v${{ steps.get_version.outputs.version }}"
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,89 +0,0 @@
name: Publish Python SDK
on:
push:
branches: [main]
paths:
- 'packages/python-sdk/**'
jobs:
publish-pypi:
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'
- name: Install build dependencies
run: |
python -m pip install --upgrade pip
pip install build twine pytest requests tomli
- name: Run tests
working-directory: packages/python-sdk
run: |
PYTHONPATH=. pytest tests/ -v
- name: Get package version
id: package_version
working-directory: packages/python-sdk
run: echo "version=$(python -c "import tomli; print(tomli.load(open('pyproject.toml', 'rb'))['project']['version'])")" >> $GITHUB_OUTPUT
- name: Check if version already exists
id: version_check
run: |
if pip index versions simstudio-sdk | grep -q "${{ steps.package_version.outputs.version }}"; then
echo "exists=true" >> $GITHUB_OUTPUT
else
echo "exists=false" >> $GITHUB_OUTPUT
fi
- name: Build package
if: steps.version_check.outputs.exists == 'false'
working-directory: packages/python-sdk
run: python -m build
- name: Check package
if: steps.version_check.outputs.exists == 'false'
working-directory: packages/python-sdk
run: twine check dist/*
- name: Publish to PyPI
if: steps.version_check.outputs.exists == 'false'
working-directory: packages/python-sdk
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
run: twine upload dist/*
- name: Log skipped publish
if: steps.version_check.outputs.exists == 'true'
run: echo "Skipped publishing because version ${{ steps.package_version.outputs.version }} already exists on PyPI"
- name: Create GitHub Release
if: steps.version_check.outputs.exists == 'false'
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: python-sdk-v${{ steps.package_version.outputs.version }}
name: Python SDK v${{ steps.package_version.outputs.version }}
body: |
## Python SDK v${{ steps.package_version.outputs.version }}
Published simstudio-sdk==${{ steps.package_version.outputs.version }} to PyPI.
### Installation
```bash
pip install simstudio-sdk==${{ steps.package_version.outputs.version }}
```
### Documentation
See the [README](https://github.com/simstudioai/sim/tree/main/packages/python-sdk) or the [docs](https://docs.sim.ai/sdks/python) for more information.
draft: false
prerelease: false

View File

@@ -1,95 +0,0 @@
name: Publish TypeScript SDK
on:
push:
branches: [main]
paths:
- 'packages/ts-sdk/**'
jobs:
publish-npm:
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.3
- name: Setup Node.js for npm publishing
uses: actions/setup-node@v4
with:
node-version: '22'
registry-url: 'https://registry.npmjs.org/'
- name: Cache Bun dependencies
uses: actions/cache@v4
with:
path: |
~/.bun/install/cache
node_modules
**/node_modules
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
restore-keys: |
${{ runner.os }}-bun-
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Run tests
working-directory: packages/ts-sdk
run: bun run test
- name: Build package
working-directory: packages/ts-sdk
run: bun run build
- name: Get package version
id: package_version
working-directory: packages/ts-sdk
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
- name: Check if version already exists
id: version_check
run: |
if npm view simstudio-ts-sdk@${{ steps.package_version.outputs.version }} version &> /dev/null; then
echo "exists=true" >> $GITHUB_OUTPUT
else
echo "exists=false" >> $GITHUB_OUTPUT
fi
- name: Publish to npm
if: steps.version_check.outputs.exists == 'false'
working-directory: packages/ts-sdk
run: npm publish --access=public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Log skipped publish
if: steps.version_check.outputs.exists == 'true'
run: echo "Skipped publishing because version ${{ steps.package_version.outputs.version }} already exists on npm"
- name: Create GitHub Release
if: steps.version_check.outputs.exists == 'false'
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: typescript-sdk-v${{ steps.package_version.outputs.version }}
name: TypeScript SDK v${{ steps.package_version.outputs.version }}
body: |
## TypeScript SDK v${{ steps.package_version.outputs.version }}
Published simstudio-ts-sdk@${{ steps.package_version.outputs.version }} to npm.
### Installation
```bash
npm install simstudio-ts-sdk@${{ steps.package_version.outputs.version }}
```
### Documentation
See the [README](https://github.com/simstudioai/sim/tree/main/packages/ts-sdk) or the [docs](https://docs.sim.ai/sdks/typescript) for more information.
draft: false
prerelease: false

View File

@@ -1,82 +0,0 @@
name: Test and Build
on:
workflow_call:
workflow_dispatch:
jobs:
test-build:
name: Test and Build
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.3
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: latest
- name: Mount Bun cache (Sticky Disk)
uses: useblacksmith/stickydisk@v1
with:
key: ${{ github.repository }}-bun-cache
path: ~/.bun/install/cache
- name: Mount node_modules (Sticky Disk)
uses: useblacksmith/stickydisk@v1
with:
key: ${{ github.repository }}-node-modules
path: ./node_modules
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Lint code
run: bun run lint:check
- name: Run tests with coverage
env:
NODE_OPTIONS: '--no-warnings'
NEXT_PUBLIC_APP_URL: 'https://www.sim.ai'
DATABASE_URL: 'postgresql://postgres:postgres@localhost:5432/simstudio'
ENCRYPTION_KEY: '7cf672e460e430c1fba707575c2b0e2ad5a99dddf9b7b7e3b5646e630861db1c' # dummy key for CI only
run: bun run test
- name: Check schema and migrations are in sync
working-directory: packages/db
run: |
bunx drizzle-kit generate --config=./drizzle.config.ts
if [ -n "$(git status --porcelain ./migrations)" ]; then
echo "❌ Schema and migrations are out of sync!"
echo "Run 'cd packages/db && bunx drizzle-kit generate' and commit the new migrations."
git status --porcelain ./migrations
git diff ./migrations
exit 1
fi
echo "✅ Schema and migrations are in sync"
- name: Build application
env:
NODE_OPTIONS: '--no-warnings'
NEXT_PUBLIC_APP_URL: 'https://www.sim.ai'
DATABASE_URL: 'postgresql://postgres:postgres@localhost:5432/simstudio'
STRIPE_SECRET_KEY: 'dummy_key_for_ci_only'
STRIPE_WEBHOOK_SECRET: 'dummy_secret_for_ci_only'
RESEND_API_KEY: 'dummy_key_for_ci_only'
AWS_REGION: 'us-west-2'
ENCRYPTION_KEY: '7cf672e460e430c1fba707575c2b0e2ad5a99dddf9b7b7e3b5646e630861db1c' # dummy key for CI only
run: bun run build
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
directory: ./apps/sim/coverage
fail_ci_if_error: false
verbose: true

55
.gitignore vendored
View File

@@ -1,23 +1,28 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/apps/**/node_modules
*/node_modules
docs/node_modules
/packages/**/node_modules
/scripts/node_modules
# bun specific
bun-debug.log*
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
/apps/**/coverage
# next.js
/.next/
/apps/**/out/
/apps/**/.next/
/apps/**/build
sim/.next/
sim/out/
sim/build
docs/.next/
docs/out/
docs/build
# production
/build
@@ -30,6 +35,12 @@ sim-standalone.tar.gz
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files
.env
*.env
@@ -46,30 +57,20 @@ sim-standalone.tar.gz
next-env.d.ts
# cursorrules
# .cursorrules
.cursorrules
# docs
/apps/docs/.source
/apps/docs/.contentlayer
/apps/docs/.content-collections
docs/.source
docs/.contentlayer
docs/.content-collections
# database instantiation
**/postgres_data/
# file uploads
uploads/
# collector configuration
collector-config.yaml
docker-compose.collector.yml
start-collector.sh
# Turborepo
.turbo
# VSCode
.vscode
# IntelliJ
.idea
## Helm Chart Tests
helm/sim/test
i18n.cache

View File

@@ -1 +1 @@
bunx lint-staged
cd sim && npx lint-staged

1
.npmrc
View File

@@ -1 +0,0 @@
ignore-scripts=true

295
CLAUDE.md
View File

@@ -1,295 +0,0 @@
# Sim Studio Development Guidelines
You are a professional software engineer. All code must follow best practices: accurate, readable, clean, and efficient.
## Global Standards
- **Logging**: Import `createLogger` from `@sim/logger`. Use `logger.info`, `logger.warn`, `logger.error` instead of `console.log`
- **Comments**: Use TSDoc for documentation. No `====` separators. No non-TSDoc comments
- **Styling**: Never update global styles. Keep all styling local to components
- **Package Manager**: Use `bun` and `bunx`, not `npm` and `npx`
## Architecture
### Core Principles
1. Single Responsibility: Each component, hook, store has one clear purpose
2. Composition Over Complexity: Break down complex logic into smaller pieces
3. Type Safety First: TypeScript interfaces for all props, state, return types
4. Predictable State: Zustand for global state, useState for UI-only concerns
### Root Structure
```
apps/sim/
├── app/ # Next.js app router (pages, API routes)
├── blocks/ # Block definitions and registry
├── components/ # Shared UI (emcn/, ui/)
├── executor/ # Workflow execution engine
├── hooks/ # Shared hooks (queries/, selectors/)
├── lib/ # App-wide utilities
├── providers/ # LLM provider integrations
├── stores/ # Zustand stores
├── tools/ # Tool definitions
└── triggers/ # Trigger definitions
```
### Naming Conventions
- Components: PascalCase (`WorkflowList`)
- Hooks: `use` prefix (`useWorkflowOperations`)
- Files: kebab-case (`workflow-list.tsx`)
- Stores: `stores/feature/store.ts`
- Constants: SCREAMING_SNAKE_CASE
- Interfaces: PascalCase with suffix (`WorkflowListProps`)
## Imports
**Always use absolute imports.** Never use relative imports.
```typescript
// ✓ Good
import { useWorkflowStore } from '@/stores/workflows/store'
// ✗ Bad
import { useWorkflowStore } from '../../../stores/workflows/store'
```
Use barrel exports (`index.ts`) when a folder has 3+ exports. Do not re-export from non-barrel files; import directly from the source.
### Import Order
1. React/core libraries
2. External libraries
3. UI components (`@/components/emcn`, `@/components/ui`)
4. Utilities (`@/lib/...`)
5. Stores (`@/stores/...`)
6. Feature imports
7. CSS imports
Use `import type { X }` for type-only imports.
## TypeScript
1. No `any` - Use proper types or `unknown` with type guards
2. Always define props interface for components
3. `as const` for constant objects/arrays
4. Explicit ref types: `useRef<HTMLDivElement>(null)`
## Components
```typescript
'use client' // Only if using hooks
const CONFIG = { SPACING: 8 } as const
interface ComponentProps {
requiredProp: string
optionalProp?: boolean
}
export function Component({ requiredProp, optionalProp = false }: ComponentProps) {
// Order: refs → external hooks → store hooks → custom hooks → state → useMemo → useCallback → useEffect → return
}
```
Extract when: 50+ lines, used in 2+ files, or has own state/logic. Keep inline when: < 10 lines, single use, purely presentational.
## Hooks
```typescript
interface UseFeatureProps { id: string }
export function useFeature({ id }: UseFeatureProps) {
const idRef = useRef(id)
const [data, setData] = useState<Data | null>(null)
useEffect(() => { idRef.current = id }, [id])
const fetchData = useCallback(async () => { ... }, []) // Empty deps when using refs
return { data, fetchData }
}
```
## Zustand Stores
Stores live in `stores/`. Complex stores split into `store.ts` + `types.ts`.
```typescript
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
const initialState = { items: [] as Item[] }
export const useFeatureStore = create<FeatureState>()(
devtools(
(set, get) => ({
...initialState,
setItems: (items) => set({ items }),
reset: () => set(initialState),
}),
{ name: 'feature-store' }
)
)
```
Use `devtools` middleware. Use `persist` only when data should survive reload with `partialize` to persist only necessary state.
## React Query
All React Query hooks live in `hooks/queries/`.
```typescript
export const entityKeys = {
all: ['entity'] as const,
list: (workspaceId?: string) => [...entityKeys.all, 'list', workspaceId ?? ''] as const,
}
export function useEntityList(workspaceId?: string) {
return useQuery({
queryKey: entityKeys.list(workspaceId),
queryFn: () => fetchEntities(workspaceId as string),
enabled: Boolean(workspaceId),
staleTime: 60 * 1000,
placeholderData: keepPreviousData,
})
}
```
## Styling
Use Tailwind only, no inline styles. Use `cn()` from `@/lib/utils` for conditional classes.
```typescript
<div className={cn('base-classes', isActive && 'active-classes')} />
```
## EMCN Components
Import from `@/components/emcn`, never from subpaths (except CSS files). Use CVA when 2+ variants exist.
## Testing
Use Vitest. Test files: `feature.ts``feature.test.ts`
```typescript
/**
* @vitest-environment node
*/
import { databaseMock, loggerMock } from '@sim/testing'
import { describe, expect, it, vi } from 'vitest'
vi.mock('@sim/db', () => databaseMock)
vi.mock('@sim/logger', () => loggerMock)
import { myFunction } from '@/lib/feature'
describe('feature', () => {
beforeEach(() => vi.clearAllMocks())
it.concurrent('runs in parallel', () => { ... })
})
```
Use `@sim/testing` mocks/factories over local test data. See `.cursor/rules/sim-testing.mdc` for details.
## Utils Rules
- Never create `utils.ts` for single consumer - inline it
- Create `utils.ts` when 2+ files need the same helper
- Check existing sources in `lib/` before duplicating
## Adding Integrations
New integrations require: **Tools****Block****Icon** → (optional) **Trigger**
Always look up the service's API docs first.
### 1. Tools (`tools/{service}/`)
```
tools/{service}/
├── index.ts # Barrel export
├── types.ts # Params/response types
└── {action}.ts # Tool implementation
```
**Tool structure:**
```typescript
export const serviceTool: ToolConfig<Params, Response> = {
id: 'service_action',
name: 'Service Action',
description: '...',
version: '1.0.0',
oauth: { required: true, provider: 'service' },
params: { /* ... */ },
request: { url: '/api/tools/service/action', method: 'POST', ... },
transformResponse: async (response) => { /* ... */ },
outputs: { /* ... */ },
}
```
Register in `tools/registry.ts`.
### 2. Block (`blocks/blocks/{service}.ts`)
```typescript
export const ServiceBlock: BlockConfig = {
type: 'service',
name: 'Service',
description: '...',
category: 'tools',
bgColor: '#hexcolor',
icon: ServiceIcon,
subBlocks: [ /* see SubBlock Properties */ ],
tools: { access: ['service_action'], config: { tool: (p) => `service_${p.operation}` } },
inputs: { /* ... */ },
outputs: { /* ... */ },
}
```
Register in `blocks/registry.ts` (alphabetically).
**SubBlock Properties:**
```typescript
{
id: 'field', title: 'Label', type: 'short-input', placeholder: '...',
required: true, // or condition object
condition: { field: 'op', value: 'send' }, // show/hide
dependsOn: ['credential'], // clear when dep changes
mode: 'basic', // 'basic' | 'advanced' | 'both' | 'trigger'
}
```
**condition examples:**
- `{ field: 'op', value: 'send' }` - show when op === 'send'
- `{ field: 'op', value: ['a','b'] }` - show when op is 'a' OR 'b'
- `{ field: 'op', value: 'x', not: true }` - show when op !== 'x'
- `{ field: 'op', value: 'x', not: true, and: { field: 'type', value: 'dm', not: true } }` - complex
**dependsOn:** `['field']` or `{ all: ['a'], any: ['b', 'c'] }`
### 3. Icon (`components/icons.tsx`)
```typescript
export function ServiceIcon(props: SVGProps<SVGSVGElement>) {
return <svg {...props}>/* SVG from brand assets */</svg>
}
```
### 4. Trigger (`triggers/{service}/`) - Optional
```
triggers/{service}/
├── index.ts # Barrel export
├── webhook.ts # Webhook handler
└── {event}.ts # Event-specific handlers
```
Register in `triggers/registry.ts`.
### Integration Checklist
- [ ] Look up API docs
- [ ] Create `tools/{service}/` with types and tools
- [ ] Register tools in `tools/registry.ts`
- [ ] Add icon to `components/icons.tsx`
- [ ] Create block in `blocks/blocks/{service}.ts`
- [ ] Register block in `blocks/registry.ts`
- [ ] (Optional) Create and register triggers

27
Dockerfile Normal file
View File

@@ -0,0 +1,27 @@
FROM node:20-alpine
# Set working directory
WORKDIR /app
# Set Node.js memory limit
ENV NODE_OPTIONS="--max-old-space-size=4096"
# Copy package files
COPY sim/package*.json ./
# Install dependencies
RUN npm install
# Copy the rest of the application
COPY sim/ ./
# Generate database schema
RUN npx drizzle-kit generate
# Build the application
RUN npm run build
EXPOSE 3000
# Run migrations and start the app
CMD npx drizzle-kit push && npm run start

View File

@@ -187,7 +187,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2026 Sim Studio, Inc.
Copyright 2025 Sim Studio, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

4
NOTICE
View File

@@ -1,4 +1,4 @@
Sim Studio
Copyright 2026 Sim Studio
Copyright 2025 Sim Studio
This product includes software developed for the Sim project.
This product includes software developed for the Sim Studio project.

300
README.md
View File

@@ -1,281 +1,143 @@
<p align="center">
<a href="https://sim.ai" target="_blank" rel="noopener noreferrer">
<img src="apps/sim/public/logo/reverse/text/large.png" alt="Sim Logo" width="500"/>
</a>
<img src="sim/public/static/sim.png" alt="Sim Studio Logo" width="500"/>
</p>
<p align="center">Build and deploy AI agent workflows in minutes.</p>
<p align="center">
<a href="https://sim.ai" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/sim.ai-6F3DFA" alt="Sim.ai"></a>
<a href="https://discord.gg/Hr4UWYEcTT" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/Discord-Join%20Server-5865F2?logo=discord&logoColor=white" alt="Discord"></a>
<a href="https://x.com/simdotai" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/twitter/follow/simstudioai?style=social" alt="Twitter"></a>
<a href="https://docs.sim.ai" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/Docs-6F3DFA.svg" alt="Documentation"></a>
<a href="https://www.apache.org/licenses/LICENSE-2.0"><img src="https://img.shields.io/badge/License-Apache%202.0-blue.svg" alt="License: Apache-2.0"></a>
<a href="https://discord.gg/Hr4UWYEcTT"><img src="https://img.shields.io/badge/Discord-Join%20Server-7289DA?logo=discord&logoColor=white" alt="Discord"></a>
<a href="https://x.com/simstudioai"><img src="https://img.shields.io/twitter/follow/simstudioai?style=social" alt="Twitter"></a>
<a href="https://github.com/simstudioai/sim/pulls"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg" alt="PRs welcome"></a>
<a href="https://docs.simstudio.ai"><img src="https://img.shields.io/badge/Docs-visit%20documentation-blue.svg" alt="Documentation"></a>
</p>
### Build Workflows with Ease
Design agent workflows visually on a canvas—connect agents, tools, and blocks, then run them instantly.
<p align="center">
<img src="apps/sim/public/static/workflow.gif" alt="Workflow Builder Demo" width="800"/>
<strong>Sim Studio</strong> is a lightweight, user-friendly platform for building AI agent workflows.
</p>
### Supercharge with Copilot
Leverage Copilot to generate nodes, fix errors, and iterate on flows directly from natural language.
## Getting Started
<p align="center">
<img src="apps/sim/public/static/copilot.gif" alt="Copilot Demo" width="800"/>
</p>
### Run on [Sim Studio Cloud](https://simstudio.ai)
### Integrate Vector Databases
Upload documents to a vector store and let agents answer questions grounded in your specific content.
The fastest way to get started is to use our [cloud-hosted version](https://simstudio.ai) - no setup required!
<p align="center">
<img src="apps/sim/public/static/knowledge.gif" alt="Knowledge Uploads and Retrieval Demo" width="800"/>
</p>
### Self-host Sim Studio
## Quickstart
If you prefer to self-host, there are several options available:
### Cloud-hosted: [sim.ai](https://sim.ai)
### Option 1: Using CLI (Recommended)
<a href="https://sim.ai" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/sim.ai-6F3DFA?logo=data:image/svg%2bxml;base64,PHN2ZyB3aWR0aD0iNjE2IiBoZWlnaHQ9IjYxNiIgdmlld0JveD0iMCAwIDYxNiA2MTYiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMF8xMTU5XzMxMykiPgo8cGF0aCBkPSJNNjE2IDBIMFY2MTZINjE2VjBaIiBmaWxsPSIjNkYzREZBIi8+CjxwYXRoIGQ9Ik04MyAzNjUuNTY3SDExM0MxMTMgMzczLjgwNSAxMTYgMzgwLjM3MyAxMjIgMzg1LjI3MkMxMjggMzg5Ljk0OCAxMzYuMTExIDM5Mi4yODUgMTQ2LjMzMyAzOTIuMjg1QzE1Ny40NDQgMzkyLjI4NSAxNjYgMzkwLjE3MSAxNzIgMzg1LjkzOUMxNzcuOTk5IDM4MS40ODcgMTgxIDM3NS41ODYgMTgxIDM2OC4yMzlDMTgxIDM2Mi44OTUgMTc5LjMzMyAzNTguNDQyIDE3NiAzNTQuODhDMTcyLjg4OSAzNTEuMzE4IDE2Ny4xMTEgMzQ4LjQyMiAxNTguNjY3IDM0Ni4xOTZMMTMwIDMzOS41MTdDMTE1LjU1NSAzMzUuOTU1IDEwNC43NzggMzMwLjQ5OSA5Ny42NjY1IDMyMy4xNTFDOTAuNzc3NSAzMTUuODA0IDg3LjMzMzQgMzA2LjExOSA4Ny4zMzM0IDI5NC4wOTZDODcuMzMzNCAyODQuMDc2IDg5Ljg4OSAyNzUuMzkyIDk0Ljk5OTYgMjY4LjA0NUMxMDAuMzMzIDI2MC42OTcgMTA3LjU1NSAyNTUuMDIgMTE2LjY2NiAyNTEuMDEyQzEyNiAyNDcuMDA0IDEzNi42NjcgMjQ1IDE0OC42NjYgMjQ1QzE2MC42NjcgMjQ1IDE3MSAyNDcuMTE2IDE3OS42NjcgMjUxLjM0NkMxODguNTU1IDI1NS41NzYgMTk1LjQ0NCAyNjEuNDc3IDIwMC4zMzMgMjY5LjA0N0MyMDUuNDQ0IDI3Ni42MTcgMjA4LjExMSAyODUuNjM0IDIwOC4zMzMgMjk2LjA5OUgxNzguMzMzQzE3OC4xMTEgMjg3LjYzOCAxNzUuMzMzIDI4MS4wNyAxNjkuOTk5IDI3Ni4zOTRDMTY0LjY2NiAyNzEuNzE5IDE1Ny4yMjIgMjY5LjM4MSAxNDcuNjY3IDI2OS4zODFDMTM3Ljg4OSAyNjkuMzgxIDEzMC4zMzMgMjcxLjQ5NiAxMjUgMjc1LjcyNkMxMTkuNjY2IDI3OS45NTcgMTE3IDI4NS43NDYgMTE3IDI5My4wOTNDMTE3IDMwNC4wMDMgMTI1IDMxMS40NjIgMTQxIDMxNS40N0wxNjkuNjY3IDMyMi40ODNDMTgzLjQ0NSAzMjUuNiAxOTMuNzc4IDMzMC43MjIgMjAwLjY2NyAzMzcuODQ3QzIwNy41NTUgMzQ0Ljc0OSAyMTEgMzU0LjIxMiAyMTEgMzY2LjIzNUMyMTEgMzc2LjQ3NyAyMDguMjIyIDM4NS40OTQgMjAyLjY2NiAzOTMuMjg3QzE5Ny4xMTEgNDAwLjg1NyAxODkuNDQ0IDQwNi43NTggMTc5LjY2NyA0MTAuOTg5QzE3MC4xMTEgNDE0Ljk5NiAxNTguNzc4IDQxNyAxNDUuNjY3IDQxN0MxMjYuNTU1IDQxNyAxMTEuMzMzIDQxMi4zMjUgOTkuOTk5NyA0MDIuOTczQzg4LjY2NjggMzkzLjYyMSA4MyAzODEuMTUzIDgzIDM2NS41NjdaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMjMyLjI5MSA0MTNWMjUwLjA4MkMyNDQuNjg0IDI1NC42MTQgMjUwLjE0OCAyNTQuNjE0IDI2My4zNzEgMjUwLjA4MlY0MTNIMjMyLjI5MVpNMjQ3LjUgMjM5LjMxM0MyNDEuOTkgMjM5LjMxMyAyMzcuMTQgMjM3LjMxMyAyMzIuOTUyIDIzMy4zMTZDMjI4Ljk4NCAyMjkuMDk1IDIyNyAyMjQuMjA5IDIyNyAyMTguNjU2QzIyNyAyMTIuODgyIDIyOC45ODQgMjA3Ljk5NSAyMzIuOTUyIDIwMy45OTdDMjM3LjE0IDE5OS45OTkgMjQxLjk5IDE5OCAyNDcuNSAxOThDMjUzLjIzMSAxOTggMjU4LjA4IDE5OS45OTkgMjYyLjA0OSAyMDMuOTk3QzI2Ni4wMTYgMjA3Ljk5NSAyNjggMjEyLjg4MiAyNjggMjE4LjY1NkMyNjggMjI0LjIwOSAyNjYuMDE2IDIyOS4wOTUgMjYyLjA0OSAyMzMuMzE2QzI1OC4wOCAyMzcuMzEzIDI1My4yMzEgMjM5LjMxMyAyNDcuNSAyMzkuMzEzWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTMxOS4zMzMgNDEzSDI4OFYyNDkuNjc2SDMxNlYyNzcuMjMzQzMxOS4zMzMgMjY4LjEwNCAzMjUuNzc4IDI2MC4zNjQgMzM0LjY2NyAyNTQuMzUyQzM0My43NzggMjQ4LjExNyAzNTQuNzc4IDI0NSAzNjcuNjY3IDI0NUMzODIuMTExIDI0NSAzOTQuMTEyIDI0OC44OTcgNDAzLjY2NyAyNTYuNjlDNDEzLjIyMiAyNjQuNDg0IDQxOS40NDQgMjc0LjgzNyA0MjIuMzM0IDI4Ny43NTJINDE2LjY2N0M0MTguODg5IDI3NC44MzcgNDI1IDI2NC40ODQgNDM1IDI1Ni42OUM0NDUgMjQ4Ljg5NyA0NTcuMzM0IDI0NSA0NzIgMjQ1QzQ5MC42NjYgMjQ1IDUwNS4zMzQgMjUwLjQ1NSA1MTYgMjYxLjM2NkM1MjYuNjY3IDI3Mi4yNzYgNTMyIDI4Ny4xOTUgNTMyIDMwNi4xMjFWNDEzSDUwMS4zMzNWMzEzLjgwNEM1MDEuMzMzIDMwMC44ODkgNDk4IDI5MC45ODEgNDkxLjMzMyAyODQuMDc4QzQ4NC44ODkgMjc2Ljk1MiA0NzYuMTExIDI3My4zOSA0NjUgMjczLjM5QzQ1Ny4yMjIgMjczLjM5IDQ1MC4zMzMgMjc1LjE3MSA0NDQuMzM0IDI3OC43MzRDNDM4LjU1NiAyODIuMDc0IDQzNCAyODYuOTcyIDQzMC42NjcgMjkzLjQzQzQyNy4zMzMgMjk5Ljg4NyA0MjUuNjY3IDMwNy40NTcgNDI1LjY2NyAzMTYuMTQxVjQxM0gzOTQuNjY3VjMxMy40NjlDMzk0LjY2NyAzMDAuNTU1IDM5MS40NDUgMjkwLjc1OCAzODUgMjg0LjA3OEMzNzguNTU2IDI3Ny4xNzUgMzY5Ljc3OCAyNzMuNzI0IDM1OC42NjcgMjczLjcyNEMzNTAuODg5IDI3My43MjQgMzQ0IDI3NS41MDUgMzM4IDI3OS4wNjhDMzMyLjIyMiAyODIuNDA4IDMyNy42NjcgMjg3LjMwNyAzMjQuMzMzIDI5My43NjNDMzIxIDI5OS45OTggMzE5LjMzMyAzMDcuNDU3IDMxOS4zMzMgMzE2LjE0MVY0MTNaIiBmaWxsPSJ3aGl0ZSIvPgo8L2c+CjxkZWZzPgo8Y2xpcFBhdGggaWQ9ImNsaXAwXzExNTlfMzEzIj4KPHJlY3Qgd2lkdGg9IjYxNiIgaGVpZ2h0PSI2MTYiIGZpbGw9IndoaXRlIi8+CjwvY2xpcFBhdGg+CjwvZGVmcz4KPC9zdmc+Cg==&logoColor=white" alt="Sim.ai"></a>
### Self-hosted: NPM Package
The easiest way to self-host:
```bash
npx simstudio
# This will set up and run Sim Studio locally with minimal configuration
```
→ http://localhost:3000
#### Note
Docker must be installed and running on your machine.
#### Options
| Flag | Description |
|------|-------------|
| `-p, --port <port>` | Port to run Sim on (default `3000`) |
| `--no-pull` | Skip pulling latest Docker images |
### Self-hosted: Docker Compose
### Option 2: Using Docker Compose
```bash
# Clone the repository
git clone https://github.com/simstudioai/sim.git
# Navigate to the project directory
cd sim
# Start Sim
docker compose -f docker-compose.prod.yml up -d
# Create environment file (update BETTER_AUTH_SECRET and ENCRYPTION_KEY with secure random values)
cp sim/.env.example sim/.env
# Start with Docker Compose
docker compose up -d --build
```
Access the application at [http://localhost:3000/](http://localhost:3000/)
Once running, access the application at [http://localhost:3000/w/](http://localhost:3000/w/)
#### Using Local Models with Ollama
## Working with Local Models
Run Sim with local AI models using [Ollama](https://ollama.ai) - no external APIs required:
Sim Studio supports integration with local LLM models:
```bash
# Start with GPU support (automatically downloads gemma3:4b model)
docker compose -f docker-compose.ollama.yml --profile setup up -d
# Pull local models
./sim/scripts/ollama_docker.sh pull <model_name>
# For CPU-only systems:
docker compose -f docker-compose.ollama.yml --profile cpu --profile setup up -d
# Start with local model support
./start_simstudio_docker.sh --local
# For systems with NVIDIA GPU
docker compose up --profile local-gpu -d --build
# For CPU-only systems
docker compose up --profile local-cpu -d --build
```
Wait for the model to download, then visit [http://localhost:3000](http://localhost:3000). Add more models with:
```bash
docker compose -f docker-compose.ollama.yml exec ollama ollama pull llama3.1:8b
```
### Connecting to Existing Ollama Instance
#### Using an External Ollama Instance
If you already have Ollama running on your host machine (outside Docker), you need to configure the `OLLAMA_URL` to use `host.docker.internal` instead of `localhost`:
If you already have Ollama running locally:
```bash
# Docker Desktop (macOS/Windows)
OLLAMA_URL=http://host.docker.internal:11434 docker compose -f docker-compose.prod.yml up -d
# Linux (add extra_hosts or use host IP)
docker compose -f docker-compose.prod.yml up -d # Then set OLLAMA_URL to your host's IP
# Using host networking (simplest)
docker compose up --profile local-cpu -d --build --network=host
```
**Why?** When running inside Docker, `localhost` refers to the container itself, not your host machine. `host.docker.internal` is a special DNS name that resolves to the host.
Or add this to your docker-compose.yml:
For Linux users, you can either:
- Use your host machine's actual IP address (e.g., `http://192.168.1.100:11434`)
- Add `extra_hosts: ["host.docker.internal:host-gateway"]` to the simstudio service in your compose file
```yaml
services:
simstudio:
# ... existing configuration ...
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
- OLLAMA_HOST=http://host.docker.internal:11434
```
#### Using vLLM
## Development Setup
Sim also supports [vLLM](https://docs.vllm.ai/) for self-hosted models with OpenAI-compatible API:
### Prerequisites
- Node.js 20+
- Docker (recommended)
- PostgreSQL (if not using Docker)
### Required Environment Variables
For local development, create a `.env` file with these minimum variables:
```env
DATABASE_URL=postgresql://postgres:postgres@db:5432/simstudio
BETTER_AUTH_SECRET=<generate_a_secure_random_value>
ENCRYPTION_KEY=<generate_a_secure_random_value>
```
⚠️ **Note:** Without `RESEND_API_KEY`, verification codes will be logged to the console for local testing.
### Dev Container Option
1. Open in VS Code with the Remote-Containers extension
2. Click "Reopen in Container" when prompted
3. Run `npm run dev` or use the `sim-start` alias
### Manual Setup
```bash
# Set these environment variables
VLLM_BASE_URL=http://your-vllm-server:8000
VLLM_API_KEY=your_optional_api_key # Only if your vLLM instance requires auth
cd sim/sim
npm install
cp .env.example .env
npx drizzle-kit push
npm run dev
```
When running with Docker, use `host.docker.internal` if vLLM is on your host machine (same as Ollama above).
### Self-hosted: Dev Containers
1. Open VS Code with the [Remote - Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
2. Open the project and click "Reopen in Container" when prompted
3. Run `bun run dev:full` in the terminal or use the `sim-start` alias
- This starts both the main application and the realtime socket server
### Self-hosted: Manual Setup
**Requirements:**
- [Bun](https://bun.sh/) runtime
- [Node.js](https://nodejs.org/) v20+ (required for sandboxed code execution)
- PostgreSQL 12+ with [pgvector extension](https://github.com/pgvector/pgvector) (required for AI embeddings)
**Note:** Sim uses vector embeddings for AI features like knowledge bases and semantic search, which requires the `pgvector` PostgreSQL extension.
1. Clone and install dependencies:
```bash
git clone https://github.com/simstudioai/sim.git
cd sim
bun install
```
2. Set up PostgreSQL with pgvector:
You need PostgreSQL with the `vector` extension for embedding support. Choose one option:
**Option A: Using Docker (Recommended)**
```bash
# Start PostgreSQL with pgvector extension
docker run --name simstudio-db \
-e POSTGRES_PASSWORD=your_password \
-e POSTGRES_DB=simstudio \
-p 5432:5432 -d \
pgvector/pgvector:pg17
```
**Option B: Manual Installation**
- Install PostgreSQL 12+ and the pgvector extension
- See [pgvector installation guide](https://github.com/pgvector/pgvector#installation)
3. Set up environment:
```bash
cd apps/sim
cp .env.example .env # Configure with required variables (DATABASE_URL, BETTER_AUTH_SECRET, BETTER_AUTH_URL)
```
Update your `.env` file with the database URL:
```bash
DATABASE_URL="postgresql://postgres:your_password@localhost:5432/simstudio"
```
4. Set up the database:
First, configure the database package environment:
```bash
cd packages/db
cp .env.example .env
```
Update your `packages/db/.env` file with the database URL:
```bash
DATABASE_URL="postgresql://postgres:your_password@localhost:5432/simstudio"
```
Then run the migrations:
```bash
cd packages/db # Required so drizzle picks correct .env file
bunx drizzle-kit migrate --config=./drizzle.config.ts
```
5. Start the development servers:
**Recommended approach - run both servers together (from project root):**
```bash
bun run dev:full
```
This starts both the main Next.js application and the realtime socket server required for full functionality.
**Alternative - run servers separately:**
Next.js app (from project root):
```bash
bun run dev
```
Realtime socket server (from `apps/sim` directory in a separate terminal):
```bash
cd apps/sim
bun run dev:sockets
```
## Copilot API Keys
Copilot is a Sim-managed service. To use Copilot on a self-hosted instance:
- Go to https://sim.ai → Settings → Copilot and generate a Copilot API key
- Set `COPILOT_API_KEY` environment variable in your self-hosted apps/sim/.env file to that value
## Environment Variables
Key environment variables for self-hosted deployments (see `apps/sim/.env.example` for full list):
| Variable | Required | Description |
|----------|----------|-------------|
| `DATABASE_URL` | Yes | PostgreSQL connection string with pgvector |
| `BETTER_AUTH_SECRET` | Yes | Auth secret (`openssl rand -hex 32`) |
| `BETTER_AUTH_URL` | Yes | Your app URL (e.g., `http://localhost:3000`) |
| `NEXT_PUBLIC_APP_URL` | Yes | Public app URL (same as above) |
| `ENCRYPTION_KEY` | Yes | Encryption key (`openssl rand -hex 32`) |
| `OLLAMA_URL` | No | Ollama server URL (default: `http://localhost:11434`) |
| `VLLM_BASE_URL` | No | vLLM server URL for self-hosted models |
| `COPILOT_API_KEY` | No | API key from sim.ai for Copilot features |
## Troubleshooting
### Ollama models not showing in dropdown (Docker)
Common issues and solutions:
If you're running Ollama on your host machine and Sim in Docker, change `OLLAMA_URL` from `localhost` to `host.docker.internal`:
```bash
OLLAMA_URL=http://host.docker.internal:11434 docker compose -f docker-compose.prod.yml up -d
```
See [Using an External Ollama Instance](#using-an-external-ollama-instance) for details.
### Database connection issues
Ensure PostgreSQL has the pgvector extension installed. When using Docker, wait for the database to be healthy before running migrations.
### Port conflicts
If ports 3000, 3002, or 5432 are in use, configure alternatives:
```bash
# Custom ports
NEXT_PUBLIC_APP_URL=http://localhost:3100 POSTGRES_PORT=5433 docker compose up -d
```
- **Authentication problems**: Check console logs for verification codes if `RESEND_API_KEY` is not set
- **Database connection errors**: Verify PostgreSQL is running and credentials are correct
- **Port conflicts**: Check if port 3000 is already in use by another application
## Tech Stack
- **Framework**: [Next.js](https://nextjs.org/) (App Router)
- **Runtime**: [Bun](https://bun.sh/)
- **Database**: PostgreSQL with [Drizzle ORM](https://orm.drizzle.team)
- **Authentication**: [Better Auth](https://better-auth.com)
- **UI**: [Shadcn](https://ui.shadcn.com/), [Tailwind CSS](https://tailwindcss.com)
- **State Management**: [Zustand](https://zustand-demo.pmnd.rs/)
- **Flow Editor**: [ReactFlow](https://reactflow.dev/)
- **Docs**: [Fumadocs](https://fumadocs.vercel.app/)
- **Monorepo**: [Turborepo](https://turborepo.org/)
- **Realtime**: [Socket.io](https://socket.io/)
- **Background Jobs**: [Trigger.dev](https://trigger.dev/)
- **Remote Code Execution**: [E2B](https://www.e2b.dev/)
## Contributing
@@ -285,4 +147,4 @@ We welcome contributions! Please see our [Contributing Guide](.github/CONTRIBUTI
This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.
<p align="center">Made with ❤️ by the Sim Team</p>
<p align="center">Made with ❤️ by the Sim Studio Team</p>

39
apps/docs/.gitignore vendored
View File

@@ -1,39 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
# bun specific
.bun
bun.lockb
bun-debug.log*
# testing
/coverage
# next.js
/.next/
/out/
/build
# misc
.DS_Store
*.pem
# env files
.env
*.env
.env.local
.env.development
.env.test
.env.production
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# Fumadocs
/.source/

View File

@@ -1,23 +0,0 @@
# docs
This is a Next.js application generated with
[Create Fumadocs](https://github.com/fuma-nama/fumadocs).
Run development server:
```bash
bun run dev
```
Open http://localhost:3000 with your browser to see the result.
## Learn More
To learn more about Next.js and Fumadocs, take a look at the following
resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js
features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
- [Fumadocs](https://fumadocs.vercel.app) - learn about Fumadocs
- [Bun Documentation](https://bun.sh/docs) - learn about Bun features and API

View File

@@ -1,335 +0,0 @@
import type React from 'react'
import { findNeighbour } from 'fumadocs-core/page-tree'
import defaultMdxComponents from 'fumadocs-ui/mdx'
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/page'
import { ChevronLeft, ChevronRight } from 'lucide-react'
import Link from 'next/link'
import { notFound } from 'next/navigation'
import { PageNavigationArrows } from '@/components/docs-layout/page-navigation-arrows'
import { TOCFooter } from '@/components/docs-layout/toc-footer'
import { LLMCopyButton } from '@/components/page-actions'
import { StructuredData } from '@/components/structured-data'
import { CodeBlock } from '@/components/ui/code-block'
import { Heading } from '@/components/ui/heading'
import { type PageData, source } from '@/lib/source'
export default async function Page(props: { params: Promise<{ slug?: string[]; lang: string }> }) {
const params = await props.params
const page = source.getPage(params.slug, params.lang)
if (!page) notFound()
const data = page.data as PageData
const MDX = data.body
const baseUrl = 'https://docs.sim.ai'
const pageTreeRecord = source.pageTree as Record<string, any>
const pageTree =
pageTreeRecord[params.lang] ?? pageTreeRecord.en ?? Object.values(pageTreeRecord)[0]
const neighbours = pageTree ? findNeighbour(pageTree, page.url) : null
const generateBreadcrumbs = () => {
const breadcrumbs: Array<{ name: string; url: string }> = [
{
name: 'Home',
url: baseUrl,
},
]
const urlParts = page.url.split('/').filter(Boolean)
let currentPath = ''
urlParts.forEach((part, index) => {
if (index === 0 && ['en', 'es', 'fr', 'de', 'ja', 'zh'].includes(part)) {
currentPath = `/${part}`
return
}
currentPath += `/${part}`
const name = part
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ')
if (index === urlParts.length - 1) {
breadcrumbs.push({
name: data.title,
url: `${baseUrl}${page.url}`,
})
} else {
breadcrumbs.push({
name: name,
url: `${baseUrl}${currentPath}`,
})
}
})
return breadcrumbs
}
const breadcrumbs = generateBreadcrumbs()
const CustomFooter = () => (
<div className='mt-12'>
{/* Navigation links */}
<div className='flex items-center justify-between py-8'>
{neighbours?.previous ? (
<Link
href={neighbours.previous.url}
className='group flex items-center gap-2 text-muted-foreground transition-colors hover:text-foreground'
>
<ChevronLeft className='group-hover:-translate-x-1 h-4 w-4 transition-transform' />
<span className='font-medium'>{neighbours.previous.name}</span>
</Link>
) : (
<div />
)}
{neighbours?.next ? (
<Link
href={neighbours.next.url}
className='group flex items-center gap-2 text-muted-foreground transition-colors hover:text-foreground'
>
<span className='font-medium'>{neighbours.next.name}</span>
<ChevronRight className='h-4 w-4 transition-transform group-hover:translate-x-1' />
</Link>
) : (
<div />
)}
</div>
{/* Divider line */}
<div className='border-border border-t' />
{/* Social icons */}
<div className='flex items-center gap-4 py-6'>
<Link
href='https://x.com/simdotai'
target='_blank'
rel='noopener noreferrer'
aria-label='X (Twitter)'
>
<div
className='h-5 w-5 bg-gray-400 transition-colors hover:bg-gray-500 dark:bg-gray-500 dark:hover:bg-gray-400'
style={{
maskImage:
"url('data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22%3E%3Cpath d=%22M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z%22/%3E%3C/svg%3E')",
maskRepeat: 'no-repeat',
maskPosition: 'center center',
WebkitMaskImage:
"url('data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22%3E%3Cpath d=%22M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z%22/%3E%3C/svg%3E')",
WebkitMaskRepeat: 'no-repeat',
WebkitMaskPosition: 'center center',
}}
/>
</Link>
<Link
href='https://github.com/simstudioai/sim'
target='_blank'
rel='noopener noreferrer'
aria-label='GitHub'
>
<div
className='h-5 w-5 bg-gray-400 transition-colors hover:bg-gray-500 dark:bg-gray-500 dark:hover:bg-gray-400'
style={{
maskImage:
"url('data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22%3E%3Cpath d=%22M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z%22/%3E%3C/svg%3E')",
maskRepeat: 'no-repeat',
maskPosition: 'center center',
WebkitMaskImage:
"url('data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22%3E%3Cpath d=%22M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z%22/%3E%3C/svg%3E')",
WebkitMaskRepeat: 'no-repeat',
WebkitMaskPosition: 'center center',
}}
/>
</Link>
<Link
href='https://discord.gg/Hr4UWYEcTT'
target='_blank'
rel='noopener noreferrer'
aria-label='Discord'
>
<div
className='h-5 w-5 bg-gray-400 transition-colors hover:bg-gray-500 dark:bg-gray-500 dark:hover:bg-gray-400'
style={{
maskImage:
"url('data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22%3E%3Cpath d=%22M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z%22/%3E%3C/svg%3E')",
maskRepeat: 'no-repeat',
maskPosition: 'center center',
WebkitMaskImage:
"url('data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22%3E%3Cpath d=%22M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z%22/%3E%3C/svg%3E')",
WebkitMaskRepeat: 'no-repeat',
WebkitMaskPosition: 'center center',
}}
/>
</Link>
</div>
</div>
)
return (
<>
<StructuredData
title={data.title}
description={data.description || ''}
url={`${baseUrl}${page.url}`}
lang={params.lang}
breadcrumb={breadcrumbs}
/>
<DocsPage
toc={data.toc}
full={data.full}
breadcrumb={{
enabled: false,
}}
tableOfContent={{
style: 'clerk',
enabled: true,
header: (
<div key='toc-header' className='mb-2 font-medium text-sm'>
On this page
</div>
),
footer: <TOCFooter />,
single: false,
}}
tableOfContentPopover={{
style: 'clerk',
enabled: true,
}}
footer={{
enabled: true,
component: <CustomFooter />,
}}
>
<div className='relative mt-6 sm:mt-0'>
<div className='absolute top-1 right-0 flex items-center gap-2'>
<div className='hidden sm:flex'>
<LLMCopyButton markdownUrl={`${page.url}.mdx`} />
</div>
<PageNavigationArrows previous={neighbours?.previous} next={neighbours?.next} />
</div>
<DocsTitle>{data.title}</DocsTitle>
<DocsDescription>{data.description}</DocsDescription>
</div>
<DocsBody>
<MDX
components={{
...defaultMdxComponents,
CodeBlock,
h1: (props: React.HTMLAttributes<HTMLHeadingElement>) => (
<Heading as='h1' {...props} />
),
h2: (props: React.HTMLAttributes<HTMLHeadingElement>) => (
<Heading as='h2' {...props} />
),
h3: (props: React.HTMLAttributes<HTMLHeadingElement>) => (
<Heading as='h3' {...props} />
),
h4: (props: React.HTMLAttributes<HTMLHeadingElement>) => (
<Heading as='h4' {...props} />
),
h5: (props: React.HTMLAttributes<HTMLHeadingElement>) => (
<Heading as='h5' {...props} />
),
h6: (props: React.HTMLAttributes<HTMLHeadingElement>) => (
<Heading as='h6' {...props} />
),
}}
/>
</DocsBody>
</DocsPage>
</>
)
}
export async function generateStaticParams() {
return source.generateParams()
}
export async function generateMetadata(props: {
params: Promise<{ slug?: string[]; lang: string }>
}) {
const params = await props.params
const page = source.getPage(params.slug, params.lang)
if (!page) notFound()
const data = page.data as PageData
const baseUrl = 'https://docs.sim.ai'
const fullUrl = `${baseUrl}${page.url}`
const ogImageUrl = `${baseUrl}/api/og?title=${encodeURIComponent(data.title)}`
return {
title: data.title,
description:
data.description || 'Sim visual workflow builder for AI applications documentation',
keywords: [
'AI workflow builder',
'visual workflow editor',
'AI automation',
'workflow automation',
'AI agents',
'no-code AI',
'drag and drop workflows',
data.title?.toLowerCase().split(' '),
]
.flat()
.filter(Boolean),
authors: [{ name: 'Sim Team' }],
category: 'Developer Tools',
openGraph: {
title: data.title,
description:
data.description || 'Sim visual workflow builder for AI applications documentation',
url: fullUrl,
siteName: 'Sim Documentation',
type: 'article',
locale: params.lang === 'en' ? 'en_US' : `${params.lang}_${params.lang.toUpperCase()}`,
alternateLocale: ['en', 'es', 'fr', 'de', 'ja', 'zh']
.filter((lang) => lang !== params.lang)
.map((lang) => (lang === 'en' ? 'en_US' : `${lang}_${lang.toUpperCase()}`)),
images: [
{
url: ogImageUrl,
width: 1200,
height: 630,
alt: data.title,
},
],
},
twitter: {
card: 'summary_large_image',
title: data.title,
description:
data.description || 'Sim visual workflow builder for AI applications documentation',
images: [ogImageUrl],
creator: '@simdotai',
site: '@simdotai',
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
'max-video-preview': -1,
'max-image-preview': 'large',
'max-snippet': -1,
},
},
canonical: fullUrl,
alternates: {
canonical: fullUrl,
languages: {
'x-default': `${baseUrl}${page.url.replace(`/${params.lang}`, '')}`,
en: `${baseUrl}${page.url.replace(`/${params.lang}`, '')}`,
es: `${baseUrl}/es${page.url.replace(`/${params.lang}`, '')}`,
fr: `${baseUrl}/fr${page.url.replace(`/${params.lang}`, '')}`,
de: `${baseUrl}/de${page.url.replace(`/${params.lang}`, '')}`,
ja: `${baseUrl}/ja${page.url.replace(`/${params.lang}`, '')}`,
zh: `${baseUrl}/zh${page.url.replace(`/${params.lang}`, '')}`,
},
},
}
}

View File

@@ -1,137 +0,0 @@
import type { ReactNode } from 'react'
import { defineI18nUI } from 'fumadocs-ui/i18n'
import { DocsLayout } from 'fumadocs-ui/layouts/docs'
import { RootProvider } from 'fumadocs-ui/provider/next'
import { Geist_Mono, Inter } from 'next/font/google'
import Image from 'next/image'
import {
SidebarFolder,
SidebarItem,
SidebarSeparator,
} from '@/components/docs-layout/sidebar-components'
import { Navbar } from '@/components/navbar/navbar'
import { i18n } from '@/lib/i18n'
import { source } from '@/lib/source'
import '../global.css'
const inter = Inter({
subsets: ['latin'],
variable: '--font-geist-sans',
})
const geistMono = Geist_Mono({
subsets: ['latin'],
variable: '--font-geist-mono',
})
const { provider } = defineI18nUI(i18n, {
translations: {
en: {
displayName: 'English',
},
es: {
displayName: 'Español',
},
fr: {
displayName: 'Français',
},
de: {
displayName: 'Deutsch',
},
ja: {
displayName: '日本語',
},
zh: {
displayName: '简体中文',
},
},
})
type LayoutProps = {
children: ReactNode
params: Promise<{ lang: string }>
}
export default async function Layout({ children, params }: LayoutProps) {
const { lang } = await params
const structuredData = {
'@context': 'https://schema.org',
'@type': 'WebSite',
name: 'Sim Documentation',
description:
'Comprehensive documentation for Sim - the visual workflow builder for AI Agent Workflows.',
url: 'https://docs.sim.ai',
publisher: {
'@type': 'Organization',
name: 'Sim',
url: 'https://sim.ai',
logo: {
'@type': 'ImageObject',
url: 'https://docs.sim.ai/static/logo.png',
},
},
inLanguage: lang,
potentialAction: {
'@type': 'SearchAction',
target: {
'@type': 'EntryPoint',
urlTemplate: 'https://docs.sim.ai/api/search?q={search_term_string}',
},
'query-input': 'required name=search_term_string',
},
}
return (
<html
lang={lang}
className={`${inter.variable} ${geistMono.variable}`}
suppressHydrationWarning
>
<head>
<script
type='application/ld+json'
dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }}
/>
{/* OneDollarStats Analytics - CDN script handles everything automatically */}
<script defer src='https://assets.onedollarstats.com/stonks.js' />
</head>
<body className='flex min-h-screen flex-col font-sans'>
<RootProvider i18n={provider(lang)}>
<Navbar />
<DocsLayout
tree={source.pageTree[lang]}
nav={{
title: (
<Image
src='/static/logo.png'
alt='Sim'
width={72}
height={28}
className='h-7 w-auto'
priority
/>
),
}}
sidebar={{
defaultOpenLevel: 0,
collapsible: false,
footer: null,
banner: null,
components: {
Item: SidebarItem,
Folder: SidebarFolder,
Separator: SidebarSeparator,
},
}}
containerProps={{
className: '!pt-0',
}}
>
{children}
</DocsLayout>
</RootProvider>
</body>
</html>
)
}

View File

@@ -1,23 +0,0 @@
import { DocsBody, DocsPage } from 'fumadocs-ui/page'
export const metadata = {
title: 'Page Not Found',
}
export default function NotFound() {
return (
<DocsPage>
<DocsBody>
<div className='flex min-h-[60vh] flex-col items-center justify-center text-center'>
<h1 className='mb-4 bg-gradient-to-b from-[#8357FF] to-[#6F3DFA] bg-clip-text font-bold text-8xl text-transparent'>
404
</h1>
<h2 className='mb-2 font-semibold text-2xl text-foreground'>Page Not Found</h2>
<p className='text-muted-foreground'>
The page you're looking for doesn't exist or has been moved.
</p>
</div>
</DocsBody>
</DocsPage>
)
}

View File

@@ -1,152 +0,0 @@
import { ImageResponse } from 'next/og'
import type { NextRequest } from 'next/server'
export const runtime = 'edge'
const TITLE_FONT_SIZE = {
large: 64,
medium: 56,
small: 48,
} as const
function getTitleFontSize(title: string): number {
if (title.length > 45) return TITLE_FONT_SIZE.small
if (title.length > 30) return TITLE_FONT_SIZE.medium
return TITLE_FONT_SIZE.large
}
/**
* Loads a Google Font dynamically by fetching the CSS and extracting the font URL.
*/
async function loadGoogleFont(font: string, weights: string, text: string): Promise<ArrayBuffer> {
const url = `https://fonts.googleapis.com/css2?family=${font}:wght@${weights}&text=${encodeURIComponent(text)}`
const css = await (await fetch(url)).text()
const resource = css.match(/src: url\((.+)\) format\('(opentype|truetype)'\)/)
if (resource) {
const response = await fetch(resource[1])
if (response.status === 200) {
return await response.arrayBuffer()
}
}
throw new Error('Failed to load font data')
}
/**
* Generates dynamic Open Graph images for documentation pages.
*/
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const title = searchParams.get('title') || 'Documentation'
const baseUrl = new URL(request.url).origin
const allText = `${title}docs.sim.ai`
const fontData = await loadGoogleFont('Geist', '400;500;600', allText)
return new ImageResponse(
<div
style={{
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'column',
background: '#0c0c0c',
position: 'relative',
fontFamily: 'Geist',
}}
>
{/* Base gradient layer - subtle purple tint across the entire image */}
<div
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
background:
'radial-gradient(ellipse 150% 100% at 50% 100%, rgba(88, 28, 135, 0.15) 0%, rgba(88, 28, 135, 0.08) 25%, rgba(88, 28, 135, 0.03) 50%, transparent 80%)',
display: 'flex',
}}
/>
{/* Secondary glow - adds depth without harsh edges */}
<div
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
background:
'radial-gradient(ellipse 100% 80% at 80% 90%, rgba(112, 31, 252, 0.12) 0%, rgba(112, 31, 252, 0.04) 40%, transparent 70%)',
display: 'flex',
}}
/>
{/* Top darkening - creates natural vignette */}
<div
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
background:
'linear-gradient(180deg, rgba(0, 0, 0, 0.3) 0%, transparent 40%, transparent 100%)',
display: 'flex',
}}
/>
{/* Content */}
<div
style={{
display: 'flex',
flexDirection: 'column',
padding: '56px 72px',
height: '100%',
justifyContent: 'space-between',
}}
>
{/* Logo */}
<img src={`${baseUrl}/static/logo.png`} alt='sim' height={32} />
{/* Title */}
<span
style={{
fontSize: getTitleFontSize(title),
fontWeight: 600,
color: '#ffffff',
lineHeight: 1.1,
letterSpacing: '-0.02em',
}}
>
{title}
</span>
{/* Footer */}
<span
style={{
fontSize: 20,
fontWeight: 500,
color: '#71717a',
}}
>
docs.sim.ai
</span>
</div>
</div>,
{
width: 1200,
height: 630,
fonts: [
{
name: 'Geist',
data: fontData,
style: 'normal',
},
],
}
)
}

View File

@@ -1,126 +0,0 @@
import { sql } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { db, docsEmbeddings } from '@/lib/db'
import { generateSearchEmbedding } from '@/lib/embeddings'
export const runtime = 'nodejs'
export const revalidate = 0
/**
* Hybrid search API endpoint
* - English: Vector embeddings + keyword search
* - Other languages: Keyword search only
*/
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams
const query = searchParams.get('query') || searchParams.get('q') || ''
const locale = searchParams.get('locale') || 'en'
const limit = Number.parseInt(searchParams.get('limit') || '10', 10)
if (!query || query.trim().length === 0) {
return NextResponse.json([])
}
const candidateLimit = limit * 3
const similarityThreshold = 0.6
const localeMap: Record<string, string> = {
en: 'english',
es: 'spanish',
fr: 'french',
de: 'german',
ja: 'simple', // PostgreSQL doesn't have Japanese support, use simple
zh: 'simple', // PostgreSQL doesn't have Chinese support, use simple
}
const tsConfig = localeMap[locale] || 'simple'
const useVectorSearch = locale === 'en'
let vectorResults: Array<{
chunkId: string
chunkText: string
sourceDocument: string
sourceLink: string
headerText: string
headerLevel: number
similarity: number
searchType: string
}> = []
if (useVectorSearch) {
const queryEmbedding = await generateSearchEmbedding(query)
vectorResults = await db
.select({
chunkId: docsEmbeddings.chunkId,
chunkText: docsEmbeddings.chunkText,
sourceDocument: docsEmbeddings.sourceDocument,
sourceLink: docsEmbeddings.sourceLink,
headerText: docsEmbeddings.headerText,
headerLevel: docsEmbeddings.headerLevel,
similarity: sql<number>`1 - (${docsEmbeddings.embedding} <=> ${JSON.stringify(queryEmbedding)}::vector)`,
searchType: sql<string>`'vector'`,
})
.from(docsEmbeddings)
.where(
sql`1 - (${docsEmbeddings.embedding} <=> ${JSON.stringify(queryEmbedding)}::vector) >= ${similarityThreshold}`
)
.orderBy(sql`${docsEmbeddings.embedding} <=> ${JSON.stringify(queryEmbedding)}::vector`)
.limit(candidateLimit)
}
const keywordResults = await db
.select({
chunkId: docsEmbeddings.chunkId,
chunkText: docsEmbeddings.chunkText,
sourceDocument: docsEmbeddings.sourceDocument,
sourceLink: docsEmbeddings.sourceLink,
headerText: docsEmbeddings.headerText,
headerLevel: docsEmbeddings.headerLevel,
similarity: sql<number>`ts_rank(${docsEmbeddings.chunkTextTsv}, plainto_tsquery(${tsConfig}, ${query}))`,
searchType: sql<string>`'keyword'`,
})
.from(docsEmbeddings)
.where(sql`${docsEmbeddings.chunkTextTsv} @@ plainto_tsquery(${tsConfig}, ${query})`)
.orderBy(
sql`ts_rank(${docsEmbeddings.chunkTextTsv}, plainto_tsquery(${tsConfig}, ${query})) DESC`
)
.limit(candidateLimit)
const seenIds = new Set<string>()
const mergedResults = []
for (let i = 0; i < Math.max(vectorResults.length, keywordResults.length); i++) {
if (i < vectorResults.length && !seenIds.has(vectorResults[i].chunkId)) {
mergedResults.push(vectorResults[i])
seenIds.add(vectorResults[i].chunkId)
}
if (i < keywordResults.length && !seenIds.has(keywordResults[i].chunkId)) {
mergedResults.push(keywordResults[i])
seenIds.add(keywordResults[i].chunkId)
}
}
const filteredResults = mergedResults.slice(0, limit)
const searchResults = filteredResults.map((result) => {
const title = result.headerText || result.sourceDocument.replace('.mdx', '')
const pathParts = result.sourceDocument
.replace('.mdx', '')
.split('/')
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
return {
id: result.chunkId,
type: 'page' as const,
url: result.sourceLink,
content: title,
breadcrumbs: pathParts,
}
})
return NextResponse.json(searchResults)
} catch (error) {
console.error('Semantic search error:', error)
return NextResponse.json([])
}
}

View File

@@ -1,567 +0,0 @@
@import "tailwindcss";
@import "fumadocs-ui/css/neutral.css";
@import "fumadocs-ui/css/preset.css";
/* Prevent overscroll bounce effect on the page */
html,
body {
overscroll-behavior: none;
}
@theme {
--color-fd-primary: #802fff; /* Purple from control-bar component */
--font-geist-sans: var(--font-geist-sans);
--font-geist-mono: var(--font-geist-mono);
}
/* Font family utilities */
.font-sans {
font-family: var(--font-geist-sans), ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
"Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
.font-mono {
font-family: var(--font-geist-mono), ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", "Courier New", monospace;
}
/* Target any potential border classes */
* {
--fd-border-sidebar: transparent !important;
}
/* Override any CSS custom properties for borders */
:root {
--fd-border: transparent !important;
--fd-border-sidebar: transparent !important;
--fd-nav-height: 64px; /* Custom navbar height (h-16 = 4rem = 64px) */
/* Content container width used to center main content */
--spacing-fd-container: 1400px;
/* Edge gutter = leftover space on each side of centered container */
--edge-gutter: max(1rem, calc((100vw - var(--spacing-fd-container)) / 2));
/* Shift the sidebar slightly left from the content edge for extra breathing room */
--sidebar-shift: 90px;
--sidebar-offset: max(0px, calc(var(--edge-gutter) - var(--sidebar-shift)));
/* Shift TOC slightly right to match sidebar spacing for symmetry */
--toc-shift: 90px;
--toc-offset: max(0px, calc(var(--edge-gutter) - var(--toc-shift)));
/* Sidebar and TOC have 20px internal padding - navbar accounts for this directly */
/* Extra gap between sidebar/TOC and the main text content */
--content-gap: 1.75rem;
}
/* Remove custom layout variable overrides to fallback to fumadocs defaults */
/* ============================================
Navbar Light Mode Styling
============================================ */
/* Light mode navbar and search styling */
:root:not(.dark) nav {
background-color: hsla(0, 0%, 96%, 0.85) !important;
}
:root:not(.dark) nav button[type="button"] {
background-color: hsla(0, 0%, 93%, 0.85) !important;
backdrop-filter: blur(33px) saturate(180%) !important;
-webkit-backdrop-filter: blur(33px) saturate(180%) !important;
color: rgba(0, 0, 0, 0.6) !important;
}
:root:not(.dark) nav button[type="button"] kbd {
color: rgba(0, 0, 0, 0.6) !important;
}
/* Dark mode navbar and search styling */
:root.dark nav {
background-color: hsla(0, 0%, 7.04%, 0.92) !important;
backdrop-filter: blur(25px) saturate(180%) brightness(0.6) !important;
-webkit-backdrop-filter: blur(25px) saturate(180%) brightness(0.6) !important;
}
/* ============================================
Custom Sidebar Styling (Turborepo-inspired)
============================================ */
/* Floating sidebar appearance - remove background */
[data-sidebar-container],
#nd-sidebar {
background: transparent !important;
background-color: transparent !important;
border: none !important;
--color-fd-muted: transparent !important;
--color-fd-card: transparent !important;
--color-fd-secondary: transparent !important;
}
aside[data-sidebar],
aside#nd-sidebar {
background: transparent !important;
background-color: transparent !important;
border: none !important;
border-right: none !important;
}
/* Fumadocs v16: Add sidebar placeholder styling for grid area */
[data-sidebar-placeholder] {
background: transparent !important;
}
/* Fumadocs v16: Hide sidebar panel (floating collapse button) */
[data-sidebar-panel] {
display: none !important;
}
/* Mobile only: Reduce gap between navbar and content */
@media (max-width: 1023px) {
#nd-docs-layout {
margin-top: -25px;
}
}
/* Desktop only: Apply custom navbar offset, sidebar width and margin offsets */
/* On mobile, let fumadocs handle the layout natively */
@media (min-width: 1024px) {
:root {
--fd-banner-height: 64px !important;
}
#nd-docs-layout {
--fd-docs-height: calc(100dvh - 64px) !important;
--fd-sidebar-width: 300px !important;
margin-left: var(--sidebar-offset) !important;
margin-right: var(--toc-offset) !important;
}
/* Hide fumadocs nav on desktop - we use custom navbar there */
#nd-docs-layout > header {
display: none !important;
}
}
/* Sidebar spacing - compact like turborepo */
/* Fumadocs v16: [data-sidebar-viewport] doesn't exist, target #nd-sidebar > div instead */
[data-sidebar-viewport],
#nd-sidebar > div {
padding: 0.5rem 12px 12px;
background: transparent !important;
background-color: transparent !important;
}
/* Override sidebar item styling to match Raindrop */
/* Target Link and button elements in sidebar - override Fumadocs itemVariants */
/* Exclude the small chevron-only toggle buttons */
/* Using html prefix for higher specificity over Tailwind v4 utilities */
html #nd-sidebar a,
html #nd-sidebar button:not([aria-label*="ollapse"]):not([aria-label*="xpand"]) {
font-size: 0.9375rem !important; /* 15px to match Raindrop */
line-height: 1.4 !important;
padding: 0.5rem 0.75rem !important; /* More compact like Raindrop */
font-weight: 400 !important;
border-radius: 0.75rem !important; /* More rounded like Raindrop */
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial,
sans-serif !important;
}
/* Dark mode sidebar text */
html.dark #nd-sidebar a,
html.dark #nd-sidebar button:not([aria-label*="ollapse"]):not([aria-label*="xpand"]) {
color: rgba(255, 255, 255, 0.6) !important;
}
/* Light mode sidebar text */
html:not(.dark) #nd-sidebar a,
html:not(.dark) #nd-sidebar button:not([aria-label*="ollapse"]):not([aria-label*="xpand"]) {
color: rgba(0, 0, 0, 0.6) !important;
}
/* Make sure chevron icons are visible and properly styled */
#nd-sidebar svg {
display: inline-block !important;
opacity: 0.6 !important;
flex-shrink: 0 !important;
width: 0.75rem !important;
height: 0.75rem !important;
}
/* Ensure the small chevron toggle buttons are visible */
#nd-sidebar button[aria-label*="ollapse"],
#nd-sidebar button[aria-label*="xpand"] {
display: flex !important;
opacity: 1 !important;
padding: 0.25rem !important;
}
/* Root-level spacing now handled by [data-sidebar-viewport] > * rule below */
/* Add tiny gap between nested items */
#nd-sidebar ul li {
margin-bottom: 0.0625rem !important;
}
#nd-sidebar ul li:last-child {
margin-bottom: 0 !important;
}
/* Section headers should be slightly larger */
/* Fumadocs v16: Also target #nd-sidebar for compatibility */
[data-sidebar-viewport] [data-separator],
#nd-sidebar [data-separator],
#nd-sidebar p {
font-size: 0.75rem !important;
font-weight: 600 !important;
text-transform: uppercase !important;
letter-spacing: 0.05em !important;
}
/* Override active state (NO PURPLE) */
#nd-sidebar a[data-active="true"],
#nd-sidebar button[data-active="true"],
#nd-sidebar a.bg-fd-primary\/10,
#nd-sidebar a.text-fd-primary,
#nd-sidebar a[class*="bg-fd-primary"],
#nd-sidebar a[class*="text-fd-primary"],
/* Override custom sidebar purple classes */
#nd-sidebar
a.bg-purple-50\/80,
#nd-sidebar a.text-purple-600,
#nd-sidebar a[class*="bg-purple"],
#nd-sidebar a[class*="text-purple"] {
background-image: none !important;
}
/* Dark mode active state */
html.dark #nd-sidebar a[data-active="true"],
html.dark #nd-sidebar button[data-active="true"],
html.dark #nd-sidebar a.bg-fd-primary\/10,
html.dark #nd-sidebar a.text-fd-primary,
html.dark #nd-sidebar a[class*="bg-fd-primary"],
html.dark #nd-sidebar a[class*="text-fd-primary"],
html.dark #nd-sidebar a.bg-purple-50\/80,
html.dark #nd-sidebar a.text-purple-600,
html.dark #nd-sidebar a[class*="bg-purple"],
html.dark #nd-sidebar a[class*="text-purple"] {
background-color: rgba(255, 255, 255, 0.15) !important;
color: rgba(255, 255, 255, 1) !important;
}
/* Light mode active state */
html:not(.dark) #nd-sidebar a[data-active="true"],
html:not(.dark) #nd-sidebar button[data-active="true"],
html:not(.dark) #nd-sidebar a.bg-fd-primary\/10,
html:not(.dark) #nd-sidebar a.text-fd-primary,
html:not(.dark) #nd-sidebar a[class*="bg-fd-primary"],
html:not(.dark) #nd-sidebar a[class*="text-fd-primary"],
html:not(.dark) #nd-sidebar a.bg-purple-50\/80,
html:not(.dark) #nd-sidebar a.text-purple-600,
html:not(.dark) #nd-sidebar a[class*="bg-purple"],
html:not(.dark) #nd-sidebar a[class*="text-purple"] {
background-color: rgba(0, 0, 0, 0.07) !important;
color: rgba(0, 0, 0, 0.9) !important;
}
/* Dark mode hover state */
html.dark #nd-sidebar a:hover:not([data-active="true"]),
html.dark #nd-sidebar button:hover:not([data-active="true"]) {
background-color: rgba(255, 255, 255, 0.08) !important;
}
/* Light mode hover state */
html:not(.dark) #nd-sidebar a:hover:not([data-active="true"]),
html:not(.dark) #nd-sidebar button:hover:not([data-active="true"]) {
background-color: rgba(0, 0, 0, 0.03) !important;
}
/* Dark mode - ensure active/selected items don't change on hover */
html.dark #nd-sidebar a.bg-purple-50\/80:hover,
html.dark #nd-sidebar a[class*="bg-purple"]:hover,
html.dark #nd-sidebar a[data-active="true"]:hover,
html.dark #nd-sidebar button[data-active="true"]:hover {
background-color: rgba(255, 255, 255, 0.15) !important;
color: rgba(255, 255, 255, 1) !important;
}
/* Light mode - ensure active/selected items don't change on hover */
html:not(.dark) #nd-sidebar a.bg-purple-50\/80:hover,
html:not(.dark) #nd-sidebar a[class*="bg-purple"]:hover,
html:not(.dark) #nd-sidebar a[data-active="true"]:hover,
html:not(.dark) #nd-sidebar button[data-active="true"]:hover {
background-color: rgba(0, 0, 0, 0.07) !important;
color: rgba(0, 0, 0, 0.9) !important;
}
/* Hide search, platform, and collapse button from sidebar completely */
[data-sidebar] [data-search],
[data-sidebar] .search-toggle,
#nd-sidebar [data-search],
#nd-sidebar .search-toggle,
[data-sidebar-viewport] [data-search],
[data-sidebar-viewport] button[data-search],
aside[data-sidebar] [role="button"]:has([data-search]),
aside[data-sidebar] > div > button:first-child,
#nd-sidebar > div > button:first-child,
[data-sidebar] a[href*="sim.ai"],
#nd-sidebar a[href*="sim.ai"],
[data-sidebar-viewport] a[href*="sim.ai"],
/* Hide search buttons (but NOT folder chevron buttons) */
aside[data-sidebar] > div:first-child
> button:not([aria-label="Collapse"]):not([aria-label="Expand"]),
#nd-sidebar > div:first-child > button:not([aria-label="Collapse"]):not([aria-label="Expand"]),
/* Hide sidebar collapse button (panel icon) - direct children only */
aside[data-sidebar] > button:first-of-type:not([aria-label="Collapse"]):not([aria-label="Expand"]),
[data-sidebar]
> button[type="button"]:first-of-type:not([aria-label="Collapse"]):not([aria-label="Expand"]),
button[data-collapse]:not([aria-label="Collapse"]):not([aria-label="Expand"]),
[data-sidebar-header] button,
/* Hide theme toggle from sidebar footer */
aside[data-sidebar] [data-theme-toggle],
[data-sidebar-footer],
[data-sidebar] footer,
footer button[aria-label*="heme"],
aside[data-sidebar] > div:last-child:has(button[aria-label*="heme"]),
aside[data-sidebar] button[aria-label*="heme"],
[data-sidebar] button[aria-label*="Theme"],
/* Additional theme toggle selectors */
aside[data-sidebar] > *:last-child
button,
[data-sidebar-viewport] ~ *,
aside[data-sidebar] > div:not([data-sidebar-viewport]),
/* Aggressive theme toggle hiding */
aside[data-sidebar] svg[class*="sun"],
aside[data-sidebar] svg[class*="moon"],
aside[data-sidebar] button[type="button"]:last-child,
aside button:has(svg:only-child),
[data-sidebar] div:has(> button[type="button"]:only-child:last-child),
/* Hide theme toggle and other non-content elements */
aside[data-sidebar] > *:not([data-sidebar-viewport]) {
display: none !important;
visibility: hidden !important;
opacity: 0 !important;
height: 0 !important;
max-height: 0 !important;
overflow: hidden !important;
pointer-events: none !important;
position: absolute !important;
left: -9999px !important;
}
/* Desktop only: Hide sidebar toggle buttons and nav title/logo (keep visible on mobile) */
@media (min-width: 1025px) {
[data-sidebar-container] > button,
[data-sidebar-container] [data-toggle],
aside[data-sidebar] [data-sidebar-toggle],
button[data-sidebar-toggle],
nav button[data-sidebar-toggle],
button[aria-label="Toggle Sidebar"],
button[aria-label="Collapse Sidebar"],
/* Hide nav title/logo in sidebar on desktop - target all possible locations */
aside[data-sidebar] a[href="/"],
aside[data-sidebar] a[href="/"] img,
aside[data-sidebar] > a:first-child,
aside[data-sidebar] > div > a:first-child,
aside[data-sidebar] img[alt="Sim"],
[data-sidebar-header],
[data-sidebar] [data-title],
#nd-sidebar > a:first-child,
#nd-sidebar > div:first-child > a:first-child,
#nd-sidebar img[alt="Sim"],
/* Hide theme toggle at bottom of sidebar on desktop */
#nd-sidebar
> footer,
#nd-sidebar footer,
aside#nd-sidebar > *:last-child:not(div),
#nd-sidebar > button:last-child,
#nd-sidebar button[aria-label*="theme" i],
#nd-sidebar button[aria-label*="Theme"],
#nd-sidebar > div:last-child > button {
display: none !important;
visibility: hidden !important;
height: 0 !important;
max-height: 0 !important;
overflow: hidden !important;
}
}
/* Extra aggressive - hide everything after the viewport */
aside[data-sidebar] [data-sidebar-viewport] ~ * {
display: none !important;
}
/* Tighter spacing for sidebar content */
[data-sidebar-viewport] > * {
margin-bottom: 0.0625rem;
}
[data-sidebar-viewport] > *:last-child {
margin-bottom: 0;
}
[data-sidebar-viewport] ul {
margin: 0;
padding: 0;
}
/* Ensure sidebar starts with content immediately */
aside[data-sidebar] > div:first-child {
padding-top: 0;
}
/* Remove all sidebar borders and backgrounds */
[data-sidebar-container],
aside[data-sidebar],
[data-sidebar],
[data-sidebar] *,
#nd-sidebar,
#nd-sidebar * {
border: none !important;
border-right: none !important;
border-left: none !important;
border-top: none !important;
border-bottom: none !important;
}
/* Override fumadocs background colors for sidebar */
.dark #nd-sidebar,
.dark [data-sidebar-container],
.dark aside[data-sidebar] {
--color-fd-muted: transparent !important;
--color-fd-secondary: transparent !important;
background: transparent !important;
background-color: transparent !important;
}
/* Force normal text flow in sidebar */
[data-sidebar],
[data-sidebar] *,
[data-sidebar-viewport],
[data-sidebar-viewport] * {
writing-mode: horizontal-tb !important;
}
/* ============================================
Code Block Styling (Improved)
============================================ */
/* Apply Geist Mono to code elements */
code,
pre,
pre code {
font-family: var(--font-geist-mono), ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", "Courier New", monospace;
}
/* Inline code */
:not(pre) > code {
padding: 0.2em 0.4em;
border-radius: 0.25rem;
font-size: 0.875em;
font-weight: 500;
}
/* Light mode inline code */
:root:not(.dark) :not(pre) > code {
background-color: rgb(243 244 246);
color: rgb(220 38 38);
border: 1px solid rgb(229 231 235);
}
/* Dark mode inline code */
.dark :not(pre) > code {
background-color: rgb(31 41 55);
color: rgb(248 113 113);
border: 1px solid rgb(55 65 81);
}
/* Code block container improvements */
pre {
font-size: 0.875rem;
line-height: 1.7;
tab-size: 2;
-webkit-overflow-scrolling: touch;
}
pre code {
display: block;
width: fit-content;
min-width: 100%;
}
/* Syntax highlighting adjustments for better readability */
pre code .line {
padding-left: 0;
padding-right: 0;
}
/* Custom text highlighting styles */
.text-highlight {
color: var(--color-fd-primary);
}
/* Override marker color for highlighted lists */
.highlight-markers li::marker {
color: var(--color-fd-primary);
}
/* Add bottom spacing to prevent abrupt page endings */
[data-content] {
padding-top: 1.5rem !important;
padding-bottom: 4rem;
}
/* Alternative fallback for different Fumadocs versions */
main article,
.docs-page main {
padding-top: 1.5rem !important;
padding-bottom: 4rem;
}
/* ============================================
Center and Constrain Main Content Width
============================================ */
/* Main content area - center and constrain like turborepo/raindrop */
/* Note: --sidebar-offset and --toc-offset are now applied at #nd-docs-layout level */
main[data-main] {
max-width: var(--spacing-fd-container, 1400px);
margin-left: auto;
margin-right: auto;
padding-top: 1rem;
padding-left: var(--content-gap);
padding-right: var(--content-gap);
order: 1 !important;
}
/* Adjust for smaller screens */
@media (max-width: 768px) {
main[data-main] {
padding-left: 1rem;
padding-right: 1rem;
}
}
/* Ensure docs page content is properly constrained */
[data-docs-page] {
max-width: 1400px;
margin-left: auto;
margin-right: auto;
padding-top: 1.5rem !important;
}
/* Override Fumadocs default content padding */
article[data-content],
div[data-content] {
padding-top: 1.5rem !important;
}
/* Remove any unwanted borders/outlines from video elements */
video {
outline: none !important;
border-style: solid !important;
}
/* Tailwind v4 content sources */
@source '../app/**/*.{js,ts,jsx,tsx,mdx}';
@source '../components/**/*.{js,ts,jsx,tsx,mdx}';
@source '../content/**/*.{js,ts,jsx,tsx,mdx}';
@source '../mdx-components.tsx';
@source '../node_modules/fumadocs-ui/dist/**/*.js';

View File

@@ -1,21 +0,0 @@
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared'
/**
* Shared layout configurations
*
* you can customise layouts individually from:
* Home Layout: app/(home)/layout.tsx
* Docs Layout: app/docs/layout.tsx
*/
export const baseOptions: BaseLayoutProps = {
nav: {
title: (
<>
<svg width='24' height='24' xmlns='http://www.w3.org/2000/svg' aria-label='Logo'>
<circle cx={12} cy={12} r={12} fill='currentColor' />
</svg>
My App
</>
),
},
}

View File

@@ -1,100 +0,0 @@
import type { ReactNode } from 'react'
export default function RootLayout({ children }: { children: ReactNode }) {
return children
}
export const metadata = {
metadataBase: new URL('https://docs.sim.ai'),
title: {
default: 'Sim Documentation - Visual Workflow Builder for AI Applications',
template: '%s',
},
description:
'Comprehensive documentation for Sim - the visual workflow builder for AI applications. Create powerful AI agents, automation workflows, and data processing pipelines by connecting blocks on a canvas—no coding required.',
keywords: [
'AI workflow builder',
'visual workflow editor',
'AI automation',
'workflow automation',
'AI agents',
'no-code AI',
'drag and drop workflows',
'AI integrations',
'workflow canvas',
'AI Agent Workflow Builder',
'workflow orchestration',
'agent builder',
'AI workflow automation',
'visual programming',
],
authors: [{ name: 'Sim Team', url: 'https://sim.ai' }],
creator: 'Sim',
publisher: 'Sim',
category: 'Developer Tools',
classification: 'Developer Documentation',
manifest: '/favicon/site.webmanifest',
icons: {
icon: [
{ url: '/favicon/favicon-16x16.png', sizes: '16x16', type: 'image/png' },
{ url: '/favicon/favicon-32x32.png', sizes: '32x32', type: 'image/png' },
],
apple: '/favicon/apple-touch-icon.png',
shortcut: '/favicon/favicon.ico',
},
appleWebApp: {
capable: true,
statusBarStyle: 'default',
title: 'Sim Docs',
},
openGraph: {
type: 'website',
locale: 'en_US',
alternateLocale: ['es_ES', 'fr_FR', 'de_DE', 'ja_JP', 'zh_CN'],
url: 'https://docs.sim.ai',
siteName: 'Sim Documentation',
title: 'Sim Documentation - Visual Workflow Builder for AI Applications',
description:
'Comprehensive documentation for Sim - the visual workflow builder for AI applications. Create powerful AI agents, automation workflows, and data processing pipelines.',
images: [
{
url: 'https://docs.sim.ai/api/og?title=Sim%20Documentation',
width: 1200,
height: 630,
alt: 'Sim Documentation',
},
],
},
twitter: {
card: 'summary_large_image',
title: 'Sim Documentation - Visual Workflow Builder for AI Applications',
description:
'Comprehensive documentation for Sim - the visual workflow builder for AI applications.',
creator: '@simdotai',
site: '@simdotai',
images: ['https://docs.sim.ai/api/og?title=Sim%20Documentation'],
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
'max-video-preview': -1,
'max-image-preview': 'large',
'max-snippet': -1,
},
},
alternates: {
canonical: 'https://docs.sim.ai',
languages: {
'x-default': 'https://docs.sim.ai',
en: 'https://docs.sim.ai',
es: 'https://docs.sim.ai/es',
fr: 'https://docs.sim.ai/fr',
de: 'https://docs.sim.ai/de',
ja: 'https://docs.sim.ai/ja',
zh: 'https://docs.sim.ai/zh',
},
},
}

View File

@@ -1,31 +0,0 @@
import { getLLMText } from '@/lib/llms'
import { source } from '@/lib/source'
export const revalidate = false
export async function GET() {
try {
const pages = source.getPages().filter((page) => {
if (!page || !page.data || !page.url) return false
const pathParts = page.url.split('/').filter(Boolean)
const hasLangPrefix = pathParts[0] && ['es', 'fr', 'de', 'ja', 'zh'].includes(pathParts[0])
return !hasLangPrefix
})
const scan = pages.map((page) => getLLMText(page))
const scanned = await Promise.all(scan)
const filtered = scanned.filter((text) => text && text.length > 0)
return new Response(filtered.join('\n\n---\n\n'), {
headers: {
'Content-Type': 'text/plain; charset=utf-8',
},
})
} catch (error) {
console.error('Error generating LLM full text:', error)
return new Response('Error generating full documentation text', { status: 500 })
}
}

View File

@@ -1,35 +0,0 @@
import { notFound } from 'next/navigation'
import { type NextRequest, NextResponse } from 'next/server'
import { i18n } from '@/lib/i18n'
import { getLLMText } from '@/lib/llms'
import { source } from '@/lib/source'
export const revalidate = false
export async function GET(
_request: NextRequest,
{ params }: { params: Promise<{ slug?: string[] }> }
) {
const { slug } = await params
let lang: (typeof i18n.languages)[number] = i18n.defaultLanguage
let pageSlug = slug
if (slug && slug.length > 0 && i18n.languages.includes(slug[0] as typeof lang)) {
lang = slug[0] as typeof lang
pageSlug = slug.slice(1)
}
const page = source.getPage(pageSlug, lang)
if (!page) notFound()
return new NextResponse(await getLLMText(page), {
headers: {
'Content-Type': 'text/markdown',
},
})
}
export function generateStaticParams() {
return source.generateParams()
}

View File

@@ -1,87 +0,0 @@
import { source } from '@/lib/source'
export const revalidate = false
export async function GET() {
const baseUrl = 'https://docs.sim.ai'
try {
const pages = source.getPages().filter((page) => {
if (!page || !page.data || !page.url) return false
const pathParts = page.url.split('/').filter(Boolean)
const hasLangPrefix = pathParts[0] && ['es', 'fr', 'de', 'ja', 'zh'].includes(pathParts[0])
return !hasLangPrefix
})
const sections: Record<string, Array<{ title: string; url: string; description?: string }>> = {}
pages.forEach((page) => {
const pathParts = page.url.split('/').filter(Boolean)
const section =
pathParts[0] && ['en', 'es', 'fr', 'de', 'ja', 'zh'].includes(pathParts[0])
? pathParts[1] || 'root'
: pathParts[0] || 'root'
if (!sections[section]) {
sections[section] = []
}
sections[section].push({
title: page.data.title || 'Untitled',
url: `${baseUrl}${page.url}`,
description: page.data.description,
})
})
const manifest = `# Sim Documentation
> Visual Workflow Builder for AI Applications
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.
## Documentation Overview
This file provides an overview of our documentation. For full content of all pages, see ${baseUrl}/llms-full.txt
## Main Sections
${Object.entries(sections)
.map(([section, items]) => {
const sectionTitle = section
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ')
return `### ${sectionTitle}\n\n${items.map((item) => `- ${item.title}: ${item.url}${item.description ? `\n ${item.description}` : ''}`).join('\n')}`
})
.join('\n\n')}
## Additional Resources
- Full documentation content: ${baseUrl}/llms-full.txt
- Individual page content: ${baseUrl}/llms.mdx/[page-path]
- API documentation: ${baseUrl}/sdks/
- Tool integrations: ${baseUrl}/tools/
## Statistics
- Total pages: ${pages.length} (English only)
- Other languages available at: ${baseUrl}/[lang]/ (es, fr, de, ja, zh)
---
Generated: ${new Date().toISOString()}
Format: llms.txt v0.1.0
See: https://llmstxt.org for specification`
return new Response(manifest, {
headers: {
'Content-Type': 'text/plain; charset=utf-8',
},
})
} catch (error) {
console.error('Error generating LLM manifest:', error)
return new Response('Error generating documentation manifest', { status: 500 })
}
}

View File

@@ -1,100 +0,0 @@
export const revalidate = false
export async function GET() {
const baseUrl = 'https://docs.sim.ai'
const robotsTxt = `# Robots.txt for Sim Documentation
# Generated on ${new Date().toISOString()}
User-agent: *
Allow: /
# Search engine crawlers
User-agent: Googlebot
Allow: /
User-agent: Bingbot
Allow: /
User-agent: Slurp
Allow: /
User-agent: DuckDuckBot
Allow: /
User-agent: Baiduspider
Allow: /
User-agent: YandexBot
Allow: /
# AI and LLM crawlers - explicitly allowed for documentation indexing
User-agent: GPTBot
Allow: /
User-agent: ChatGPT-User
Allow: /
User-agent: CCBot
Allow: /
User-agent: anthropic-ai
Allow: /
User-agent: Claude-Web
Allow: /
User-agent: Applebot
Allow: /
User-agent: PerplexityBot
Allow: /
User-agent: Diffbot
Allow: /
User-agent: FacebookBot
Allow: /
User-agent: cohere-ai
Allow: /
# Disallow admin and internal paths (if any exist)
Disallow: /.next/
Disallow: /api/internal/
Disallow: /_next/static/
Disallow: /admin/
# Allow but don't prioritize these
Allow: /api/search
Allow: /llms.txt
Allow: /llms-full.txt
Allow: /llms.mdx/
# Sitemaps
Sitemap: ${baseUrl}/sitemap.xml
# Crawl delay for aggressive bots (optional)
# Crawl-delay: 1
# Additional resources for AI indexing
# See https://github.com/AnswerDotAI/llms-txt for more info
# LLM-friendly content:
# Manifest: ${baseUrl}/llms.txt
# Full content: ${baseUrl}/llms-full.txt
# Individual pages: ${baseUrl}/llms.mdx/[page-path]
# Multi-language documentation available at:
# ${baseUrl}/en - English
# ${baseUrl}/es - Español
# ${baseUrl}/fr - Français
# ${baseUrl}/de - Deutsch
# ${baseUrl}/ja - 日本語
# ${baseUrl}/zh - 简体中文`
return new Response(robotsTxt, {
headers: {
'Content-Type': 'text/plain',
},
})
}

View File

@@ -1,63 +0,0 @@
import { i18n } from '@/lib/i18n'
import { source } from '@/lib/source'
export const revalidate = false
export async function GET() {
const baseUrl = 'https://docs.sim.ai'
const allPages = source.getPages()
const getPriority = (url: string): string => {
if (url === '/introduction' || url === '/') return '1.0'
if (url === '/getting-started') return '0.9'
if (url.match(/^\/[^/]+$/)) return '0.8'
if (url.includes('/sdks/') || url.includes('/tools/')) return '0.7'
return '0.6'
}
const urls = allPages
.flatMap((page) => {
const urlWithoutLang = page.url.replace(/^\/[a-z]{2}\//, '/')
return i18n.languages.map((lang) => {
const url =
lang === i18n.defaultLanguage
? `${baseUrl}${urlWithoutLang}`
: `${baseUrl}/${lang}${urlWithoutLang}`
return ` <url>
<loc>${url}</loc>
<lastmod>${new Date().toISOString().split('T')[0]}</lastmod>
<changefreq>weekly</changefreq>
<priority>${getPriority(urlWithoutLang)}</priority>
${i18n.languages.length > 1 ? generateAlternateLinks(baseUrl, urlWithoutLang) : ''}
</url>`
})
})
.join('\n')
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">
${urls}
</urlset>`
return new Response(sitemap, {
headers: {
'Content-Type': 'application/xml',
'Cache-Control': 'public, max-age=3600, s-maxage=3600',
},
})
}
function generateAlternateLinks(baseUrl: string, urlWithoutLang: string): string {
return i18n.languages
.map((lang) => {
const url =
lang === i18n.defaultLanguage
? `${baseUrl}${urlWithoutLang}`
: `${baseUrl}/${lang}${urlWithoutLang}`
return ` <xhtml:link rel="alternate" hreflang="${lang}" href="${url}" />`
})
.join('\n')
}

View File

@@ -1,11 +0,0 @@
{
"aliases": {
"uiDir": "./components/ui",
"componentsDir": "./components",
"blockDir": "./components",
"cssDir": "./styles",
"libDir": "./lib"
},
"baseDir": "",
"commands": {}
}

View File

@@ -1,42 +0,0 @@
'use client'
import { ChevronLeft, ChevronRight } from 'lucide-react'
import Link from 'next/link'
interface PageNavigationArrowsProps {
previous?: {
url: string
}
next?: {
url: string
}
}
export function PageNavigationArrows({ previous, next }: PageNavigationArrowsProps) {
if (!previous && !next) return null
return (
<div className='flex items-center gap-2'>
{previous && (
<Link
href={previous.url}
className='inline-flex items-center justify-center gap-1.5 rounded-lg border border-border/40 bg-background px-2.5 py-1.5 text-muted-foreground/60 text-sm transition-all hover:border-border hover:bg-accent/50 hover:text-muted-foreground'
aria-label='Previous page'
title='Previous page'
>
<ChevronLeft className='h-4 w-4' />
</Link>
)}
{next && (
<Link
href={next.url}
className='inline-flex items-center justify-center gap-1.5 rounded-lg border border-border/40 bg-background px-2.5 py-1.5 text-muted-foreground/60 text-sm transition-all hover:border-border hover:bg-accent/50 hover:text-muted-foreground'
aria-label='Next page'
title='Next page'
>
<ChevronRight className='h-4 w-4' />
</Link>
)}
</div>
)
}

View File

@@ -1,206 +0,0 @@
'use client'
import { type ReactNode, useEffect, useState } from 'react'
import type { Folder, Item, Separator } from 'fumadocs-core/page-tree'
import { ChevronRight } from 'lucide-react'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { cn } from '@/lib/utils'
const LANG_PREFIXES = ['/en', '/es', '/fr', '/de', '/ja', '/zh']
function stripLangPrefix(path: string): string {
for (const prefix of LANG_PREFIXES) {
if (path === prefix) return '/'
if (path.startsWith(`${prefix}/`)) return path.slice(prefix.length)
}
return path
}
function isActive(url: string, pathname: string, nested = true): boolean {
const normalizedPathname = stripLangPrefix(pathname)
const normalizedUrl = stripLangPrefix(url)
return (
normalizedUrl === normalizedPathname ||
(nested && normalizedPathname.startsWith(`${normalizedUrl}/`))
)
}
export function SidebarItem({ item }: { item: Item }) {
const pathname = usePathname()
const active = isActive(item.url, pathname, false)
return (
<Link
href={item.url}
data-active={active}
className={cn(
// Mobile styles (default)
'flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors',
'text-fd-muted-foreground hover:bg-fd-accent/50 hover:text-fd-accent-foreground',
active && 'bg-fd-primary/10 font-medium text-fd-primary',
// Desktop styles (lg+)
'lg:mb-[0.0625rem] lg:block lg:rounded-md lg:px-2.5 lg:py-1.5 lg:font-normal lg:text-[13px] lg:leading-tight',
'lg:text-gray-600 lg:dark:text-gray-400',
!active && 'lg:hover:bg-gray-100/60 lg:dark:hover:bg-gray-800/40',
active &&
'lg:bg-purple-50/80 lg:font-normal lg:text-purple-600 lg:dark:bg-purple-900/15 lg:dark:text-purple-400'
)}
>
{item.name}
</Link>
)
}
export function SidebarFolder({ item, children }: { item: Folder; children: ReactNode }) {
const pathname = usePathname()
const hasActiveChild = checkHasActiveChild(item, pathname)
const hasChildren = item.children.length > 0
const [open, setOpen] = useState(hasActiveChild)
useEffect(() => {
setOpen(hasActiveChild)
}, [hasActiveChild])
const active = item.index ? isActive(item.index.url, pathname, false) : false
if (item.index && !hasChildren) {
return (
<Link
href={item.index.url}
data-active={active}
className={cn(
// Mobile styles (default)
'flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors',
'text-fd-muted-foreground hover:bg-fd-accent/50 hover:text-fd-accent-foreground',
active && 'bg-fd-primary/10 font-medium text-fd-primary',
// Desktop styles (lg+)
'lg:mb-[0.0625rem] lg:block lg:rounded-md lg:px-2.5 lg:py-1.5 lg:font-normal lg:text-[13px] lg:leading-tight',
'lg:text-gray-600 lg:dark:text-gray-400',
!active && 'lg:hover:bg-gray-100/60 lg:dark:hover:bg-gray-800/40',
active &&
'lg:bg-purple-50/80 lg:font-normal lg:text-purple-600 lg:dark:bg-purple-900/15 lg:dark:text-purple-400'
)}
>
{item.name}
</Link>
)
}
return (
<div className='flex flex-col lg:mb-[0.0625rem]'>
<div className='flex w-full items-center lg:gap-0.5'>
{item.index ? (
<Link
href={item.index.url}
data-active={active}
className={cn(
// Mobile styles (default)
'flex flex-1 items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors',
'text-fd-muted-foreground hover:bg-fd-accent/50 hover:text-fd-accent-foreground',
active && 'bg-fd-primary/10 font-medium text-fd-primary',
// Desktop styles (lg+)
'lg:block lg:flex-1 lg:rounded-md lg:px-2.5 lg:py-1.5 lg:font-medium lg:text-[13px] lg:leading-tight',
'lg:text-gray-800 lg:dark:text-gray-200',
!active && 'lg:hover:bg-gray-100/60 lg:dark:hover:bg-gray-800/40',
active &&
'lg:bg-purple-50/80 lg:text-purple-600 lg:dark:bg-purple-900/15 lg:dark:text-purple-400'
)}
>
{item.name}
</Link>
) : (
<button
onClick={() => setOpen(!open)}
className={cn(
// Mobile styles (default)
'flex flex-1 items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors',
'text-fd-muted-foreground hover:bg-fd-accent/50',
// Desktop styles (lg+)
'lg:flex lg:w-full lg:cursor-pointer lg:items-center lg:justify-between lg:rounded-md lg:px-2.5 lg:py-1.5 lg:text-left lg:font-medium lg:text-[13px] lg:leading-tight',
'lg:text-gray-800 lg:hover:bg-gray-100/60 lg:dark:text-gray-200 lg:dark:hover:bg-gray-800/40'
)}
>
<span>{item.name}</span>
{/* Desktop-only chevron for non-index folders */}
<ChevronRight
className={cn(
'ml-auto hidden h-3 w-3 flex-shrink-0 text-gray-400 transition-transform duration-200 ease-in-out lg:block dark:text-gray-500',
open && 'rotate-90'
)}
/>
</button>
)}
{hasChildren && (
<button
onClick={() => setOpen(!open)}
className={cn(
// Mobile styles
'rounded p-1 hover:bg-fd-accent/50',
// Desktop styles
'lg:cursor-pointer lg:rounded lg:p-1 lg:transition-colors lg:hover:bg-gray-100/60 lg:dark:hover:bg-gray-800/40'
)}
aria-label={open ? 'Collapse' : 'Expand'}
>
<ChevronRight
className={cn(
// Mobile styles
'h-4 w-4 transition-transform',
// Desktop styles
'lg:h-3 lg:w-3 lg:text-gray-400 lg:duration-200 lg:ease-in-out lg:dark:text-gray-500',
open && 'rotate-90'
)}
/>
</button>
)}
</div>
{hasChildren && (
<div
className={cn(
'overflow-hidden transition-all duration-200 ease-in-out',
open ? 'max-h-[10000px] opacity-100' : 'max-h-0 opacity-0'
)}
>
{/* Mobile: simple indent */}
<div className='ml-4 flex flex-col gap-0.5 lg:hidden'>{children}</div>
{/* Desktop: styled with border */}
<ul className='mt-0.5 ml-2 hidden space-y-[0.0625rem] border-gray-200/60 border-l pl-2.5 lg:block dark:border-gray-700/60'>
{children}
</ul>
</div>
)}
</div>
)
}
export function SidebarSeparator({ item }: { item: Separator }) {
return (
<p
className={cn(
// Mobile styles
'mt-4 mb-2 px-2 font-medium text-fd-muted-foreground text-xs',
// Desktop styles
'lg:mt-4 lg:mb-1.5 lg:px-2.5 lg:font-semibold lg:text-[10px] lg:text-gray-500/80 lg:uppercase lg:tracking-wide lg:dark:text-gray-500'
)}
>
{item.name}
</p>
)
}
function checkHasActiveChild(node: Folder, pathname: string): boolean {
if (node.index && isActive(node.index.url, pathname)) {
return true
}
for (const child of node.children) {
if (child.type === 'page' && isActive(child.url, pathname)) {
return true
}
if (child.type === 'folder' && checkHasActiveChild(child, pathname)) {
return true
}
}
return false
}

View File

@@ -1,41 +0,0 @@
'use client'
import { useState } from 'react'
import { ArrowRight, ChevronRight } from 'lucide-react'
import Link from 'next/link'
export function TOCFooter() {
const [isHovered, setIsHovered] = useState(false)
return (
<div className='sticky bottom-0 mt-6'>
<div className='flex flex-col gap-2 rounded-lg border border-border bg-secondary p-6 text-sm'>
<div className='text-balance font-semibold text-base leading-tight'>
Start building today
</div>
<div className='text-muted-foreground'>Trusted by over 60,000 builders.</div>
<div className='text-muted-foreground'>
Build Agentic workflows visually on a drag-and-drop canvas or with natural language.
</div>
<Link
href='https://sim.ai/signup'
target='_blank'
rel='noopener noreferrer'
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
className='group mt-2 inline-flex h-8 w-fit items-center justify-center gap-1 whitespace-nowrap rounded-[10px] border border-[#6F3DFA] bg-gradient-to-b from-[#8357FF] to-[#6F3DFA] px-3 pr-[10px] pl-[12px] font-medium text-sm text-white shadow-[inset_0_2px_4px_0_#9B77FF] outline-none transition-all hover:shadow-lg focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50'
aria-label='Get started with Sim - Sign up for free'
>
<span>Get started</span>
<span className='inline-flex transition-transform duration-200 group-hover:translate-x-0.5'>
{isHovered ? (
<ArrowRight className='h-4 w-4' aria-hidden='true' />
) : (
<ChevronRight className='h-4 w-4' aria-hidden='true' />
)}
</span>
</Link>
</div>
</div>
)
}

File diff suppressed because one or more lines are too long

View File

@@ -1,66 +0,0 @@
'use client'
import Image from 'next/image'
import Link from 'next/link'
import { LanguageDropdown } from '@/components/ui/language-dropdown'
import { SearchTrigger } from '@/components/ui/search-trigger'
import { ThemeToggle } from '@/components/ui/theme-toggle'
export function Navbar() {
return (
<nav
className='sticky top-0 z-50 border-border/50 border-b'
style={{
backdropFilter: 'blur(25px) saturate(180%)',
WebkitBackdropFilter: 'blur(25px) saturate(180%)',
}}
>
{/* Desktop: Single row layout */}
<div className='hidden h-16 w-full items-center lg:flex'>
<div
className='relative flex w-full items-center justify-between'
style={{
paddingLeft: 'calc(var(--sidebar-offset) + 32px)',
paddingRight: 'calc(var(--toc-offset) + 60px)',
}}
>
{/* Left cluster: logo */}
<div className='flex items-center'>
<Link href='/' className='flex min-w-[100px] items-center'>
<Image
src='/static/logo.png'
alt='Sim'
width={72}
height={28}
className='h-7 w-auto'
/>
</Link>
</div>
{/* Center cluster: search - absolutely positioned to center */}
<div className='-translate-x-1/2 absolute left-1/2 flex items-center justify-center'>
<SearchTrigger />
</div>
{/* Right cluster aligns with TOC edge */}
<div className='flex items-center gap-4'>
<Link
href='https://sim.ai'
target='_blank'
rel='noopener noreferrer'
className='rounded-xl px-3 py-2 font-normal text-[0.9375rem] text-foreground/60 leading-[1.4] transition-colors hover:bg-foreground/8 hover:text-foreground'
style={{
fontFamily:
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
}}
>
Platform
</Link>
<LanguageDropdown />
<ThemeToggle />
</div>
</div>
</div>
</nav>
)
}

View File

@@ -1,60 +0,0 @@
'use client'
import { useState } from 'react'
import { useCopyButton } from 'fumadocs-ui/utils/use-copy-button'
import { Check, Copy } from 'lucide-react'
const cache = new Map<string, string>()
export function LLMCopyButton({
markdownUrl,
}: {
/**
* A URL to fetch the raw Markdown/MDX content of page
*/
markdownUrl: string
}) {
const [isLoading, setLoading] = useState(false)
const [checked, onClick] = useCopyButton(async () => {
const cached = cache.get(markdownUrl)
if (cached) return navigator.clipboard.writeText(cached)
setLoading(true)
try {
await navigator.clipboard.write([
new ClipboardItem({
'text/plain': fetch(markdownUrl).then(async (res) => {
const content = await res.text()
cache.set(markdownUrl, content)
return content
}),
}),
])
} finally {
setLoading(false)
}
})
return (
<button
disabled={isLoading}
onClick={onClick}
className='flex cursor-pointer items-center gap-1.5 rounded-lg border border-border/40 bg-background px-2.5 py-2 text-muted-foreground/60 text-sm leading-none transition-all hover:border-border hover:bg-accent/50 hover:text-muted-foreground'
aria-label={checked ? 'Copied to clipboard' : 'Copy page content'}
>
{checked ? (
<>
<Check className='h-3.5 w-3.5' />
<span>Copied</span>
</>
) : (
<>
<Copy className='h-3.5 w-3.5' />
<span>Copy page</span>
</>
)}
</button>
)
}

View File

@@ -1,174 +0,0 @@
import Script from 'next/script'
interface StructuredDataProps {
title: string
description: string
url: string
lang: string
dateModified?: string
breadcrumb?: Array<{ name: string; url: string }>
}
export function StructuredData({
title,
description,
url,
lang,
dateModified,
breadcrumb,
}: StructuredDataProps) {
const baseUrl = 'https://docs.sim.ai'
const articleStructuredData = {
'@context': 'https://schema.org',
'@type': 'TechArticle',
headline: title,
description: description,
url: url,
datePublished: dateModified || new Date().toISOString(),
dateModified: dateModified || new Date().toISOString(),
author: {
'@type': 'Organization',
name: 'Sim Team',
url: baseUrl,
},
publisher: {
'@type': 'Organization',
name: 'Sim',
url: baseUrl,
logo: {
'@type': 'ImageObject',
url: `${baseUrl}/static/logo.png`,
},
},
mainEntityOfPage: {
'@type': 'WebPage',
'@id': url,
},
inLanguage: lang,
isPartOf: {
'@type': 'WebSite',
name: 'Sim Documentation',
url: baseUrl,
},
potentialAction: {
'@type': 'ReadAction',
target: url,
},
}
const breadcrumbStructuredData = breadcrumb && {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: breadcrumb.map((item, index) => ({
'@type': 'ListItem',
position: index + 1,
name: item.name,
item: item.url,
})),
}
const websiteStructuredData = url === baseUrl && {
'@context': 'https://schema.org',
'@type': 'WebSite',
name: 'Sim Documentation',
url: baseUrl,
description:
'Comprehensive documentation for Sim visual workflow builder for AI applications. Create powerful AI agents, automation workflows, and data processing pipelines.',
publisher: {
'@type': 'Organization',
name: 'Sim',
url: baseUrl,
},
potentialAction: {
'@type': 'SearchAction',
target: {
'@type': 'EntryPoint',
urlTemplate: `${baseUrl}/search?q={search_term_string}`,
},
'query-input': 'required name=search_term_string',
},
inLanguage: ['en', 'es', 'fr', 'de', 'ja', 'zh'],
}
const faqStructuredData = title.toLowerCase().includes('faq') && {
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: [],
}
const softwareStructuredData = {
'@context': 'https://schema.org',
'@type': 'SoftwareApplication',
name: 'Sim',
applicationCategory: 'DeveloperApplication',
operatingSystem: 'Any',
description:
'Visual workflow builder for AI applications. Create powerful AI agents, automation workflows, and data processing pipelines by connecting blocks on a canvas—no coding required.',
url: baseUrl,
author: {
'@type': 'Organization',
name: 'Sim Team',
},
offers: {
'@type': 'Offer',
category: 'Developer Tools',
},
featureList: [
'Visual workflow builder with drag-and-drop interface',
'AI agent creation and automation',
'80+ built-in integrations',
'Real-time team collaboration',
'Multiple deployment options',
'Custom integrations via MCP protocol',
],
}
return (
<>
<Script
id='article-structured-data'
type='application/ld+json'
dangerouslySetInnerHTML={{
__html: JSON.stringify(articleStructuredData),
}}
/>
{breadcrumbStructuredData && (
<Script
id='breadcrumb-structured-data'
type='application/ld+json'
dangerouslySetInnerHTML={{
__html: JSON.stringify(breadcrumbStructuredData),
}}
/>
)}
{websiteStructuredData && (
<Script
id='website-structured-data'
type='application/ld+json'
dangerouslySetInnerHTML={{
__html: JSON.stringify(websiteStructuredData),
}}
/>
)}
{faqStructuredData && (
<Script
id='faq-structured-data'
type='application/ld+json'
dangerouslySetInnerHTML={{
__html: JSON.stringify(faqStructuredData),
}}
/>
)}
{url === baseUrl && (
<Script
id='software-structured-data'
type='application/ld+json'
dangerouslySetInnerHTML={{
__html: JSON.stringify(softwareStructuredData),
}}
/>
)}
</>
)
}

View File

@@ -1,40 +0,0 @@
'use client'
import type * as React from 'react'
import { blockTypeToIconMap } from '@/components/ui/icon-mapping'
interface BlockInfoCardProps {
type: string
color: string
icon?: React.ComponentType<{ className?: string }>
iconSvg?: string // Deprecated: Use automatic icon resolution instead
}
export function BlockInfoCard({
type,
color,
icon: IconComponent,
iconSvg,
}: BlockInfoCardProps): React.ReactNode {
// Auto-resolve icon component from block type if not explicitly provided
const ResolvedIcon = IconComponent || blockTypeToIconMap[type] || null
return (
<div className='mb-6 overflow-hidden rounded-lg border border-border'>
<div className='flex items-center justify-center p-6'>
<div
className='flex h-20 w-20 items-center justify-center rounded-lg'
style={{ background: color }}
>
{ResolvedIcon ? (
<ResolvedIcon className='h-10 w-10 text-white' />
) : iconSvg ? (
<div className='h-10 w-10 text-white' dangerouslySetInnerHTML={{ __html: iconSvg }} />
) : (
<div className='font-mono text-xl opacity-70'>{type.substring(0, 2)}</div>
)}
</div>
</div>
</div>
)
}

View File

@@ -1,27 +0,0 @@
import { cva, type VariantProps } from 'class-variance-authority'
const variants = {
primary: 'bg-fd-primary text-fd-primary-foreground hover:bg-fd-primary/80',
outline: 'border hover:bg-fd-accent hover:text-fd-accent-foreground',
ghost: 'hover:bg-fd-accent hover:text-fd-accent-foreground',
secondary:
'border bg-fd-secondary text-fd-secondary-foreground hover:bg-fd-accent hover:text-fd-accent-foreground',
} as const
export const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md p-2 text-sm font-medium transition-colors duration-100 disabled:pointer-events-none disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fd-ring',
{
variants: {
variant: variants,
color: variants,
size: {
sm: 'gap-1 px-2 py-1.5 text-xs',
icon: 'p-1.5 [&_svg]:size-5',
'icon-sm': 'p-1.5 [&_svg]:size-4.5',
'icon-xs': 'p-1 [&_svg]:size-4',
},
},
}
)
export type ButtonProps = VariantProps<typeof buttonVariants>

View File

@@ -1,50 +0,0 @@
'use client'
import { useState } from 'react'
import { CodeBlock as FumadocsCodeBlock } from 'fumadocs-ui/components/codeblock'
import { Check, Copy } from 'lucide-react'
import { cn } from '@/lib/utils'
export function CodeBlock(props: React.ComponentProps<typeof FumadocsCodeBlock>) {
const [copied, setCopied] = useState(false)
const handleCopy = async (text: string) => {
await navigator.clipboard.writeText(text)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
return (
<FumadocsCodeBlock
{...props}
Actions={({ children, className }) => (
<div className={cn('empty:hidden', className)}>
{/* Custom copy button */}
<button
type='button'
aria-label={copied ? 'Copied Text' : 'Copy Text'}
onClick={(e) => {
const pre = (e.currentTarget as HTMLElement)
.closest('.nd-codeblock')
?.querySelector('pre')
if (pre) handleCopy(pre.textContent || '')
}}
className={cn(
'cursor-pointer rounded-md p-2 transition-all',
'border border-border bg-background/80 hover:bg-muted',
'backdrop-blur-sm'
)}
>
<span className='flex items-center justify-center'>
{copied ? (
<Check size={16} className='text-green-600 dark:text-green-400' />
) : (
<Copy size={16} className='text-muted-foreground' />
)}
</span>
</button>
</div>
)}
/>
)
}

View File

@@ -1,58 +0,0 @@
'use client'
import { type ComponentPropsWithoutRef, useState } from 'react'
import { Check, Link } from 'lucide-react'
import { cn } from '@/lib/utils'
type HeadingTag = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
interface HeadingProps extends ComponentPropsWithoutRef<'h1'> {
as?: HeadingTag
}
export function Heading({ as, className, ...props }: HeadingProps) {
const [copied, setCopied] = useState(false)
const As = as ?? 'h1'
if (!props.id) {
return <As className={className} {...props} />
}
const handleClick = async (e: React.MouseEvent) => {
e.preventDefault()
const url = `${window.location.origin}${window.location.pathname}#${props.id}`
try {
await navigator.clipboard.writeText(url)
setCopied(true)
// Update URL hash without scrolling
window.history.pushState(null, '', `#${props.id}`)
setTimeout(() => setCopied(false), 2000)
} catch {
// Fallback: just navigate to the anchor
window.location.hash = props.id as string
}
}
return (
<As className={cn('group flex scroll-m-28 flex-row items-center gap-2', className)} {...props}>
<a data-card='' href={`#${props.id}`} className='peer' onClick={handleClick}>
{props.children}
</a>
{copied ? (
<Check
aria-hidden
className='size-3.5 shrink-0 text-green-500 opacity-100 transition-opacity'
/>
) : (
<Link
aria-hidden
className='size-3.5 shrink-0 text-fd-muted-foreground opacity-0 transition-opacity group-hover:opacity-100 peer-hover:opacity-100'
/>
)}
</As>
)
}

View File

@@ -1,249 +0,0 @@
// Auto-generated file - do not edit manually
// Generated by scripts/generate-docs.ts
// Maps block types to their icon component references
import type { ComponentType, SVGProps } from 'react'
import {
AhrefsIcon,
AirtableIcon,
ApifyIcon,
ApolloIcon,
ArxivIcon,
AsanaIcon,
BrainIcon,
BrowserUseIcon,
CalendlyIcon,
CirclebackIcon,
ClayIcon,
ConfluenceIcon,
CursorIcon,
DatadogIcon,
DiscordIcon,
DocumentIcon,
DropboxIcon,
DuckDuckGoIcon,
DynamoDBIcon,
ElasticsearchIcon,
ElevenLabsIcon,
ExaAIIcon,
EyeIcon,
FirecrawlIcon,
FirefliesIcon,
GithubIcon,
GitLabIcon,
GmailIcon,
GoogleCalendarIcon,
GoogleDocsIcon,
GoogleDriveIcon,
GoogleFormsIcon,
GoogleGroupsIcon,
GoogleIcon,
GoogleSheetsIcon,
GoogleSlidesIcon,
GoogleVaultIcon,
GrafanaIcon,
GrainIcon,
GreptileIcon,
HubspotIcon,
HuggingFaceIcon,
HunterIOIcon,
ImageIcon,
IncidentioIcon,
IntercomIcon,
JinaAIIcon,
JiraIcon,
JiraServiceManagementIcon,
KalshiIcon,
LemlistIcon,
LinearIcon,
LinkedInIcon,
LinkupIcon,
MailchimpIcon,
MailgunIcon,
MailServerIcon,
Mem0Icon,
MicrosoftExcelIcon,
MicrosoftOneDriveIcon,
MicrosoftPlannerIcon,
MicrosoftSharepointIcon,
MicrosoftTeamsIcon,
MistralIcon,
MongoDBIcon,
MySQLIcon,
Neo4jIcon,
NotionIcon,
OpenAIIcon,
OutlookIcon,
PackageSearchIcon,
ParallelIcon,
PerplexityIcon,
PineconeIcon,
PipedriveIcon,
PolymarketIcon,
PostgresIcon,
PosthogIcon,
QdrantIcon,
RDSIcon,
RedditIcon,
ResendIcon,
S3Icon,
SalesforceIcon,
SearchIcon,
SendgridIcon,
SentryIcon,
SerperIcon,
ServiceNowIcon,
SftpIcon,
ShopifyIcon,
SlackIcon,
SmtpIcon,
SpotifyIcon,
SQSIcon,
SshIcon,
STTIcon,
StagehandIcon,
StripeIcon,
SupabaseIcon,
TavilyIcon,
TelegramIcon,
TranslateIcon,
TrelloIcon,
TTSIcon,
TwilioIcon,
TypeformIcon,
VideoIcon,
WealthboxIcon,
WebflowIcon,
WhatsAppIcon,
WikipediaIcon,
WordpressIcon,
xIcon,
YouTubeIcon,
ZendeskIcon,
ZepIcon,
ZoomIcon,
} from '@/components/icons'
type IconComponent = ComponentType<SVGProps<SVGSVGElement>>
export const blockTypeToIconMap: Record<string, IconComponent> = {
ahrefs: AhrefsIcon,
airtable: AirtableIcon,
apify: ApifyIcon,
apollo: ApolloIcon,
arxiv: ArxivIcon,
asana: AsanaIcon,
browser_use: BrowserUseIcon,
calendly: CalendlyIcon,
circleback: CirclebackIcon,
clay: ClayIcon,
confluence: ConfluenceIcon,
cursor_v2: CursorIcon,
datadog: DatadogIcon,
discord: DiscordIcon,
dropbox: DropboxIcon,
duckduckgo: DuckDuckGoIcon,
dynamodb: DynamoDBIcon,
elasticsearch: ElasticsearchIcon,
elevenlabs: ElevenLabsIcon,
exa: ExaAIIcon,
file: DocumentIcon,
firecrawl: FirecrawlIcon,
fireflies: FirefliesIcon,
github_v2: GithubIcon,
gitlab: GitLabIcon,
gmail_v2: GmailIcon,
google_calendar_v2: GoogleCalendarIcon,
google_docs: GoogleDocsIcon,
google_drive: GoogleDriveIcon,
google_forms: GoogleFormsIcon,
google_groups: GoogleGroupsIcon,
google_search: GoogleIcon,
google_sheets: GoogleSheetsIcon,
google_slides: GoogleSlidesIcon,
google_vault: GoogleVaultIcon,
grafana: GrafanaIcon,
grain: GrainIcon,
greptile: GreptileIcon,
hubspot: HubspotIcon,
huggingface: HuggingFaceIcon,
hunter: HunterIOIcon,
image_generator: ImageIcon,
imap: MailServerIcon,
incidentio: IncidentioIcon,
intercom_v2: IntercomIcon,
jina: JinaAIIcon,
jira: JiraIcon,
jira_service_management: JiraServiceManagementIcon,
kalshi: KalshiIcon,
knowledge: PackageSearchIcon,
lemlist: LemlistIcon,
linear: LinearIcon,
linkedin: LinkedInIcon,
linkup: LinkupIcon,
mailchimp: MailchimpIcon,
mailgun: MailgunIcon,
mem0: Mem0Icon,
memory: BrainIcon,
microsoft_excel: MicrosoftExcelIcon,
microsoft_planner: MicrosoftPlannerIcon,
microsoft_teams: MicrosoftTeamsIcon,
mistral_parse: MistralIcon,
mongodb: MongoDBIcon,
mysql: MySQLIcon,
neo4j: Neo4jIcon,
notion_v2: NotionIcon,
onedrive: MicrosoftOneDriveIcon,
openai: OpenAIIcon,
outlook: OutlookIcon,
parallel_ai: ParallelIcon,
perplexity: PerplexityIcon,
pinecone: PineconeIcon,
pipedrive: PipedriveIcon,
polymarket: PolymarketIcon,
postgresql: PostgresIcon,
posthog: PosthogIcon,
qdrant: QdrantIcon,
rds: RDSIcon,
reddit: RedditIcon,
resend: ResendIcon,
s3: S3Icon,
salesforce: SalesforceIcon,
search: SearchIcon,
sendgrid: SendgridIcon,
sentry: SentryIcon,
serper: SerperIcon,
servicenow: ServiceNowIcon,
sftp: SftpIcon,
sharepoint: MicrosoftSharepointIcon,
shopify: ShopifyIcon,
slack: SlackIcon,
smtp: SmtpIcon,
spotify: SpotifyIcon,
sqs: SQSIcon,
ssh: SshIcon,
stagehand: StagehandIcon,
stripe: StripeIcon,
stt: STTIcon,
supabase: SupabaseIcon,
tavily: TavilyIcon,
telegram: TelegramIcon,
translate: TranslateIcon,
trello: TrelloIcon,
tts: TTSIcon,
twilio_sms: TwilioIcon,
twilio_voice: TwilioIcon,
typeform: TypeformIcon,
video_generator: VideoIcon,
vision: EyeIcon,
wealthbox: WealthboxIcon,
webflow: WebflowIcon,
whatsapp: WhatsAppIcon,
wikipedia: WikipediaIcon,
wordpress: WordpressIcon,
x: xIcon,
youtube: YouTubeIcon,
zendesk: ZendeskIcon,
zep: ZepIcon,
zoom: ZoomIcon,
}

View File

@@ -1,53 +0,0 @@
'use client'
import { useState } from 'react'
import NextImage, { type ImageProps as NextImageProps } from 'next/image'
import { Lightbox } from '@/components/ui/lightbox'
import { cn } from '@/lib/utils'
interface ImageProps extends Omit<NextImageProps, 'className'> {
className?: string
enableLightbox?: boolean
}
export function Image({
className = 'w-full',
enableLightbox = true,
alt = '',
src,
...props
}: ImageProps) {
const [isLightboxOpen, setIsLightboxOpen] = useState(false)
const handleImageClick = () => {
if (enableLightbox) {
setIsLightboxOpen(true)
}
}
return (
<>
<NextImage
className={cn(
'overflow-hidden rounded-xl border border-border object-cover shadow-sm',
enableLightbox && 'cursor-pointer transition-opacity hover:opacity-90',
className
)}
alt={alt}
src={src}
onClick={handleImageClick}
{...props}
/>
{enableLightbox && (
<Lightbox
isOpen={isLightboxOpen}
onClose={() => setIsLightboxOpen(false)}
src={typeof src === 'string' ? src : String(src)}
alt={alt}
type='image'
/>
)}
</>
)
}

View File

@@ -1,129 +0,0 @@
'use client'
import { useEffect, useState } from 'react'
import { Check, ChevronRight } from 'lucide-react'
import { useParams, usePathname, useRouter } from 'next/navigation'
const languages = {
en: { name: 'English', flag: '🇺🇸' },
es: { name: 'Español', flag: '🇪🇸' },
fr: { name: 'Français', flag: '🇫🇷' },
de: { name: 'Deutsch', flag: '🇩🇪' },
ja: { name: '日本語', flag: '🇯🇵' },
zh: { name: '简体中文', flag: '🇨🇳' },
}
export function LanguageDropdown() {
const [isOpen, setIsOpen] = useState(false)
const pathname = usePathname()
const params = useParams()
const router = useRouter()
const [currentLang, setCurrentLang] = useState(() => {
const langFromParams = params?.lang as string
return langFromParams && Object.keys(languages).includes(langFromParams) ? langFromParams : 'en'
})
useEffect(() => {
const langFromParams = params?.lang as string
if (langFromParams && Object.keys(languages).includes(langFromParams)) {
if (langFromParams !== currentLang) {
setCurrentLang(langFromParams)
}
} else {
if (currentLang !== 'en') {
setCurrentLang('en')
}
}
}, [params, currentLang])
const handleLanguageChange = (locale: string) => {
if (locale === currentLang) {
setIsOpen(false)
return
}
setIsOpen(false)
const segments = pathname.split('/').filter(Boolean)
if (segments[0] && Object.keys(languages).includes(segments[0])) {
segments.shift()
}
let newPath = ''
if (locale === 'en') {
newPath = segments.length > 0 ? `/${segments.join('/')}` : '/introduction'
} else {
newPath = `/${locale}${segments.length > 0 ? `/${segments.join('/')}` : '/introduction'}`
}
router.push(newPath)
}
useEffect(() => {
if (!isOpen) return
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') setIsOpen(false)
}
window.addEventListener('keydown', onKey)
return () => window.removeEventListener('keydown', onKey)
}, [isOpen])
return (
<div className='relative'>
<button
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
setIsOpen(!isOpen)
}}
aria-haspopup='listbox'
aria-expanded={isOpen}
aria-controls='language-menu'
className='flex cursor-pointer items-center gap-1.5 rounded-xl px-3 py-2 font-normal text-[0.9375rem] text-foreground/60 leading-[1.4] transition-colors hover:bg-foreground/8 hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring'
style={{
fontFamily:
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
}}
>
<span>{languages[currentLang as keyof typeof languages]?.name}</span>
<ChevronRight className='h-3.5 w-3.5' />
</button>
{isOpen && (
<>
<div className='fixed inset-0 z-[1000]' aria-hidden onClick={() => setIsOpen(false)} />
<div
id='language-menu'
role='listbox'
className='absolute top-full right-0 z-[1001] mt-1 max-h-[75vh] w-56 overflow-auto rounded-xl border border-border/50 bg-white shadow-2xl md:w-44 md:bg-background/95 md:backdrop-blur-md dark:bg-neutral-950 md:dark:bg-background/95'
>
{Object.entries(languages).map(([code, lang]) => (
<button
key={code}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
handleLanguageChange(code)
}}
role='option'
aria-selected={currentLang === code}
className={`flex w-full cursor-pointer items-center gap-3 px-3 py-3 text-base transition-colors first:rounded-t-xl last:rounded-b-xl hover:bg-muted/80 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring md:gap-2 md:px-2.5 md:py-2 md:text-sm ${
currentLang === code ? 'bg-muted/60 font-medium text-primary' : 'text-foreground'
}`}
>
<span className='text-base md:text-sm'>{lang.flag}</span>
<span className='leading-none'>{lang.name}</span>
{currentLang === code && (
<Check className='ml-auto h-4 w-4 text-primary md:h-3.5 md:w-3.5' />
)}
</button>
))}
</div>
</>
)}
</div>
)
}

View File

@@ -1,74 +0,0 @@
'use client'
import { useEffect, useRef } from 'react'
import { getAssetUrl } from '@/lib/utils'
interface LightboxProps {
isOpen: boolean
onClose: () => void
src: string
alt: string
type: 'image' | 'video'
}
export function Lightbox({ isOpen, onClose, src, alt, type }: LightboxProps) {
const overlayRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
onClose()
}
}
const handleClickOutside = (event: MouseEvent) => {
if (overlayRef.current && event.target === overlayRef.current) {
onClose()
}
}
if (isOpen) {
document.addEventListener('keydown', handleKeyDown)
document.addEventListener('click', handleClickOutside)
document.body.style.overflow = 'hidden'
}
return () => {
document.removeEventListener('keydown', handleKeyDown)
document.removeEventListener('click', handleClickOutside)
document.body.style.overflow = 'unset'
}
}, [isOpen, onClose])
if (!isOpen) return null
return (
<div
ref={overlayRef}
className='fixed inset-0 z-50 flex items-center justify-center bg-black/80 p-12 backdrop-blur-sm'
role='dialog'
aria-modal='true'
aria-label='Media viewer'
>
<div className='relative max-h-full max-w-full overflow-hidden rounded-xl shadow-2xl'>
{type === 'image' ? (
<img
src={src}
alt={alt}
className='max-h-[calc(100vh-6rem)] max-w-[calc(100vw-6rem)] rounded-xl object-contain'
loading='lazy'
/>
) : (
<video
src={getAssetUrl(src)}
autoPlay
loop
muted
playsInline
className='max-h-[calc(100vh-6rem)] max-w-[calc(100vw-6rem)] rounded-xl outline-none focus:outline-none'
/>
)}
</div>
</div>
)
}

View File

@@ -1,38 +0,0 @@
'use client'
import { Search } from 'lucide-react'
export function SearchTrigger() {
const handleClick = () => {
const event = new KeyboardEvent('keydown', {
key: 'k',
metaKey: true,
bubbles: true,
})
document.dispatchEvent(event)
}
return (
<button
type='button'
className='flex h-10 w-[460px] cursor-pointer items-center gap-2 rounded-xl border border-border/50 px-3 py-2 text-sm backdrop-blur-xl transition-colors hover:border-border'
style={{
backgroundColor: 'hsla(0, 0%, 5%, 0.85)',
backdropFilter: 'blur(33px) saturate(180%)',
WebkitBackdropFilter: 'blur(33px) saturate(180%)',
color: 'rgba(255, 255, 255, 0.6)',
}}
onClick={handleClick}
>
<Search className='h-4 w-4' />
<span>Search...</span>
<kbd
className='ml-auto flex items-center gap-0.5 font-medium'
style={{ color: 'rgba(255, 255, 255, 0.6)' }}
>
<span style={{ fontSize: '15px', lineHeight: '1' }}></span>
<span style={{ fontSize: '13px', lineHeight: '1' }}>K</span>
</kbd>
</button>
)
}

View File

@@ -1,32 +0,0 @@
'use client'
import { useEffect, useState } from 'react'
import { Moon, Sun } from 'lucide-react'
import { useTheme } from 'next-themes'
export function ThemeToggle() {
const { theme, setTheme } = useTheme()
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) {
return (
<button className='flex cursor-pointer items-center justify-center rounded-md p-1 text-muted-foreground'>
<Moon className='h-4 w-4' />
</button>
)
}
return (
<button
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
className='flex cursor-pointer items-center justify-center rounded-md p-1 text-muted-foreground transition-colors hover:text-foreground'
aria-label='Toggle theme'
>
{theme === 'dark' ? <Moon className='h-4 w-4' /> : <Sun className='h-4 w-4' />}
</button>
)
}

View File

@@ -1,57 +0,0 @@
'use client'
import { useState } from 'react'
import { getAssetUrl } from '@/lib/utils'
import { Lightbox } from './lightbox'
interface VideoProps {
src: string
className?: string
autoPlay?: boolean
loop?: boolean
muted?: boolean
playsInline?: boolean
enableLightbox?: boolean
}
export function Video({
src,
className = 'w-full rounded-xl border border-border shadow-sm overflow-hidden outline-none focus:outline-none',
autoPlay = true,
loop = true,
muted = true,
playsInline = true,
enableLightbox = true,
}: VideoProps) {
const [isLightboxOpen, setIsLightboxOpen] = useState(false)
const handleVideoClick = () => {
if (enableLightbox) {
setIsLightboxOpen(true)
}
}
return (
<>
<video
autoPlay={autoPlay}
loop={loop}
muted={muted}
playsInline={playsInline}
className={`${className} ${enableLightbox ? 'cursor-pointer transition-opacity hover:opacity-90' : ''}`}
src={getAssetUrl(src)}
onClick={handleVideoClick}
/>
{enableLightbox && (
<Lightbox
isOpen={isLightboxOpen}
onClose={() => setIsLightboxOpen(false)}
src={src}
alt={`Video: ${src}`}
type='video'
/>
)}
</>
)
}

View File

@@ -1,154 +0,0 @@
---
title: Agent
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
import { Image } from '@/components/ui/image'
Der Agent-Block verbindet deinen Workflow mit Large Language Models (LLMs). Er verarbeitet natürlichsprachliche Eingaben, ruft externe Tools auf und generiert strukturierte oder unstrukturierte Ausgaben.
<div className="flex justify-center">
<Image
src="/static/blocks/agent.png"
alt="Agent-Block-Konfiguration"
width={500}
height={400}
className="my-6"
/>
</div>
## Konfigurationsoptionen
### System-Prompt
Der System-Prompt legt die Betriebsparameter und Verhaltenseinschränkungen des Agenten fest. Diese Konfiguration definiert die Rolle des Agenten, die Antwortmethodik und die Verarbeitungsgrenzen für alle eingehenden Anfragen.
```markdown
You are a helpful assistant that specializes in financial analysis.
Always provide clear explanations and cite sources when possible.
When responding to questions about investments, include risk disclaimers.
```
### Benutzer-Prompt
Der Benutzer-Prompt stellt die primären Eingabedaten für die Inferenzverarbeitung dar. Dieser Parameter akzeptiert natürlichsprachlichen Text oder strukturierte Daten, die der Agent analysieren und auf die er reagieren wird. Zu den Eingabequellen gehören:
- **Statische Konfiguration**: Direkte Texteingabe, die in der Block-Konfiguration angegeben ist
- **Dynamische Eingabe**: Daten, die von vorgelagerten Blöcken über Verbindungsschnittstellen übergeben werden
- **Laufzeitgenerierung**: Programmatisch generierte Inhalte während der Workflow-Ausführung
### Modellauswahl
Der Agent-Block unterstützt mehrere LLM-Anbieter über eine einheitliche Inferenzschnittstelle. Verfügbare Modelle umfassen:
- **OpenAI**: GPT-5.1, GPT-5, GPT-4o, o1, o3, o4-mini, gpt-4.1
- **Anthropic**: Claude 4.5 Sonnet, Claude Opus 4.1
- **Google**: Gemini 2.5 Pro, Gemini 2.0 Flash
- **Andere Anbieter**: Groq, Cerebras, xAI, Azure OpenAI, OpenRouter
- **Lokale Modelle**: Ollama oder VLLM-kompatible Modelle
### Temperatur
Steuert die Zufälligkeit und Kreativität der Antworten:
- **Niedrig (0-0,3)**: Deterministisch und fokussiert. Am besten für faktische Aufgaben und Genauigkeit.
- **Mittel (0,3-0,7)**: Ausgewogene Kreativität und Fokus. Gut für allgemeine Verwendung.
- **Hoch (0,7-2,0)**: Kreativ und abwechslungsreich. Ideal für Brainstorming und Content-Generierung.
### API-Schlüssel
Ihr API-Schlüssel für den ausgewählten LLM-Anbieter. Dieser wird sicher gespeichert und für die Authentifizierung verwendet.
### Tools
Erweitern Sie die Fähigkeiten des Agenten mit externen Integrationen. Wählen Sie aus über 60 vorgefertigten Tools oder definieren Sie benutzerdefinierte Funktionen.
**Verfügbare Kategorien:**
- **Kommunikation**: Gmail, Slack, Telegram, WhatsApp, Microsoft Teams
- **Datenquellen**: Notion, Google Sheets, Airtable, Supabase, Pinecone
- **Webdienste**: Firecrawl, Google Search, Exa AI, Browser-Automatisierung
- **Entwicklung**: GitHub, Jira, Linear
- **KI-Dienste**: OpenAI, Perplexity, Hugging Face, ElevenLabs
**Ausführungsmodi:**
- **Auto**: Modell entscheidet kontextbasiert, wann Tools verwendet werden
- **Erforderlich**: Tool muss bei jeder Anfrage aufgerufen werden
- **Keine**: Tool verfügbar, aber dem Modell nicht vorgeschlagen
### Antwortformat
Der Parameter für das Antwortformat erzwingt die Generierung strukturierter Ausgaben durch JSON-Schema-Validierung. Dies gewährleistet konsistente, maschinenlesbare Antworten, die vordefinierten Datenstrukturen entsprechen:
```json
{
"type": "object",
"properties": {
"sentiment": {
"type": "string",
"enum": ["positive", "neutral", "negative"]
},
"summary": {
"type": "string",
"description": "Brief summary of the content"
}
},
"required": ["sentiment", "summary"]
}
```
Diese Konfiguration beschränkt die Ausgabe des Modells auf die Einhaltung des angegebenen Schemas, verhindert Freitextantworten und stellt die Generierung strukturierter Daten sicher.
### Zugriff auf Ergebnisse
Nach Abschluss eines Agenten können Sie auf seine Ausgaben zugreifen:
- **response**: Der Antworttext oder die strukturierten Daten des Agenten
- **usage**: Token-Nutzungsstatistiken (Prompt, Completion, Gesamt)
- **toolExecutions**: Details zu allen Tools, die der Agent während der Ausführung verwendet hat
- **estimatedCost**: Geschätzte Kosten des API-Aufrufs (falls verfügbar)
## Erweiterte Funktionen
### Memory + Agent: Gesprächsverlauf
Verwenden Sie einen memory Block mit einer konsistenten memoryId (zum Beispiel, conversationHistory), um Nachrichten zwischen Durchläufen zu speichern und diesen Verlauf in den Prompt des Agenten einzubeziehen.
- Fügen Sie die Nachricht des Benutzers vor dem Agenten hinzu
- Lesen Sie den Gesprächsverlauf für den Kontext
- Hängen Sie die Antwort des Agenten nach dessen Ausführung an
Siehe den [`Memory`](/tools/memory) Blockverweis für Details.
## Ausgaben
- **`<agent.content>`**: Antworttext des Agenten
- **`<agent.tokens>`**: Token-Nutzungsstatistiken
- **`<agent.tool_calls>`**: Details zur Tool-Ausführung
- **`<agent.cost>`**: Geschätzte Kosten des API-Aufrufs
## Beispielanwendungsfälle
**Automatisierung des Kundenservice** - Bearbeitung von Anfragen mit Datenbank- und Tool-Zugriff
```
API (Ticket) → Agent (Postgres, KB, Linear) → Gmail (Reply) → Memory (Save)
```
**Multi-Modell-Inhaltsanalyse** - Analyse von Inhalten mit verschiedenen KI-Modellen
```
Function (Process) → Agent (GPT-4o Technical) → Agent (Claude Sentiment) → Function (Report)
```
**Tool-gestützter Rechercheassistent** - Recherche mit Websuche und Dokumentenzugriff
```
Input → Agent (Google Search, Notion) → Function (Compile Report)
```
## Bewährte Praktiken
- **Sei spezifisch in System-Prompts**: Definiere die Rolle, den Ton und die Einschränkungen des Agenten klar. Je spezifischer deine Anweisungen sind, desto besser kann der Agent seinen vorgesehenen Zweck erfüllen.
- **Wähle die richtige Temperatureinstellung**: Verwende niedrigere Temperatureinstellungen (0-0,3), wenn Genauigkeit wichtig ist, oder erhöhe die Temperatur (0,7-2,0) für kreativere oder vielfältigere Antworten
- **Nutze Tools effektiv**: Integriere Tools, die den Zweck des Agenten ergänzen und seine Fähigkeiten erweitern. Sei selektiv bei der Auswahl der Tools, um den Agenten nicht zu überfordern. Für Aufgaben mit wenig Überschneidung verwende einen anderen Agent-Block für die besten Ergebnisse.

View File

@@ -1,145 +0,0 @@
---
title: API
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
import { Image } from '@/components/ui/image'
Der API-Block verbindet Ihren Workflow mit externen Diensten durch HTTP-Anfragen. Unterstützt GET, POST, PUT, DELETE und PATCH Methoden für die Interaktion mit REST-APIs.
<div className="flex justify-center">
<Image
src="/static/blocks/api.png"
alt="API-Block"
width={500}
height={400}
className="my-6"
/>
</div>
## Konfigurationsoptionen
### URL
Die Endpunkt-URL für die API-Anfrage. Diese kann sein:
- Eine statische URL, die direkt im Block eingegeben wird
- Eine dynamische URL, die mit der Ausgabe eines anderen Blocks verbunden ist
- Eine URL mit Pfadparametern
### Methode
Wählen Sie die HTTP-Methode für Ihre Anfrage:
- **GET**: Daten vom Server abrufen
- **POST**: Daten an den Server senden, um eine Ressource zu erstellen
- **PUT**: Eine bestehende Ressource auf dem Server aktualisieren
- **DELETE**: Eine Ressource vom Server entfernen
- **PATCH**: Eine bestehende Ressource teilweise aktualisieren
### Abfrageparameter
Definieren Sie Schlüssel-Wert-Paare, die als Abfrageparameter an die URL angehängt werden. Zum Beispiel:
```
Key: apiKey
Value: your_api_key_here
Key: limit
Value: 10
```
Diese würden der URL als `?apiKey=your_api_key_here&limit=10` hinzugefügt.
### Header
Konfigurieren Sie HTTP-Header für Ihre Anfrage. Häufige Header sind:
```
Key: Content-Type
Value: application/json
Key: Authorization
Value: Bearer your_token_here
```
### Anfragekörper
Für Methoden, die einen Anfragekörper unterstützen (POST, PUT, PATCH), können Sie die zu sendenden Daten definieren. Der Körper kann sein:
- JSON-Daten, die direkt im Block eingegeben werden
- Daten, die mit der Ausgabe eines anderen Blocks verbunden sind
- Dynamisch während der Workflow-Ausführung generiert
### Zugriff auf Ergebnisse
Nach Abschluss einer API-Anfrage können Sie auf folgende Ausgaben zugreifen:
- **`<api.data>`**: Die Antwortdaten vom API
- **`<api.status>`**: HTTP-Statuscode (200, 404, 500, usw.)
- **`<api.headers>`**: Antwort-Header vom Server
- **`<api.error>`**: Fehlerdetails, falls die Anfrage fehlgeschlagen ist
## Erweiterte Funktionen
### Dynamische URL-Konstruktion
Erstellen Sie URLs dynamisch mit Variablen aus vorherigen Blöcken:
```javascript
// In a Function block before the API
const userId = <start.userId>;
const apiUrl = `https://api.example.com/users/${userId}/profile`;
```
### Anfrage-Wiederholungen
Der API-Block verarbeitet automatisch:
- Netzwerk-Timeouts mit exponentiellem Backoff
- Antworten bei Ratenbegrenzung (429-Statuscodes)
- Serverfehler (5xx-Statuscodes) mit Wiederholungslogik
- Verbindungsfehler mit Wiederverbindungsversuchen
### Antwortvalidierung
Validieren Sie API-Antworten vor der Verarbeitung:
```javascript
// In a Function block after the API
if (<api.status> === 200) {
const data = <api.data>;
// Process successful response
} else {
// Handle error response
console.error(`API Error: ${<api.status>}`);
}
```
## Ausgaben
- **`<api.data>`**: Antwortdaten vom API
- **`<api.status>`**: HTTP-Statuscode
- **`<api.headers>`**: Antwort-Header
- **`<api.error>`**: Fehlerdetails bei fehlgeschlagener Anfrage
## Anwendungsbeispiele
**Benutzerprofildaten abrufen** - Benutzerinformationen von externem Dienst abrufen
```
Function (Build ID) → API (GET /users/{id}) → Function (Format) → Response
```
**Zahlungsabwicklung** - Zahlung über die Stripe-API verarbeiten
```
Function (Validate) → API (Stripe) → Condition (Success) → Supabase (Update)
```
## Bewährte Praktiken
- **Umgebungsvariablen für sensible Daten verwenden**: Keine API-Schlüssel oder Anmeldedaten im Code festlegen
- **Fehler elegant behandeln**: Fehlerbehandlungslogik für fehlgeschlagene Anfragen einbinden
- **Antworten validieren**: Statuscode und Antwortformate vor der Datenverarbeitung prüfen
- **Ratenbegrenzungen respektieren**: Achten Sie auf API-Ratenbegrenzungen und implementieren Sie angemessene Drosselung

View File

@@ -1,149 +0,0 @@
---
title: Bedingung
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
import { Image } from '@/components/ui/image'
Der Bedingungsblock verzweigt die Workflow-Ausführung basierend auf booleschen Ausdrücken. Bewerten Sie Bedingungen anhand vorheriger Block-Ausgaben und leiten Sie zu verschiedenen Pfaden weiter, ohne dass ein LLM erforderlich ist.
<div className="flex justify-center">
<Image
src="/static/blocks/condition.png"
alt="Bedingungsblock"
width={500}
height={400}
className="my-6"
/>
</div>
## Konfigurationsoptionen
### Bedingungen
Definieren Sie eine oder mehrere Bedingungen, die ausgewertet werden. Jede Bedingung umfasst:
- **Ausdruck**: Ein JavaScript/TypeScript-Ausdruck, der zu wahr oder falsch ausgewertet wird
- **Pfad**: Der Zielblock, zu dem weitergeleitet werden soll, wenn die Bedingung wahr ist
- **Beschreibung**: Optionale Erklärung, was die Bedingung prüft
Sie können mehrere Bedingungen erstellen, die der Reihe nach ausgewertet werden, wobei die erste übereinstimmende Bedingung den Ausführungspfad bestimmt.
### Format für Bedingungsausdrücke
Bedingungen verwenden JavaScript-Syntax und können auf Eingabewerte aus vorherigen Blöcken verweisen.
<Tabs items={['Schwellenwert', 'Textanalyse', 'Mehrere Bedingungen']}>
<Tab>
```javascript
// Check if a score is above a threshold
<agent.score> > 75
```
</Tab>
<Tab>
```javascript
// Check if a text contains specific keywords
<agent.text>.includes('urgent') || <agent.text>.includes('emergency')
```
</Tab>
<Tab>
```javascript
// Check multiple conditions
<agent.age> >= 18 && <agent.country> === 'US'
```
</Tab>
</Tabs>
### Zugriff auf Ergebnisse
Nach der Auswertung einer Bedingung können Sie auf folgende Ausgaben zugreifen:
- **condition.result**: Boolesches Ergebnis der Bedingungsauswertung
- **condition.matched_condition**: ID der übereinstimmenden Bedingung
- **condition.content**: Beschreibung des Auswertungsergebnisses
- **condition.path**: Details zum gewählten Routing-Ziel
## Erweiterte Funktionen
### Komplexe Ausdrücke
Verwenden Sie JavaScript-Operatoren und -Funktionen in Bedingungen:
```javascript
// String operations
<user.email>.endsWith('@company.com')
// Array operations
<api.tags>.includes('urgent')
// Mathematical operations
<agent.confidence> * 100 > 85
// Date comparisons
new Date(<api.created_at>) > new Date('2024-01-01')
```
### Auswertung mehrerer Bedingungen
Bedingungen werden der Reihe nach ausgewertet, bis eine übereinstimmt:
```javascript
// Condition 1: Check for high priority
<ticket.priority> === 'high'
// Condition 2: Check for urgent keywords
<ticket.subject>.toLowerCase().includes('urgent')
// Condition 3: Default fallback
true
```
### Fehlerbehandlung
Bedingungen behandeln automatisch:
- Undefinierte oder Null-Werte mit sicherer Auswertung
- Typabweichungen mit geeigneten Fallbacks
- Ungültige Ausdrücke mit Fehlerprotokollierung
- Fehlende Variablen mit Standardwerten
## Ausgaben
- **`<condition.result>`**: Boolesches Ergebnis der Auswertung
- **`<condition.matched_condition>`**: ID der übereinstimmenden Bedingung
- **`<condition.content>`**: Beschreibung des Auswertungsergebnisses
- **`<condition.path>`**: Details zum gewählten Routing-Ziel
## Beispielanwendungsfälle
**Kundenservice-Routing** - Tickets basierend auf Priorität weiterleiten
```
API (Ticket) → Condition (priority === 'high') → Agent (Escalation) or Agent (Standard)
```
**Inhaltsmoderation** - Inhalte basierend auf Analysen filtern
```
Agent (Analyze) → Condition (toxicity > 0.7) → Moderation or Publish
```
**Benutzer-Onboarding-Ablauf** - Onboarding basierend auf Benutzertyp personalisieren
```
Function (Process) → Condition (account_type === 'enterprise') → Advanced or Simple
```
## Bewährte Praktiken
- **Bedingungen korrekt anordnen**: Platzieren Sie spezifischere Bedingungen vor allgemeinen, um sicherzustellen, dass spezifische Logik Vorrang vor Fallbacks hat
- **Verwenden Sie den Else-Zweig bei Bedarf**: Wenn keine Bedingungen übereinstimmen und der Else-Zweig nicht verbunden ist, endet der Workflow-Zweig ordnungsgemäß. Verbinden Sie den Else-Zweig, wenn Sie einen Fallback-Pfad für nicht übereinstimmende Fälle benötigen
- **Halten Sie Ausdrücke einfach**: Verwenden Sie klare, unkomplizierte boolesche Ausdrücke für bessere Lesbarkeit und einfachere Fehlersuche
- **Dokumentieren Sie Ihre Bedingungen**: Fügen Sie Beschreibungen hinzu, um den Zweck jeder Bedingung für bessere Teamzusammenarbeit und Wartung zu erklären
- **Testen Sie Grenzfälle**: Überprüfen Sie, ob Bedingungen Grenzwerte korrekt behandeln, indem Sie mit Werten an den Grenzen Ihrer Bedingungsbereiche testen

View File

@@ -1,96 +0,0 @@
---
title: Evaluator
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
import { Image } from '@/components/ui/image'
Der Evaluator-Block nutzt KI, um die Inhaltsqualität anhand benutzerdefinierter Metriken zu bewerten. Perfekt für Qualitätskontrolle, A/B-Tests und um sicherzustellen, dass KI-Ausgaben bestimmte Standards erfüllen.
<div className="flex justify-center">
<Image
src="/static/blocks/evaluator.png"
alt="Evaluator-Block-Konfiguration"
width={500}
height={400}
className="my-6"
/>
</div>
## Konfigurationsoptionen
### Bewertungsmetriken
Definieren Sie benutzerdefinierte Metriken, anhand derer Inhalte bewertet werden. Jede Metrik umfasst:
- **Name**: Eine kurze Bezeichnung für die Metrik
- **Beschreibung**: Eine detaillierte Erklärung, was die Metrik misst
- **Bereich**: Der numerische Bereich für die Bewertung (z.B. 1-5, 0-10)
Beispielmetriken:
```
Accuracy (1-5): How factually accurate is the content?
Clarity (1-5): How clear and understandable is the content?
Relevance (1-5): How relevant is the content to the original query?
```
### Inhalt
Der zu bewertende Inhalt. Dies kann sein:
- Direkt in der Blockkonfiguration bereitgestellt
- Verbunden mit der Ausgabe eines anderen Blocks (typischerweise ein Agent-Block)
- Dynamisch während der Workflow-Ausführung generiert
### Modellauswahl
Wählen Sie ein KI-Modell für die Durchführung der Bewertung:
- **OpenAI**: GPT-4o, o1, o3, o4-mini, gpt-4.1
- **Anthropic**: Claude 3.7 Sonnet
- **Google**: Gemini 2.5 Pro, Gemini 2.0 Flash
- **Andere Anbieter**: Groq, Cerebras, xAI, DeepSeek
- **Lokale Modelle**: Ollama oder VLLM-kompatible Modelle
Verwenden Sie Modelle mit starken Argumentationsfähigkeiten wie GPT-4o oder Claude 3.7 Sonnet für beste Ergebnisse.
### API-Schlüssel
Ihr API-Schlüssel für den ausgewählten LLM-Anbieter. Dieser wird sicher gespeichert und für die Authentifizierung verwendet.
## Beispielanwendungsfälle
**Bewertung der Inhaltsqualität** - Inhalte vor der Veröffentlichung bewerten
```
Agent (Generate) → Evaluator (Score) → Condition (Check threshold) → Publish or Revise
```
**A/B-Tests von Inhalten** - Vergleich mehrerer KI-generierter Antworten
```
Parallel (Variations) → Evaluator (Score Each) → Function (Select Best) → Response
```
**Qualitätskontrolle im Kundenservice** - Sicherstellen, dass Antworten Qualitätsstandards erfüllen
```
Agent (Support Response) → Evaluator (Score) → Function (Log) → Condition (Review if Low)
```
## Ausgaben
- **`<evaluator.content>`**: Zusammenfassung der Bewertung mit Punktzahlen
- **`<evaluator.model>`**: Für die Bewertung verwendetes Modell
- **`<evaluator.tokens>`**: Statistik zur Token-Nutzung
- **`<evaluator.cost>`**: Geschätzte Bewertungskosten
## Best Practices
- **Verwenden Sie spezifische Metrikbeschreibungen**: Definieren Sie klar, was jede Metrik misst, um genauere Bewertungen zu erhalten
- **Wählen Sie geeignete Bereiche**: Wählen Sie Bewertungsbereiche, die ausreichend Granularität bieten, ohne zu komplex zu sein
- **Verbinden Sie mit Agent-Blöcken**: Verwenden Sie Evaluator-Blöcke, um die Ausgaben von Agent-Blöcken zu bewerten und Feedback-Schleifen zu erstellen
- **Verwenden Sie konsistente Metriken**: Für vergleichende Analysen sollten Sie konsistente Metriken über ähnliche Bewertungen hinweg beibehalten
- **Kombinieren Sie mehrere Metriken**: Verwenden Sie verschiedene Metriken, um eine umfassende Bewertung zu erhalten

View File

@@ -1,76 +0,0 @@
---
title: Funktion
---
import { Image } from '@/components/ui/image'
Der Funktionsblock führt benutzerdefinierten JavaScript- oder TypeScript-Code in Ihren Workflows aus. Transformieren Sie Daten, führen Sie Berechnungen durch oder implementieren Sie benutzerdefinierte Logik.
<div className="flex justify-center">
<Image
src="/static/blocks/function.png"
alt="Funktionsblock mit Code-Editor"
width={500}
height={400}
className="my-6"
/>
</div>
## Ausgaben
- **`<function.result>`**: Der von Ihrer Funktion zurückgegebene Wert
- **`<function.stdout>`**: Console.log()-Ausgabe Ihres Codes
## Beispielanwendungsfälle
**Datenverarbeitungspipeline** - Transformation von API-Antworten in strukturierte Daten
```
API (Fetch) → Function (Process & Validate) → Function (Calculate Metrics) → Response
```
**Implementierung von Geschäftslogik** - Berechnung von Treuepunkten und Stufen
```
Agent (Get History) → Function (Calculate Score) → Function (Determine Tier) → Condition (Route)
```
**Datenvalidierung und -bereinigung** - Validierung und Bereinigung von Benutzereingaben
```
Input → Function (Validate & Sanitize) → API (Save to Database)
```
### Beispiel: Treuepunkte-Rechner
```javascript title="loyalty-calculator.js"
// Process customer data and calculate loyalty score
const { purchaseHistory, accountAge, supportTickets } = <agent>;
// Calculate metrics
const totalSpent = purchaseHistory.reduce((sum, purchase) => sum + purchase.amount, 0);
const purchaseFrequency = purchaseHistory.length / (accountAge / 365);
const ticketRatio = supportTickets.resolved / supportTickets.total;
// Calculate loyalty score (0-100)
const spendScore = Math.min(totalSpent / 1000 * 30, 30);
const frequencyScore = Math.min(purchaseFrequency * 20, 40);
const supportScore = ticketRatio * 30;
const loyaltyScore = Math.round(spendScore + frequencyScore + supportScore);
return {
customer: <agent.name>,
loyaltyScore,
loyaltyTier: loyaltyScore >= 80 ? "Platinum" : loyaltyScore >= 60 ? "Gold" : "Silver",
metrics: { spendScore, frequencyScore, supportScore }
};
```
## Best Practices
- **Funktionen fokussiert halten**: Schreiben Sie Funktionen, die eine Sache gut erledigen, um die Wartbarkeit und Fehlersuche zu verbessern
- **Fehler elegant behandeln**: Verwenden Sie try/catch-Blöcke, um potenzielle Fehler zu behandeln und aussagekräftige Fehlermeldungen bereitzustellen
- **Grenzfälle testen**: Stellen Sie sicher, dass Ihr Code ungewöhnliche Eingaben, Null-Werte und Grenzbedingungen korrekt behandelt
- **Für Leistung optimieren**: Achten Sie bei großen Datensätzen auf die Berechnungskomplexität und den Speicherverbrauch
- **Console.log() zum Debuggen verwenden**: Nutzen Sie die Stdout-Ausgabe zum Debuggen und Überwachen der Funktionsausführung

View File

@@ -1,207 +0,0 @@
---
title: Guardrails
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Image } from '@/components/ui/image'
import { Video } from '@/components/ui/video'
Der Guardrails-Block validiert und schützt Ihre KI-Workflows, indem er Inhalte anhand mehrerer Validierungstypen überprüft. Stellen Sie die Datenqualität sicher, verhindern Sie Halluzinationen, erkennen Sie personenbezogene Daten und erzwingen Sie Formatanforderungen, bevor Inhalte durch Ihren Workflow fließen.
<div className="flex justify-center">
<Image
src="/static/blocks/guardrails.png"
alt="Guardrails-Block"
width={500}
height={400}
className="my-6"
/>
</div>
## Validierungstypen
### JSON-Validierung
Überprüft, ob der Inhalt korrekt formatiertes JSON ist. Perfekt, um sicherzustellen, dass strukturierte LLM-Ausgaben sicher geparst werden können.
**Anwendungsfälle:**
- Validierung von JSON-Antworten aus Agent-Blöcken vor dem Parsen
- Sicherstellen, dass API-Payloads korrekt formatiert sind
- Überprüfung der Integrität strukturierter Daten
**Ausgabe:**
- `passed`: `true` bei gültigem JSON, sonst `false`
- `error`: Fehlermeldung bei fehlgeschlagener Validierung (z.B. "Ungültiges JSON: Unerwartetes Token...")
### Regex-Validierung
Prüft, ob der Inhalt einem bestimmten regulären Ausdrucksmuster entspricht.
**Anwendungsfälle:**
- Validierung von E-Mail-Adressen
- Überprüfung von Telefonnummernformaten
- Verifizierung von URLs oder benutzerdefinierten Kennungen
- Durchsetzung spezifischer Textmuster
**Konfiguration:**
- **Regex-Muster**: Der reguläre Ausdruck, gegen den geprüft wird (z.B. `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$` für E-Mails)
**Ausgabe:**
- `passed`: `true` wenn der Inhalt dem Muster entspricht, sonst `false`
- `error`: Fehlermeldung bei fehlgeschlagener Validierung
### Halluzinationserkennung
Verwendet Retrieval-Augmented Generation (RAG) mit LLM-Bewertung, um zu erkennen, wann KI-generierte Inhalte im Widerspruch zu Ihrer Wissensdatenbank stehen oder nicht darin begründet sind.
**Funktionsweise:**
1. Abfrage Ihrer Wissensdatenbank nach relevantem Kontext
2. Übermittlung sowohl der KI-Ausgabe als auch des abgerufenen Kontexts an ein LLM
3. LLM weist einen Konfidenzwert zu (Skala 0-10)
- **0** = Vollständige Halluzination (völlig unbegründet)
- **10** = Vollständig begründet (komplett durch die Wissensdatenbank gestützt)
4. Validierung besteht, wenn der Wert ≥ Schwellenwert ist (Standard: 3)
**Konfiguration:**
- **Wissensdatenbank**: Wählen Sie aus Ihren vorhandenen Wissensdatenbanken
- **Modell**: Wählen Sie LLM für die Bewertung (erfordert starkes Denkvermögen - GPT-4o, Claude 3.7 Sonnet empfohlen)
- **API-Schlüssel**: Authentifizierung für den ausgewählten LLM-Anbieter (automatisch ausgeblendet für gehostete/Ollama oder VLLM-kompatible Modelle)
- **Vertrauensschwelle**: Mindestpunktzahl zum Bestehen (0-10, Standard: 3)
- **Top K** (Erweitert): Anzahl der abzurufenden Wissensdatenbank-Chunks (Standard: 10)
**Ausgabe:**
- `passed`: `true` wenn Konfidenzwert ≥ Schwellenwert
- `score`: Konfidenzwert (0-10)
- `reasoning`: Erklärung des LLM für den Wert
- `error`: Fehlermeldung bei fehlgeschlagener Validierung
**Anwendungsfälle:**
- Validierung von Agent-Antworten anhand der Dokumentation
- Sicherstellung der faktischen Richtigkeit von Kundendienstantworten
- Überprüfung, ob generierte Inhalte mit dem Quellmaterial übereinstimmen
- Qualitätskontrolle für RAG-Anwendungen
### PII-Erkennung
Erkennt personenbezogene Daten mithilfe von Microsoft Presidio. Unterstützt über 40 Entitätstypen in mehreren Ländern und Sprachen.
<div className="flex justify-center">
<Image
src="/static/blocks/guardrails-2.png"
alt="PII-Erkennungskonfiguration"
width={700}
height={450}
className="my-6"
/>
</div>
**Funktionsweise:**
1. Übergabe des zu validierenden Inhalts (z.B. `<agent1.content>`)
2. Auswahl der zu erkennenden PII-Typen über den Modal-Selektor
3. Auswahl des Erkennungsmodus (Erkennen oder Maskieren)
4. Inhalt wird auf übereinstimmende PII-Entitäten gescannt
5. Gibt Erkennungsergebnisse und optional maskierten Text zurück
<div className="mx-auto w-3/5 overflow-hidden rounded-lg">
<Video src="guardrails.mp4" width={500} height={350} />
</div>
**Konfiguration:**
- **Zu erkennende PII-Typen**: Auswahl aus gruppierten Kategorien über Modal-Selektor
- **Allgemein**: Personenname, E-Mail, Telefon, Kreditkarte, IP-Adresse usw.
- **USA**: Sozialversicherungsnummer, Führerschein, Reisepass usw.
- **UK**: NHS-Nummer, Sozialversicherungsnummer
- **Spanien**: NIF, NIE, CIF
- **Italien**: Steuernummer, Führerschein, Umsatzsteuer-ID
- **Polen**: PESEL, NIP, REGON
- **Singapur**: NRIC/FIN, UEN
- **Australien**: ABN, ACN, TFN, Medicare
- **Indien**: Aadhaar, PAN, Reisepass, Wählernummer
- **Modus**:
- **Erkennen**: Nur PII identifizieren (Standard)
- **Maskieren**: Erkannte PII durch maskierte Werte ersetzen
- **Sprache**: Erkennungssprache (Standard: Englisch)
**Ausgabe:**
- `passed`: `false` wenn ausgewählte PII-Typen erkannt werden
- `detectedEntities`: Array erkannter PII mit Typ, Position und Konfidenz
- `maskedText`: Inhalt mit maskierter PII (nur wenn Modus = "Mask")
- `error`: Fehlermeldung bei fehlgeschlagener Validierung
**Anwendungsfälle:**
- Blockieren von Inhalten mit sensiblen persönlichen Informationen
- Maskieren von personenbezogenen Daten vor der Protokollierung oder Speicherung
- Einhaltung der DSGVO, HIPAA und anderer Datenschutzbestimmungen
- Bereinigung von Benutzereingaben vor der Verarbeitung
## Konfiguration
### Zu validierende Inhalte
Der zu validierende Eingabeinhalt. Dieser stammt typischerweise aus:
- Ausgaben von Agent-Blöcken: `<agent.content>`
- Ergebnisse von Funktionsblöcken: `<function.output>`
- API-Antworten: `<api.output>`
- Jede andere Blockausgabe
### Validierungstyp
Wählen Sie aus vier Validierungstypen:
- **Gültiges JSON**: Prüfen, ob der Inhalt korrekt formatiertes JSON ist
- **Regex-Übereinstimmung**: Überprüfen, ob der Inhalt einem Regex-Muster entspricht
- **Halluzinationsprüfung**: Validierung gegen Wissensdatenbank mit LLM-Bewertung
- **PII-Erkennung**: Erkennung und optional Maskierung personenbezogener Daten
## Ausgaben
Alle Validierungstypen geben zurück:
- **`<guardrails.passed>`**: Boolescher Wert, der angibt, ob die Validierung bestanden wurde
- **`<guardrails.validationType>`**: Der durchgeführte Validierungstyp
- **`<guardrails.input>`**: Die ursprüngliche Eingabe, die validiert wurde
- **`<guardrails.error>`**: Fehlermeldung, wenn die Validierung fehlgeschlagen ist (optional)
Zusätzliche Ausgaben nach Typ:
**Halluzinationsprüfung:**
- **`<guardrails.score>`**: Konfidenzwert (0-10)
- **`<guardrails.reasoning>`**: Erklärung des LLM
**PII-Erkennung:**
- **`<guardrails.detectedEntities>`**: Array erkannter PII-Entitäten
- **`<guardrails.maskedText>`**: Inhalt mit maskierten PII (wenn Modus = "Mask")
## Beispielanwendungsfälle
**JSON vor dem Parsen validieren** - Stellen Sie sicher, dass die Agent-Ausgabe gültiges JSON ist
```
Agent (Generate) → Guardrails (Validate) → Condition (Check passed) → Function (Parse)
```
**Halluzinationen verhindern** - Validieren Sie Kundendienstantworten anhand der Wissensdatenbank
```
Agent (Response) → Guardrails (Check KB) → Condition (Score ≥ 3) → Send or Flag
```
**PII in Benutzereingaben blockieren** - Bereinigen Sie von Benutzern übermittelte Inhalte
```
Input → Guardrails (Detect PII) → Condition (No PII) → Process or Reject
```
## Bewährte Praktiken
- **Verkettung mit Bedingungsblöcken**: Verwenden Sie `<guardrails.passed>`, um die Workflow-Logik basierend auf Validierungsergebnissen zu verzweigen
- **JSON-Validierung vor dem Parsen verwenden**: Validieren Sie immer die JSON-Struktur, bevor Sie versuchen, LLM-Ausgaben zu parsen
- **Geeignete PII-Typen auswählen**: Wählen Sie nur die für Ihren Anwendungsfall relevanten PII-Entitätstypen für bessere Leistung
- **Angemessene Konfidenzgrenzwerte festlegen**: Passen Sie für die Halluzinationserkennung den Grenzwert an Ihre Genauigkeitsanforderungen an (höher = strenger)
- **Starke Modelle für die Halluzinationserkennung verwenden**: GPT-4o oder Claude 3.7 Sonnet bieten genauere Konfidenzwerte
- **PII für die Protokollierung maskieren**: Verwenden Sie den Modus "Mask", wenn Sie Inhalte protokollieren oder speichern müssen, die PII enthalten könnten
- **Regex-Muster testen**: Validieren Sie Ihre Regex-Muster gründlich, bevor Sie sie in der Produktion einsetzen
- **Validierungsfehler überwachen**: Verfolgen Sie `<guardrails.error>`Nachrichten, um häufige Validierungsprobleme zu identifizieren
<Callout type="info">
Die Validierung von Guardrails erfolgt synchron in Ihrem Workflow. Für die Erkennung von Halluzinationen sollten Sie schnellere Modelle (wie GPT-4o-mini) wählen, wenn die Latenz kritisch ist.
</Callout>

View File

@@ -1,188 +0,0 @@
---
title: Human in the Loop
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
import { Image } from '@/components/ui/image'
import { Video } from '@/components/ui/video'
Der Human in the Loop Block pausiert die Workflow-Ausführung und wartet auf menschliches Eingreifen, bevor er fortfährt. Verwenden Sie ihn, um Genehmigungspunkte hinzuzufügen, Feedback zu sammeln oder zusätzliche Eingaben an kritischen Entscheidungspunkten einzuholen.
<div className="flex justify-center">
<Image
src="/static/blocks/hitl-1.png"
alt="Human in the Loop Block Konfiguration"
width={500}
height={400}
className="my-6"
/>
</div>
Wenn die Ausführung diesen Block erreicht, pausiert der Workflow auf unbestimmte Zeit, bis ein Mensch über das Genehmigungsportal, die API oder den Webhook eine Eingabe macht.
<div className="flex justify-center">
<Image
src="/static/blocks/hitl-2.png"
alt="Human in the Loop Genehmigungsportal"
width={700}
height={500}
className="my-6"
/>
</div>
## Konfigurationsoptionen
### Pausierte Ausgabe
Definiert, welche Daten dem Genehmigenden angezeigt werden. Dies ist der Kontext, der im Genehmigungsportal angezeigt wird, um eine fundierte Entscheidung zu ermöglichen.
Verwenden Sie den visuellen Builder oder den JSON-Editor, um die Daten zu strukturieren. Referenzieren Sie Workflow-Variablen mit der `<blockName.output>` Syntax.
```json
{
"customerName": "<agent1.content.name>",
"proposedAction": "<router1.selectedPath>",
"confidenceScore": "<evaluator1.score>",
"generatedEmail": "<agent2.content>"
}
```
### Benachrichtigung
Konfiguriert, wie Genehmigende benachrichtigt werden, wenn eine Genehmigung erforderlich ist. Unterstützte Kanäle sind:
- **Slack** - Nachrichten an Kanäle oder DMs
- **Gmail** - E-Mail mit Genehmigungslink
- **Microsoft Teams** - Team-Kanal-Benachrichtigungen
- **SMS** - Textwarnungen über Twilio
- **Webhooks** - Benutzerdefinierte Benachrichtigungssysteme
Fügen Sie die Genehmigungs-URL (`<blockId.url>`) in Ihre Benachrichtigungsnachrichten ein, damit Genehmigende auf das Portal zugreifen können.
### Fortsetzungseingabe
Definiert die Felder, die Genehmigende bei der Antwort ausfüllen. Diese Daten werden nach der Fortsetzung des Workflows für nachfolgende Blöcke verfügbar.
```json
{
"approved": {
"type": "boolean",
"description": "Approve or reject this request"
},
"comments": {
"type": "string",
"description": "Optional feedback or explanation"
}
}
```
Greifen Sie in nachgelagerten Blöcken auf Wiederaufnahmedaten mit `<blockId.resumeInput.fieldName>` zu.
## Genehmigungsmethoden
<Tabs items={['Genehmigungsportal', 'API', 'Webhook']}>
<Tab>
### Genehmigungsportal
Jeder Block generiert eine eindeutige Portal-URL (`<blockId.url>`) mit einer visuellen Oberfläche, die alle pausierten Ausgabedaten und Formularfelder für die Fortsetzungseingabe anzeigt. Mobilgerätekompatibel und sicher.
Teilen Sie diese URL in Benachrichtigungen, damit Genehmiger die Anfragen prüfen und beantworten können.
</Tab>
<Tab>
### REST API
Workflows programmatisch fortsetzen:
```bash
POST /api/workflows/{workflowId}/executions/{executionId}/resume/{blockId}
{
"approved": true,
"comments": "Looks good to proceed"
}
```
Erstellen Sie benutzerdefinierte Genehmigungs-UIs oder integrieren Sie bestehende Systeme.
</Tab>
<Tab>
### Webhook
Fügen Sie ein Webhook-Tool im Benachrichtigungsbereich hinzu, um Genehmigungsanfragen an externe Systeme zu senden. Integration mit Ticketing-Systemen wie Jira oder ServiceNow.
</Tab>
</Tabs>
## Häufige Anwendungsfälle
**Inhaltsgenehmigung** - Überprüfung von KI-generierten Inhalten vor der Veröffentlichung
```
Agent → Human in the Loop → API (Publish)
```
**Mehrstufige Genehmigungen** - Verkettung mehrerer Genehmigungsschritte für wichtige Entscheidungen
```
Agent → Human in the Loop (Manager) → Human in the Loop (Director) → Execute
```
**Datenvalidierung** - Überprüfung extrahierter Daten vor der Verarbeitung
```
Agent (Extract) → Human in the Loop (Validate) → Function (Process)
```
**Qualitätskontrolle** - Überprüfung von KI-Ausgaben vor dem Versand an Kunden
```
Agent (Generate) → Human in the Loop (QA) → Gmail (Send)
```
## Block-Ausgaben
**`url`** - Eindeutige URL für das Genehmigungsportal
**`resumeInput.*`** - Alle in der Fortsetzungseingabe definierten Felder werden verfügbar, nachdem der Workflow fortgesetzt wird
Zugriff über `<blockId.resumeInput.fieldName>`.
## Beispiel
**Pausierte Ausgabe:**
```json
{
"title": "<agent1.content.title>",
"body": "<agent1.content.body>",
"qualityScore": "<evaluator1.score>"
}
```
**Fortsetzungseingabe:**
```json
{
"approved": { "type": "boolean" },
"feedback": { "type": "string" }
}
```
**Nachgelagerte Verwendung:**
```javascript
// Condition block
<approval1.resumeInput.approved> === true
```
Das Beispiel unten zeigt ein Genehmigungsportal, wie es von einem Genehmiger gesehen wird, nachdem der Workflow angehalten wurde. Genehmiger können die Daten überprüfen und Eingaben als Teil der Workflow-Wiederaufnahme bereitstellen. Auf das Genehmigungsportal kann direkt über die eindeutige URL, `<blockId.url>`, zugegriffen werden.
<div className="mx-auto w-full overflow-hidden rounded-lg my-6">
<Video src="hitl-resume.mp4" width={700} height={450} />
</div>
## Verwandte Blöcke
- **[Bedingung](/blocks/condition)** - Verzweigung basierend auf Genehmigungsentscheidungen
- **[Variablen](/blocks/variables)** - Speichern von Genehmigungsverlauf und Metadaten
- **[Antwort](/blocks/response)** - Rückgabe von Workflow-Ergebnissen an API-Aufrufer

View File

@@ -1,143 +0,0 @@
---
title: Übersicht
description: Die Bausteine deiner KI-Workflows
---
import { Card, Cards } from 'fumadocs-ui/components/card'
import { Step, Steps } from 'fumadocs-ui/components/steps'
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
import { Video } from '@/components/ui/video'
Blöcke sind die Bausteine, die du miteinander verbindest, um KI-Workflows zu erstellen. Betrachte sie als spezialisierte Module, die jeweils eine bestimmte Aufgabe übernehmen vom Chatten mit KI-Modellen über API-Aufrufe bis hin zur Datenverarbeitung.
<div className="w-full max-w-2xl mx-auto overflow-hidden rounded-lg">
<Video src="connections.mp4" width={700} height={450} />
</div>
## Grundlegende Blocktypen
Sim bietet wesentliche Blocktypen, die die Kernfunktionen von KI-Workflows abdecken:
### Verarbeitungsblöcke
- **[Agent](/blocks/agent)** - Chatte mit KI-Modellen (OpenAI, Anthropic, Google, lokale Modelle)
- **[Function](/blocks/function)** - Führe benutzerdefinierten JavaScript/TypeScript-Code aus
- **[API](/blocks/api)** - Verbinde dich mit externen Diensten über HTTP-Anfragen
### Logikblöcke
- **[Condition](/blocks/condition)** - Verzweige Workflow-Pfade basierend auf booleschen Ausdrücken
- **[Router](/blocks/router)** - Nutze KI, um Anfragen intelligent auf verschiedene Pfade zu leiten
- **[Evaluator](/blocks/evaluator)** - Bewerte und beurteile die Inhaltsqualität mit KI
### Ablaufsteuerungsblöcke
- **[Variablen](/blocks/variables)** - Workflow-bezogene Variablen setzen und verwalten
- **[Warten](/blocks/wait)** - Workflow-Ausführung für eine bestimmte Zeitverzögerung pausieren
- **[Mensch in der Schleife](/blocks/human-in-the-loop)** - Pausieren für menschliche Genehmigung und Feedback vor dem Fortfahren
### Ausgabeblöcke
- **[Antwort](/blocks/response)** - Formatieren und Zurückgeben der endgültigen Ergebnisse aus Ihrem Workflow
## Wie Blöcke funktionieren
Jeder Block hat drei Hauptkomponenten:
**Eingaben**: Daten, die in den Block von anderen Blöcken oder Benutzereingaben kommen
**Konfiguration**: Einstellungen, die steuern, wie der Block sich verhält
**Ausgaben**: Daten, die der Block für andere Blöcke zur Verwendung erzeugt
<Steps>
<Step>
<strong>Eingabe empfangen</strong>: Block erhält Daten von verbundenen Blöcken oder Benutzereingaben
</Step>
<Step>
<strong>Verarbeiten</strong>: Block verarbeitet die Eingabe gemäß seiner Konfiguration
</Step>
<Step>
<strong>Ergebnisse ausgeben</strong>: Block erzeugt Ausgabedaten für die nächsten Blöcke im Workflow
</Step>
</Steps>
## Blöcke verbinden
Sie erstellen Workflows, indem Sie Blöcke miteinander verbinden. Die Ausgabe eines Blocks wird zur Eingabe eines anderen:
- **Ziehen zum Verbinden**: Ziehen Sie von einem Ausgabeport zu einem Eingabeport
- **Mehrfachverbindungen**: Eine Ausgabe kann mit mehreren Eingaben verbunden werden
- **Verzweigende Pfade**: Einige Blöcke können basierend auf Bedingungen zu verschiedenen Pfaden weiterleiten
<div className="w-full max-w-2xl mx-auto overflow-hidden rounded-lg">
<Video src="connections.mp4" width={700} height={450} />
</div>
## Häufige Muster
### Sequentielle Verarbeitung
Verbinden Sie Blöcke in einer Kette, wobei jeder Block die Ausgabe des vorherigen verarbeitet:
```
User Input → Agent → Function → Response
```
### Bedingte Verzweigung
Verwenden Sie Bedingung- oder Router-Blöcke, um verschiedene Pfade zu erstellen:
```
User Input → Router → Agent A (for questions)
→ Agent B (for commands)
```
### Qualitätskontrolle
Verwenden Sie Evaluator-Blöcke, um Ausgaben zu bewerten und zu filtern:
```
Agent → Evaluator → Condition → Response (if good)
→ Agent (retry if bad)
```
## Block-Konfiguration
Jeder Blocktyp hat spezifische Konfigurationsoptionen:
**Alle Blöcke**:
- Eingabe-/Ausgabeverbindungen
- Fehlerbehandlungsverhalten
- Einstellungen für Ausführungs-Timeout
**KI-Blöcke** (Agent, Router, Evaluator):
- Modellauswahl (OpenAI, Anthropic, Google, lokal)
- API-Schlüssel und Authentifizierung
- Temperatur und andere Modellparameter
- Systemaufforderungen und Anweisungen
**Logik-Blöcke** (Bedingung, Funktion):
- Benutzerdefinierte Ausdrücke oder Code
- Variablenreferenzen
- Einstellungen für Ausführungsumgebung
**Integrations-Blöcke** (API, Response):
- Endpunktkonfiguration
- Header und Authentifizierung
- Anfrage-/Antwortformatierung
<Cards>
<Card title="Agent-Block" href="/blocks/agent">
Verbindung zu KI-Modellen herstellen und intelligente Antworten erstellen
</Card>
<Card title="Funktionsblock" href="/blocks/function">
Benutzerdefinierten Code ausführen, um Daten zu verarbeiten und zu transformieren
</Card>
<Card title="API-Block" href="/blocks/api">
Integration mit externen Diensten und APIs
</Card>
<Card title="Bedingungsblock" href="/blocks/condition">
Verzweigende Logik basierend auf Datenbewertung erstellen
</Card>
<Card title="Mensch-in-der-Schleife-Block" href="/blocks/human-in-the-loop">
Pausieren für menschliche Genehmigung und Feedback vor dem Fortfahren
</Card>
<Card title="Variablenblock" href="/blocks/variables">
Workflow-bezogene Variablen setzen und verwalten
</Card>
<Card title="Warteblock" href="/blocks/wait">
Workflow-Ausführung für bestimmte Zeitverzögerungen pausieren
</Card>
</Cards>

View File

@@ -1,257 +0,0 @@
---
title: Loop
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
import { Image } from '@/components/ui/image'
Der Schleifenblock ist ein Container, der Blöcke wiederholt ausführt. Iteriere über Sammlungen, wiederhole Operationen eine festgelegte Anzahl von Malen oder fahre fort, solange eine Bedingung erfüllt ist.
<Callout type="info">
Schleifenblöcke sind Container-Knoten, die andere Blöcke in sich enthalten. Die enthaltenen Blöcke werden mehrfach ausgeführt, basierend auf deiner Konfiguration.
</Callout>
## Konfigurationsoptionen
### Schleifentyp
Wähle zwischen vier Arten von Schleifen:
<Tabs items={['For-Schleife', 'ForEach-Schleife', 'While-Schleife', 'Do-While-Schleife']}>
<Tab>
**For-Schleife (Iterationen)** - Eine numerische Schleife, die eine festgelegte Anzahl von Malen ausgeführt wird:
<div className="flex justify-center">
<Image
src="/static/blocks/loop-1.png"
alt="For-Schleife mit Iterationen"
width={500}
height={400}
className="my-6"
/>
</div>
Verwende diese, wenn du eine Operation eine bestimmte Anzahl von Malen wiederholen musst.
```
Example: Run 5 times
- Iteration 1
- Iteration 2
- Iteration 3
- Iteration 4
- Iteration 5
```
</Tab>
<Tab>
**ForEach-Schleife (Sammlung)** - Eine sammlungsbasierte Schleife, die über jedes Element in einem Array oder Objekt iteriert:
<div className="flex justify-center">
<Image
src="/static/blocks/loop-2.png"
alt="ForEach-Schleife mit Sammlung"
width={500}
height={400}
className="my-6"
/>
</div>
Verwende diese, wenn du eine Sammlung von Elementen verarbeiten musst.
```
Example: Process ["apple", "banana", "orange"]
- Iteration 1: Process "apple"
- Iteration 2: Process "banana"
- Iteration 3: Process "orange"
```
</Tab>
<Tab>
**While-Schleife (Bedingungsbasiert)** - Wird ausgeführt, solange eine Bedingung als wahr ausgewertet wird:
<div className="flex justify-center">
<Image
src="/static/blocks/loop-3.png"
alt="While-Schleife mit Bedingung"
width={500}
height={400}
className="my-6"
/>
</div>
Verwende diese, wenn du eine Schleife benötigst, die läuft, bis eine bestimmte Bedingung erfüllt ist. Die Bedingung wird **vor** jeder Iteration überprüft.
```
Example: While {"<variable.i>"} < 10
- Check condition → Execute if true
- Inside loop: Increment {"<variable.i>"}
- Inside loop: Variables assigns i = {"<variable.i>"} + 1
- Check condition → Execute if true
- Check condition → Exit if false
```
</Tab>
<Tab>
**Do-While-Schleife (Bedingungsbasiert)** - Wird mindestens einmal ausgeführt und dann fortgesetzt, solange eine Bedingung wahr ist:
<div className="flex justify-center">
<Image
src="/static/blocks/loop-4.png"
alt="Do-While-Schleife mit Bedingung"
width={500}
height={400}
className="my-6"
/>
</div>
Verwende diese, wenn du eine Operation mindestens einmal ausführen musst und dann die Schleife fortsetzen willst, bis eine Bedingung erfüllt ist. Die Bedingung wird **nach** jeder Iteration überprüft.
```
Example: Do-while {"<variable.i>"} < 10
- Execute blocks
- Inside loop: Increment {"<variable.i>"}
- Inside loop: Variables assigns i = {"<variable.i>"} + 1
- Check condition → Continue if true
- Check condition → Exit if false
```
</Tab>
</Tabs>
## Wie man Schleifen verwendet
### Eine Schleife erstellen
1. Ziehe einen Schleifenblock aus der Werkzeugleiste auf deine Leinwand
2. Konfiguriere den Schleifentyp und die Parameter
3. Ziehe andere Blöcke in den Schleifencontainer
4. Verbinde die Blöcke nach Bedarf
### Auf Ergebnisse zugreifen
Nach Abschluss einer Schleife kannst du auf aggregierte Ergebnisse zugreifen:
- **loop.results**: Array mit Ergebnissen aller Schleifendurchläufe
## Beispielanwendungsfälle
**Verarbeitung von API-Ergebnissen** - ForEach-Schleife verarbeitet Kundendatensätze aus einer API
```javascript
// Beispiel: ForEach-Schleife für API-Ergebnisse
const customers = await api.getCustomers();
loop.forEach(customers, (customer) => {
// Verarbeite jeden Kunden
if (customer.status === 'active') {
sendEmail(customer.email, 'Sonderangebot');
}
});
```
**Iterative Inhaltsgenerierung** - For-Schleife generiert mehrere Inhaltsvariationen
```javascript
// Beispiel: For-Schleife für Inhaltsgenerierung
const variations = [];
loop.for(5, (i) => {
// Generiere 5 verschiedene Variationen
const content = ai.generateContent({
prompt: `Variation ${i+1} für Produktbeschreibung`,
temperature: 0.7 + (i * 0.1)
});
variations.push(content);
});
```
**Zähler mit While-Schleife** - While-Schleife verarbeitet Elemente mit Zähler
```javascript
// Beispiel: While-Schleife mit Zähler
let counter = 0;
let processedItems = 0;
loop.while(() => counter < items.length, () => {
if (items[counter].isValid) {
processItem(items[counter]);
processedItems++;
}
counter++;
});
console.log(`${processedItems} gültige Elemente verarbeitet`);
```
## Erweiterte Funktionen
### Einschränkungen
<Callout type="warning">
Container-Blöcke (Schleifen und Parallele) können nicht ineinander verschachtelt werden. Das bedeutet:
- Du kannst keinen Schleifenblock in einen anderen Schleifenblock platzieren
- Du kannst keinen Parallel-Block in einen Schleifenblock platzieren
- Du kannst keinen Container-Block in einen anderen Container-Block platzieren
Wenn du mehrdimensionale Iterationen benötigst, erwäge eine Umstrukturierung deines Workflows, um sequentielle Schleifen zu verwenden oder Daten in Stufen zu verarbeiten.
</Callout>
<Callout type="info">
Schleifen werden sequentiell ausgeführt, nicht parallel. Wenn du eine gleichzeitige Ausführung benötigst, verwende stattdessen den Parallel-Block.
</Callout>
## Eingaben und Ausgaben
<Tabs items={['Configuration', 'Variables', 'Results']}>
<Tab>
<ul className="list-disc space-y-2 pl-6">
<li>
<strong>Schleifentyp</strong>: Wähle zwischen 'for', 'forEach', 'while' oder 'doWhile'
</li>
<li>
<strong>Iterationen</strong>: Anzahl der Ausführungen (für for-Schleifen)
</li>
<li>
<strong>Sammlung</strong>: Array oder Objekt zum Durchlaufen (für forEach-Schleifen)
</li>
<li>
<strong>Bedingung</strong>: Boolescher Ausdruck zur Auswertung (für while/do-while-Schleifen)
</li>
</ul>
</Tab>
<Tab>
<ul className="list-disc space-y-2 pl-6">
<li>
<strong>loop.currentItem</strong>: Aktuell verarbeitetes Element
</li>
<li>
<strong>loop.index</strong>: Aktuelle Iterationsnummer (0-basiert)
</li>
<li>
<strong>loop.items</strong>: Vollständige Sammlung (für forEach-Schleifen)
</li>
</ul>
</Tab>
<Tab>
<ul className="list-disc space-y-2 pl-6">
<li>
<strong>loop.results</strong>: Array aller Iterationsergebnisse
</li>
<li>
<strong>Struktur</strong>: Ergebnisse behalten die Iterationsreihenfolge bei
</li>
<li>
<strong>Zugriff</strong>: Verfügbar in Blöcken nach der Schleife
</li>
</ul>
</Tab>
</Tabs>
## Bewährte Praktiken
- **Setzen Sie vernünftige Grenzen**: Halten Sie die Anzahl der Iterationen in einem vernünftigen Rahmen, um lange Ausführungszeiten zu vermeiden
- **Verwenden Sie ForEach für Sammlungen**: Verwenden Sie beim Verarbeiten von Arrays oder Objekten ForEach anstelle von For-Schleifen
- **Behandeln Sie Fehler elegant**: Erwägen Sie, Fehlerbehandlung innerhalb von Schleifen hinzuzufügen, um robuste Workflows zu gewährleisten

View File

@@ -1,216 +0,0 @@
---
title: Parallel
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
import { Image } from '@/components/ui/image'
Der Parallel-Block ist ein Container, der mehrere Instanzen gleichzeitig ausführt, um Workflows schneller zu verarbeiten. Verarbeiten Sie Elemente simultan statt sequentiell.
<Callout type="info">
Parallel-Blöcke sind Container-Knoten, die ihre Inhalte mehrfach gleichzeitig ausführen, im Gegensatz zu Schleifen, die sequentiell ausgeführt werden.
</Callout>
## Konfigurationsoptionen
### Parallel-Typ
Wählen Sie zwischen zwei Arten der parallelen Ausführung:
<Tabs items={['Count-based', 'Collection-based']}>
<Tab>
**Anzahlbasierte Parallelisierung** - Führen Sie eine feste Anzahl paralleler Instanzen aus:
<div className="flex justify-center">
<Image
src="/static/blocks/parallel-1.png"
alt="Anzahlbasierte parallele Ausführung"
width={500}
height={400}
className="my-6"
/>
</div>
Verwenden Sie diese Option, wenn Sie dieselbe Operation mehrmals gleichzeitig ausführen müssen.
```javascript
// Beispiel: 3 parallele Instanzen ausführen
const results = await blocks.parallel({
type: 'count',
count: 3,
async process(index) {
// Jede Instanz erhält einen eindeutigen Index (0, 1, 2)
return `Ergebnis von Instanz ${index}`;
}
});
```
</Tab>
<Tab>
**Sammlungsbasierte Parallelisierung** - Verteilen Sie eine Sammlung auf parallele Instanzen:
<div className="flex justify-center">
<Image
src="/static/blocks/parallel-2.png"
alt="Sammlungsbasierte parallele Ausführung"
width={500}
height={400}
className="my-6"
/>
</div>
Jede Instanz verarbeitet gleichzeitig ein Element aus der Sammlung.
```javascript
// Beispiel: Eine Liste von URLs parallel verarbeiten
const urls = [
'https://example.com/api/1',
'https://example.com/api/2',
'https://example.com/api/3'
];
const results = await blocks.parallel({
type: 'collection',
items: urls,
async process(url, index) {
// Jede Instanz verarbeitet eine URL
const response = await fetch(url);
return response.json();
}
});
```
</Tab>
</Tabs>
## Verwendung von Parallel-Blöcken
### Erstellen eines Parallel-Blocks
1. Ziehen Sie einen Parallel-Block aus der Symbolleiste auf Ihre Leinwand
2. Konfigurieren Sie den Parallel-Typ und die Parameter
3. Ziehen Sie einen einzelnen Block in den Parallel-Container
4. Verbinden Sie den Block nach Bedarf
### Zugriff auf Ergebnisse
Nach Abschluss eines Parallel-Blocks können Sie auf aggregierte Ergebnisse zugreifen:
- **`results`**: Array von Ergebnissen aus allen parallelen Instanzen
## Beispielanwendungsfälle
**Batch-API-Verarbeitung** - Verarbeiten Sie mehrere API-Aufrufe gleichzeitig
<div className="mb-4 rounded-md border p-4">
<h4 className="font-medium">Szenario: Mehrere API-Endpunkte abfragen</h4>
<ol className="list-decimal pl-5 text-sm">
<li>Sammlungsbasierte Parallelisierung über eine Liste von API-Endpunkten</li>
<li>Jede Instanz führt einen API-Aufruf durch und verarbeitet die Antwort</li>
<li>Ergebnisse werden in einem Array gesammelt und können weiterverarbeitet werden</li>
</ol>
</div>
**Multi-Modell-KI-Verarbeitung** - Erhalten Sie Antworten von mehreren KI-Modellen gleichzeitig
<div className="mb-4 rounded-md border p-4">
<h4 className="font-medium">Szenario: Antworten von mehreren KI-Modellen erhalten</h4>
<ol className="list-decimal pl-5 text-sm">
<li>Sammlungsbasierte Parallelverarbeitung über eine Liste von Modell-IDs (z.B. ["gpt-4o", "claude-3.7-sonnet", "gemini-2.5-pro"])</li>
<li>Innerhalb des parallelen Blocks: Das Modell des Agenten wird auf das aktuelle Element aus der Sammlung gesetzt</li>
<li>Nach dem parallelen Block: Vergleichen und Auswählen der besten Antwort</li>
</ol>
</div>
## Erweiterte Funktionen
### Ergebnisaggregation
Ergebnisse aus allen parallelen Instanzen werden automatisch gesammelt:
### Anwendungsbeispiele
### Instanzisolierung
Jede parallele Instanz läuft unabhängig:
- Separate Variablenbereiche
- Kein gemeinsamer Zustand zwischen Instanzen
- Fehler in einer Instanz beeinflussen andere nicht
### Einschränkungen
<Callout type="warning">
Container-Blöcke (Schleifen und Parallele) können nicht ineinander verschachtelt werden. Das bedeutet:
- Sie können keinen Schleifenblock in einen Parallelblock platzieren
- Sie können keinen weiteren Parallelblock in einen Parallelblock platzieren
- Sie können keinen Container-Block in einen anderen Container-Block platzieren
</Callout>
<Callout type="info">
Während die parallele Ausführung schneller ist, beachten Sie bitte:
- API-Ratenbegrenzungen bei gleichzeitigen Anfragen
- Speicherverbrauch bei großen Datensätzen
- Maximum von 20 gleichzeitigen Instanzen, um Ressourcenerschöpfung zu vermeiden
</Callout>
## Parallel vs. Schleife
Wann Sie welche Methode verwenden sollten:
| Funktion | Parallel | Schleife |
|---------|----------|------|
| Ausführung | Gleichzeitig | Sequentiell |
| Geschwindigkeit | Schneller für unabhängige Operationen | Langsamer, aber geordnet |
| Reihenfolge | Keine garantierte Reihenfolge | Behält Reihenfolge bei |
| Anwendungsfall | Unabhängige Operationen | Abhängige Operationen |
| Ressourcennutzung | Höher | Niedriger |
## Eingaben und Ausgaben
<Tabs items={['Configuration', 'Variables', 'Results']}>
<Tab>
<ul className="list-disc space-y-2 pl-6">
<li>
<strong>Parallel-Typ</strong>: Wählen Sie zwischen 'count' oder 'collection'
</li>
<li>
<strong>Anzahl</strong>: Anzahl der auszuführenden Instanzen (anzahlbasiert)
</li>
<li>
<strong>Sammlung</strong>: Array oder Objekt zur Verteilung (sammlungsbasiert)
</li>
</ul>
</Tab>
<Tab>
<ul className="list-disc space-y-2 pl-6">
<li>
<strong>parallel.currentItem</strong>: Element für diese Instanz
</li>
<li>
<strong>parallel.index</strong>: Instanznummer (0-basiert)
</li>
<li>
<strong>parallel.items</strong>: Vollständige Sammlung (sammlungsbasiert)
</li>
</ul>
</Tab>
<Tab>
<ul className="list-disc space-y-2 pl-6">
<li>
<strong>parallel.results</strong>: Array aller Instanzergebnisse
</li>
<li>
<strong>Zugriff</strong>: Verfügbar in Blöcken nach der Parallelausführung
</li>
</ul>
</Tab>
</Tabs>
## Best Practices
- **Nur unabhängige Operationen**: Stellen Sie sicher, dass Operationen nicht voneinander abhängen
- **Ratenbegrenzungen berücksichtigen**: Fügen Sie Verzögerungen oder Drosselungen für API-intensive Workflows hinzu
- **Fehlerbehandlung**: Jede Instanz sollte ihre eigenen Fehler angemessen behandeln

View File

@@ -1,120 +0,0 @@
---
title: Antwort
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
import { Image } from '@/components/ui/image'
Der Response-Block formatiert und sendet strukturierte HTTP-Antworten zurück an API-Aufrufer. Verwenden Sie ihn, um Workflow-Ergebnisse mit korrekten Statuscodes und Headern zurückzugeben.
<div className="flex justify-center">
<Image
src="/static/blocks/response.png"
alt="Konfiguration des Antwort-Blocks"
width={500}
height={400}
className="my-6"
/>
</div>
<Callout type="info">
Response-Blöcke sind terminale Blöcke - sie beenden die Workflow-Ausführung und können nicht mit anderen Blöcken verbunden werden.
</Callout>
## Konfigurationsoptionen
### Antwortdaten
Die Antwortdaten sind der Hauptinhalt, der an den API-Aufrufer zurückgesendet wird. Diese sollten als JSON formatiert sein und können Folgendes enthalten:
- Statische Werte
- Dynamische Verweise auf Workflow-Variablen mit der `<variable.name>` Syntax
- Verschachtelte Objekte und Arrays
- Jede gültige JSON-Struktur
### Statuscode
Legen Sie den HTTP-Statuscode für die Antwort fest (standardmäßig 200):
**Erfolg (2xx):**
- **200**: OK - Standard-Erfolgsantwort
- **201**: Erstellt - Ressource erfolgreich erstellt
- **204**: Kein Inhalt - Erfolg ohne Antworttext
**Client-Fehler (4xx):**
- **400**: Ungültige Anfrage - Ungültige Anfrageparameter
- **401**: Nicht autorisiert - Authentifizierung erforderlich
- **404**: Nicht gefunden - Ressource existiert nicht
- **422**: Nicht verarbeitbare Entität - Validierungsfehler
**Server-Fehler (5xx):**
- **500**: Interner Serverfehler - Serverseitiger Fehler
- **502**: Bad Gateway - Fehler eines externen Dienstes
- **503**: Dienst nicht verfügbar - Dienst vorübergehend nicht erreichbar
### Antwort-Header
Konfigurieren Sie zusätzliche HTTP-Header, die in die Antwort aufgenommen werden sollen.
Header werden als Schlüssel-Wert-Paare konfiguriert:
| Schlüssel | Wert |
|-----|-------|
| Content-Type | application/json |
| Cache-Control | no-cache |
| X-API-Version | 1.0 |
## Beispielanwendungsfälle
**API-Endpunkt-Antwort** - Strukturierte Daten von einer Such-API zurückgeben
```
Agent (Search) → Function (Format & Paginate) → Response (200, JSON)
```
**Webhook-Bestätigung** - Bestätigung des Webhook-Empfangs und der Verarbeitung
```
Webhook Trigger → Function (Process) → Response (200, Confirmation)
```
**Fehlerantwort-Behandlung** - Angemessene Fehlerantworten zurückgeben
```
Condition (Error Detected) → Router → Response (400/500, Error Details)
```
## Ausgaben
Antwortblöcke sind endgültig - sie beenden die Workflow-Ausführung und senden die HTTP-Antwort an den API-Aufrufer. Es stehen keine Ausgaben für nachgelagerte Blöcke zur Verfügung.
## Variablenreferenzen
Verwenden Sie die `<variable.name>` Syntax, um Workflow-Variablen dynamisch in Ihre Antwort einzufügen:
```json
{
"user": {
"id": "<variable.userId>",
"name": "<variable.userName>",
"email": "<variable.userEmail>"
},
"query": "<variable.searchQuery>",
"results": "<variable.searchResults>",
"totalFound": "<variable.resultCount>",
"processingTime": "<variable.executionTime>ms"
}
```
<Callout type="warning">
Variablennamen sind Groß- und Kleinschreibung sensitiv und müssen exakt mit den in Ihrem Workflow verfügbaren Variablen übereinstimmen.
</Callout>
## Best Practices
- **Verwenden Sie aussagekräftige Statuscodes**: Wählen Sie passende HTTP-Statuscodes, die das Ergebnis des Workflows genau widerspiegeln
- **Strukturieren Sie Ihre Antworten einheitlich**: Behalten Sie eine konsistente JSON-Struktur über alle Ihre API-Endpunkte bei, um eine bessere Entwicklererfahrung zu gewährleisten
- **Fügen Sie relevante Metadaten hinzu**: Fügen Sie Zeitstempel und Versionsinformationen hinzu, um bei der Fehlerbehebung und Überwachung zu helfen
- **Behandeln Sie Fehler elegant**: Verwenden Sie bedingte Logik in Ihrem Workflow, um angemessene Fehlerantworten mit aussagekräftigen Meldungen zu setzen
- **Validieren Sie Variablenreferenzen**: Stellen Sie sicher, dass alle referenzierten Variablen existieren und die erwarteten Datentypen enthalten, bevor der Antwortblock ausgeführt wird

View File

@@ -1,102 +0,0 @@
---
title: Router
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
import { Image } from '@/components/ui/image'
Der Router-Block verwendet KI, um Workflows basierend auf Inhaltsanalysen intelligent zu leiten. Im Gegensatz zu Bedingungsblöcken, die einfache Regeln verwenden, verstehen Router Kontext und Absicht.
<div className="flex justify-center">
<Image
src="/static/blocks/router.png"
alt="Router-Block mit mehreren Pfaden"
width={500}
height={400}
className="my-6"
/>
</div>
## Router vs. Bedingung
**Verwende Router, wenn:**
- KI-gestützte Inhaltsanalyse benötigt wird
- Mit unstrukturierten oder variierenden Inhalten gearbeitet wird
- Absichtsbasierte Weiterleitung erforderlich ist (z.B. "Support-Tickets an Abteilungen weiterleiten")
**Verwende Bedingung, wenn:**
- Einfache regelbasierte Entscheidungen ausreichen
- Mit strukturierten Daten oder numerischen Vergleichen gearbeitet wird
- Schnelle, deterministische Weiterleitung benötigt wird
## Konfigurationsoptionen
### Inhalt/Prompt
Der Inhalt oder Prompt, den der Router analysieren wird, um Weiterleitungsentscheidungen zu treffen. Dies kann sein:
- Eine direkte Benutzeranfrage oder -eingabe
- Ausgabe eines vorherigen Blocks
- Eine systemgenerierte Nachricht
### Zielblöcke
Die möglichen Zielblöcke, aus denen der Router auswählen kann. Der Router erkennt automatisch verbundene Blöcke, aber du kannst auch:
- Die Beschreibungen von Zielblöcken anpassen, um die Weiterleitungsgenauigkeit zu verbessern
- Weiterleitungskriterien für jeden Zielblock festlegen
- Bestimmte Blöcke von der Berücksichtigung als Weiterleitungsziele ausschließen
### Modellauswahl
Wähle ein KI-Modell für die Weiterleitungsentscheidung:
- **OpenAI**: GPT-4o, o1, o3, o4-mini, gpt-4.1
- **Anthropic**: Claude 3.7 Sonnet
- **Google**: Gemini 2.5 Pro, Gemini 2.0 Flash
- **Andere Anbieter**: Groq, Cerebras, xAI, DeepSeek
- **Lokale Modelle**: Ollama oder VLLM-kompatible Modelle
Verwende Modelle mit starken Argumentationsfähigkeiten wie GPT-4o oder Claude 3.7 Sonnet für beste Ergebnisse.
### API-Schlüssel
Ihr API-Schlüssel für den ausgewählten LLM-Anbieter. Dieser wird sicher gespeichert und für die Authentifizierung verwendet.
## Ausgaben
- **`<router.prompt>`**: Zusammenfassung des Routing-Prompts
- **`<router.selected_path>`**: Ausgewählter Zielblock
- **`<router.tokens>`**: Token-Nutzungsstatistiken
- **`<router.cost>`**: Geschätzte Routing-Kosten
- **`<router.model>`**: Für die Entscheidungsfindung verwendetes Modell
## Beispielanwendungsfälle
**Kundensupport-Triage** - Tickets an spezialisierte Abteilungen weiterleiten
```
Input (Ticket) → Router → Agent (Engineering) or Agent (Finance)
```
**Inhaltsklassifizierung** - Nutzergenerierte Inhalte klassifizieren und weiterleiten
```
Input (Feedback) → Router → Workflow (Product) or Workflow (Technical)
```
**Lead-Qualifizierung** - Leads basierend auf Qualifizierungskriterien weiterleiten
```
Input (Lead) → Router → Agent (Enterprise Sales) or Workflow (Self-serve)
```
## Best Practices
- **Klare Zielbeschreibungen bereitstellen**: Helfen Sie dem Router zu verstehen, wann jedes Ziel ausgewählt werden soll, mit spezifischen, detaillierten Beschreibungen
- **Spezifische Routing-Kriterien verwenden**: Definieren Sie klare Bedingungen und Beispiele für jeden Pfad, um die Genauigkeit zu verbessern
- **Fallback-Pfade implementieren**: Verbinden Sie ein Standardziel für Fälle, in denen kein spezifischer Pfad geeignet ist
- **Mit verschiedenen Eingaben testen**: Stellen Sie sicher, dass der Router verschiedene Eingabetypen, Grenzfälle und unerwartete Inhalte verarbeiten kann
- **Routing-Leistung überwachen**: Überprüfen Sie Routing-Entscheidungen regelmäßig und verfeinern Sie Kriterien basierend auf tatsächlichen Nutzungsmustern
- **Geeignete Modelle auswählen**: Verwenden Sie Modelle mit starken Argumentationsfähigkeiten für komplexe Routing-Entscheidungen

View File

@@ -1,86 +0,0 @@
---
title: Variablen
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Step, Steps } from 'fumadocs-ui/components/steps'
import { Image } from '@/components/ui/image'
Der Variablen-Block aktualisiert Workflow-Variablen während der Ausführung. Variablen müssen zuerst im Variablen-Bereich deines Workflows initialisiert werden, dann kannst du diesen Block verwenden, um ihre Werte während der Ausführung deines Workflows zu aktualisieren.
<div className="flex justify-center">
<Image
src="/static/blocks/variables.png"
alt="Variablen-Block"
width={500}
height={400}
className="my-6"
/>
</div>
<Callout>
Greife überall in deinem Workflow auf Variablen zu, indem du die `<variable.variableName>` Syntax verwendest.
</Callout>
## Wie man Variablen verwendet
### 1. Initialisierung in Workflow-Variablen
Erstellen Sie zunächst Ihre Variablen im Variablenbereich des Workflows (zugänglich über die Workflow-Einstellungen):
```
customerEmail = ""
retryCount = 0
currentStatus = "pending"
```
### 2. Aktualisierung mit dem Variablen-Block
Verwenden Sie den Variablen-Block, um diese Werte während der Ausführung zu aktualisieren:
```
customerEmail = <api.email>
retryCount = <variable.retryCount> + 1
currentStatus = "processing"
```
### 3. Überall zugreifen
Referenzieren Sie Variablen in jedem Block:
```
Agent prompt: "Send email to <variable.customerEmail>"
Condition: <variable.retryCount> < 5
API body: {"status": "<variable.currentStatus>"}
```
## Beispielanwendungsfälle
**Schleifenzähler und Status** - Fortschritt durch Iterationen verfolgen
```
Loop → Agent (Process) → Variables (itemsProcessed + 1) → Variables (Store lastResult)
```
**Wiederholungslogik** - API-Wiederholungsversuche verfolgen
```
API (Try) → Variables (retryCount + 1) → Condition (retryCount < 3)
```
**Dynamische Konfiguration** - Benutzerkontext für Workflow speichern
```
API (Fetch Profile) → Variables (userId, userTier) → Agent (Personalize)
```
## Ausgaben
- **`<variables.assignments>`**: JSON-Objekt mit allen Variablenzuweisungen aus diesem Block
## Bewährte Praktiken
- **In Workflow-Einstellungen initialisieren**: Erstellen Sie Variablen immer im Variablenbereich des Workflows, bevor Sie sie verwenden
- **Dynamisch aktualisieren**: Verwenden Sie Variablen-Blöcke, um Werte basierend auf Block-Ausgaben oder Berechnungen zu aktualisieren
- **In Schleifen verwenden**: Perfekt für die Verfolgung des Status über Iterationen hinweg
- **Beschreibend benennen**: Verwenden Sie klare Namen wie `currentIndex`, `totalProcessed` oder `lastError`

View File

@@ -1,67 +0,0 @@
---
title: Warten
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Step, Steps } from 'fumadocs-ui/components/steps'
import { Image } from '@/components/ui/image'
Der Warten-Block pausiert deinen Workflow für eine bestimmte Zeit, bevor er mit dem nächsten Block fortfährt. Verwende ihn, um Verzögerungen zwischen Aktionen einzufügen, API-Ratenbegrenzungen einzuhalten oder Operationen zeitlich zu verteilen.
<div className="flex justify-center">
<Image
src="/static/blocks/wait.png"
alt="Warte-Block"
width={500}
height={400}
className="my-6"
/>
</div>
## Konfiguration
### Wartezeit
Geben Sie die Dauer für die Ausführungspause ein:
- **Eingabe**: Positive Zahl
- **Maximum**: 600 Sekunden (10 Minuten) oder 10 Minuten
### Einheit
Wählen Sie die Zeiteinheit:
- **Sekunden**: Für kurze, präzise Verzögerungen
- **Minuten**: Für längere Pausen
<Callout type="info">
Warteblöcke können durch Stoppen des Workflows abgebrochen werden. Die maximale Wartezeit beträgt 10 Minuten.
</Callout>
## Ausgaben
- **`<wait.waitDuration>`**: Die Wartezeit in Millisekunden
- **`<wait.status>`**: Status der Wartezeit ('waiting', 'completed' oder 'cancelled')
## Beispielanwendungsfälle
**API-Ratenbegrenzung** - Bleiben Sie zwischen Anfragen innerhalb der API-Ratenlimits
```
API (Request 1) → Wait (2s) → API (Request 2)
```
**Zeitgesteuerte Benachrichtigungen** - Senden Sie Folgenachrichten nach einer Verzögerung
```
Function (Send Email) → Wait (5min) → Function (Follow-up)
```
**Verarbeitungsverzögerungen** - Warten Sie, bis das externe System die Verarbeitung abgeschlossen hat
```
API (Trigger Job) → Wait (30s) → API (Check Status)
```
## Bewährte Praktiken
- **Halten Sie Wartezeiten angemessen**: Verwenden Sie Wait für Verzögerungen bis zu 10 Minuten. Für längere Verzögerungen sollten Sie geplante Workflows in Betracht ziehen
- **Überwachen Sie die Ausführungszeit**: Denken Sie daran, dass Wartezeiten die Gesamtdauer des Workflows verlängern

View File

@@ -1,89 +0,0 @@
---
title: Webhook
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Image } from '@/components/ui/image'
Der Webhook-Block sendet HTTP-POST-Anfragen an externe Webhook-Endpunkte mit automatischen Webhook-Headern und optionaler HMAC-Signierung.
<div className="flex justify-center">
<Image
src="/static/blocks/webhook.png"
alt="Webhook-Block"
width={500}
height={400}
className="my-6"
/>
</div>
## Konfiguration
### Webhook-URL
Der Ziel-Endpunkt für Ihre Webhook-Anfrage. Unterstützt sowohl statische URLs als auch dynamische Werte aus anderen Blöcken.
### Payload
JSON-Daten, die im Anfrage-Body gesendet werden. Verwenden Sie den KI-Zauberstab, um Payloads zu generieren oder auf Workflow-Variablen zu verweisen:
```json
{
"event": "workflow.completed",
"data": {
"result": "<agent.content>",
"timestamp": "<function.result>"
}
}
```
### Signierungsgeheimnis
Optionales Geheimnis für die HMAC-SHA256-Payload-Signierung. Wenn angegeben, wird ein `X-Webhook-Signature`Header hinzugefügt:
```
X-Webhook-Signature: t=1704067200000,v1=5d41402abc4b2a76b9719d911017c592...
```
Um Signaturen zu verifizieren, berechnen Sie `HMAC-SHA256(secret, "${timestamp}.${body}")` und vergleichen Sie mit dem `v1`Wert.
### Zusätzliche Header
Benutzerdefinierte Schlüssel-Wert-Header, die in die Anfrage aufgenommen werden. Diese überschreiben alle automatischen Header mit demselben Namen.
## Automatische Header
Jede Anfrage enthält automatisch diese Header:
| Header | Beschreibung |
|--------|-------------|
| `Content-Type` | `application/json` |
| `X-Webhook-Timestamp` | Unix-Zeitstempel in Millisekunden |
| `X-Delivery-ID` | Eindeutige UUID für diese Zustellung |
| `Idempotency-Key` | Identisch mit `X-Delivery-ID` zur Deduplizierung |
## Ausgaben
| Ausgabe | Typ | Beschreibung |
|--------|------|-------------|
| `data` | json | Antwort-Body vom Endpunkt |
| `status` | number | HTTP-Statuscode |
| `headers` | object | Antwort-Header |
## Beispiel-Anwendungsfälle
**Externe Dienste benachrichtigen** - Workflow-Ergebnisse an Slack, Discord oder benutzerdefinierte Endpunkte senden
```
Agent → Function (format) → Webhook (notify)
```
**Externe Workflows auslösen** - Prozesse in anderen Systemen starten, wenn Bedingungen erfüllt sind
```
Condition (check) → Webhook (trigger) → Response
```
<Callout>
Der Webhook-Block verwendet immer POST. Für andere HTTP-Methoden oder mehr Kontrolle verwenden Sie den [API-Block](/blocks/api).
</Callout>

View File

@@ -1,63 +0,0 @@
---
title: Workflow-Block
description: Führe einen anderen Workflow innerhalb des aktuellen Ablaufs aus
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Image } from '@/components/ui/image'
## Was er macht
<div className='flex justify-center my-6'>
<Image
src='/static/blocks/workflow.png'
alt='Workflow-Block-Konfiguration'
width={500}
height={400}
className='rounded-xl border border-border shadow-sm'
/>
</div>
Füge einen Workflow-Block hinzu, wenn du einen untergeordneten Workflow als Teil eines größeren Ablaufs aufrufen möchtest. Der Block führt die neueste bereitgestellte Version dieses Workflows aus, wartet auf dessen Abschluss und setzt dann mit dem übergeordneten Workflow fort.
## Konfiguration
1. **Wähle einen Workflow** aus dem Dropdown-Menü (Selbstreferenzen sind blockiert, um Schleifen zu verhindern).
2. **Eingaben zuordnen**: Wenn der untergeordnete Workflow einen Eingabeformular-Trigger hat, siehst du jedes Feld und kannst übergeordnete Variablen verbinden. Die zugeordneten Werte sind das, was der untergeordnete Workflow erhält.
<div className='flex justify-center my-6'>
<Image
src='/static/blocks/workflow-2.png'
alt='Workflow-Block mit Beispiel für Eingabezuordnung'
width={700}
height={400}
className='rounded-xl border border-border shadow-sm'
/>
</div>
3. **Ausgaben**: Nachdem der untergeordnete Workflow abgeschlossen ist, stellt der Block folgendes bereit:
- `result` die endgültige Antwort des untergeordneten Workflows
- `success` ob er ohne Fehler ausgeführt wurde
- `error` Nachricht, wenn die Ausführung fehlschlägt
## Bereitstellungsstatus-Badge
Der Workflow-Block zeigt ein Bereitstellungsstatus-Badge an, das dir hilft zu verfolgen, ob der untergeordnete Workflow ausführungsbereit ist:
- **Bereitgestellt** Der untergeordnete Workflow wurde bereitgestellt und ist einsatzbereit. Der Block führt die aktuell bereitgestellte Version aus.
- **Nicht bereitgestellt** Der untergeordnete Workflow wurde noch nie bereitgestellt. Du musst ihn bereitstellen, bevor der Workflow-Block ihn ausführen kann.
- **Erneut bereitstellen** Seit der letzten Bereitstellung wurden Änderungen im untergeordneten Workflow erkannt. Klicke auf das Badge, um den untergeordneten Workflow mit den neuesten Änderungen erneut bereitzustellen.
<Callout type="warn">
Der Workflow-Block führt immer die zuletzt bereitgestellte Version des untergeordneten Workflows aus, nicht die Editor-Version. Stelle sicher, dass du nach Änderungen eine erneute Bereitstellung durchführst, damit der Block die neueste Logik verwendet.
</Callout>
## Hinweise zur Ausführung
- Untergeordnete Workflows laufen im gleichen Workspace-Kontext, sodass Umgebungsvariablen und Tools übernommen werden.
- Der Block verwendet Bereitstellungsversionierung: Jede API-, Zeitplan-, Webhook-, manuelle oder Chat-Ausführung ruft den bereitgestellten Snapshot auf. Stelle den untergeordneten Workflow nach Änderungen erneut bereit.
- Wenn der untergeordnete Workflow fehlschlägt, löst der Block einen Fehler aus, es sei denn, du behandelst ihn nachgelagert.
<Callout>
Halte untergeordnete Workflows fokussiert. Kleine, wiederverwendbare Abläufe machen es einfacher, sie zu kombinieren, ohne tiefe Verschachtelungen zu erzeugen.
</Callout>

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