mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-13 17:08:01 -05:00
Compare commits
27 Commits
v0.5.59
...
feat/aws-l
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f553667242 | ||
|
|
0c753c4394 | ||
|
|
2ac203e233 | ||
|
|
d29692ede4 | ||
|
|
f60232fa5b | ||
|
|
4ceec7ff9a | ||
|
|
0ff86a1413 | ||
|
|
4886e5aae8 | ||
|
|
6274bdcb18 | ||
|
|
7064f69520 | ||
|
|
154d8a674a | ||
|
|
a6e144ad93 | ||
|
|
d0514a39a8 | ||
|
|
ee66cd262b | ||
|
|
5aab24e1ed | ||
|
|
689d88fd7e | ||
|
|
b1047503b9 | ||
|
|
ec1eec4546 | ||
|
|
2b3989edd2 | ||
|
|
cb393c1638 | ||
|
|
c82e5ac3b3 | ||
|
|
67030d9576 | ||
|
|
8c157083bc | ||
|
|
6f07c2958e | ||
|
|
be100e4f86 | ||
|
|
46be9e3558 | ||
|
|
abf1ac06ce |
@@ -1,591 +0,0 @@
|
||||
---
|
||||
description: Create a block configuration for a Sim 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. 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
|
||||
@@ -1,450 +0,0 @@
|
||||
---
|
||||
description: Add a complete integration to Sim (tools, block, icon, registration)
|
||||
argument-hint: <service-name> [api-docs-url]
|
||||
---
|
||||
|
||||
# Add Integration Skill
|
||||
|
||||
You are an expert at adding complete integrations to Sim. 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
|
||||
@@ -1,284 +0,0 @@
|
||||
---
|
||||
description: Create tool configurations for a Sim 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 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
|
||||
@@ -1,656 +0,0 @@
|
||||
---
|
||||
description: Create webhook triggers for a Sim integration using the generic trigger builder
|
||||
argument-hint: <service-name>
|
||||
---
|
||||
|
||||
# Add Trigger Skill
|
||||
|
||||
You are an expert at creating webhook triggers for Sim. You understand the trigger system, the generic `buildTriggerSubBlocks` helper, and how triggers connect to blocks.
|
||||
|
||||
## 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)
|
||||
@@ -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
|
||||
@@ -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`.
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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'
|
||||
```
|
||||
@@ -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`
|
||||
@@ -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)
|
||||
@@ -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 })
|
||||
```
|
||||
@@ -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)' }} />
|
||||
```
|
||||
@@ -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' })
|
||||
```
|
||||
@@ -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>) => {}
|
||||
```
|
||||
69
.devcontainer/.bashrc
Normal file
69
.devcontainer/.bashrc
Normal file
@@ -0,0 +1,69 @@
|
||||
# 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/apps/sim && tail -f logs/*.log 2>/dev/null || echo 'No log files found'"
|
||||
alias sim-start="cd /workspace && bun run dev"
|
||||
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"
|
||||
|
||||
# Turbo related commands
|
||||
alias turbo-build="cd /workspace && bunx turbo run build"
|
||||
alias turbo-dev="cd /workspace && bunx turbo run dev"
|
||||
alias turbo-test="cd /workspace && bunx turbo run test"
|
||||
|
||||
# Bun specific commands
|
||||
alias bun-update="cd /workspace && bun update"
|
||||
alias bun-add="cd /workspace && bun add"
|
||||
alias bun-pm="cd /workspace && bun pm"
|
||||
alias bun-canary="bun upgrade --canary"
|
||||
|
||||
# Default to workspace directory
|
||||
cd /workspace 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 all apps in development mode"
|
||||
echo " sim-migrate - Push schema changes to the database for sim app"
|
||||
echo " sim-generate - Generate new migrations for sim app"
|
||||
echo " sim-rebuild - Build and start all apps"
|
||||
echo " docs-dev - Start only the docs app in development mode"
|
||||
echo ""
|
||||
echo "Turbo commands:"
|
||||
echo " turbo-build - Build all apps using Turborepo"
|
||||
echo " turbo-dev - Start development mode for all apps"
|
||||
echo " turbo-test - Run tests for all packages"
|
||||
echo ""
|
||||
echo "Bun commands:"
|
||||
echo " bun-update - Update dependencies"
|
||||
echo " bun-add - Add a new dependency"
|
||||
echo " bun-pm - Manage dependencies"
|
||||
echo " bun-canary - Upgrade to the latest canary version of Bun"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
fi
|
||||
@@ -1,43 +1,38 @@
|
||||
FROM oven/bun:1.3.3-alpine
|
||||
# Use the latest Bun canary image for development
|
||||
FROM oven/bun:canary
|
||||
|
||||
# Avoid warnings by switching to noninteractive
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# 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 vim nano \
|
||||
bash-completion ca-certificates lsb-release gnupg \
|
||||
&& apt-get clean -y \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Create a non-root user with matching UID/GID
|
||||
# Create a non-root user
|
||||
ARG USERNAME=bun
|
||||
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
|
||||
|
||||
# Install global packages for development
|
||||
RUN bun install -g turbo drizzle-kit typescript @types/node
|
||||
|
||||
# Install bun completions
|
||||
RUN bun completions > /etc/bash_completion.d/bun
|
||||
|
||||
# Set up shell environment
|
||||
RUN echo "export PATH=\$PATH:/home/$USERNAME/.bun/bin" >> /etc/profile
|
||||
RUN echo "export PATH=$PATH:/home/$USERNAME/.bun/bin" >> /etc/profile
|
||||
RUN echo "source /etc/profile" >> /etc/bash.bashrc
|
||||
|
||||
# Switch back to dialog for any ad-hoc use of apt-get
|
||||
ENV DEBIAN_FRONTEND=dialog
|
||||
|
||||
WORKDIR /workspace
|
||||
|
||||
|
||||
@@ -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 `bun 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).
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "Sim Dev Environment",
|
||||
"name": "Sim Studio Dev Environment",
|
||||
"dockerComposeFile": "docker-compose.yml",
|
||||
"service": "app",
|
||||
"workspaceFolder": "/workspace",
|
||||
@@ -13,6 +13,13 @@
|
||||
"source.fixAll.biome": "explicit",
|
||||
"source.organizeImports.biome": "explicit"
|
||||
},
|
||||
"terminal.integrated.defaultProfile.linux": "bash",
|
||||
"terminal.integrated.profiles.linux": {
|
||||
"bash": {
|
||||
"path": "/bin/bash",
|
||||
"args": ["--login"]
|
||||
}
|
||||
},
|
||||
"terminal.integrated.shellIntegration.enabled": true
|
||||
},
|
||||
"extensions": [
|
||||
@@ -29,9 +36,18 @@
|
||||
}
|
||||
},
|
||||
|
||||
"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": "bun",
|
||||
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/git:1": {},
|
||||
"ghcr.io/prulloac/devcontainer-features/bun:1": {
|
||||
"version": "latest"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,56 +7,53 @@ services:
|
||||
- ..:/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
|
||||
realtime:
|
||||
condition: service_healthy
|
||||
migrations:
|
||||
condition: service_completed_successfully
|
||||
ports:
|
||||
- "3000:3000"
|
||||
- "3001:3001"
|
||||
working_dir: /workspace
|
||||
healthcheck:
|
||||
test: ['CMD', 'wget', '--spider', '--quiet', 'http://127.0.0.1:3000']
|
||||
interval: 90s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
|
||||
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
|
||||
healthcheck:
|
||||
test: ['CMD', 'wget', '--spider', '--quiet', 'http://127.0.0.1:3002']
|
||||
interval: 90s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
|
||||
migrations:
|
||||
build:
|
||||
@@ -80,7 +77,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
|
||||
|
||||
@@ -3,48 +3,16 @@
|
||||
# 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
|
||||
|
||||
# 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..."
|
||||
@@ -61,12 +29,18 @@ chmod 700 ~/.bun ~/.bun/cache
|
||||
|
||||
# Install dependencies with platform-specific binaries
|
||||
echo "Installing dependencies with Bun..."
|
||||
bun install
|
||||
bun install || {
|
||||
echo "⚠️ bun 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."
|
||||
NATIVE_DEPS=$(grep '"trustedDependencies"' apps/sim/package.json || echo "")
|
||||
if [ ! -z "$NATIVE_DEPS" ]; then
|
||||
echo "⚠️ Native dependencies detected. Ensuring compatibility with Bun..."
|
||||
for pkg in $(echo $NATIVE_DEPS | grep -oP '"[^"]*"' | tr -d '"' | grep -v "trustedDependencies"); do
|
||||
echo "Checking compatibility for $pkg..."
|
||||
done
|
||||
fi
|
||||
|
||||
# Set up environment variables if .env doesn't exist for the sim app
|
||||
@@ -108,12 +82,29 @@ 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/apps/sim && DATABASE_URL=postgresql://postgres:postgres@db:5432/simstudio bunx drizzle-kit push"
|
||||
alias generate="cd /workspace/apps/sim && bunx drizzle-kit generate"
|
||||
alias dev="cd /workspace && bun run dev"
|
||||
alias build="cd /workspace && bun run build"
|
||||
alias start="cd /workspace && bun run dev"
|
||||
alias lint="cd /workspace/apps/sim && bun run lint"
|
||||
alias test="cd /workspace && bun run test"
|
||||
alias bun-update="cd /workspace && bun update"
|
||||
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:"
|
||||
|
||||
@@ -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
|
||||
@@ -1,67 +1,11 @@
|
||||
# Git
|
||||
.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
|
||||
README.md
|
||||
.gitignore
|
||||
.husky
|
||||
.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
|
||||
.env.example
|
||||
node_modules
|
||||
34
.gitattributes
vendored
34
.gitattributes
vendored
@@ -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
|
||||
4
.github/CODE_OF_CONDUCT.md
vendored
4
.github/CODE_OF_CONDUCT.md
vendored
@@ -1,4 +1,4 @@
|
||||
# Code of Conduct - Sim
|
||||
# Code of Conduct - Sim Studio
|
||||
|
||||
## Our Pledge
|
||||
|
||||
@@ -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
|
||||
|
||||
174
.github/CONTRIBUTING.md
vendored
174
.github/CONTRIBUTING.md
vendored
@@ -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 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.
|
||||
|
||||
---
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -130,7 +132,7 @@ To set up your local development environment:
|
||||
|
||||
### Option 1: Using NPM Package (Simplest)
|
||||
|
||||
The easiest way to run Sim locally is using our NPM package:
|
||||
The easiest way to run Sim Studio locally is using our NPM package:
|
||||
|
||||
```bash
|
||||
npx simstudio
|
||||
@@ -140,7 +142,7 @@ After running this command, open [http://localhost:3000/](http://localhost:3000/
|
||||
|
||||
#### Options
|
||||
|
||||
- `-p, --port <port>`: Specify the port to run Sim on (default: 3000)
|
||||
- `-p, --port <port>`: Specify the port to run Sim Studio on (default: 3000)
|
||||
- `--no-pull`: Skip pulling the latest Docker images
|
||||
|
||||
#### Requirements
|
||||
@@ -154,7 +156,7 @@ After running this command, open [http://localhost:3000/](http://localhost:3000/
|
||||
git clone https://github.com/<your-username>/sim.git
|
||||
cd sim
|
||||
|
||||
# Start Sim
|
||||
# Start Sim Studio
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
@@ -162,19 +164,15 @@ Access the application at [http://localhost:3000/](http://localhost:3000/)
|
||||
|
||||
#### Using Local Models
|
||||
|
||||
To use local models with Sim:
|
||||
To use local models with Sim Studio:
|
||||
|
||||
1. Install Ollama and pull models:
|
||||
1. Pull models using our helper script:
|
||||
|
||||
```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
|
||||
./apps/sim/scripts/ollama_docker.sh pull <model_name>
|
||||
```
|
||||
|
||||
2. Start Sim with local model support:
|
||||
2. Start Sim Studio with local model support:
|
||||
|
||||
```bash
|
||||
# With NVIDIA GPU support
|
||||
@@ -211,14 +209,13 @@ Dev Containers provide a consistent and easy-to-use development environment:
|
||||
|
||||
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
|
||||
- Run `bun run dev` in the terminal or use the `sim-start` alias
|
||||
- All dependencies and configurations are automatically set up
|
||||
- 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
|
||||
|
||||
@@ -249,11 +246,9 @@ If you prefer not to use Docker or Dev Containers:
|
||||
4. **Run the Development Server:**
|
||||
|
||||
```bash
|
||||
bun run dev:full
|
||||
bun 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.**
|
||||
|
||||
### Email Template Development
|
||||
@@ -280,7 +275,7 @@ 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
|
||||
|
||||
@@ -305,8 +300,8 @@ In addition, you will need to update the registries:
|
||||
|
||||
```typescript:/apps/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,56 +312,13 @@ 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' },
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
@@ -374,7 +326,7 @@ In addition, you will need to update the registries:
|
||||
Add your block to the blocks registry (`/apps/sim/blocks/registry.ts`):
|
||||
|
||||
```typescript:/apps/sim/blocks/registry.ts
|
||||
import { PineconeBlock } from '@/blocks/blocks/pinecone'
|
||||
import { PineconeBlock } from './blocks/pinecone'
|
||||
|
||||
// Registry of all available blocks
|
||||
export const registry: Record<string, BlockConfig> = {
|
||||
@@ -414,8 +366,8 @@ In addition, you will need to update the registries:
|
||||
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'
|
||||
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 +379,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,6 +387,9 @@ In addition, you will need to update the registries:
|
||||
transformResponse: async (response: Response) => {
|
||||
// Transform response
|
||||
},
|
||||
transformError: (error) => {
|
||||
// Handle errors
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
@@ -453,7 +397,7 @@ In addition, you will need to update the registries:
|
||||
Update the tools registry in `/apps/sim/tools/index.ts` to include your new tool:
|
||||
|
||||
```typescript:/apps/sim/tools/index.ts
|
||||
import { fetchTool, generateEmbeddingsTool, searchTextTool } from '/@tools/pinecone'
|
||||
import { fetchTool, generateEmbeddingsTool, searchTextTool } from './pinecone'
|
||||
// ... other imports
|
||||
|
||||
export const tools: Record<string, ToolConfig> = {
|
||||
@@ -485,57 +429,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 +451,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 +463,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!
|
||||
|
||||
55
.github/PULL_REQUEST_TEMPLATE.md
vendored
55
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -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 (`bun run 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.
|
||||
|
||||
4
.github/SECURITY.md
vendored
4
.github/SECURITY.md
vendored
@@ -8,11 +8,11 @@
|
||||
|
||||
## 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:
|
||||
|
||||
|
||||
66
.github/workflows/build.yml
vendored
Normal file
66
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
name: Build and Publish Docker Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ['v*']
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
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
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ matrix.image }}
|
||||
tags: |
|
||||
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}.{{minor}}.{{patch}}
|
||||
type=sha,format=long
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ${{ matrix.dockerfile }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
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
|
||||
308
.github/workflows/ci.yml
vendored
308
.github/workflows/ci.yml
vendored
@@ -6,274 +6,72 @@ on:
|
||||
pull_request:
|
||||
branches: [main, staging]
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
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
|
||||
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"
|
||||
|
||||
# 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
|
||||
with:
|
||||
context: .
|
||||
file: ${{ matrix.dockerfile }}
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
provenance: false
|
||||
sbom: false
|
||||
|
||||
# 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
|
||||
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
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v3
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
bun-version: latest
|
||||
|
||||
- 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
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
context: .
|
||||
file: ${{ matrix.dockerfile }}
|
||||
platforms: linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
provenance: false
|
||||
sbom: false
|
||||
node-version: latest
|
||||
|
||||
# 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
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
|
||||
- name: Run tests with coverage
|
||||
env:
|
||||
NODE_OPTIONS: '--no-warnings'
|
||||
NEXT_PUBLIC_APP_URL: 'https://www.simstudio.ai'
|
||||
ENCRYPTION_KEY: '7cf672e460e430c1fba707575c2b0e2ad5a99dddf9b7b7e3b5646e630861db1c' # dummy key for CI only
|
||||
run: bun run test
|
||||
|
||||
- name: Build application
|
||||
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'
|
||||
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
|
||||
|
||||
migrations:
|
||||
name: Apply Database Migrations
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging')
|
||||
needs: test
|
||||
steps:
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v3
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
bun-version: latest
|
||||
|
||||
- name: Create and push manifests
|
||||
run: |
|
||||
IMAGE_BASE="${{ matrix.image }}"
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
|
||||
# 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
|
||||
- name: Apply migrations
|
||||
working-directory: ./apps/sim
|
||||
env:
|
||||
DATABASE_URL: ${{ github.ref == 'refs/heads/main' && secrets.DATABASE_URL || secrets.STAGING_DATABASE_URL }}
|
||||
run: bunx drizzle-kit push
|
||||
|
||||
46
.github/workflows/docs-embeddings.yml
vendored
46
.github/workflows/docs-embeddings.yml
vendored
@@ -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
|
||||
180
.github/workflows/i18n.yml
vendored
180
.github/workflows/i18n.yml
vendored
@@ -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
|
||||
181
.github/workflows/images.yml
vendored
181
.github/workflows/images.yml
vendored
@@ -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 }}"
|
||||
39
.github/workflows/migrations.yml
vendored
39
.github/workflows/migrations.yml
vendored
@@ -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
|
||||
17
.github/workflows/publish-cli.yml
vendored
17
.github/workflows/publish-cli.yml
vendored
@@ -8,7 +8,7 @@ on:
|
||||
|
||||
jobs:
|
||||
publish-npm:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.3.3
|
||||
bun-version: latest
|
||||
|
||||
- name: Setup Node.js for npm publishing
|
||||
uses: actions/setup-node@v4
|
||||
@@ -24,20 +24,9 @@ jobs:
|
||||
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-
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: packages/cli
|
||||
run: bun install --frozen-lockfile
|
||||
run: bun install
|
||||
|
||||
- name: Build package
|
||||
working-directory: packages/cli
|
||||
|
||||
4
.github/workflows/publish-python-sdk.yml
vendored
4
.github/workflows/publish-python-sdk.yml
vendored
@@ -8,7 +8,7 @@ on:
|
||||
|
||||
jobs:
|
||||
publish-pypi:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
@@ -84,6 +84,6 @@ jobs:
|
||||
```
|
||||
|
||||
### 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.
|
||||
See the [README](https://github.com/simstudio/sim/tree/main/packages/python-sdk) for usage instructions.
|
||||
draft: false
|
||||
prerelease: false
|
||||
22
.github/workflows/publish-ts-sdk.yml
vendored
22
.github/workflows/publish-ts-sdk.yml
vendored
@@ -8,7 +8,7 @@ on:
|
||||
|
||||
jobs:
|
||||
publish-npm:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
@@ -16,27 +16,17 @@ jobs:
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.3.3
|
||||
bun-version: latest
|
||||
|
||||
- name: Setup Node.js for npm publishing
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
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-
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
working-directory: packages/ts-sdk
|
||||
run: bun install
|
||||
|
||||
- name: Run tests
|
||||
working-directory: packages/ts-sdk
|
||||
@@ -90,6 +80,6 @@ jobs:
|
||||
```
|
||||
|
||||
### 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.
|
||||
See the [README](https://github.com/simstudio/sim/tree/main/packages/ts-sdk) for usage instructions.
|
||||
draft: false
|
||||
prerelease: false
|
||||
117
.github/workflows/test-build.yml
vendored
117
.github/workflows/test-build.yml
vendored
@@ -1,117 +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: Validate feature flags
|
||||
run: |
|
||||
FILE="apps/sim/lib/core/config/feature-flags.ts"
|
||||
ERRORS=""
|
||||
|
||||
echo "Checking for hardcoded boolean feature flags..."
|
||||
|
||||
# Use perl for multiline matching to catch both:
|
||||
# export const isHosted = true
|
||||
# export const isHosted =
|
||||
# true
|
||||
HARDCODED=$(perl -0777 -ne 'while (/export const (is[A-Za-z]+)\s*=\s*\n?\s*(true|false)\b/g) { print " $1 = $2\n" }' "$FILE")
|
||||
|
||||
if [ -n "$HARDCODED" ]; then
|
||||
ERRORS="${ERRORS}\n❌ Feature flags must not be hardcoded to boolean literals!\n\nFound hardcoded flags:\n${HARDCODED}\n\nFeature flags should derive their values from environment variables.\n"
|
||||
fi
|
||||
|
||||
echo "Checking feature flag naming conventions..."
|
||||
|
||||
# Check that all export const (except functions) start with 'is'
|
||||
# This finds exports like "export const someFlag" that don't start with "is" or "get"
|
||||
BAD_NAMES=$(grep -E "^export const [a-z]" "$FILE" | grep -vE "^export const (is|get)" | sed 's/export const \([a-zA-Z]*\).*/ \1/')
|
||||
|
||||
if [ -n "$BAD_NAMES" ]; then
|
||||
ERRORS="${ERRORS}\n❌ Feature flags must use 'is' prefix for boolean flags!\n\nFound incorrectly named flags:\n${BAD_NAMES}\n\nExample: 'hostedMode' should be 'isHostedMode'\n"
|
||||
fi
|
||||
|
||||
if [ -n "$ERRORS" ]; then
|
||||
echo ""
|
||||
echo -e "$ERRORS"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ All feature flags are properly configured"
|
||||
|
||||
- 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
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -46,7 +46,7 @@ sim-standalone.tar.gz
|
||||
next-env.d.ts
|
||||
|
||||
# cursorrules
|
||||
# .cursorrules
|
||||
.cursorrules
|
||||
|
||||
# docs
|
||||
/apps/docs/.source
|
||||
@@ -65,11 +65,4 @@ start-collector.sh
|
||||
.turbo
|
||||
|
||||
# VSCode
|
||||
.vscode
|
||||
|
||||
# IntelliJ
|
||||
.idea
|
||||
|
||||
## Helm Chart Tests
|
||||
helm/sim/test
|
||||
i18n.cache
|
||||
.vscode
|
||||
@@ -1 +1 @@
|
||||
bunx lint-staged
|
||||
bun lint
|
||||
295
CLAUDE.md
295
CLAUDE.md
@@ -1,295 +0,0 @@
|
||||
# Sim 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
|
||||
2
LICENSE
2
LICENSE
@@ -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
4
NOTICE
@@ -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.
|
||||
190
README.md
190
README.md
@@ -1,118 +1,96 @@
|
||||
<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>
|
||||
</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>
|
||||
<img src="apps/sim/public/static/sim.png" alt="Sim Studio Logo" width="500"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://cursor.com/link/prompt?text=Help%20me%20set%20up%20Sim%20Studio%20locally.%20Follow%20these%20steps%3A%0A%0A1.%20First%2C%20verify%20Docker%20is%20installed%20and%20running%3A%0A%20%20%20docker%20--version%0A%20%20%20docker%20info%0A%0A2.%20Clone%20the%20repository%3A%0A%20%20%20git%20clone%20https%3A%2F%2Fgithub.com%2Fsimstudioai%2Fsim.git%0A%20%20%20cd%20sim%0A%0A3.%20Start%20the%20services%20with%20Docker%20Compose%3A%0A%20%20%20docker%20compose%20-f%20docker-compose.prod.yml%20up%20-d%0A%0A4.%20Wait%20for%20all%20containers%20to%20be%20healthy%20(this%20may%20take%201-2%20minutes)%3A%0A%20%20%20docker%20compose%20-f%20docker-compose.prod.yml%20ps%0A%0A5.%20Verify%20the%20app%20is%20accessible%20at%20http%3A%2F%2Flocalhost%3A3000%0A%0AIf%20there%20are%20any%20errors%2C%20help%20me%20troubleshoot%20them.%20Common%20issues%3A%0A-%20Port%203000%2C%203002%2C%20or%205432%20already%20in%20use%0A-%20Docker%20not%20running%0A-%20Insufficient%20memory%20(needs%2012GB%2B%20RAM)%0A%0AFor%20local%20AI%20models%20with%20Ollama%2C%20use%20this%20instead%20of%20step%203%3A%0A%20%20%20docker%20compose%20-f%20docker-compose.ollama.yml%20--profile%20setup%20up%20-d"><img src="https://img.shields.io/badge/Set%20Up%20with-Cursor-000000?logo=cursor&logoColor=white" alt="Set Up with Cursor"></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.
|
||||
|
||||
<p align="center">
|
||||
<img src="apps/sim/public/static/copilot.gif" alt="Copilot Demo" width="800"/>
|
||||
<img src="apps/sim/public/static/demo.gif" alt="Sim Studio Demo" width="800"/>
|
||||
</p>
|
||||
|
||||
### Integrate Vector Databases
|
||||
Upload documents to a vector store and let agents answer questions grounded in your specific content.
|
||||
## Getting Started
|
||||
|
||||
<p align="center">
|
||||
<img src="apps/sim/public/static/knowledge.gif" alt="Knowledge Uploads and Retrieval Demo" width="800"/>
|
||||
</p>
|
||||
1. Use our [cloud-hosted version](https://simstudio.ai)
|
||||
2. Self-host using one of the methods below
|
||||
|
||||
## Quickstart
|
||||
## Self-Hosting Options
|
||||
|
||||
### Cloud-hosted: [sim.ai](https://sim.ai)
|
||||
### Option 1: NPM Package (Simplest)
|
||||
|
||||
<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 run Sim Studio locally is using our [NPM package](https://www.npmjs.com/package/simstudio?activeTab=readme):
|
||||
|
||||
```bash
|
||||
npx simstudio
|
||||
```
|
||||
→ http://localhost:3000
|
||||
|
||||
#### Note
|
||||
Docker must be installed and running on your machine.
|
||||
After running these commands, open [http://localhost:3000/](http://localhost:3000/) in your browser.
|
||||
|
||||
#### Options
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-p, --port <port>` | Port to run Sim on (default `3000`) |
|
||||
| `--no-pull` | Skip pulling latest Docker images |
|
||||
- `-p, --port <port>`: Specify the port to run Sim Studio on (default: 3000)
|
||||
- `--no-pull`: Skip pulling the latest Docker images
|
||||
|
||||
### Self-hosted: Docker Compose
|
||||
#### Requirements
|
||||
|
||||
- Docker must be installed and running on your machine
|
||||
|
||||
### Option 2: Docker Compose
|
||||
|
||||
```bash
|
||||
git clone https://github.com/simstudioai/sim.git && cd sim
|
||||
# Clone the repository
|
||||
git clone https://github.com/simstudioai/sim.git
|
||||
|
||||
# Navigate to the project directory
|
||||
cd sim
|
||||
|
||||
# Start Sim Studio
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000)
|
||||
Access the application at [http://localhost:3000/](http://localhost:3000/)
|
||||
|
||||
#### Using Local Models with Ollama
|
||||
#### Using Local Models
|
||||
|
||||
Run Sim with local AI models using [Ollama](https://ollama.ai) - no external APIs required:
|
||||
To use local models with Sim Studio:
|
||||
|
||||
1. Pull models using our helper script:
|
||||
|
||||
```bash
|
||||
# Start with GPU support (automatically downloads gemma3:4b model)
|
||||
docker compose -f docker-compose.ollama.yml --profile setup up -d
|
||||
|
||||
# For CPU-only systems:
|
||||
docker compose -f docker-compose.ollama.yml --profile cpu --profile setup up -d
|
||||
./apps/sim/scripts/ollama_docker.sh pull <model_name>
|
||||
```
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
#### Using an External Ollama Instance
|
||||
|
||||
If Ollama is running on your host machine, use `host.docker.internal` instead of `localhost`:
|
||||
2. Start Sim Studio with local model support:
|
||||
|
||||
```bash
|
||||
OLLAMA_URL=http://host.docker.internal:11434 docker compose -f docker-compose.prod.yml up -d
|
||||
# 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
|
||||
```
|
||||
|
||||
On Linux, use your host's IP address or add `extra_hosts: ["host.docker.internal:host-gateway"]` to the compose file.
|
||||
|
||||
#### Using vLLM
|
||||
|
||||
Sim supports [vLLM](https://docs.vllm.ai/) for self-hosted models. Set `VLLM_BASE_URL` and optionally `VLLM_API_KEY` in your environment.
|
||||
|
||||
### Self-hosted: Dev Containers
|
||||
### Option 3: 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
|
||||
### Option 4: Manual Setup
|
||||
|
||||
**Requirements:** [Bun](https://bun.sh/), [Node.js](https://nodejs.org/) v20+, PostgreSQL 12+ with [pgvector](https://github.com/pgvector/pgvector)
|
||||
|
||||
1. Clone and install:
|
||||
1. Clone and install dependencies:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/simstudioai/sim.git
|
||||
@@ -120,81 +98,37 @@ cd sim
|
||||
bun install
|
||||
```
|
||||
|
||||
2. Set up PostgreSQL with pgvector:
|
||||
2. Set up environment:
|
||||
|
||||
```bash
|
||||
docker run --name simstudio-db -e POSTGRES_PASSWORD=your_password -e POSTGRES_DB=simstudio -p 5432:5432 -d pgvector/pgvector:pg17
|
||||
cd apps/sim
|
||||
cp .env.example .env # Configure with required variables (DATABASE_URL, BETTER_AUTH_SECRET, BETTER_AUTH_URL)
|
||||
```
|
||||
|
||||
Or install manually via the [pgvector guide](https://github.com/pgvector/pgvector#installation).
|
||||
|
||||
3. Configure environment:
|
||||
3. Set up the database:
|
||||
|
||||
```bash
|
||||
cp apps/sim/.env.example apps/sim/.env
|
||||
cp packages/db/.env.example packages/db/.env
|
||||
# Edit both .env files to set DATABASE_URL="postgresql://postgres:your_password@localhost:5432/simstudio"
|
||||
bunx drizzle-kit push
|
||||
```
|
||||
|
||||
4. Run migrations:
|
||||
4. Start the development servers:
|
||||
|
||||
Next.js app:
|
||||
|
||||
```bash
|
||||
cd packages/db && bunx drizzle-kit migrate --config=./drizzle.config.ts
|
||||
bun run dev
|
||||
```
|
||||
|
||||
5. Start development servers:
|
||||
Start the realtime server:
|
||||
|
||||
```bash
|
||||
bun run dev:full # Starts both Next.js app and realtime socket server
|
||||
bun run dev:sockets
|
||||
```
|
||||
|
||||
Or run separately: `bun run dev` (Next.js) and `cd apps/sim && bun run dev:sockets` (realtime).
|
||||
|
||||
## 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 [`.env.example`](apps/sim/.env.example) for defaults or [`env.ts`](apps/sim/lib/core/config/env.ts) for the 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 | Encrypts environment variables (`openssl rand -hex 32`) |
|
||||
| `INTERNAL_API_SECRET` | Yes | Encrypts internal API routes (`openssl rand -hex 32`) |
|
||||
| `API_ENCRYPTION_KEY` | Yes | Encrypts API keys (`openssl rand -hex 32`) |
|
||||
| `COPILOT_API_KEY` | No | API key from sim.ai for Copilot features |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Ollama models not showing in dropdown (Docker)
|
||||
|
||||
If you're running Ollama on your host machine and Sim in Docker, change `OLLAMA_URL` from `localhost` to `host.docker.internal`:
|
||||
Run both together (recommended):
|
||||
|
||||
```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
|
||||
bun run dev:full
|
||||
```
|
||||
|
||||
## Tech Stack
|
||||
@@ -209,8 +143,6 @@ NEXT_PUBLIC_APP_URL=http://localhost:3100 POSTGRES_PORT=5433 docker compose up -
|
||||
- **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
|
||||
|
||||
@@ -220,4 +152,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>
|
||||
5
apps/docs/app/(docs)/[[...slug]]/layout.tsx
Normal file
5
apps/docs/app/(docs)/[[...slug]]/layout.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
export default function SlugLayout({ children }: { children: ReactNode }) {
|
||||
return children
|
||||
}
|
||||
58
apps/docs/app/(docs)/[[...slug]]/page.tsx
Normal file
58
apps/docs/app/(docs)/[[...slug]]/page.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/page'
|
||||
import { notFound } from 'next/navigation'
|
||||
import mdxComponents from '@/components/mdx-components'
|
||||
import { source } from '@/lib/source'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export default async function Page(props: { params: Promise<{ slug?: string[] }> }) {
|
||||
const params = await props.params
|
||||
const page = source.getPage(params.slug)
|
||||
if (!page) notFound()
|
||||
|
||||
const MDX = page.data.body
|
||||
|
||||
return (
|
||||
<DocsPage
|
||||
toc={page.data.toc}
|
||||
full={page.data.full}
|
||||
tableOfContent={{
|
||||
style: 'clerk',
|
||||
enabled: true,
|
||||
header: <div className='mb-2 font-medium text-sm'>On this page</div>,
|
||||
single: false,
|
||||
}}
|
||||
article={{
|
||||
className: 'scroll-smooth max-sm:pb-16',
|
||||
}}
|
||||
tableOfContentPopover={{
|
||||
style: 'clerk',
|
||||
enabled: true,
|
||||
}}
|
||||
footer={{
|
||||
enabled: false,
|
||||
}}
|
||||
>
|
||||
<DocsTitle>{page.data.title}</DocsTitle>
|
||||
<DocsDescription>{page.data.description}</DocsDescription>
|
||||
<DocsBody>
|
||||
<MDX components={mdxComponents} />
|
||||
</DocsBody>
|
||||
</DocsPage>
|
||||
)
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return source.generateParams()
|
||||
}
|
||||
|
||||
export async function generateMetadata(props: { params: Promise<{ slug?: string[] }> }) {
|
||||
const params = await props.params
|
||||
const page = source.getPage(params.slug)
|
||||
if (!page) notFound()
|
||||
|
||||
return {
|
||||
title: page.data.title,
|
||||
description: page.data.description,
|
||||
}
|
||||
}
|
||||
46
apps/docs/app/(docs)/layout.tsx
Normal file
46
apps/docs/app/(docs)/layout.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { DocsLayout } from 'fumadocs-ui/layouts/docs'
|
||||
import { ExternalLink, GithubIcon } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { source } from '@/lib/source'
|
||||
|
||||
const GitHubLink = () => (
|
||||
<div className='fixed right-4 bottom-4 z-50'>
|
||||
<Link
|
||||
href='https://github.com/simstudioai/sim'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='flex h-8 w-8 items-center justify-center rounded-full border border-border bg-background transition-colors hover:bg-muted'
|
||||
>
|
||||
<GithubIcon className='h-4 w-4' />
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<>
|
||||
<DocsLayout
|
||||
tree={source.pageTree}
|
||||
nav={{
|
||||
title: <div className='flex items-center font-medium'>Sim Studio</div>,
|
||||
}}
|
||||
links={[
|
||||
{
|
||||
text: 'Visit Sim Studio',
|
||||
url: 'https://simstudio.ai',
|
||||
icon: <ExternalLink className='h-4 w-4' />,
|
||||
},
|
||||
]}
|
||||
sidebar={{
|
||||
defaultOpenLevel: 1,
|
||||
collapsible: true,
|
||||
footer: null,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</DocsLayout>
|
||||
<GitHubLink />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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}`, '')}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1,126 +1,4 @@
|
||||
import { sql } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { db, docsEmbeddings } from '@/lib/db'
|
||||
import { generateSearchEmbedding } from '@/lib/embeddings'
|
||||
import { createFromSource } from 'fumadocs-core/search/server'
|
||||
import { source } from '@/lib/source'
|
||||
|
||||
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([])
|
||||
}
|
||||
}
|
||||
export const { GET } = createFromSource(source)
|
||||
|
||||
@@ -1,495 +1,8 @@
|
||||
@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;
|
||||
--color-fd-primary: #802fff; /* Purple from control-bar component */
|
||||
}
|
||||
|
||||
/* Custom text highlighting styles */
|
||||
@@ -502,66 +15,4 @@ pre code .line {
|
||||
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';
|
||||
|
||||
@@ -1,38 +1,30 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { RootProvider } from 'fumadocs-ui/provider'
|
||||
import { Inter } from 'next/font/google'
|
||||
import './global.css'
|
||||
import { Analytics } from '@vercel/analytics/next'
|
||||
|
||||
export default function RootLayout({ children }: { children: ReactNode }) {
|
||||
return children
|
||||
const inter = Inter({
|
||||
subsets: ['latin'],
|
||||
})
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<html lang='en' className={inter.className} suppressHydrationWarning>
|
||||
<body className='flex min-h-screen flex-col'>
|
||||
<RootProvider>
|
||||
{children}
|
||||
<Analytics />
|
||||
</RootProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
|
||||
export const metadata = {
|
||||
metadataBase: new URL('https://docs.sim.ai'),
|
||||
title: {
|
||||
default: 'Sim Documentation - Visual Workflow Builder for AI Applications',
|
||||
template: '%s',
|
||||
},
|
||||
title: 'Sim Studio',
|
||||
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',
|
||||
'Build agents in seconds with a drag and drop workflow builder. Access comprehensive documentation to help you create efficient workflows and maximize your automation capabilities.',
|
||||
manifest: '/favicon/site.webmanifest',
|
||||
icons: {
|
||||
icon: [
|
||||
@@ -45,56 +37,6 @@ export const metadata = {
|
||||
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',
|
||||
},
|
||||
title: 'Sim Studio Docs',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
@@ -1,33 +1,16 @@
|
||||
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[] }> }
|
||||
) {
|
||||
export async function GET(_req: 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)
|
||||
const page = source.getPage(slug)
|
||||
if (!page) notFound()
|
||||
|
||||
return new NextResponse(await getLLMText(page), {
|
||||
headers: {
|
||||
'Content-Type': 'text/markdown',
|
||||
},
|
||||
})
|
||||
return new NextResponse(await getLLMText(page))
|
||||
}
|
||||
|
||||
export function generateStaticParams() {
|
||||
|
||||
@@ -1,87 +1,12 @@
|
||||
import { getLLMText } from '@/lib/llms'
|
||||
import { source } from '@/lib/source'
|
||||
|
||||
// cached forever
|
||||
export const revalidate = false
|
||||
|
||||
export async function GET() {
|
||||
const baseUrl = 'https://docs.sim.ai'
|
||||
const scan = source.getPages().map(getLLMText)
|
||||
const scanned = await Promise.all(scan)
|
||||
|
||||
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 })
|
||||
}
|
||||
return new Response(scanned.join('\n\n'))
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"aliases": {
|
||||
"uiDir": "./components/ui",
|
||||
"componentsDir": "./components",
|
||||
"blockDir": "./components",
|
||||
"cssDir": "./styles",
|
||||
"libDir": "./lib"
|
||||
},
|
||||
"baseDir": "",
|
||||
"commands": {}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
10
apps/docs/components/mdx-components.tsx
Normal file
10
apps/docs/components/mdx-components.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import defaultMdxComponents from 'fumadocs-ui/mdx'
|
||||
import { ThemeImage } from './ui/theme-image'
|
||||
|
||||
// Extend the default MDX components with our custom components
|
||||
const mdxComponents = {
|
||||
...defaultMdxComponents,
|
||||
ThemeImage,
|
||||
}
|
||||
|
||||
export default mdxComponents
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,40 +1,44 @@
|
||||
'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
|
||||
icon?: boolean
|
||||
iconSvg?: string
|
||||
}
|
||||
|
||||
export function BlockInfoCard({
|
||||
type,
|
||||
color,
|
||||
icon: IconComponent,
|
||||
icon = false,
|
||||
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 }}
|
||||
style={{ backgroundColor: color }}
|
||||
>
|
||||
{ResolvedIcon ? (
|
||||
<ResolvedIcon className='h-10 w-10 text-white' />
|
||||
) : iconSvg ? (
|
||||
{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>
|
||||
{icon && (
|
||||
<style jsx global>{`
|
||||
.block-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin: 1rem auto;
|
||||
display: block;
|
||||
}
|
||||
`}</style>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
163
apps/docs/components/ui/block-types.tsx
Normal file
163
apps/docs/components/ui/block-types.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
AgentIcon,
|
||||
ApiIcon,
|
||||
ChartBarIcon,
|
||||
CodeIcon,
|
||||
ConditionalIcon,
|
||||
ConnectIcon,
|
||||
ResponseIcon,
|
||||
} from '../icons'
|
||||
|
||||
// Custom Feature component specifically for BlockTypes to handle the 6-item layout
|
||||
const BlockFeature = ({
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
href,
|
||||
index,
|
||||
totalItems,
|
||||
itemsPerRow,
|
||||
}: {
|
||||
title: string
|
||||
description: string
|
||||
icon: React.ReactNode
|
||||
href?: string
|
||||
index: number
|
||||
totalItems: number
|
||||
itemsPerRow: number
|
||||
}) => {
|
||||
const blockColor = {
|
||||
'--block-color':
|
||||
title === 'Agent'
|
||||
? '#8b5cf6'
|
||||
: title === 'API'
|
||||
? '#3b82f6'
|
||||
: title === 'Condition'
|
||||
? '#f59e0b'
|
||||
: title === 'Function'
|
||||
? '#10b981'
|
||||
: title === 'Router'
|
||||
? '#6366f1'
|
||||
: title === 'Evaluator'
|
||||
? '#ef4444'
|
||||
: '#8b5cf6',
|
||||
} as React.CSSProperties
|
||||
|
||||
const content = (
|
||||
<>
|
||||
{index < itemsPerRow && (
|
||||
<div className='pointer-events-none absolute inset-0 h-full w-full bg-gradient-to-t from-neutral-100 to-transparent opacity-0 transition duration-200 group-hover/feature:opacity-100 dark:from-neutral-800' />
|
||||
)}
|
||||
{index >= itemsPerRow && (
|
||||
<div className='pointer-events-none absolute inset-0 h-full w-full bg-gradient-to-b from-neutral-100 to-transparent opacity-0 transition duration-200 group-hover/feature:opacity-100 dark:from-neutral-800' />
|
||||
)}
|
||||
<div
|
||||
className='relative z-10 mb-4 px-10 text-neutral-500 transition-colors duration-200 group-hover/feature:text-[color:var(--block-color,#8b5cf6)] dark:text-neutral-400 dark:group-hover/feature:text-[color:var(--block-color,#a78bfa)]'
|
||||
style={blockColor}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
<div className='relative z-10 mb-2 px-10 font-bold text-lg'>
|
||||
<div
|
||||
className='absolute inset-y-0 left-0 h-6 w-1 origin-center rounded-tr-full rounded-br-full bg-neutral-300 transition-all duration-200 group-hover/feature:h-8 group-hover/feature:bg-[color:var(--block-color,#8b5cf6)] dark:bg-neutral-700'
|
||||
style={blockColor}
|
||||
/>
|
||||
<span className='inline-block text-neutral-800 transition duration-200 group-hover/feature:translate-x-2 dark:text-neutral-100'>
|
||||
{title}
|
||||
</span>
|
||||
</div>
|
||||
<p className='relative z-10 max-w-xs px-10 text-neutral-600 text-sm dark:text-neutral-300'>
|
||||
{description}
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
|
||||
const containerClasses = cn(
|
||||
'flex flex-col lg:border-r py-5 relative group/feature dark:border-neutral-800',
|
||||
(index === 0 || index === itemsPerRow) && 'lg:border-l dark:border-neutral-800',
|
||||
index < itemsPerRow && 'lg:border-b dark:border-neutral-800',
|
||||
href && 'cursor-pointer hover:bg-neutral-50 dark:hover:bg-neutral-900/50 transition-colors'
|
||||
)
|
||||
|
||||
if (href) {
|
||||
return (
|
||||
<a href={href} className={containerClasses} style={{ textDecoration: 'none' }}>
|
||||
{content}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
return <div className={containerClasses}>{content}</div>
|
||||
}
|
||||
|
||||
export function BlockTypes() {
|
||||
const features = [
|
||||
{
|
||||
title: 'Agent',
|
||||
description:
|
||||
'Create powerful AI agents using any LLM provider with customizable system prompts and tool integrations.',
|
||||
icon: <AgentIcon className='h-6 w-6' />,
|
||||
href: '/blocks/agent',
|
||||
},
|
||||
{
|
||||
title: 'API',
|
||||
description:
|
||||
'Connect to any external API with support for all standard HTTP methods and customizable request parameters.',
|
||||
icon: <ApiIcon className='h-6 w-6' />,
|
||||
href: '/blocks/api',
|
||||
},
|
||||
{
|
||||
title: 'Condition',
|
||||
description:
|
||||
'Add a condition to the workflow to branch the execution path based on a boolean expression.',
|
||||
icon: <ConditionalIcon className='h-6 w-6' />,
|
||||
href: '/blocks/condition',
|
||||
},
|
||||
{
|
||||
title: 'Function',
|
||||
description:
|
||||
'Execute custom JavaScript or TypeScript code within your workflow to transform data or implement complex logic.',
|
||||
icon: <CodeIcon className='h-6 w-6' />,
|
||||
href: '/blocks/function',
|
||||
},
|
||||
{
|
||||
title: 'Router',
|
||||
description:
|
||||
'Intelligently direct workflow execution to different paths based on input analysis.',
|
||||
icon: <ConnectIcon className='h-6 w-6' />,
|
||||
href: '/blocks/router',
|
||||
},
|
||||
{
|
||||
title: 'Evaluator',
|
||||
description:
|
||||
'Assess content using customizable evaluation metrics and scoring criteria across multiple dimensions.',
|
||||
icon: <ChartBarIcon className='h-6 w-6' />,
|
||||
href: '/blocks/evaluator',
|
||||
},
|
||||
{
|
||||
title: 'Response',
|
||||
description:
|
||||
'Send a response back to the caller with customizable data, status, and headers.',
|
||||
icon: <ResponseIcon className='h-6 w-6' />,
|
||||
href: '/blocks/response',
|
||||
},
|
||||
]
|
||||
|
||||
const totalItems = features.length
|
||||
const itemsPerRow = 3 // For large screens
|
||||
|
||||
return (
|
||||
<div className='relative z-10 mx-auto grid max-w-7xl grid-cols-1 py-10 md:grid-cols-2 lg:grid-cols-3'>
|
||||
{features.map((feature, index) => (
|
||||
<BlockFeature
|
||||
key={feature.title}
|
||||
{...feature}
|
||||
index={index}
|
||||
totalItems={totalItems}
|
||||
itemsPerRow={itemsPerRow}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
102
apps/docs/components/ui/features.tsx
Normal file
102
apps/docs/components/ui/features.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import {
|
||||
IconAdjustmentsBolt,
|
||||
IconCloud,
|
||||
IconEaseInOut,
|
||||
IconHeart,
|
||||
IconHelp,
|
||||
IconHistory,
|
||||
IconRouteAltLeft,
|
||||
IconTerminal2,
|
||||
} from '@tabler/icons-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function Features() {
|
||||
const features = [
|
||||
{
|
||||
title: 'Multi-LLM Support',
|
||||
description: 'Connect to any LLM provider including OpenAI, Anthropic, and more',
|
||||
icon: <IconCloud />,
|
||||
},
|
||||
{
|
||||
title: 'API Deployment',
|
||||
description: 'Deploy your workflows as secure, scalable APIs',
|
||||
icon: <IconTerminal2 />,
|
||||
},
|
||||
{
|
||||
title: 'Webhook Integration',
|
||||
description: 'Trigger workflows via webhooks from external services',
|
||||
icon: <IconRouteAltLeft />,
|
||||
},
|
||||
{
|
||||
title: 'Scheduled Execution',
|
||||
description: 'Schedule workflows to run at specific times or intervals',
|
||||
icon: <IconEaseInOut />,
|
||||
},
|
||||
{
|
||||
title: '40+ Integrations',
|
||||
description: 'Connect to hundreds of external services and data sources',
|
||||
icon: <IconAdjustmentsBolt />,
|
||||
},
|
||||
{
|
||||
title: 'Visual Debugging',
|
||||
description: 'Debug workflows visually with detailed execution logs',
|
||||
icon: <IconHelp />,
|
||||
},
|
||||
{
|
||||
title: 'Version Control',
|
||||
description: 'Track changes and roll back to previous versions',
|
||||
icon: <IconHistory />,
|
||||
},
|
||||
{
|
||||
title: 'Team Collaboration',
|
||||
description: 'Collaborate with team members on workflow development',
|
||||
icon: <IconHeart />,
|
||||
},
|
||||
]
|
||||
return (
|
||||
<div className='relative z-20 mx-auto grid max-w-7xl grid-cols-1 py-10 md:grid-cols-2 lg:grid-cols-4'>
|
||||
{features.map((feature, index) => (
|
||||
<Feature key={feature.title} {...feature} index={index} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Feature = ({
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
index,
|
||||
}: {
|
||||
title: string
|
||||
description: string
|
||||
icon: React.ReactNode
|
||||
index: number
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'group/feature relative flex flex-col py-5 lg:border-r dark:border-neutral-800',
|
||||
(index === 0 || index === 4) && 'lg:border-l dark:border-neutral-800',
|
||||
index < 4 && 'lg:border-b dark:border-neutral-800'
|
||||
)}
|
||||
>
|
||||
{index < 4 && (
|
||||
<div className='pointer-events-none absolute inset-0 h-full w-full bg-gradient-to-t from-neutral-100 to-transparent opacity-0 transition duration-200 group-hover/feature:opacity-100 dark:from-neutral-800' />
|
||||
)}
|
||||
{index >= 4 && (
|
||||
<div className='pointer-events-none absolute inset-0 h-full w-full bg-gradient-to-b from-neutral-100 to-transparent opacity-0 transition duration-200 group-hover/feature:opacity-100 dark:from-neutral-800' />
|
||||
)}
|
||||
<div className='relative z-10 mb-4 px-10 text-neutral-600 dark:text-neutral-400'>{icon}</div>
|
||||
<div className='relative z-10 mb-2 px-10 font-bold text-lg'>
|
||||
<div className='absolute inset-y-0 left-0 h-6 w-1 origin-center rounded-tr-full rounded-br-full bg-neutral-300 transition-all duration-200 group-hover/feature:h-8 group-hover/feature:bg-purple-500 dark:bg-neutral-700' />
|
||||
<span className='inline-block text-neutral-800 transition duration-200 group-hover/feature:translate-x-2 dark:text-neutral-100'>
|
||||
{title}
|
||||
</span>
|
||||
</div>
|
||||
<p className='relative z-10 max-w-xs px-10 text-neutral-600 text-sm dark:text-neutral-300'>
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -1,251 +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 {
|
||||
A2AIcon,
|
||||
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> = {
|
||||
a2a: A2AIcon,
|
||||
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,
|
||||
}
|
||||
@@ -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'
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
48
apps/docs/components/ui/theme-image.tsx
Normal file
48
apps/docs/components/ui/theme-image.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import Image from 'next/image'
|
||||
import { useTheme } from 'next-themes'
|
||||
|
||||
interface ThemeImageProps {
|
||||
lightSrc: string
|
||||
darkSrc: string
|
||||
alt: string
|
||||
width?: number
|
||||
height?: number
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function ThemeImage({
|
||||
lightSrc,
|
||||
darkSrc,
|
||||
alt,
|
||||
width = 600,
|
||||
height = 400,
|
||||
className = 'rounded-lg border border-border my-6',
|
||||
}: ThemeImageProps) {
|
||||
const { resolvedTheme } = useTheme()
|
||||
const [imageSrc, setImageSrc] = useState(lightSrc)
|
||||
const [mounted, setMounted] = useState(false)
|
||||
|
||||
// Wait until component is mounted to avoid hydration mismatch
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (mounted) {
|
||||
setImageSrc(resolvedTheme === 'dark' ? darkSrc : lightSrc)
|
||||
}
|
||||
}, [resolvedTheme, lightSrc, darkSrc, mounted])
|
||||
|
||||
if (!mounted) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex justify-center'>
|
||||
<Image src={imageSrc} alt={alt} width={width} height={height} className={className} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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'
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
212
apps/docs/content/docs/blocks/agent.mdx
Normal file
212
apps/docs/content/docs/blocks/agent.mdx
Normal file
@@ -0,0 +1,212 @@
|
||||
---
|
||||
title: Agent
|
||||
description: Create powerful AI agents using any LLM provider
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps'
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
|
||||
import { ThemeImage } from '@/components/ui/theme-image'
|
||||
|
||||
The Agent block is a fundamental component in Sim Studio that allows you to create powerful AI agents using various LLM providers. These agents can process inputs based on customizable system prompts and utilize integrated tools to enhance their capabilities.
|
||||
|
||||
<ThemeImage
|
||||
lightSrc="/static/light/agent-light.png"
|
||||
darkSrc="/static/dark/agent-dark.png"
|
||||
alt="Agent Block"
|
||||
width={300}
|
||||
height={175}
|
||||
/>
|
||||
|
||||
<Callout type="info">
|
||||
Agent blocks serve as interfaces to Large Language Models, enabling your workflow to leverage
|
||||
state-of-the-art AI capabilities.
|
||||
</Callout>
|
||||
|
||||
## Overview
|
||||
|
||||
The Agent block serves as an interface to Large Language Models (LLMs), enabling you to create agents that can:
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
<strong>Respond to user inputs</strong>: Generate natural language responses based on provided
|
||||
inputs
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Follow instructions</strong>: Adhere to specific instructions defined in the system
|
||||
prompt
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Use specialized tools</strong>: Interact with integrated tools to extend capabilities
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Structure output</strong>: Generate responses in structured formats when needed
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### System Prompt
|
||||
|
||||
The system prompt defines the agent's behavior, capabilities, and limitations. It's the primary way to instruct the agent on how to respond to inputs.
|
||||
|
||||
```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.
|
||||
```
|
||||
|
||||
### User Prompt
|
||||
|
||||
The user prompt or context is the specific input or question that the agent should respond to. This can be:
|
||||
|
||||
- Directly provided in the block configuration
|
||||
- Connected from another block's output
|
||||
- Dynamically generated during workflow execution
|
||||
|
||||
### Model Selection
|
||||
|
||||
Choose from a variety of LLM providers:
|
||||
|
||||
- OpenAI (GPT-4o, o1, o3, o4-mini, gpt-4.1)
|
||||
- Anthropic (Claude 3.7 Sonnet)
|
||||
- Google (Gemini 2.5 Pro, Gemini 2.0 Flash)
|
||||
- Groq, Cerebras
|
||||
- Ollama Local Models
|
||||
- And more
|
||||
|
||||
### Temperature
|
||||
|
||||
Control the creativity and randomness of responses:
|
||||
|
||||
<Tabs items={['Low (0-0.3)', 'Medium (0.3-0.7)', 'High (0.7-2.0)']}>
|
||||
<Tab>
|
||||
<p>
|
||||
More deterministic, focused responses. Best for factual tasks, customer support, and
|
||||
situations where accuracy is critical.
|
||||
</p>
|
||||
</Tab>
|
||||
<Tab>
|
||||
<p>
|
||||
Balanced creativity and focus. Suitable for general purpose applications that require both
|
||||
accuracy and some creativity.
|
||||
</p>
|
||||
</Tab>
|
||||
<Tab>
|
||||
<p>
|
||||
More creative, varied responses. Ideal for creative writing, brainstorming, and generating
|
||||
diverse ideas.
|
||||
</p>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
<p className="mt-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
The temperature range (0-1 or 0-2) varies depending on the selected model.
|
||||
</p>
|
||||
|
||||
### API Key
|
||||
|
||||
Your API key for the selected LLM provider. This is securely stored and used for authentication.
|
||||
|
||||
### Tools
|
||||
|
||||
Integrate specialized tools to enhance the agent's capabilities. You can add tools to your agent by:
|
||||
|
||||
1. Clicking the Tools section in the Agent configuration
|
||||
2. Selecting from the tools dropdown menu
|
||||
3. Choosing an existing tool or creating a new one
|
||||
|
||||
<ThemeImage
|
||||
lightSrc="/static/light/tooldropdown-light.png"
|
||||
darkSrc="/static/dark/tooldropdown-dark.png"
|
||||
alt="Tools Dropdown"
|
||||
width={150}
|
||||
height={125}
|
||||
/>
|
||||
|
||||
Available tools include:
|
||||
|
||||
- **Confluence**: Access and query Confluence knowledge bases
|
||||
- **Evaluator**: Use evaluation metrics to assess content
|
||||
- **GitHub**: Interact with GitHub repositories and issues
|
||||
- **Gmail**: Process and respond to emails
|
||||
- **Firecrawl**: Web search and content retrieval
|
||||
- And many, many more pre-built integrations
|
||||
|
||||
You can also create custom tools to meet specific requirements for your agent's capabilities.
|
||||
|
||||
<Callout type="info">
|
||||
Tools significantly expand what your agent can do, allowing it to access external systems,
|
||||
retrieve information, and take actions beyond simple text generation.
|
||||
</Callout>
|
||||
|
||||
### Response Format
|
||||
|
||||
Define a structured format for the agent's response when needed, using JSON or other formats.
|
||||
|
||||
## Inputs and Outputs
|
||||
|
||||
<Tabs items={['Inputs', 'Outputs']}>
|
||||
<Tab>
|
||||
<ul className="list-disc space-y-2 pl-6">
|
||||
<li>
|
||||
<strong>User Prompt</strong>: The user's query or context for the agent
|
||||
</li>
|
||||
<li>
|
||||
<strong>System Prompt</strong>: Instructions for the agent (optional)
|
||||
</li>
|
||||
<li>
|
||||
<strong>Tools</strong>: Optional tool connections that the agent can use
|
||||
</li>
|
||||
</ul>
|
||||
</Tab>
|
||||
<Tab>
|
||||
<ul className="list-disc space-y-2 pl-6">
|
||||
<li>
|
||||
<strong>Content</strong>: The agent's response text
|
||||
</li>
|
||||
<li>
|
||||
<strong>Model</strong>: The model used for generation
|
||||
</li>
|
||||
<li>
|
||||
<strong>Tokens</strong>: Usage statistics (prompt, completion, total)
|
||||
</li>
|
||||
<li>
|
||||
<strong>Tool Calls</strong>: Details of any tools used during processing
|
||||
</li>
|
||||
<li>
|
||||
<strong>Cost</strong>: Cost of the response
|
||||
</li>
|
||||
<li>
|
||||
<strong>Usage</strong>: Usage statistics (prompt, completion, total)
|
||||
</li>
|
||||
</ul>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Example Usage
|
||||
|
||||
Here's an example of how an Agent block might be configured for a customer support workflow:
|
||||
|
||||
```yaml
|
||||
# Example Agent Configuration
|
||||
systemPrompt: |
|
||||
You are a customer support agent for TechCorp.
|
||||
Always maintain a professional, friendly tone.
|
||||
If you don't know an answer, direct the customer to email support@techcorp.com.
|
||||
Never make up information about products or policies.
|
||||
|
||||
model: OpenAI/gpt-4
|
||||
temperature: 0.2
|
||||
tools:
|
||||
- ProductDatabase
|
||||
- OrderHistory
|
||||
- SupportTicketCreator
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Be specific in system prompts**: Clearly define the agent's role, tone, and limitations. The more specific your instructions are, the better the agent will be able to fulfill its intended purpose.
|
||||
- **Choose the right temperature setting**: Use lower temperature settings (0-0.3) when accuracy is important, or increase temperature (0.7-2.0) for more creative or varied responses
|
||||
- **Combine with Evaluator blocks**: Use Evaluator blocks to assess agent responses and ensure quality. This allows you to create feedback loops and implement quality control measures.
|
||||
- **Leverage tools effectively**: Integrate tools that complement the agent's purpose and enhance its capabilities. Be selective about which tools you provide to avoid overwhelming the agent.
|
||||
128
apps/docs/content/docs/blocks/api.mdx
Normal file
128
apps/docs/content/docs/blocks/api.mdx
Normal file
@@ -0,0 +1,128 @@
|
||||
---
|
||||
title: API
|
||||
description: Connect to external services through API endpoints
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps'
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
|
||||
import { ThemeImage } from '@/components/ui/theme-image'
|
||||
|
||||
The API block enables you to connect your workflow to external services through HTTP requests. It supports various methods like GET, POST, PUT, DELETE, and PATCH, allowing you to interact with virtually any API endpoint.
|
||||
|
||||
<ThemeImage
|
||||
lightSrc="/static/light/api-light.png"
|
||||
darkSrc="/static/dark/api-dark.png"
|
||||
alt="API Block"
|
||||
width={300}
|
||||
height={175}
|
||||
/>
|
||||
|
||||
## Overview
|
||||
|
||||
The API block enables you to:
|
||||
|
||||
- Make HTTP requests to external services and APIs
|
||||
- Process and transform data from external sources
|
||||
- Send data to external systems
|
||||
- Integrate with third-party platforms and services
|
||||
- Create webhooks and callbacks
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### URL
|
||||
|
||||
The endpoint URL for the API request. This can be:
|
||||
|
||||
- A static URL entered directly in the block
|
||||
- A dynamic URL connected from another block's output
|
||||
- A URL with path parameters
|
||||
|
||||
### Method
|
||||
|
||||
Select the HTTP method for your request:
|
||||
|
||||
- **GET**: Retrieve data from the server
|
||||
- **POST**: Send data to the server to create a resource
|
||||
- **PUT**: Update an existing resource on the server
|
||||
- **DELETE**: Remove a resource from the server
|
||||
- **PATCH**: Partially update an existing resource
|
||||
|
||||
### Query Parameters
|
||||
|
||||
Define key-value pairs that will be appended to the URL as query parameters. For example:
|
||||
|
||||
```
|
||||
Key: apiKey
|
||||
Value: your_api_key_here
|
||||
|
||||
Key: limit
|
||||
Value: 10
|
||||
```
|
||||
|
||||
These would be added to the URL as `?apiKey=your_api_key_here&limit=10`.
|
||||
|
||||
### Headers
|
||||
|
||||
Configure HTTP headers for your request. Common headers include:
|
||||
|
||||
```
|
||||
Key: Content-Type
|
||||
Value: application/json
|
||||
|
||||
Key: Authorization
|
||||
Value: Bearer your_token_here
|
||||
```
|
||||
|
||||
### Request Body
|
||||
|
||||
For methods that support a request body (POST, PUT, PATCH), you can define the data to send. The body can be:
|
||||
|
||||
- JSON data entered directly in the block
|
||||
- Data connected from another block's output
|
||||
- Dynamically generated during workflow execution
|
||||
|
||||
## Inputs and Outputs
|
||||
|
||||
### Inputs
|
||||
|
||||
- **URL**: The endpoint to send the request to
|
||||
- **Method**: The HTTP method to use
|
||||
- **Query Parameters**: Key-value pairs for URL parameters
|
||||
- **Headers**: HTTP headers for the request
|
||||
- **Body**: Data to send with the request (for applicable methods)
|
||||
|
||||
### Outputs
|
||||
|
||||
- **Status Code**: The HTTP status code returned by the server
|
||||
- **Response Body**: The data returned by the server
|
||||
- **Headers**: Response headers from the server
|
||||
- **Error**: Any error information if the request fails
|
||||
|
||||
## Example Usage
|
||||
|
||||
Here's an example of how an API block might be configured to fetch weather data:
|
||||
|
||||
```yaml
|
||||
# Example API Configuration
|
||||
url: https://api.weatherapi.com/v1/current.json
|
||||
method: GET
|
||||
params:
|
||||
- key: key
|
||||
value: your_api_key_here
|
||||
- key: q
|
||||
value: London
|
||||
- key: aqi
|
||||
value: no
|
||||
headers:
|
||||
- key: Accept
|
||||
value: application/json
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Use environment variables for sensitive data**: Don't hardcode API keys or credentials
|
||||
- **Handle errors gracefully**: Connect error handling logic for failed requests
|
||||
- **Validate responses**: Check status codes and response formats before processing data
|
||||
- **Respect rate limits**: Be mindful of API rate limits and implement appropriate throttling
|
||||
- **Cache responses when appropriate**: For frequently accessed data that doesn't change often
|
||||
191
apps/docs/content/docs/blocks/condition.mdx
Normal file
191
apps/docs/content/docs/blocks/condition.mdx
Normal file
@@ -0,0 +1,191 @@
|
||||
---
|
||||
title: Condition
|
||||
description: Create conditional logic and branching in your workflows
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { File, Files, Folder } from 'fumadocs-ui/components/files'
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps'
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
|
||||
import { ThemeImage } from '@/components/ui/theme-image'
|
||||
|
||||
The Condition block allows you to branch your workflow execution path based on boolean expressions. It evaluates conditions and routes the workflow accordingly, enabling you to create dynamic, responsive workflows with different execution paths.
|
||||
|
||||
<ThemeImage
|
||||
lightSrc="/static/light/condition-light.png"
|
||||
darkSrc="/static/dark/condition-dark.png"
|
||||
alt="Condition Block"
|
||||
width={300}
|
||||
height={175}
|
||||
/>
|
||||
|
||||
<Callout>
|
||||
Condition blocks enable deterministic decision-making without requiring an LLM, making them ideal
|
||||
for straightforward branching logic.
|
||||
</Callout>
|
||||
|
||||
## Overview
|
||||
|
||||
The Condition block serves as a decision point in your workflow, enabling:
|
||||
|
||||
<div className="my-6 grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div className="rounded-lg border border-gray-200 p-4 dark:border-gray-800">
|
||||
<h3 className="mb-2 text-lg font-medium">Branching Logic</h3>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Create different execution paths based on specific conditions
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-gray-200 p-4 dark:border-gray-800">
|
||||
<h3 className="mb-2 text-lg font-medium">Rule-Based Routing</h3>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Route workflows deterministically without needing an LLM
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-gray-200 p-4 dark:border-gray-800">
|
||||
<h3 className="mb-2 text-lg font-medium">Data-Driven Decisions</h3>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Create workflow paths based on structured data values
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-gray-200 p-4 dark:border-gray-800">
|
||||
<h3 className="mb-2 text-lg font-medium">If-Then-Else Logic</h3>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Implement conditional programming paradigms in your workflows
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
## How It Works
|
||||
|
||||
The Condition block:
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
<strong>Evaluate Expression</strong>: Evaluates a boolean expression or condition
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Determine Result</strong>: Determines whether the condition evaluates to true or false
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Route Workflow</strong>: Routes the workflow to the appropriate path based on the result
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Provide Context</strong>: Provides context about the decision made
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Conditions
|
||||
|
||||
Define one or more conditions that will be evaluated. Each condition includes:
|
||||
|
||||
- **Expression**: A JavaScript/TypeScript expression that evaluates to true or false
|
||||
- **Path**: The destination block to route to if the condition is true
|
||||
- **Description**: Optional explanation of what the condition checks
|
||||
|
||||
You can create multiple conditions that are evaluated in order, with the first matching condition determining the execution path.
|
||||
|
||||
### Condition Expression Format
|
||||
|
||||
Conditions use JavaScript syntax and can reference input values from previous blocks.
|
||||
|
||||
<Tabs items={['Score Threshold', 'Text Analysis', 'Multiple Conditions']}>
|
||||
<Tab>
|
||||
```javascript
|
||||
// Check if a score is above a threshold
|
||||
input.score > 75
|
||||
```
|
||||
</Tab>
|
||||
<Tab>
|
||||
```javascript
|
||||
// Check if a text contains specific keywords
|
||||
input.text.includes('urgent') || input.text.includes('emergency')
|
||||
```
|
||||
</Tab>
|
||||
<Tab>
|
||||
```javascript
|
||||
// Check multiple conditions
|
||||
input.age >= 18 && input.country === 'US'
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Inputs and Outputs
|
||||
|
||||
<Tabs items={['Inputs', 'Outputs']}>
|
||||
<Tab>
|
||||
<ul className="list-disc space-y-2 pl-6">
|
||||
<li>
|
||||
<strong>Variables</strong>: Values from previous blocks that can be referenced in conditions
|
||||
</li>
|
||||
<li>
|
||||
<strong>Conditions</strong>: Boolean expressions to evaluate
|
||||
</li>
|
||||
</ul>
|
||||
</Tab>
|
||||
<Tab>
|
||||
<ul className="list-disc space-y-2 pl-6">
|
||||
<li>
|
||||
<strong>Content</strong>: A description of the evaluation result
|
||||
</li>
|
||||
<li>
|
||||
<strong>Condition Result</strong>: The boolean result of the condition evaluation
|
||||
</li>
|
||||
<li>
|
||||
<strong>Selected Path</strong>: Details of the chosen routing destination
|
||||
</li>
|
||||
<li>
|
||||
<strong>Selected Condition ID</strong>: Identifier of the condition that was matched
|
||||
</li>
|
||||
</ul>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Example Usage
|
||||
|
||||
Here's an example of how a Condition block might be used in a customer satisfaction workflow:
|
||||
|
||||
```yaml
|
||||
# Example Condition Configuration
|
||||
conditions:
|
||||
- id: 'high_satisfaction'
|
||||
expression: 'input.satisfactionScore >= 8'
|
||||
description: 'Customer is highly satisfied'
|
||||
path: 'positive_feedback_block'
|
||||
|
||||
- id: 'medium_satisfaction'
|
||||
expression: 'input.satisfactionScore >= 5'
|
||||
description: 'Customer is moderately satisfied'
|
||||
path: 'neutral_feedback_block'
|
||||
|
||||
- id: 'default'
|
||||
expression: 'true'
|
||||
description: 'Customer is not satisfied'
|
||||
path: 'improvement_feedback_block'
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Order conditions correctly
|
||||
|
||||
Conditions are evaluated in order, so place more specific conditions before general ones. This ensures that more specific logic takes precedence over general fallbacks.
|
||||
|
||||
### Include a default condition
|
||||
|
||||
Add a catch-all condition (e.g., `true`) as the last condition to handle cases when no other conditions match. This prevents workflow execution from getting stuck.
|
||||
|
||||
### Keep expressions simple
|
||||
|
||||
Use clear, straightforward boolean expressions for better readability. Complex expressions can be difficult to debug and maintain.
|
||||
|
||||
### Document your conditions
|
||||
|
||||
Add descriptions to explain the purpose of each condition. This helps other team members understand the logic and makes maintenance easier.
|
||||
|
||||
### Test edge cases
|
||||
|
||||
Ensure your conditions handle boundary values correctly. Test with values at the edges of your condition ranges to verify correct behavior.
|
||||
128
apps/docs/content/docs/blocks/evaluator.mdx
Normal file
128
apps/docs/content/docs/blocks/evaluator.mdx
Normal file
@@ -0,0 +1,128 @@
|
||||
---
|
||||
title: Evaluator
|
||||
description: Assess content quality using customizable evaluation metrics
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps'
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
|
||||
import { ThemeImage } from '@/components/ui/theme-image'
|
||||
|
||||
The Evaluator block allows you to assess the quality of content using customizable evaluation metrics. This is particularly useful for evaluating AI-generated text, ensuring outputs meet specific criteria, and building quality-control mechanisms into your workflows.
|
||||
|
||||
<ThemeImage
|
||||
lightSrc="/static/light/evaluator-light.png"
|
||||
darkSrc="/static/dark/evaluator-dark.png"
|
||||
alt="Evaluator Block"
|
||||
width={300}
|
||||
height={175}
|
||||
/>
|
||||
|
||||
## Overview
|
||||
|
||||
The Evaluator block utilizes LLMs to objectively evaluate content based on custom metrics you define. This is especially useful for:
|
||||
|
||||
- Assessing the quality of AI-generated content
|
||||
- Evaluating responses against specific criteria
|
||||
- Creating scoring frameworks for different types of content
|
||||
- Building objective feedback loops in your workflows
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Evaluation Metrics
|
||||
|
||||
Define custom metrics to evaluate content against. Each metric includes:
|
||||
|
||||
- **Name**: A short identifier for the metric
|
||||
- **Description**: A detailed explanation of what the metric measures
|
||||
- **Range**: The numeric range for scoring (e.g., 1-5, 0-10)
|
||||
|
||||
Example metrics:
|
||||
|
||||
```
|
||||
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?
|
||||
```
|
||||
|
||||
### Content
|
||||
|
||||
The content to be evaluated. This can be:
|
||||
|
||||
- Directly provided in the block configuration
|
||||
- Connected from another block's output (typically an Agent block)
|
||||
- Dynamically generated during workflow execution
|
||||
|
||||
### Model Selection
|
||||
|
||||
Choose an LLM provider to perform the evaluation:
|
||||
|
||||
- OpenAI (GPT-4o, o1, o3, , gpt-4.1)
|
||||
- Anthropic (Claude 3.7 Sonnet)
|
||||
- Google (Gemini 2.5 Pro, Gemini 2.0 Flash)
|
||||
- Groq, Cerebras
|
||||
- Ollama Local Models
|
||||
- And more
|
||||
|
||||
The chosen model should have strong reasoning capabilities to provide accurate evaluations.
|
||||
|
||||
### API Key
|
||||
|
||||
Your API key for the selected LLM provider. This is securely stored and used for authentication.
|
||||
|
||||
## How It Works
|
||||
|
||||
1. The Evaluator block takes the provided content and your custom metrics
|
||||
2. It generates a specialized prompt that instructs the LLM to evaluate the content
|
||||
3. The prompt includes clear guidelines on how to score each metric
|
||||
4. The LLM evaluates the content and returns numeric scores for each metric
|
||||
5. The Evaluator block formats these scores as structured output for use in your workflow
|
||||
|
||||
## Inputs and Outputs
|
||||
|
||||
### Inputs
|
||||
|
||||
- **Content**: The text or structured data to evaluate
|
||||
- **Metrics**: Custom evaluation criteria with scoring ranges
|
||||
- **Model Settings**: LLM provider and parameters
|
||||
|
||||
### Outputs
|
||||
|
||||
- **Content**: A summary of the evaluation
|
||||
- **Model**: The model used for evaluation
|
||||
- **Tokens**: Usage statistics
|
||||
- **Metric Scores**: Numeric scores for each defined metric
|
||||
|
||||
## Example Usage
|
||||
|
||||
Here's an example of how an Evaluator block might be configured for assessing customer service responses:
|
||||
|
||||
```yaml
|
||||
# Example Evaluator Configuration
|
||||
metrics:
|
||||
- name: Empathy
|
||||
description: How well does the response acknowledge and address the customer's emotional state?
|
||||
range:
|
||||
min: 1
|
||||
max: 5
|
||||
- name: Solution
|
||||
description: How effectively does the response solve the customer's problem?
|
||||
range:
|
||||
min: 1
|
||||
max: 5
|
||||
- name: Clarity
|
||||
description: How clear and easy to understand is the response?
|
||||
range:
|
||||
min: 1
|
||||
max: 5
|
||||
|
||||
model: Anthropic/claude-3-opus
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Use specific metric descriptions**: Clearly define what each metric measures to get more accurate evaluations
|
||||
- **Choose appropriate ranges**: Select scoring ranges that provide enough granularity without being overly complex
|
||||
- **Connect with Agent blocks**: Use Evaluator blocks to assess Agent block outputs and create feedback loops
|
||||
- **Use consistent metrics**: For comparative analysis, maintain consistent metrics across similar evaluations
|
||||
- **Combine multiple metrics**: Use several metrics to get a comprehensive evaluation
|
||||
137
apps/docs/content/docs/blocks/function.mdx
Normal file
137
apps/docs/content/docs/blocks/function.mdx
Normal file
@@ -0,0 +1,137 @@
|
||||
---
|
||||
title: Function
|
||||
description: Execute custom JavaScript or TypeScript code in your workflows
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps'
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
|
||||
import { ThemeImage } from '@/components/ui/theme-image'
|
||||
|
||||
The Function block allows you to write and execute custom JavaScript or TypeScript code directly within your workflow. This powerful feature enables you to implement complex logic, data transformations, and integration with external libraries.
|
||||
|
||||
<ThemeImage
|
||||
lightSrc="/static/light/function-light.png"
|
||||
darkSrc="/static/dark/function-dark.png"
|
||||
alt="Function Block"
|
||||
width={300}
|
||||
height={175}
|
||||
/>
|
||||
|
||||
## Overview
|
||||
|
||||
The Function block brings the full power of JavaScript/TypeScript to your workflows, allowing for:
|
||||
|
||||
- Custom data transformation and manipulation
|
||||
- Complex conditional logic
|
||||
- Mathematical calculations and algorithms
|
||||
- Integration with external libraries
|
||||
- Creation of reusable utility functions
|
||||
|
||||
## How It Works
|
||||
|
||||
The Function block:
|
||||
|
||||
1. Takes your custom JavaScript/TypeScript code
|
||||
2. Executes it in a secure, isolated environment
|
||||
3. Processes any inputs provided from previous blocks
|
||||
4. Returns the result for use in subsequent blocks
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Code Editor
|
||||
|
||||
A full-featured code editor where you can write your JavaScript/TypeScript code. The editor supports:
|
||||
|
||||
- Syntax highlighting
|
||||
- Code completion
|
||||
- Error checking
|
||||
- Multiple lines of code
|
||||
|
||||
### Inputs
|
||||
|
||||
Your function can access inputs from previous blocks through an `input` object. For example:
|
||||
|
||||
```javascript
|
||||
// Access data from a previous block
|
||||
const customerName = input.customerData.name;
|
||||
const orderTotal = input.orderData.total;
|
||||
|
||||
// Process the data
|
||||
const discount = orderTotal > 100 ? 0.1 : 0;
|
||||
const finalPrice = orderTotal * (1 - discount);
|
||||
|
||||
// Return the result
|
||||
return {
|
||||
customerName,
|
||||
originalTotal: orderTotal,
|
||||
discount: discount * 100 + '%',
|
||||
finalPrice
|
||||
};
|
||||
```
|
||||
|
||||
## Safety and Limitations
|
||||
|
||||
For security and performance reasons, function execution has certain limitations:
|
||||
|
||||
- **Execution Time**: Functions have a maximum execution time to prevent infinite loops
|
||||
- **Memory Usage**: Memory is limited to prevent excessive resource usage
|
||||
- **Network Access**: Network calls are restricted to prevent unauthorized access
|
||||
- **Available APIs**: Only a subset of browser APIs are available
|
||||
|
||||
## Inputs and Outputs
|
||||
|
||||
### Inputs
|
||||
|
||||
- **Code**: Your JavaScript/TypeScript code to execute
|
||||
- **Input Data**: Values from previous blocks that can be accessed in your code
|
||||
|
||||
### Outputs
|
||||
|
||||
- **Result**: The value returned by your function
|
||||
- **Standard Output**: Any console output from your function
|
||||
- **Execution Time**: The time taken to execute your function (in milliseconds)
|
||||
|
||||
## Example Usage
|
||||
|
||||
Here's an example of a Function block that processes customer data and calculates a loyalty score:
|
||||
|
||||
```javascript
|
||||
// Example Function Block Code
|
||||
// Process customer data and calculate loyalty score
|
||||
|
||||
// Access input from previous blocks
|
||||
const { purchaseHistory, accountAge, supportTickets } = input;
|
||||
|
||||
// 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 results
|
||||
return {
|
||||
customer: input.name,
|
||||
loyaltyScore,
|
||||
loyaltyTier: loyaltyScore >= 80 ? "Platinum" : loyaltyScore >= 60 ? "Gold" : loyaltyScore >= 40 ? "Silver" : "Bronze",
|
||||
metrics: {
|
||||
spendScore,
|
||||
frequencyScore,
|
||||
supportScore
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Keep functions focused**: Write functions that do one thing well
|
||||
- **Handle errors gracefully**: Use try/catch blocks to handle potential errors
|
||||
- **Document your code**: Add comments to explain complex logic
|
||||
- **Test edge cases**: Ensure your code handles unusual inputs correctly
|
||||
- **Optimize for performance**: Be mindful of computational complexity for large datasets
|
||||
54
apps/docs/content/docs/blocks/index.mdx
Normal file
54
apps/docs/content/docs/blocks/index.mdx
Normal file
@@ -0,0 +1,54 @@
|
||||
---
|
||||
title: Blocks
|
||||
description: Building blocks for your agentic 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 { BlockTypes } from '@/components/ui/block-types'
|
||||
|
||||
Blocks are the fundamental building components of Sim Studio workflows. Each block has a specific purpose and can be connected to other blocks to create sophisticated workflows.
|
||||
|
||||
## What is a Block?
|
||||
|
||||
A block is a reusable, configurable component that performs a specific function within your workflow. Blocks have inputs and outputs that allow them to communicate with other blocks. They can process data, make decisions, interact with external systems, or perform computations.
|
||||
|
||||
## Primary Block Types
|
||||
|
||||
Sim Studio provides six powerful block types that form the foundation of any workflow. Each block is designed to handle specific aspects of your agentic applications, from AI-powered reasoning to conditional logic and external integrations.
|
||||
|
||||
<BlockTypes />
|
||||
|
||||
## Block Connections
|
||||
|
||||
Blocks can be connected to form a directed graph representing your workflow. Each connection represents the flow of data from one block to another:
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
<strong>Outputs to Inputs</strong>: A block's outputs can be connected to another block's
|
||||
inputs.
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Multiple Connections</strong>: A block can have multiple incoming and outgoing
|
||||
connections.
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Conditional Flows</strong>: Some blocks (like Router and Condition) can have multiple
|
||||
output paths based on conditions.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Block Configuration
|
||||
|
||||
Each block type has its own configuration options allowing you to customize its behavior.
|
||||
|
||||
### Common Configuration Options
|
||||
|
||||
- **Input/output definitions**: Define how data flows in and out of the block
|
||||
- **Processing instructions**: Configure how the block processes its inputs
|
||||
- **API keys or authentication details**: Provide necessary credentials for external services
|
||||
- **Retry policies**: Configure how the block handles failures
|
||||
- **Error handling behavior**: Define how errors are managed and reported
|
||||
|
||||
See the specific documentation for each block type to learn about its configuration options.
|
||||
4
apps/docs/content/docs/blocks/meta.json
Normal file
4
apps/docs/content/docs/blocks/meta.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"title": "Blocks",
|
||||
"pages": ["agent", "api", "condition", "function", "evaluator", "router", "response", "workflow"]
|
||||
}
|
||||
188
apps/docs/content/docs/blocks/response.mdx
Normal file
188
apps/docs/content/docs/blocks/response.mdx
Normal file
@@ -0,0 +1,188 @@
|
||||
---
|
||||
title: Response
|
||||
description: Send a structured response back to API calls
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps'
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
|
||||
import { ThemeImage } from '@/components/ui/theme-image'
|
||||
|
||||
The Response block is the final component in API-enabled workflows that transforms your workflow's variables into a structured HTTP response. This block serves as the endpoint that returns data, status codes, and headers back to API callers.
|
||||
|
||||
<ThemeImage
|
||||
lightSrc="/static/light/response-light.png"
|
||||
darkSrc="/static/dark/response-dark.png"
|
||||
alt="Response Block"
|
||||
width={430}
|
||||
height={784}
|
||||
/>
|
||||
|
||||
<Callout type="info">
|
||||
Response blocks are terminal blocks - they mark the end of a workflow execution and cannot have further connections.
|
||||
</Callout>
|
||||
|
||||
## Overview
|
||||
|
||||
The Response block serves as the final output mechanism for API workflows, enabling you to:
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
<strong>Return structured data</strong>: Transform workflow variables into JSON responses
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Set HTTP status codes</strong>: Control the response status (200, 400, 500, etc.)
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Configure headers</strong>: Add custom HTTP headers to the response
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Reference variables</strong>: Use workflow variables dynamically in the response
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Response Data
|
||||
|
||||
The response data is the main content that will be sent back to the API caller. This should be formatted as JSON and can include:
|
||||
|
||||
- Static values
|
||||
- Dynamic references to workflow variables using the `<variable.name>` syntax
|
||||
- Nested objects and arrays
|
||||
- Any valid JSON structure
|
||||
|
||||
### Status Code
|
||||
|
||||
Set the HTTP status code for the response. Common status codes include:
|
||||
|
||||
<Tabs items={['Success (2xx)', 'Client Error (4xx)', 'Server Error (5xx)']}>
|
||||
<Tab>
|
||||
<ul className="list-disc space-y-2 pl-6">
|
||||
<li><strong>200</strong>: OK - Standard success response</li>
|
||||
<li><strong>201</strong>: Created - Resource successfully created</li>
|
||||
<li><strong>204</strong>: No Content - Success with no response body</li>
|
||||
</ul>
|
||||
</Tab>
|
||||
<Tab>
|
||||
<ul className="list-disc space-y-2 pl-6">
|
||||
<li><strong>400</strong>: Bad Request - Invalid request parameters</li>
|
||||
<li><strong>401</strong>: Unauthorized - Authentication required</li>
|
||||
<li><strong>404</strong>: Not Found - Resource doesn't exist</li>
|
||||
<li><strong>422</strong>: Unprocessable Entity - Validation errors</li>
|
||||
</ul>
|
||||
</Tab>
|
||||
<Tab>
|
||||
<ul className="list-disc space-y-2 pl-6">
|
||||
<li><strong>500</strong>: Internal Server Error - Server-side error</li>
|
||||
<li><strong>502</strong>: Bad Gateway - External service error</li>
|
||||
<li><strong>503</strong>: Service Unavailable - Service temporarily down</li>
|
||||
</ul>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
<p className="mt-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
Default status code is 200 if not specified.
|
||||
</p>
|
||||
|
||||
### Response Headers
|
||||
|
||||
Configure additional HTTP headers to include in the response.
|
||||
|
||||
Headers are configured as key-value pairs:
|
||||
|
||||
| Key | Value |
|
||||
|-----|-------|
|
||||
| Content-Type | application/json |
|
||||
| Cache-Control | no-cache |
|
||||
| X-API-Version | 1.0 |
|
||||
|
||||
## Inputs and Outputs
|
||||
|
||||
<Tabs items={['Inputs', 'Outputs']}>
|
||||
<Tab>
|
||||
<ul className="list-disc space-y-2 pl-6">
|
||||
<li>
|
||||
<strong>data</strong> (JSON, optional): The JSON data to send in the response body
|
||||
</li>
|
||||
<li>
|
||||
<strong>status</strong> (number, optional): HTTP status code (default: 200)
|
||||
</li>
|
||||
<li>
|
||||
<strong>headers</strong> (JSON, optional): Additional response headers
|
||||
</li>
|
||||
</ul>
|
||||
</Tab>
|
||||
<Tab>
|
||||
<ul className="list-disc space-y-2 pl-6">
|
||||
<li>
|
||||
<strong>response</strong>: Complete response object containing:
|
||||
<ul className="list-disc space-y-1 pl-6 mt-2">
|
||||
<li><strong>data</strong>: The response body data</li>
|
||||
<li><strong>status</strong>: HTTP status code</li>
|
||||
<li><strong>headers</strong>: Response headers</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Variable References
|
||||
|
||||
Use the `<variable.name>` syntax to dynamically insert workflow variables into your response:
|
||||
|
||||
```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">
|
||||
Variable names are case-sensitive and must match exactly with the variables available in your workflow.
|
||||
</Callout>
|
||||
|
||||
## Example Usage
|
||||
|
||||
Here's an example of how a Response block might be configured for a user search API:
|
||||
|
||||
```yaml
|
||||
data: |
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"users": "<variable.searchResults>",
|
||||
"pagination": {
|
||||
"page": "<variable.currentPage>",
|
||||
"limit": "<variable.pageSize>",
|
||||
"total": "<variable.totalUsers>"
|
||||
}
|
||||
},
|
||||
"query": {
|
||||
"searchTerm": "<variable.searchTerm>",
|
||||
"filters": "<variable.appliedFilters>"
|
||||
},
|
||||
"timestamp": "<variable.timestamp>"
|
||||
}
|
||||
status: 200
|
||||
headers:
|
||||
- key: X-Total-Count
|
||||
value: <variable.totalUsers>
|
||||
- key: Cache-Control
|
||||
value: public, max-age=300
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Use meaningful status codes**: Choose appropriate HTTP status codes that accurately reflect the outcome of the workflow
|
||||
- **Structure your responses consistently**: Maintain a consistent JSON structure across all your API endpoints for better developer experience
|
||||
- **Include relevant metadata**: Add timestamps and version information to help with debugging and monitoring
|
||||
- **Handle errors gracefully**: Use conditional logic in your workflow to set appropriate error responses with descriptive messages
|
||||
- **Validate variable references**: Ensure all referenced variables exist and contain the expected data types before the Response block executes
|
||||
120
apps/docs/content/docs/blocks/router.mdx
Normal file
120
apps/docs/content/docs/blocks/router.mdx
Normal file
@@ -0,0 +1,120 @@
|
||||
---
|
||||
title: Router
|
||||
description: Route workflow execution based on specific conditions or logic
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps'
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
|
||||
import { ThemeImage } from '@/components/ui/theme-image'
|
||||
|
||||
The Router block is a powerful component in Sim Studio that intelligently routes workflow execution based on content analysis, user input, or predefined conditions. It acts as a decision-making junction in your workflow, directing the flow to different paths based on various criteria.
|
||||
|
||||
<ThemeImage
|
||||
lightSrc="/static/light/router-light.png"
|
||||
darkSrc="/static/dark/router-dark.png"
|
||||
alt="Router Block"
|
||||
width={300}
|
||||
height={175}
|
||||
/>
|
||||
|
||||
## Overview
|
||||
|
||||
The Router block uses LLMs to analyze input content and determine the most appropriate next step in your workflow. This allows for:
|
||||
|
||||
- Creating dynamic, adaptable workflows
|
||||
- Implementing complex decision trees
|
||||
- Routing user requests to specialized components
|
||||
- Building conversational systems that can handle diverse inputs
|
||||
|
||||
## How It Works
|
||||
|
||||
The Router block:
|
||||
|
||||
1. Analyzes the input content using an LLM
|
||||
2. Evaluates the content against the available target blocks in your workflow
|
||||
3. Identifies the most appropriate destination based on the content's intent or requirements
|
||||
4. Routes the workflow execution to the selected block
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Content/Prompt
|
||||
|
||||
The content or prompt that the Router will analyze to make routing decisions. This can be:
|
||||
|
||||
- A direct user query or input
|
||||
- Output from a previous block
|
||||
- A system-generated message
|
||||
|
||||
### Target Blocks
|
||||
|
||||
The possible destination blocks that the Router can select from. The Router will automatically detect connected blocks, but you can also:
|
||||
|
||||
- Customize the descriptions of target blocks to improve routing accuracy
|
||||
- Specify routing criteria for each target block
|
||||
- Exclude certain blocks from being considered as routing targets
|
||||
|
||||
### Model Selection
|
||||
|
||||
Choose an LLM provider to power the routing decision:
|
||||
|
||||
- OpenAI (GPT-4o, o1, o3, o4-mini)
|
||||
- Anthropic (Claude 3.7 Sonnet)
|
||||
- Google (Gemini 2.5 Pro, Gemini 2.0 Flash)
|
||||
- Groq, Cerebras
|
||||
- Ollama Local Models
|
||||
- And more
|
||||
|
||||
Select a model with strong reasoning capabilities for more accurate routing decisions.
|
||||
|
||||
### API Key
|
||||
|
||||
Your API key for the selected LLM provider. This is securely stored and used for authentication.
|
||||
|
||||
## Inputs and Outputs
|
||||
|
||||
### Inputs
|
||||
|
||||
- **Content/Prompt**: The text to analyze for routing decisions
|
||||
- **Target Blocks**: Connected blocks that are potential routing destinations
|
||||
- **Model Settings**: LLM provider and parameters
|
||||
|
||||
### Outputs
|
||||
|
||||
- **Content**: A summary of the routing decision
|
||||
- **Model**: The model used for decision-making
|
||||
- **Tokens**: Usage statistics
|
||||
- **Selected Path**: Details of the chosen routing destination, including:
|
||||
- Block ID
|
||||
- Block Type
|
||||
- Block Title
|
||||
|
||||
## Example Usage
|
||||
|
||||
Here's an example of how a Router block might be used in a customer support workflow:
|
||||
|
||||
```yaml
|
||||
# Example Router Configuration
|
||||
prompt: |
|
||||
Analyze the user query and route to the most appropriate department.
|
||||
Choose ONE destination based on the query content and intent.
|
||||
|
||||
model: OpenAI/gpt-4
|
||||
```
|
||||
|
||||
In this example, the Router might be connected to:
|
||||
|
||||
- A product support block
|
||||
- A billing inquiries block
|
||||
- A technical support block
|
||||
- A general inquiries block
|
||||
|
||||
Based on the user's query, the Router would analyze the content and direct it to the most appropriate specialized support block.
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Provide clear descriptions for target blocks**: Help the Router understand when to select each destination
|
||||
- **Use specific routing criteria**: Define clear conditions for selecting each path
|
||||
- **Consider fallback paths**: Connect a default destination for when no specific path is appropriate
|
||||
- **Test with diverse inputs**: Ensure the Router handles various input types correctly
|
||||
- **Review routing decisions**: Monitor the Router's performance and refine as needed
|
||||
231
apps/docs/content/docs/blocks/workflow.mdx
Normal file
231
apps/docs/content/docs/blocks/workflow.mdx
Normal file
@@ -0,0 +1,231 @@
|
||||
---
|
||||
title: Workflow
|
||||
description: Execute other workflows as reusable components within your current workflow
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps'
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
|
||||
import { ThemeImage } from '@/components/ui/theme-image'
|
||||
|
||||
The Workflow block allows you to execute other workflows as reusable components within your current workflow. This powerful feature enables modular design, code reuse, and the creation of complex nested workflows that can be composed from smaller, focused workflows.
|
||||
|
||||
<ThemeImage
|
||||
lightSrc="/static/light/workflow-light.png"
|
||||
darkSrc="/static/dark/workflow-dark.png"
|
||||
alt="Workflow Block"
|
||||
width={300}
|
||||
height={175}
|
||||
/>
|
||||
|
||||
<Callout type="info">
|
||||
Workflow blocks enable modular design by allowing you to compose complex workflows from smaller, reusable components.
|
||||
</Callout>
|
||||
|
||||
## Overview
|
||||
|
||||
The Workflow block serves as a bridge between workflows, enabling you to:
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
<strong>Reuse existing workflows</strong>: Execute previously created workflows as components within new workflows
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Create modular designs</strong>: Break down complex processes into smaller, manageable workflows
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Maintain separation of concerns</strong>: Keep different business logic isolated in separate workflows
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Enable team collaboration</strong>: Share and reuse workflows across different projects and team members
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## How It Works
|
||||
|
||||
The Workflow block:
|
||||
|
||||
1. Takes a reference to another workflow in your workspace
|
||||
2. Passes input data from the current workflow to the child workflow
|
||||
3. Executes the child workflow in an isolated context
|
||||
4. Returns the results back to the parent workflow for further processing
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Workflow Selection
|
||||
|
||||
Choose which workflow to execute from a dropdown list of available workflows in your workspace. The list includes:
|
||||
|
||||
- All workflows you have access to in the current workspace
|
||||
- Workflows shared with you by other team members
|
||||
- Both enabled and disabled workflows (though only enabled workflows can be executed)
|
||||
|
||||
### Input Data
|
||||
|
||||
Define the data to pass to the child workflow:
|
||||
|
||||
- **Single Variable Input**: Select a variable or block output to pass to the child workflow
|
||||
- **Variable References**: Use `<variable.name>` to reference workflow variables
|
||||
- **Block References**: Use `<blockName.field>` to reference outputs from previous blocks
|
||||
- **Automatic Mapping**: The selected data is automatically available as `start.input` in the child workflow
|
||||
- **Optional**: The input field is optional - child workflows can run without input data
|
||||
- **Type Preservation**: Variable types (strings, numbers, objects, etc.) are preserved when passed to the child workflow
|
||||
|
||||
### Examples of Input References
|
||||
|
||||
- `<variable.customerData>` - Pass a workflow variable
|
||||
- `<dataProcessor.result>` - Pass the result from a previous block
|
||||
- `<start.input>` - Pass the original workflow input
|
||||
- `<apiCall.data.user>` - Pass a specific field from an API response
|
||||
|
||||
### Execution Context
|
||||
|
||||
The child workflow executes with:
|
||||
|
||||
- Its own isolated execution context
|
||||
- Access to the same workspace resources (API keys, environment variables)
|
||||
- Proper workspace membership and permission checks
|
||||
- Independent logging and monitoring
|
||||
|
||||
## Safety and Limitations
|
||||
|
||||
To prevent infinite recursion and ensure system stability, the Workflow block includes several safety mechanisms:
|
||||
|
||||
<Callout type="warning">
|
||||
**Cycle Detection**: The system automatically detects and prevents circular dependencies between workflows to avoid infinite loops.
|
||||
</Callout>
|
||||
|
||||
- **Maximum Depth Limit**: Nested workflows are limited to a maximum depth of 10 levels
|
||||
- **Cycle Detection**: Automatic detection and prevention of circular workflow dependencies
|
||||
- **Timeout Protection**: Child workflows inherit timeout settings to prevent indefinite execution
|
||||
- **Resource Limits**: Memory and execution time limits apply to prevent resource exhaustion
|
||||
|
||||
## Inputs and Outputs
|
||||
|
||||
<Tabs items={['Inputs', 'Outputs']}>
|
||||
<Tab>
|
||||
<ul className="list-disc space-y-2 pl-6">
|
||||
<li>
|
||||
<strong>Workflow ID</strong>: The identifier of the workflow to execute
|
||||
</li>
|
||||
<li>
|
||||
<strong>Input Variable</strong>: Variable or block reference to pass to the child workflow (e.g., `<variable.name>` or `<block.field>`)
|
||||
</li>
|
||||
</ul>
|
||||
</Tab>
|
||||
<Tab>
|
||||
<ul className="list-disc space-y-2 pl-6">
|
||||
<li>
|
||||
<strong>Response</strong>: The complete output from the child workflow execution
|
||||
</li>
|
||||
<li>
|
||||
<strong>Child Workflow Name</strong>: The name of the executed child workflow
|
||||
</li>
|
||||
<li>
|
||||
<strong>Success Status</strong>: Boolean indicating whether the child workflow completed successfully
|
||||
</li>
|
||||
<li>
|
||||
<strong>Error Information</strong>: Details about any errors that occurred during execution
|
||||
</li>
|
||||
<li>
|
||||
<strong>Execution Metadata</strong>: Information about execution time, resource usage, and performance
|
||||
</li>
|
||||
</ul>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Example Usage
|
||||
|
||||
Here's an example of how a Workflow block might be used to create a modular customer onboarding process:
|
||||
|
||||
### Parent Workflow: Customer Onboarding
|
||||
```yaml
|
||||
# Main customer onboarding workflow
|
||||
blocks:
|
||||
- type: workflow
|
||||
name: "Validate Customer Data"
|
||||
workflowId: "customer-validation-workflow"
|
||||
input: "<variable.newCustomer>"
|
||||
|
||||
- type: workflow
|
||||
name: "Setup Customer Account"
|
||||
workflowId: "account-setup-workflow"
|
||||
input: "<Validate Customer Data.result>"
|
||||
|
||||
- type: workflow
|
||||
name: "Send Welcome Email"
|
||||
workflowId: "welcome-email-workflow"
|
||||
input: "<Setup Customer Account.result.accountDetails>"
|
||||
```
|
||||
|
||||
### Child Workflow: Customer Validation
|
||||
```yaml
|
||||
# Reusable customer validation workflow
|
||||
# Access the input data using: start.input
|
||||
blocks:
|
||||
- type: function
|
||||
name: "Validate Email"
|
||||
code: |
|
||||
const customerData = start.input;
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(customerData.email);
|
||||
|
||||
- type: api
|
||||
name: "Check Credit Score"
|
||||
url: "https://api.creditcheck.com/score"
|
||||
method: "POST"
|
||||
body: "<start.input>"
|
||||
```
|
||||
|
||||
### Variable Reference Examples
|
||||
|
||||
```yaml
|
||||
# Using workflow variables
|
||||
input: "<variable.customerInfo>"
|
||||
|
||||
# Using block outputs
|
||||
input: "<dataProcessor.cleanedData>"
|
||||
|
||||
# Using nested object properties
|
||||
input: "<apiCall.data.user.profile>"
|
||||
|
||||
# Using array elements (if supported by the resolver)
|
||||
input: "<listProcessor.items[0]>"
|
||||
```
|
||||
|
||||
## Access Control and Permissions
|
||||
|
||||
The Workflow block respects workspace permissions and access controls:
|
||||
|
||||
- **Workspace Membership**: Only workflows within the same workspace can be executed
|
||||
- **Permission Inheritance**: Child workflows inherit the execution permissions of the parent workflow
|
||||
- **API Key Access**: Child workflows have access to the same API keys and environment variables as the parent
|
||||
- **User Context**: The execution maintains the original user context for audit and logging purposes
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Keep workflows focused**: Design child workflows to handle specific, well-defined tasks
|
||||
- **Minimize nesting depth**: Avoid deeply nested workflow hierarchies for better maintainability
|
||||
- **Handle errors gracefully**: Implement proper error handling for child workflow failures
|
||||
- **Document dependencies**: Clearly document which workflows depend on others
|
||||
- **Version control**: Consider versioning strategies for workflows that are used as components
|
||||
- **Test independently**: Ensure child workflows can be tested and validated independently
|
||||
- **Monitor performance**: Be aware that nested workflows can impact overall execution time
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Microservice Architecture
|
||||
Break down complex business processes into smaller, focused workflows that can be developed and maintained independently.
|
||||
|
||||
### Reusable Components
|
||||
Create library workflows for common operations like data validation, email sending, or API integrations that can be reused across multiple projects.
|
||||
|
||||
### Conditional Execution
|
||||
Use workflow blocks within conditional logic to execute different business processes based on runtime conditions.
|
||||
|
||||
### Parallel Processing
|
||||
Combine workflow blocks with parallel execution to run multiple child workflows simultaneously for improved performance.
|
||||
|
||||
<Callout type="tip">
|
||||
When designing modular workflows, think of each workflow as a function with clear inputs, outputs, and a single responsibility.
|
||||
</Callout>
|
||||
190
apps/docs/content/docs/connections/accessing-data.mdx
Normal file
190
apps/docs/content/docs/connections/accessing-data.mdx
Normal file
@@ -0,0 +1,190 @@
|
||||
---
|
||||
title: Accessing Connected Data
|
||||
description: Techniques for accessing and manipulating data from connected blocks
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { File, Files, Folder } from 'fumadocs-ui/components/files'
|
||||
|
||||
Once blocks are connected, you can access data from source blocks in destination blocks using connection tags and various data access techniques.
|
||||
|
||||
## Basic Data Access
|
||||
|
||||
The simplest way to access data is through direct references using connection tags:
|
||||
|
||||
<Files>
|
||||
<File name="Simple Property" annotation="<block.content>" />
|
||||
<File name="Nested Property" annotation="<block.tokens.total>" />
|
||||
<File name="Array Element" annotation="<block.items[0].name>" />
|
||||
<File name="Complex Path" annotation="<block.data.users[2].profile.email>" />
|
||||
</Files>
|
||||
|
||||
## Advanced Data Access Techniques
|
||||
|
||||
### Array Access
|
||||
|
||||
You can access array elements using square bracket notation:
|
||||
|
||||
```javascript
|
||||
// Access the first item in an array
|
||||
<block.items[0]>
|
||||
|
||||
// Access a specific property of an array item
|
||||
<block.items[2].name>
|
||||
|
||||
// Access the last item in an array (in Function blocks)
|
||||
const items = input.block.items;
|
||||
const lastItem = items[items.length - 1];
|
||||
```
|
||||
|
||||
### Object Property Access
|
||||
|
||||
Access object properties using dot notation:
|
||||
|
||||
```javascript
|
||||
// Access a simple property
|
||||
<block.content>
|
||||
|
||||
// Access a nested property
|
||||
<block.data.user.profile.name>
|
||||
|
||||
// Access a property with special characters (in Function blocks)
|
||||
const data = input.block.data;
|
||||
const specialProp = data['property-with-dashes'];
|
||||
```
|
||||
|
||||
### Dynamic References
|
||||
|
||||
Connection references are evaluated at runtime, allowing for dynamic data flow through your workflow:
|
||||
|
||||
```javascript
|
||||
// In a Function block, you can access connected data
|
||||
const userName = input.userBlock.name;
|
||||
const orderTotal = input.apiBlock.body.order.total;
|
||||
|
||||
// Process the data
|
||||
const discount = orderTotal > 100 ? 0.1 : 0;
|
||||
const finalPrice = orderTotal * (1 - discount);
|
||||
|
||||
// Return the result
|
||||
return {
|
||||
userName,
|
||||
originalTotal: orderTotal,
|
||||
discount: discount * 100 + '%',
|
||||
finalPrice
|
||||
};
|
||||
```
|
||||
|
||||
## Data Transformation
|
||||
|
||||
### Using Function Blocks
|
||||
|
||||
Function blocks are the most powerful way to transform data between connections:
|
||||
|
||||
```javascript
|
||||
// Example: Transform API response data
|
||||
const apiResponse = input.apiBlock.data;
|
||||
const transformedData = {
|
||||
users: apiResponse.results.map(user => ({
|
||||
id: user.id,
|
||||
fullName: `${user.firstName} ${user.lastName}`,
|
||||
email: user.email.toLowerCase(),
|
||||
isActive: user.status === 'active'
|
||||
})),
|
||||
totalCount: apiResponse.count,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
return transformedData;
|
||||
```
|
||||
|
||||
### String Interpolation
|
||||
|
||||
You can combine connection tags with static text:
|
||||
|
||||
```
|
||||
Hello, <userBlock.name>! Your order #<orderBlock.id> has been processed.
|
||||
```
|
||||
|
||||
### Conditional Content
|
||||
|
||||
In Function blocks, you can create conditional content based on connected data:
|
||||
|
||||
```javascript
|
||||
const user = input.userBlock;
|
||||
const orderTotal = input.orderBlock.total;
|
||||
|
||||
let message = `Thank you for your order, ${user.name}!`;
|
||||
|
||||
if (orderTotal > 100) {
|
||||
message += " You've qualified for free shipping!";
|
||||
} else {
|
||||
message += ` Add $${(100 - orderTotal).toFixed(2)} more to qualify for free shipping.`;
|
||||
}
|
||||
|
||||
return { message };
|
||||
```
|
||||
|
||||
## Handling Missing Data
|
||||
|
||||
It's important to handle cases where connected data might be missing or null:
|
||||
|
||||
<Callout type="warning">
|
||||
Always validate connected data before using it, especially when accessing nested properties or
|
||||
array elements.
|
||||
</Callout>
|
||||
|
||||
### Default Values
|
||||
|
||||
In Function blocks, you can provide default values for missing data:
|
||||
|
||||
```javascript
|
||||
const userName = input.userBlock?.name || 'Guest'
|
||||
const items = input.orderBlock?.items || []
|
||||
const total = input.orderBlock?.total ?? 0
|
||||
```
|
||||
|
||||
### Conditional Checks
|
||||
|
||||
Check if data exists before accessing nested properties:
|
||||
|
||||
```javascript
|
||||
let userEmail = 'No email provided'
|
||||
if (input.userBlock && input.userBlock.contact && input.userBlock.contact.email) {
|
||||
userEmail = input.userBlock.contact.email
|
||||
}
|
||||
```
|
||||
|
||||
### Optional Chaining
|
||||
|
||||
In Function blocks, use optional chaining to safely access nested properties:
|
||||
|
||||
```javascript
|
||||
const userCity = input.userBlock?.address?.city
|
||||
const firstItemName = input.orderBlock?.items?.[0]?.name
|
||||
```
|
||||
|
||||
## Debugging Connection Data
|
||||
|
||||
When troubleshooting connection issues, these techniques can help:
|
||||
|
||||
1. **Log Data**: In Function blocks, use `console.log()` to inspect connected data
|
||||
2. **Return Full Objects**: Return the full input object to see all available data
|
||||
3. **Check Types**: Verify the data types of connected values
|
||||
4. **Validate Paths**: Ensure you're using the correct path to access nested data
|
||||
|
||||
```javascript
|
||||
// Example debugging function
|
||||
function debugConnections() {
|
||||
console.log('All inputs:', input)
|
||||
console.log('User data type:', typeof input.userBlock)
|
||||
console.log('Order items:', input.orderBlock?.items)
|
||||
|
||||
return {
|
||||
debug: true,
|
||||
allInputs: input,
|
||||
userExists: !!input.userBlock,
|
||||
orderItemCount: input.orderBlock?.items?.length || 0,
|
||||
}
|
||||
}
|
||||
```
|
||||
76
apps/docs/content/docs/connections/basics.mdx
Normal file
76
apps/docs/content/docs/connections/basics.mdx
Normal file
@@ -0,0 +1,76 @@
|
||||
---
|
||||
title: Connection Basics
|
||||
description: Learn how connections work in Sim Studio
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps'
|
||||
|
||||
## How Connections Work
|
||||
|
||||
Connections are the pathways that allow data to flow between blocks in your workflow. When you connect two blocks in Sim Studio, you're establishing a data flow relationship that defines how information passes from one block to another.
|
||||
|
||||
<Callout type="info">
|
||||
Each connection represents a directed relationship where data flows from a source block's output
|
||||
to a destination block's input.
|
||||
</Callout>
|
||||
|
||||
### Creating Connections
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
<strong>Select Source Block</strong>: Click on the output port of the block you want to connect
|
||||
from
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Draw Connection</strong>: Drag to the input port of the destination block
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Confirm Connection</strong>: Release to create the connection
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Configure (Optional)</strong>: Some connections may require additional configuration
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
### Connection Flow
|
||||
|
||||
The flow of data through connections follows these principles:
|
||||
|
||||
1. **Directional Flow**: Data always flows from outputs to inputs
|
||||
2. **Execution Order**: Blocks execute in order based on their connections
|
||||
3. **Data Transformation**: Data may be transformed as it passes between blocks
|
||||
4. **Conditional Paths**: Some blocks (like Router and Condition) can direct flow to different paths
|
||||
|
||||
### Connection Visualization
|
||||
|
||||
Connections are visually represented in the workflow editor:
|
||||
|
||||
- **Solid Lines**: Active connections that will pass data
|
||||
- **Animated Flow**: During execution, data flow is visualized along connections
|
||||
- **Color Coding**: Different connection types may have different colors
|
||||
- **Connection Tags**: Visual indicators showing what data is available
|
||||
|
||||
### Managing Connections
|
||||
|
||||
You can manage your connections in several ways:
|
||||
|
||||
- **Delete**: Click on a connection and press Delete or use the context menu
|
||||
- **Reroute**: Drag a connection to change its path
|
||||
- **Inspect**: Click on a connection to see details about the data being passed
|
||||
- **Disable**: Temporarily disable a connection without deleting it
|
||||
|
||||
<Callout type="warning">
|
||||
Deleting a connection will immediately stop data flow between the blocks. Make sure this is
|
||||
intended before removing connections.
|
||||
</Callout>
|
||||
|
||||
## Connection Compatibility
|
||||
|
||||
Not all blocks can be connected to each other. Compatibility depends on:
|
||||
|
||||
1. **Data Type Compatibility**: The output type must be compatible with the input type
|
||||
2. **Block Restrictions**: Some blocks may have restrictions on what they can connect to
|
||||
3. **Workflow Logic**: Connections must make logical sense in the context of your workflow
|
||||
|
||||
The editor will indicate when connections are invalid or incompatible.
|
||||
208
apps/docs/content/docs/connections/best-practices.mdx
Normal file
208
apps/docs/content/docs/connections/best-practices.mdx
Normal file
@@ -0,0 +1,208 @@
|
||||
---
|
||||
title: Connection Best Practices
|
||||
description: Recommended patterns for effective connection management
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps'
|
||||
|
||||
## Workflow Organization
|
||||
|
||||
### Organize Your Connections
|
||||
|
||||
Keep your workflow clean and understandable by organizing connections logically:
|
||||
|
||||
- **Minimize crossing connections** when possible to reduce visual complexity
|
||||
- **Group related blocks together** to make data flow more intuitive
|
||||
- **Use consistent flow direction** (typically left-to-right or top-to-bottom)
|
||||
- **Label complex connections** with descriptive names
|
||||
|
||||
<Callout type="info">
|
||||
A well-organized workflow is easier to understand, debug, and maintain. Take time to arrange your
|
||||
blocks and connections in a logical manner.
|
||||
</Callout>
|
||||
|
||||
### Connection Naming Conventions
|
||||
|
||||
When working with multiple connections, consistent naming helps maintain clarity:
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
<strong>Use descriptive block names</strong>: Name blocks based on their function (e.g.,
|
||||
"UserDataFetcher", "ResponseGenerator")
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Be specific with connection references</strong>: Use clear variable names when
|
||||
referencing connections in code
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Document complex connections</strong>: Add comments explaining non-obvious data
|
||||
transformations
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Data Validation
|
||||
|
||||
### Validate Data Flow
|
||||
|
||||
Ensure that the data being passed between blocks is compatible:
|
||||
|
||||
- **Check that required fields are available** in the source block
|
||||
- **Verify data types match expectations** before using them
|
||||
- **Use Function blocks to transform data** when necessary
|
||||
- **Handle missing or null values** with default values or conditional logic
|
||||
|
||||
```javascript
|
||||
// Example: Validating and transforming data in a Function block
|
||||
function processUserData() {
|
||||
// Validate required fields
|
||||
if (!input.userBlock || !input.userBlock.id) {
|
||||
return { error: 'Missing user data', valid: false }
|
||||
}
|
||||
|
||||
// Transform and validate data types
|
||||
const userId = String(input.userBlock.id)
|
||||
const userName = input.userBlock.name || 'Unknown User'
|
||||
const userScore = Number(input.userBlock.score) || 0
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
user: {
|
||||
id: userId,
|
||||
name: userName,
|
||||
score: userScore,
|
||||
isHighScore: userScore > 100,
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
### Document Connection Purpose
|
||||
|
||||
Add comments or descriptions to clarify the purpose of connections, especially in complex workflows:
|
||||
|
||||
- **What data is being passed**: Document the key fields and their purpose
|
||||
- **Why this connection exists**: Explain the relationship between blocks
|
||||
- **Any transformations or conditions applied**: Note any data processing that occurs
|
||||
|
||||
```javascript
|
||||
// Example: Documenting connection purpose in a Function block
|
||||
/*
|
||||
* This function processes user data from the UserFetcher block
|
||||
* and order history from the OrderHistory block to generate
|
||||
* personalized product recommendations.
|
||||
*
|
||||
* Input:
|
||||
* - userBlock: User profile data (id, preferences, history)
|
||||
* - orderBlock: Recent order history (items, dates, amounts)
|
||||
*
|
||||
* Output:
|
||||
* - recommendations: Array of recommended product IDs
|
||||
* - userSegment: Calculated user segment for marketing
|
||||
* - conversionProbability: Estimated likelihood of purchase
|
||||
*/
|
||||
function generateRecommendations() {
|
||||
// Implementation...
|
||||
}
|
||||
```
|
||||
|
||||
## Testing and Debugging
|
||||
|
||||
### Test Connection References
|
||||
|
||||
Verify that connection references work as expected:
|
||||
|
||||
- **Test with different input values** to ensure robustness
|
||||
- **Check edge cases** (empty values, large datasets, special characters)
|
||||
- **Ensure error handling for missing or invalid data**
|
||||
- **Use console logging in Function blocks** to debug connection issues
|
||||
|
||||
```javascript
|
||||
// Example: Testing connection references with edge cases
|
||||
function testConnections() {
|
||||
console.log('Testing connections...')
|
||||
|
||||
// Log all inputs for debugging
|
||||
console.log('All inputs:', JSON.stringify(input, null, 2))
|
||||
|
||||
// Test for missing data
|
||||
const hasUserData = !!input.userBlock
|
||||
console.log('Has user data:', hasUserData)
|
||||
|
||||
// Test edge cases
|
||||
const items = input.orderBlock?.items || []
|
||||
console.log('Item count:', items.length)
|
||||
console.log('Empty items test:', items.length === 0 ? 'Passed' : 'Failed')
|
||||
|
||||
// Return test results
|
||||
return {
|
||||
tests: {
|
||||
hasUserData,
|
||||
hasItems: items.length > 0,
|
||||
hasLargeOrder: items.length > 10,
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Optimize Data Flow
|
||||
|
||||
Keep your workflows efficient by optimizing how data flows through connections:
|
||||
|
||||
- **Pass only necessary data** between blocks to reduce memory usage
|
||||
- **Use Function blocks to filter large datasets** before passing them on
|
||||
- **Consider caching results** for expensive operations
|
||||
- **Break complex workflows into smaller, reusable components**
|
||||
|
||||
```javascript
|
||||
// Example: Optimizing data flow by filtering
|
||||
function optimizeUserData() {
|
||||
const userData = input.userBlock
|
||||
|
||||
// Only pass necessary fields to downstream blocks
|
||||
return {
|
||||
id: userData.id,
|
||||
name: userData.name,
|
||||
email: userData.email,
|
||||
// Filter out unnecessary profile data, history, etc.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
### Secure Sensitive Data
|
||||
|
||||
Protect sensitive information when using connections:
|
||||
|
||||
- **Never expose API keys or credentials** in connection data
|
||||
- **Sanitize user input** before processing it
|
||||
- **Redact sensitive information** when logging connection data
|
||||
- **Use secure connections** for external API calls
|
||||
|
||||
<Callout type="warning">
|
||||
Be careful when logging connection data that might contain sensitive information. Always redact or
|
||||
mask sensitive fields like passwords, API keys, or personal information.
|
||||
</Callout>
|
||||
|
||||
## Advanced Patterns
|
||||
|
||||
### Conditional Connections
|
||||
|
||||
Use Condition blocks to create dynamic workflows:
|
||||
|
||||
- **Route data based on content** to different processing paths
|
||||
- **Implement fallback paths** for error handling
|
||||
- **Create decision trees** for complex business logic
|
||||
|
||||
### Feedback Loops
|
||||
|
||||
Create more sophisticated workflows with feedback connections:
|
||||
|
||||
- **Implement iterative processing** by connecting later blocks back to earlier ones
|
||||
- **Use Memory blocks** to store state between iterations
|
||||
- **Set termination conditions** to prevent infinite loops
|
||||
192
apps/docs/content/docs/connections/data-structure.mdx
Normal file
192
apps/docs/content/docs/connections/data-structure.mdx
Normal file
@@ -0,0 +1,192 @@
|
||||
---
|
||||
title: Connection Data Structure
|
||||
description: Understanding the data structure of different block outputs
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
|
||||
|
||||
When you connect blocks, the output data structure from the source block determines what values are available in the destination block. Each block type produces a specific output structure that you can reference in downstream blocks.
|
||||
|
||||
<Callout type="info">
|
||||
Understanding these data structures is essential for effectively using connection tags and
|
||||
accessing the right data in your workflows.
|
||||
</Callout>
|
||||
|
||||
## Block Output Structures
|
||||
|
||||
Different block types produce different output structures. Here's what you can expect from each block type:
|
||||
|
||||
<Tabs items={['Agent Output', 'API Output', 'Function Output', 'Evaluator Output', 'Condition Output', 'Router Output']}>
|
||||
<Tab>
|
||||
```json
|
||||
{
|
||||
"content": "The generated text response",
|
||||
"model": "gpt-4o",
|
||||
"tokens": {
|
||||
"prompt": 120,
|
||||
"completion": 85,
|
||||
"total": 205
|
||||
},
|
||||
"toolCalls": [...],
|
||||
"cost": [...],
|
||||
"usage": [...]
|
||||
}
|
||||
```
|
||||
|
||||
### Agent Block Output Fields
|
||||
|
||||
- **content**: The main text response generated by the agent
|
||||
- **model**: The AI model used (e.g., "gpt-4o", "claude-3-opus")
|
||||
- **tokens**: Token usage statistics
|
||||
- **prompt**: Number of tokens in the prompt
|
||||
- **completion**: Number of tokens in the completion
|
||||
- **total**: Total tokens used
|
||||
- **toolCalls**: Array of tool calls made by the agent (if any)
|
||||
- **cost**: Array of cost objects for each tool call (if any)
|
||||
- **usage**: Token usage statistics for the entire response
|
||||
|
||||
</Tab>
|
||||
<Tab>
|
||||
```json
|
||||
{
|
||||
"data": "Response data",
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "application/json",
|
||||
"cache-control": "no-cache"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### API Block Output Fields
|
||||
|
||||
- **data**: The response data from the API (can be any type)
|
||||
- **status**: HTTP status code of the response
|
||||
- **headers**: HTTP headers returned by the API
|
||||
|
||||
</Tab>
|
||||
<Tab>
|
||||
```json
|
||||
{
|
||||
"result": "Function return value",
|
||||
"stdout": "Console output",
|
||||
"executionTime": 45
|
||||
}
|
||||
```
|
||||
|
||||
### Function Block Output Fields
|
||||
|
||||
- **result**: The return value of the function (can be any type)
|
||||
- **stdout**: Console output captured during function execution
|
||||
- **executionTime**: Time taken to execute the function (in milliseconds)
|
||||
|
||||
</Tab>
|
||||
<Tab>
|
||||
```json
|
||||
{
|
||||
"content": "Evaluation summary",
|
||||
"model": "gpt-4o",
|
||||
"tokens": {
|
||||
"prompt": 120,
|
||||
"completion": 85,
|
||||
"total": 205
|
||||
},
|
||||
"metric1": 8.5,
|
||||
"metric2": 7.2,
|
||||
"metric3": 9.0
|
||||
}
|
||||
```
|
||||
|
||||
### Evaluator Block Output Fields
|
||||
|
||||
- **content**: Summary of the evaluation
|
||||
- **model**: The AI model used for evaluation
|
||||
- **tokens**: Token usage statistics
|
||||
- **[metricName]**: Score for each metric defined in the evaluator (dynamic fields)
|
||||
|
||||
</Tab>
|
||||
<Tab>
|
||||
```json
|
||||
{
|
||||
"content": "Original content passed through",
|
||||
"conditionResult": true,
|
||||
"selectedPath": {
|
||||
"blockId": "2acd9007-27e8-4510-a487-73d3b825e7c1",
|
||||
"blockType": "agent",
|
||||
"blockTitle": "Follow-up Agent"
|
||||
},
|
||||
"selectedConditionId": "condition-1"
|
||||
}
|
||||
```
|
||||
|
||||
### Condition Block Output Fields
|
||||
|
||||
- **content**: The original content passed through
|
||||
- **conditionResult**: Boolean result of the condition evaluation
|
||||
- **selectedPath**: Information about the selected path
|
||||
- **blockId**: ID of the next block in the selected path
|
||||
- **blockType**: Type of the next block
|
||||
- **blockTitle**: Title of the next block
|
||||
- **selectedConditionId**: ID of the selected condition
|
||||
|
||||
</Tab>
|
||||
<Tab>
|
||||
```json
|
||||
{
|
||||
"content": "Routing decision",
|
||||
"model": "gpt-4o",
|
||||
"tokens": {
|
||||
"prompt": 120,
|
||||
"completion": 85,
|
||||
"total": 205
|
||||
},
|
||||
"selectedPath": {
|
||||
"blockId": "2acd9007-27e8-4510-a487-73d3b825e7c1",
|
||||
"blockType": "agent",
|
||||
"blockTitle": "Customer Service Agent"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Router Block Output Fields
|
||||
|
||||
- **content**: The routing decision text
|
||||
- **model**: The AI model used for routing
|
||||
- **tokens**: Token usage statistics
|
||||
- **selectedPath**: Information about the selected path
|
||||
- **blockId**: ID of the selected destination block
|
||||
- **blockType**: Type of the selected block
|
||||
- **blockTitle**: Title of the selected block
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Custom Output Structures
|
||||
|
||||
Some blocks may produce custom output structures based on their configuration:
|
||||
|
||||
1. **Agent Blocks with Response Format**: When using a response format in an Agent block, the output structure will match the defined schema instead of the standard structure.
|
||||
|
||||
2. **Function Blocks**: The `result` field can contain any data structure returned by your function code.
|
||||
|
||||
3. **API Blocks**: The `data` field will contain whatever the API returns, which could be any valid JSON structure.
|
||||
|
||||
<Callout type="warning">
|
||||
Always check the actual output structure of your blocks during development to ensure you're
|
||||
referencing the correct fields in your connections.
|
||||
</Callout>
|
||||
|
||||
## Nested Data Structures
|
||||
|
||||
Many block outputs contain nested data structures. You can access these using dot notation in connection tags:
|
||||
|
||||
```
|
||||
<blockId.path.to.nested.data>
|
||||
```
|
||||
|
||||
For example:
|
||||
|
||||
- `<agent1.tokens.total>` - Access the total tokens from an Agent block
|
||||
- `<api1.data.results[0].id>` - Access the ID of the first result from an API response
|
||||
- `<function1.result.calculations.total>` - Access a nested field in a Function block's result
|
||||
41
apps/docs/content/docs/connections/index.mdx
Normal file
41
apps/docs/content/docs/connections/index.mdx
Normal file
@@ -0,0 +1,41 @@
|
||||
---
|
||||
title: Connections
|
||||
description: Connect your blocks to one another.
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Card, Cards } from 'fumadocs-ui/components/card'
|
||||
import { ConnectIcon } from '@/components/icons'
|
||||
|
||||
Connections are the pathways that allow data to flow between blocks in your workflow. They define how information is passed from one block to another, enabling you to create sophisticated, multi-step processes.
|
||||
|
||||
<Callout type="info">
|
||||
Properly configured connections are essential for creating effective workflows. They determine how
|
||||
data moves through your system and how blocks interact with each other.
|
||||
</Callout>
|
||||
|
||||
<div>
|
||||
<video autoPlay loop muted playsInline className="w-full" src="/connections.mp4"></video>
|
||||
</div>
|
||||
|
||||
## Connection Types
|
||||
|
||||
Sim Studio supports different types of connections that enable various workflow patterns:
|
||||
|
||||
<Cards>
|
||||
<Card title="Connection Basics" href="/connections/basics">
|
||||
Learn how connections work and how to create them in your workflows
|
||||
</Card>
|
||||
<Card title="Connection Tags" href="/connections/tags">
|
||||
Understand how to use connection tags to reference data between blocks
|
||||
</Card>
|
||||
<Card title="Data Structure" href="/connections/data-structure">
|
||||
Explore the output data structures of different block types
|
||||
</Card>
|
||||
<Card title="Accessing Data" href="/connections/accessing-data">
|
||||
Learn techniques for accessing and manipulating connected data
|
||||
</Card>
|
||||
<Card title="Best Practices" href="/connections/best-practices">
|
||||
Follow recommended patterns for effective connection management
|
||||
</Card>
|
||||
</Cards>
|
||||
4
apps/docs/content/docs/connections/meta.json
Normal file
4
apps/docs/content/docs/connections/meta.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"title": "Connections",
|
||||
"pages": ["basics", "tags", "data-structure", "accessing-data", "best-practices"]
|
||||
}
|
||||
109
apps/docs/content/docs/connections/tags.mdx
Normal file
109
apps/docs/content/docs/connections/tags.mdx
Normal file
@@ -0,0 +1,109 @@
|
||||
---
|
||||
title: Connection Tags
|
||||
description: Using connection tags to reference data between blocks
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
|
||||
Connection tags are visual representations of the data available from connected blocks. They provide an easy way to reference outputs from previous blocks in your workflow.
|
||||
|
||||
<div>
|
||||
<video autoPlay loop muted playsInline className="w-full" src="/connections.mp4"></video>
|
||||
</div>
|
||||
|
||||
### What Are Connection Tags?
|
||||
|
||||
Connection tags are interactive elements that appear when blocks are connected. They represent the data that can flow from one block to another and allow you to:
|
||||
|
||||
- Visualize available data from source blocks
|
||||
- Reference specific data fields in destination blocks
|
||||
- Create dynamic data flows between blocks
|
||||
|
||||
<Callout type="info">
|
||||
Connection tags make it easy to see what data is available from previous blocks and use it in your
|
||||
current block without having to remember complex data structures.
|
||||
</Callout>
|
||||
|
||||
## Using Connection Tags
|
||||
|
||||
There are two primary ways to use connection tags in your workflows:
|
||||
|
||||
<div className="my-6 grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div className="rounded-lg border border-gray-200 p-4 dark:border-gray-800">
|
||||
<h3 className="mb-2 text-lg font-medium">Drag and Drop</h3>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Click on a connection tag and drag it into input fields of destination blocks. A dropdown will
|
||||
appear showing available values.
|
||||
</div>
|
||||
<ol className="mt-2 list-decimal pl-5 text-sm text-gray-600 dark:text-gray-400">
|
||||
<li>Hover over a connection tag to see available data</li>
|
||||
<li>Click and drag the tag to an input field</li>
|
||||
<li>Select the specific data field from the dropdown</li>
|
||||
<li>The reference is inserted automatically</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-gray-200 p-4 dark:border-gray-800">
|
||||
<h3 className="mb-2 text-lg font-medium">Angle Bracket Syntax</h3>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Type <code><></code> in input fields to see a dropdown of available connection values
|
||||
from previous blocks.
|
||||
</div>
|
||||
<ol className="mt-2 list-decimal pl-5 text-sm text-gray-600 dark:text-gray-400">
|
||||
<li>Click in any input field where you want to use connected data</li>
|
||||
<li>
|
||||
Type <code><></code> to trigger the connection dropdown
|
||||
</li>
|
||||
<li>Browse and select the data you want to reference</li>
|
||||
<li>Continue typing or select from the dropdown to complete the reference</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
## Tag Syntax
|
||||
|
||||
Connection tags use a simple syntax to reference data:
|
||||
|
||||
```
|
||||
<blockId.path.to.data>
|
||||
```
|
||||
|
||||
Where:
|
||||
|
||||
- `blockId` is the identifier of the source block
|
||||
- `path.to.data` is the path to the specific data field
|
||||
|
||||
For example:
|
||||
|
||||
- `<agent1.content>` - References the content field from a block with ID "agent1"
|
||||
- `<api2.data.users[0].name>` - References the name of the first user in the users array from the data field of a block with ID "api2"
|
||||
|
||||
## Dynamic Tag References
|
||||
|
||||
Connection tags are evaluated at runtime, which means:
|
||||
|
||||
1. They always reference the most current data
|
||||
2. They can be used in expressions and combined with static text
|
||||
3. They can be nested within other data structures
|
||||
|
||||
### Examples
|
||||
|
||||
```javascript
|
||||
// Reference in text
|
||||
"The user's name is <userBlock.name>"
|
||||
|
||||
// Reference in JSON
|
||||
{
|
||||
"userName": "<userBlock.name>",
|
||||
"orderTotal": <apiBlock.data.total>
|
||||
}
|
||||
|
||||
// Reference in code
|
||||
const greeting = "Hello, <userBlock.name>!";
|
||||
const total = <apiBlock.data.total> * 1.1; // Add 10% tax
|
||||
```
|
||||
|
||||
<Callout type="warning">
|
||||
When using connection tags in numeric contexts, make sure the referenced data is actually a number
|
||||
to avoid type conversion issues.
|
||||
</Callout>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user