mirror of
https://github.com/simstudioai/sim.git
synced 2026-03-15 03:00:33 -04:00
Compare commits
48 Commits
improvemen
...
v0.5.102
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d2e6ff31d | ||
|
|
9be75e3633 | ||
|
|
40bab7731a | ||
|
|
96096e0ad1 | ||
|
|
647a3eb05b | ||
|
|
0195a4cd18 | ||
|
|
b42f80e8ab | ||
|
|
38ac86c4fd | ||
|
|
4cfe8be75a | ||
|
|
49db3ca50b | ||
|
|
e3ff595a84 | ||
|
|
b3424e2047 | ||
|
|
71ecf6c82e | ||
|
|
e9e5ba2c5b | ||
|
|
9233d4ebc9 | ||
|
|
78901ef517 | ||
|
|
47fef540cc | ||
|
|
f193e9ebbc | ||
|
|
c0f22d7722 | ||
|
|
bf0e25c9d0 | ||
|
|
d4f8ac8107 | ||
|
|
63fa938dd7 | ||
|
|
50b882a3ad | ||
|
|
c8a0b62a9c | ||
|
|
4ccb57371b | ||
|
|
c6e147e56a | ||
|
|
4fd0989264 | ||
|
|
345a95f48d | ||
|
|
e07963f88c | ||
|
|
25c59e3e2e | ||
|
|
dde098e8e5 | ||
|
|
5ae0115444 | ||
|
|
fbafe204e5 | ||
|
|
ba7d6ff298 | ||
|
|
40016e79a1 | ||
|
|
e4fb8b2fdd | ||
|
|
d98545d554 | ||
|
|
fadbad4085 | ||
|
|
67f8a687f6 | ||
|
|
af592349d3 | ||
|
|
0d86ea01f0 | ||
|
|
115f04e989 | ||
|
|
34d92fae89 | ||
|
|
67aa4bb332 | ||
|
|
15ace5e63f | ||
|
|
fdca73679d | ||
|
|
da46a387c9 | ||
|
|
b7e377ec4b |
@@ -532,6 +532,41 @@ outputs: {
|
||||
}
|
||||
```
|
||||
|
||||
### Typed JSON Outputs
|
||||
|
||||
When using `type: 'json'` and you know the object shape in advance, **describe the inner fields in the description** so downstream blocks know what properties are available. For well-known, stable objects, use nested output definitions instead:
|
||||
|
||||
```typescript
|
||||
outputs: {
|
||||
// BAD: Opaque json with no info about what's inside
|
||||
plan: { type: 'json', description: 'Zone plan information' },
|
||||
|
||||
// GOOD: Describe the known fields in the description
|
||||
plan: {
|
||||
type: 'json',
|
||||
description: 'Zone plan information (id, name, price, currency, frequency, is_subscribed)',
|
||||
},
|
||||
|
||||
// BEST: Use nested output definition when the shape is stable and well-known
|
||||
plan: {
|
||||
id: { type: 'string', description: 'Plan identifier' },
|
||||
name: { type: 'string', description: 'Plan name' },
|
||||
price: { type: 'number', description: 'Plan price' },
|
||||
currency: { type: 'string', description: 'Price currency' },
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Use the nested pattern when:
|
||||
- The object has a small, stable set of fields (< 10)
|
||||
- Downstream blocks will commonly access specific properties
|
||||
- The API response shape is well-documented and unlikely to change
|
||||
|
||||
Use `type: 'json'` with a descriptive string when:
|
||||
- The object has many fields or a dynamic shape
|
||||
- It represents a list/array of items
|
||||
- The shape varies by operation
|
||||
|
||||
## V2 Block Pattern
|
||||
|
||||
When creating V2 blocks (alongside legacy V1):
|
||||
@@ -695,6 +730,62 @@ Please provide the SVG and I'll convert it to a React component.
|
||||
You can usually find this in the service's brand/press kit page, or copy it from their website.
|
||||
```
|
||||
|
||||
## Advanced Mode for Optional Fields
|
||||
|
||||
Optional fields that are rarely used should be set to `mode: 'advanced'` so they don't clutter the basic UI. This includes:
|
||||
- Pagination tokens
|
||||
- Time range filters (start/end time)
|
||||
- Sort order options
|
||||
- Reply settings
|
||||
- Rarely used IDs (e.g., reply-to tweet ID, quote tweet ID)
|
||||
- Max results / limits
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: 'startTime',
|
||||
title: 'Start Time',
|
||||
type: 'short-input',
|
||||
placeholder: 'ISO 8601 timestamp',
|
||||
condition: { field: 'operation', value: ['search', 'list'] },
|
||||
mode: 'advanced', // Rarely used, hide from basic view
|
||||
}
|
||||
```
|
||||
|
||||
## WandConfig for Complex Inputs
|
||||
|
||||
Use `wandConfig` for fields that are hard to fill out manually, such as timestamps, comma-separated lists, and complex query strings. This gives users an AI-assisted input experience.
|
||||
|
||||
```typescript
|
||||
// Timestamps - use generationType: 'timestamp' to inject current date context
|
||||
{
|
||||
id: 'startTime',
|
||||
title: 'Start Time',
|
||||
type: 'short-input',
|
||||
mode: 'advanced',
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: 'Generate an ISO 8601 timestamp based on the user description. Return ONLY the timestamp string.',
|
||||
generationType: 'timestamp',
|
||||
},
|
||||
}
|
||||
|
||||
// Comma-separated lists - simple prompt without generationType
|
||||
{
|
||||
id: 'mediaIds',
|
||||
title: 'Media IDs',
|
||||
type: 'short-input',
|
||||
mode: 'advanced',
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: 'Generate a comma-separated list of media IDs. Return ONLY the comma-separated values.',
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Naming Convention
|
||||
|
||||
All tool IDs referenced in `tools.access` and returned by `tools.config.tool` MUST use `snake_case` (e.g., `x_create_tweet`, `slack_send_message`). Never use camelCase or PascalCase.
|
||||
|
||||
## Checklist Before Finishing
|
||||
|
||||
- [ ] All subBlocks have `id`, `title` (except switch), and `type`
|
||||
@@ -702,9 +793,24 @@ You can usually find this in the service's brand/press kit page, or copy it from
|
||||
- [ ] 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
|
||||
- [ ] Tools.access lists all tool IDs (snake_case)
|
||||
- [ ] Tools.config.tool returns correct tool ID (snake_case)
|
||||
- [ ] Outputs match tool outputs
|
||||
- [ ] Block registered in registry.ts
|
||||
- [ ] If icon missing: asked user to provide SVG
|
||||
- [ ] If triggers exist: `triggers` config set, trigger subBlocks spread
|
||||
- [ ] Optional/rarely-used fields set to `mode: 'advanced'`
|
||||
- [ ] Timestamps and complex inputs have `wandConfig` enabled
|
||||
|
||||
## Final Validation (Required)
|
||||
|
||||
After creating the block, you MUST validate it against every tool it references:
|
||||
|
||||
1. **Read every tool definition** that appears in `tools.access` — do not skip any
|
||||
2. **For each tool, verify the block has correct:**
|
||||
- SubBlock inputs that cover all required tool params (with correct `condition` to show for that operation)
|
||||
- SubBlock input types that match the tool param types (e.g., dropdown for enums, short-input for strings)
|
||||
- `tools.config.params` correctly maps subBlock IDs to tool param names (if they differ)
|
||||
- Type coercions in `tools.config.params` for any params that need conversion (Number(), Boolean(), JSON.parse())
|
||||
3. **Verify block outputs** cover the key fields returned by all tools
|
||||
4. **Verify conditions** — each subBlock should only show for the operations that actually use it
|
||||
|
||||
@@ -102,6 +102,7 @@ export const {service}{Action}Tool: ToolConfig<Params, Response> = {
|
||||
- Always use `?? []` for optional array fields
|
||||
- Set `optional: true` for outputs that may not exist
|
||||
- Never output raw JSON dumps - extract meaningful fields
|
||||
- When using `type: 'json'` and you know the object shape, define `properties` with the inner fields so downstream consumers know the structure. Only use bare `type: 'json'` when the shape is truly dynamic
|
||||
|
||||
## Step 3: Create Block
|
||||
|
||||
@@ -436,6 +437,12 @@ If creating V2 versions (API-aligned outputs):
|
||||
- [ ] Ran `bun run scripts/generate-docs.ts`
|
||||
- [ ] Verified docs file created
|
||||
|
||||
### Final Validation (Required)
|
||||
- [ ] Read every tool file and cross-referenced inputs/outputs against the API docs
|
||||
- [ ] Verified block subBlocks cover all required tool params with correct conditions
|
||||
- [ ] Verified block outputs match what the tools actually return
|
||||
- [ ] Verified `tools.config.params` correctly maps and coerces all param types
|
||||
|
||||
## Example Command
|
||||
|
||||
When the user asks to add an integration:
|
||||
@@ -685,13 +692,40 @@ return NextResponse.json({
|
||||
| `isUserFile` | `@/lib/core/utils/user-file` | Type guard for UserFile objects |
|
||||
| `FileInputSchema` | `@/lib/uploads/utils/file-schemas` | Zod schema for file validation |
|
||||
|
||||
### Advanced Mode for Optional Fields
|
||||
|
||||
Optional fields that are rarely used should be set to `mode: 'advanced'` so they don't clutter the basic UI. Examples: pagination tokens, time range filters, sort order, max results, reply settings.
|
||||
|
||||
### WandConfig for Complex Inputs
|
||||
|
||||
Use `wandConfig` for fields that are hard to fill out manually:
|
||||
- **Timestamps**: Use `generationType: 'timestamp'` to inject current date context into the AI prompt
|
||||
- **JSON arrays**: Use `generationType: 'json-object'` for structured data
|
||||
- **Complex queries**: Use a descriptive prompt explaining the expected format
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: 'startTime',
|
||||
title: 'Start Time',
|
||||
type: 'short-input',
|
||||
mode: 'advanced',
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: 'Generate an ISO 8601 timestamp. Return ONLY the timestamp string.',
|
||||
generationType: 'timestamp',
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### 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`
|
||||
2. **All tool IDs MUST be snake_case** - `stripe_create_payment`, not `stripeCreatePayment`. This applies to tool `id` fields, registry keys, `tools.access` arrays, and `tools.config.tool` return values
|
||||
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
|
||||
7. **Never pass Buffer directly to fetch** - Convert to `new Uint8Array(buffer)` for TypeScript compatibility
|
||||
8. **Always handle legacy file params** - Keep hidden `fileContent` params for backwards compatibility
|
||||
9. **Optional fields use advanced mode** - Set `mode: 'advanced'` on rarely-used optional fields
|
||||
10. **Complex inputs need wandConfig** - Timestamps, JSON arrays, and other hard-to-type values should have `wandConfig` enabled
|
||||
|
||||
@@ -147,9 +147,18 @@ closedAt: {
|
||||
},
|
||||
```
|
||||
|
||||
### Nested Properties
|
||||
For complex outputs, define nested structure:
|
||||
### Typed JSON Outputs
|
||||
|
||||
When using `type: 'json'` and you know the object shape in advance, **always define the inner structure** using `properties` so downstream consumers know what fields are available:
|
||||
|
||||
```typescript
|
||||
// BAD: Opaque json with no info about what's inside
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Response metadata',
|
||||
},
|
||||
|
||||
// GOOD: Define the known properties
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Response metadata',
|
||||
@@ -159,7 +168,10 @@ metadata: {
|
||||
count: { type: 'number', description: 'Total count' },
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
For arrays of objects, define the item structure:
|
||||
```typescript
|
||||
items: {
|
||||
type: 'array',
|
||||
description: 'List of items',
|
||||
@@ -173,6 +185,8 @@ items: {
|
||||
},
|
||||
```
|
||||
|
||||
Only use bare `type: 'json'` without `properties` when the shape is truly dynamic or unknown.
|
||||
|
||||
## Critical Rules for transformResponse
|
||||
|
||||
### Handle Nullable Fields
|
||||
@@ -272,8 +286,13 @@ If creating V2 tools (API-aligned outputs), use `_v2` suffix:
|
||||
- Version: `'2.0.0'`
|
||||
- Outputs: Flat, API-aligned (no content/metadata wrapper)
|
||||
|
||||
## Naming Convention
|
||||
|
||||
All tool IDs MUST use `snake_case`: `{service}_{action}` (e.g., `x_create_tweet`, `slack_send_message`). Never use camelCase or PascalCase for tool IDs.
|
||||
|
||||
## Checklist Before Finishing
|
||||
|
||||
- [ ] All tool IDs use snake_case
|
||||
- [ ] All params have explicit `required: true` or `required: false`
|
||||
- [ ] All params have appropriate `visibility`
|
||||
- [ ] All nullable response fields use `?? null`
|
||||
@@ -281,4 +300,22 @@ If creating V2 tools (API-aligned outputs), use `_v2` suffix:
|
||||
- [ ] No raw JSON dumps in outputs
|
||||
- [ ] Types file has all interfaces
|
||||
- [ ] Index.ts exports all tools
|
||||
- [ ] Tool IDs use snake_case
|
||||
|
||||
## Final Validation (Required)
|
||||
|
||||
After creating all tools, you MUST validate every tool before finishing:
|
||||
|
||||
1. **Read every tool file** you created — do not skip any
|
||||
2. **Cross-reference with the API docs** to verify:
|
||||
- All required params are marked `required: true`
|
||||
- All optional params are marked `required: false`
|
||||
- Param types match the API (string, number, boolean, json)
|
||||
- Request URL, method, headers, and body match the API spec
|
||||
- `transformResponse` extracts the correct fields from the API response
|
||||
- All output fields match what the API actually returns
|
||||
- No fields are missing from outputs that the API provides
|
||||
- No extra fields are defined in outputs that the API doesn't return
|
||||
3. **Verify consistency** across tools:
|
||||
- Shared types in `types.ts` match all tools that use them
|
||||
- Tool IDs in the barrel export match the tool file definitions
|
||||
- Error handling is consistent (error checks, meaningful messages)
|
||||
|
||||
283
.claude/commands/validate-integration.md
Normal file
283
.claude/commands/validate-integration.md
Normal file
@@ -0,0 +1,283 @@
|
||||
---
|
||||
description: Validate an existing Sim integration (tools, block, registry) against the service's API docs
|
||||
argument-hint: <service-name> [api-docs-url]
|
||||
---
|
||||
|
||||
# Validate Integration Skill
|
||||
|
||||
You are an expert auditor for Sim integrations. Your job is to thoroughly validate that an existing integration is correct, complete, and follows all conventions.
|
||||
|
||||
## Your Task
|
||||
|
||||
When the user asks you to validate an integration:
|
||||
1. Read the service's API documentation (via WebFetch or Context7)
|
||||
2. Read every tool, the block, and registry entries
|
||||
3. Cross-reference everything against the API docs and Sim conventions
|
||||
4. Report all issues found, grouped by severity (critical, warning, suggestion)
|
||||
5. Fix all issues after reporting them
|
||||
|
||||
## Step 1: Gather All Files
|
||||
|
||||
Read **every** file for the integration — do not skip any:
|
||||
|
||||
```
|
||||
apps/sim/tools/{service}/ # All tool files, types.ts, index.ts
|
||||
apps/sim/blocks/blocks/{service}.ts # Block definition
|
||||
apps/sim/tools/registry.ts # Tool registry entries for this service
|
||||
apps/sim/blocks/registry.ts # Block registry entry for this service
|
||||
apps/sim/components/icons.tsx # Icon definition
|
||||
apps/sim/lib/auth/auth.ts # OAuth scopes (if OAuth service)
|
||||
apps/sim/lib/oauth/oauth.ts # OAuth provider config (if OAuth service)
|
||||
```
|
||||
|
||||
## Step 2: Pull API Documentation
|
||||
|
||||
Fetch the official API docs for the service. This is the **source of truth** for:
|
||||
- Endpoint URLs, HTTP methods, and auth headers
|
||||
- Required vs optional parameters
|
||||
- Parameter types and allowed values
|
||||
- Response shapes and field names
|
||||
- Pagination patterns (which param name, which response field)
|
||||
- Rate limits and error formats
|
||||
|
||||
## Step 3: Validate Tools
|
||||
|
||||
For **every** tool file, check:
|
||||
|
||||
### Tool ID and Naming
|
||||
- [ ] Tool ID uses `snake_case`: `{service}_{action}` (e.g., `x_create_tweet`, `slack_send_message`)
|
||||
- [ ] Tool `name` is human-readable (e.g., `'X Create Tweet'`)
|
||||
- [ ] Tool `description` is a concise one-liner describing what it does
|
||||
- [ ] Tool `version` is set (`'1.0.0'` or `'2.0.0'` for V2)
|
||||
|
||||
### Params
|
||||
- [ ] All required API params are marked `required: true`
|
||||
- [ ] All optional API params are marked `required: false`
|
||||
- [ ] Every param has explicit `required: true` or `required: false` — never omitted
|
||||
- [ ] Param types match the API (`'string'`, `'number'`, `'boolean'`, `'json'`)
|
||||
- [ ] Visibility is correct:
|
||||
- `'hidden'` — ONLY for OAuth access tokens and system-injected params
|
||||
- `'user-only'` — for API keys, credentials, and account-specific IDs the user must provide
|
||||
- `'user-or-llm'` — for everything else (search queries, content, filters, IDs that could come from other blocks)
|
||||
- [ ] Every param has a `description` that explains what it does
|
||||
|
||||
### Request
|
||||
- [ ] URL matches the API endpoint exactly (correct base URL, path segments, path params)
|
||||
- [ ] HTTP method matches the API spec (GET, POST, PUT, PATCH, DELETE)
|
||||
- [ ] Headers include correct auth pattern:
|
||||
- OAuth: `Authorization: Bearer ${params.accessToken}`
|
||||
- API Key: correct header name and format per the service's docs
|
||||
- [ ] `Content-Type` header is set for POST/PUT/PATCH requests
|
||||
- [ ] Body sends all required fields and only includes optional fields when provided
|
||||
- [ ] For GET requests with query params: URL is constructed correctly with query string
|
||||
- [ ] ID fields in URL paths are `.trim()`-ed to prevent copy-paste whitespace errors
|
||||
- [ ] Path params use template literals correctly: `` `https://api.service.com/v1/${params.id.trim()}` ``
|
||||
|
||||
### Response / transformResponse
|
||||
- [ ] Correctly parses the API response (`await response.json()`)
|
||||
- [ ] Extracts the right fields from the response structure (e.g., `data.data` vs `data` vs `data.results`)
|
||||
- [ ] All nullable fields use `?? null`
|
||||
- [ ] All optional arrays use `?? []`
|
||||
- [ ] Error cases are handled: checks for missing/empty data and returns meaningful error
|
||||
- [ ] Does NOT do raw JSON dumps — extracts meaningful, individual fields
|
||||
|
||||
### Outputs
|
||||
- [ ] All output fields match what the API actually returns
|
||||
- [ ] No fields are missing that the API provides and users would commonly need
|
||||
- [ ] No phantom fields defined that the API doesn't return
|
||||
- [ ] `optional: true` is set on fields that may not exist in all responses
|
||||
- [ ] When using `type: 'json'` and the shape is known, `properties` defines the inner fields
|
||||
- [ ] When using `type: 'array'`, `items` defines the item structure with `properties`
|
||||
- [ ] Field descriptions are accurate and helpful
|
||||
|
||||
### Types (types.ts)
|
||||
- [ ] Has param interfaces for every tool (e.g., `XCreateTweetParams`)
|
||||
- [ ] Has response interfaces for every tool (extending `ToolResponse`)
|
||||
- [ ] Optional params use `?` in the interface (e.g., `replyTo?: string`)
|
||||
- [ ] Field names in types match actual API field names
|
||||
- [ ] Shared response types are properly reused (e.g., `XTweetResponse` shared across tweet tools)
|
||||
|
||||
### Barrel Export (index.ts)
|
||||
- [ ] Every tool is exported
|
||||
- [ ] All types are re-exported (`export * from './types'`)
|
||||
- [ ] No orphaned exports (tools that don't exist)
|
||||
|
||||
### Tool Registry (tools/registry.ts)
|
||||
- [ ] Every tool is imported and registered
|
||||
- [ ] Registry keys use snake_case and match tool IDs exactly
|
||||
- [ ] Entries are in alphabetical order within the file
|
||||
|
||||
## Step 4: Validate Block
|
||||
|
||||
### Block ↔ Tool Alignment (CRITICAL)
|
||||
|
||||
This is the most important validation — the block must be perfectly aligned with every tool it references.
|
||||
|
||||
For **each tool** in `tools.access`:
|
||||
- [ ] The operation dropdown has an option whose ID matches the tool ID (or the `tools.config.tool` function correctly maps to it)
|
||||
- [ ] Every **required** tool param (except `accessToken`) has a corresponding subBlock input that is:
|
||||
- Shown when that operation is selected (correct `condition`)
|
||||
- Marked as `required: true` (or conditionally required)
|
||||
- [ ] Every **optional** tool param has a corresponding subBlock input (or is intentionally omitted if truly never needed)
|
||||
- [ ] SubBlock `id` values are unique across the entire block — no duplicates even across different conditions
|
||||
- [ ] The `tools.config.tool` function returns the correct tool ID for every possible operation value
|
||||
- [ ] The `tools.config.params` function correctly maps subBlock IDs to tool param names when they differ
|
||||
|
||||
### SubBlocks
|
||||
- [ ] Operation dropdown lists ALL tool operations available in `tools.access`
|
||||
- [ ] Dropdown option labels are human-readable and descriptive
|
||||
- [ ] Conditions use correct syntax:
|
||||
- Single value: `{ field: 'operation', value: 'x_create_tweet' }`
|
||||
- Multiple values (OR): `{ field: 'operation', value: ['x_create_tweet', 'x_delete_tweet'] }`
|
||||
- Negation: `{ field: 'operation', value: 'delete', not: true }`
|
||||
- Compound: `{ field: 'op', value: 'send', and: { field: 'type', value: 'dm' } }`
|
||||
- [ ] Condition arrays include ALL operations that use that field — none missing
|
||||
- [ ] `dependsOn` is set for fields that need other values (selectors depending on credential, cascading dropdowns)
|
||||
- [ ] SubBlock types match tool param types:
|
||||
- Enum/fixed options → `dropdown`
|
||||
- Free text → `short-input`
|
||||
- Long text/content → `long-input`
|
||||
- True/false → `dropdown` with Yes/No options (not `switch` unless purely UI toggle)
|
||||
- Credentials → `oauth-input` with correct `serviceId`
|
||||
- [ ] Dropdown `value: () => 'default'` is set for dropdowns with a sensible default
|
||||
|
||||
### Advanced Mode
|
||||
- [ ] Optional, rarely-used fields are set to `mode: 'advanced'`:
|
||||
- Pagination tokens / next tokens
|
||||
- Time range filters (start/end time)
|
||||
- Sort order / direction options
|
||||
- Max results / per page limits
|
||||
- Reply settings / threading options
|
||||
- Rarely used IDs (reply-to, quote-tweet, etc.)
|
||||
- Exclude filters
|
||||
- [ ] **Required** fields are NEVER set to `mode: 'advanced'`
|
||||
- [ ] Fields that users fill in most of the time are NOT set to `mode: 'advanced'`
|
||||
|
||||
### WandConfig
|
||||
- [ ] Timestamp fields have `wandConfig` with `generationType: 'timestamp'`
|
||||
- [ ] Comma-separated list fields have `wandConfig` with a descriptive prompt
|
||||
- [ ] Complex filter/query fields have `wandConfig` with format examples in the prompt
|
||||
- [ ] All `wandConfig` prompts end with "Return ONLY the [format] - no explanations, no extra text."
|
||||
- [ ] `wandConfig.placeholder` describes what to type in natural language
|
||||
|
||||
### Tools Config
|
||||
- [ ] `tools.access` lists **every** tool ID the block can use — none missing
|
||||
- [ ] `tools.config.tool` returns the correct tool ID for each operation
|
||||
- [ ] Type coercions are in `tools.config.params` (runs at execution time), NOT in `tools.config.tool` (runs at serialization time before variable resolution)
|
||||
- [ ] `tools.config.params` handles:
|
||||
- `Number()` conversion for numeric params that come as strings from inputs
|
||||
- `Boolean` / string-to-boolean conversion for toggle params
|
||||
- Empty string → `undefined` conversion for optional dropdown values
|
||||
- Any subBlock ID → tool param name remapping
|
||||
- [ ] No `Number()`, `JSON.parse()`, or other coercions in `tools.config.tool` — these would destroy dynamic references like `<Block.output>`
|
||||
|
||||
### Block Outputs
|
||||
- [ ] Outputs cover the key fields returned by ALL tools (not just one operation)
|
||||
- [ ] Output types are correct (`'string'`, `'number'`, `'boolean'`, `'json'`)
|
||||
- [ ] `type: 'json'` outputs either:
|
||||
- Describe inner fields in the description string (GOOD): `'User profile (id, name, username, bio)'`
|
||||
- Use nested output definitions (BEST): `{ id: { type: 'string' }, name: { type: 'string' } }`
|
||||
- [ ] No opaque `type: 'json'` with vague descriptions like `'Response data'`
|
||||
- [ ] Outputs that only appear for certain operations use `condition` if supported, or document which operations return them
|
||||
|
||||
### Block Metadata
|
||||
- [ ] `type` is snake_case (e.g., `'x'`, `'cloudflare'`)
|
||||
- [ ] `name` is human-readable (e.g., `'X'`, `'Cloudflare'`)
|
||||
- [ ] `description` is a concise one-liner
|
||||
- [ ] `longDescription` provides detail for docs
|
||||
- [ ] `docsLink` points to `'https://docs.sim.ai/tools/{service}'`
|
||||
- [ ] `category` is `'tools'`
|
||||
- [ ] `bgColor` uses the service's brand color hex
|
||||
- [ ] `icon` references the correct icon component from `@/components/icons`
|
||||
- [ ] `authMode` is set correctly (`AuthMode.OAuth` or `AuthMode.ApiKey`)
|
||||
- [ ] Block is registered in `blocks/registry.ts` alphabetically
|
||||
|
||||
### Block Inputs
|
||||
- [ ] `inputs` section lists all subBlock params that the block accepts
|
||||
- [ ] Input types match the subBlock types
|
||||
- [ ] When using `canonicalParamId`, inputs list the canonical ID (not the raw subBlock IDs)
|
||||
|
||||
## Step 5: Validate OAuth Scopes (if OAuth service)
|
||||
|
||||
- [ ] `auth.ts` scopes include ALL scopes needed by ALL tools in the integration
|
||||
- [ ] `oauth.ts` provider config scopes match `auth.ts` scopes
|
||||
- [ ] Block `requiredScopes` (if defined) matches `auth.ts` scopes
|
||||
- [ ] No excess scopes that aren't needed by any tool
|
||||
- [ ] Each scope has a human-readable description in `oauth-required-modal.tsx`'s `SCOPE_DESCRIPTIONS`
|
||||
|
||||
## Step 6: Validate Pagination Consistency
|
||||
|
||||
If any tools support pagination:
|
||||
- [ ] Pagination param names match the API docs (e.g., `pagination_token` vs `next_token` vs `cursor`)
|
||||
- [ ] Different API endpoints that use different pagination param names have separate subBlocks in the block
|
||||
- [ ] Pagination response fields (`nextToken`, `cursor`, etc.) are included in tool outputs
|
||||
- [ ] Pagination subBlocks are set to `mode: 'advanced'`
|
||||
|
||||
## Step 7: Validate Error Handling
|
||||
|
||||
- [ ] `transformResponse` checks for error conditions before accessing data
|
||||
- [ ] Error responses include meaningful messages (not just generic "failed")
|
||||
- [ ] HTTP error status codes are handled (check `response.ok` or status codes)
|
||||
|
||||
## Step 8: Report and Fix
|
||||
|
||||
### Report Format
|
||||
|
||||
Group findings by severity:
|
||||
|
||||
**Critical** (will cause runtime errors or incorrect behavior):
|
||||
- Wrong endpoint URL or HTTP method
|
||||
- Missing required params or wrong `required` flag
|
||||
- Incorrect response field mapping (accessing wrong path in response)
|
||||
- Missing error handling that would cause crashes
|
||||
- Tool ID mismatch between tool file, registry, and block `tools.access`
|
||||
- OAuth scopes missing in `auth.ts` that tools need
|
||||
- `tools.config.tool` returning wrong tool ID for an operation
|
||||
- Type coercions in `tools.config.tool` instead of `tools.config.params`
|
||||
|
||||
**Warning** (follows conventions incorrectly or has usability issues):
|
||||
- Optional field not set to `mode: 'advanced'`
|
||||
- Missing `wandConfig` on timestamp/complex fields
|
||||
- Wrong `visibility` on params (e.g., `'hidden'` instead of `'user-or-llm'`)
|
||||
- Missing `optional: true` on nullable outputs
|
||||
- Opaque `type: 'json'` without property descriptions
|
||||
- Missing `.trim()` on ID fields in request URLs
|
||||
- Missing `?? null` on nullable response fields
|
||||
- Block condition array missing an operation that uses that field
|
||||
- Missing scope description in `oauth-required-modal.tsx`
|
||||
|
||||
**Suggestion** (minor improvements):
|
||||
- Better description text
|
||||
- Inconsistent naming across tools
|
||||
- Missing `longDescription` or `docsLink`
|
||||
- Pagination fields that could benefit from `wandConfig`
|
||||
|
||||
### Fix All Issues
|
||||
|
||||
After reporting, fix every **critical** and **warning** issue. Apply **suggestions** where they don't add unnecessary complexity.
|
||||
|
||||
### Validation Output
|
||||
|
||||
After fixing, confirm:
|
||||
1. `bun run lint` passes with no fixes needed
|
||||
2. TypeScript compiles clean (no type errors)
|
||||
3. Re-read all modified files to verify fixes are correct
|
||||
|
||||
## Checklist Summary
|
||||
|
||||
- [ ] Read ALL tool files, block, types, index, and registries
|
||||
- [ ] Pulled and read official API documentation
|
||||
- [ ] Validated every tool's ID, params, request, response, outputs, and types against API docs
|
||||
- [ ] Validated block ↔ tool alignment (every tool param has a subBlock, every condition is correct)
|
||||
- [ ] Validated advanced mode on optional/rarely-used fields
|
||||
- [ ] Validated wandConfig on timestamps and complex inputs
|
||||
- [ ] Validated tools.config mapping, tool selector, and type coercions
|
||||
- [ ] Validated block outputs match what tools return, with typed JSON where possible
|
||||
- [ ] Validated OAuth scopes alignment across auth.ts, oauth.ts, block, and modal (if OAuth)
|
||||
- [ ] Validated pagination consistency across tools and block
|
||||
- [ ] Validated error handling (error checks, meaningful messages)
|
||||
- [ ] Validated registry entries (tools and block, alphabetical, correct imports)
|
||||
- [ ] Reported all issues grouped by severity
|
||||
- [ ] Fixed all critical and warning issues
|
||||
- [ ] Ran `bun run lint` after fixes
|
||||
- [ ] Verified TypeScript compiles clean
|
||||
@@ -8,51 +8,210 @@ paths:
|
||||
|
||||
Use Vitest. Test files: `feature.ts` → `feature.test.ts`
|
||||
|
||||
## Global Mocks (vitest.setup.ts)
|
||||
|
||||
These modules are mocked globally — do NOT re-mock them in test files unless you need to override behavior:
|
||||
|
||||
- `@sim/db` → `databaseMock`
|
||||
- `drizzle-orm` → `drizzleOrmMock`
|
||||
- `@sim/logger` → `loggerMock`
|
||||
- `@/stores/console/store`, `@/stores/terminal`, `@/stores/execution/store`
|
||||
- `@/blocks/registry`
|
||||
- `@trigger.dev/sdk`
|
||||
|
||||
## Structure
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { databaseMock, loggerMock } from '@sim/testing'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { createMockRequest } from '@sim/testing'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('@sim/db', () => databaseMock)
|
||||
vi.mock('@sim/logger', () => loggerMock)
|
||||
const { mockGetSession } = vi.hoisted(() => ({
|
||||
mockGetSession: vi.fn(),
|
||||
}))
|
||||
|
||||
import { myFunction } from '@/lib/feature'
|
||||
vi.mock('@/lib/auth', () => ({
|
||||
auth: { api: { getSession: vi.fn() } },
|
||||
getSession: mockGetSession,
|
||||
}))
|
||||
|
||||
describe('myFunction', () => {
|
||||
beforeEach(() => vi.clearAllMocks())
|
||||
it.concurrent('isolated tests run in parallel', () => { ... })
|
||||
import { GET, POST } from '@/app/api/my-route/route'
|
||||
|
||||
describe('my route', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockGetSession.mockResolvedValue({ user: { id: 'user-1' } })
|
||||
})
|
||||
|
||||
it('returns data', async () => {
|
||||
const req = createMockRequest('GET')
|
||||
const res = await GET(req)
|
||||
expect(res.status).toBe(200)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Performance Rules (Critical)
|
||||
|
||||
### NEVER use `vi.resetModules()` + `vi.doMock()` + `await import()`
|
||||
|
||||
This is the #1 cause of slow tests. It forces complete module re-evaluation per test.
|
||||
|
||||
```typescript
|
||||
// BAD — forces module re-evaluation every test (~50-100ms each)
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
vi.doMock('@/lib/auth', () => ({ getSession: vi.fn() }))
|
||||
})
|
||||
it('test', async () => {
|
||||
const { GET } = await import('./route') // slow dynamic import
|
||||
})
|
||||
|
||||
// GOOD — module loaded once, mocks reconfigured per test (~1ms each)
|
||||
const { mockGetSession } = vi.hoisted(() => ({
|
||||
mockGetSession: vi.fn(),
|
||||
}))
|
||||
vi.mock('@/lib/auth', () => ({ getSession: mockGetSession }))
|
||||
import { GET } from '@/app/api/my-route/route'
|
||||
|
||||
beforeEach(() => { vi.clearAllMocks() })
|
||||
it('test', () => {
|
||||
mockGetSession.mockResolvedValue({ user: { id: '1' } })
|
||||
})
|
||||
```
|
||||
|
||||
**Only exception:** Singleton modules that cache state at module scope (e.g., Redis clients, connection pools). These genuinely need `vi.resetModules()` + dynamic import to get a fresh instance per test.
|
||||
|
||||
### NEVER use `vi.importActual()`
|
||||
|
||||
This defeats the purpose of mocking by loading the real module and all its dependencies.
|
||||
|
||||
```typescript
|
||||
// BAD — loads real module + all transitive deps
|
||||
vi.mock('@/lib/workspaces/utils', async () => {
|
||||
const actual = await vi.importActual('@/lib/workspaces/utils')
|
||||
return { ...actual, myFn: vi.fn() }
|
||||
})
|
||||
|
||||
// GOOD — mock everything, only implement what tests need
|
||||
vi.mock('@/lib/workspaces/utils', () => ({
|
||||
myFn: vi.fn(),
|
||||
otherFn: vi.fn(),
|
||||
}))
|
||||
```
|
||||
|
||||
### NEVER use `mockAuth()`, `mockConsoleLogger()`, or `setupCommonApiMocks()` from `@sim/testing`
|
||||
|
||||
These helpers internally use `vi.doMock()` which is slow. Use direct `vi.hoisted()` + `vi.mock()` instead.
|
||||
|
||||
### Mock heavy transitive dependencies
|
||||
|
||||
If a module under test imports `@/blocks` (200+ files), `@/tools/registry`, or other heavy modules, mock them:
|
||||
|
||||
```typescript
|
||||
vi.mock('@/blocks', () => ({
|
||||
getBlock: () => null,
|
||||
getAllBlocks: () => ({}),
|
||||
getAllBlockTypes: () => [],
|
||||
registry: {},
|
||||
}))
|
||||
```
|
||||
|
||||
### Use `@vitest-environment node` unless DOM is needed
|
||||
|
||||
Only use `@vitest-environment jsdom` if the test uses `window`, `document`, `FormData`, or other browser APIs. Node environment is significantly faster.
|
||||
|
||||
### Avoid real timers in tests
|
||||
|
||||
```typescript
|
||||
// BAD
|
||||
await new Promise(r => setTimeout(r, 500))
|
||||
|
||||
// GOOD — use minimal delays or fake timers
|
||||
await new Promise(r => setTimeout(r, 1))
|
||||
// or
|
||||
vi.useFakeTimers()
|
||||
```
|
||||
|
||||
## Mock Pattern Reference
|
||||
|
||||
### Auth mocking (API routes)
|
||||
|
||||
```typescript
|
||||
const { mockGetSession } = vi.hoisted(() => ({
|
||||
mockGetSession: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/auth', () => ({
|
||||
auth: { api: { getSession: vi.fn() } },
|
||||
getSession: mockGetSession,
|
||||
}))
|
||||
|
||||
// In tests:
|
||||
mockGetSession.mockResolvedValue({ user: { id: 'user-1', email: 'test@example.com' } })
|
||||
mockGetSession.mockResolvedValue(null) // unauthenticated
|
||||
```
|
||||
|
||||
### Hybrid auth mocking
|
||||
|
||||
```typescript
|
||||
const { mockCheckSessionOrInternalAuth } = vi.hoisted(() => ({
|
||||
mockCheckSessionOrInternalAuth: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/auth/hybrid', () => ({
|
||||
checkSessionOrInternalAuth: mockCheckSessionOrInternalAuth,
|
||||
}))
|
||||
|
||||
// In tests:
|
||||
mockCheckSessionOrInternalAuth.mockResolvedValue({
|
||||
success: true, userId: 'user-1', authType: 'session',
|
||||
})
|
||||
```
|
||||
|
||||
### Database chain mocking
|
||||
|
||||
```typescript
|
||||
const { mockSelect, mockFrom, mockWhere } = vi.hoisted(() => ({
|
||||
mockSelect: vi.fn(),
|
||||
mockFrom: vi.fn(),
|
||||
mockWhere: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@sim/db', () => ({
|
||||
db: { select: mockSelect },
|
||||
}))
|
||||
|
||||
beforeEach(() => {
|
||||
mockSelect.mockReturnValue({ from: mockFrom })
|
||||
mockFrom.mockReturnValue({ where: mockWhere })
|
||||
mockWhere.mockResolvedValue([{ id: '1', name: 'test' }])
|
||||
})
|
||||
```
|
||||
|
||||
## @sim/testing Package
|
||||
|
||||
Always prefer over local mocks.
|
||||
Always prefer over local test data.
|
||||
|
||||
| Category | Utilities |
|
||||
|----------|-----------|
|
||||
| **Mocks** | `loggerMock`, `databaseMock`, `setupGlobalFetchMock()` |
|
||||
| **Factories** | `createSession()`, `createWorkflowRecord()`, `createBlock()`, `createExecutorContext()` |
|
||||
| **Mocks** | `loggerMock`, `databaseMock`, `drizzleOrmMock`, `setupGlobalFetchMock()` |
|
||||
| **Factories** | `createSession()`, `createWorkflowRecord()`, `createBlock()`, `createExecutionContext()` |
|
||||
| **Builders** | `WorkflowBuilder`, `ExecutionContextBuilder` |
|
||||
| **Assertions** | `expectWorkflowAccessGranted()`, `expectBlockExecuted()` |
|
||||
| **Requests** | `createMockRequest()`, `createEnvMock()` |
|
||||
|
||||
## Rules
|
||||
## Rules Summary
|
||||
|
||||
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. `@vitest-environment node` unless DOM is required
|
||||
2. `vi.hoisted()` + `vi.mock()` + static imports — never `vi.resetModules()` + `vi.doMock()` + dynamic imports
|
||||
3. `vi.mock()` calls before importing mocked modules
|
||||
4. `@sim/testing` utilities over local mocks
|
||||
5. `beforeEach(() => vi.clearAllMocks())` to reset state — no redundant `afterEach`
|
||||
6. No `vi.importActual()` — mock everything explicitly
|
||||
7. No `mockAuth()`, `mockConsoleLogger()`, `setupCommonApiMocks()` — use direct mocks
|
||||
8. Mock heavy deps (`@/blocks`, `@/tools/registry`, `@/triggers`) in tests that don't need them
|
||||
9. Use absolute imports in test files
|
||||
10. Avoid real timers — use 1ms delays or `vi.useFakeTimers()`
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
---
|
||||
description: SEO and GEO guidelines for the landing page
|
||||
globs: ["apps/sim/app/(home)/**/*.tsx"]
|
||||
---
|
||||
|
||||
# Landing Page — SEO / GEO
|
||||
|
||||
## SEO
|
||||
|
||||
- One `<h1>` per page, in Hero only — never add another.
|
||||
- Strict heading hierarchy: H1 (Hero) → H2 (section titles) → H3 (feature names).
|
||||
- Every section: `<section id="…" aria-labelledby="…-heading">`.
|
||||
- Decorative/animated elements: `aria-hidden="true"`.
|
||||
- All internal routes use Next.js `<Link>` (crawlable). External links get `rel="noopener noreferrer"`.
|
||||
- Navbar is a Server Component (no `'use client'`) for immediate crawlability. Logo `<Image>` has `priority` (LCP element).
|
||||
- Navbar `<nav>` carries `SiteNavigationElement` schema.org markup.
|
||||
- Feature lists must stay in sync with `WebApplication.featureList` in `structured-data.tsx`.
|
||||
|
||||
## GEO (Generative Engine Optimisation)
|
||||
|
||||
- **Answer-first pattern**: each section's H2 + subtitle should directly answer a user question (e.g. "What is Sim?", "How fast can I deploy?").
|
||||
- **Atomic answer blocks**: each feature / template card should be independently extractable by an AI summariser.
|
||||
- **Entity consistency**: always write "Sim" by name — never "the platform" or "our tool".
|
||||
- **Keyword density**: first 150 visible chars of Hero must name "Sim", "AI agents", "agentic workflows".
|
||||
- **sr-only summaries**: Hero and Templates each have a `<p className="sr-only">` (~50 words) as an atomic product/catalog summary for AI citation.
|
||||
- **Specific numbers**: prefer concrete figures ("1,000+ integrations", "15+ AI providers") over vague claims.
|
||||
@@ -7,51 +7,210 @@ globs: ["apps/sim/**/*.test.ts", "apps/sim/**/*.test.tsx"]
|
||||
|
||||
Use Vitest. Test files: `feature.ts` → `feature.test.ts`
|
||||
|
||||
## Global Mocks (vitest.setup.ts)
|
||||
|
||||
These modules are mocked globally — do NOT re-mock them in test files unless you need to override behavior:
|
||||
|
||||
- `@sim/db` → `databaseMock`
|
||||
- `drizzle-orm` → `drizzleOrmMock`
|
||||
- `@sim/logger` → `loggerMock`
|
||||
- `@/stores/console/store`, `@/stores/terminal`, `@/stores/execution/store`
|
||||
- `@/blocks/registry`
|
||||
- `@trigger.dev/sdk`
|
||||
|
||||
## Structure
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { databaseMock, loggerMock } from '@sim/testing'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { createMockRequest } from '@sim/testing'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('@sim/db', () => databaseMock)
|
||||
vi.mock('@sim/logger', () => loggerMock)
|
||||
const { mockGetSession } = vi.hoisted(() => ({
|
||||
mockGetSession: vi.fn(),
|
||||
}))
|
||||
|
||||
import { myFunction } from '@/lib/feature'
|
||||
vi.mock('@/lib/auth', () => ({
|
||||
auth: { api: { getSession: vi.fn() } },
|
||||
getSession: mockGetSession,
|
||||
}))
|
||||
|
||||
describe('myFunction', () => {
|
||||
beforeEach(() => vi.clearAllMocks())
|
||||
it.concurrent('isolated tests run in parallel', () => { ... })
|
||||
import { GET, POST } from '@/app/api/my-route/route'
|
||||
|
||||
describe('my route', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockGetSession.mockResolvedValue({ user: { id: 'user-1' } })
|
||||
})
|
||||
|
||||
it('returns data', async () => {
|
||||
const req = createMockRequest('GET')
|
||||
const res = await GET(req)
|
||||
expect(res.status).toBe(200)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Performance Rules (Critical)
|
||||
|
||||
### NEVER use `vi.resetModules()` + `vi.doMock()` + `await import()`
|
||||
|
||||
This is the #1 cause of slow tests. It forces complete module re-evaluation per test.
|
||||
|
||||
```typescript
|
||||
// BAD — forces module re-evaluation every test (~50-100ms each)
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
vi.doMock('@/lib/auth', () => ({ getSession: vi.fn() }))
|
||||
})
|
||||
it('test', async () => {
|
||||
const { GET } = await import('./route') // slow dynamic import
|
||||
})
|
||||
|
||||
// GOOD — module loaded once, mocks reconfigured per test (~1ms each)
|
||||
const { mockGetSession } = vi.hoisted(() => ({
|
||||
mockGetSession: vi.fn(),
|
||||
}))
|
||||
vi.mock('@/lib/auth', () => ({ getSession: mockGetSession }))
|
||||
import { GET } from '@/app/api/my-route/route'
|
||||
|
||||
beforeEach(() => { vi.clearAllMocks() })
|
||||
it('test', () => {
|
||||
mockGetSession.mockResolvedValue({ user: { id: '1' } })
|
||||
})
|
||||
```
|
||||
|
||||
**Only exception:** Singleton modules that cache state at module scope (e.g., Redis clients, connection pools). These genuinely need `vi.resetModules()` + dynamic import to get a fresh instance per test.
|
||||
|
||||
### NEVER use `vi.importActual()`
|
||||
|
||||
This defeats the purpose of mocking by loading the real module and all its dependencies.
|
||||
|
||||
```typescript
|
||||
// BAD — loads real module + all transitive deps
|
||||
vi.mock('@/lib/workspaces/utils', async () => {
|
||||
const actual = await vi.importActual('@/lib/workspaces/utils')
|
||||
return { ...actual, myFn: vi.fn() }
|
||||
})
|
||||
|
||||
// GOOD — mock everything, only implement what tests need
|
||||
vi.mock('@/lib/workspaces/utils', () => ({
|
||||
myFn: vi.fn(),
|
||||
otherFn: vi.fn(),
|
||||
}))
|
||||
```
|
||||
|
||||
### NEVER use `mockAuth()`, `mockConsoleLogger()`, or `setupCommonApiMocks()` from `@sim/testing`
|
||||
|
||||
These helpers internally use `vi.doMock()` which is slow. Use direct `vi.hoisted()` + `vi.mock()` instead.
|
||||
|
||||
### Mock heavy transitive dependencies
|
||||
|
||||
If a module under test imports `@/blocks` (200+ files), `@/tools/registry`, or other heavy modules, mock them:
|
||||
|
||||
```typescript
|
||||
vi.mock('@/blocks', () => ({
|
||||
getBlock: () => null,
|
||||
getAllBlocks: () => ({}),
|
||||
getAllBlockTypes: () => [],
|
||||
registry: {},
|
||||
}))
|
||||
```
|
||||
|
||||
### Use `@vitest-environment node` unless DOM is needed
|
||||
|
||||
Only use `@vitest-environment jsdom` if the test uses `window`, `document`, `FormData`, or other browser APIs. Node environment is significantly faster.
|
||||
|
||||
### Avoid real timers in tests
|
||||
|
||||
```typescript
|
||||
// BAD
|
||||
await new Promise(r => setTimeout(r, 500))
|
||||
|
||||
// GOOD — use minimal delays or fake timers
|
||||
await new Promise(r => setTimeout(r, 1))
|
||||
// or
|
||||
vi.useFakeTimers()
|
||||
```
|
||||
|
||||
## Mock Pattern Reference
|
||||
|
||||
### Auth mocking (API routes)
|
||||
|
||||
```typescript
|
||||
const { mockGetSession } = vi.hoisted(() => ({
|
||||
mockGetSession: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/auth', () => ({
|
||||
auth: { api: { getSession: vi.fn() } },
|
||||
getSession: mockGetSession,
|
||||
}))
|
||||
|
||||
// In tests:
|
||||
mockGetSession.mockResolvedValue({ user: { id: 'user-1', email: 'test@example.com' } })
|
||||
mockGetSession.mockResolvedValue(null) // unauthenticated
|
||||
```
|
||||
|
||||
### Hybrid auth mocking
|
||||
|
||||
```typescript
|
||||
const { mockCheckSessionOrInternalAuth } = vi.hoisted(() => ({
|
||||
mockCheckSessionOrInternalAuth: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/auth/hybrid', () => ({
|
||||
checkSessionOrInternalAuth: mockCheckSessionOrInternalAuth,
|
||||
}))
|
||||
|
||||
// In tests:
|
||||
mockCheckSessionOrInternalAuth.mockResolvedValue({
|
||||
success: true, userId: 'user-1', authType: 'session',
|
||||
})
|
||||
```
|
||||
|
||||
### Database chain mocking
|
||||
|
||||
```typescript
|
||||
const { mockSelect, mockFrom, mockWhere } = vi.hoisted(() => ({
|
||||
mockSelect: vi.fn(),
|
||||
mockFrom: vi.fn(),
|
||||
mockWhere: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@sim/db', () => ({
|
||||
db: { select: mockSelect },
|
||||
}))
|
||||
|
||||
beforeEach(() => {
|
||||
mockSelect.mockReturnValue({ from: mockFrom })
|
||||
mockFrom.mockReturnValue({ where: mockWhere })
|
||||
mockWhere.mockResolvedValue([{ id: '1', name: 'test' }])
|
||||
})
|
||||
```
|
||||
|
||||
## @sim/testing Package
|
||||
|
||||
Always prefer over local mocks.
|
||||
Always prefer over local test data.
|
||||
|
||||
| Category | Utilities |
|
||||
|----------|-----------|
|
||||
| **Mocks** | `loggerMock`, `databaseMock`, `setupGlobalFetchMock()` |
|
||||
| **Factories** | `createSession()`, `createWorkflowRecord()`, `createBlock()`, `createExecutorContext()` |
|
||||
| **Mocks** | `loggerMock`, `databaseMock`, `drizzleOrmMock`, `setupGlobalFetchMock()` |
|
||||
| **Factories** | `createSession()`, `createWorkflowRecord()`, `createBlock()`, `createExecutionContext()` |
|
||||
| **Builders** | `WorkflowBuilder`, `ExecutionContextBuilder` |
|
||||
| **Assertions** | `expectWorkflowAccessGranted()`, `expectBlockExecuted()` |
|
||||
| **Requests** | `createMockRequest()`, `createEnvMock()` |
|
||||
|
||||
## Rules
|
||||
## Rules Summary
|
||||
|
||||
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. `@vitest-environment node` unless DOM is required
|
||||
2. `vi.hoisted()` + `vi.mock()` + static imports — never `vi.resetModules()` + `vi.doMock()` + dynamic imports
|
||||
3. `vi.mock()` calls before importing mocked modules
|
||||
4. `@sim/testing` utilities over local mocks
|
||||
5. `beforeEach(() => vi.clearAllMocks())` to reset state — no redundant `afterEach`
|
||||
6. No `vi.importActual()` — mock everything explicitly
|
||||
7. No `mockAuth()`, `mockConsoleLogger()`, `setupCommonApiMocks()` — use direct mocks
|
||||
8. Mock heavy deps (`@/blocks`, `@/tools/registry`, `@/triggers`) in tests that don't need them
|
||||
9. Use absolute imports in test files
|
||||
10. Avoid real timers — use 1ms delays or `vi.useFakeTimers()`
|
||||
|
||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -8,7 +8,7 @@ on:
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
18
.github/workflows/test-build.yml
vendored
18
.github/workflows/test-build.yml
vendored
@@ -10,7 +10,7 @@ permissions:
|
||||
jobs:
|
||||
test-build:
|
||||
name: Test and Build
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@@ -38,6 +38,20 @@ jobs:
|
||||
key: ${{ github.repository }}-node-modules
|
||||
path: ./node_modules
|
||||
|
||||
- name: Mount Turbo cache (Sticky Disk)
|
||||
uses: useblacksmith/stickydisk@v1
|
||||
with:
|
||||
key: ${{ github.repository }}-turbo-cache
|
||||
path: ./.turbo
|
||||
|
||||
- name: Restore Next.js build cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ./apps/sim/.next/cache
|
||||
key: ${{ runner.os }}-nextjs-${{ hashFiles('bun.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-nextjs-
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
@@ -85,6 +99,7 @@ jobs:
|
||||
NEXT_PUBLIC_APP_URL: 'https://www.sim.ai'
|
||||
DATABASE_URL: 'postgresql://postgres:postgres@localhost:5432/simstudio'
|
||||
ENCRYPTION_KEY: '7cf672e460e430c1fba707575c2b0e2ad5a99dddf9b7b7e3b5646e630861db1c' # dummy key for CI only
|
||||
TURBO_CACHE_DIR: .turbo
|
||||
run: bun run test
|
||||
|
||||
- name: Check schema and migrations are in sync
|
||||
@@ -110,6 +125,7 @@ jobs:
|
||||
RESEND_API_KEY: 'dummy_key_for_ci_only'
|
||||
AWS_REGION: 'us-west-2'
|
||||
ENCRYPTION_KEY: '7cf672e460e430c1fba707575c2b0e2ad5a99dddf9b7b7e3b5646e630861db1c' # dummy key for CI only
|
||||
TURBO_CACHE_DIR: .turbo
|
||||
run: bunx turbo run build --filter=sim
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
|
||||
44
CLAUDE.md
44
CLAUDE.md
@@ -167,27 +167,51 @@ Import from `@/components/emcn`, never from subpaths (except CSS files). Use CVA
|
||||
|
||||
## Testing
|
||||
|
||||
Use Vitest. Test files: `feature.ts` → `feature.test.ts`
|
||||
Use Vitest. Test files: `feature.ts` → `feature.test.ts`. See `.cursor/rules/sim-testing.mdc` for full details.
|
||||
|
||||
### Global Mocks (vitest.setup.ts)
|
||||
|
||||
`@sim/db`, `drizzle-orm`, `@sim/logger`, `@/blocks/registry`, `@trigger.dev/sdk`, and store mocks are provided globally. Do NOT re-mock them unless overriding behavior.
|
||||
|
||||
### Standard Test Pattern
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { databaseMock, loggerMock } from '@sim/testing'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { createMockRequest } from '@sim/testing'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('@sim/db', () => databaseMock)
|
||||
vi.mock('@sim/logger', () => loggerMock)
|
||||
const { mockGetSession } = vi.hoisted(() => ({
|
||||
mockGetSession: vi.fn(),
|
||||
}))
|
||||
|
||||
import { myFunction } from '@/lib/feature'
|
||||
vi.mock('@/lib/auth', () => ({
|
||||
auth: { api: { getSession: vi.fn() } },
|
||||
getSession: mockGetSession,
|
||||
}))
|
||||
|
||||
describe('feature', () => {
|
||||
beforeEach(() => vi.clearAllMocks())
|
||||
it.concurrent('runs in parallel', () => { ... })
|
||||
import { GET } from '@/app/api/my-route/route'
|
||||
|
||||
describe('my route', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockGetSession.mockResolvedValue({ user: { id: 'user-1' } })
|
||||
})
|
||||
it('returns data', async () => { ... })
|
||||
})
|
||||
```
|
||||
|
||||
Use `@sim/testing` mocks/factories over local test data. See `.cursor/rules/sim-testing.mdc` for details.
|
||||
### Performance Rules
|
||||
|
||||
- **NEVER** use `vi.resetModules()` + `vi.doMock()` + `await import()` — use `vi.hoisted()` + `vi.mock()` + static imports
|
||||
- **NEVER** use `vi.importActual()` — mock everything explicitly
|
||||
- **NEVER** use `mockAuth()`, `mockConsoleLogger()`, `setupCommonApiMocks()` from `@sim/testing` — they use `vi.doMock()` internally
|
||||
- **Mock heavy deps** (`@/blocks`, `@/tools/registry`, `@/triggers`) in tests that don't need them
|
||||
- **Use `@vitest-environment node`** unless DOM APIs are needed (`window`, `document`, `FormData`)
|
||||
- **Avoid real timers** — use 1ms delays or `vi.useFakeTimers()`
|
||||
|
||||
Use `@sim/testing` mocks/factories over local test data.
|
||||
|
||||
## Utils Rules
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">The open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to orchestrate agentic workflows.</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>
|
||||
|
||||
@@ -264,17 +264,15 @@ export async function generateMetadata(props: {
|
||||
return {
|
||||
title: data.title,
|
||||
description:
|
||||
data.description ||
|
||||
'Documentation for Sim — the open-source platform to build AI agents and run your agentic workforce.',
|
||||
data.description || 'Sim visual workflow builder for AI applications documentation',
|
||||
keywords: [
|
||||
'AI agents',
|
||||
'agentic workforce',
|
||||
'AI agent platform',
|
||||
'agentic workflows',
|
||||
'LLM orchestration',
|
||||
'AI workflow builder',
|
||||
'visual workflow editor',
|
||||
'AI automation',
|
||||
'knowledge base',
|
||||
'AI integrations',
|
||||
'workflow automation',
|
||||
'AI agents',
|
||||
'no-code AI',
|
||||
'drag and drop workflows',
|
||||
data.title?.toLowerCase().split(' '),
|
||||
]
|
||||
.flat()
|
||||
@@ -284,8 +282,7 @@ export async function generateMetadata(props: {
|
||||
openGraph: {
|
||||
title: data.title,
|
||||
description:
|
||||
data.description ||
|
||||
'Documentation for Sim — the open-source platform to build AI agents and run your agentic workforce.',
|
||||
data.description || 'Sim visual workflow builder for AI applications documentation',
|
||||
url: fullUrl,
|
||||
siteName: 'Sim Documentation',
|
||||
type: 'article',
|
||||
@@ -306,8 +303,7 @@ export async function generateMetadata(props: {
|
||||
card: 'summary_large_image',
|
||||
title: data.title,
|
||||
description:
|
||||
data.description ||
|
||||
'Documentation for Sim — the open-source platform to build AI agents and run your agentic workforce.',
|
||||
data.description || 'Sim visual workflow builder for AI applications documentation',
|
||||
images: [ogImageUrl],
|
||||
creator: '@simdotai',
|
||||
site: '@simdotai',
|
||||
|
||||
@@ -63,7 +63,7 @@ export default async function Layout({ children, params }: LayoutProps) {
|
||||
'@type': 'WebSite',
|
||||
name: 'Sim Documentation',
|
||||
description:
|
||||
'Documentation for Sim — the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows.',
|
||||
'Comprehensive documentation for Sim - the visual workflow builder for AI Agent Workflows.',
|
||||
url: 'https://docs.sim.ai',
|
||||
publisher: {
|
||||
'@type': 'Organization',
|
||||
@@ -99,19 +99,6 @@ export default async function Layout({ children, params }: LayoutProps) {
|
||||
</head>
|
||||
<body className='flex min-h-screen flex-col font-sans'>
|
||||
<Script src='https://assets.onedollarstats.com/stonks.js' strategy='lazyOnload' />
|
||||
{process.env.REACT_GRAB_ENABLED === 'TRUE' && (
|
||||
<Script
|
||||
src='https://unpkg.com/react-grab/dist/index.global.js'
|
||||
crossOrigin='anonymous'
|
||||
strategy='beforeInteractive'
|
||||
/>
|
||||
)}
|
||||
{process.env.REACT_GRAB_ENABLED === 'TRUE' && (
|
||||
<Script
|
||||
src='https://unpkg.com/@react-grab/cursor/dist/client.global.js'
|
||||
strategy='lazyOnload'
|
||||
/>
|
||||
)}
|
||||
<RootProvider i18n={provider(lang)}>
|
||||
<Navbar />
|
||||
<DocsLayout
|
||||
|
||||
@@ -21,13 +21,6 @@ body {
|
||||
|
||||
.dark {
|
||||
--color-fd-primary: #33c482;
|
||||
--color-fd-background: #1c1c1c;
|
||||
--color-fd-card: #1b1b1b;
|
||||
--color-fd-muted: #1b1b1b;
|
||||
--color-fd-secondary: #1b1b1b;
|
||||
--color-fd-popover: #1b1b1b;
|
||||
--color-fd-border: #2a2a2a;
|
||||
--color-fd-accent: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
/* Font family utilities */
|
||||
@@ -84,9 +77,9 @@ body {
|
||||
|
||||
/* Dark mode navbar and search styling */
|
||||
:root.dark nav {
|
||||
background-color: rgba(28, 28, 28, 0.92) !important;
|
||||
backdrop-filter: blur(25px) saturate(180%) !important;
|
||||
-webkit-backdrop-filter: blur(25px) saturate(180%) !important;
|
||||
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;
|
||||
}
|
||||
|
||||
/* Floating sidebar appearance - remove background */
|
||||
@@ -490,9 +483,9 @@ pre code {
|
||||
|
||||
/* Dark mode inline code */
|
||||
.dark :not(pre) > code {
|
||||
background-color: #1b1b1b;
|
||||
background-color: rgb(31 41 55);
|
||||
color: rgb(248 113 113);
|
||||
border: 1px solid #2a2a2a;
|
||||
border: 1px solid rgb(55 65 81);
|
||||
}
|
||||
|
||||
/* Code block container improvements */
|
||||
|
||||
@@ -7,27 +7,26 @@ export default function RootLayout({ children }: { children: ReactNode }) {
|
||||
export const metadata = {
|
||||
metadataBase: new URL('https://docs.sim.ai'),
|
||||
title: {
|
||||
default: 'Sim Documentation — Build AI Agents & Run Your Agentic Workforce',
|
||||
default: 'Sim Documentation - Visual Workflow Builder for AI Applications',
|
||||
template: '%s',
|
||||
},
|
||||
description:
|
||||
'Documentation for Sim — the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows.',
|
||||
'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 agents',
|
||||
'agentic workforce',
|
||||
'AI agent platform',
|
||||
'open-source AI agents',
|
||||
'agentic workflows',
|
||||
'LLM orchestration',
|
||||
'AI integrations',
|
||||
'knowledge base',
|
||||
'AI workflow builder',
|
||||
'visual workflow editor',
|
||||
'AI automation',
|
||||
'workflow builder',
|
||||
'AI workflow orchestration',
|
||||
'enterprise AI',
|
||||
'AI agent deployment',
|
||||
'intelligent automation',
|
||||
'AI tools',
|
||||
'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',
|
||||
@@ -54,9 +53,9 @@ export const metadata = {
|
||||
alternateLocale: ['es_ES', 'fr_FR', 'de_DE', 'ja_JP', 'zh_CN'],
|
||||
url: 'https://docs.sim.ai',
|
||||
siteName: 'Sim Documentation',
|
||||
title: 'Sim Documentation — Build AI Agents & Run Your Agentic Workforce',
|
||||
title: 'Sim Documentation - Visual Workflow Builder for AI Applications',
|
||||
description:
|
||||
'Documentation for Sim — the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows.',
|
||||
'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',
|
||||
@@ -68,9 +67,9 @@ export const metadata = {
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: 'Sim Documentation — Build AI Agents & Run Your Agentic Workforce',
|
||||
title: 'Sim Documentation - Visual Workflow Builder for AI Applications',
|
||||
description:
|
||||
'Documentation for Sim — the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows.',
|
||||
'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'],
|
||||
|
||||
@@ -37,9 +37,9 @@ export async function GET() {
|
||||
|
||||
const manifest = `# Sim Documentation
|
||||
|
||||
> The open-source platform to build AI agents and run your agentic workforce.
|
||||
> Visual Workflow Builder for AI Applications
|
||||
|
||||
Sim is the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows. Create agents, workflows, knowledge bases, tables, and docs. Trusted by over 100,000 builders.
|
||||
Sim is 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
|
||||
|
||||
|
||||
@@ -13,9 +13,9 @@ export function TOCFooter() {
|
||||
<div className='text-balance font-semibold text-base leading-tight'>
|
||||
Start building today
|
||||
</div>
|
||||
<div className='text-muted-foreground'>Trusted by over 100,000 builders.</div>
|
||||
<div className='text-muted-foreground'>Trusted by over 70,000 builders.</div>
|
||||
<div className='text-muted-foreground'>
|
||||
The open-source platform to build AI agents and run your agentic workforce.
|
||||
Build Agentic workflows visually on a drag-and-drop canvas or with natural language.
|
||||
</div>
|
||||
<Link
|
||||
href='https://sim.ai/signup'
|
||||
|
||||
@@ -526,6 +526,17 @@ export function SlackMonoIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function GammaIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='-14 0 192 192' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
fill='currentColor'
|
||||
d='M47.2,14.4c-14.4,8.2-26,19.6-34.4,33.6C4.3,62.1,0,77.7,0,94.3s4.3,32.2,12.7,46.3c8.5,14.1,20,25.4,34.4,33.6,14.4,8.2,30.4,12.4,47.7,12.4h69.8v-112.5h-81v39.1h38.2v31.8h-25.6c-9.1,0-17.6-2.3-25.2-6.9-7.6-4.6-13.8-10.8-18.3-18.4-4.5-7.7-6.7-16.2-6.7-25.3s2.3-17.7,6.7-25.3c4.5-7.7,10.6-13.9,18.3-18.4,7.6-4.6,16.1-6.9,25.2-6.9h68.5V2h-69.8c-17.3,0-33.3,4.2-47.7,12.4h0Z'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function GithubIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} width='26' height='26' viewBox='0 0 26 26' xmlns='http://www.w3.org/2000/svg'>
|
||||
@@ -939,6 +950,25 @@ export function GoogleIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function DevinIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 500 500' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
d='M59.29,209.39l48.87,28.21c1.75,1.01,3.71,1.51,5.67,1.51c1.95,0,3.92-0.52,5.67-1.51l48.87-28.21c0,0,0.14-0.11,0.2-0.16c0.74-0.45,1.44-0.99,2.07-1.6c0.09-0.09,0.18-0.2,0.27-0.29c0.54-0.58,1.03-1.21,1.44-1.89c0.06-0.11,0.16-0.2,0.2-0.32c0.43-0.74,0.74-1.53,0.99-2.37c0.05-0.18,0.09-0.36,0.14-0.54c0.2-0.86,0.36-1.74,0.36-2.66v-28.21c0-10.89,5.87-21.03,15.3-26.48c9.42-5.45,21.15-5.44,30.59,0l24.43,14.11c0.79,0.45,1.62,0.77,2.47,1.01c0.18,0.05,0.37,0.11,0.54,0.16c0.83,0.2,1.69,0.32,2.54,0.34c0.05,0,0.09,0,0.11,0c0.09,0,0.18-0.05,0.26-0.05c0.79,0,1.58-0.11,2.34-0.32c0.14-0.03,0.27-0.05,0.4-0.09c0.83-0.23,1.64-0.57,2.41-0.99c0.06-0.05,0.16-0.05,0.23-0.09l48.87-28.21c3.51-2.03,5.67-5.76,5.67-9.81V64.52c0-4.05-2.16-7.78-5.67-9.81l-48.91-28.19c-3.51-2.03-7.81-2.03-11.32,0l-48.87,28.21c0,0-0.14,0.11-0.2,0.16c-0.74,0.45-1.44,0.99-2.07,1.6c-0.09,0.09-0.18,0.2-0.27,0.29c-0.54,0.58-1.03,1.21-1.44,1.89c-0.06,0.11-0.16,0.2-0.2,0.31c-0.43,0.74-0.74,1.53-0.99,2.37c-0.05,0.18-0.09,0.36-0.14,0.54c-0.2,0.86-0.36,1.74-0.36,2.66v28.21c0,10.89-5.87,21.03-15.3,26.5c-9.42,5.44-21.15,5.44-30.59,0l-24.42-14.1c-0.79-0.45-1.63-0.77-2.47-1.01c-0.18-0.05-0.36-0.11-0.54-0.16c-0.84-0.2-1.69-0.31-2.55-0.34c-0.14,0-0.25,0-0.38,0c-0.81,0-1.6,0.11-2.37,0.31c-0.14,0.02-0.25,0.05-0.38,0.09c-0.82,0.23-1.63,0.57-2.4,1c-0.06,0.05-0.16,0.05-0.23,0.09l-48.84,28.24c-3.51,2.03-5.67,5.76-5.67,9.81v56.42c0,4.05,2.16,7.78,5.67,9.81C59.29,209.41,59.29,209.39,59.29,209.39z'
|
||||
fill='#2A6DCE'
|
||||
/>
|
||||
<path
|
||||
d='M325.46,223.49c9.42-5.44,21.15-5.44,30.59,0l24.43,14.11c0.79,0.45,1.62,0.77,2.47,1.01c0.18,0.05,0.36,0.11,0.54,0.16c0.83,0.2,1.69,0.31,2.54,0.34c0.05,0,0.09,0,0.11,0c0.09,0,0.18-0.03,0.26-0.05c0.79,0,1.58-0.11,2.34-0.31c0.14-0.03,0.27-0.05,0.4-0.09c0.83-0.23,1.62-0.57,2.41-0.99c0.06-0.05,0.16-0.05,0.25-0.09l48.87-28.21c3.51-2.03,5.67-5.76,5.67-9.81v-56.43c0-4.05-2.16-7.78-5.67-9.81l-48.84-28.22c-3.51-2.03-7.81-2.03-11.32,0l-48.87,28.21c0,0-0.14,0.11-0.2,0.16c-0.74,0.45-1.44,0.99-2.07,1.6c-0.09,0.09-0.18,0.2-0.26,0.29c-0.54,0.58-1.03,1.21-1.44,1.89c-0.06,0.11-0.16,0.2-0.2,0.32c-0.43,0.74-0.74,1.53-0.99,2.37c-0.05,0.18-0.09,0.36-0.14,0.54c-0.2,0.86-0.36,1.74-0.36,2.66v28.21c0,10.89-5.87,21.03-15.3,26.5c-9.42,5.44-21.15,5.44-30.59,0l-24.43-14.11c-0.79-0.45-1.62-0.77-2.47-1.01c-0.18-0.05-0.36-0.11-0.54-0.16c-0.83-0.2-1.69-0.32-2.54-0.34c-0.14,0-0.25,0-0.38,0c-0.81,0-1.6,0.11-2.37,0.32c-0.14,0.03-0.25,0.05-0.38,0.09c-0.83,0.23-1.64,0.57-2.41,0.99c-0.06,0.05-0.16,0.05-0.23,0.09l-48.87,28.21c-3.51,2.03-5.67,5.76-5.67,9.81v56.43c0,4.05,2.16,7.78,5.67,9.81l48.87,28.21c0,0,0.16,0.05,0.23,0.09c0.77,0.43,1.58,0.77,2.41,0.99c0.14,0.05,0.27,0.05,0.4,0.09c0.77,0.18,1.55,0.29,2.34,0.32c0.09,0,0.18,0.05,0.27,0.05c0.05,0,0.09,0,0.11,0c0.86,0,1.69-0.14,2.54-0.34c0.18-0.05,0.36-0.09,0.54-0.16c0.86-0.25,1.69-0.57,2.47-1.01l24.43-14.11c9.42-5.44,21.15-5.44,30.59,0c9.42,5.44,15.3,15.59,15.3,26.48v28.21c0,0.92,0.14,1.8,0.36,2.66c0.05,0.18,0.09,0.36,0.14,0.54c0.25,0.83,0.56,1.62,0.99,2.37c0.06,0.11,0.14,0.2,0.2,0.31c0.4,0.68,0.9,1.31,1.44,1.89c0.09,0.09,0.18,0.2,0.26,0.29c0.61,0.6,1.31,1.12,2.07,1.6c0.06,0.05,0.11,0.11,0.2,0.16l48.87,28.21c1.75,1.01,3.72,1.51,5.67,1.51s3.92-0.52,5.67-1.51l48.87-28.21c3.51-2.03,5.67-5.76,5.67-9.81v-56.43c0-4.05-2.16-7.78-5.67-9.81l-48.87-28.21c0,0-0.16-0.05-0.23-0.09c-0.77-0.43-1.58-0.77-2.41-0.99c-0.14-0.05-0.25-0.05-0.38-0.09c-0.79-0.18-1.57-0.29-2.38-0.32c-0.11,0-0.25,0-0.36,0c-0.86,0-1.71,0.14-2.54,0.34c-0.18,0.05-0.34,0.09-0.52,0.16c-0.86,0.25-1.69,0.57-2.47,1.01l-24.43,14.11c-9.42,5.44-21.15,5.44-30.58,0c-9.42-5.44-15.3-15.59-15.3-26.5c0-10.91,5.87-21.03,15.3-26.48C325.55,223.49,325.46,223.49,325.46,223.49z'
|
||||
fill='#1DC19C'
|
||||
/>
|
||||
<path
|
||||
d='M304.5,369.22l-48.87-28.21c0,0-0.16-0.05-0.23-0.09c-0.77-0.43-1.57-0.77-2.41-0.99c-0.14-0.05-0.27-0.05-0.4-0.09c-0.79-0.18-1.57-0.29-2.37-0.32c-0.14,0-0.25,0-0.38,0c-0.86,0-1.71,0.14-2.54,0.34c-0.18,0.05-0.34,0.09-0.52,0.16c-0.86,0.25-1.69,0.57-2.47,1.01l-24.43,14.11c-9.42,5.44-21.15,5.44-30.58,0c-9.42-5.44-15.3-15.59-15.3-26.5v-28.22c0-0.92-0.14-1.8-0.36-2.66c-0.05-0.18-0.09-0.36-0.14-0.54c-0.25-0.83-0.57-1.62-0.99-2.37c-0.06-0.11-0.14-0.2-0.2-0.32c-0.4-0.68-0.9-1.31-1.44-1.89c-0.09-0.09-0.18-0.2-0.27-0.29c-0.6-0.6-1.31-1.12-2.07-1.6c-0.06-0.05-0.11-0.11-0.2-0.16l-48.87-28.21c-3.51-2.03-7.81-2.03-11.32,0L59.28,290.6c-3.51,2.03-5.67,5.76-5.67,9.81v56.43c0,4.05,2.16,7.78,5.67,9.81l48.87,28.21c0,0,0.16,0.06,0.23,0.09c0.77,0.43,1.55,0.77,2.38,0.99c0.14,0.05,0.27,0.06,0.4,0.09c0.77,0.18,1.55,0.29,2.34,0.32c0.09,0,0.18,0.05,0.29,0.05c0.05,0,0.09,0,0.14,0c0.86,0,1.69-0.14,2.52-0.34c0.18-0.05,0.36-0.09,0.54-0.16c0.86-0.25,1.69-0.57,2.47-1.01l24.43-14.11c9.42-5.44,21.15-5.44,30.59,0c9.42,5.44,15.3,15.59,15.3,26.48v28.21c0,0.92,0.14,1.8,0.36,2.66c0.05,0.18,0.09,0.36,0.14,0.54c0.25,0.83,0.57,1.62,0.99,2.37c0.06,0.11,0.14,0.2,0.2,0.32c0.4,0.68,0.9,1.31,1.44,1.89c0.09,0.09,0.18,0.2,0.27,0.29c0.61,0.61,1.31,1.12,2.07,1.6c0.06,0.05,0.11,0.11,0.2,0.16l48.87,28.21c1.75,1.01,3.71,1.51,5.67,1.51c1.96,0,3.92-0.52,5.67-1.51l48.87-28.21c3.51-2.03,5.67-5.76,5.67-9.81v-56.43c0-4.05-2.16-7.78-5.67-9.81L304.5,369.22z'
|
||||
fill='#1796E2'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function DiscordIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
@@ -1235,6 +1265,20 @@ export function GoogleSlidesIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function GoogleContactsIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 500 500'>
|
||||
<path fill='#86a9ff' d='M199 244c-89 0-161 71-161 160v67c0 16 13 29 29 29h77l77-256z' />
|
||||
<path fill='#578cff' d='M462 349c0-58-48-105-106-105h-77v256h77c58 0 106-47 106-106' />
|
||||
<path
|
||||
fill='#0057cc'
|
||||
d='M115 349c0-58 48-105 106-105h58c58 0 106 47 106 105v45c0 59-48 106-106 106H144c-16 0-29-13-29-29z'
|
||||
/>
|
||||
<circle cx='250' cy='99.4' r='99.4' fill='#0057cc' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function GoogleCalendarIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
@@ -1302,6 +1346,21 @@ export function GoogleCalendarIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function GoogleTasksIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 527.1 500' xmlns='http://www.w3.org/2000/svg'>
|
||||
<polygon
|
||||
fill='#0066DA'
|
||||
points='410.4,58.3 368.8,81.2 348.2,120.6 368.8,168.8 407.8,211 450,187.5 475.9,142.8 450,87.5'
|
||||
/>
|
||||
<path
|
||||
fill='#2684FC'
|
||||
d='M249.3,219.4l98.9-98.9c29.1,22.1,50.5,53.8,59.6,90.4L272.1,346.7c-12.2,12.2-32,12.2-44.2,0l-91.5-91.5 c-9.8-9.8-9.8-25.6,0-35.3l39-39c9.8-9.8,25.6-9.8,35.3,0L249.3,219.4z M519.8,63.6l-39.7-39.7c-9.7-9.7-25.6-9.7-35.3,0 l-34.4,34.4c27.5,23,49.9,51.8,65.5,84.5l43.9-43.9C529.6,89.2,529.6,73.3,519.8,63.6z M412.5,250c0,89.8-72.8,162.5-162.5,162.5 S87.5,339.8,87.5,250S160.2,87.5,250,87.5c36.9,0,70.9,12.3,98.2,33.1l62.2-62.2C367,21.9,311.1,0,250,0C111.9,0,0,111.9,0,250 s111.9,250,250,250s250-111.9,250-250c0-38.3-8.7-74.7-24.1-107.2L407.8,211C410.8,223.5,412.5,236.6,412.5,250z'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function SupabaseIcon(props: SVGProps<SVGSVGElement>) {
|
||||
const id = useId()
|
||||
const gradient0 = `supabase_paint0_${id}`
|
||||
@@ -2928,6 +2987,19 @@ export function QdrantIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function AshbyIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 254 260' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
d='M76.07 250.537v9.16H.343v-9.16c19.618 0 27.465-4.381 34.527-23.498l73.764-209.09h34.92l81.219 209.09c7.847 19.515 11.77 23.498 28.642 23.498v9.16H134.363v-9.16c28.242 0 30.625-2.582 22.14-23.498l-21.58-57.35H69.399l-19.226 56.155c-5.614 18.997-4.387 24.693 25.896 24.693zm24.326-171.653l-26.681 78.459h56.5l-29.819-78.459z'
|
||||
fill='currentColor'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function ArxivIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} id='logomark' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 17.732 24.269'>
|
||||
@@ -3430,6 +3502,23 @@ export const ResendIcon = (props: SVGProps<SVGSVGElement>) => (
|
||||
</svg>
|
||||
)
|
||||
|
||||
export const GoogleBigQueryIcon = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'>
|
||||
<path
|
||||
d='M14.48 58.196L.558 34.082c-.744-1.288-.744-2.876 0-4.164L14.48 5.805c.743-1.287 2.115-2.08 3.6-2.082h27.857c1.48.007 2.845.8 3.585 2.082l13.92 24.113c.744 1.288.744 2.876 0 4.164L49.52 58.196c-.743 1.287-2.115 2.08-3.6 2.082H18.07c-1.483-.005-2.85-.798-3.593-2.082z'
|
||||
fill='#4386fa'
|
||||
/>
|
||||
<path
|
||||
d='M40.697 24.235s3.87 9.283-1.406 14.545-14.883 1.894-14.883 1.894L43.95 60.27h1.984c1.486-.002 2.858-.796 3.6-2.082L58.75 42.23z'
|
||||
opacity='.1'
|
||||
/>
|
||||
<path
|
||||
d='M45.267 43.23L41 38.953a.67.67 0 0 0-.158-.12 11.63 11.63 0 1 0-2.032 2.037.67.67 0 0 0 .113.15l4.277 4.277a.67.67 0 0 0 .947 0l1.12-1.12a.67.67 0 0 0 0-.947zM31.64 40.464a8.75 8.75 0 1 1 8.749-8.749 8.75 8.75 0 0 1-8.749 8.749zm-5.593-9.216v3.616c.557.983 1.363 1.803 2.338 2.375v-6.013zm4.375-2.998v9.772a6.45 6.45 0 0 0 2.338 0V28.25zm6.764 6.606v-2.142H34.85v4.5a6.43 6.43 0 0 0 2.338-2.368z'
|
||||
fill='#fff'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export const GoogleVaultIcon = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 82 82'>
|
||||
<path
|
||||
@@ -3905,6 +3994,28 @@ export function IntercomIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function LoopsIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 256 256' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
fill='#FD4E00'
|
||||
d='M192.352 88.042c0-7.012-5.685-12.697-12.697-12.697s-12.697 5.685-12.697 12.697c0 .634.052 1.255.142 1.866a25.248 25.248 0 0 0-4.9-.49c-14.006 0-25.36 11.354-25.36 25.36 0 1.63.16 3.222.456 4.765a37.8 37.8 0 0 0-9.296-1.173c-20.95 0-37.935 16.985-37.935 37.935S107.05 194.24 128 194.24s37.935-16.985 37.935-37.935a37.7 37.7 0 0 0-3.78-16.555 25.2 25.2 0 0 0 12.487-3.336 25.2 25.2 0 0 0 4.558 3.336v.02c14.006 0 25.36-11.354 25.36-25.36 0-12.48-9.018-22.855-20.888-24.996a12.6 12.6 0 0 0 8.68-11.972m-77.05 68.263c0-7.012 5.685-12.697 12.697-12.697s12.697 5.685 12.697 12.697c0 7.013-5.685 12.697-12.697 12.697s-12.697-5.685-12.697-12.697'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function LumaIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} fill='none' viewBox='0 0 133 134' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
d='M133 67C96.282 67 66.5 36.994 66.5 0c0 36.994-29.782 67-66.5 67 36.718 0 66.5 30.006 66.5 67 0-36.994 29.782-67 66.5-67'
|
||||
fill='#000000'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function MailchimpIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
@@ -4426,6 +4537,17 @@ export function SSHIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function DatabricksIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 241 266' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
d='M228.085 109.654L120.615 171.674L5.53493 105.41L0 108.475V156.582L120.615 225.911L228.085 164.128V189.596L120.615 251.615L5.53493 185.351L0 188.417V196.67L120.615 266L241 196.67V148.564L235.465 145.498L120.615 211.527L12.9148 149.743V124.275L120.615 186.059L241 116.729V69.3298L235.004 65.7925L120.615 131.585L18.4498 73.1028L120.615 14.3848L204.562 62.7269L211.942 58.4823V52.5869L120.615 0L0 69.3298V76.8759L120.615 146.206L228.085 84.1862V109.654Z'
|
||||
fill='#FF3621'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function DatadogIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'>
|
||||
@@ -4774,6 +4896,17 @@ export function CirclebackIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function GreenhouseIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 51.4 107.7' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
d='M44.9,32c0,5.2-2.2,9.8-5.8,13.4c-4,4-9.8,5-9.8,8.4c0,4.6,7.4,3.2,14.5,10.3c4.7,4.7,7.6,10.9,7.6,18.1c0,14.2-11.4,25.5-25.7,25.5S0,96.4,0,82.2C0,75,2.9,68.8,7.6,64.1c7.1-7.1,14.5-5.7,14.5-10.3c0-3.4-5.8-4.4-9.8-8.4c-3.6-3.6-5.8-8.2-5.8-13.6C6.5,21.4,15,13,25.4,13c2,0,3.8,0.3,5.3,0.3c2.7,0,4.1-1.2,4.1-3.1c0-1.1-0.5-2.5-0.5-4c0-3.4,2.9-6.2,6.4-6.2S47,2.9,47,6.4c0,3.7-2.9,5.4-5.1,6.2c-1.8,0.6-3.2,1.4-3.2,3.2C38.7,19.2,44.9,22.5,44.9,32z M42.9,82.2c0-9.9-7.3-17.9-17.2-17.9s-17.2,8-17.2,17.9c0,9.8,7.3,17.9,17.2,17.9S42.9,92,42.9,82.2z M37,31.8c0-6.3-5.1-11.5-11.3-11.5s-11.3,5.2-11.3,11.5s5.1,11.5,11.3,11.5S37,38.1,37,31.8z'
|
||||
fill='currentColor'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function GreptileIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'>
|
||||
|
||||
@@ -74,7 +74,7 @@ export function StructuredData({
|
||||
name: 'Sim Documentation',
|
||||
url: baseUrl,
|
||||
description:
|
||||
'Documentation for Sim — the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows.',
|
||||
'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',
|
||||
@@ -104,7 +104,7 @@ export function StructuredData({
|
||||
applicationCategory: 'DeveloperApplication',
|
||||
operatingSystem: 'Any',
|
||||
description:
|
||||
'Sim is the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows. Create agents, workflows, knowledge bases, tables, and docs.',
|
||||
'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',
|
||||
@@ -115,13 +115,12 @@ export function StructuredData({
|
||||
category: 'Developer Tools',
|
||||
},
|
||||
featureList: [
|
||||
'AI agent creation',
|
||||
'Agentic workflow orchestration',
|
||||
'1,000+ integrations',
|
||||
'LLM orchestration (OpenAI, Anthropic, Google, xAI, Mistral, Perplexity)',
|
||||
'Knowledge base creation',
|
||||
'Table creation',
|
||||
'Document creation',
|
||||
'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',
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -1,336 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import Image from 'next/image'
|
||||
import { useTheme } from 'next-themes'
|
||||
|
||||
/**
|
||||
* Static block pattern SVG rects matching the hero page's color palette.
|
||||
* These are arranged in a horizontal strip, similar to BlocksTopRightAnimated.
|
||||
*/
|
||||
const BLOCK_COLORS = ['#2ABBF8', '#00F701', '#FFCC02', '#FA4EDF'] as const
|
||||
const RX = '2.59574'
|
||||
|
||||
/** Decorative background for the docs site (dark mode only).
|
||||
* Renders card-left.svg, union-right.svg, and static block patterns. */
|
||||
export function DocsBackground() {
|
||||
const { resolvedTheme } = useTheme()
|
||||
const [mounted, setMounted] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
if (!mounted || resolvedTheme !== 'dark') return null
|
||||
|
||||
return (
|
||||
<div aria-hidden='true' className='pointer-events-none fixed inset-0 z-0 overflow-hidden'>
|
||||
{/* Card-left SVG — top left */}
|
||||
<div className='absolute top-[-0.7vw] left-[-2.8vw] aspect-[344/328] w-[23.9vw] opacity-40'>
|
||||
<Image src='/landing/card-left.svg' alt='' fill className='object-contain' />
|
||||
</div>
|
||||
|
||||
{/* Card-right SVG — top right */}
|
||||
<div className='absolute top-[-2.8vw] right-[0vw] aspect-[471/470] w-[32.7vw] opacity-40'>
|
||||
<Image src='/landing/card-right.svg' alt='' fill className='object-contain' />
|
||||
</div>
|
||||
|
||||
{/* Union-right SVG — bottom right */}
|
||||
<div className='absolute right-[-20%] bottom-[-10%] w-[75%] rotate-90 opacity-60'>
|
||||
<Image
|
||||
src='/landing/union-right.svg'
|
||||
alt=''
|
||||
width={768}
|
||||
height={768}
|
||||
className='h-auto w-full'
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Static block strip — top right area */}
|
||||
<div className='absolute top-[10px] right-[13vw] w-[calc(140px_+_10.76vw)] max-w-[295px] opacity-60'>
|
||||
<svg
|
||||
width={295}
|
||||
height={34}
|
||||
viewBox='0 0 295 34'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
className='h-auto w-full'
|
||||
>
|
||||
<rect opacity='0.6' width='85.3433' height='16.8626' rx={RX} fill='#2ABBF8' />
|
||||
<rect opacity='1' width='16.8626' height='16.8626' rx={RX} fill='#2ABBF8' />
|
||||
<rect opacity='0.6' x='34.2403' width='34.2403' height='33.7252' rx={RX} fill='#2ABBF8' />
|
||||
<rect opacity='1' x='34.2403' width='16.8626' height='16.8626' rx={RX} fill='#2ABBF8' />
|
||||
<rect
|
||||
opacity='1'
|
||||
x='51.6188'
|
||||
y='16.8626'
|
||||
width='16.8626'
|
||||
height='16.8626'
|
||||
rx={RX}
|
||||
fill='#2ABBF8'
|
||||
/>
|
||||
<rect opacity='1' x='68.4812' width='54.6502' height='16.8626' rx={RX} fill='#00F701' />
|
||||
<rect opacity='0.6' x='106.268' width='34.2403' height='33.7252' rx={RX} fill='#00F701' />
|
||||
<rect opacity='0.6' x='106.268' width='51.103' height='16.8626' rx={RX} fill='#00F701' />
|
||||
<rect
|
||||
opacity='1'
|
||||
x='123.6484'
|
||||
y='16.8626'
|
||||
width='16.8626'
|
||||
height='16.8626'
|
||||
rx={RX}
|
||||
fill='#00F701'
|
||||
/>
|
||||
<rect opacity='0.6' x='157.371' width='34.2403' height='16.8626' rx={RX} fill='#FFCC02' />
|
||||
<rect opacity='1' x='157.371' width='16.8626' height='16.8626' rx={RX} fill='#FFCC02' />
|
||||
<rect opacity='0.6' x='208.993' width='68.4805' height='16.8626' rx={RX} fill='#FA4EDF' />
|
||||
<rect opacity='0.6' x='209.137' width='16.8626' height='33.7252' rx={RX} fill='#FA4EDF' />
|
||||
<rect opacity='0.6' x='243.233' width='34.2403' height='33.7252' rx={RX} fill='#FA4EDF' />
|
||||
<rect opacity='1' x='243.233' width='16.8626' height='16.8626' rx={RX} fill='#FA4EDF' />
|
||||
<rect opacity='0.6' x='260.096' width='34.04' height='16.8626' rx={RX} fill='#FA4EDF' />
|
||||
<rect
|
||||
opacity='1'
|
||||
x='260.611'
|
||||
y='16.8626'
|
||||
width='16.8626'
|
||||
height='16.8626'
|
||||
rx={RX}
|
||||
fill='#FA4EDF'
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Static block strip — top left area */}
|
||||
<div className='absolute top-[10px] left-[16vw] w-[calc(140px_+_10.76vw)] max-w-[295px] opacity-60'>
|
||||
<svg
|
||||
width={295}
|
||||
height={34}
|
||||
viewBox='0 0 295 34'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
className='h-auto w-full'
|
||||
>
|
||||
<rect opacity='0.6' width='85.3433' height='16.8626' rx={RX} fill='#00F701' />
|
||||
<rect opacity='1' width='16.8626' height='16.8626' rx={RX} fill='#00F701' />
|
||||
<rect opacity='0.6' x='34.2403' width='34.2403' height='33.7252' rx={RX} fill='#00F701' />
|
||||
<rect opacity='1' x='34.2403' width='16.8626' height='16.8626' rx={RX} fill='#00F701' />
|
||||
<rect
|
||||
opacity='1'
|
||||
x='51.6188'
|
||||
y='16.8626'
|
||||
width='16.8626'
|
||||
height='16.8626'
|
||||
rx={RX}
|
||||
fill='#00F701'
|
||||
/>
|
||||
<rect opacity='1' x='68.4812' width='54.6502' height='16.8626' rx={RX} fill='#FFCC02' />
|
||||
<rect opacity='0.6' x='106.268' width='34.2403' height='33.7252' rx={RX} fill='#FFCC02' />
|
||||
<rect opacity='0.6' x='106.268' width='51.103' height='16.8626' rx={RX} fill='#FFCC02' />
|
||||
<rect
|
||||
opacity='1'
|
||||
x='123.6484'
|
||||
y='16.8626'
|
||||
width='16.8626'
|
||||
height='16.8626'
|
||||
rx={RX}
|
||||
fill='#FFCC02'
|
||||
/>
|
||||
<rect opacity='0.6' x='157.371' width='34.2403' height='16.8626' rx={RX} fill='#FA4EDF' />
|
||||
<rect opacity='1' x='157.371' width='16.8626' height='16.8626' rx={RX} fill='#FA4EDF' />
|
||||
<rect opacity='0.6' x='208.993' width='68.4805' height='16.8626' rx={RX} fill='#2ABBF8' />
|
||||
<rect opacity='0.6' x='209.137' width='16.8626' height='33.7252' rx={RX} fill='#2ABBF8' />
|
||||
<rect opacity='0.6' x='243.233' width='34.2403' height='33.7252' rx={RX} fill='#2ABBF8' />
|
||||
<rect opacity='1' x='243.233' width='16.8626' height='16.8626' rx={RX} fill='#2ABBF8' />
|
||||
<rect opacity='0.6' x='260.096' width='34.04' height='16.8626' rx={RX} fill='#2ABBF8' />
|
||||
<rect
|
||||
opacity='1'
|
||||
x='260.611'
|
||||
y='16.8626'
|
||||
width='16.8626'
|
||||
height='16.8626'
|
||||
rx={RX}
|
||||
fill='#2ABBF8'
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Vertical block strip — left edge */}
|
||||
<div className='-translate-y-1/2 absolute top-[50%] left-0 w-[calc(16px_+_1.25vw)] max-w-[34px] opacity-60'>
|
||||
<svg
|
||||
width={34}
|
||||
height={226}
|
||||
viewBox='0 0 34 226.021'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
className='h-auto w-full'
|
||||
>
|
||||
<rect
|
||||
opacity='0.6'
|
||||
width='34.240'
|
||||
height='33.725'
|
||||
rx={RX}
|
||||
fill='#FA4EDF'
|
||||
transform='matrix(0 1 1 0 0 0)'
|
||||
/>
|
||||
<rect
|
||||
opacity='0.6'
|
||||
width='16.8626'
|
||||
height='68.480'
|
||||
rx={RX}
|
||||
fill='#FA4EDF'
|
||||
transform='matrix(-1 0 0 1 33.727 0)'
|
||||
/>
|
||||
<rect
|
||||
opacity='1'
|
||||
width='16.8626'
|
||||
height='16.8626'
|
||||
rx={RX}
|
||||
fill='#FA4EDF'
|
||||
transform='matrix(-1 0 0 1 33.727 17.378)'
|
||||
/>
|
||||
<rect
|
||||
opacity='0.6'
|
||||
width='16.8626'
|
||||
height='33.986'
|
||||
rx={RX}
|
||||
fill='#FA4EDF'
|
||||
transform='matrix(0 1 1 0 0 51.616)'
|
||||
/>
|
||||
<rect
|
||||
opacity='0.6'
|
||||
width='16.8626'
|
||||
height='140.507'
|
||||
rx={RX}
|
||||
fill='#00F701'
|
||||
transform='matrix(-1 0 0 1 33.986 85.335)'
|
||||
/>
|
||||
<rect
|
||||
opacity='0.4'
|
||||
x='17.119'
|
||||
y='136.962'
|
||||
width='34.240'
|
||||
height='16.8626'
|
||||
rx={RX}
|
||||
fill='#FFCC02'
|
||||
transform='rotate(-90 17.119 136.962)'
|
||||
/>
|
||||
<rect
|
||||
opacity='1'
|
||||
x='17.119'
|
||||
y='136.962'
|
||||
width='16.8626'
|
||||
height='16.8626'
|
||||
rx={RX}
|
||||
fill='#FFCC02'
|
||||
transform='rotate(-90 17.119 136.962)'
|
||||
/>
|
||||
<rect
|
||||
opacity='0.5'
|
||||
width='34.240'
|
||||
height='33.725'
|
||||
rx={RX}
|
||||
fill='#00F701'
|
||||
transform='matrix(0 1 1 0 0.257 153.825)'
|
||||
/>
|
||||
<rect
|
||||
opacity='1'
|
||||
width='16.8626'
|
||||
height='16.8626'
|
||||
rx={RX}
|
||||
fill='#00F701'
|
||||
transform='matrix(0 1 1 0 0.257 153.825)'
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Vertical block strip — right edge */}
|
||||
<div className='-translate-y-1/2 absolute top-[50%] right-0 w-[calc(16px_+_1.25vw)] max-w-[34px] opacity-60'>
|
||||
<svg
|
||||
width={34}
|
||||
height={205}
|
||||
viewBox='0 0 34 204.769'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
className='h-auto w-full'
|
||||
>
|
||||
<rect
|
||||
opacity='0.6'
|
||||
width='16.8626'
|
||||
height='33.726'
|
||||
rx={RX}
|
||||
fill='#FA4EDF'
|
||||
transform='matrix(0 1 1 0 0 0)'
|
||||
/>
|
||||
<rect
|
||||
opacity='0.6'
|
||||
width='34.241'
|
||||
height='16.8626'
|
||||
rx={RX}
|
||||
fill='#FA4EDF'
|
||||
transform='matrix(0 1 1 0 16.891 0)'
|
||||
/>
|
||||
<rect
|
||||
opacity='0.6'
|
||||
width='16.8626'
|
||||
height='68.482'
|
||||
rx={RX}
|
||||
fill='#FA4EDF'
|
||||
transform='matrix(-1 0 0 1 33.739 16.888)'
|
||||
/>
|
||||
<rect
|
||||
opacity='0.6'
|
||||
width='16.8626'
|
||||
height='33.726'
|
||||
rx={RX}
|
||||
fill='#FA4EDF'
|
||||
transform='matrix(0 1 1 0 0 33.776)'
|
||||
/>
|
||||
<rect
|
||||
opacity='1'
|
||||
width='16.8626'
|
||||
height='16.8626'
|
||||
rx={RX}
|
||||
fill='#FA4EDF'
|
||||
transform='matrix(-1 0 0 1 33.739 34.272)'
|
||||
/>
|
||||
<rect
|
||||
opacity='0.6'
|
||||
width='16.8626'
|
||||
height='33.726'
|
||||
rx={RX}
|
||||
fill='#FA4EDF'
|
||||
transform='matrix(0 1 1 0 0.012 68.510)'
|
||||
/>
|
||||
<rect
|
||||
opacity='0.6'
|
||||
width='16.8626'
|
||||
height='102.384'
|
||||
rx={RX}
|
||||
fill='#2ABBF8'
|
||||
transform='matrix(-1 0 0 1 33.787 102.384)'
|
||||
/>
|
||||
<rect
|
||||
opacity='0.4'
|
||||
x='17.131'
|
||||
y='153.859'
|
||||
width='34.241'
|
||||
height='16.8626'
|
||||
rx={RX}
|
||||
fill='#00F701'
|
||||
transform='rotate(-90 17.131 153.859)'
|
||||
/>
|
||||
<rect
|
||||
opacity='1'
|
||||
x='17.131'
|
||||
y='153.859'
|
||||
width='16.8626'
|
||||
height='16.8626'
|
||||
rx={RX}
|
||||
fill='#00F701'
|
||||
transform='rotate(-90 17.131 153.859)'
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
ApolloIcon,
|
||||
ArxivIcon,
|
||||
AsanaIcon,
|
||||
AshbyIcon,
|
||||
AttioIcon,
|
||||
BrainIcon,
|
||||
BrowserUseIcon,
|
||||
@@ -24,7 +25,9 @@ import {
|
||||
CloudflareIcon,
|
||||
ConfluenceIcon,
|
||||
CursorIcon,
|
||||
DatabricksIcon,
|
||||
DatadogIcon,
|
||||
DevinIcon,
|
||||
DiscordIcon,
|
||||
DocumentIcon,
|
||||
DropboxIcon,
|
||||
@@ -38,12 +41,15 @@ import {
|
||||
EyeIcon,
|
||||
FirecrawlIcon,
|
||||
FirefliesIcon,
|
||||
GammaIcon,
|
||||
GithubIcon,
|
||||
GitLabIcon,
|
||||
GmailIcon,
|
||||
GongIcon,
|
||||
GoogleBigQueryIcon,
|
||||
GoogleBooksIcon,
|
||||
GoogleCalendarIcon,
|
||||
GoogleContactsIcon,
|
||||
GoogleDocsIcon,
|
||||
GoogleDriveIcon,
|
||||
GoogleFormsIcon,
|
||||
@@ -52,10 +58,12 @@ import {
|
||||
GoogleMapsIcon,
|
||||
GoogleSheetsIcon,
|
||||
GoogleSlidesIcon,
|
||||
GoogleTasksIcon,
|
||||
GoogleTranslateIcon,
|
||||
GoogleVaultIcon,
|
||||
GrafanaIcon,
|
||||
GrainIcon,
|
||||
GreenhouseIcon,
|
||||
GreptileIcon,
|
||||
HexIcon,
|
||||
HubspotIcon,
|
||||
@@ -73,6 +81,8 @@ import {
|
||||
LinearIcon,
|
||||
LinkedInIcon,
|
||||
LinkupIcon,
|
||||
LoopsIcon,
|
||||
LumaIcon,
|
||||
MailchimpIcon,
|
||||
MailgunIcon,
|
||||
MailServerIcon,
|
||||
@@ -161,6 +171,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
apollo: ApolloIcon,
|
||||
arxiv: ArxivIcon,
|
||||
asana: AsanaIcon,
|
||||
ashby: AshbyIcon,
|
||||
attio: AttioIcon,
|
||||
browser_use: BrowserUseIcon,
|
||||
calcom: CalComIcon,
|
||||
@@ -171,7 +182,9 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
cloudflare: CloudflareIcon,
|
||||
confluence_v2: ConfluenceIcon,
|
||||
cursor_v2: CursorIcon,
|
||||
databricks: DatabricksIcon,
|
||||
datadog: DatadogIcon,
|
||||
devin: DevinIcon,
|
||||
discord: DiscordIcon,
|
||||
dropbox: DropboxIcon,
|
||||
dspy: DsPyIcon,
|
||||
@@ -184,12 +197,15 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
file_v3: DocumentIcon,
|
||||
firecrawl: FirecrawlIcon,
|
||||
fireflies_v2: FirefliesIcon,
|
||||
gamma: GammaIcon,
|
||||
github_v2: GithubIcon,
|
||||
gitlab: GitLabIcon,
|
||||
gmail_v2: GmailIcon,
|
||||
gong: GongIcon,
|
||||
google_bigquery: GoogleBigQueryIcon,
|
||||
google_books: GoogleBooksIcon,
|
||||
google_calendar_v2: GoogleCalendarIcon,
|
||||
google_contacts: GoogleContactsIcon,
|
||||
google_docs: GoogleDocsIcon,
|
||||
google_drive: GoogleDriveIcon,
|
||||
google_forms: GoogleFormsIcon,
|
||||
@@ -198,10 +214,12 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
google_search: GoogleIcon,
|
||||
google_sheets_v2: GoogleSheetsIcon,
|
||||
google_slides_v2: GoogleSlidesIcon,
|
||||
google_tasks: GoogleTasksIcon,
|
||||
google_translate: GoogleTranslateIcon,
|
||||
google_vault: GoogleVaultIcon,
|
||||
grafana: GrafanaIcon,
|
||||
grain: GrainIcon,
|
||||
greenhouse: GreenhouseIcon,
|
||||
greptile: GreptileIcon,
|
||||
hex: HexIcon,
|
||||
hubspot: HubspotIcon,
|
||||
@@ -221,6 +239,8 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
linear: LinearIcon,
|
||||
linkedin: LinkedInIcon,
|
||||
linkup: LinkupIcon,
|
||||
loops: LoopsIcon,
|
||||
luma: LumaIcon,
|
||||
mailchimp: MailchimpIcon,
|
||||
mailgun: MailgunIcon,
|
||||
mem0: Mem0Icon,
|
||||
|
||||
473
apps/docs/content/docs/en/tools/ashby.mdx
Normal file
473
apps/docs/content/docs/en/tools/ashby.mdx
Normal file
@@ -0,0 +1,473 @@
|
||||
---
|
||||
title: Ashby
|
||||
description: Manage candidates, jobs, and applications in Ashby
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="ashby"
|
||||
color="#5D4ED6"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Ashby](https://www.ashbyhq.com/) is an all-in-one recruiting platform that combines an applicant tracking system (ATS), CRM, scheduling, and analytics to help teams hire more effectively.
|
||||
|
||||
With Ashby, you can:
|
||||
|
||||
- **List and search candidates**: Browse your full candidate pipeline or search by name and email to quickly find specific people
|
||||
- **Create candidates**: Add new candidates to your Ashby organization with contact details
|
||||
- **View candidate details**: Retrieve full candidate profiles including tags, email, phone, and timestamps
|
||||
- **Add notes to candidates**: Attach notes to candidate records to capture feedback, context, or follow-up items
|
||||
- **List and view jobs**: Browse all open, closed, and archived job postings with location and department info
|
||||
- **List applications**: View all applications across your organization with candidate and job details, status tracking, and pagination
|
||||
|
||||
In Sim, the Ashby integration enables your agents to programmatically manage your recruiting pipeline. Agents can search for candidates, create new candidate records, add notes after interviews, and monitor applications across jobs. This allows you to automate recruiting workflows like candidate intake, interview follow-ups, pipeline reporting, and cross-referencing candidates across roles.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Ashby into the workflow. Can list, search, create, and update candidates, list and get job details, create notes, list notes, list and get applications, create applications, and list offers.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `ashby_create_application`
|
||||
|
||||
Creates a new application for a candidate on a job. Optionally specify interview plan, stage, source, and credited user.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Ashby API Key |
|
||||
| `candidateId` | string | Yes | The UUID of the candidate to consider for the job |
|
||||
| `jobId` | string | Yes | The UUID of the job to consider the candidate for |
|
||||
| `interviewPlanId` | string | No | UUID of the interview plan to use \(defaults to the job default plan\) |
|
||||
| `interviewStageId` | string | No | UUID of the interview stage to place the application in \(defaults to first Lead stage\) |
|
||||
| `sourceId` | string | No | UUID of the source to set on the application |
|
||||
| `creditedToUserId` | string | No | UUID of the user the application is credited to |
|
||||
| `createdAt` | string | No | ISO 8601 timestamp to set as the application creation date \(defaults to now\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Created application UUID |
|
||||
| `status` | string | Application status \(Active, Hired, Archived, Lead\) |
|
||||
| `candidate` | object | Associated candidate |
|
||||
| ↳ `id` | string | Candidate UUID |
|
||||
| ↳ `name` | string | Candidate name |
|
||||
| `job` | object | Associated job |
|
||||
| ↳ `id` | string | Job UUID |
|
||||
| ↳ `title` | string | Job title |
|
||||
| `currentInterviewStage` | object | Current interview stage |
|
||||
| ↳ `id` | string | Stage UUID |
|
||||
| ↳ `title` | string | Stage title |
|
||||
| ↳ `type` | string | Stage type |
|
||||
| `source` | object | Application source |
|
||||
| ↳ `id` | string | Source UUID |
|
||||
| ↳ `title` | string | Source title |
|
||||
| `createdAt` | string | ISO 8601 creation timestamp |
|
||||
| `updatedAt` | string | ISO 8601 last update timestamp |
|
||||
|
||||
### `ashby_create_candidate`
|
||||
|
||||
Creates a new candidate record in Ashby.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Ashby API Key |
|
||||
| `name` | string | Yes | The candidate full name |
|
||||
| `email` | string | No | Primary email address for the candidate |
|
||||
| `emailType` | string | No | Email address type: Personal, Work, or Other \(default Work\) |
|
||||
| `phoneNumber` | string | No | Primary phone number for the candidate |
|
||||
| `phoneType` | string | No | Phone number type: Personal, Work, or Other \(default Work\) |
|
||||
| `linkedInUrl` | string | No | LinkedIn profile URL |
|
||||
| `githubUrl` | string | No | GitHub profile URL |
|
||||
| `sourceId` | string | No | UUID of the source to attribute the candidate to |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Created candidate UUID |
|
||||
| `name` | string | Full name |
|
||||
| `primaryEmailAddress` | object | Primary email contact info |
|
||||
| ↳ `value` | string | Email address |
|
||||
| ↳ `type` | string | Contact type \(Personal, Work, Other\) |
|
||||
| ↳ `isPrimary` | boolean | Whether this is the primary email |
|
||||
| `primaryPhoneNumber` | object | Primary phone contact info |
|
||||
| ↳ `value` | string | Phone number |
|
||||
| ↳ `type` | string | Contact type \(Personal, Work, Other\) |
|
||||
| ↳ `isPrimary` | boolean | Whether this is the primary phone |
|
||||
| `createdAt` | string | ISO 8601 creation timestamp |
|
||||
|
||||
### `ashby_create_note`
|
||||
|
||||
Creates a note on a candidate in Ashby. Supports plain text and HTML content (bold, italic, underline, links, lists, code).
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Ashby API Key |
|
||||
| `candidateId` | string | Yes | The UUID of the candidate to add the note to |
|
||||
| `note` | string | Yes | The note content. If noteType is text/html, supports: <b>, <i>, <u>, <a>, <ul>, <ol>, <li>, <code>, <pre> |
|
||||
| `noteType` | string | No | Content type of the note: text/plain \(default\) or text/html |
|
||||
| `sendNotifications` | boolean | No | Whether to send notifications to subscribed users \(default false\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Created note UUID |
|
||||
| `content` | string | Note content as stored |
|
||||
| `author` | object | Note author |
|
||||
| ↳ `id` | string | Author user UUID |
|
||||
| ↳ `firstName` | string | First name |
|
||||
| ↳ `lastName` | string | Last name |
|
||||
| ↳ `email` | string | Email address |
|
||||
| `createdAt` | string | ISO 8601 creation timestamp |
|
||||
|
||||
### `ashby_get_application`
|
||||
|
||||
Retrieves full details about a single application by its ID.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Ashby API Key |
|
||||
| `applicationId` | string | Yes | The UUID of the application to fetch |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Application UUID |
|
||||
| `status` | string | Application status \(Active, Hired, Archived, Lead\) |
|
||||
| `candidate` | object | Associated candidate |
|
||||
| ↳ `id` | string | Candidate UUID |
|
||||
| ↳ `name` | string | Candidate name |
|
||||
| `job` | object | Associated job |
|
||||
| ↳ `id` | string | Job UUID |
|
||||
| ↳ `title` | string | Job title |
|
||||
| `currentInterviewStage` | object | Current interview stage |
|
||||
| ↳ `id` | string | Stage UUID |
|
||||
| ↳ `title` | string | Stage title |
|
||||
| ↳ `type` | string | Stage type |
|
||||
| `source` | object | Application source |
|
||||
| ↳ `id` | string | Source UUID |
|
||||
| ↳ `title` | string | Source title |
|
||||
| `archiveReason` | object | Reason for archival |
|
||||
| ↳ `id` | string | Reason UUID |
|
||||
| ↳ `text` | string | Reason text |
|
||||
| ↳ `reasonType` | string | Reason type |
|
||||
| `archivedAt` | string | ISO 8601 archive timestamp |
|
||||
| `createdAt` | string | ISO 8601 creation timestamp |
|
||||
| `updatedAt` | string | ISO 8601 last update timestamp |
|
||||
|
||||
### `ashby_get_candidate`
|
||||
|
||||
Retrieves full details about a single candidate by their ID.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Ashby API Key |
|
||||
| `candidateId` | string | Yes | The UUID of the candidate to fetch |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Candidate UUID |
|
||||
| `name` | string | Full name |
|
||||
| `primaryEmailAddress` | object | Primary email contact info |
|
||||
| ↳ `value` | string | Email address |
|
||||
| ↳ `type` | string | Contact type \(Personal, Work, Other\) |
|
||||
| ↳ `isPrimary` | boolean | Whether this is the primary email |
|
||||
| `primaryPhoneNumber` | object | Primary phone contact info |
|
||||
| ↳ `value` | string | Phone number |
|
||||
| ↳ `type` | string | Contact type \(Personal, Work, Other\) |
|
||||
| ↳ `isPrimary` | boolean | Whether this is the primary phone |
|
||||
| `profileUrl` | string | URL to the candidate Ashby profile |
|
||||
| `position` | string | Current position or title |
|
||||
| `company` | string | Current company |
|
||||
| `linkedInUrl` | string | LinkedIn profile URL |
|
||||
| `githubUrl` | string | GitHub profile URL |
|
||||
| `tags` | array | Tags applied to the candidate |
|
||||
| ↳ `id` | string | Tag UUID |
|
||||
| ↳ `title` | string | Tag title |
|
||||
| `applicationIds` | array | IDs of associated applications |
|
||||
| `createdAt` | string | ISO 8601 creation timestamp |
|
||||
| `updatedAt` | string | ISO 8601 last update timestamp |
|
||||
|
||||
### `ashby_get_job`
|
||||
|
||||
Retrieves full details about a single job by its ID.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Ashby API Key |
|
||||
| `jobId` | string | Yes | The UUID of the job to fetch |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Job UUID |
|
||||
| `title` | string | Job title |
|
||||
| `status` | string | Job status \(Open, Closed, Draft, Archived, On Hold\) |
|
||||
| `employmentType` | string | Employment type \(FullTime, PartTime, Intern, Contract, Temporary\) |
|
||||
| `departmentId` | string | Department UUID |
|
||||
| `locationId` | string | Location UUID |
|
||||
| `descriptionPlain` | string | Job description in plain text |
|
||||
| `isArchived` | boolean | Whether the job is archived |
|
||||
| `createdAt` | string | ISO 8601 creation timestamp |
|
||||
| `updatedAt` | string | ISO 8601 last update timestamp |
|
||||
|
||||
### `ashby_list_applications`
|
||||
|
||||
Lists all applications in an Ashby organization with pagination and optional filters for status, job, candidate, and creation date.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Ashby API Key |
|
||||
| `cursor` | string | No | Opaque pagination cursor from a previous response nextCursor value |
|
||||
| `perPage` | number | No | Number of results per page \(default 100\) |
|
||||
| `status` | string | No | Filter by application status: Active, Hired, Archived, or Lead |
|
||||
| `jobId` | string | No | Filter applications by a specific job UUID |
|
||||
| `candidateId` | string | No | Filter applications by a specific candidate UUID |
|
||||
| `createdAfter` | string | No | Filter to applications created after this ISO 8601 timestamp \(e.g. 2024-01-01T00:00:00Z\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `applications` | array | List of applications |
|
||||
| ↳ `id` | string | Application UUID |
|
||||
| ↳ `status` | string | Application status \(Active, Hired, Archived, Lead\) |
|
||||
| ↳ `candidate` | object | Associated candidate |
|
||||
| ↳ `id` | string | Candidate UUID |
|
||||
| ↳ `name` | string | Candidate name |
|
||||
| ↳ `job` | object | Associated job |
|
||||
| ↳ `id` | string | Job UUID |
|
||||
| ↳ `title` | string | Job title |
|
||||
| ↳ `currentInterviewStage` | object | Current interview stage |
|
||||
| ↳ `id` | string | Stage UUID |
|
||||
| ↳ `title` | string | Stage title |
|
||||
| ↳ `type` | string | Stage type |
|
||||
| ↳ `source` | object | Application source |
|
||||
| ↳ `id` | string | Source UUID |
|
||||
| ↳ `title` | string | Source title |
|
||||
| ↳ `createdAt` | string | ISO 8601 creation timestamp |
|
||||
| ↳ `updatedAt` | string | ISO 8601 last update timestamp |
|
||||
| `moreDataAvailable` | boolean | Whether more pages of results exist |
|
||||
| `nextCursor` | string | Opaque cursor for fetching the next page |
|
||||
|
||||
### `ashby_list_candidates`
|
||||
|
||||
Lists all candidates in an Ashby organization with cursor-based pagination.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Ashby API Key |
|
||||
| `cursor` | string | No | Opaque pagination cursor from a previous response nextCursor value |
|
||||
| `perPage` | number | No | Number of results per page \(default 100\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `candidates` | array | List of candidates |
|
||||
| ↳ `id` | string | Candidate UUID |
|
||||
| ↳ `name` | string | Full name |
|
||||
| ↳ `primaryEmailAddress` | object | Primary email contact info |
|
||||
| ↳ `value` | string | Email address |
|
||||
| ↳ `type` | string | Contact type \(Personal, Work, Other\) |
|
||||
| ↳ `isPrimary` | boolean | Whether this is the primary email |
|
||||
| ↳ `primaryPhoneNumber` | object | Primary phone contact info |
|
||||
| ↳ `value` | string | Phone number |
|
||||
| ↳ `type` | string | Contact type \(Personal, Work, Other\) |
|
||||
| ↳ `isPrimary` | boolean | Whether this is the primary phone |
|
||||
| ↳ `createdAt` | string | ISO 8601 creation timestamp |
|
||||
| ↳ `updatedAt` | string | ISO 8601 last update timestamp |
|
||||
| `moreDataAvailable` | boolean | Whether more pages of results exist |
|
||||
| `nextCursor` | string | Opaque cursor for fetching the next page |
|
||||
|
||||
### `ashby_list_jobs`
|
||||
|
||||
Lists all jobs in an Ashby organization. By default returns Open, Closed, and Archived jobs. Specify status to filter.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Ashby API Key |
|
||||
| `cursor` | string | No | Opaque pagination cursor from a previous response nextCursor value |
|
||||
| `perPage` | number | No | Number of results per page \(default 100\) |
|
||||
| `status` | string | No | Filter by job status: Open, Closed, Archived, or Draft |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `jobs` | array | List of jobs |
|
||||
| ↳ `id` | string | Job UUID |
|
||||
| ↳ `title` | string | Job title |
|
||||
| ↳ `status` | string | Job status \(Open, Closed, Archived, Draft\) |
|
||||
| ↳ `employmentType` | string | Employment type \(FullTime, PartTime, Intern, Contract, Temporary\) |
|
||||
| ↳ `departmentId` | string | Department UUID |
|
||||
| ↳ `locationId` | string | Location UUID |
|
||||
| ↳ `createdAt` | string | ISO 8601 creation timestamp |
|
||||
| ↳ `updatedAt` | string | ISO 8601 last update timestamp |
|
||||
| `moreDataAvailable` | boolean | Whether more pages of results exist |
|
||||
| `nextCursor` | string | Opaque cursor for fetching the next page |
|
||||
|
||||
### `ashby_list_notes`
|
||||
|
||||
Lists all notes on a candidate with pagination support.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Ashby API Key |
|
||||
| `candidateId` | string | Yes | The UUID of the candidate to list notes for |
|
||||
| `cursor` | string | No | Opaque pagination cursor from a previous response nextCursor value |
|
||||
| `perPage` | number | No | Number of results per page |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `notes` | array | List of notes on the candidate |
|
||||
| ↳ `id` | string | Note UUID |
|
||||
| ↳ `content` | string | Note content |
|
||||
| ↳ `author` | object | Note author |
|
||||
| ↳ `id` | string | Author user UUID |
|
||||
| ↳ `firstName` | string | First name |
|
||||
| ↳ `lastName` | string | Last name |
|
||||
| ↳ `email` | string | Email address |
|
||||
| ↳ `createdAt` | string | ISO 8601 creation timestamp |
|
||||
| `moreDataAvailable` | boolean | Whether more pages of results exist |
|
||||
| `nextCursor` | string | Opaque cursor for fetching the next page |
|
||||
|
||||
### `ashby_list_offers`
|
||||
|
||||
Lists all offers with their latest version in an Ashby organization.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Ashby API Key |
|
||||
| `cursor` | string | No | Opaque pagination cursor from a previous response nextCursor value |
|
||||
| `perPage` | number | No | Number of results per page |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `offers` | array | List of offers |
|
||||
| ↳ `id` | string | Offer UUID |
|
||||
| ↳ `status` | string | Offer status |
|
||||
| ↳ `candidate` | object | Associated candidate |
|
||||
| ↳ `id` | string | Candidate UUID |
|
||||
| ↳ `name` | string | Candidate name |
|
||||
| ↳ `job` | object | Associated job |
|
||||
| ↳ `id` | string | Job UUID |
|
||||
| ↳ `title` | string | Job title |
|
||||
| ↳ `createdAt` | string | ISO 8601 creation timestamp |
|
||||
| ↳ `updatedAt` | string | ISO 8601 last update timestamp |
|
||||
| `moreDataAvailable` | boolean | Whether more pages of results exist |
|
||||
| `nextCursor` | string | Opaque cursor for fetching the next page |
|
||||
|
||||
### `ashby_search_candidates`
|
||||
|
||||
Searches for candidates by name and/or email with AND logic. Results are limited to 100 matches. Use candidate.list for full pagination.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Ashby API Key |
|
||||
| `name` | string | No | Candidate name to search for \(combined with email using AND logic\) |
|
||||
| `email` | string | No | Candidate email to search for \(combined with name using AND logic\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `candidates` | array | Matching candidates \(max 100 results\) |
|
||||
| ↳ `id` | string | Candidate UUID |
|
||||
| ↳ `name` | string | Full name |
|
||||
| ↳ `primaryEmailAddress` | object | Primary email contact info |
|
||||
| ↳ `value` | string | Email address |
|
||||
| ↳ `type` | string | Contact type \(Personal, Work, Other\) |
|
||||
| ↳ `isPrimary` | boolean | Whether this is the primary email |
|
||||
| ↳ `primaryPhoneNumber` | object | Primary phone contact info |
|
||||
| ↳ `value` | string | Phone number |
|
||||
| ↳ `type` | string | Contact type \(Personal, Work, Other\) |
|
||||
| ↳ `isPrimary` | boolean | Whether this is the primary phone |
|
||||
|
||||
### `ashby_update_candidate`
|
||||
|
||||
Updates an existing candidate record in Ashby. Only provided fields are changed.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Ashby API Key |
|
||||
| `candidateId` | string | Yes | The UUID of the candidate to update |
|
||||
| `name` | string | No | Updated full name |
|
||||
| `email` | string | No | Updated primary email address |
|
||||
| `emailType` | string | No | Email address type: Personal, Work, or Other \(default Work\) |
|
||||
| `phoneNumber` | string | No | Updated primary phone number |
|
||||
| `phoneType` | string | No | Phone number type: Personal, Work, or Other \(default Work\) |
|
||||
| `linkedInUrl` | string | No | LinkedIn profile URL |
|
||||
| `githubUrl` | string | No | GitHub profile URL |
|
||||
| `websiteUrl` | string | No | Personal website URL |
|
||||
| `sourceId` | string | No | UUID of the source to attribute the candidate to |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Candidate UUID |
|
||||
| `name` | string | Full name |
|
||||
| `primaryEmailAddress` | object | Primary email contact info |
|
||||
| ↳ `value` | string | Email address |
|
||||
| ↳ `type` | string | Contact type \(Personal, Work, Other\) |
|
||||
| ↳ `isPrimary` | boolean | Whether this is the primary email |
|
||||
| `primaryPhoneNumber` | object | Primary phone contact info |
|
||||
| ↳ `value` | string | Phone number |
|
||||
| ↳ `type` | string | Contact type \(Personal, Work, Other\) |
|
||||
| ↳ `isPrimary` | boolean | Whether this is the primary phone |
|
||||
| `profileUrl` | string | URL to the candidate Ashby profile |
|
||||
| `position` | string | Current position or title |
|
||||
| `company` | string | Current company |
|
||||
| `linkedInUrl` | string | LinkedIn profile URL |
|
||||
| `githubUrl` | string | GitHub profile URL |
|
||||
| `tags` | array | Tags applied to the candidate |
|
||||
| ↳ `id` | string | Tag UUID |
|
||||
| ↳ `title` | string | Tag title |
|
||||
| `applicationIds` | array | IDs of associated applications |
|
||||
| `createdAt` | string | ISO 8601 creation timestamp |
|
||||
| `updatedAt` | string | ISO 8601 last update timestamp |
|
||||
|
||||
|
||||
@@ -1013,6 +1013,85 @@ Get details about a specific Confluence space.
|
||||
| ↳ `value` | string | Description text content |
|
||||
| ↳ `representation` | string | Content representation format \(e.g., plain, view, storage\) |
|
||||
|
||||
### `confluence_create_space`
|
||||
|
||||
Create a new Confluence space.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `name` | string | Yes | Name for the new space |
|
||||
| `key` | string | Yes | Unique key for the space \(uppercase, no spaces\) |
|
||||
| `description` | string | No | Description for the new space |
|
||||
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | ISO 8601 timestamp of the operation |
|
||||
| `spaceId` | string | Created space ID |
|
||||
| `name` | string | Space name |
|
||||
| `key` | string | Space key |
|
||||
| `type` | string | Space type |
|
||||
| `status` | string | Space status |
|
||||
| `url` | string | URL to view the space |
|
||||
| `homepageId` | string | Homepage ID |
|
||||
| `description` | object | Space description |
|
||||
| ↳ `value` | string | Description text content |
|
||||
| ↳ `representation` | string | Content representation format \(e.g., plain, view, storage\) |
|
||||
|
||||
### `confluence_update_space`
|
||||
|
||||
Update a Confluence space name or description.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `spaceId` | string | Yes | ID of the space to update |
|
||||
| `name` | string | No | New name for the space |
|
||||
| `description` | string | No | New description for the space |
|
||||
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | ISO 8601 timestamp of the operation |
|
||||
| `spaceId` | string | Updated space ID |
|
||||
| `name` | string | Space name |
|
||||
| `key` | string | Space key |
|
||||
| `type` | string | Space type |
|
||||
| `status` | string | Space status |
|
||||
| `url` | string | URL to view the space |
|
||||
| `description` | object | Space description |
|
||||
| ↳ `value` | string | Description text content |
|
||||
| ↳ `representation` | string | Content representation format \(e.g., plain, view, storage\) |
|
||||
|
||||
### `confluence_delete_space`
|
||||
|
||||
Delete a Confluence space.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `spaceId` | string | Yes | ID of the space to delete |
|
||||
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | ISO 8601 timestamp of the operation |
|
||||
| `spaceId` | string | Deleted space ID |
|
||||
| `deleted` | boolean | Deletion status |
|
||||
|
||||
### `confluence_list_spaces`
|
||||
|
||||
List all Confluence spaces accessible to the user.
|
||||
@@ -1045,4 +1124,311 @@ List all Confluence spaces accessible to the user.
|
||||
| ↳ `representation` | string | Content representation format \(e.g., plain, view, storage\) |
|
||||
| `nextCursor` | string | Cursor for fetching the next page of results |
|
||||
|
||||
### `confluence_list_space_properties`
|
||||
|
||||
List properties on a Confluence space.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `spaceId` | string | Yes | Space ID to list properties for |
|
||||
| `limit` | number | No | Maximum number of properties to return \(default: 50, max: 250\) |
|
||||
| `cursor` | string | No | Pagination cursor from previous response |
|
||||
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | ISO 8601 timestamp of the operation |
|
||||
| `properties` | array | Array of space properties |
|
||||
| ↳ `id` | string | Property ID |
|
||||
| ↳ `key` | string | Property key |
|
||||
| ↳ `value` | json | Property value |
|
||||
| `spaceId` | string | Space ID |
|
||||
| `nextCursor` | string | Cursor for fetching the next page of results |
|
||||
|
||||
### `confluence_create_space_property`
|
||||
|
||||
Create a property on a Confluence space.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `spaceId` | string | Yes | Space ID to create the property on |
|
||||
| `key` | string | Yes | Property key/name |
|
||||
| `value` | json | No | Property value \(JSON\) |
|
||||
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | ISO 8601 timestamp of the operation |
|
||||
| `propertyId` | string | Created property ID |
|
||||
| `key` | string | Property key |
|
||||
| `value` | json | Property value |
|
||||
| `spaceId` | string | Space ID |
|
||||
|
||||
### `confluence_delete_space_property`
|
||||
|
||||
Delete a property from a Confluence space.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `spaceId` | string | Yes | Space ID the property belongs to |
|
||||
| `propertyId` | string | Yes | Property ID to delete |
|
||||
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | ISO 8601 timestamp of the operation |
|
||||
| `spaceId` | string | Space ID |
|
||||
| `propertyId` | string | Deleted property ID |
|
||||
| `deleted` | boolean | Deletion status |
|
||||
|
||||
### `confluence_list_space_permissions`
|
||||
|
||||
List permissions for a Confluence space.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `spaceId` | string | Yes | Space ID to list permissions for |
|
||||
| `limit` | number | No | Maximum number of permissions to return \(default: 50, max: 250\) |
|
||||
| `cursor` | string | No | Pagination cursor from previous response |
|
||||
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | ISO 8601 timestamp of the operation |
|
||||
| `permissions` | array | Array of space permissions |
|
||||
| ↳ `id` | string | Permission ID |
|
||||
| ↳ `principalType` | string | Principal type \(user, group, role\) |
|
||||
| ↳ `principalId` | string | Principal ID |
|
||||
| ↳ `operationKey` | string | Operation key \(read, create, delete, etc.\) |
|
||||
| ↳ `operationTargetType` | string | Target type \(page, blogpost, space, etc.\) |
|
||||
| ↳ `anonymousAccess` | boolean | Whether anonymous access is allowed |
|
||||
| ↳ `unlicensedAccess` | boolean | Whether unlicensed access is allowed |
|
||||
| `spaceId` | string | Space ID |
|
||||
| `nextCursor` | string | Cursor for fetching the next page of results |
|
||||
|
||||
### `confluence_get_page_descendants`
|
||||
|
||||
Get all descendants of a Confluence page recursively.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `pageId` | string | Yes | Page ID to get descendants for |
|
||||
| `limit` | number | No | Maximum number of descendants to return \(default: 50, max: 250\) |
|
||||
| `cursor` | string | No | Pagination cursor from previous response |
|
||||
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | ISO 8601 timestamp of the operation |
|
||||
| `descendants` | array | Array of descendant pages |
|
||||
| ↳ `id` | string | Page ID |
|
||||
| ↳ `title` | string | Page title |
|
||||
| ↳ `type` | string | Content type \(page, whiteboard, database, etc.\) |
|
||||
| ↳ `status` | string | Page status |
|
||||
| ↳ `spaceId` | string | Space ID |
|
||||
| ↳ `parentId` | string | Parent page ID |
|
||||
| ↳ `childPosition` | number | Position among siblings |
|
||||
| ↳ `depth` | number | Depth in the hierarchy |
|
||||
| `pageId` | string | Parent page ID |
|
||||
| `nextCursor` | string | Cursor for fetching the next page of results |
|
||||
|
||||
### `confluence_list_tasks`
|
||||
|
||||
List inline tasks from Confluence. Optionally filter by page, space, assignee, or status.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `pageId` | string | No | Filter tasks by page ID |
|
||||
| `spaceId` | string | No | Filter tasks by space ID |
|
||||
| `assignedTo` | string | No | Filter tasks by assignee account ID |
|
||||
| `status` | string | No | Filter tasks by status \(complete or incomplete\) |
|
||||
| `limit` | number | No | Maximum number of tasks to return \(default: 50, max: 250\) |
|
||||
| `cursor` | string | No | Pagination cursor from previous response |
|
||||
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | ISO 8601 timestamp of the operation |
|
||||
| `tasks` | array | Array of Confluence tasks |
|
||||
| ↳ `id` | string | Task ID |
|
||||
| ↳ `localId` | string | Local task ID |
|
||||
| ↳ `spaceId` | string | Space ID |
|
||||
| ↳ `pageId` | string | Page ID |
|
||||
| ↳ `blogPostId` | string | Blog post ID |
|
||||
| ↳ `status` | string | Task status \(complete or incomplete\) |
|
||||
| ↳ `body` | string | Task body content in storage format |
|
||||
| ↳ `createdBy` | string | Creator account ID |
|
||||
| ↳ `assignedTo` | string | Assignee account ID |
|
||||
| ↳ `completedBy` | string | Completer account ID |
|
||||
| ↳ `createdAt` | string | Creation timestamp |
|
||||
| ↳ `updatedAt` | string | Last update timestamp |
|
||||
| ↳ `dueAt` | string | Due date |
|
||||
| ↳ `completedAt` | string | Completion timestamp |
|
||||
| `nextCursor` | string | Cursor for fetching the next page of results |
|
||||
|
||||
### `confluence_get_task`
|
||||
|
||||
Get a specific Confluence inline task by ID.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `taskId` | string | Yes | The ID of the task to retrieve |
|
||||
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | ISO 8601 timestamp of the operation |
|
||||
| `id` | string | Task ID |
|
||||
| `localId` | string | Local task ID |
|
||||
| `spaceId` | string | Space ID |
|
||||
| `pageId` | string | Page ID |
|
||||
| `blogPostId` | string | Blog post ID |
|
||||
| `status` | string | Task status \(complete or incomplete\) |
|
||||
| `body` | string | Task body content in storage format |
|
||||
| `createdBy` | string | Creator account ID |
|
||||
| `assignedTo` | string | Assignee account ID |
|
||||
| `completedBy` | string | Completer account ID |
|
||||
| `createdAt` | string | Creation timestamp |
|
||||
| `updatedAt` | string | Last update timestamp |
|
||||
| `dueAt` | string | Due date |
|
||||
| `completedAt` | string | Completion timestamp |
|
||||
|
||||
### `confluence_update_task`
|
||||
|
||||
Update the status of a Confluence inline task (complete or incomplete).
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `taskId` | string | Yes | The ID of the task to update |
|
||||
| `status` | string | Yes | New status for the task \(complete or incomplete\) |
|
||||
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | ISO 8601 timestamp of the operation |
|
||||
| `id` | string | Task ID |
|
||||
| `localId` | string | Local task ID |
|
||||
| `spaceId` | string | Space ID |
|
||||
| `pageId` | string | Page ID |
|
||||
| `blogPostId` | string | Blog post ID |
|
||||
| `status` | string | Updated task status |
|
||||
| `body` | string | Task body content in storage format |
|
||||
| `createdBy` | string | Creator account ID |
|
||||
| `assignedTo` | string | Assignee account ID |
|
||||
| `completedBy` | string | Completer account ID |
|
||||
| `createdAt` | string | Creation timestamp |
|
||||
| `updatedAt` | string | Last update timestamp |
|
||||
| `dueAt` | string | Due date |
|
||||
| `completedAt` | string | Completion timestamp |
|
||||
|
||||
### `confluence_update_blogpost`
|
||||
|
||||
Update an existing Confluence blog post title and/or content.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `blogPostId` | string | Yes | The ID of the blog post to update |
|
||||
| `title` | string | No | New title for the blog post |
|
||||
| `content` | string | No | New content for the blog post in storage format |
|
||||
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | ISO 8601 timestamp of the operation |
|
||||
| `blogPostId` | string | Updated blog post ID |
|
||||
| `title` | string | Blog post title |
|
||||
| `status` | string | Blog post status |
|
||||
| `spaceId` | string | Space ID |
|
||||
| `version` | json | Version information |
|
||||
| `url` | string | URL to view the blog post |
|
||||
|
||||
### `confluence_delete_blogpost`
|
||||
|
||||
Delete a Confluence blog post.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `blogPostId` | string | Yes | The ID of the blog post to delete |
|
||||
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | ISO 8601 timestamp of the operation |
|
||||
| `blogPostId` | string | Deleted blog post ID |
|
||||
| `deleted` | boolean | Deletion status |
|
||||
|
||||
### `confluence_get_user`
|
||||
|
||||
Get display name and profile info for a Confluence user by account ID.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `accountId` | string | Yes | The Atlassian account ID of the user to look up |
|
||||
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | ISO 8601 timestamp of the operation |
|
||||
| `accountId` | string | Atlassian account ID of the user |
|
||||
| `displayName` | string | Display name of the user |
|
||||
| `email` | string | Email address of the user |
|
||||
| `accountType` | string | Account type \(e.g., atlassian, app, customer\) |
|
||||
| `profilePicture` | string | Path to the user profile picture |
|
||||
| `publicName` | string | Public name of the user |
|
||||
|
||||
|
||||
|
||||
267
apps/docs/content/docs/en/tools/databricks.mdx
Normal file
267
apps/docs/content/docs/en/tools/databricks.mdx
Normal file
@@ -0,0 +1,267 @@
|
||||
---
|
||||
title: Databricks
|
||||
description: Run SQL queries and manage jobs on Databricks
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="databricks"
|
||||
color="#F9F7F4"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Databricks](https://www.databricks.com/) is a unified data analytics platform built on Apache Spark, providing a collaborative environment for data engineering, data science, and machine learning. Databricks combines data warehousing, ETL, and AI workloads into a single lakehouse architecture, with support for SQL analytics, job orchestration, and cluster management across major cloud providers.
|
||||
|
||||
With the Databricks integration in Sim, you can:
|
||||
|
||||
- **Execute SQL queries**: Run SQL statements against Databricks SQL warehouses with support for parameterized queries and Unity Catalog
|
||||
- **Manage jobs**: List, trigger, and monitor Databricks job runs programmatically
|
||||
- **Track run status**: Get detailed run information including timing, state, and output results
|
||||
- **Control clusters**: List and inspect cluster configurations, states, and resource details
|
||||
- **Retrieve run outputs**: Access notebook results, error messages, and logs from completed job runs
|
||||
|
||||
In Sim, the Databricks integration enables your agents to interact with your data lakehouse as part of automated workflows. Agents can query large-scale datasets, orchestrate ETL pipelines by triggering jobs, monitor job execution, and retrieve results—all without leaving the workflow canvas. This is ideal for automated reporting, data pipeline management, scheduled analytics, and building AI-driven data workflows that react to query results or job outcomes.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Connect to Databricks to execute SQL queries against SQL warehouses, trigger and monitor job runs, manage clusters, and retrieve run outputs. Requires a Personal Access Token and workspace host URL.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `databricks_execute_sql`
|
||||
|
||||
Execute a SQL statement against a Databricks SQL warehouse and return results inline. Supports parameterized queries and Unity Catalog.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) |
|
||||
| `apiKey` | string | Yes | Databricks Personal Access Token |
|
||||
| `warehouseId` | string | Yes | The ID of the SQL warehouse to execute against |
|
||||
| `statement` | string | Yes | The SQL statement to execute \(max 16 MiB\) |
|
||||
| `catalog` | string | No | Unity Catalog name \(equivalent to USE CATALOG\) |
|
||||
| `schema` | string | No | Schema name \(equivalent to USE SCHEMA\) |
|
||||
| `rowLimit` | number | No | Maximum number of rows to return |
|
||||
| `waitTimeout` | string | No | How long to wait for results \(e.g., "50s"\). Range: "0s" or "5s" to "50s". Default: "50s" |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `statementId` | string | Unique identifier for the executed statement |
|
||||
| `status` | string | Execution status \(SUCCEEDED, PENDING, RUNNING, FAILED, CANCELED, CLOSED\) |
|
||||
| `columns` | array | Column schema of the result set |
|
||||
| ↳ `name` | string | Column name |
|
||||
| ↳ `position` | number | Column position \(0-based\) |
|
||||
| ↳ `typeName` | string | Column type \(STRING, INT, LONG, DOUBLE, BOOLEAN, TIMESTAMP, DATE, DECIMAL, etc.\) |
|
||||
| `data` | array | Result rows as a 2D array of strings where each inner array is a row of column values |
|
||||
| `totalRows` | number | Total number of rows in the result |
|
||||
| `truncated` | boolean | Whether the result set was truncated due to row_limit or byte_limit |
|
||||
|
||||
### `databricks_list_jobs`
|
||||
|
||||
List all jobs in a Databricks workspace with optional filtering by name.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) |
|
||||
| `apiKey` | string | Yes | Databricks Personal Access Token |
|
||||
| `limit` | number | No | Maximum number of jobs to return \(range 1-100, default 20\) |
|
||||
| `offset` | number | No | Offset for pagination |
|
||||
| `name` | string | No | Filter jobs by exact name \(case-insensitive\) |
|
||||
| `expandTasks` | boolean | No | Include task and cluster details in the response \(max 100 elements\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `jobs` | array | List of jobs in the workspace |
|
||||
| ↳ `jobId` | number | Unique job identifier |
|
||||
| ↳ `name` | string | Job name |
|
||||
| ↳ `createdTime` | number | Job creation timestamp \(epoch ms\) |
|
||||
| ↳ `creatorUserName` | string | Email of the job creator |
|
||||
| ↳ `maxConcurrentRuns` | number | Maximum number of concurrent runs |
|
||||
| ↳ `format` | string | Job format \(SINGLE_TASK or MULTI_TASK\) |
|
||||
| `hasMore` | boolean | Whether more jobs are available for pagination |
|
||||
| `nextPageToken` | string | Token for fetching the next page of results |
|
||||
|
||||
### `databricks_run_job`
|
||||
|
||||
Trigger an existing Databricks job to run immediately with optional job-level or notebook parameters.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) |
|
||||
| `apiKey` | string | Yes | Databricks Personal Access Token |
|
||||
| `jobId` | number | Yes | The ID of the job to trigger |
|
||||
| `jobParameters` | string | No | Job-level parameter overrides as a JSON object \(e.g., \{"key": "value"\}\) |
|
||||
| `notebookParams` | string | No | Notebook task parameters as a JSON object \(e.g., \{"param1": "value1"\}\) |
|
||||
| `idempotencyToken` | string | No | Idempotency token to prevent duplicate runs \(max 64 characters\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `runId` | number | The globally unique ID of the triggered run |
|
||||
| `numberInJob` | number | The sequence number of this run among all runs of the job |
|
||||
|
||||
### `databricks_get_run`
|
||||
|
||||
Get the status, timing, and details of a Databricks job run by its run ID.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) |
|
||||
| `apiKey` | string | Yes | Databricks Personal Access Token |
|
||||
| `runId` | number | Yes | The canonical identifier of the run |
|
||||
| `includeHistory` | boolean | No | Include repair history in the response |
|
||||
| `includeResolvedValues` | boolean | No | Include resolved parameter values in the response |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `runId` | number | The run ID |
|
||||
| `jobId` | number | The job ID this run belongs to |
|
||||
| `runName` | string | Name of the run |
|
||||
| `runType` | string | Type of run \(JOB_RUN, WORKFLOW_RUN, SUBMIT_RUN\) |
|
||||
| `attemptNumber` | number | Retry attempt number \(0 for initial attempt\) |
|
||||
| `state` | object | Run state information |
|
||||
| ↳ `lifeCycleState` | string | Lifecycle state \(QUEUED, PENDING, RUNNING, TERMINATING, TERMINATED, SKIPPED, INTERNAL_ERROR, BLOCKED, WAITING_FOR_RETRY\) |
|
||||
| ↳ `resultState` | string | Result state \(SUCCESS, FAILED, TIMEDOUT, CANCELED, SUCCESS_WITH_FAILURES, UPSTREAM_FAILED, UPSTREAM_CANCELED, EXCLUDED\) |
|
||||
| ↳ `stateMessage` | string | Descriptive message for the current state |
|
||||
| ↳ `userCancelledOrTimedout` | boolean | Whether the run was cancelled by user or timed out |
|
||||
| `startTime` | number | Run start timestamp \(epoch ms\) |
|
||||
| `endTime` | number | Run end timestamp \(epoch ms, 0 if still running\) |
|
||||
| `setupDuration` | number | Cluster setup duration \(ms\) |
|
||||
| `executionDuration` | number | Execution duration \(ms\) |
|
||||
| `cleanupDuration` | number | Cleanup duration \(ms\) |
|
||||
| `queueDuration` | number | Time spent in queue before execution \(ms\) |
|
||||
| `runPageUrl` | string | URL to the run detail page in Databricks UI |
|
||||
| `creatorUserName` | string | Email of the user who triggered the run |
|
||||
|
||||
### `databricks_list_runs`
|
||||
|
||||
List job runs in a Databricks workspace with optional filtering by job, status, and time range.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) |
|
||||
| `apiKey` | string | Yes | Databricks Personal Access Token |
|
||||
| `jobId` | number | No | Filter runs by job ID. Omit to list runs across all jobs |
|
||||
| `activeOnly` | boolean | No | Only include active runs \(PENDING, RUNNING, or TERMINATING\) |
|
||||
| `completedOnly` | boolean | No | Only include completed runs |
|
||||
| `limit` | number | No | Maximum number of runs to return \(range 1-24, default 20\) |
|
||||
| `offset` | number | No | Offset for pagination |
|
||||
| `runType` | string | No | Filter by run type \(JOB_RUN, WORKFLOW_RUN, SUBMIT_RUN\) |
|
||||
| `startTimeFrom` | number | No | Filter runs started at or after this timestamp \(epoch ms\) |
|
||||
| `startTimeTo` | number | No | Filter runs started at or before this timestamp \(epoch ms\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `runs` | array | List of job runs |
|
||||
| ↳ `runId` | number | Unique run identifier |
|
||||
| ↳ `jobId` | number | Job this run belongs to |
|
||||
| ↳ `runName` | string | Run name |
|
||||
| ↳ `runType` | string | Run type \(JOB_RUN, WORKFLOW_RUN, SUBMIT_RUN\) |
|
||||
| ↳ `state` | object | Run state information |
|
||||
| ↳ `lifeCycleState` | string | Lifecycle state \(QUEUED, PENDING, RUNNING, TERMINATING, TERMINATED, SKIPPED, INTERNAL_ERROR, BLOCKED, WAITING_FOR_RETRY\) |
|
||||
| ↳ `resultState` | string | Result state \(SUCCESS, FAILED, TIMEDOUT, CANCELED, SUCCESS_WITH_FAILURES, UPSTREAM_FAILED, UPSTREAM_CANCELED, EXCLUDED\) |
|
||||
| ↳ `stateMessage` | string | Descriptive state message |
|
||||
| ↳ `userCancelledOrTimedout` | boolean | Whether the run was cancelled by user or timed out |
|
||||
| ↳ `startTime` | number | Run start timestamp \(epoch ms\) |
|
||||
| ↳ `endTime` | number | Run end timestamp \(epoch ms\) |
|
||||
| `hasMore` | boolean | Whether more runs are available for pagination |
|
||||
| `nextPageToken` | string | Token for fetching the next page of results |
|
||||
|
||||
### `databricks_cancel_run`
|
||||
|
||||
Cancel a running or pending Databricks job run. Cancellation is asynchronous; poll the run status to confirm termination.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) |
|
||||
| `apiKey` | string | Yes | Databricks Personal Access Token |
|
||||
| `runId` | number | Yes | The canonical identifier of the run to cancel |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether the cancel request was accepted |
|
||||
|
||||
### `databricks_get_run_output`
|
||||
|
||||
Get the output of a completed Databricks job run, including notebook results, error messages, and logs. For multi-task jobs, use the task run ID (not the parent run ID).
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) |
|
||||
| `apiKey` | string | Yes | Databricks Personal Access Token |
|
||||
| `runId` | number | Yes | The run ID to get output for. For multi-task jobs, use the task run ID |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `notebookOutput` | object | Notebook task output \(from dbutils.notebook.exit\(\)\) |
|
||||
| ↳ `result` | string | Value passed to dbutils.notebook.exit\(\) \(max 5 MB\) |
|
||||
| ↳ `truncated` | boolean | Whether the result was truncated |
|
||||
| `error` | string | Error message if the run failed or output is unavailable |
|
||||
| `errorTrace` | string | Error stack trace if available |
|
||||
| `logs` | string | Log output \(last 5 MB\) from spark_jar, spark_python, or python_wheel tasks |
|
||||
| `logsTruncated` | boolean | Whether the log output was truncated |
|
||||
|
||||
### `databricks_list_clusters`
|
||||
|
||||
List all clusters in a Databricks workspace including their state, configuration, and resource details.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) |
|
||||
| `apiKey` | string | Yes | Databricks Personal Access Token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `clusters` | array | List of clusters in the workspace |
|
||||
| ↳ `clusterId` | string | Unique cluster identifier |
|
||||
| ↳ `clusterName` | string | Cluster display name |
|
||||
| ↳ `state` | string | Current state \(PENDING, RUNNING, RESTARTING, RESIZING, TERMINATING, TERMINATED, ERROR, UNKNOWN\) |
|
||||
| ↳ `stateMessage` | string | Human-readable state description |
|
||||
| ↳ `creatorUserName` | string | Email of the cluster creator |
|
||||
| ↳ `sparkVersion` | string | Spark runtime version \(e.g., 13.3.x-scala2.12\) |
|
||||
| ↳ `nodeTypeId` | string | Worker node type identifier |
|
||||
| ↳ `driverNodeTypeId` | string | Driver node type identifier |
|
||||
| ↳ `numWorkers` | number | Number of worker nodes \(for fixed-size clusters\) |
|
||||
| ↳ `autoscale` | object | Autoscaling configuration \(null for fixed-size clusters\) |
|
||||
| ↳ `minWorkers` | number | Minimum number of workers |
|
||||
| ↳ `maxWorkers` | number | Maximum number of workers |
|
||||
| ↳ `clusterSource` | string | Origin \(API, UI, JOB, MODELS, PIPELINE, PIPELINE_MAINTENANCE, SQL\) |
|
||||
| ↳ `autoterminationMinutes` | number | Minutes of inactivity before auto-termination \(0 = disabled\) |
|
||||
| ↳ `startTime` | number | Cluster start timestamp \(epoch ms\) |
|
||||
|
||||
|
||||
157
apps/docs/content/docs/en/tools/devin.mdx
Normal file
157
apps/docs/content/docs/en/tools/devin.mdx
Normal file
@@ -0,0 +1,157 @@
|
||||
---
|
||||
title: Devin
|
||||
description: Autonomous AI software engineer
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="devin"
|
||||
color="#12141A"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Devin](https://devin.ai/) is an autonomous AI software engineer by Cognition that can independently write, run, debug, and deploy code.
|
||||
|
||||
With Devin, you can:
|
||||
|
||||
- **Automate coding tasks**: Assign software engineering tasks and let Devin autonomously write, test, and iterate on code
|
||||
- **Manage sessions**: Create, monitor, and interact with Devin sessions to track progress on assigned tasks
|
||||
- **Guide active work**: Send messages to running sessions to provide additional context, redirect efforts, or answer questions
|
||||
- **Retrieve structured output**: Poll completed sessions for pull requests, structured results, and detailed status
|
||||
- **Control costs**: Set ACU (Autonomous Compute Unit) limits to cap spending on long-running tasks
|
||||
- **Standardize workflows**: Use playbook IDs to apply repeatable task patterns across sessions
|
||||
|
||||
In Sim, the Devin integration enables your agents to programmatically manage Devin sessions as part of their workflows:
|
||||
|
||||
- **Create sessions**: Kick off new Devin sessions with a prompt describing the task, optional playbook, ACU limits, and tags
|
||||
- **Get session details**: Retrieve the full state of a session including status, pull requests, structured output, and resource consumption
|
||||
- **List sessions**: Query all sessions in your organization with optional pagination
|
||||
- **Send messages**: Communicate with active or suspended sessions to provide guidance, and automatically resume suspended sessions
|
||||
|
||||
This allows for powerful automation scenarios such as triggering code generation from upstream events, polling for completion before consuming results, orchestrating multi-step development pipelines, and integrating Devin's output into broader agent workflows.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Devin into your workflow. Create sessions to assign coding tasks, send messages to guide active sessions, and retrieve session status and results. Devin autonomously writes, runs, and tests code.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `devin_create_session`
|
||||
|
||||
Create a new Devin session with a prompt. Devin will autonomously work on the task described in the prompt.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Devin API key \(service user credential starting with cog_\) |
|
||||
| `prompt` | string | Yes | The task prompt for Devin to work on |
|
||||
| `playbookId` | string | No | Optional playbook ID to guide the session |
|
||||
| `maxAcuLimit` | number | No | Maximum ACU limit for the session |
|
||||
| `tags` | string | No | Comma-separated tags for the session |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `sessionId` | string | Unique identifier for the session |
|
||||
| `url` | string | URL to view the session in the Devin UI |
|
||||
| `status` | string | Session status \(new, claimed, running, exit, error, suspended, resuming\) |
|
||||
| `statusDetail` | string | Detailed status \(working, waiting_for_user, waiting_for_approval, finished, inactivity, etc.\) |
|
||||
| `title` | string | Session title |
|
||||
| `createdAt` | number | Unix timestamp when the session was created |
|
||||
| `updatedAt` | number | Unix timestamp when the session was last updated |
|
||||
| `acusConsumed` | number | ACUs consumed by the session |
|
||||
| `tags` | json | Tags associated with the session |
|
||||
| `pullRequests` | json | Pull requests created during the session |
|
||||
| `structuredOutput` | json | Structured output from the session |
|
||||
| `playbookId` | string | Associated playbook ID |
|
||||
|
||||
### `devin_get_session`
|
||||
|
||||
Retrieve details of an existing Devin session including status, tags, pull requests, and structured output.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Devin API key \(service user credential starting with cog_\) |
|
||||
| `sessionId` | string | Yes | The session ID to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `sessionId` | string | Unique identifier for the session |
|
||||
| `url` | string | URL to view the session in the Devin UI |
|
||||
| `status` | string | Session status \(new, claimed, running, exit, error, suspended, resuming\) |
|
||||
| `statusDetail` | string | Detailed status \(working, waiting_for_user, waiting_for_approval, finished, inactivity, etc.\) |
|
||||
| `title` | string | Session title |
|
||||
| `createdAt` | number | Unix timestamp when the session was created |
|
||||
| `updatedAt` | number | Unix timestamp when the session was last updated |
|
||||
| `acusConsumed` | number | ACUs consumed by the session |
|
||||
| `tags` | json | Tags associated with the session |
|
||||
| `pullRequests` | json | Pull requests created during the session |
|
||||
| `structuredOutput` | json | Structured output from the session |
|
||||
| `playbookId` | string | Associated playbook ID |
|
||||
|
||||
### `devin_list_sessions`
|
||||
|
||||
List Devin sessions in the organization. Returns up to 100 sessions by default.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Devin API key \(service user credential starting with cog_\) |
|
||||
| `limit` | number | No | Maximum number of sessions to return \(1-200, default: 100\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `sessions` | array | List of Devin sessions |
|
||||
| ↳ `sessionId` | string | Unique identifier for the session |
|
||||
| ↳ `url` | string | URL to view the session |
|
||||
| ↳ `status` | string | Session status |
|
||||
| ↳ `statusDetail` | string | Detailed status |
|
||||
| ↳ `title` | string | Session title |
|
||||
| ↳ `createdAt` | number | Creation timestamp \(Unix\) |
|
||||
| ↳ `updatedAt` | number | Last updated timestamp \(Unix\) |
|
||||
| ↳ `tags` | json | Session tags |
|
||||
|
||||
### `devin_send_message`
|
||||
|
||||
Send a message to a Devin session. If the session is suspended, it will be automatically resumed. Returns the updated session state.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Devin API key \(service user credential starting with cog_\) |
|
||||
| `sessionId` | string | Yes | The session ID to send the message to |
|
||||
| `message` | string | Yes | The message to send to Devin |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `sessionId` | string | Unique identifier for the session |
|
||||
| `url` | string | URL to view the session in the Devin UI |
|
||||
| `status` | string | Session status \(new, claimed, running, exit, error, suspended, resuming\) |
|
||||
| `statusDetail` | string | Detailed status \(working, waiting_for_user, waiting_for_approval, finished, inactivity, etc.\) |
|
||||
| `title` | string | Session title |
|
||||
| `createdAt` | number | Unix timestamp when the session was created |
|
||||
| `updatedAt` | number | Unix timestamp when the session was last updated |
|
||||
| `acusConsumed` | number | ACUs consumed by the session |
|
||||
| `tags` | json | Tags associated with the session |
|
||||
| `pullRequests` | json | Pull requests created during the session |
|
||||
| `structuredOutput` | json | Structured output from the session |
|
||||
| `playbookId` | string | Associated playbook ID |
|
||||
|
||||
|
||||
165
apps/docs/content/docs/en/tools/gamma.mdx
Normal file
165
apps/docs/content/docs/en/tools/gamma.mdx
Normal file
@@ -0,0 +1,165 @@
|
||||
---
|
||||
title: Gamma
|
||||
description: Generate presentations, documents, and webpages with AI
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="gamma"
|
||||
color="#002253"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Gamma](https://gamma.app/) is an AI-powered platform for creating presentations, documents, webpages, and social posts. Gamma's API lets you programmatically generate polished, visually rich content from text prompts, adapt existing templates, and manage workspace assets like themes and folders.
|
||||
|
||||
With Gamma, you can:
|
||||
|
||||
- **Generate presentations and documents:** Create slide decks, documents, webpages, and social posts from text input with full control over format, tone, and image sourcing.
|
||||
- **Create from templates:** Adapt existing Gamma templates with custom prompts to quickly produce tailored content.
|
||||
- **Check generation status:** Poll for completion of async generation jobs and retrieve the final Gamma URL.
|
||||
- **Browse themes and folders:** List available workspace themes and folders to organize and style your generated content.
|
||||
|
||||
In Sim, the Gamma integration enables your agents to automatically generate presentations and documents, create content from templates, and manage workspace assets directly within your workflows. This allows you to automate content creation pipelines, batch-produce slide decks, and integrate AI-generated presentations into broader business automation scenarios.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Gamma into the workflow. Can generate presentations, documents, webpages, and social posts from text, create from templates, check generation status, and browse themes and folders.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `gamma_generate`
|
||||
|
||||
Generate a new Gamma presentation, document, webpage, or social post from text input.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Gamma API key |
|
||||
| `inputText` | string | Yes | Text and image URLs used to generate your gamma \(1-100,000 tokens\) |
|
||||
| `textMode` | string | Yes | How to handle input text: generate \(AI expands\), condense \(AI summarizes\), or preserve \(keep as-is\) |
|
||||
| `format` | string | No | Output format: presentation, document, webpage, or social \(default: presentation\) |
|
||||
| `themeId` | string | No | Custom Gamma workspace theme ID \(use List Themes to find available themes\) |
|
||||
| `numCards` | number | No | Number of cards/slides to generate \(1-60 for Pro, 1-75 for Ultra; default: 10\) |
|
||||
| `cardSplit` | string | No | How to split content into cards: auto or inputTextBreaks \(default: auto\) |
|
||||
| `cardDimensions` | string | No | Card aspect ratio. Presentation: fluid, 16x9, 4x3. Document: fluid, pageless, letter, a4. Social: 1x1, 4x5, 9x16 |
|
||||
| `additionalInstructions` | string | No | Additional instructions for the AI generation \(max 2000 chars\) |
|
||||
| `exportAs` | string | No | Automatically export the generated gamma as pdf or pptx |
|
||||
| `folderIds` | string | No | Comma-separated folder IDs to store the generated gamma in |
|
||||
| `textAmount` | string | No | Amount of text per card: brief, medium, detailed, or extensive |
|
||||
| `textTone` | string | No | Tone of the generated text, e.g. "professional", "casual" \(max 500 chars\) |
|
||||
| `textAudience` | string | No | Target audience for the generated text, e.g. "executives", "students" \(max 500 chars\) |
|
||||
| `textLanguage` | string | No | Language code for the generated text \(default: en\) |
|
||||
| `imageSource` | string | No | Where to source images: aiGenerated, pictographic, unsplash, webAllImages, webFreeToUse, webFreeToUseCommercially, giphy, placeholder, or noImages |
|
||||
| `imageModel` | string | No | AI image generation model to use when imageSource is aiGenerated |
|
||||
| `imageStyle` | string | No | Style directive for AI-generated images, e.g. "watercolor", "photorealistic" \(max 500 chars\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `generationId` | string | The ID of the generation job. Use with Check Status to poll for completion. |
|
||||
|
||||
### `gamma_generate_from_template`
|
||||
|
||||
Generate a new Gamma by adapting an existing template with a prompt.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Gamma API key |
|
||||
| `gammaId` | string | Yes | The ID of the template gamma to adapt |
|
||||
| `prompt` | string | Yes | Instructions for how to adapt the template \(1-100,000 tokens\) |
|
||||
| `themeId` | string | No | Custom Gamma workspace theme ID to apply |
|
||||
| `exportAs` | string | No | Automatically export the generated gamma as pdf or pptx |
|
||||
| `folderIds` | string | No | Comma-separated folder IDs to store the generated gamma in |
|
||||
| `imageModel` | string | No | AI image generation model to use when imageSource is aiGenerated |
|
||||
| `imageStyle` | string | No | Style directive for AI-generated images, e.g. "watercolor", "photorealistic" \(max 500 chars\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `generationId` | string | The ID of the generation job. Use with Check Status to poll for completion. |
|
||||
|
||||
### `gamma_check_status`
|
||||
|
||||
Check the status of a Gamma generation job. Returns the gamma URL when completed, or error details if failed.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Gamma API key |
|
||||
| `generationId` | string | Yes | The generation ID returned by the Generate or Generate from Template tool |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `generationId` | string | The generation ID that was checked |
|
||||
| `status` | string | Generation status: pending, completed, or failed |
|
||||
| `gammaUrl` | string | URL of the generated gamma \(only present when status is completed\) |
|
||||
| `credits` | object | Credit usage information \(only present when status is completed\) |
|
||||
| ↳ `deducted` | number | Number of credits deducted for this generation |
|
||||
| ↳ `remaining` | number | Remaining credits in the account |
|
||||
| `error` | object | Error details \(only present when status is failed\) |
|
||||
| ↳ `message` | string | Human-readable error message |
|
||||
| ↳ `statusCode` | number | HTTP status code of the error |
|
||||
|
||||
### `gamma_list_themes`
|
||||
|
||||
List available themes in your Gamma workspace. Returns theme IDs, names, and keywords for styling.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Gamma API key |
|
||||
| `query` | string | No | Search query to filter themes by name \(case-insensitive\) |
|
||||
| `limit` | number | No | Maximum number of themes to return per page \(max 50\) |
|
||||
| `after` | string | No | Pagination cursor from a previous response \(nextCursor\) to fetch the next page |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `themes` | array | List of available themes |
|
||||
| ↳ `id` | string | Theme ID \(use with themeId parameter\) |
|
||||
| ↳ `name` | string | Theme display name |
|
||||
| ↳ `type` | string | Theme type: standard or custom |
|
||||
| ↳ `colorKeywords` | array | Color descriptors for this theme |
|
||||
| ↳ `toneKeywords` | array | Tone descriptors for this theme |
|
||||
| `hasMore` | boolean | Whether more results are available on the next page |
|
||||
| `nextCursor` | string | Pagination cursor to pass as the after parameter for the next page |
|
||||
|
||||
### `gamma_list_folders`
|
||||
|
||||
List available folders in your Gamma workspace. Returns folder IDs and names for organizing generated content.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Gamma API key |
|
||||
| `query` | string | No | Search query to filter folders by name \(case-sensitive\) |
|
||||
| `limit` | number | No | Maximum number of folders to return per page \(max 50\) |
|
||||
| `after` | string | No | Pagination cursor from a previous response \(nextCursor\) to fetch the next page |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `folders` | array | List of available folders |
|
||||
| ↳ `id` | string | Folder ID \(use with folderIds parameter\) |
|
||||
| ↳ `name` | string | Folder display name |
|
||||
| `hasMore` | boolean | Whether more results are available on the next page |
|
||||
| `nextCursor` | string | Pagination cursor to pass as the after parameter for the next page |
|
||||
|
||||
|
||||
@@ -11,19 +11,21 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Gmail](https://mail.google.com/) is one of the world’s most popular and reliable email services, trusted by individuals and organizations to send, receive, and manage messages. Gmail offers a secure, intuitive interface with advanced organization and search capabilities, making it a top choice for personal and professional communication.
|
||||
[Gmail](https://mail.google.com/) is one of the world’s most popular email services, trusted by individuals and organizations to send, receive, and manage messages securely.
|
||||
|
||||
Gmail provides a comprehensive suite of features for efficient email management, message filtering, and workflow integration. With its powerful API, Gmail enables developers and platforms to automate common email-related tasks, integrate mailbox activities into broader workflows, and enhance productivity by reducing manual effort.
|
||||
With the Gmail integration in Sim, you can:
|
||||
|
||||
Key features of Gmail include:
|
||||
- **Send emails**: Compose and send emails with support for recipients, CC, BCC, subject, body, and attachments
|
||||
- **Create drafts**: Save email drafts for later review and sending
|
||||
- **Read emails**: Retrieve email messages by ID with full content and metadata
|
||||
- **Search emails**: Find emails using Gmail’s powerful search query syntax
|
||||
- **Move emails**: Move messages between folders or labels
|
||||
- **Manage read status**: Mark emails as read or unread
|
||||
- **Archive and unarchive**: Archive messages to clean up your inbox or restore them
|
||||
- **Delete emails**: Remove messages from your mailbox
|
||||
- **Manage labels**: Add or remove labels from emails for organization
|
||||
|
||||
- Email Sending and Receiving: Compose, send, and receive emails reliably and securely
|
||||
- Message Search and Organization: Advanced search, labels, and filters to easily find and categorize messages
|
||||
- Conversation Threading: Keeps related messages grouped together for better conversation tracking
|
||||
- Attachments and Formatting: Support for file attachments, rich formatting, and embedded media
|
||||
- Integration and Automation: Robust API for integrating with other tools and automating email workflows
|
||||
|
||||
In Sim, the Gmail integration allows your agents to interact with your emails programmatically—sending, receiving, searching, and organizing messages as part of powerful AI workflows. Agents can draft emails, trigger processes based on new email arrivals, and automate repetitive email tasks, freeing up time and reducing manual labor. By connecting Sim with Gmail, you can build intelligent agents to manage communications, automate follow-ups, and maintain organized inboxes within your workflows.
|
||||
In Sim, the Gmail integration enables your agents to interact with your inbox programmatically as part of automated workflows. Agents can send notifications, search for specific emails, organize messages, and trigger actions based on email content—enabling intelligent email automation and communication workflows.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
|
||||
175
apps/docs/content/docs/en/tools/google_bigquery.mdx
Normal file
175
apps/docs/content/docs/en/tools/google_bigquery.mdx
Normal file
@@ -0,0 +1,175 @@
|
||||
---
|
||||
title: Google BigQuery
|
||||
description: Query, list, and insert data in Google BigQuery
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="google_bigquery"
|
||||
color="#E0E0E0"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Google BigQuery](https://cloud.google.com/bigquery) is Google Cloud's fully managed, serverless data warehouse designed for large-scale data analytics. BigQuery lets you run fast SQL queries on massive datasets, making it ideal for business intelligence, data exploration, and machine learning pipelines.
|
||||
|
||||
With the Google BigQuery integration in Sim, you can:
|
||||
|
||||
- **Run SQL queries**: Execute queries against your BigQuery datasets and retrieve results for analysis or downstream processing
|
||||
- **List datasets**: Browse available datasets within a Google Cloud project
|
||||
- **List and inspect tables**: Enumerate tables within a dataset and retrieve detailed schema information
|
||||
- **Insert rows**: Stream new rows into BigQuery tables for real-time data ingestion
|
||||
|
||||
In Sim, the Google BigQuery integration enables your agents to query datasets, inspect schemas, and insert rows as part of automated workflows. This is ideal for automated reporting, data pipeline orchestration, real-time data ingestion, and analytics-driven decision making.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Connect to Google BigQuery to run SQL queries, list datasets and tables, get table metadata, and insert rows.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `google_bigquery_query`
|
||||
|
||||
Run a SQL query against Google BigQuery and return the results
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | Yes | Google Cloud project ID |
|
||||
| `query` | string | Yes | SQL query to execute |
|
||||
| `useLegacySql` | boolean | No | Whether to use legacy SQL syntax \(default: false\) |
|
||||
| `maxResults` | number | No | Maximum number of rows to return |
|
||||
| `defaultDatasetId` | string | No | Default dataset for unqualified table names |
|
||||
| `location` | string | No | Processing location \(e.g., "US", "EU"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `columns` | array | Array of column names from the query result |
|
||||
| `rows` | array | Array of row objects keyed by column name |
|
||||
| `totalRows` | string | Total number of rows in the complete result set |
|
||||
| `jobComplete` | boolean | Whether the query completed within the timeout |
|
||||
| `totalBytesProcessed` | string | Total bytes processed by the query |
|
||||
| `cacheHit` | boolean | Whether the query result was served from cache |
|
||||
| `jobReference` | object | Job reference \(useful when jobComplete is false\) |
|
||||
| ↳ `projectId` | string | Project ID containing the job |
|
||||
| ↳ `jobId` | string | Unique job identifier |
|
||||
| ↳ `location` | string | Geographic location of the job |
|
||||
| `pageToken` | string | Token for fetching additional result pages |
|
||||
|
||||
### `google_bigquery_list_datasets`
|
||||
|
||||
List all datasets in a Google BigQuery project
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | Yes | Google Cloud project ID |
|
||||
| `maxResults` | number | No | Maximum number of datasets to return |
|
||||
| `pageToken` | string | No | Token for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `datasets` | array | Array of dataset objects |
|
||||
| ↳ `datasetId` | string | Unique dataset identifier |
|
||||
| ↳ `projectId` | string | Project ID containing this dataset |
|
||||
| ↳ `friendlyName` | string | Descriptive name for the dataset |
|
||||
| ↳ `location` | string | Geographic location where the data resides |
|
||||
| `nextPageToken` | string | Token for fetching next page of results |
|
||||
|
||||
### `google_bigquery_list_tables`
|
||||
|
||||
List all tables in a Google BigQuery dataset
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | Yes | Google Cloud project ID |
|
||||
| `datasetId` | string | Yes | BigQuery dataset ID |
|
||||
| `maxResults` | number | No | Maximum number of tables to return |
|
||||
| `pageToken` | string | No | Token for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `tables` | array | Array of table objects |
|
||||
| ↳ `tableId` | string | Table identifier |
|
||||
| ↳ `datasetId` | string | Dataset ID containing this table |
|
||||
| ↳ `projectId` | string | Project ID containing this table |
|
||||
| ↳ `type` | string | Table type \(TABLE, VIEW, EXTERNAL, etc.\) |
|
||||
| ↳ `friendlyName` | string | User-friendly name for the table |
|
||||
| ↳ `creationTime` | string | Time when created, in milliseconds since epoch |
|
||||
| `totalItems` | number | Total number of tables in the dataset |
|
||||
| `nextPageToken` | string | Token for fetching next page of results |
|
||||
|
||||
### `google_bigquery_get_table`
|
||||
|
||||
Get metadata and schema for a Google BigQuery table
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | Yes | Google Cloud project ID |
|
||||
| `datasetId` | string | Yes | BigQuery dataset ID |
|
||||
| `tableId` | string | Yes | BigQuery table ID |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `tableId` | string | Table ID |
|
||||
| `datasetId` | string | Dataset ID |
|
||||
| `projectId` | string | Project ID |
|
||||
| `type` | string | Table type \(TABLE, VIEW, SNAPSHOT, MATERIALIZED_VIEW, EXTERNAL\) |
|
||||
| `description` | string | Table description |
|
||||
| `numRows` | string | Total number of rows |
|
||||
| `numBytes` | string | Total size in bytes, excluding data in streaming buffer |
|
||||
| `schema` | array | Array of column definitions |
|
||||
| ↳ `name` | string | Column name |
|
||||
| ↳ `type` | string | Data type \(STRING, INTEGER, FLOAT, BOOLEAN, TIMESTAMP, RECORD, etc.\) |
|
||||
| ↳ `mode` | string | Column mode \(NULLABLE, REQUIRED, or REPEATED\) |
|
||||
| ↳ `description` | string | Column description |
|
||||
| `creationTime` | string | Table creation time \(milliseconds since epoch\) |
|
||||
| `lastModifiedTime` | string | Last modification time \(milliseconds since epoch\) |
|
||||
| `location` | string | Geographic location where the table resides |
|
||||
|
||||
### `google_bigquery_insert_rows`
|
||||
|
||||
Insert rows into a Google BigQuery table using streaming insert
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | Yes | Google Cloud project ID |
|
||||
| `datasetId` | string | Yes | BigQuery dataset ID |
|
||||
| `tableId` | string | Yes | BigQuery table ID |
|
||||
| `rows` | string | Yes | JSON array of row objects to insert |
|
||||
| `skipInvalidRows` | boolean | No | Whether to insert valid rows even if some are invalid |
|
||||
| `ignoreUnknownValues` | boolean | No | Whether to ignore columns not in the table schema |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `insertedRows` | number | Number of rows successfully inserted |
|
||||
| `errors` | array | Array of per-row insertion errors \(empty if all succeeded\) |
|
||||
| ↳ `index` | number | Zero-based index of the row that failed |
|
||||
| ↳ `errors` | array | Error details for this row |
|
||||
| ↳ `reason` | string | Short error code summarizing the error |
|
||||
| ↳ `location` | string | Where the error occurred |
|
||||
| ↳ `message` | string | Human-readable error description |
|
||||
|
||||
|
||||
@@ -10,6 +10,18 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
color="#E0E0E0"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Google Books](https://books.google.com) is Google's comprehensive book discovery and metadata service, providing access to millions of books from publishers, libraries, and digitized collections worldwide.
|
||||
|
||||
With the Google Books integration in Sim, you can:
|
||||
|
||||
- **Search for books**: Find volumes by title, author, ISBN, or keyword across the entire Google Books catalog
|
||||
- **Retrieve volume details**: Get detailed metadata for a specific book including title, authors, description, ratings, and publication details
|
||||
|
||||
In Sim, the Google Books integration allows your agents to search for books and retrieve volume details as part of automated workflows. This enables use cases such as content research, reading list curation, bibliographic data enrichment, and knowledge gathering from published works.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Search for books using the Google Books API. Find volumes by title, author, ISBN, or keywords, and retrieve detailed information about specific books including descriptions, ratings, and publication details.
|
||||
|
||||
144
apps/docs/content/docs/en/tools/google_contacts.mdx
Normal file
144
apps/docs/content/docs/en/tools/google_contacts.mdx
Normal file
@@ -0,0 +1,144 @@
|
||||
---
|
||||
title: Google Contacts
|
||||
description: Manage Google Contacts
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="google_contacts"
|
||||
color="#E0E0E0"
|
||||
/>
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Google Contacts into the workflow. Can create, read, update, delete, list, and search contacts.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `google_contacts_create`
|
||||
|
||||
Create a new contact in Google Contacts
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `givenName` | string | Yes | First name of the contact |
|
||||
| `familyName` | string | No | Last name of the contact |
|
||||
| `email` | string | No | Email address of the contact |
|
||||
| `emailType` | string | No | Email type: home, work, or other |
|
||||
| `phone` | string | No | Phone number of the contact |
|
||||
| `phoneType` | string | No | Phone type: mobile, home, work, or other |
|
||||
| `organization` | string | No | Organization/company name |
|
||||
| `jobTitle` | string | No | Job title at the organization |
|
||||
| `notes` | string | No | Notes or biography for the contact |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `content` | string | Contact creation confirmation message |
|
||||
| `metadata` | json | Created contact metadata including resource name and details |
|
||||
|
||||
### `google_contacts_get`
|
||||
|
||||
Get a specific contact from Google Contacts
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `resourceName` | string | Yes | Resource name of the contact \(e.g., people/c1234567890\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `content` | string | Contact retrieval confirmation message |
|
||||
| `metadata` | json | Contact details including name, email, phone, and organization |
|
||||
|
||||
### `google_contacts_list`
|
||||
|
||||
List contacts from Google Contacts
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `pageSize` | number | No | Number of contacts to return \(1-1000, default 100\) |
|
||||
| `pageToken` | string | No | Page token from a previous list request for pagination |
|
||||
| `sortOrder` | string | No | Sort order for contacts |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `content` | string | Summary of found contacts count |
|
||||
| `metadata` | json | List of contacts with pagination tokens |
|
||||
|
||||
### `google_contacts_search`
|
||||
|
||||
Search contacts in Google Contacts by name, email, phone, or organization
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `query` | string | Yes | Search query to match against contact names, emails, phones, and organizations |
|
||||
| `pageSize` | number | No | Number of results to return \(default 10, max 30\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `content` | string | Summary of search results count |
|
||||
| `metadata` | json | Search results with matching contacts |
|
||||
|
||||
### `google_contacts_update`
|
||||
|
||||
Update an existing contact in Google Contacts
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `resourceName` | string | Yes | Resource name of the contact \(e.g., people/c1234567890\) |
|
||||
| `etag` | string | Yes | ETag from a previous get request \(required for concurrency control\) |
|
||||
| `givenName` | string | No | Updated first name |
|
||||
| `familyName` | string | No | Updated last name |
|
||||
| `email` | string | No | Updated email address |
|
||||
| `emailType` | string | No | Email type: home, work, or other |
|
||||
| `phone` | string | No | Updated phone number |
|
||||
| `phoneType` | string | No | Phone type: mobile, home, work, or other |
|
||||
| `organization` | string | No | Updated organization/company name |
|
||||
| `jobTitle` | string | No | Updated job title |
|
||||
| `notes` | string | No | Updated notes or biography |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `content` | string | Contact update confirmation message |
|
||||
| `metadata` | json | Updated contact metadata |
|
||||
|
||||
### `google_contacts_delete`
|
||||
|
||||
Delete a contact from Google Contacts
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `resourceName` | string | Yes | Resource name of the contact to delete \(e.g., people/c1234567890\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `content` | string | Contact deletion confirmation message |
|
||||
| `metadata` | json | Deletion details including resource name |
|
||||
|
||||
|
||||
@@ -11,9 +11,13 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Google Search](https://www.google.com) is the world's most widely used web search engine, making it easy to find information, discover new content, and answer questions in real time. With advanced search algorithms, Google Search helps you quickly locate web pages, images, news, and more using simple or complex queries.
|
||||
[Google Search](https://www.google.com) is the world's most widely used web search engine, making it easy to find information, discover new content, and answer questions in real time.
|
||||
|
||||
In Sim, the Google Search integration allows your agents to search the web and retrieve live information as part of automated workflows. This enables powerful use cases such as automated research, fact-checking, knowledge synthesis, and dynamic content discovery. By connecting Sim with Google Search, your agents can perform queries, process and analyze web results, and incorporate the latest information into their decisions—without manual effort. Enhance your workflows with always up-to-date knowledge from across the internet.
|
||||
With the Google Search integration in Sim, you can:
|
||||
|
||||
- **Search the web**: Perform queries using Google's Custom Search API and retrieve structured search results with titles, snippets, and URLs
|
||||
|
||||
In Sim, the Google Search integration allows your agents to search the web and retrieve live information as part of automated workflows. This enables use cases such as automated research, fact-checking, knowledge synthesis, and dynamic content discovery.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
|
||||
@@ -11,18 +11,20 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Google Sheets](https://www.google.com/sheets/about/) is a powerful, cloud-based spreadsheet platform that allows teams and individuals to create, edit, and collaborate on spreadsheets in real-time. Widely used for data tracking, reporting, and lightweight database needs, Google Sheets seamlessly integrates with many tools and services to empower workflow automation and data-driven operations.
|
||||
[Google Sheets](https://www.google.com/sheets/about/) is a cloud-based spreadsheet platform that allows teams and individuals to create, edit, and collaborate on spreadsheets in real-time. Widely used for data tracking, reporting, and lightweight database needs, Google Sheets integrates with many tools and services.
|
||||
|
||||
Google Sheets offers an extensive feature set for managing and analyzing tabular data, supporting everything from basic calculations to complex reporting and collaborative editing. Its robust API and integration capabilities enable automated access, updates, and reporting from agents and external services.
|
||||
With the Google Sheets integration in Sim, you can:
|
||||
|
||||
Key features of Google Sheets include:
|
||||
- **Read data**: Retrieve cell values from specific ranges in a spreadsheet
|
||||
- **Write data**: Write values to specific cell ranges
|
||||
- **Update data**: Modify existing cell values in a spreadsheet
|
||||
- **Append rows**: Add new rows of data to the end of a sheet
|
||||
- **Clear ranges**: Remove data from specific cell ranges
|
||||
- **Manage spreadsheets**: Create new spreadsheets or retrieve metadata about existing ones
|
||||
- **Batch operations**: Perform batch read, update, and clear operations across multiple ranges
|
||||
- **Copy sheets**: Duplicate sheets within or between spreadsheets
|
||||
|
||||
- Real-Time Collaboration: Multiple users can edit and view spreadsheets simultaneously from anywhere.
|
||||
- Rich Data Manipulation: Support for formulas, charts, pivots, and add-ons to analyze and visualize data.
|
||||
- Easy Data Import/Export: Ability to connect and sync data from various sources using integrations and APIs.
|
||||
- Powerful Permissions: Fine-grained sharing, access controls, and version history for team management.
|
||||
|
||||
In Sim, the Google Sheets integration empowers your agents to automate reading from, writing to, and updating specific sheets within spreadsheets. Agents can interact programmatically with Google Sheets to retrieve or modify data, manage collaborative documents, and automate reporting or record-keeping as part of your AI workflows. By connecting Sim with Google Sheets, you can build intelligent agents that manage, analyze, and update your data dynamically—streamlining operations, enhancing productivity, and ensuring up-to-date data access across your organization.
|
||||
In Sim, the Google Sheets integration enables your agents to read from, write to, and manage spreadsheets as part of automated workflows. This is ideal for automated reporting, data synchronization, record-keeping, and building data pipelines that use spreadsheets as a collaborative data layer.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
|
||||
214
apps/docs/content/docs/en/tools/google_tasks.mdx
Normal file
214
apps/docs/content/docs/en/tools/google_tasks.mdx
Normal file
@@ -0,0 +1,214 @@
|
||||
---
|
||||
title: Google Tasks
|
||||
description: Manage Google Tasks
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="google_tasks"
|
||||
color="#E0E0E0"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Google Tasks](https://support.google.com/tasks) is Google's lightweight task management service, integrated into Gmail, Google Calendar, and the standalone Google Tasks app. It provides a simple way to create, organize, and track to-do items with support for due dates, subtasks, and task lists.
|
||||
|
||||
With the Google Tasks integration in Sim, you can:
|
||||
|
||||
- **Create tasks**: Add new to-do items to any task list with titles, notes, and due dates
|
||||
- **List tasks**: Retrieve all tasks from a specific task list
|
||||
- **Get task details**: Fetch detailed information about a specific task by ID
|
||||
- **Update tasks**: Modify task titles, notes, due dates, or completion status
|
||||
- **Delete tasks**: Remove tasks from a task list
|
||||
- **List task lists**: Browse all available task lists in a Google account
|
||||
|
||||
In Sim, the Google Tasks integration allows your agents to manage to-do items programmatically as part of automated workflows. This enables use cases such as automated task creation from incoming data, deadline monitoring, and workflow-triggered task management.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Google Tasks into your workflow. Create, read, update, delete, and list tasks and task lists.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `google_tasks_create`
|
||||
|
||||
Create a new task in a Google Tasks list
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `taskListId` | string | No | Task list ID \(defaults to primary task list "@default"\) |
|
||||
| `title` | string | Yes | Title of the task \(max 1024 characters\) |
|
||||
| `notes` | string | No | Notes/description for the task \(max 8192 characters\) |
|
||||
| `due` | string | No | Due date in RFC 3339 format \(e.g., 2025-06-03T00:00:00.000Z\) |
|
||||
| `status` | string | No | Task status: "needsAction" or "completed" |
|
||||
| `parent` | string | No | Parent task ID to create this task as a subtask. Omit for top-level tasks. |
|
||||
| `previous` | string | No | Previous sibling task ID to position after. Omit to place first among siblings. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Task ID |
|
||||
| `title` | string | Task title |
|
||||
| `notes` | string | Task notes |
|
||||
| `status` | string | Task status \(needsAction or completed\) |
|
||||
| `due` | string | Due date |
|
||||
| `updated` | string | Last modification time |
|
||||
| `selfLink` | string | URL for the task |
|
||||
| `webViewLink` | string | Link to task in Google Tasks UI |
|
||||
| `parent` | string | Parent task ID |
|
||||
| `position` | string | Position among sibling tasks |
|
||||
| `completed` | string | Completion date |
|
||||
| `deleted` | boolean | Whether the task is deleted |
|
||||
|
||||
### `google_tasks_list`
|
||||
|
||||
List all tasks in a Google Tasks list
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `taskListId` | string | No | Task list ID \(defaults to primary task list "@default"\) |
|
||||
| `maxResults` | number | No | Maximum number of tasks to return \(default 20, max 100\) |
|
||||
| `pageToken` | string | No | Token for pagination |
|
||||
| `showCompleted` | boolean | No | Whether to show completed tasks \(default true\) |
|
||||
| `showDeleted` | boolean | No | Whether to show deleted tasks \(default false\) |
|
||||
| `showHidden` | boolean | No | Whether to show hidden tasks \(default false\) |
|
||||
| `dueMin` | string | No | Lower bound for due date filter \(RFC 3339 timestamp\) |
|
||||
| `dueMax` | string | No | Upper bound for due date filter \(RFC 3339 timestamp\) |
|
||||
| `completedMin` | string | No | Lower bound for task completion date \(RFC 3339 timestamp\) |
|
||||
| `completedMax` | string | No | Upper bound for task completion date \(RFC 3339 timestamp\) |
|
||||
| `updatedMin` | string | No | Lower bound for last modification time \(RFC 3339 timestamp\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `tasks` | array | List of tasks |
|
||||
| ↳ `id` | string | Task identifier |
|
||||
| ↳ `title` | string | Title of the task |
|
||||
| ↳ `notes` | string | Notes/description for the task |
|
||||
| ↳ `status` | string | Task status: "needsAction" or "completed" |
|
||||
| ↳ `due` | string | Due date \(RFC 3339 timestamp\) |
|
||||
| ↳ `completed` | string | Completion date \(RFC 3339 timestamp\) |
|
||||
| ↳ `updated` | string | Last modification time \(RFC 3339 timestamp\) |
|
||||
| ↳ `selfLink` | string | URL pointing to this task |
|
||||
| ↳ `webViewLink` | string | Link to task in Google Tasks UI |
|
||||
| ↳ `parent` | string | Parent task identifier |
|
||||
| ↳ `position` | string | Position among sibling tasks \(string-based ordering\) |
|
||||
| ↳ `hidden` | boolean | Whether the task is hidden |
|
||||
| ↳ `deleted` | boolean | Whether the task is deleted |
|
||||
| ↳ `links` | array | Collection of links associated with the task |
|
||||
| ↳ `type` | string | Link type \(e.g., "email", "generic", "chat_message"\) |
|
||||
| ↳ `description` | string | Link description |
|
||||
| ↳ `link` | string | The URL |
|
||||
| `nextPageToken` | string | Token for retrieving the next page of results |
|
||||
|
||||
### `google_tasks_get`
|
||||
|
||||
Retrieve a specific task by ID from a Google Tasks list
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `taskListId` | string | No | Task list ID \(defaults to primary task list "@default"\) |
|
||||
| `taskId` | string | Yes | The ID of the task to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Task ID |
|
||||
| `title` | string | Task title |
|
||||
| `notes` | string | Task notes |
|
||||
| `status` | string | Task status \(needsAction or completed\) |
|
||||
| `due` | string | Due date |
|
||||
| `updated` | string | Last modification time |
|
||||
| `selfLink` | string | URL for the task |
|
||||
| `webViewLink` | string | Link to task in Google Tasks UI |
|
||||
| `parent` | string | Parent task ID |
|
||||
| `position` | string | Position among sibling tasks |
|
||||
| `completed` | string | Completion date |
|
||||
| `deleted` | boolean | Whether the task is deleted |
|
||||
|
||||
### `google_tasks_update`
|
||||
|
||||
Update an existing task in a Google Tasks list
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `taskListId` | string | No | Task list ID \(defaults to primary task list "@default"\) |
|
||||
| `taskId` | string | Yes | The ID of the task to update |
|
||||
| `title` | string | No | New title for the task |
|
||||
| `notes` | string | No | New notes for the task |
|
||||
| `due` | string | No | New due date in RFC 3339 format |
|
||||
| `status` | string | No | New status: "needsAction" or "completed" |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Task ID |
|
||||
| `title` | string | Task title |
|
||||
| `notes` | string | Task notes |
|
||||
| `status` | string | Task status \(needsAction or completed\) |
|
||||
| `due` | string | Due date |
|
||||
| `updated` | string | Last modification time |
|
||||
| `selfLink` | string | URL for the task |
|
||||
| `webViewLink` | string | Link to task in Google Tasks UI |
|
||||
| `parent` | string | Parent task ID |
|
||||
| `position` | string | Position among sibling tasks |
|
||||
| `completed` | string | Completion date |
|
||||
| `deleted` | boolean | Whether the task is deleted |
|
||||
|
||||
### `google_tasks_delete`
|
||||
|
||||
Delete a task from a Google Tasks list
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `taskListId` | string | No | Task list ID \(defaults to primary task list "@default"\) |
|
||||
| `taskId` | string | Yes | The ID of the task to delete |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `taskId` | string | Deleted task ID |
|
||||
| `deleted` | boolean | Whether deletion was successful |
|
||||
|
||||
### `google_tasks_list_task_lists`
|
||||
|
||||
Retrieve all task lists for the authenticated user
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `maxResults` | number | No | Maximum number of task lists to return \(default 20, max 100\) |
|
||||
| `pageToken` | string | No | Token for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `taskLists` | array | List of task lists |
|
||||
| ↳ `id` | string | Task list identifier |
|
||||
| ↳ `title` | string | Title of the task list |
|
||||
| ↳ `updated` | string | Last modification time \(RFC 3339 timestamp\) |
|
||||
| ↳ `selfLink` | string | URL pointing to this task list |
|
||||
| `nextPageToken` | string | Token for retrieving the next page of results |
|
||||
|
||||
|
||||
@@ -10,6 +10,18 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
color="#E0E0E0"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Google Translate](https://translate.google.com/) is Google's powerful translation service, supporting over 100 languages for text, documents, and websites. Backed by advanced neural machine translation, Google Translate delivers fast and accurate translations for a wide range of use cases.
|
||||
|
||||
With the Google Translate integration in Sim, you can:
|
||||
|
||||
- **Translate text**: Convert text between over 100 languages using Google Cloud Translation
|
||||
- **Detect languages**: Automatically identify the language of a given text input
|
||||
|
||||
In Sim, the Google Translate integration allows your agents to translate text and detect languages as part of automated workflows. This enables use cases such as localization, multilingual support, content translation, and language detection at scale.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Translate and detect languages using the Google Cloud Translation API. Supports auto-detection of the source language.
|
||||
|
||||
506
apps/docs/content/docs/en/tools/greenhouse.mdx
Normal file
506
apps/docs/content/docs/en/tools/greenhouse.mdx
Normal file
@@ -0,0 +1,506 @@
|
||||
---
|
||||
title: Greenhouse
|
||||
description: Manage candidates, jobs, and applications in Greenhouse
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="greenhouse"
|
||||
color="#469776"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Greenhouse](https://www.greenhouse.com/) is a leading applicant tracking system (ATS) and hiring platform designed to help companies optimize their recruiting processes. Greenhouse provides structured hiring workflows, candidate management, interview scheduling, and analytics to help organizations make better hiring decisions at scale.
|
||||
|
||||
With the Greenhouse integration in Sim, you can:
|
||||
|
||||
- **Manage candidates**: List and retrieve detailed candidate profiles including contact information, tags, and application history
|
||||
- **Track jobs**: List and view job postings with details on hiring teams, openings, and confidentiality settings
|
||||
- **Monitor applications**: List and retrieve applications with status, source, and interview stage information
|
||||
- **Access user data**: List and look up Greenhouse users including recruiters, coordinators, and hiring managers
|
||||
- **Browse organizational data**: List departments, offices, and job stages to understand your hiring pipeline structure
|
||||
|
||||
In Sim, the Greenhouse integration enables your agents to interact with your recruiting data as part of automated workflows. Agents can pull candidate information, monitor application pipelines, track job openings, and cross-reference hiring team data—all programmatically. This is ideal for building automated recruiting reports, candidate pipeline monitoring, hiring analytics dashboards, and workflows that react to changes in your talent pipeline.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Greenhouse into the workflow. List and retrieve candidates, jobs, applications, users, departments, offices, and job stages from your Greenhouse ATS account.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `greenhouse_list_candidates`
|
||||
|
||||
Lists candidates from Greenhouse with optional filtering by date, job, or email
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Greenhouse Harvest API key |
|
||||
| `per_page` | number | No | Number of results per page \(1-500, default 100\) |
|
||||
| `page` | number | No | Page number for pagination |
|
||||
| `created_after` | string | No | Return only candidates created at or after this ISO 8601 timestamp |
|
||||
| `created_before` | string | No | Return only candidates created before this ISO 8601 timestamp |
|
||||
| `updated_after` | string | No | Return only candidates updated at or after this ISO 8601 timestamp |
|
||||
| `updated_before` | string | No | Return only candidates updated before this ISO 8601 timestamp |
|
||||
| `job_id` | string | No | Filter to candidates who applied to this job ID \(excludes prospects\) |
|
||||
| `email` | string | No | Filter to candidates with this email address |
|
||||
| `candidate_ids` | string | No | Comma-separated candidate IDs to retrieve \(max 50\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `candidates` | array | List of candidates |
|
||||
| ↳ `id` | number | Candidate ID |
|
||||
| ↳ `first_name` | string | First name |
|
||||
| ↳ `last_name` | string | Last name |
|
||||
| ↳ `company` | string | Current employer |
|
||||
| ↳ `title` | string | Current job title |
|
||||
| ↳ `is_private` | boolean | Whether candidate is private |
|
||||
| ↳ `can_email` | boolean | Whether candidate can be emailed |
|
||||
| ↳ `email_addresses` | array | Email addresses |
|
||||
| ↳ `value` | string | Email address |
|
||||
| ↳ `type` | string | Email type \(personal, work, other\) |
|
||||
| ↳ `tags` | array | Candidate tags |
|
||||
| ↳ `application_ids` | array | Associated application IDs |
|
||||
| ↳ `created_at` | string | Creation timestamp \(ISO 8601\) |
|
||||
| ↳ `updated_at` | string | Last updated timestamp \(ISO 8601\) |
|
||||
| ↳ `last_activity` | string | Last activity timestamp \(ISO 8601\) |
|
||||
| `count` | number | Number of candidates returned |
|
||||
|
||||
### `greenhouse_get_candidate`
|
||||
|
||||
Retrieves a specific candidate by ID with full details including contact info, education, and employment history
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Greenhouse Harvest API key |
|
||||
| `candidateId` | string | Yes | The ID of the candidate to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | number | Candidate ID |
|
||||
| `first_name` | string | First name |
|
||||
| `last_name` | string | Last name |
|
||||
| `company` | string | Current employer |
|
||||
| `title` | string | Current job title |
|
||||
| `is_private` | boolean | Whether candidate is private |
|
||||
| `can_email` | boolean | Whether candidate can be emailed |
|
||||
| `created_at` | string | Creation timestamp \(ISO 8601\) |
|
||||
| `updated_at` | string | Last updated timestamp \(ISO 8601\) |
|
||||
| `last_activity` | string | Last activity timestamp \(ISO 8601\) |
|
||||
| `email_addresses` | array | Email addresses |
|
||||
| ↳ `value` | string | Email address |
|
||||
| ↳ `type` | string | Type \(personal, work, other\) |
|
||||
| `phone_numbers` | array | Phone numbers |
|
||||
| ↳ `value` | string | Phone number |
|
||||
| ↳ `type` | string | Type \(home, work, mobile, skype, other\) |
|
||||
| `addresses` | array | Addresses |
|
||||
| ↳ `value` | string | Address |
|
||||
| ↳ `type` | string | Type \(home, work, other\) |
|
||||
| `website_addresses` | array | Website addresses |
|
||||
| ↳ `value` | string | URL |
|
||||
| ↳ `type` | string | Type \(personal, company, portfolio, blog, other\) |
|
||||
| `social_media_addresses` | array | Social media profiles |
|
||||
| ↳ `value` | string | URL or handle |
|
||||
| `tags` | array | Tags |
|
||||
| `application_ids` | array | Associated application IDs |
|
||||
| `recruiter` | object | Assigned recruiter |
|
||||
| ↳ `id` | number | User ID |
|
||||
| ↳ `first_name` | string | First name |
|
||||
| ↳ `last_name` | string | Last name |
|
||||
| ↳ `name` | string | Full name |
|
||||
| ↳ `employee_id` | string | Employee ID |
|
||||
| `coordinator` | object | Assigned coordinator |
|
||||
| ↳ `id` | number | User ID |
|
||||
| ↳ `first_name` | string | First name |
|
||||
| ↳ `last_name` | string | Last name |
|
||||
| ↳ `name` | string | Full name |
|
||||
| ↳ `employee_id` | string | Employee ID |
|
||||
| `attachments` | array | File attachments \(URLs expire after 7 days\) |
|
||||
| ↳ `filename` | string | File name |
|
||||
| ↳ `url` | string | Download URL \(expires after 7 days\) |
|
||||
| ↳ `type` | string | Type \(resume, cover_letter, offer_packet, other\) |
|
||||
| ↳ `created_at` | string | Upload timestamp |
|
||||
| `educations` | array | Education history |
|
||||
| ↳ `id` | number | Education record ID |
|
||||
| ↳ `school_name` | string | School name |
|
||||
| ↳ `degree` | string | Degree type |
|
||||
| ↳ `discipline` | string | Field of study |
|
||||
| ↳ `start_date` | string | Start date \(ISO 8601\) |
|
||||
| ↳ `end_date` | string | End date \(ISO 8601\) |
|
||||
| `employments` | array | Employment history |
|
||||
| ↳ `id` | number | Employment record ID |
|
||||
| ↳ `company_name` | string | Company name |
|
||||
| ↳ `title` | string | Job title |
|
||||
| ↳ `start_date` | string | Start date \(ISO 8601\) |
|
||||
| ↳ `end_date` | string | End date \(ISO 8601\) |
|
||||
| `custom_fields` | object | Custom field values |
|
||||
|
||||
### `greenhouse_list_jobs`
|
||||
|
||||
Lists jobs from Greenhouse with optional filtering by status, department, or office
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Greenhouse Harvest API key |
|
||||
| `per_page` | number | No | Number of results per page \(1-500, default 100\) |
|
||||
| `page` | number | No | Page number for pagination |
|
||||
| `status` | string | No | Filter by job status \(open, closed, draft\) |
|
||||
| `created_after` | string | No | Return only jobs created at or after this ISO 8601 timestamp |
|
||||
| `created_before` | string | No | Return only jobs created before this ISO 8601 timestamp |
|
||||
| `updated_after` | string | No | Return only jobs updated at or after this ISO 8601 timestamp |
|
||||
| `updated_before` | string | No | Return only jobs updated before this ISO 8601 timestamp |
|
||||
| `department_id` | string | No | Filter to jobs in this department ID |
|
||||
| `office_id` | string | No | Filter to jobs in this office ID |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `jobs` | array | List of jobs |
|
||||
| ↳ `id` | number | Job ID |
|
||||
| ↳ `name` | string | Job title |
|
||||
| ↳ `status` | string | Job status \(open, closed, draft\) |
|
||||
| ↳ `confidential` | boolean | Whether the job is confidential |
|
||||
| ↳ `departments` | array | Associated departments |
|
||||
| ↳ `id` | number | Department ID |
|
||||
| ↳ `name` | string | Department name |
|
||||
| ↳ `offices` | array | Associated offices |
|
||||
| ↳ `id` | number | Office ID |
|
||||
| ↳ `name` | string | Office name |
|
||||
| ↳ `opened_at` | string | Date job was opened \(ISO 8601\) |
|
||||
| ↳ `closed_at` | string | Date job was closed \(ISO 8601\) |
|
||||
| ↳ `created_at` | string | Creation timestamp \(ISO 8601\) |
|
||||
| ↳ `updated_at` | string | Last updated timestamp \(ISO 8601\) |
|
||||
| `count` | number | Number of jobs returned |
|
||||
|
||||
### `greenhouse_get_job`
|
||||
|
||||
Retrieves a specific job by ID with full details including hiring team, openings, and custom fields
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Greenhouse Harvest API key |
|
||||
| `jobId` | string | Yes | The ID of the job to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | number | Job ID |
|
||||
| `name` | string | Job title |
|
||||
| `requisition_id` | string | External requisition ID |
|
||||
| `status` | string | Job status \(open, closed, draft\) |
|
||||
| `confidential` | boolean | Whether the job is confidential |
|
||||
| `created_at` | string | Creation timestamp \(ISO 8601\) |
|
||||
| `opened_at` | string | Date job was opened \(ISO 8601\) |
|
||||
| `closed_at` | string | Date job was closed \(ISO 8601\) |
|
||||
| `updated_at` | string | Last updated timestamp \(ISO 8601\) |
|
||||
| `is_template` | boolean | Whether this is a job template |
|
||||
| `notes` | string | Hiring plan notes \(may contain HTML\) |
|
||||
| `departments` | array | Associated departments |
|
||||
| ↳ `id` | number | Department ID |
|
||||
| ↳ `name` | string | Department name |
|
||||
| ↳ `parent_id` | number | Parent department ID |
|
||||
| `offices` | array | Associated offices |
|
||||
| ↳ `id` | number | Office ID |
|
||||
| ↳ `name` | string | Office name |
|
||||
| ↳ `location` | object | Office location |
|
||||
| ↳ `name` | string | Location name |
|
||||
| `hiring_team` | object | Hiring team members |
|
||||
| ↳ `hiring_managers` | array | Hiring managers |
|
||||
| ↳ `recruiters` | array | Recruiters \(includes responsible flag\) |
|
||||
| ↳ `coordinators` | array | Coordinators \(includes responsible flag\) |
|
||||
| ↳ `sourcers` | array | Sourcers |
|
||||
| `openings` | array | Job openings/slots |
|
||||
| ↳ `id` | number | Opening internal ID |
|
||||
| ↳ `opening_id` | string | Custom opening identifier |
|
||||
| ↳ `status` | string | Opening status \(open, closed\) |
|
||||
| ↳ `opened_at` | string | Date opened \(ISO 8601\) |
|
||||
| ↳ `closed_at` | string | Date closed \(ISO 8601\) |
|
||||
| ↳ `application_id` | number | Hired application ID |
|
||||
| ↳ `close_reason` | object | Reason for closing |
|
||||
| ↳ `id` | number | Close reason ID |
|
||||
| ↳ `name` | string | Close reason name |
|
||||
| `custom_fields` | object | Custom field values |
|
||||
|
||||
### `greenhouse_list_applications`
|
||||
|
||||
Lists applications from Greenhouse with optional filtering by job, status, or date
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Greenhouse Harvest API key |
|
||||
| `per_page` | number | No | Number of results per page \(1-500, default 100\) |
|
||||
| `page` | number | No | Page number for pagination |
|
||||
| `job_id` | string | No | Filter applications by job ID |
|
||||
| `status` | string | No | Filter by status \(active, converted, hired, rejected\) |
|
||||
| `created_after` | string | No | Return only applications created at or after this ISO 8601 timestamp |
|
||||
| `created_before` | string | No | Return only applications created before this ISO 8601 timestamp |
|
||||
| `last_activity_after` | string | No | Return only applications with activity at or after this ISO 8601 timestamp |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `applications` | array | List of applications |
|
||||
| ↳ `id` | number | Application ID |
|
||||
| ↳ `candidate_id` | number | Associated candidate ID |
|
||||
| ↳ `prospect` | boolean | Whether this is a prospect application |
|
||||
| ↳ `status` | string | Status \(active, converted, hired, rejected\) |
|
||||
| ↳ `current_stage` | object | Current interview stage |
|
||||
| ↳ `id` | number | Stage ID |
|
||||
| ↳ `name` | string | Stage name |
|
||||
| ↳ `jobs` | array | Associated jobs |
|
||||
| ↳ `id` | number | Job ID |
|
||||
| ↳ `name` | string | Job name |
|
||||
| ↳ `applied_at` | string | Application date \(ISO 8601\) |
|
||||
| ↳ `rejected_at` | string | Rejection date \(ISO 8601\) |
|
||||
| ↳ `last_activity_at` | string | Last activity date \(ISO 8601\) |
|
||||
| `count` | number | Number of applications returned |
|
||||
|
||||
### `greenhouse_get_application`
|
||||
|
||||
Retrieves a specific application by ID with full details including source, stage, answers, and attachments
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Greenhouse Harvest API key |
|
||||
| `applicationId` | string | Yes | The ID of the application to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | number | Application ID |
|
||||
| `candidate_id` | number | Associated candidate ID |
|
||||
| `prospect` | boolean | Whether this is a prospect application |
|
||||
| `status` | string | Status \(active, converted, hired, rejected\) |
|
||||
| `applied_at` | string | Application date \(ISO 8601\) |
|
||||
| `rejected_at` | string | Rejection date \(ISO 8601\) |
|
||||
| `last_activity_at` | string | Last activity date \(ISO 8601\) |
|
||||
| `location` | object | Candidate location |
|
||||
| ↳ `address` | string | Location address |
|
||||
| `source` | object | Application source |
|
||||
| ↳ `id` | number | Source ID |
|
||||
| ↳ `public_name` | string | Source name |
|
||||
| `credited_to` | object | User credited for the application |
|
||||
| ↳ `id` | number | User ID |
|
||||
| ↳ `first_name` | string | First name |
|
||||
| ↳ `last_name` | string | Last name |
|
||||
| ↳ `name` | string | Full name |
|
||||
| ↳ `employee_id` | string | Employee ID |
|
||||
| `recruiter` | object | Assigned recruiter |
|
||||
| ↳ `id` | number | User ID |
|
||||
| ↳ `first_name` | string | First name |
|
||||
| ↳ `last_name` | string | Last name |
|
||||
| ↳ `name` | string | Full name |
|
||||
| ↳ `employee_id` | string | Employee ID |
|
||||
| `coordinator` | object | Assigned coordinator |
|
||||
| ↳ `id` | number | User ID |
|
||||
| ↳ `first_name` | string | First name |
|
||||
| ↳ `last_name` | string | Last name |
|
||||
| ↳ `name` | string | Full name |
|
||||
| ↳ `employee_id` | string | Employee ID |
|
||||
| `current_stage` | object | Current interview stage \(null when hired\) |
|
||||
| ↳ `id` | number | Stage ID |
|
||||
| ↳ `name` | string | Stage name |
|
||||
| `rejection_reason` | object | Rejection reason |
|
||||
| ↳ `id` | number | Rejection reason ID |
|
||||
| ↳ `name` | string | Rejection reason name |
|
||||
| ↳ `type` | object | Rejection reason type |
|
||||
| ↳ `id` | number | Type ID |
|
||||
| ↳ `name` | string | Type name |
|
||||
| `jobs` | array | Associated jobs |
|
||||
| ↳ `id` | number | Job ID |
|
||||
| ↳ `name` | string | Job name |
|
||||
| `job_post_id` | number | Job post ID |
|
||||
| `answers` | array | Application question answers |
|
||||
| ↳ `question` | string | Question text |
|
||||
| ↳ `answer` | string | Answer text |
|
||||
| `attachments` | array | File attachments \(URLs expire after 7 days\) |
|
||||
| ↳ `filename` | string | File name |
|
||||
| ↳ `url` | string | Download URL \(expires after 7 days\) |
|
||||
| ↳ `type` | string | Type \(resume, cover_letter, offer_packet, other\) |
|
||||
| ↳ `created_at` | string | Upload timestamp |
|
||||
| `custom_fields` | object | Custom field values |
|
||||
|
||||
### `greenhouse_list_users`
|
||||
|
||||
Lists Greenhouse users (recruiters, hiring managers, admins) with optional filtering
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Greenhouse Harvest API key |
|
||||
| `per_page` | number | No | Number of results per page \(1-500, default 100\) |
|
||||
| `page` | number | No | Page number for pagination |
|
||||
| `created_after` | string | No | Return only users created at or after this ISO 8601 timestamp |
|
||||
| `created_before` | string | No | Return only users created before this ISO 8601 timestamp |
|
||||
| `updated_after` | string | No | Return only users updated at or after this ISO 8601 timestamp |
|
||||
| `updated_before` | string | No | Return only users updated before this ISO 8601 timestamp |
|
||||
| `email` | string | No | Filter by email address |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `users` | array | List of Greenhouse users |
|
||||
| ↳ `id` | number | User ID |
|
||||
| ↳ `name` | string | Full name |
|
||||
| ↳ `first_name` | string | First name |
|
||||
| ↳ `last_name` | string | Last name |
|
||||
| ↳ `primary_email_address` | string | Primary email |
|
||||
| ↳ `disabled` | boolean | Whether the user is disabled |
|
||||
| ↳ `site_admin` | boolean | Whether the user is a site admin |
|
||||
| ↳ `emails` | array | All email addresses |
|
||||
| ↳ `employee_id` | string | Employee ID |
|
||||
| ↳ `linked_candidate_ids` | array | IDs of candidates linked to this user |
|
||||
| ↳ `created_at` | string | Creation timestamp \(ISO 8601\) |
|
||||
| ↳ `updated_at` | string | Last updated timestamp \(ISO 8601\) |
|
||||
| `count` | number | Number of users returned |
|
||||
|
||||
### `greenhouse_get_user`
|
||||
|
||||
Retrieves a specific Greenhouse user by ID
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Greenhouse Harvest API key |
|
||||
| `userId` | string | Yes | The ID of the user to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | number | User ID |
|
||||
| `name` | string | Full name |
|
||||
| `first_name` | string | First name |
|
||||
| `last_name` | string | Last name |
|
||||
| `primary_email_address` | string | Primary email address |
|
||||
| `disabled` | boolean | Whether the user is disabled |
|
||||
| `site_admin` | boolean | Whether the user is a site admin |
|
||||
| `emails` | array | All email addresses |
|
||||
| `employee_id` | string | Employee ID |
|
||||
| `linked_candidate_ids` | array | IDs of candidates linked to this user |
|
||||
| `created_at` | string | Creation timestamp \(ISO 8601\) |
|
||||
| `updated_at` | string | Last updated timestamp \(ISO 8601\) |
|
||||
|
||||
### `greenhouse_list_departments`
|
||||
|
||||
Lists all departments configured in Greenhouse
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Greenhouse Harvest API key |
|
||||
| `per_page` | number | No | Number of results per page \(1-500, default 100\) |
|
||||
| `page` | number | No | Page number for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `departments` | array | List of departments |
|
||||
| ↳ `id` | number | Department ID |
|
||||
| ↳ `name` | string | Department name |
|
||||
| ↳ `parent_id` | number | Parent department ID |
|
||||
| ↳ `child_ids` | array | Child department IDs |
|
||||
| ↳ `external_id` | string | External system ID |
|
||||
| `count` | number | Number of departments returned |
|
||||
|
||||
### `greenhouse_list_offices`
|
||||
|
||||
Lists all offices configured in Greenhouse
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Greenhouse Harvest API key |
|
||||
| `per_page` | number | No | Number of results per page \(1-500, default 100\) |
|
||||
| `page` | number | No | Page number for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `offices` | array | List of offices |
|
||||
| ↳ `id` | number | Office ID |
|
||||
| ↳ `name` | string | Office name |
|
||||
| ↳ `location` | object | Office location |
|
||||
| ↳ `name` | string | Location name |
|
||||
| ↳ `primary_contact_user_id` | number | Primary contact user ID |
|
||||
| ↳ `parent_id` | number | Parent office ID |
|
||||
| ↳ `child_ids` | array | Child office IDs |
|
||||
| ↳ `external_id` | string | External system ID |
|
||||
| `count` | number | Number of offices returned |
|
||||
|
||||
### `greenhouse_list_job_stages`
|
||||
|
||||
Lists all interview stages for a specific job in Greenhouse
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Greenhouse Harvest API key |
|
||||
| `jobId` | string | Yes | The job ID to list stages for |
|
||||
| `per_page` | number | No | Number of results per page \(1-500, default 100\) |
|
||||
| `page` | number | No | Page number for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `stages` | array | List of job stages in order |
|
||||
| ↳ `id` | number | Stage ID |
|
||||
| ↳ `name` | string | Stage name |
|
||||
| ↳ `created_at` | string | Creation timestamp \(ISO 8601\) |
|
||||
| ↳ `updated_at` | string | Last updated timestamp \(ISO 8601\) |
|
||||
| ↳ `job_id` | number | Associated job ID |
|
||||
| ↳ `priority` | number | Stage order priority |
|
||||
| ↳ `active` | boolean | Whether the stage is active |
|
||||
| ↳ `interviews` | array | Interview steps in this stage |
|
||||
| ↳ `id` | number | Interview ID |
|
||||
| ↳ `name` | string | Interview name |
|
||||
| ↳ `schedulable` | boolean | Whether the interview is schedulable |
|
||||
| ↳ `estimated_minutes` | number | Estimated duration in minutes |
|
||||
| ↳ `default_interviewer_users` | array | Default interviewers |
|
||||
| ↳ `id` | number | User ID |
|
||||
| ↳ `name` | string | Full name |
|
||||
| ↳ `first_name` | string | First name |
|
||||
| ↳ `last_name` | string | Last name |
|
||||
| ↳ `employee_id` | string | Employee ID |
|
||||
| ↳ `interview_kit` | object | Interview kit details |
|
||||
| ↳ `id` | number | Kit ID |
|
||||
| ↳ `content` | string | Kit content \(HTML\) |
|
||||
| ↳ `questions` | array | Interview kit questions |
|
||||
| ↳ `id` | number | Question ID |
|
||||
| ↳ `question` | string | Question text |
|
||||
| `count` | number | Number of stages returned |
|
||||
|
||||
|
||||
@@ -11,20 +11,16 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[HubSpot](https://www.hubspot.com) is a comprehensive CRM platform that provides a full suite of marketing, sales, and customer service tools to help businesses grow better. With its powerful automation capabilities and extensive API, HubSpot has become one of the world's leading CRM platforms, serving businesses of all sizes across industries.
|
||||
[HubSpot](https://www.hubspot.com) is a comprehensive CRM platform that provides a full suite of marketing, sales, and customer service tools to help businesses grow. With powerful automation capabilities and an extensive API, HubSpot serves businesses of all sizes across industries.
|
||||
|
||||
HubSpot CRM offers a complete solution for managing customer relationships, from initial contact through to long-term customer success. The platform combines contact management, deal tracking, marketing automation, and customer service tools into a unified system that helps teams stay aligned and focused on customer success.
|
||||
With the HubSpot integration in Sim, you can:
|
||||
|
||||
Key features of HubSpot CRM include:
|
||||
- **Manage contacts**: List, get, create, update, and search contacts in your CRM
|
||||
- **Manage companies**: List, get, create, update, and search company records
|
||||
- **Track deals**: List deals in your sales pipeline
|
||||
- **Access users**: Retrieve user information from your HubSpot account
|
||||
|
||||
- Contact & Company Management: Comprehensive database for storing and organizing customer and prospect information
|
||||
- Deal Pipeline: Visual sales pipeline for tracking opportunities through customizable stages
|
||||
- Marketing Events: Track and manage marketing campaigns and events with detailed attribution
|
||||
- Ticket Management: Customer support ticketing system for tracking and resolving customer issues
|
||||
- Quotes & Line Items: Create and manage sales quotes with detailed product line items
|
||||
- User & Team Management: Organize teams, assign ownership, and track user activity across the platform
|
||||
|
||||
In Sim, the HubSpot integration enables your AI agents to seamlessly interact with your CRM data and automate key business processes. This creates powerful opportunities for intelligent lead qualification, automated contact enrichment, deal management, customer support automation, and data synchronization across your tech stack. The integration allows agents to create, retrieve, update, and search across all major HubSpot objects, enabling sophisticated workflows that can respond to CRM events, maintain data quality, and ensure your team has the most up-to-date customer information. By connecting Sim with HubSpot, you can build AI agents that automatically qualify leads, route support tickets, update deal stages based on customer interactions, generate quotes, and keep your CRM data synchronized with other business systems—ultimately increasing team productivity and improving customer experiences.
|
||||
In Sim, the HubSpot integration enables your agents to interact with your CRM data as part of automated workflows. Agents can qualify leads, enrich contact records, track deals, and synchronize data across your tech stack—enabling intelligent sales and marketing automation.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
|
||||
@@ -11,16 +11,13 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[HuggingFace](https://huggingface.co/) is a leading AI platform that provides access to thousands of pre-trained machine learning models and powerful inference capabilities. With its extensive model hub and robust API, HuggingFace offers comprehensive tools for both research and production AI applications.
|
||||
With HuggingFace, you can:
|
||||
[Hugging Face](https://huggingface.co/) is a leading AI platform that provides access to thousands of pre-trained machine learning models and powerful inference capabilities. With its extensive model hub and robust API, Hugging Face offers comprehensive tools for both research and production AI applications.
|
||||
|
||||
Access pre-trained models: Utilize models for text generation, translation, image processing, and more
|
||||
Generate AI completions: Create content using state-of-the-art language models through the Inference API
|
||||
Natural language processing: Process and analyze text with specialized NLP models
|
||||
Deploy at scale: Host and serve models for production applications
|
||||
Customize models: Fine-tune existing models for specific use cases
|
||||
With the Hugging Face integration in Sim, you can:
|
||||
|
||||
In Sim, the HuggingFace integration enables your agents to programmatically generate completions using the HuggingFace Inference API. This allows for powerful automation scenarios such as content generation, text analysis, code completion, and creative writing. Your agents can generate completions with natural language prompts, access specialized models for different tasks, and integrate AI-generated content into workflows. This integration bridges the gap between your AI workflows and machine learning capabilities, enabling seamless AI-powered automation with one of the world's most comprehensive ML platforms.
|
||||
- **Generate completions**: Create text content using state-of-the-art language models through the Hugging Face Inference API, with support for custom prompts and model selection
|
||||
|
||||
In Sim, the Hugging Face integration enables your agents to generate AI completions as part of automated workflows. This allows for content generation, text analysis, code completion, and creative writing using models from the Hugging Face model hub.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
|
||||
@@ -11,18 +11,22 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Jira](https://www.atlassian.com/jira) is a leading project management and issue tracking platform that helps teams plan, track, and manage agile software development projects effectively. As part of the Atlassian suite, Jira has become the industry standard for software development teams and project management professionals worldwide.
|
||||
[Jira](https://www.atlassian.com/jira) is a leading project management and issue tracking platform from Atlassian that helps teams plan, track, and manage agile software development projects. Jira supports Scrum and Kanban methodologies with customizable boards, workflows, and advanced reporting.
|
||||
|
||||
Jira provides a comprehensive set of tools for managing complex projects through its flexible and customizable workflow system. With its robust API and integration capabilities, Jira enables teams to streamline their development processes and maintain clear visibility of project progress.
|
||||
With the Jira integration in Sim, you can:
|
||||
|
||||
Key features of Jira include:
|
||||
- **Manage issues**: Create, retrieve, update, delete, and bulk-read issues in your Jira projects
|
||||
- **Transition issues**: Move issues through workflow stages programmatically
|
||||
- **Assign issues**: Set or change issue assignees
|
||||
- **Search issues**: Use JQL (Jira Query Language) to find and filter issues
|
||||
- **Manage comments**: Add, retrieve, update, and delete comments on issues
|
||||
- **Handle attachments**: Upload, retrieve, and delete file attachments on issues
|
||||
- **Track work**: Add, retrieve, update, and delete worklogs for time tracking
|
||||
- **Link issues**: Create and delete issue links to establish relationships between issues
|
||||
- **Manage watchers**: Add or remove watchers from issues
|
||||
- **Access users**: Retrieve user information from your Jira instance
|
||||
|
||||
- Agile Project Management: Support for Scrum and Kanban methodologies with customizable boards and workflows
|
||||
- Issue Tracking: Sophisticated tracking system for bugs, stories, epics, and tasks with detailed reporting
|
||||
- Workflow Automation: Powerful automation rules to streamline repetitive tasks and processes
|
||||
- Advanced Search: JQL (Jira Query Language) for complex issue filtering and reporting
|
||||
|
||||
In Sim, the Jira integration allows your agents to seamlessly interact with your project management workflow. This creates opportunities for automated issue creation, updates, and tracking as part of your AI workflows. The integration enables agents to create, retrieve, and update Jira issues programmatically, facilitating automated project management tasks and ensuring that important information is properly tracked and documented. By connecting Sim with Jira, you can build intelligent agents that maintain project visibility while automating routine project management tasks, enhancing team productivity and ensuring consistent project tracking.
|
||||
In Sim, the Jira integration enables your agents to interact with your project management workflow as part of automated processes. Agents can create issues from external triggers, update statuses, track progress, and manage project data—enabling intelligent project management automation.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
|
||||
@@ -11,19 +11,15 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
Sim's Knowledge Base is a powerful native feature that enables you to create, manage, and query custom knowledge bases directly within the platform. Using advanced AI embeddings and vector search technology, the Knowledge Base block allows you to build intelligent search capabilities into your workflows, making it easy to find and utilize relevant information across your organization.
|
||||
Sim's Knowledge Base is a native feature that enables you to create, manage, and query custom knowledge bases directly within the platform. Using advanced AI embeddings and vector search, the Knowledge Base block allows you to build intelligent search capabilities into your workflows.
|
||||
|
||||
The Knowledge Base system provides a comprehensive solution for managing organizational knowledge through its flexible and scalable architecture. With its built-in vector search capabilities, teams can perform semantic searches that understand meaning and context, going beyond traditional keyword matching.
|
||||
With the Knowledge Base in Sim, you can:
|
||||
|
||||
Key features of the Knowledge Base include:
|
||||
- **Search knowledge**: Perform semantic searches across your custom knowledge bases using AI-powered vector similarity matching
|
||||
- **Upload chunks**: Add text chunks with metadata to a knowledge base for indexing
|
||||
- **Create documents**: Add new documents to a knowledge base for searchable content
|
||||
|
||||
- Semantic Search: Advanced AI-powered search that understands meaning and context, not just keywords
|
||||
- Vector Embeddings: Automatic conversion of text into high-dimensional vectors for intelligent similarity matching
|
||||
- Custom Knowledge Bases: Create and manage multiple knowledge bases for different purposes or departments
|
||||
- Flexible Content Types: Support for various document formats and content types
|
||||
- Real-time Updates: Immediate indexing of new content for instant searchability
|
||||
|
||||
In Sim, the Knowledge Base block enables your agents to perform intelligent semantic searches across your custom knowledge bases. This creates opportunities for automated information retrieval, content recommendations, and knowledge discovery as part of your AI workflows. The integration allows agents to search and retrieve relevant information programmatically, facilitating automated knowledge management tasks and ensuring that important information is easily accessible. By leveraging the Knowledge Base block, you can build intelligent agents that enhance information discovery while automating routine knowledge management tasks, improving team efficiency and ensuring consistent access to organizational knowledge.
|
||||
In Sim, the Knowledge Base block enables your agents to perform intelligent semantic searches across your organizational knowledge as part of automated workflows. This is ideal for information retrieval, content recommendations, FAQ automation, and grounding agent responses in your own data.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
|
||||
@@ -11,18 +11,21 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Linear](https://linear.app) is a leading project management and issue tracking platform that helps teams plan, track, and manage their work effectively. As a modern project management tool, Linear has become increasingly popular among software development teams and project management professionals for its streamlined interface and powerful features.
|
||||
[Linear](https://linear.app) is a modern project management and issue tracking platform that helps teams plan, track, and manage their work with a streamlined interface. Linear supports agile methodologies with customizable workflows, cycles, and project milestones.
|
||||
|
||||
Linear provides a comprehensive set of tools for managing complex projects through its flexible and customizable workflow system. With its robust API and integration capabilities, Linear enables teams to streamline their development processes and maintain clear visibility of project progress.
|
||||
With the Linear integration in Sim, you can:
|
||||
|
||||
Key features of Linear include:
|
||||
- **Manage issues**: Create, read, update, search, archive, unarchive, and delete issues
|
||||
- **Manage labels**: Add or remove labels from issues, and create, update, or archive labels
|
||||
- **Comment on issues**: Create, update, delete, and list comments on issues
|
||||
- **Manage projects**: List, get, create, update, archive, and delete projects with milestones, labels, and statuses
|
||||
- **Track cycles**: List, get, and create cycles, and retrieve the active cycle
|
||||
- **Handle attachments**: Create, list, update, and delete attachments on issues
|
||||
- **Manage issue relations**: Create, list, and delete relationships between issues
|
||||
- **Access team data**: List users, teams, workflow states, notifications, and favorites
|
||||
- **Manage customers**: Create, update, delete, list, and merge customers with statuses, tiers, and requests
|
||||
|
||||
- Agile Project Management: Support for Scrum and Kanban methodologies with customizable boards and workflows
|
||||
- Issue Tracking: Sophisticated tracking system for bugs, stories, epics, and tasks with detailed reporting
|
||||
- Workflow Automation: Powerful automation rules to streamline repetitive tasks and processes
|
||||
- Advanced Search: Complex filtering and reporting capabilities for efficient issue management
|
||||
|
||||
In Sim, the Linear integration allows your agents to seamlessly interact with your project management workflow. This creates opportunities for automated issue creation, updates, and tracking as part of your AI workflows. The integration enables agents to read existing issues and create new ones programmatically, facilitating automated project management tasks and ensuring that important information is properly tracked and documented. By connecting Sim with Linear, you can build intelligent agents that maintain project visibility while automating routine project management tasks, enhancing team productivity and ensuring consistent project tracking.
|
||||
In Sim, the Linear integration enables your agents to interact with your project management workflow as part of automated processes. Agents can create issues from external triggers, update statuses, manage projects and cycles, and synchronize data—enabling intelligent project management automation at scale.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
|
||||
273
apps/docs/content/docs/en/tools/loops.mdx
Normal file
273
apps/docs/content/docs/en/tools/loops.mdx
Normal file
@@ -0,0 +1,273 @@
|
||||
---
|
||||
title: Loops
|
||||
description: Manage contacts and send emails with Loops
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="loops"
|
||||
color="#FAFAF9"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Loops](https://loops.so/) is an email platform built for modern SaaS companies, offering transactional emails, marketing campaigns, and event-driven automations through a clean API. This integration connects Loops directly into Sim workflows.
|
||||
|
||||
With Loops in Sim, you can:
|
||||
|
||||
- **Manage contacts**: Create, update, find, and delete contacts in your Loops audience
|
||||
- **Send transactional emails**: Trigger templated transactional emails with dynamic data variables
|
||||
- **Fire events**: Send events to Loops to trigger automated email sequences and workflows
|
||||
- **Manage subscriptions**: Control mailing list subscriptions and contact properties programmatically
|
||||
- **Enrich contact data**: Attach custom properties, user groups, and mailing list memberships to contacts
|
||||
|
||||
In Sim, the Loops integration enables your agents to manage email operations as part of their workflows. Supported operations include:
|
||||
|
||||
- **Create Contact**: Add a new contact to your Loops audience with email, name, and custom properties.
|
||||
- **Update Contact**: Update an existing contact or create one if no match exists (upsert behavior).
|
||||
- **Find Contact**: Look up a contact by email address or userId.
|
||||
- **Delete Contact**: Remove a contact from your audience.
|
||||
- **Send Transactional Email**: Send a templated transactional email to a recipient with dynamic data variables.
|
||||
- **Send Event**: Trigger a Loops event to start automated email sequences for a contact.
|
||||
|
||||
Configure the Loops block with your API key from the Loops dashboard (Settings > API), select an operation, and provide the required parameters. Your agents can then manage contacts and send emails as part of any workflow.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Loops into the workflow. Create and manage contacts, send transactional emails, and trigger event-based automations.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `loops_create_contact`
|
||||
|
||||
Create a new contact in your Loops audience with an email address and optional properties like name, user group, and mailing list subscriptions.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Loops API key for authentication |
|
||||
| `email` | string | Yes | The email address for the new contact |
|
||||
| `firstName` | string | No | The contact first name |
|
||||
| `lastName` | string | No | The contact last name |
|
||||
| `source` | string | No | Custom source value replacing the default "API" |
|
||||
| `subscribed` | boolean | No | Whether the contact receives campaign emails \(defaults to true\) |
|
||||
| `userGroup` | string | No | Group to segment the contact into \(one group per contact\) |
|
||||
| `userId` | string | No | Unique user identifier from your application |
|
||||
| `mailingLists` | json | No | Mailing list IDs mapped to boolean values \(true to subscribe, false to unsubscribe\) |
|
||||
| `customProperties` | json | No | Custom contact properties as key-value pairs \(string, number, boolean, or date values\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether the contact was created successfully |
|
||||
| `id` | string | The Loops-assigned ID of the created contact |
|
||||
|
||||
### `loops_update_contact`
|
||||
|
||||
Update an existing contact in Loops by email or userId. Creates a new contact if no match is found (upsert). Can update name, subscription status, user group, mailing lists, and custom properties.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Loops API key for authentication |
|
||||
| `email` | string | No | The contact email address \(at least one of email or userId is required\) |
|
||||
| `userId` | string | No | The contact userId \(at least one of email or userId is required\) |
|
||||
| `firstName` | string | No | The contact first name |
|
||||
| `lastName` | string | No | The contact last name |
|
||||
| `source` | string | No | Custom source value replacing the default "API" |
|
||||
| `subscribed` | boolean | No | Whether the contact receives campaign emails \(sending true re-subscribes unsubscribed contacts\) |
|
||||
| `userGroup` | string | No | Group to segment the contact into \(one group per contact\) |
|
||||
| `mailingLists` | json | No | Mailing list IDs mapped to boolean values \(true to subscribe, false to unsubscribe\) |
|
||||
| `customProperties` | json | No | Custom contact properties as key-value pairs \(send null to reset a property\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether the contact was updated successfully |
|
||||
| `id` | string | The Loops-assigned ID of the updated or created contact |
|
||||
|
||||
### `loops_find_contact`
|
||||
|
||||
Find a contact in Loops by email address or userId. Returns an array of matching contacts with all their properties including name, subscription status, user group, and mailing lists.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Loops API key for authentication |
|
||||
| `email` | string | No | The contact email address to search for \(at least one of email or userId is required\) |
|
||||
| `userId` | string | No | The contact userId to search for \(at least one of email or userId is required\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `contacts` | array | Array of matching contact objects \(empty array if no match found\) |
|
||||
| ↳ `id` | string | Loops-assigned contact ID |
|
||||
| ↳ `email` | string | Contact email address |
|
||||
| ↳ `firstName` | string | Contact first name |
|
||||
| ↳ `lastName` | string | Contact last name |
|
||||
| ↳ `source` | string | Source the contact was created from |
|
||||
| ↳ `subscribed` | boolean | Whether the contact receives campaign emails |
|
||||
| ↳ `userGroup` | string | Contact user group |
|
||||
| ↳ `userId` | string | External user identifier |
|
||||
| ↳ `mailingLists` | object | Mailing list IDs mapped to subscription status |
|
||||
| ↳ `optInStatus` | string | Double opt-in status: pending, accepted, rejected, or null |
|
||||
|
||||
### `loops_delete_contact`
|
||||
|
||||
Delete a contact from Loops by email address or userId. At least one identifier must be provided.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Loops API key for authentication |
|
||||
| `email` | string | No | The email address of the contact to delete \(at least one of email or userId is required\) |
|
||||
| `userId` | string | No | The userId of the contact to delete \(at least one of email or userId is required\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether the contact was deleted successfully |
|
||||
| `message` | string | Status message from the API |
|
||||
|
||||
### `loops_send_transactional_email`
|
||||
|
||||
Send a transactional email to a recipient using a Loops template. Supports dynamic data variables for personalization and optionally adds the recipient to your audience.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Loops API key for authentication |
|
||||
| `email` | string | Yes | The email address of the recipient |
|
||||
| `transactionalId` | string | Yes | The ID of the transactional email template to send |
|
||||
| `dataVariables` | json | No | Template data variables as key-value pairs \(string or number values\) |
|
||||
| `addToAudience` | boolean | No | Whether to create the recipient as a contact if they do not already exist \(default: false\) |
|
||||
| `attachments` | json | No | Array of file attachments. Each object must have filename \(string\), contentType \(MIME type string\), and data \(base64-encoded string\). |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether the transactional email was sent successfully |
|
||||
|
||||
### `loops_send_event`
|
||||
|
||||
Send an event to Loops to trigger automated email sequences for a contact. Identify the contact by email or userId and include optional event properties and mailing list changes.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Loops API key for authentication |
|
||||
| `email` | string | No | The email address of the contact \(at least one of email or userId is required\) |
|
||||
| `userId` | string | No | The userId of the contact \(at least one of email or userId is required\) |
|
||||
| `eventName` | string | Yes | The name of the event to trigger |
|
||||
| `eventProperties` | json | No | Event data as key-value pairs \(string, number, boolean, or date values\) |
|
||||
| `mailingLists` | json | No | Mailing list IDs mapped to boolean values \(true to subscribe, false to unsubscribe\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether the event was sent successfully |
|
||||
|
||||
### `loops_list_mailing_lists`
|
||||
|
||||
Retrieve all mailing lists from your Loops account. Returns each list with its ID, name, description, and public/private status.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Loops API key for authentication |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `mailingLists` | array | Array of mailing list objects |
|
||||
| ↳ `id` | string | The mailing list ID |
|
||||
| ↳ `name` | string | The mailing list name |
|
||||
| ↳ `description` | string | The mailing list description \(null if not set\) |
|
||||
| ↳ `isPublic` | boolean | Whether the list is public or private |
|
||||
|
||||
### `loops_list_transactional_emails`
|
||||
|
||||
Retrieve a list of published transactional email templates from your Loops account. Returns each template with its ID, name, last updated timestamp, and data variables.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Loops API key for authentication |
|
||||
| `perPage` | string | No | Number of results per page \(10-50, default: 20\) |
|
||||
| `cursor` | string | No | Pagination cursor from a previous response to fetch the next page |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `transactionalEmails` | array | Array of published transactional email templates |
|
||||
| ↳ `id` | string | The transactional email template ID |
|
||||
| ↳ `name` | string | The template name |
|
||||
| ↳ `lastUpdated` | string | Last updated timestamp |
|
||||
| ↳ `dataVariables` | array | Template data variable names |
|
||||
| `pagination` | object | Pagination information |
|
||||
| ↳ `totalResults` | number | Total number of results |
|
||||
| ↳ `returnedResults` | number | Number of results returned |
|
||||
| ↳ `perPage` | number | Results per page |
|
||||
| ↳ `totalPages` | number | Total number of pages |
|
||||
| ↳ `nextCursor` | string | Cursor for next page \(null if no more pages\) |
|
||||
| ↳ `nextPage` | string | URL for next page \(null if no more pages\) |
|
||||
|
||||
### `loops_create_contact_property`
|
||||
|
||||
Create a new custom contact property in your Loops account. The property name must be in camelCase format.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Loops API key for authentication |
|
||||
| `name` | string | Yes | The property name in camelCase format \(e.g., "favoriteColor"\) |
|
||||
| `type` | string | Yes | The property data type \(e.g., "string", "number", "boolean", "date"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether the contact property was created successfully |
|
||||
|
||||
### `loops_list_contact_properties`
|
||||
|
||||
Retrieve a list of contact properties from your Loops account. Returns each property with its key, label, and data type. Can filter to show all properties or only custom ones.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Loops API key for authentication |
|
||||
| `list` | string | No | Filter type: "all" for all properties \(default\) or "custom" for custom properties only |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `properties` | array | Array of contact property objects |
|
||||
| ↳ `key` | string | The property key \(camelCase identifier\) |
|
||||
| ↳ `label` | string | The property display label |
|
||||
| ↳ `type` | string | The property data type \(string, number, boolean, date\) |
|
||||
|
||||
|
||||
284
apps/docs/content/docs/en/tools/luma.mdx
Normal file
284
apps/docs/content/docs/en/tools/luma.mdx
Normal file
@@ -0,0 +1,284 @@
|
||||
---
|
||||
title: Luma
|
||||
description: Manage events and guests on Luma
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="luma"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Luma](https://lu.ma/) is an event management platform that makes it easy to create, manage, and share events with your community.
|
||||
|
||||
With Luma integrated into Sim, your agents can:
|
||||
|
||||
- **Create events**: Set up new events with name, time, timezone, description, and visibility settings.
|
||||
- **Update events**: Modify existing event details like name, time, description, and visibility.
|
||||
- **Get event details**: Retrieve full details for any event by its ID.
|
||||
- **List calendar events**: Browse your calendar's events with date range filtering and pagination.
|
||||
- **Manage guest lists**: View attendees for an event, filtered by approval status.
|
||||
- **Add guests**: Invite new guests to events programmatically.
|
||||
|
||||
By connecting Sim with Luma, you can automate event operations within your agent workflows. Automatically create events based on triggers, sync guest lists, monitor registrations, and manage your event calendar—all handled directly by your agents via the Luma API.
|
||||
|
||||
Whether you're running community meetups, conferences, or internal team events, the Luma tool makes it easy to coordinate event management within your Sim workflows.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Luma into the workflow. Can create events, update events, get event details, list calendar events, get guest lists, and add guests to events.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `luma_get_event`
|
||||
|
||||
Retrieve details of a Luma event including name, time, location, hosts, and visibility settings.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Luma API key |
|
||||
| `eventId` | string | Yes | Event ID \(starts with evt-\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `event` | object | Event details |
|
||||
| ↳ `id` | string | Event ID |
|
||||
| ↳ `name` | string | Event name |
|
||||
| ↳ `startAt` | string | Event start time \(ISO 8601\) |
|
||||
| ↳ `endAt` | string | Event end time \(ISO 8601\) |
|
||||
| ↳ `timezone` | string | Event timezone \(IANA\) |
|
||||
| ↳ `durationInterval` | string | Event duration \(ISO 8601 interval, e.g. PT2H\) |
|
||||
| ↳ `createdAt` | string | Event creation timestamp \(ISO 8601\) |
|
||||
| ↳ `description` | string | Event description \(plain text\) |
|
||||
| ↳ `descriptionMd` | string | Event description \(Markdown\) |
|
||||
| ↳ `coverUrl` | string | Event cover image URL |
|
||||
| ↳ `url` | string | Event page URL on lu.ma |
|
||||
| ↳ `visibility` | string | Event visibility \(public, members-only, private\) |
|
||||
| ↳ `meetingUrl` | string | Virtual meeting URL |
|
||||
| ↳ `geoAddressJson` | json | Structured location/address data |
|
||||
| ↳ `geoLatitude` | string | Venue latitude coordinate |
|
||||
| ↳ `geoLongitude` | string | Venue longitude coordinate |
|
||||
| ↳ `calendarId` | string | Associated calendar ID |
|
||||
| `hosts` | array | Event hosts |
|
||||
| ↳ `id` | string | Host ID |
|
||||
| ↳ `name` | string | Host display name |
|
||||
| ↳ `firstName` | string | Host first name |
|
||||
| ↳ `lastName` | string | Host last name |
|
||||
| ↳ `email` | string | Host email address |
|
||||
| ↳ `avatarUrl` | string | Host avatar image URL |
|
||||
|
||||
### `luma_create_event`
|
||||
|
||||
Create a new event on Luma with a name, start time, timezone, and optional details like description, location, and visibility.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Luma API key |
|
||||
| `name` | string | Yes | Event name/title |
|
||||
| `startAt` | string | Yes | Event start time in ISO 8601 format \(e.g., 2025-03-15T18:00:00Z\) |
|
||||
| `timezone` | string | Yes | IANA timezone \(e.g., America/New_York, Europe/London\) |
|
||||
| `endAt` | string | No | Event end time in ISO 8601 format \(e.g., 2025-03-15T20:00:00Z\) |
|
||||
| `durationInterval` | string | No | Event duration as ISO 8601 interval \(e.g., PT2H for 2 hours, PT30M for 30 minutes\). Used if endAt is not provided. |
|
||||
| `descriptionMd` | string | No | Event description in Markdown format |
|
||||
| `meetingUrl` | string | No | Virtual meeting URL for online events \(e.g., Zoom, Google Meet link\) |
|
||||
| `visibility` | string | No | Event visibility: public, members-only, or private \(defaults to public\) |
|
||||
| `coverUrl` | string | No | Cover image URL \(must be a Luma CDN URL from images.lumacdn.com\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `event` | object | Created event details |
|
||||
| ↳ `id` | string | Event ID |
|
||||
| ↳ `name` | string | Event name |
|
||||
| ↳ `startAt` | string | Event start time \(ISO 8601\) |
|
||||
| ↳ `endAt` | string | Event end time \(ISO 8601\) |
|
||||
| ↳ `timezone` | string | Event timezone \(IANA\) |
|
||||
| ↳ `durationInterval` | string | Event duration \(ISO 8601 interval, e.g. PT2H\) |
|
||||
| ↳ `createdAt` | string | Event creation timestamp \(ISO 8601\) |
|
||||
| ↳ `description` | string | Event description \(plain text\) |
|
||||
| ↳ `descriptionMd` | string | Event description \(Markdown\) |
|
||||
| ↳ `coverUrl` | string | Event cover image URL |
|
||||
| ↳ `url` | string | Event page URL on lu.ma |
|
||||
| ↳ `visibility` | string | Event visibility \(public, members-only, private\) |
|
||||
| ↳ `meetingUrl` | string | Virtual meeting URL |
|
||||
| ↳ `geoAddressJson` | json | Structured location/address data |
|
||||
| ↳ `geoLatitude` | string | Venue latitude coordinate |
|
||||
| ↳ `geoLongitude` | string | Venue longitude coordinate |
|
||||
| ↳ `calendarId` | string | Associated calendar ID |
|
||||
| `hosts` | array | Event hosts |
|
||||
| ↳ `id` | string | Host ID |
|
||||
| ↳ `name` | string | Host display name |
|
||||
| ↳ `firstName` | string | Host first name |
|
||||
| ↳ `lastName` | string | Host last name |
|
||||
| ↳ `email` | string | Host email address |
|
||||
| ↳ `avatarUrl` | string | Host avatar image URL |
|
||||
|
||||
### `luma_update_event`
|
||||
|
||||
Update an existing Luma event. Only the fields you provide will be changed; all other fields remain unchanged.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Luma API key |
|
||||
| `eventId` | string | Yes | Event ID to update \(starts with evt-\) |
|
||||
| `name` | string | No | New event name/title |
|
||||
| `startAt` | string | No | New start time in ISO 8601 format \(e.g., 2025-03-15T18:00:00Z\) |
|
||||
| `timezone` | string | No | New IANA timezone \(e.g., America/New_York, Europe/London\) |
|
||||
| `endAt` | string | No | New end time in ISO 8601 format \(e.g., 2025-03-15T20:00:00Z\) |
|
||||
| `durationInterval` | string | No | New duration as ISO 8601 interval \(e.g., PT2H for 2 hours\). Used if endAt is not provided. |
|
||||
| `descriptionMd` | string | No | New event description in Markdown format |
|
||||
| `meetingUrl` | string | No | New virtual meeting URL \(e.g., Zoom, Google Meet link\) |
|
||||
| `visibility` | string | No | New visibility: public, members-only, or private |
|
||||
| `coverUrl` | string | No | New cover image URL \(must be a Luma CDN URL from images.lumacdn.com\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `event` | object | Updated event details |
|
||||
| ↳ `id` | string | Event ID |
|
||||
| ↳ `name` | string | Event name |
|
||||
| ↳ `startAt` | string | Event start time \(ISO 8601\) |
|
||||
| ↳ `endAt` | string | Event end time \(ISO 8601\) |
|
||||
| ↳ `timezone` | string | Event timezone \(IANA\) |
|
||||
| ↳ `durationInterval` | string | Event duration \(ISO 8601 interval, e.g. PT2H\) |
|
||||
| ↳ `createdAt` | string | Event creation timestamp \(ISO 8601\) |
|
||||
| ↳ `description` | string | Event description \(plain text\) |
|
||||
| ↳ `descriptionMd` | string | Event description \(Markdown\) |
|
||||
| ↳ `coverUrl` | string | Event cover image URL |
|
||||
| ↳ `url` | string | Event page URL on lu.ma |
|
||||
| ↳ `visibility` | string | Event visibility \(public, members-only, private\) |
|
||||
| ↳ `meetingUrl` | string | Virtual meeting URL |
|
||||
| ↳ `geoAddressJson` | json | Structured location/address data |
|
||||
| ↳ `geoLatitude` | string | Venue latitude coordinate |
|
||||
| ↳ `geoLongitude` | string | Venue longitude coordinate |
|
||||
| ↳ `calendarId` | string | Associated calendar ID |
|
||||
| `hosts` | array | Event hosts |
|
||||
| ↳ `id` | string | Host ID |
|
||||
| ↳ `name` | string | Host display name |
|
||||
| ↳ `firstName` | string | Host first name |
|
||||
| ↳ `lastName` | string | Host last name |
|
||||
| ↳ `email` | string | Host email address |
|
||||
| ↳ `avatarUrl` | string | Host avatar image URL |
|
||||
|
||||
### `luma_list_events`
|
||||
|
||||
List events from your Luma calendar with optional date range filtering, sorting, and pagination.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Luma API key |
|
||||
| `after` | string | No | Return events after this ISO 8601 datetime \(e.g., 2025-01-01T00:00:00Z\) |
|
||||
| `before` | string | No | Return events before this ISO 8601 datetime \(e.g., 2025-12-31T23:59:59Z\) |
|
||||
| `paginationLimit` | number | No | Maximum number of events to return per page |
|
||||
| `paginationCursor` | string | No | Pagination cursor from a previous response \(next_cursor\) to fetch the next page of results |
|
||||
| `sortColumn` | string | No | Column to sort by \(only start_at is supported\) |
|
||||
| `sortDirection` | string | No | Sort direction: asc, desc, asc nulls last, or desc nulls last |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `events` | array | List of calendar events |
|
||||
| ↳ `id` | string | Event ID |
|
||||
| ↳ `name` | string | Event name |
|
||||
| ↳ `startAt` | string | Event start time \(ISO 8601\) |
|
||||
| ↳ `endAt` | string | Event end time \(ISO 8601\) |
|
||||
| ↳ `timezone` | string | Event timezone \(IANA\) |
|
||||
| ↳ `durationInterval` | string | Event duration \(ISO 8601 interval, e.g. PT2H\) |
|
||||
| ↳ `createdAt` | string | Event creation timestamp \(ISO 8601\) |
|
||||
| ↳ `description` | string | Event description \(plain text\) |
|
||||
| ↳ `descriptionMd` | string | Event description \(Markdown\) |
|
||||
| ↳ `coverUrl` | string | Event cover image URL |
|
||||
| ↳ `url` | string | Event page URL on lu.ma |
|
||||
| ↳ `visibility` | string | Event visibility \(public, members-only, private\) |
|
||||
| ↳ `meetingUrl` | string | Virtual meeting URL |
|
||||
| ↳ `geoAddressJson` | json | Structured location/address data |
|
||||
| ↳ `geoLatitude` | string | Venue latitude coordinate |
|
||||
| ↳ `geoLongitude` | string | Venue longitude coordinate |
|
||||
| ↳ `calendarId` | string | Associated calendar ID |
|
||||
| `hasMore` | boolean | Whether more results are available for pagination |
|
||||
| `nextCursor` | string | Cursor to pass as paginationCursor to fetch the next page |
|
||||
|
||||
### `luma_get_guests`
|
||||
|
||||
Retrieve the guest list for a Luma event with optional filtering by approval status, sorting, and pagination.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Luma API key |
|
||||
| `eventId` | string | Yes | Event ID \(starts with evt-\) |
|
||||
| `approvalStatus` | string | No | Filter by approval status: approved, session, pending_approval, invited, declined, or waitlist |
|
||||
| `paginationLimit` | number | No | Maximum number of guests to return per page |
|
||||
| `paginationCursor` | string | No | Pagination cursor from a previous response \(next_cursor\) to fetch the next page of results |
|
||||
| `sortColumn` | string | No | Column to sort by: name, email, created_at, registered_at, or checked_in_at |
|
||||
| `sortDirection` | string | No | Sort direction: asc, desc, asc nulls last, or desc nulls last |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `guests` | array | List of event guests |
|
||||
| ↳ `id` | string | Guest ID |
|
||||
| ↳ `email` | string | Guest email address |
|
||||
| ↳ `name` | string | Guest full name |
|
||||
| ↳ `firstName` | string | Guest first name |
|
||||
| ↳ `lastName` | string | Guest last name |
|
||||
| ↳ `approvalStatus` | string | Guest approval status \(approved, session, pending_approval, invited, declined, waitlist\) |
|
||||
| ↳ `registeredAt` | string | Registration timestamp \(ISO 8601\) |
|
||||
| ↳ `invitedAt` | string | Invitation timestamp \(ISO 8601\) |
|
||||
| ↳ `joinedAt` | string | Join timestamp \(ISO 8601\) |
|
||||
| ↳ `checkedInAt` | string | Check-in timestamp \(ISO 8601\) |
|
||||
| ↳ `phoneNumber` | string | Guest phone number |
|
||||
| `hasMore` | boolean | Whether more results are available for pagination |
|
||||
| `nextCursor` | string | Cursor to pass as paginationCursor to fetch the next page |
|
||||
|
||||
### `luma_add_guests`
|
||||
|
||||
Add guests to a Luma event by email. Guests are added with Going (approved) status and receive one ticket of the default ticket type.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Luma API key |
|
||||
| `eventId` | string | Yes | Event ID \(starts with evt-\) |
|
||||
| `guests` | string | Yes | JSON array of guest objects. Each guest requires an "email" field and optionally "name", "first_name", "last_name". Example: \[\{"email": "user@example.com", "name": "John Doe"\}\] |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `guests` | array | List of added guests with their assigned status and ticket info |
|
||||
| ↳ `id` | string | Guest ID |
|
||||
| ↳ `email` | string | Guest email address |
|
||||
| ↳ `name` | string | Guest full name |
|
||||
| ↳ `firstName` | string | Guest first name |
|
||||
| ↳ `lastName` | string | Guest last name |
|
||||
| ↳ `approvalStatus` | string | Guest approval status \(approved, session, pending_approval, invited, declined, waitlist\) |
|
||||
| ↳ `registeredAt` | string | Registration timestamp \(ISO 8601\) |
|
||||
| ↳ `invitedAt` | string | Invitation timestamp \(ISO 8601\) |
|
||||
| ↳ `joinedAt` | string | Join timestamp \(ISO 8601\) |
|
||||
| ↳ `checkedInAt` | string | Check-in timestamp \(ISO 8601\) |
|
||||
| ↳ `phoneNumber` | string | Guest phone number |
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"apollo",
|
||||
"arxiv",
|
||||
"asana",
|
||||
"ashby",
|
||||
"attio",
|
||||
"browser_use",
|
||||
"calcom",
|
||||
@@ -20,7 +21,9 @@
|
||||
"cloudflare",
|
||||
"confluence",
|
||||
"cursor",
|
||||
"databricks",
|
||||
"datadog",
|
||||
"devin",
|
||||
"discord",
|
||||
"dropbox",
|
||||
"dspy",
|
||||
@@ -33,12 +36,15 @@
|
||||
"file",
|
||||
"firecrawl",
|
||||
"fireflies",
|
||||
"gamma",
|
||||
"github",
|
||||
"gitlab",
|
||||
"gmail",
|
||||
"gong",
|
||||
"google_bigquery",
|
||||
"google_books",
|
||||
"google_calendar",
|
||||
"google_contacts",
|
||||
"google_docs",
|
||||
"google_drive",
|
||||
"google_forms",
|
||||
@@ -47,10 +53,12 @@
|
||||
"google_search",
|
||||
"google_sheets",
|
||||
"google_slides",
|
||||
"google_tasks",
|
||||
"google_translate",
|
||||
"google_vault",
|
||||
"grafana",
|
||||
"grain",
|
||||
"greenhouse",
|
||||
"greptile",
|
||||
"hex",
|
||||
"hubspot",
|
||||
@@ -70,6 +78,8 @@
|
||||
"linear",
|
||||
"linkedin",
|
||||
"linkup",
|
||||
"loops",
|
||||
"luma",
|
||||
"mailchimp",
|
||||
"mailgun",
|
||||
"mem0",
|
||||
|
||||
@@ -11,19 +11,19 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Pipedrive](https://www.pipedrive.com) is a powerful sales-focused CRM platform designed to help sales teams manage leads, track deals, and optimize their sales pipeline. Built with simplicity and effectiveness in mind, Pipedrive has become a favorite among sales professionals and growing businesses worldwide for its intuitive visual pipeline management and actionable sales insights.
|
||||
[Pipedrive](https://www.pipedrive.com) is a sales-focused CRM platform designed to help sales teams manage leads, track deals, and optimize their sales pipeline. Built with simplicity and effectiveness in mind, Pipedrive provides intuitive visual pipeline management and actionable sales insights.
|
||||
|
||||
Pipedrive provides a comprehensive suite of tools for managing the entire sales process from lead capture to deal closure. With its robust API and extensive integration capabilities, Pipedrive enables sales teams to automate repetitive tasks, maintain data consistency, and focus on what matters most—closing deals.
|
||||
With the Pipedrive integration in Sim, you can:
|
||||
|
||||
Key features of Pipedrive include:
|
||||
- **Manage deals**: List, get, create, and update deals in your sales pipeline
|
||||
- **Manage leads**: Get, create, update, and delete leads for prospect tracking
|
||||
- **Track activities**: Get, create, and update sales activities such as calls, meetings, and tasks
|
||||
- **Manage projects**: List and create projects for post-sale delivery tracking
|
||||
- **Access pipelines**: Get pipeline configurations and list deals within specific pipelines
|
||||
- **Retrieve files**: Access files attached to deals, contacts, or other records
|
||||
- **Access email**: Get mail messages and threads linked to CRM records
|
||||
|
||||
- Visual Sales Pipeline: Intuitive drag-and-drop interface for managing deals through customizable sales stages
|
||||
- Lead Management: Comprehensive lead inbox for capturing, qualifying, and converting potential opportunities
|
||||
- Activity Tracking: Sophisticated system for scheduling and tracking calls, meetings, emails, and tasks
|
||||
- Project Management: Built-in project tracking capabilities for post-sale customer success and delivery
|
||||
- Email Integration: Native mailbox integration for seamless communication tracking within the CRM
|
||||
|
||||
In Sim, the Pipedrive integration allows your AI agents to seamlessly interact with your sales workflow. This creates opportunities for automated lead qualification, deal creation and updates, activity scheduling, and pipeline management as part of your AI-powered sales processes. The integration enables agents to create, retrieve, update, and manage deals, leads, activities, and projects programmatically, facilitating intelligent sales automation and ensuring that critical customer information is properly tracked and acted upon. By connecting Sim with Pipedrive, you can build AI agents that maintain sales pipeline visibility, automate routine CRM tasks, qualify leads intelligently, and ensure no opportunities slip through the cracks—enhancing sales team productivity and driving consistent revenue growth.
|
||||
In Sim, the Pipedrive integration enables your agents to interact with your sales workflow as part of automated processes. Agents can qualify leads, manage deals through pipeline stages, schedule activities, and keep CRM data synchronized—enabling intelligent sales automation.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Resend
|
||||
description: Send emails with Resend.
|
||||
description: Send emails and manage contacts with Resend.
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
@@ -27,7 +27,7 @@ In Sim, the Resend integration allows your agents to programmatically send email
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Resend into the workflow. Can send emails. Requires API Key.
|
||||
Integrate Resend into your workflow. Send emails, retrieve email status, manage contacts, and view domains. Requires API Key.
|
||||
|
||||
|
||||
|
||||
@@ -46,6 +46,11 @@ Send an email using your own Resend API key and from address
|
||||
| `subject` | string | Yes | Email subject line |
|
||||
| `body` | string | Yes | Email body content \(plain text or HTML based on contentType\) |
|
||||
| `contentType` | string | No | Content type for the email body: "text" for plain text or "html" for HTML content |
|
||||
| `cc` | string | No | Carbon copy recipient email address |
|
||||
| `bcc` | string | No | Blind carbon copy recipient email address |
|
||||
| `replyTo` | string | No | Reply-to email address |
|
||||
| `scheduledAt` | string | No | Schedule email to be sent later in ISO 8601 format |
|
||||
| `tags` | string | No | Comma-separated key:value pairs for email tags \(e.g., "category:welcome,type:onboarding"\) |
|
||||
| `resendApiKey` | string | Yes | Resend API key for sending emails |
|
||||
|
||||
#### Output
|
||||
@@ -53,8 +58,152 @@ Send an email using your own Resend API key and from address
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether the email was sent successfully |
|
||||
| `id` | string | Email ID returned by Resend |
|
||||
| `to` | string | Recipient email address |
|
||||
| `subject` | string | Email subject |
|
||||
| `body` | string | Email body content |
|
||||
|
||||
### `resend_get_email`
|
||||
|
||||
Retrieve details of a previously sent email by its ID
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `emailId` | string | Yes | The ID of the email to retrieve |
|
||||
| `resendApiKey` | string | Yes | Resend API key |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Email ID |
|
||||
| `from` | string | Sender email address |
|
||||
| `to` | json | Recipient email addresses |
|
||||
| `subject` | string | Email subject |
|
||||
| `html` | string | HTML email content |
|
||||
| `text` | string | Plain text email content |
|
||||
| `cc` | json | CC email addresses |
|
||||
| `bcc` | json | BCC email addresses |
|
||||
| `replyTo` | json | Reply-to email addresses |
|
||||
| `lastEvent` | string | Last event status \(e.g., delivered, bounced\) |
|
||||
| `createdAt` | string | Email creation timestamp |
|
||||
| `scheduledAt` | string | Scheduled send timestamp |
|
||||
| `tags` | json | Email tags as name-value pairs |
|
||||
|
||||
### `resend_create_contact`
|
||||
|
||||
Create a new contact in Resend
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `email` | string | Yes | Email address of the contact |
|
||||
| `firstName` | string | No | First name of the contact |
|
||||
| `lastName` | string | No | Last name of the contact |
|
||||
| `unsubscribed` | boolean | No | Whether the contact is unsubscribed from all broadcasts |
|
||||
| `resendApiKey` | string | Yes | Resend API key |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Created contact ID |
|
||||
|
||||
### `resend_list_contacts`
|
||||
|
||||
List all contacts in Resend
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `resendApiKey` | string | Yes | Resend API key |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `contacts` | json | Array of contacts with id, email, first_name, last_name, created_at, unsubscribed |
|
||||
| `hasMore` | boolean | Whether there are more contacts to retrieve |
|
||||
|
||||
### `resend_get_contact`
|
||||
|
||||
Retrieve details of a contact by ID or email
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `contactId` | string | Yes | The contact ID or email address to retrieve |
|
||||
| `resendApiKey` | string | Yes | Resend API key |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Contact ID |
|
||||
| `email` | string | Contact email address |
|
||||
| `firstName` | string | Contact first name |
|
||||
| `lastName` | string | Contact last name |
|
||||
| `createdAt` | string | Contact creation timestamp |
|
||||
| `unsubscribed` | boolean | Whether the contact is unsubscribed |
|
||||
|
||||
### `resend_update_contact`
|
||||
|
||||
Update an existing contact in Resend
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `contactId` | string | Yes | The contact ID or email address to update |
|
||||
| `firstName` | string | No | Updated first name |
|
||||
| `lastName` | string | No | Updated last name |
|
||||
| `unsubscribed` | boolean | No | Whether the contact should be unsubscribed from all broadcasts |
|
||||
| `resendApiKey` | string | Yes | Resend API key |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Updated contact ID |
|
||||
|
||||
### `resend_delete_contact`
|
||||
|
||||
Delete a contact from Resend by ID or email
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `contactId` | string | Yes | The contact ID or email address to delete |
|
||||
| `resendApiKey` | string | Yes | Resend API key |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Deleted contact ID |
|
||||
| `deleted` | boolean | Whether the contact was successfully deleted |
|
||||
|
||||
### `resend_list_domains`
|
||||
|
||||
List all verified domains in your Resend account
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `resendApiKey` | string | Yes | Resend API key |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `domains` | json | Array of domains with id, name, status, region, and createdAt |
|
||||
| `hasMore` | boolean | Whether there are more domains to retrieve |
|
||||
|
||||
|
||||
|
||||
@@ -13,39 +13,19 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Slack](https://www.slack.com/) is a business communication platform that offers teams a unified place for messaging, tools, and files.
|
||||
|
||||
With Slack, you can:
|
||||
With the Slack integration in Sim, you can:
|
||||
|
||||
- **Automate agent notifications**: Send real-time updates from your Sim agents to any Slack channel
|
||||
- **Create webhook endpoints**: Configure Slack bots as webhooks to trigger Sim workflows from Slack activities
|
||||
- **Enhance agent workflows**: Integrate Slack messaging into your agents to deliver results, alerts, and status updates
|
||||
- **Create and share Slack canvases**: Programmatically generate collaborative documents (canvases) in Slack channels
|
||||
- **Read messages from channels**: Retrieve and process recent messages from any Slack channel for monitoring or workflow triggers
|
||||
- **Manage bot messages**: Update, delete, and add reactions to messages sent by your bot
|
||||
|
||||
In Sim, the Slack integration enables your agents to programmatically interact with Slack with full message management capabilities as part of their workflows:
|
||||
|
||||
- **Send messages**: Agents can send formatted messages to any Slack channel or user, supporting Slack's mrkdwn syntax for rich formatting
|
||||
- **Send messages**: Send formatted messages to any Slack channel or user, supporting Slack's mrkdwn syntax for rich formatting
|
||||
- **Send ephemeral messages**: Send temporary messages visible only to a specific user in a channel
|
||||
- **Update messages**: Edit previously sent bot messages to correct information or provide status updates
|
||||
- **Delete messages**: Remove bot messages when they're no longer needed or contain errors
|
||||
- **Add reactions**: Express sentiment or acknowledgment by adding emoji reactions to any message
|
||||
- **Create canvases**: Create and share Slack canvases (collaborative documents) directly in channels, enabling richer content sharing and documentation
|
||||
- **Read messages**: Read recent messages from channels, allowing for monitoring, reporting, or triggering further actions based on channel activity
|
||||
- **Create canvases**: Create and share Slack canvases (collaborative documents) directly in channels
|
||||
- **Read messages**: Retrieve recent messages from channels or DMs, with filtering by time range
|
||||
- **Manage channels and users**: List channels, members, and users in your Slack workspace
|
||||
- **Download files**: Retrieve files shared in Slack channels for processing or archival
|
||||
|
||||
This allows for powerful automation scenarios such as sending notifications with dynamic updates, managing conversational flows with editable status messages, acknowledging important messages with reactions, and maintaining clean channels by removing outdated bot messages. Your agents can deliver timely information, update messages as workflows progress, create collaborative documents, or alert team members when attention is needed. This integration bridges the gap between your AI workflows and your team's communication, ensuring everyone stays informed with accurate, up-to-date information. By connecting Sim with Slack, you can create agents that keep your team updated with relevant information at the right time, enhance collaboration by sharing and updating insights automatically, and reduce the need for manual status updates—all while leveraging your existing Slack workspace where your team already communicates.
|
||||
|
||||
## Getting Started
|
||||
|
||||
To connect Slack to your Sim workflows:
|
||||
|
||||
1. Sign up or log in at [sim.ai](https://sim.ai)
|
||||
2. Create a new workflow or open an existing one
|
||||
3. Drag a **Slack** block onto your canvas
|
||||
4. Click the credential selector and choose **Connect**
|
||||
5. Authorize Sim to access your Slack workspace
|
||||
6. Select your target channel or user
|
||||
|
||||
Once connected, you can use any of the Slack operations listed below.
|
||||
In Sim, the Slack integration enables your agents to programmatically interact with Slack as part of their workflows. This allows for automation scenarios such as sending notifications with dynamic updates, managing conversational flows with editable status messages, acknowledging important messages with reactions, and maintaining clean channels by removing outdated bot messages. The integration can also be used in trigger mode to start a workflow when a message is sent to a channel.
|
||||
|
||||
## AI-Generated Content
|
||||
|
||||
|
||||
@@ -5,11 +5,12 @@ description: User-defined data tables for storing and querying structured data
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
<BlockInfoCard
|
||||
type="table"
|
||||
color="#10B981"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
Tables allow you to create and manage custom data tables directly within Sim. Store, query, and manipulate structured data within your workflows without needing external database integrations.
|
||||
|
||||
**Why Use Tables?**
|
||||
@@ -26,6 +27,7 @@ Tables allow you to create and manage custom data tables directly within Sim. St
|
||||
- Batch operations for bulk inserts
|
||||
- Bulk updates and deletes by filter
|
||||
- Up to 10,000 rows per table, 100 tables per workspace
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
## Creating Tables
|
||||
|
||||
|
||||
@@ -11,11 +11,21 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Telegram](https://telegram.org) is a secure, cloud-based messaging platform that enables fast and reliable communication across devices and platforms. With over 700 million monthly active users, Telegram has established itself as one of the world's leading messaging services, known for its security, speed, and powerful API capabilities.
|
||||
[Telegram](https://telegram.org) is a secure, cloud-based messaging platform that enables fast and reliable communication across devices. With its powerful Bot API, Telegram provides a robust framework for automated messaging and integration.
|
||||
|
||||
Telegram's Bot API provides a robust framework for creating automated messaging solutions and integrating communication features into applications. With support for rich media, inline keyboards, and custom commands, Telegram bots can facilitate sophisticated interaction patterns and automated workflows.
|
||||
With the Telegram integration in Sim, you can:
|
||||
|
||||
Learn how to create a webhook trigger in Sim that seamlessly initiates workflows from Telegram messages. This tutorial walks you through setting up a webhook, configuring it with Telegram's bot API, and triggering automated actions in real-time. Perfect for streamlining tasks directly from your chat!
|
||||
- **Send messages**: Send text messages to Telegram chats, groups, or channels
|
||||
- **Delete messages**: Remove previously sent messages from a chat
|
||||
- **Send photos**: Share images with optional captions
|
||||
- **Send videos**: Share video files with optional captions
|
||||
- **Send audio**: Share audio files with optional captions
|
||||
- **Send animations**: Share GIF animations with optional captions
|
||||
- **Send documents**: Share files of any type with optional captions
|
||||
|
||||
In Sim, the Telegram integration enables your agents to send messages and rich media to Telegram chats as part of automated workflows. This is ideal for automated notifications, alerts, content distribution, and interactive bot experiences.
|
||||
|
||||
Learn how to create a webhook trigger in Sim that seamlessly initiates workflows from Telegram messages. This tutorial walks you through setting up a webhook, configuring it with Telegram's bot API, and triggering automated actions in real-time.
|
||||
|
||||
<iframe
|
||||
width="100%"
|
||||
@@ -27,7 +37,7 @@ Learn how to create a webhook trigger in Sim that seamlessly initiates workflows
|
||||
allowFullScreen
|
||||
></iframe>
|
||||
|
||||
Learn how to use the Telegram Tool in Sim to seamlessly automate message delivery to any Telegram group. This tutorial walks you through integrating the tool into your workflow, configuring group messaging, and triggering automated updates in real-time. Perfect for enhancing communication directly from your workspace!
|
||||
Learn how to use the Telegram Tool in Sim to seamlessly automate message delivery to any Telegram group. This tutorial walks you through integrating the tool into your workflow, configuring group messaging, and triggering automated updates in real-time.
|
||||
|
||||
<iframe
|
||||
width="100%"
|
||||
@@ -38,15 +48,6 @@ Learn how to use the Telegram Tool in Sim to seamlessly automate message deliver
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
></iframe>
|
||||
|
||||
Key features of Telegram include:
|
||||
|
||||
- Secure Communication: End-to-end encryption and secure cloud storage for messages and media
|
||||
- Bot Platform: Powerful bot API for creating automated messaging solutions and interactive experiences
|
||||
- Rich Media Support: Send and receive messages with text formatting, images, files, and interactive elements
|
||||
- Global Reach: Connect with users worldwide with support for multiple languages and platforms
|
||||
|
||||
In Sim, the Telegram integration enables your agents to leverage these powerful messaging capabilities as part of their workflows. This creates opportunities for automated notifications, alerts, and interactive conversations through Telegram's secure messaging platform. The integration allows agents to send messages programmatically to individuals or channels, enabling timely communication and updates. By connecting Sim with Telegram, you can build intelligent agents that engage users through a secure and widely-adopted messaging platform, perfect for delivering notifications, updates, and interactive communications.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
|
||||
@@ -11,11 +11,19 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[WordPress](https://wordpress.org/) is the world’s leading open-source content management system, making it easy to publish and manage websites, blogs, and all types of online content. With WordPress, you can create and update posts or pages, organize your content with categories and tags, manage media files, moderate comments, and handle user accounts—allowing you to run everything from personal blogs to complex business sites.
|
||||
[WordPress](https://wordpress.org/) is the world’s leading open-source content management system, powering websites, blogs, and online stores of all sizes. WordPress provides a flexible platform for publishing and managing content with extensive plugin and theme support.
|
||||
|
||||
Sim’s integration with WordPress lets your agents automate essential website tasks. You can programmatically create new blog posts with specific titles, content, categories, tags, and featured images. Updating existing posts—such as changing their content, title, or publishing status—is straightforward. You can also publish or save content as drafts, manage static pages, work with media uploads, oversee comments, and assign content to relevant organizational taxonomies.
|
||||
With the WordPress integration in Sim, you can:
|
||||
|
||||
By connecting WordPress to your automations, Sim empowers your agents to streamline content publishing, editorial workflows, and everyday site management—helping you keep your website fresh, organized, and secure without manual effort.
|
||||
- **Manage posts**: Create, update, delete, get, and list blog posts with full control over content, status, categories, and tags
|
||||
- **Manage pages**: Create, update, delete, get, and list static pages
|
||||
- **Handle media**: Upload, get, list, and delete media files such as images, videos, and documents
|
||||
- **Moderate comments**: Create, list, update, and delete comments on posts and pages
|
||||
- **Organize content**: Create and list categories and tags for content taxonomy
|
||||
- **Manage users**: Get the current user, list users, and retrieve user details
|
||||
- **Search content**: Search across all content types on the WordPress site
|
||||
|
||||
In Sim, the WordPress integration enables your agents to automate content publishing and site management as part of automated workflows. Agents can create and publish posts, manage media assets, moderate comments, and organize content—keeping your website fresh and organized without manual effort.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
|
||||
@@ -29,74 +29,65 @@ In Sim, the X integration enables sophisticated social media automation scenario
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate X into the workflow. Can post a new tweet, get tweet details, search tweets, and get user profile.
|
||||
Integrate X into the workflow. Search tweets, manage bookmarks, follow/block/mute users, like and retweet, view trends, and more.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `x_write`
|
||||
### `x_create_tweet`
|
||||
|
||||
Post new tweets, reply to tweets, or create polls on X (Twitter)
|
||||
Create a new tweet, reply, or quote tweet on X
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `text` | string | Yes | The text content of your tweet \(max 280 characters\) |
|
||||
| `replyTo` | string | No | ID of the tweet to reply to \(e.g., 1234567890123456789\) |
|
||||
| `mediaIds` | array | No | Array of media IDs to attach to the tweet |
|
||||
| `poll` | object | No | Poll configuration for the tweet |
|
||||
| `text` | string | Yes | The text content of the tweet \(max 280 characters\) |
|
||||
| `replyToTweetId` | string | No | Tweet ID to reply to |
|
||||
| `quoteTweetId` | string | No | Tweet ID to quote |
|
||||
| `mediaIds` | string | No | Comma-separated media IDs to attach \(up to 4\) |
|
||||
| `replySettings` | string | No | Who can reply: "mentionedUsers", "following", "subscribers", or "verified" |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `tweet` | object | The newly created tweet data |
|
||||
| ↳ `id` | string | Tweet ID |
|
||||
| ↳ `text` | string | Tweet content text |
|
||||
| ↳ `createdAt` | string | Tweet creation timestamp |
|
||||
| ↳ `authorId` | string | ID of the tweet author |
|
||||
| ↳ `conversationId` | string | Conversation thread ID |
|
||||
| ↳ `attachments` | object | Media or poll attachments |
|
||||
| ↳ `mediaKeys` | array | Media attachment keys |
|
||||
| ↳ `pollId` | string | Poll ID if poll attached |
|
||||
| `id` | string | The ID of the created tweet |
|
||||
| `text` | string | The text of the created tweet |
|
||||
|
||||
### `x_read`
|
||||
### `x_delete_tweet`
|
||||
|
||||
Read tweet details, including replies and conversation context
|
||||
Delete a tweet authored by the authenticated user
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `tweetId` | string | Yes | ID of the tweet to read \(e.g., 1234567890123456789\) |
|
||||
| `includeReplies` | boolean | No | Whether to include replies to the tweet |
|
||||
| `tweetId` | string | Yes | The ID of the tweet to delete |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `tweet` | object | The main tweet data |
|
||||
| ↳ `id` | string | Tweet ID |
|
||||
| ↳ `text` | string | Tweet content text |
|
||||
| ↳ `createdAt` | string | Tweet creation timestamp |
|
||||
| ↳ `authorId` | string | ID of the tweet author |
|
||||
| `context` | object | Conversation context including parent and root tweets |
|
||||
| `deleted` | boolean | Whether the tweet was successfully deleted |
|
||||
|
||||
### `x_search`
|
||||
### `x_search_tweets`
|
||||
|
||||
Search for tweets using keywords, hashtags, or advanced queries
|
||||
Search for recent tweets using keywords, hashtags, or advanced query operators
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `query` | string | Yes | Search query \(e.g., "AI news", "#technology", "from:username"\). Supports X search operators |
|
||||
| `maxResults` | number | No | Maximum number of results to return \(e.g., 10, 25, 50\). Default: 10, max: 100 |
|
||||
| `startTime` | string | No | Start time for search \(ISO 8601 format\) |
|
||||
| `endTime` | string | No | End time for search \(ISO 8601 format\) |
|
||||
| `sortOrder` | string | No | Sort order for results \(recency or relevancy\) |
|
||||
| `query` | string | Yes | Search query \(supports operators like "from:", "to:", "#hashtag", "has:images", "is:retweet", "lang:"\) |
|
||||
| `maxResults` | number | No | Maximum number of results \(10-100, default 10\) |
|
||||
| `startTime` | string | No | Oldest UTC timestamp in ISO 8601 format \(e.g., 2024-01-01T00:00:00Z\) |
|
||||
| `endTime` | string | No | Newest UTC timestamp in ISO 8601 format |
|
||||
| `sinceId` | string | No | Returns tweets with ID greater than this |
|
||||
| `untilId` | string | No | Returns tweets with ID less than this |
|
||||
| `sortOrder` | string | No | Sort order: "recency" or "relevancy" |
|
||||
| `nextToken` | string | No | Pagination token for next page of results |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -104,38 +95,748 @@ Search for tweets using keywords, hashtags, or advanced queries
|
||||
| --------- | ---- | ----------- |
|
||||
| `tweets` | array | Array of tweets matching the search query |
|
||||
| ↳ `id` | string | Tweet ID |
|
||||
| ↳ `text` | string | Tweet content |
|
||||
| ↳ `createdAt` | string | Creation timestamp |
|
||||
| ↳ `text` | string | Tweet text content |
|
||||
| ↳ `createdAt` | string | Tweet creation timestamp |
|
||||
| ↳ `authorId` | string | Author user ID |
|
||||
| `includes` | object | Additional data including user profiles and media |
|
||||
| ↳ `conversationId` | string | Conversation thread ID |
|
||||
| ↳ `inReplyToUserId` | string | User ID being replied to |
|
||||
| ↳ `publicMetrics` | object | Engagement metrics |
|
||||
| ↳ `retweetCount` | number | Number of retweets |
|
||||
| ↳ `replyCount` | number | Number of replies |
|
||||
| ↳ `likeCount` | number | Number of likes |
|
||||
| ↳ `quoteCount` | number | Number of quotes |
|
||||
| `includes` | object | Additional data including user profiles |
|
||||
| ↳ `users` | array | Array of user objects referenced in tweets |
|
||||
| ↳ `id` | string | User ID |
|
||||
| ↳ `username` | string | Username without @ symbol |
|
||||
| ↳ `name` | string | Display name |
|
||||
| ↳ `description` | string | User bio |
|
||||
| ↳ `profileImageUrl` | string | Profile image URL |
|
||||
| ↳ `verified` | boolean | Whether the user is verified |
|
||||
| ↳ `metrics` | object | User statistics |
|
||||
| ↳ `followersCount` | number | Number of followers |
|
||||
| ↳ `followingCount` | number | Number of users following |
|
||||
| ↳ `tweetCount` | number | Total number of tweets |
|
||||
| `meta` | object | Search metadata including result count and pagination tokens |
|
||||
| ↳ `resultCount` | number | Number of results returned |
|
||||
| ↳ `newestId` | string | ID of the newest tweet |
|
||||
| ↳ `oldestId` | string | ID of the oldest tweet |
|
||||
| ↳ `nextToken` | string | Pagination token for next page |
|
||||
|
||||
### `x_user`
|
||||
### `x_get_tweets_by_ids`
|
||||
|
||||
Get user profile information
|
||||
Look up multiple tweets by their IDs (up to 100)
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `username` | string | Yes | Username to look up without @ symbol \(e.g., elonmusk, openai\) |
|
||||
| `ids` | string | Yes | Comma-separated tweet IDs \(up to 100\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `user` | object | X user profile information |
|
||||
| `tweets` | array | Array of tweets matching the provided IDs |
|
||||
| ↳ `id` | string | Tweet ID |
|
||||
| ↳ `text` | string | Tweet text content |
|
||||
| ↳ `createdAt` | string | Tweet creation timestamp |
|
||||
| ↳ `authorId` | string | Author user ID |
|
||||
| ↳ `conversationId` | string | Conversation thread ID |
|
||||
| ↳ `inReplyToUserId` | string | User ID being replied to |
|
||||
| ↳ `publicMetrics` | object | Engagement metrics |
|
||||
| ↳ `retweetCount` | number | Number of retweets |
|
||||
| ↳ `replyCount` | number | Number of replies |
|
||||
| ↳ `likeCount` | number | Number of likes |
|
||||
| ↳ `quoteCount` | number | Number of quotes |
|
||||
| `includes` | object | Additional data including user profiles |
|
||||
| ↳ `users` | array | Array of user objects referenced in tweets |
|
||||
| ↳ `id` | string | User ID |
|
||||
| ↳ `username` | string | Username without @ symbol |
|
||||
| ↳ `name` | string | Display name |
|
||||
| ↳ `description` | string | User bio |
|
||||
| ↳ `profileImageUrl` | string | Profile image URL |
|
||||
| ↳ `verified` | boolean | Whether the user is verified |
|
||||
| ↳ `metrics` | object | User statistics |
|
||||
| ↳ `followersCount` | number | Number of followers |
|
||||
| ↳ `followingCount` | number | Number of users following |
|
||||
| ↳ `tweetCount` | number | Total number of tweets |
|
||||
|
||||
### `x_get_quote_tweets`
|
||||
|
||||
Get tweets that quote a specific tweet
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `tweetId` | string | Yes | The tweet ID to get quote tweets for |
|
||||
| `maxResults` | number | No | Maximum number of results \(10-100, default 10\) |
|
||||
| `paginationToken` | string | No | Pagination token for next page |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `tweets` | array | Array of quote tweets |
|
||||
| ↳ `id` | string | Tweet ID |
|
||||
| ↳ `text` | string | Tweet text content |
|
||||
| ↳ `createdAt` | string | Tweet creation timestamp |
|
||||
| ↳ `authorId` | string | Author user ID |
|
||||
| ↳ `conversationId` | string | Conversation thread ID |
|
||||
| ↳ `inReplyToUserId` | string | User ID being replied to |
|
||||
| ↳ `publicMetrics` | object | Engagement metrics |
|
||||
| ↳ `retweetCount` | number | Number of retweets |
|
||||
| ↳ `replyCount` | number | Number of replies |
|
||||
| ↳ `likeCount` | number | Number of likes |
|
||||
| ↳ `quoteCount` | number | Number of quotes |
|
||||
| `meta` | object | Pagination metadata |
|
||||
| ↳ `resultCount` | number | Number of results returned |
|
||||
| ↳ `nextToken` | string | Token for next page |
|
||||
|
||||
### `x_hide_reply`
|
||||
|
||||
Hide or unhide a reply to a tweet authored by the authenticated user
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `tweetId` | string | Yes | The reply tweet ID to hide or unhide |
|
||||
| `hidden` | boolean | Yes | Set to true to hide the reply, false to unhide |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `hidden` | boolean | Whether the reply is now hidden |
|
||||
|
||||
### `x_get_user_tweets`
|
||||
|
||||
Get tweets authored by a specific user
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `userId` | string | Yes | The user ID whose tweets to retrieve |
|
||||
| `maxResults` | number | No | Maximum number of results \(5-100, default 10\) |
|
||||
| `paginationToken` | string | No | Pagination token for next page of results |
|
||||
| `startTime` | string | No | Oldest UTC timestamp in ISO 8601 format |
|
||||
| `endTime` | string | No | Newest UTC timestamp in ISO 8601 format |
|
||||
| `sinceId` | string | No | Returns tweets with ID greater than this |
|
||||
| `untilId` | string | No | Returns tweets with ID less than this |
|
||||
| `exclude` | string | No | Comma-separated types to exclude: "retweets", "replies" |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `tweets` | array | Array of tweets by the user |
|
||||
| ↳ `id` | string | Tweet ID |
|
||||
| ↳ `text` | string | Tweet text content |
|
||||
| ↳ `createdAt` | string | Tweet creation timestamp |
|
||||
| ↳ `authorId` | string | Author user ID |
|
||||
| ↳ `conversationId` | string | Conversation thread ID |
|
||||
| ↳ `inReplyToUserId` | string | User ID being replied to |
|
||||
| ↳ `publicMetrics` | object | Engagement metrics |
|
||||
| ↳ `retweetCount` | number | Number of retweets |
|
||||
| ↳ `replyCount` | number | Number of replies |
|
||||
| ↳ `likeCount` | number | Number of likes |
|
||||
| ↳ `quoteCount` | number | Number of quotes |
|
||||
| `includes` | object | Additional data including user profiles |
|
||||
| ↳ `users` | array | Array of user objects referenced in tweets |
|
||||
| ↳ `id` | string | User ID |
|
||||
| ↳ `username` | string | Username without @ symbol |
|
||||
| ↳ `name` | string | Display name |
|
||||
| ↳ `description` | string | User bio |
|
||||
| ↳ `profileImageUrl` | string | Profile image URL |
|
||||
| ↳ `verified` | boolean | Whether the user is verified |
|
||||
| ↳ `metrics` | object | User statistics |
|
||||
| ↳ `followersCount` | number | Number of followers |
|
||||
| ↳ `followingCount` | number | Number of users following |
|
||||
| ↳ `tweetCount` | number | Total number of tweets |
|
||||
| `meta` | object | Pagination metadata |
|
||||
| ↳ `resultCount` | number | Number of results returned |
|
||||
| ↳ `newestId` | string | ID of the newest tweet |
|
||||
| ↳ `oldestId` | string | ID of the oldest tweet |
|
||||
| ↳ `nextToken` | string | Token for next page |
|
||||
| ↳ `previousToken` | string | Token for previous page |
|
||||
|
||||
### `x_get_user_mentions`
|
||||
|
||||
Get tweets that mention a specific user
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `userId` | string | Yes | The user ID whose mentions to retrieve |
|
||||
| `maxResults` | number | No | Maximum number of results \(5-100, default 10\) |
|
||||
| `paginationToken` | string | No | Pagination token for next page of results |
|
||||
| `startTime` | string | No | Oldest UTC timestamp in ISO 8601 format |
|
||||
| `endTime` | string | No | Newest UTC timestamp in ISO 8601 format |
|
||||
| `sinceId` | string | No | Returns tweets with ID greater than this |
|
||||
| `untilId` | string | No | Returns tweets with ID less than this |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `tweets` | array | Array of tweets mentioning the user |
|
||||
| ↳ `id` | string | Tweet ID |
|
||||
| ↳ `text` | string | Tweet text content |
|
||||
| ↳ `createdAt` | string | Tweet creation timestamp |
|
||||
| ↳ `authorId` | string | Author user ID |
|
||||
| ↳ `conversationId` | string | Conversation thread ID |
|
||||
| ↳ `inReplyToUserId` | string | User ID being replied to |
|
||||
| ↳ `publicMetrics` | object | Engagement metrics |
|
||||
| ↳ `retweetCount` | number | Number of retweets |
|
||||
| ↳ `replyCount` | number | Number of replies |
|
||||
| ↳ `likeCount` | number | Number of likes |
|
||||
| ↳ `quoteCount` | number | Number of quotes |
|
||||
| `includes` | object | Additional data including user profiles |
|
||||
| ↳ `users` | array | Array of user objects referenced in tweets |
|
||||
| ↳ `id` | string | User ID |
|
||||
| ↳ `username` | string | Username without @ symbol |
|
||||
| ↳ `name` | string | Display name |
|
||||
| ↳ `description` | string | User bio |
|
||||
| ↳ `profileImageUrl` | string | Profile image URL |
|
||||
| ↳ `verified` | boolean | Whether the user is verified |
|
||||
| ↳ `metrics` | object | User statistics |
|
||||
| ↳ `followersCount` | number | Number of followers |
|
||||
| ↳ `followingCount` | number | Number of users following |
|
||||
| ↳ `tweetCount` | number | Total number of tweets |
|
||||
| `meta` | object | Pagination metadata |
|
||||
| ↳ `resultCount` | number | Number of results returned |
|
||||
| ↳ `newestId` | string | ID of the newest tweet |
|
||||
| ↳ `oldestId` | string | ID of the oldest tweet |
|
||||
| ↳ `nextToken` | string | Token for next page |
|
||||
| ↳ `previousToken` | string | Token for previous page |
|
||||
|
||||
### `x_get_user_timeline`
|
||||
|
||||
Get the reverse chronological home timeline for the authenticated user
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `userId` | string | Yes | The authenticated user ID |
|
||||
| `maxResults` | number | No | Maximum number of results \(1-100, default 10\) |
|
||||
| `paginationToken` | string | No | Pagination token for next page of results |
|
||||
| `startTime` | string | No | Oldest UTC timestamp in ISO 8601 format |
|
||||
| `endTime` | string | No | Newest UTC timestamp in ISO 8601 format |
|
||||
| `sinceId` | string | No | Returns tweets with ID greater than this |
|
||||
| `untilId` | string | No | Returns tweets with ID less than this |
|
||||
| `exclude` | string | No | Comma-separated types to exclude: "retweets", "replies" |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `tweets` | array | Array of timeline tweets |
|
||||
| ↳ `id` | string | Tweet ID |
|
||||
| ↳ `text` | string | Tweet text content |
|
||||
| ↳ `createdAt` | string | Tweet creation timestamp |
|
||||
| ↳ `authorId` | string | Author user ID |
|
||||
| ↳ `conversationId` | string | Conversation thread ID |
|
||||
| ↳ `inReplyToUserId` | string | User ID being replied to |
|
||||
| ↳ `publicMetrics` | object | Engagement metrics |
|
||||
| ↳ `retweetCount` | number | Number of retweets |
|
||||
| ↳ `replyCount` | number | Number of replies |
|
||||
| ↳ `likeCount` | number | Number of likes |
|
||||
| ↳ `quoteCount` | number | Number of quotes |
|
||||
| `includes` | object | Additional data including user profiles |
|
||||
| ↳ `users` | array | Array of user objects referenced in tweets |
|
||||
| ↳ `id` | string | User ID |
|
||||
| ↳ `username` | string | Username without @ symbol |
|
||||
| ↳ `name` | string | Display name |
|
||||
| ↳ `description` | string | User bio |
|
||||
| ↳ `profileImageUrl` | string | Profile image URL |
|
||||
| ↳ `verified` | boolean | Whether the user is verified |
|
||||
| ↳ `metrics` | object | User statistics |
|
||||
| ↳ `followersCount` | number | Number of followers |
|
||||
| ↳ `followingCount` | number | Number of users following |
|
||||
| ↳ `tweetCount` | number | Total number of tweets |
|
||||
| `meta` | object | Pagination metadata |
|
||||
| ↳ `resultCount` | number | Number of results returned |
|
||||
| ↳ `newestId` | string | ID of the newest tweet |
|
||||
| ↳ `oldestId` | string | ID of the oldest tweet |
|
||||
| ↳ `nextToken` | string | Token for next page |
|
||||
| ↳ `previousToken` | string | Token for previous page |
|
||||
|
||||
### `x_manage_like`
|
||||
|
||||
Like or unlike a tweet on X
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `userId` | string | Yes | The authenticated user ID |
|
||||
| `tweetId` | string | Yes | The tweet ID to like or unlike |
|
||||
| `action` | string | Yes | Action to perform: "like" or "unlike" |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `liked` | boolean | Whether the tweet is now liked |
|
||||
|
||||
### `x_manage_retweet`
|
||||
|
||||
Retweet or unretweet a tweet on X
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `userId` | string | Yes | The authenticated user ID |
|
||||
| `tweetId` | string | Yes | The tweet ID to retweet or unretweet |
|
||||
| `action` | string | Yes | Action to perform: "retweet" or "unretweet" |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `retweeted` | boolean | Whether the tweet is now retweeted |
|
||||
|
||||
### `x_get_liked_tweets`
|
||||
|
||||
Get tweets liked by a specific user
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `userId` | string | Yes | The user ID whose liked tweets to retrieve |
|
||||
| `maxResults` | number | No | Maximum number of results \(5-100\) |
|
||||
| `paginationToken` | string | No | Pagination token for next page |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `tweets` | array | Array of liked tweets |
|
||||
| ↳ `id` | string | Tweet ID |
|
||||
| ↳ `text` | string | Tweet content |
|
||||
| ↳ `createdAt` | string | Creation timestamp |
|
||||
| ↳ `authorId` | string | Author user ID |
|
||||
| `meta` | object | Pagination metadata |
|
||||
| ↳ `resultCount` | number | Number of results returned |
|
||||
| ↳ `nextToken` | string | Token for next page |
|
||||
|
||||
### `x_get_liking_users`
|
||||
|
||||
Get the list of users who liked a specific tweet
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `tweetId` | string | Yes | The tweet ID to get liking users for |
|
||||
| `maxResults` | number | No | Maximum number of results \(1-100, default 100\) |
|
||||
| `paginationToken` | string | No | Pagination token for next page |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `users` | array | Array of users who liked the tweet |
|
||||
| ↳ `id` | string | User ID |
|
||||
| ↳ `username` | string | Username without @ symbol |
|
||||
| ↳ `name` | string | Display name |
|
||||
| ↳ `description` | string | User bio/description |
|
||||
| ↳ `description` | string | User bio |
|
||||
| ↳ `profileImageUrl` | string | Profile image URL |
|
||||
| ↳ `verified` | boolean | Whether the user is verified |
|
||||
| ↳ `metrics` | object | User statistics |
|
||||
| ↳ `followersCount` | number | Number of followers |
|
||||
| ↳ `followingCount` | number | Number of users following |
|
||||
| ↳ `tweetCount` | number | Total number of tweets |
|
||||
| `meta` | object | Pagination metadata |
|
||||
| ↳ `resultCount` | number | Number of results returned |
|
||||
| ↳ `nextToken` | string | Token for next page |
|
||||
|
||||
### `x_get_retweeted_by`
|
||||
|
||||
Get the list of users who retweeted a specific tweet
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `tweetId` | string | Yes | The tweet ID to get retweeters for |
|
||||
| `maxResults` | number | No | Maximum number of results \(1-100, default 100\) |
|
||||
| `paginationToken` | string | No | Pagination token for next page |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `users` | array | Array of users who retweeted the tweet |
|
||||
| ↳ `id` | string | User ID |
|
||||
| ↳ `username` | string | Username without @ symbol |
|
||||
| ↳ `name` | string | Display name |
|
||||
| ↳ `description` | string | User bio |
|
||||
| ↳ `profileImageUrl` | string | Profile image URL |
|
||||
| ↳ `verified` | boolean | Whether the user is verified |
|
||||
| ↳ `metrics` | object | User statistics |
|
||||
| ↳ `followersCount` | number | Number of followers |
|
||||
| ↳ `followingCount` | number | Number of users following |
|
||||
| ↳ `tweetCount` | number | Total number of tweets |
|
||||
| `meta` | object | Metadata |
|
||||
| ↳ `resultCount` | number | Number of results returned |
|
||||
| ↳ `nextToken` | string | Token for next page |
|
||||
|
||||
### `x_get_bookmarks`
|
||||
|
||||
Get bookmarked tweets for the authenticated user
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `userId` | string | Yes | The authenticated user ID |
|
||||
| `maxResults` | number | No | Maximum number of results \(1-100\) |
|
||||
| `paginationToken` | string | No | Pagination token for next page of results |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `tweets` | array | Array of bookmarked tweets |
|
||||
| ↳ `id` | string | Tweet ID |
|
||||
| ↳ `text` | string | Tweet text content |
|
||||
| ↳ `createdAt` | string | Tweet creation timestamp |
|
||||
| ↳ `authorId` | string | Author user ID |
|
||||
| ↳ `conversationId` | string | Conversation thread ID |
|
||||
| ↳ `inReplyToUserId` | string | User ID being replied to |
|
||||
| ↳ `publicMetrics` | object | Engagement metrics |
|
||||
| ↳ `retweetCount` | number | Number of retweets |
|
||||
| ↳ `replyCount` | number | Number of replies |
|
||||
| ↳ `likeCount` | number | Number of likes |
|
||||
| ↳ `quoteCount` | number | Number of quotes |
|
||||
| `includes` | object | Additional data including user profiles |
|
||||
| ↳ `users` | array | Array of user objects referenced in tweets |
|
||||
| ↳ `id` | string | User ID |
|
||||
| ↳ `username` | string | Username without @ symbol |
|
||||
| ↳ `name` | string | Display name |
|
||||
| ↳ `description` | string | User bio |
|
||||
| ↳ `profileImageUrl` | string | Profile image URL |
|
||||
| ↳ `verified` | boolean | Whether the user is verified |
|
||||
| ↳ `metrics` | object | User statistics |
|
||||
| ↳ `followersCount` | number | Number of followers |
|
||||
| ↳ `followingCount` | number | Number of users following |
|
||||
| ↳ `tweetCount` | number | Total number of tweets |
|
||||
| `meta` | object | Pagination metadata |
|
||||
| ↳ `resultCount` | number | Number of results returned |
|
||||
| ↳ `newestId` | string | ID of the newest tweet |
|
||||
| ↳ `oldestId` | string | ID of the oldest tweet |
|
||||
| ↳ `nextToken` | string | Token for next page |
|
||||
| ↳ `previousToken` | string | Token for previous page |
|
||||
|
||||
### `x_create_bookmark`
|
||||
|
||||
Bookmark a tweet for the authenticated user
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `userId` | string | Yes | The authenticated user ID |
|
||||
| `tweetId` | string | Yes | The tweet ID to bookmark |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `bookmarked` | boolean | Whether the tweet was successfully bookmarked |
|
||||
|
||||
### `x_delete_bookmark`
|
||||
|
||||
Remove a tweet from the authenticated user
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `userId` | string | Yes | The authenticated user ID |
|
||||
| `tweetId` | string | Yes | The tweet ID to remove from bookmarks |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `bookmarked` | boolean | Whether the tweet is still bookmarked \(should be false after deletion\) |
|
||||
|
||||
### `x_get_me`
|
||||
|
||||
Get the authenticated user
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `user` | object | Authenticated user profile |
|
||||
| ↳ `id` | string | User ID |
|
||||
| ↳ `username` | string | Username without @ symbol |
|
||||
| ↳ `name` | string | Display name |
|
||||
| ↳ `description` | string | User bio |
|
||||
| ↳ `profileImageUrl` | string | Profile image URL |
|
||||
| ↳ `verified` | boolean | Whether the user is verified |
|
||||
| ↳ `metrics` | object | User statistics |
|
||||
| ↳ `followersCount` | number | Number of followers |
|
||||
| ↳ `followingCount` | number | Number of users following |
|
||||
| ↳ `tweetCount` | number | Total number of tweets |
|
||||
|
||||
### `x_search_users`
|
||||
|
||||
Search for X users by name, username, or bio
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `query` | string | Yes | Search keyword \(1-50 chars, matches name, username, or bio\) |
|
||||
| `maxResults` | number | No | Maximum number of results \(1-1000, default 100\) |
|
||||
| `nextToken` | string | No | Pagination token for next page |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `users` | array | Array of users matching the search query |
|
||||
| ↳ `id` | string | User ID |
|
||||
| ↳ `username` | string | Username without @ symbol |
|
||||
| ↳ `name` | string | Display name |
|
||||
| ↳ `description` | string | User bio |
|
||||
| ↳ `profileImageUrl` | string | Profile image URL |
|
||||
| ↳ `verified` | boolean | Whether the user is verified |
|
||||
| ↳ `metrics` | object | User statistics |
|
||||
| ↳ `followersCount` | number | Number of followers |
|
||||
| ↳ `followingCount` | number | Number of users following |
|
||||
| ↳ `tweetCount` | number | Total number of tweets |
|
||||
| `meta` | object | Search metadata |
|
||||
| ↳ `resultCount` | number | Number of results returned |
|
||||
| ↳ `nextToken` | string | Pagination token for next page |
|
||||
|
||||
### `x_get_followers`
|
||||
|
||||
Get the list of followers for a user
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `userId` | string | Yes | The user ID whose followers to retrieve |
|
||||
| `maxResults` | number | No | Maximum number of results \(1-1000, default 100\) |
|
||||
| `paginationToken` | string | No | Pagination token for next page |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `users` | array | Array of follower user profiles |
|
||||
| ↳ `id` | string | User ID |
|
||||
| ↳ `username` | string | Username without @ symbol |
|
||||
| ↳ `name` | string | Display name |
|
||||
| ↳ `description` | string | User bio |
|
||||
| ↳ `profileImageUrl` | string | Profile image URL |
|
||||
| ↳ `verified` | boolean | Whether the user is verified |
|
||||
| ↳ `metrics` | object | User statistics |
|
||||
| ↳ `followersCount` | number | Number of followers |
|
||||
| ↳ `followingCount` | number | Number of users following |
|
||||
| ↳ `tweetCount` | number | Total number of tweets |
|
||||
| `meta` | object | Pagination metadata |
|
||||
| ↳ `resultCount` | number | Number of results returned |
|
||||
| ↳ `nextToken` | string | Token for next page |
|
||||
|
||||
### `x_get_following`
|
||||
|
||||
Get the list of users that a user is following
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `userId` | string | Yes | The user ID whose following list to retrieve |
|
||||
| `maxResults` | number | No | Maximum number of results \(1-1000, default 100\) |
|
||||
| `paginationToken` | string | No | Pagination token for next page |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `users` | array | Array of users being followed |
|
||||
| ↳ `id` | string | User ID |
|
||||
| ↳ `username` | string | Username without @ symbol |
|
||||
| ↳ `name` | string | Display name |
|
||||
| ↳ `description` | string | User bio |
|
||||
| ↳ `profileImageUrl` | string | Profile image URL |
|
||||
| ↳ `verified` | boolean | Whether the user is verified |
|
||||
| ↳ `metrics` | object | User statistics |
|
||||
| ↳ `followersCount` | number | Number of followers |
|
||||
| ↳ `followingCount` | number | Number of users following |
|
||||
| ↳ `tweetCount` | number | Total number of tweets |
|
||||
| `meta` | object | Pagination metadata |
|
||||
| ↳ `resultCount` | number | Number of results returned |
|
||||
| ↳ `nextToken` | string | Token for next page |
|
||||
|
||||
### `x_manage_follow`
|
||||
|
||||
Follow or unfollow a user on X
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `userId` | string | Yes | The authenticated user ID |
|
||||
| `targetUserId` | string | Yes | The user ID to follow or unfollow |
|
||||
| `action` | string | Yes | Action to perform: "follow" or "unfollow" |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `following` | boolean | Whether you are now following the user |
|
||||
| `pendingFollow` | boolean | Whether the follow request is pending \(for protected accounts\) |
|
||||
|
||||
### `x_manage_block`
|
||||
|
||||
Block or unblock a user on X
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `userId` | string | Yes | The authenticated user ID |
|
||||
| `targetUserId` | string | Yes | The user ID to block or unblock |
|
||||
| `action` | string | Yes | Action to perform: "block" or "unblock" |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `blocking` | boolean | Whether you are now blocking the user |
|
||||
|
||||
### `x_get_blocking`
|
||||
|
||||
Get the list of users blocked by the authenticated user
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `userId` | string | Yes | The authenticated user ID |
|
||||
| `maxResults` | number | No | Maximum number of results \(1-1000\) |
|
||||
| `paginationToken` | string | No | Pagination token for next page |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `users` | array | Array of blocked user profiles |
|
||||
| ↳ `id` | string | User ID |
|
||||
| ↳ `username` | string | Username without @ symbol |
|
||||
| ↳ `name` | string | Display name |
|
||||
| ↳ `description` | string | User bio |
|
||||
| ↳ `profileImageUrl` | string | Profile image URL |
|
||||
| ↳ `verified` | boolean | Whether the user is verified |
|
||||
| ↳ `metrics` | object | User statistics |
|
||||
| ↳ `followersCount` | number | Number of followers |
|
||||
| ↳ `followingCount` | number | Number of users following |
|
||||
| ↳ `tweetCount` | number | Total number of tweets |
|
||||
| `meta` | object | Pagination metadata |
|
||||
| ↳ `resultCount` | number | Number of results returned |
|
||||
| ↳ `nextToken` | string | Token for next page |
|
||||
|
||||
### `x_manage_mute`
|
||||
|
||||
Mute or unmute a user on X
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `userId` | string | Yes | The authenticated user ID |
|
||||
| `targetUserId` | string | Yes | The user ID to mute or unmute |
|
||||
| `action` | string | Yes | Action to perform: "mute" or "unmute" |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `muting` | boolean | Whether you are now muting the user |
|
||||
|
||||
### `x_get_trends_by_woeid`
|
||||
|
||||
Get trending topics for a specific location by WOEID (e.g., 1 for worldwide, 23424977 for US)
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `woeid` | string | Yes | Yahoo Where On Earth ID \(e.g., "1" for worldwide, "23424977" for US, "23424975" for UK\) |
|
||||
| `maxTrends` | number | No | Maximum number of trends to return \(1-50, default 20\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `trends` | array | Array of trending topics |
|
||||
| ↳ `trendName` | string | Name of the trending topic |
|
||||
| ↳ `tweetCount` | number | Number of tweets for this trend |
|
||||
|
||||
### `x_get_personalized_trends`
|
||||
|
||||
Get personalized trending topics for the authenticated user
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `trends` | array | Array of personalized trending topics |
|
||||
| ↳ `trendName` | string | Name of the trending topic |
|
||||
| ↳ `postCount` | number | Number of posts for this trend |
|
||||
| ↳ `category` | string | Category of the trend |
|
||||
| ↳ `trendingSince` | string | ISO 8601 timestamp of when the topic started trending |
|
||||
|
||||
### `x_get_usage`
|
||||
|
||||
Get the API usage data for your X project
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `days` | number | No | Number of days of usage data to return \(1-90, default 7\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `capResetDay` | number | Day of month when usage cap resets |
|
||||
| `projectId` | string | The project ID |
|
||||
| `projectCap` | number | The project tweet consumption cap |
|
||||
| `projectUsage` | number | Total tweets consumed in current period |
|
||||
| `dailyProjectUsage` | array | Daily project usage breakdown |
|
||||
| ↳ `date` | string | Usage date in ISO 8601 format |
|
||||
| ↳ `usage` | number | Number of tweets consumed |
|
||||
| `dailyClientAppUsage` | array | Daily per-app usage breakdown |
|
||||
| ↳ `clientAppId` | string | Client application ID |
|
||||
| ↳ `usage` | array | Daily usage entries for this app |
|
||||
| ↳ `date` | string | Usage date in ISO 8601 format |
|
||||
| ↳ `usage` | number | Number of tweets consumed |
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<svg width="344" height="328" viewBox="0 0 344 328" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M322.641 326.586L335.508 326.586C339.926 326.586 343.508 323.004 343.508 318.586V153.613C343.508 149.195 339.926 145.613 335.508 145.613H228.282C223.864 145.613 220.282 142.031 220.282 137.613V-50H190.282V137.613C190.282 142.031 186.7 145.613 182.282 145.613H-157V318.586C-157 323.004 -153.418 326.586 -149 326.586H322.641Z" fill="#1C1C1C" stroke="#323232" stroke-opacity="0.4" stroke-width="1"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 513 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="471" height="470" viewBox="0 0 471 470" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M471 94.274L471 124.274L365.88 124.274C361.462 124.274 357.88 127.856 357.88 132.274L357.88 225.495C357.88 229.913 354.298 233.495 349.88 233.495L219.5 233.495C215.082 233.495 211.5 237.077 211.5 241.495L211.5 461.5C211.5 465.918 207.918 469.5 203.5 469.5L8.5 469.5C4.082 469.5 0.5 465.918 0.5 461.5L0.5 157.274C0.5 152.856 4.082 149.274 8.5 149.274L184 149.274C188.418 149.274 192 145.692 192 141.274L192 102.274C192 97.856 195.582 94.274 200 94.274L471 94.274Z" fill="#1C1C1C" stroke="#323232" stroke-opacity="0.4" stroke-width="1"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 652 B |
@@ -1,6 +0,0 @@
|
||||
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 768.219 767.667" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Union">
|
||||
<path d="M715.886 0.820573C744.399 1.18152 767.402 24.4083 767.403 53.0071V150.79C767.403 179.389 744.4 202.616 715.886 202.977L715.212 202.982H586.265C583.868 202.982 582.266 205.495 582.968 207.787C583.989 211.117 584.538 214.654 584.538 218.319V345.442C584.538 365.287 568.45 381.375 548.605 381.375H348.717C346.913 381.375 345.45 382.838 345.45 384.642V730.917C345.45 750.763 329.362 766.851 309.517 766.851H36.7503C16.9049 766.851 0.816756 750.763 0.816667 730.917V218.319C0.816667 198.473 16.9048 182.385 36.7503 182.385H164.698C166.503 182.385 167.965 180.922 167.965 179.118V53.0081C167.965 24.1843 191.332 0.817072 220.156 0.816667H715.212L715.886 0.820573ZM220.156 39.9602C212.95 39.9606 207.109 45.8024 207.109 53.0081V160.571C207.109 162.376 208.571 163.838 210.375 163.838H715.212C722.418 163.838 728.26 157.996 728.26 150.79V53.0071C728.26 45.8016 722.418 39.9604 715.212 39.9602H220.156Z" fill="var(--fill-0, #1C1C1C)"/>
|
||||
<path d="M715.886 0.820573L715.896 0.00395244L715.891 0.00391996L715.886 0.820573ZM767.403 53.0071H768.219V53.0071L767.403 53.0071ZM715.886 202.977L715.892 203.793L715.896 203.793L715.886 202.977ZM715.212 202.982V203.798L715.218 203.798L715.212 202.982ZM584.538 345.442H585.355V345.442H584.538ZM548.605 381.375V382.192V382.192V381.375ZM345.45 730.917H346.267V730.917H345.45ZM309.517 766.851V767.667V767.667V766.851ZM0.816667 730.917H0V730.917H0.816667ZM167.965 53.0081L167.148 53.0081V53.0081H167.965ZM220.156 0.816667V0H220.156L220.156 0.816667ZM715.212 0.816667L715.217 0H715.212V0.816667ZM220.156 39.9602V39.1436H220.155L220.156 39.9602ZM207.109 53.0081L206.292 53.008V53.0081H207.109ZM715.212 163.838V164.655V164.655V163.838ZM728.26 53.0071H729.077V53.007L728.26 53.0071ZM715.212 39.9602V39.1436V39.1436V39.9602ZM582.968 207.787L583.749 207.548L582.968 207.787ZM715.886 0.820573L715.876 1.63717C743.943 1.99247 766.585 24.8559 766.586 53.0071L767.403 53.0071L768.219 53.0071C768.219 23.9608 744.856 0.370568 715.896 0.0039717L715.886 0.820573ZM767.403 53.0071H766.586V150.79H767.403H768.219V53.0071H767.403ZM767.403 150.79H766.586C766.586 178.942 743.943 201.805 715.876 202.16L715.886 202.977L715.896 203.793C744.856 203.427 768.219 179.837 768.219 150.79H767.403ZM715.886 202.977L715.88 202.16L715.206 202.165L715.212 202.982L715.218 203.798L715.892 203.793L715.886 202.977ZM715.212 202.982V202.165H586.265V202.982V203.798H715.212V202.982ZM582.968 207.787L582.188 208.026C583.184 211.28 583.722 214.736 583.722 218.319H584.538H585.355C585.355 214.572 584.793 210.955 583.749 207.548L582.968 207.787ZM584.538 218.319H583.722V345.442H584.538H585.355V218.319H584.538ZM584.538 345.442H583.722C583.722 364.836 567.999 380.559 548.605 380.559V381.375V382.192C568.901 382.192 585.355 365.738 585.355 345.442H584.538ZM548.605 381.375V380.559H348.717V381.375V382.192H548.605V381.375ZM345.45 384.642H344.634V730.917H345.45H346.267V384.642H345.45ZM345.45 730.917H344.634C344.634 750.312 328.911 766.034 309.517 766.034V766.851V767.667C329.813 767.667 346.267 751.214 346.267 730.917H345.45ZM309.517 766.851V766.034H36.7503V766.851V767.667H309.517V766.851ZM36.7503 766.851V766.034C17.3559 766.034 1.63342 750.312 1.63333 730.917H0.816667H0C9.16123e-05 751.214 16.4538 767.667 36.7503 767.667V766.851ZM0.816667 730.917H1.63333V218.319H0.816667H0V730.917H0.816667ZM0.816667 218.319H1.63333C1.63333 198.924 17.3559 183.202 36.7503 183.202V182.385V181.568C16.4538 181.568 0 198.022 0 218.319H0.816667ZM36.7503 182.385V183.202H164.698V182.385V181.568H36.7503V182.385ZM167.965 179.118H168.782V53.0081H167.965H167.148V179.118H167.965ZM167.965 53.0081L168.782 53.0081C168.782 24.6353 191.783 1.63373 220.156 1.63333L220.156 0.816667L220.156 0C190.881 0.00041157 167.149 23.7333 167.148 53.0081L167.965 53.0081ZM220.156 0.816667V1.63333H715.212V0.816667V0H220.156V0.816667ZM715.212 0.816667L715.207 1.63332L715.881 1.63723L715.886 0.820573L715.891 0.00391996L715.217 1.37091e-05L715.212 0.816667ZM220.156 39.9602L220.155 39.1436C212.499 39.144 206.292 45.3514 206.292 53.008L207.109 53.0081L207.925 53.0081C207.926 46.2534 213.401 40.7773 220.156 40.7769L220.156 39.9602ZM207.109 53.0081H206.292V160.571H207.109H207.925V53.0081H207.109ZM210.375 163.838V164.655H715.212V163.838V163.021H210.375V163.838ZM715.212 163.838V164.655C722.869 164.655 729.077 158.447 729.077 150.79H728.26H727.443C727.443 157.545 721.967 163.021 715.212 163.021V163.838ZM728.26 150.79H729.077V53.0071H728.26H727.443V150.79H728.26ZM728.26 53.0071L729.077 53.007C729.076 45.3505 722.869 39.1437 715.212 39.1436V39.9602V40.7769C721.967 40.7771 727.443 46.2527 727.443 53.0072L728.26 53.0071ZM715.212 39.9602V39.1436H220.156V39.9602V40.7769H715.212V39.9602ZM207.109 160.571H206.292C206.292 162.827 208.12 164.655 210.375 164.655V163.838V163.021C209.022 163.021 207.925 161.925 207.925 160.571H207.109ZM164.698 182.385V183.202C166.954 183.202 168.782 181.374 168.782 179.118H167.965H167.148C167.148 180.471 166.052 181.568 164.698 181.568V182.385ZM348.717 381.375V380.559C346.462 380.559 344.634 382.387 344.634 384.642H345.45H346.267C346.267 383.289 347.364 382.192 348.717 382.192V381.375ZM586.265 202.982V202.165C583.235 202.165 581.35 205.293 582.188 208.026L582.968 207.787L583.749 207.548C583.182 205.697 584.502 203.798 586.265 203.798V202.982Z" fill="var(--stroke-0, #323232)" fill-opacity="0.4"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 5.4 KiB |
@@ -1,332 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { Badge, ChevronDown } from '@/components/emcn'
|
||||
|
||||
interface DotGridProps {
|
||||
className?: string
|
||||
cols: number
|
||||
rows: number
|
||||
gap?: number
|
||||
}
|
||||
|
||||
function DotGrid({ className, cols, rows, gap = 0 }: DotGridProps) {
|
||||
return (
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className={className}
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: `repeat(${cols}, 1fr)`,
|
||||
gap,
|
||||
placeItems: 'center',
|
||||
}}
|
||||
>
|
||||
{Array.from({ length: cols * rows }, (_, i) => (
|
||||
<div key={i} className='h-[2px] w-[2px] rounded-full bg-[#2A2A2A]' />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const CURSOR_KEYFRAMES = `
|
||||
@keyframes cursorVikhyath {
|
||||
0% { transform: translate(0, 0); }
|
||||
12% { transform: translate(120px, 10px); }
|
||||
24% { transform: translate(80px, 80px); }
|
||||
36% { transform: translate(-10px, 60px); }
|
||||
48% { transform: translate(-15px, -20px); }
|
||||
60% { transform: translate(100px, -40px); }
|
||||
72% { transform: translate(180px, 30px); }
|
||||
84% { transform: translate(50px, 50px); }
|
||||
100% { transform: translate(0, 0); }
|
||||
}
|
||||
@keyframes cursorAlexa {
|
||||
0% { transform: translate(0, 0); }
|
||||
14% { transform: translate(45px, -35px); }
|
||||
28% { transform: translate(-75px, 20px); }
|
||||
42% { transform: translate(25px, -50px); }
|
||||
57% { transform: translate(-65px, 15px); }
|
||||
71% { transform: translate(35px, -30px); }
|
||||
85% { transform: translate(-30px, -10px); }
|
||||
100% { transform: translate(0, 0); }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
@keyframes cursorVikhyath { 0%, 100% { transform: none; } }
|
||||
@keyframes cursorAlexa { 0%, 100% { transform: none; } }
|
||||
}
|
||||
`
|
||||
|
||||
const CURSOR_ARROW_PATH =
|
||||
'M17.135 2.198L12.978 14.821C12.478 16.339 10.275 16.16 10.028 14.581L9.106 8.703C9.01 8.092 8.554 7.599 7.952 7.457L1.591 5.953C0 5.577 0.039 3.299 1.642 2.978L15.39 0.229C16.534 0 17.499 1.09 17.135 2.198Z'
|
||||
|
||||
const CURSOR_ARROW_MIRRORED_PATH =
|
||||
'M0.365 2.198L4.522 14.821C5.022 16.339 7.225 16.16 7.472 14.58L8.394 8.702C8.49 8.091 8.946 7.599 9.548 7.456L15.909 5.953C17.5 5.577 17.461 3.299 15.857 2.978L2.11 0.228C0.966 0 0.001 1.09 0.365 2.198Z'
|
||||
|
||||
function CursorArrow({ fill }: { fill: string }) {
|
||||
return (
|
||||
<svg width='23.15' height='21.1' viewBox='0 0 17.5 16.4' fill='none'>
|
||||
<path d={fill === '#2ABBF8' ? CURSOR_ARROW_PATH : CURSOR_ARROW_MIRRORED_PATH} fill={fill} />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function VikhyathCursor() {
|
||||
return (
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='pointer-events-none absolute'
|
||||
style={{
|
||||
top: '27.47%',
|
||||
left: '25%',
|
||||
animation: 'cursorVikhyath 16s ease-in-out infinite',
|
||||
willChange: 'transform',
|
||||
}}
|
||||
>
|
||||
<div className='relative h-[37.14px] w-[79.18px]'>
|
||||
<div className='absolute top-0 left-[56.02px]'>
|
||||
<CursorArrow fill='#2ABBF8' />
|
||||
</div>
|
||||
<div className='-left-[4px] absolute top-[18px] flex items-center rounded bg-[#2ABBF8] px-[5px] py-[3px] font-[420] font-season text-[#202020] text-[14px] leading-[100%] tracking-[-0.02em]'>
|
||||
Vikhyath
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function AlexaCursor() {
|
||||
return (
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='pointer-events-none absolute'
|
||||
style={{
|
||||
top: '66.80%',
|
||||
left: '49%',
|
||||
animation: 'cursorAlexa 13s ease-in-out infinite',
|
||||
willChange: 'transform',
|
||||
}}
|
||||
>
|
||||
<div className='relative h-[35.09px] w-[62.16px]'>
|
||||
<div className='absolute top-0 left-0'>
|
||||
<CursorArrow fill='#FFCC02' />
|
||||
</div>
|
||||
<div className='absolute top-[16px] left-[23px] flex items-center rounded bg-[#FFCC02] px-[5px] py-[3px] font-[420] font-season text-[#202020] text-[14px] leading-[100%] tracking-[-0.02em]'>
|
||||
Alexa
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface YouCursorProps {
|
||||
x: number
|
||||
y: number
|
||||
visible: boolean
|
||||
}
|
||||
|
||||
function YouCursor({ x, y, visible }: YouCursorProps) {
|
||||
if (!visible) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='pointer-events-none fixed z-50'
|
||||
style={{
|
||||
left: x,
|
||||
top: y,
|
||||
transform: 'translate(-2px, -2px)',
|
||||
}}
|
||||
>
|
||||
<svg width='23.15' height='21.1' viewBox='0 0 17.5 16.4' fill='none'>
|
||||
<path d={CURSOR_ARROW_MIRRORED_PATH} fill='#33C482' />
|
||||
</svg>
|
||||
<div className='absolute top-[16px] left-[23px] flex items-center rounded bg-[#33C482] px-[5px] py-[3px] font-[420] font-season text-[#202020] text-[14px] leading-[100%] tracking-[-0.02em]'>
|
||||
You
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Collaboration section — team workflows and real-time collaboration.
|
||||
*
|
||||
* SEO:
|
||||
* - `<section id="collaboration" aria-labelledby="collaboration-heading">`.
|
||||
* - `<h2 id="collaboration-heading">` for the section title.
|
||||
* - Product visuals use `<figure>` with `<figcaption>` and descriptive `alt` text.
|
||||
*
|
||||
* GEO:
|
||||
* - Name specific capabilities (version control, shared workspaces, RBAC, audit logs).
|
||||
* - Lead with a summary so AI can answer "Does Sim support team collaboration?".
|
||||
* - Reference "Sim" by name per capability ("Sim's real-time collaboration").
|
||||
*/
|
||||
|
||||
const CURSOR_LERP_FACTOR = 0.3
|
||||
|
||||
export default function Collaboration() {
|
||||
const [cursorPos, setCursorPos] = useState({ x: 0, y: 0 })
|
||||
const [isHovering, setIsHovering] = useState(false)
|
||||
const sectionRef = useRef<HTMLElement>(null)
|
||||
const targetPos = useRef({ x: 0, y: 0 })
|
||||
const animationRef = useRef<number>(0)
|
||||
|
||||
useEffect(() => {
|
||||
const animate = () => {
|
||||
setCursorPos((prev) => ({
|
||||
x: prev.x + (targetPos.current.x - prev.x) * CURSOR_LERP_FACTOR,
|
||||
y: prev.y + (targetPos.current.y - prev.y) * CURSOR_LERP_FACTOR,
|
||||
}))
|
||||
animationRef.current = requestAnimationFrame(animate)
|
||||
}
|
||||
|
||||
if (isHovering) {
|
||||
animationRef.current = requestAnimationFrame(animate)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (animationRef.current) {
|
||||
cancelAnimationFrame(animationRef.current)
|
||||
}
|
||||
}
|
||||
}, [isHovering])
|
||||
|
||||
const handleMouseMove = useCallback((e: React.MouseEvent) => {
|
||||
targetPos.current = { x: e.clientX, y: e.clientY }
|
||||
}, [])
|
||||
|
||||
const handleMouseEnter = useCallback((e: React.MouseEvent) => {
|
||||
targetPos.current = { x: e.clientX, y: e.clientY }
|
||||
setCursorPos({ x: e.clientX, y: e.clientY })
|
||||
setIsHovering(true)
|
||||
}, [])
|
||||
|
||||
const handleMouseLeave = useCallback(() => {
|
||||
setIsHovering(false)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<section
|
||||
ref={sectionRef}
|
||||
id='collaboration'
|
||||
aria-labelledby='collaboration-heading'
|
||||
className='bg-[#1C1C1C]'
|
||||
style={{ cursor: isHovering ? 'none' : 'auto' }}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<YouCursor x={cursorPos.x} y={cursorPos.y} visible={isHovering} />
|
||||
<style dangerouslySetInnerHTML={{ __html: CURSOR_KEYFRAMES }} />
|
||||
|
||||
<DotGrid
|
||||
className='border-[#2A2A2A] border-y bg-[#1C1C1C] p-[6px]'
|
||||
cols={120}
|
||||
rows={1}
|
||||
gap={6}
|
||||
/>
|
||||
|
||||
<div className='relative overflow-hidden'>
|
||||
<Link
|
||||
href='/studio/multiplayer'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='absolute bottom-10 left-4 z-20 flex cursor-none items-center gap-[14px] rounded-[5px] border border-[#2A2A2A] bg-[#1C1C1C] px-[12px] py-[10px] transition-colors hover:border-[#3d3d3d] hover:bg-[#232323] sm:left-8 md:left-[80px]'
|
||||
>
|
||||
<div className='relative h-7 w-11 shrink-0'>
|
||||
<Image src='/landing/multiplayer-cursors.svg' alt='' fill className='object-contain' />
|
||||
</div>
|
||||
<div className='flex flex-col gap-[2px]'>
|
||||
<span className='font-[430] font-season text-[#F6F6F0]/50 text-[12px] uppercase leading-[100%] tracking-[0.08em]'>
|
||||
Blog
|
||||
</span>
|
||||
<span className='font-[430] font-season text-[#F6F6F0] text-[14px] leading-[125%] tracking-[0.02em]'>
|
||||
How we built realtime collaboration
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<div className='grid grid-cols-[auto_1fr]'>
|
||||
<div className='flex flex-col items-start gap-3 px-4 pt-[100px] pb-8 sm:gap-4 sm:px-8 md:gap-[20px] md:px-[80px]'>
|
||||
<Badge
|
||||
variant='blue'
|
||||
size='md'
|
||||
dot
|
||||
className='bg-[#33C482]/10 font-season text-[#33C482] uppercase tracking-[0.02em]'
|
||||
>
|
||||
Teams
|
||||
</Badge>
|
||||
|
||||
<h2
|
||||
id='collaboration-heading'
|
||||
className='font-[430] font-season text-[32px] text-white leading-[100%] tracking-[-0.02em] sm:text-[36px] md:text-[40px]'
|
||||
>
|
||||
Realtime
|
||||
<br />
|
||||
collaboration
|
||||
</h2>
|
||||
|
||||
<p className='font-[430] font-season text-[#F6F6F0]/50 text-[14px] leading-[125%] tracking-[0.02em] sm:text-[16px]'>
|
||||
Grab your team. Build agents together <br /> in real-time inside your workspace.
|
||||
</p>
|
||||
|
||||
<Link
|
||||
href='/signup'
|
||||
className='group/cta mt-[12px] inline-flex h-[32px] cursor-none items-center gap-[6px] rounded-[5px] border border-[#33C482] bg-[#33C482] px-[10px] font-[430] font-season text-[14px] text-black transition-[filter] hover:brightness-110'
|
||||
>
|
||||
Build together
|
||||
<span className='relative h-[10px] w-[10px] shrink-0'>
|
||||
<ChevronDown className='-rotate-90 absolute inset-0 h-[10px] w-[10px] transition-opacity duration-150 group-hover/cta:opacity-0' />
|
||||
<svg
|
||||
className='absolute inset-0 h-[10px] w-[10px] opacity-0 transition-opacity duration-150 group-hover/cta:opacity-100'
|
||||
viewBox='0 0 10 10'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<path
|
||||
d='M1 5H8M5.5 2L8.5 5L5.5 8'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.33'
|
||||
strokeLinecap='square'
|
||||
strokeLinejoin='miter'
|
||||
fill='none'
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<figure className='pointer-events-none relative h-[600px] w-full'>
|
||||
<div className='-left-[18%] absolute inset-y-0 min-w-full'>
|
||||
<Image
|
||||
src='/landing/collaboration-visual.svg'
|
||||
alt='Collaboration visual showing team workflows with real-time editing, shared cursors, and version control interface'
|
||||
width={876}
|
||||
height={480}
|
||||
className='h-full w-auto min-w-[100vw] object-left'
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
<div className='hidden lg:block'>
|
||||
<VikhyathCursor />
|
||||
<AlexaCursor />
|
||||
</div>
|
||||
<figcaption className='sr-only'>
|
||||
Sim collaboration interface with real-time cursors, shared workspace, and team
|
||||
presence indicators
|
||||
</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DotGrid
|
||||
className='border-[#2A2A2A] border-y bg-[#1C1C1C] p-[6px]'
|
||||
cols={120}
|
||||
rows={1}
|
||||
gap={6}
|
||||
/>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
/**
|
||||
* Enterprise section — compliance, scale, and security messaging.
|
||||
*
|
||||
* SEO:
|
||||
* - `<section id="enterprise" aria-labelledby="enterprise-heading">`.
|
||||
* - `<h2 id="enterprise-heading">` for the section title.
|
||||
* - Compliance certs (SOC2, HIPAA) as visible `<strong>` text.
|
||||
* - Enterprise CTA links to contact form via `<a>` with `rel="noopener noreferrer"`.
|
||||
*
|
||||
* GEO:
|
||||
* - Entity-rich: "Sim is SOC2 and HIPAA compliant" — not "We are compliant."
|
||||
* - `<ul>` checklist of features (SSO, RBAC, audit logs, SLA, on-premise deployment)
|
||||
* as an atomic answer block for "What enterprise features does Sim offer?".
|
||||
*/
|
||||
export default function Enterprise() {
|
||||
return null
|
||||
}
|
||||
@@ -1,229 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import Image from 'next/image'
|
||||
import { Badge } from '@/components/emcn'
|
||||
|
||||
function hexToRgba(hex: string, alpha: number): string {
|
||||
const r = Number.parseInt(hex.slice(1, 3), 16)
|
||||
const g = Number.parseInt(hex.slice(3, 5), 16)
|
||||
const b = Number.parseInt(hex.slice(5, 7), 16)
|
||||
return `rgba(${r},${g},${b},${alpha})`
|
||||
}
|
||||
|
||||
const FEATURE_TABS = [
|
||||
{
|
||||
label: 'Integrations',
|
||||
color: '#FA4EDF',
|
||||
segments: [
|
||||
[0.3, 8],
|
||||
[0.25, 10],
|
||||
[0.45, 12],
|
||||
[0.5, 8],
|
||||
[0.65, 10],
|
||||
[0.8, 12],
|
||||
[0.75, 8],
|
||||
[0.95, 10],
|
||||
[1, 12],
|
||||
[0.85, 10],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Copilot',
|
||||
color: '#2ABBF8',
|
||||
segments: [
|
||||
[0.25, 12],
|
||||
[0.4, 10],
|
||||
[0.35, 8],
|
||||
[0.55, 12],
|
||||
[0.7, 10],
|
||||
[0.85, 8],
|
||||
[1, 14],
|
||||
[0.9, 12],
|
||||
[1, 14],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Models',
|
||||
color: '#00F701',
|
||||
badgeColor: '#22C55E',
|
||||
segments: [
|
||||
[0.2, 6],
|
||||
[0.35, 10],
|
||||
[0.3, 8],
|
||||
[0.5, 10],
|
||||
[0.6, 8],
|
||||
[0.75, 12],
|
||||
[0.85, 10],
|
||||
[1, 8],
|
||||
[0.9, 12],
|
||||
[1, 10],
|
||||
[0.95, 6],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Deploy',
|
||||
color: '#FFCC02',
|
||||
badgeColor: '#EAB308',
|
||||
segments: [
|
||||
[0.3, 12],
|
||||
[0.25, 8],
|
||||
[0.4, 10],
|
||||
[0.55, 10],
|
||||
[0.7, 8],
|
||||
[0.6, 10],
|
||||
[0.85, 12],
|
||||
[1, 10],
|
||||
[0.9, 10],
|
||||
[1, 10],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Logs',
|
||||
color: '#FF6B35',
|
||||
segments: [
|
||||
[0.25, 10],
|
||||
[0.35, 8],
|
||||
[0.3, 10],
|
||||
[0.5, 10],
|
||||
[0.65, 8],
|
||||
[0.8, 12],
|
||||
[0.9, 10],
|
||||
[1, 10],
|
||||
[0.85, 12],
|
||||
[1, 10],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Knowledge Base',
|
||||
color: '#8B5CF6',
|
||||
segments: [
|
||||
[0.3, 10],
|
||||
[0.25, 8],
|
||||
[0.4, 10],
|
||||
[0.5, 10],
|
||||
[0.65, 10],
|
||||
[0.8, 10],
|
||||
[0.9, 12],
|
||||
[1, 10],
|
||||
[0.95, 10],
|
||||
[1, 10],
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
function DotGrid({
|
||||
cols,
|
||||
rows,
|
||||
width,
|
||||
borderLeft,
|
||||
}: {
|
||||
cols: number
|
||||
rows: number
|
||||
width?: number
|
||||
borderLeft?: boolean
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className={`shrink-0 bg-[#FDFDFD] p-[6px] ${borderLeft ? 'border-[#E9E9E9] border-l' : ''}`}
|
||||
style={{
|
||||
width: width ? `${width}px` : undefined,
|
||||
display: 'grid',
|
||||
gridTemplateColumns: `repeat(${cols}, 1fr)`,
|
||||
gap: 4,
|
||||
placeItems: 'center',
|
||||
}}
|
||||
>
|
||||
{Array.from({ length: cols * rows }, (_, i) => (
|
||||
<div key={i} className='h-[2px] w-[2px] rounded-full bg-[#DEDEDE]' />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Features() {
|
||||
const [activeTab, setActiveTab] = useState(0)
|
||||
|
||||
return (
|
||||
<section
|
||||
id='features'
|
||||
aria-labelledby='features-heading'
|
||||
className='relative overflow-hidden bg-[#F6F6F6] pb-[144px]'
|
||||
>
|
||||
<div aria-hidden='true' className='absolute top-0 left-0 w-full'>
|
||||
<Image
|
||||
src='/landing/features-transition.svg'
|
||||
alt=''
|
||||
width={1440}
|
||||
height={366}
|
||||
className='h-auto w-full'
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='relative z-10 pt-[100px]'>
|
||||
<div className='flex flex-col items-start gap-[20px] px-[80px]'>
|
||||
<Badge
|
||||
variant='blue'
|
||||
size='md'
|
||||
dot
|
||||
className='font-season uppercase tracking-[0.02em] transition-colors duration-200'
|
||||
style={{
|
||||
color: FEATURE_TABS[activeTab].badgeColor ?? FEATURE_TABS[activeTab].color,
|
||||
backgroundColor: hexToRgba(
|
||||
FEATURE_TABS[activeTab].badgeColor ?? FEATURE_TABS[activeTab].color,
|
||||
0.1
|
||||
),
|
||||
}}
|
||||
>
|
||||
Features
|
||||
</Badge>
|
||||
<h2
|
||||
id='features-heading'
|
||||
className='font-[430] font-season text-[#1C1C1C] text-[40px] leading-[100%] tracking-[-0.02em]'
|
||||
>
|
||||
Power your AI workforce
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className='mt-[73px] flex h-[68px] overflow-hidden border border-[#E9E9E9]'>
|
||||
<DotGrid cols={10} rows={8} width={80} />
|
||||
|
||||
<div role='tablist' aria-label='Feature categories' className='flex flex-1'>
|
||||
{FEATURE_TABS.map((tab, index) => (
|
||||
<button
|
||||
key={tab.label}
|
||||
type='button'
|
||||
role='tab'
|
||||
aria-selected={index === activeTab}
|
||||
onClick={() => setActiveTab(index)}
|
||||
className='relative flex h-full flex-1 items-center justify-center border-[#E9E9E9] border-l font-medium font-season text-[#212121] text-[14px] uppercase'
|
||||
style={{ backgroundColor: index === activeTab ? '#FDFDFD' : '#F6F6F6' }}
|
||||
>
|
||||
{tab.label}
|
||||
{index === activeTab && (
|
||||
<div className='absolute right-0 bottom-0 left-0 flex h-[6px]'>
|
||||
{tab.segments.map(([opacity, width], i) => (
|
||||
<div
|
||||
key={i}
|
||||
className='h-full shrink-0'
|
||||
style={{
|
||||
width: `${width}%`,
|
||||
backgroundColor: tab.color,
|
||||
opacity,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<DotGrid cols={10} rows={8} width={80} borderLeft />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
/**
|
||||
* Landing page footer — navigation, legal links, and entity reinforcement.
|
||||
*
|
||||
* SEO:
|
||||
* - `<footer role="contentinfo">` with `<nav aria-label="Footer navigation">`.
|
||||
* - Link groups under semantic headings (`<h3>`). All links are `<Link>` or `<a>` with `href`.
|
||||
* - External links include `rel="noopener noreferrer"`.
|
||||
* - Legal links (Privacy, Terms) must be crawlable (trust signals).
|
||||
*
|
||||
* GEO:
|
||||
* - Include "Sim — Build AI agents and run your agentic workforce" as visible text (entity reinforcement).
|
||||
* - Social links (X, GitHub, LinkedIn, Discord) must match `sameAs` in structured-data.tsx.
|
||||
* - Link to all major pages: Docs, Pricing, Enterprise, Careers, Changelog (internal link graph).
|
||||
* - Display compliance badges (SOC2, HIPAA) and status page link as visible trust signals.
|
||||
*/
|
||||
export default function Footer() {
|
||||
return null
|
||||
}
|
||||
@@ -1,604 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { motion, type Variants } from 'framer-motion'
|
||||
|
||||
/** Stagger between each block appearing (seconds). */
|
||||
const ENTER_STAGGER = 0.06
|
||||
|
||||
/** Duration of each block's fade-in (seconds). */
|
||||
const ENTER_DURATION = 0.3
|
||||
|
||||
/** Stagger between each block disappearing (seconds). */
|
||||
const EXIT_STAGGER = 0.12
|
||||
|
||||
/** Duration of each block's fade-out (seconds). */
|
||||
const EXIT_DURATION = 0.5
|
||||
|
||||
/** Shared corner radius for all decorative rects. */
|
||||
const RX = '2.59574'
|
||||
|
||||
/** Hold time after the initial enter animation before cycling starts (ms). */
|
||||
const INITIAL_HOLD_MS = 2500
|
||||
|
||||
/** Pause between an exit completing and the next enter starting (ms). */
|
||||
const TRANSITION_PAUSE_MS = 400
|
||||
|
||||
/** Hold time between successive transitions (ms). */
|
||||
const HOLD_BETWEEN_MS = 2500
|
||||
|
||||
/** Animation state for a block group. */
|
||||
export type BlockAnimState = 'entering' | 'visible' | 'exiting' | 'hidden'
|
||||
|
||||
/** Positions around the hero where block groups can appear. */
|
||||
export type BlockPosition = 'topRight' | 'left' | 'rightEdge' | 'rightSide' | 'topLeft'
|
||||
|
||||
/** Attributes for a single animated SVG rect. */
|
||||
interface BlockRect {
|
||||
opacity: number
|
||||
width: string
|
||||
height: string
|
||||
fill: string
|
||||
x?: string
|
||||
y?: string
|
||||
transform?: string
|
||||
}
|
||||
|
||||
const containerVariants: Variants = {
|
||||
hidden: {},
|
||||
visible: { transition: { staggerChildren: ENTER_STAGGER } },
|
||||
exit: { transition: { staggerChildren: EXIT_STAGGER } },
|
||||
}
|
||||
|
||||
const containerVariantsReverseExit: Variants = {
|
||||
hidden: {},
|
||||
visible: { transition: { staggerChildren: ENTER_STAGGER } },
|
||||
exit: { transition: { staggerChildren: EXIT_STAGGER, staggerDirection: -1 } },
|
||||
}
|
||||
|
||||
const blockVariants: Variants = {
|
||||
hidden: { opacity: 0, transition: { duration: 0 } },
|
||||
visible: (targetOpacity: number) => ({
|
||||
opacity: targetOpacity,
|
||||
transition: { duration: ENTER_DURATION },
|
||||
}),
|
||||
exit: {
|
||||
opacity: 0,
|
||||
transition: { duration: EXIT_DURATION },
|
||||
},
|
||||
}
|
||||
|
||||
/** Maps a BlockAnimState to the framer-motion animate value. */
|
||||
function toAnimateValue(state: BlockAnimState): string {
|
||||
if (state === 'entering' || state === 'visible') return 'visible'
|
||||
if (state === 'exiting') return 'exit'
|
||||
return 'hidden'
|
||||
}
|
||||
|
||||
/** Shared SVG wrapper that staggers child rects in and out. */
|
||||
function AnimatedBlocksSvg({
|
||||
width,
|
||||
height,
|
||||
viewBox,
|
||||
rects,
|
||||
animState = 'entering',
|
||||
reverseExit = false,
|
||||
}: {
|
||||
width: number
|
||||
height: number
|
||||
viewBox: string
|
||||
rects: readonly BlockRect[]
|
||||
animState?: BlockAnimState
|
||||
reverseExit?: boolean
|
||||
}) {
|
||||
return (
|
||||
<motion.svg
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox={viewBox}
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
className='h-auto w-full'
|
||||
initial='hidden'
|
||||
animate={toAnimateValue(animState)}
|
||||
variants={reverseExit ? containerVariantsReverseExit : containerVariants}
|
||||
>
|
||||
{rects.map((r, i) => (
|
||||
<motion.rect
|
||||
key={i}
|
||||
variants={blockVariants}
|
||||
custom={r.opacity}
|
||||
x={r.x}
|
||||
y={r.y}
|
||||
width={r.width}
|
||||
height={r.height}
|
||||
rx={RX}
|
||||
fill={r.fill}
|
||||
transform={r.transform}
|
||||
/>
|
||||
))}
|
||||
</motion.svg>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Rect data for the top-right position.
|
||||
* Two-row horizontal strip, ordered left-to-right.
|
||||
*/
|
||||
const TOP_RIGHT_RECTS: readonly BlockRect[] = [
|
||||
{ opacity: 1, x: '0', y: '0', width: '16.8626', height: '33.7252', fill: '#2ABBF8' },
|
||||
{ opacity: 0.6, x: '0', y: '0', width: '85.3433', height: '16.8626', fill: '#2ABBF8' },
|
||||
{ opacity: 1, x: '0', y: '0', width: '16.8626', height: '16.8626', fill: '#2ABBF8' },
|
||||
{ opacity: 0.6, x: '34.2403', y: '0', width: '34.2403', height: '33.7252', fill: '#2ABBF8' },
|
||||
{ opacity: 1, x: '34.2403', y: '0', width: '16.8626', height: '16.8626', fill: '#2ABBF8' },
|
||||
{ opacity: 1, x: '51.6188', y: '16.8626', width: '16.8626', height: '16.8626', fill: '#2ABBF8' },
|
||||
{ opacity: 1, x: '68.4812', y: '0', width: '54.6502', height: '16.8626', fill: '#00F701' },
|
||||
{ opacity: 0.6, x: '106.268', y: '0', width: '34.2403', height: '33.7252', fill: '#00F701' },
|
||||
{ opacity: 0.6, x: '106.268', y: '0', width: '51.103', height: '16.8626', fill: '#00F701' },
|
||||
{ opacity: 1, x: '123.6484', y: '16.8626', width: '16.8626', height: '16.8626', fill: '#00F701' },
|
||||
{ opacity: 0.6, x: '157.371', y: '0', width: '34.2403', height: '16.8626', fill: '#FFCC02' },
|
||||
{ opacity: 1, x: '157.371', y: '0', width: '16.8626', height: '16.8626', fill: '#FFCC02' },
|
||||
{ opacity: 0.6, x: '208.993', y: '0', width: '68.4805', height: '16.8626', fill: '#FA4EDF' },
|
||||
{ opacity: 0.6, x: '209.137', y: '0', width: '16.8626', height: '33.7252', fill: '#FA4EDF' },
|
||||
{ opacity: 0.6, x: '243.233', y: '0', width: '34.2403', height: '33.7252', fill: '#FA4EDF' },
|
||||
{ opacity: 1, x: '243.233', y: '0', width: '16.8626', height: '16.8626', fill: '#FA4EDF' },
|
||||
{ opacity: 0.6, x: '260.096', y: '0', width: '34.04', height: '16.8626', fill: '#FA4EDF' },
|
||||
{ opacity: 1, x: '260.611', y: '16.8626', width: '16.8626', height: '16.8626', fill: '#FA4EDF' },
|
||||
]
|
||||
|
||||
/**
|
||||
* Rect data for the top-left position.
|
||||
* Same two-row structure as top-right with rotated colour palette:
|
||||
* blue→green, green→yellow, yellow→pink, pink→blue.
|
||||
*/
|
||||
const TOP_LEFT_RECTS: readonly BlockRect[] = [
|
||||
{ opacity: 1, x: '0', y: '0', width: '16.8626', height: '33.7252', fill: '#00F701' },
|
||||
{ opacity: 0.6, x: '0', y: '0', width: '85.3433', height: '16.8626', fill: '#00F701' },
|
||||
{ opacity: 1, x: '0', y: '0', width: '16.8626', height: '16.8626', fill: '#00F701' },
|
||||
{ opacity: 0.6, x: '34.2403', y: '0', width: '34.2403', height: '33.7252', fill: '#00F701' },
|
||||
{ opacity: 1, x: '34.2403', y: '0', width: '16.8626', height: '16.8626', fill: '#00F701' },
|
||||
{ opacity: 1, x: '51.6188', y: '16.8626', width: '16.8626', height: '16.8626', fill: '#00F701' },
|
||||
{ opacity: 1, x: '68.4812', y: '0', width: '54.6502', height: '16.8626', fill: '#FFCC02' },
|
||||
{ opacity: 0.6, x: '106.268', y: '0', width: '34.2403', height: '33.7252', fill: '#FFCC02' },
|
||||
{ opacity: 0.6, x: '106.268', y: '0', width: '51.103', height: '16.8626', fill: '#FFCC02' },
|
||||
{ opacity: 1, x: '123.6484', y: '16.8626', width: '16.8626', height: '16.8626', fill: '#FFCC02' },
|
||||
{ opacity: 0.6, x: '157.371', y: '0', width: '34.2403', height: '16.8626', fill: '#FA4EDF' },
|
||||
{ opacity: 1, x: '157.371', y: '0', width: '16.8626', height: '16.8626', fill: '#FA4EDF' },
|
||||
{ opacity: 0.6, x: '208.993', y: '0', width: '68.4805', height: '16.8626', fill: '#2ABBF8' },
|
||||
{ opacity: 0.6, x: '209.137', y: '0', width: '16.8626', height: '33.7252', fill: '#2ABBF8' },
|
||||
{ opacity: 0.6, x: '243.233', y: '0', width: '34.2403', height: '33.7252', fill: '#2ABBF8' },
|
||||
{ opacity: 1, x: '243.233', y: '0', width: '16.8626', height: '16.8626', fill: '#2ABBF8' },
|
||||
{ opacity: 0.6, x: '260.096', y: '0', width: '34.04', height: '16.8626', fill: '#2ABBF8' },
|
||||
{ opacity: 1, x: '260.611', y: '16.8626', width: '16.8626', height: '16.8626', fill: '#2ABBF8' },
|
||||
]
|
||||
|
||||
/**
|
||||
* Rect data for the left position.
|
||||
* Two-column vertical strip, ordered top-to-bottom.
|
||||
*/
|
||||
const LEFT_RECTS: readonly BlockRect[] = [
|
||||
{
|
||||
opacity: 0.6,
|
||||
width: '34.240',
|
||||
height: '33.725',
|
||||
fill: '#FA4EDF',
|
||||
transform: 'matrix(0 1 1 0 0 0)',
|
||||
},
|
||||
{
|
||||
opacity: 0.6,
|
||||
width: '16.8626',
|
||||
height: '68.480',
|
||||
fill: '#FA4EDF',
|
||||
transform: 'matrix(-1 0 0 1 33.727 0)',
|
||||
},
|
||||
{
|
||||
opacity: 1,
|
||||
width: '16.8626',
|
||||
height: '16.8626',
|
||||
fill: '#FA4EDF',
|
||||
transform: 'matrix(-1 0 0 1 33.727 17.378)',
|
||||
},
|
||||
{
|
||||
opacity: 0.6,
|
||||
width: '16.8626',
|
||||
height: '33.986',
|
||||
fill: '#FA4EDF',
|
||||
transform: 'matrix(0 1 1 0 0 51.616)',
|
||||
},
|
||||
{
|
||||
opacity: 0.6,
|
||||
width: '16.8626',
|
||||
height: '140.507',
|
||||
fill: '#00F701',
|
||||
transform: 'matrix(-1 0 0 1 33.986 85.335)',
|
||||
},
|
||||
{
|
||||
opacity: 0.4,
|
||||
x: '17.119',
|
||||
y: '136.962',
|
||||
width: '34.240',
|
||||
height: '16.8626',
|
||||
fill: '#FFCC02',
|
||||
transform: 'rotate(-90 17.119 136.962)',
|
||||
},
|
||||
{
|
||||
opacity: 1,
|
||||
x: '17.119',
|
||||
y: '136.962',
|
||||
width: '16.8626',
|
||||
height: '16.8626',
|
||||
fill: '#FFCC02',
|
||||
transform: 'rotate(-90 17.119 136.962)',
|
||||
},
|
||||
{
|
||||
opacity: 0.5,
|
||||
width: '34.240',
|
||||
height: '33.725',
|
||||
fill: '#00F701',
|
||||
transform: 'matrix(0 1 1 0 0.257 153.825)',
|
||||
},
|
||||
{
|
||||
opacity: 1,
|
||||
width: '16.8626',
|
||||
height: '16.8626',
|
||||
fill: '#00F701',
|
||||
transform: 'matrix(0 1 1 0 0.257 153.825)',
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* Rect data for the right-side position (right edge of screenshot).
|
||||
* Same two-column structure as left with rotated colours:
|
||||
* pink→blue, green→pink, yellow→green.
|
||||
*/
|
||||
const RIGHT_SIDE_RECTS: readonly BlockRect[] = [
|
||||
{
|
||||
opacity: 0.6,
|
||||
width: '34.240',
|
||||
height: '33.725',
|
||||
fill: '#2ABBF8',
|
||||
transform: 'matrix(0 1 1 0 0 0)',
|
||||
},
|
||||
{
|
||||
opacity: 0.6,
|
||||
width: '16.8626',
|
||||
height: '68.480',
|
||||
fill: '#2ABBF8',
|
||||
transform: 'matrix(-1 0 0 1 33.727 0)',
|
||||
},
|
||||
{
|
||||
opacity: 1,
|
||||
width: '16.8626',
|
||||
height: '16.8626',
|
||||
fill: '#2ABBF8',
|
||||
transform: 'matrix(-1 0 0 1 33.727 17.378)',
|
||||
},
|
||||
{
|
||||
opacity: 0.6,
|
||||
width: '16.8626',
|
||||
height: '33.986',
|
||||
fill: '#2ABBF8',
|
||||
transform: 'matrix(0 1 1 0 0 51.616)',
|
||||
},
|
||||
{
|
||||
opacity: 0.6,
|
||||
width: '16.8626',
|
||||
height: '140.507',
|
||||
fill: '#FA4EDF',
|
||||
transform: 'matrix(-1 0 0 1 33.986 85.335)',
|
||||
},
|
||||
{
|
||||
opacity: 0.4,
|
||||
x: '17.119',
|
||||
y: '136.962',
|
||||
width: '34.240',
|
||||
height: '16.8626',
|
||||
fill: '#00F701',
|
||||
transform: 'rotate(-90 17.119 136.962)',
|
||||
},
|
||||
{
|
||||
opacity: 1,
|
||||
x: '17.119',
|
||||
y: '136.962',
|
||||
width: '16.8626',
|
||||
height: '16.8626',
|
||||
fill: '#00F701',
|
||||
transform: 'rotate(-90 17.119 136.962)',
|
||||
},
|
||||
{
|
||||
opacity: 0.5,
|
||||
width: '34.240',
|
||||
height: '33.725',
|
||||
fill: '#FA4EDF',
|
||||
transform: 'matrix(0 1 1 0 0.257 153.825)',
|
||||
},
|
||||
{
|
||||
opacity: 1,
|
||||
width: '16.8626',
|
||||
height: '16.8626',
|
||||
fill: '#FA4EDF',
|
||||
transform: 'matrix(0 1 1 0 0.257 153.825)',
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* Rect data for the right-edge position (far right of screen).
|
||||
* Two-column vertical strip, ordered top-to-bottom.
|
||||
*/
|
||||
const RIGHT_RECTS: readonly BlockRect[] = [
|
||||
{
|
||||
opacity: 0.6,
|
||||
width: '16.8626',
|
||||
height: '33.726',
|
||||
fill: '#FA4EDF',
|
||||
transform: 'matrix(0 1 1 0 0 0)',
|
||||
},
|
||||
{
|
||||
opacity: 0.6,
|
||||
width: '34.241',
|
||||
height: '16.8626',
|
||||
fill: '#FA4EDF',
|
||||
transform: 'matrix(0 1 1 0 16.891 0)',
|
||||
},
|
||||
{
|
||||
opacity: 0.6,
|
||||
width: '16.8626',
|
||||
height: '68.482',
|
||||
fill: '#FA4EDF',
|
||||
transform: 'matrix(-1 0 0 1 33.739 16.888)',
|
||||
},
|
||||
{
|
||||
opacity: 0.6,
|
||||
width: '16.8626',
|
||||
height: '33.726',
|
||||
fill: '#FA4EDF',
|
||||
transform: 'matrix(0 1 1 0 0 33.776)',
|
||||
},
|
||||
{
|
||||
opacity: 1,
|
||||
width: '16.8626',
|
||||
height: '16.8626',
|
||||
fill: '#FA4EDF',
|
||||
transform: 'matrix(-1 0 0 1 33.739 34.272)',
|
||||
},
|
||||
{
|
||||
opacity: 0.6,
|
||||
width: '16.8626',
|
||||
height: '33.726',
|
||||
fill: '#FA4EDF',
|
||||
transform: 'matrix(0 1 1 0 0.012 68.510)',
|
||||
},
|
||||
{
|
||||
opacity: 0.6,
|
||||
width: '16.8626',
|
||||
height: '102.384',
|
||||
fill: '#2ABBF8',
|
||||
transform: 'matrix(-1 0 0 1 33.787 102.384)',
|
||||
},
|
||||
{
|
||||
opacity: 0.4,
|
||||
x: '17.131',
|
||||
y: '153.859',
|
||||
width: '34.241',
|
||||
height: '16.8626',
|
||||
fill: '#00F701',
|
||||
transform: 'rotate(-90 17.131 153.859)',
|
||||
},
|
||||
{
|
||||
opacity: 1,
|
||||
x: '17.131',
|
||||
y: '153.859',
|
||||
width: '16.8626',
|
||||
height: '16.8626',
|
||||
fill: '#00F701',
|
||||
transform: 'rotate(-90 17.131 153.859)',
|
||||
},
|
||||
]
|
||||
|
||||
/** Number of rects per position, used to compute animation durations. */
|
||||
const RECT_COUNTS: Record<BlockPosition, number> = {
|
||||
topRight: TOP_RIGHT_RECTS.length,
|
||||
topLeft: TOP_LEFT_RECTS.length,
|
||||
left: LEFT_RECTS.length,
|
||||
rightSide: RIGHT_SIDE_RECTS.length,
|
||||
rightEdge: RIGHT_RECTS.length,
|
||||
}
|
||||
|
||||
/** Total enter animation time for a position (seconds). */
|
||||
function enterTime(pos: BlockPosition): number {
|
||||
return (RECT_COUNTS[pos] - 1) * ENTER_STAGGER + ENTER_DURATION
|
||||
}
|
||||
|
||||
/** Total exit animation time for a position (seconds). */
|
||||
function exitTime(pos: BlockPosition): number {
|
||||
return (RECT_COUNTS[pos] - 1) * EXIT_STAGGER + EXIT_DURATION
|
||||
}
|
||||
|
||||
/** A single step in the repeating animation cycle. */
|
||||
type CycleStep =
|
||||
| { action: 'exit'; position: BlockPosition }
|
||||
| { action: 'enter'; position: BlockPosition }
|
||||
| { action: 'hold'; ms: number }
|
||||
|
||||
/**
|
||||
* The repeating cycle sequence. After all steps, the layout returns to its
|
||||
* initial state (topRight + left + rightEdge) so the loop is seamless.
|
||||
*
|
||||
* Order: exit top → exit right-edge → enter right-side-of-preview →
|
||||
* exit left → enter top-left → exit right-side → enter left →
|
||||
* exit top-left → enter top-right → enter right-edge → back to initial.
|
||||
*/
|
||||
const CYCLE_STEPS: readonly CycleStep[] = [
|
||||
{ action: 'exit', position: 'topRight' },
|
||||
{ action: 'exit', position: 'rightEdge' },
|
||||
{ action: 'enter', position: 'rightSide' },
|
||||
{ action: 'hold', ms: HOLD_BETWEEN_MS },
|
||||
{ action: 'exit', position: 'left' },
|
||||
{ action: 'enter', position: 'topLeft' },
|
||||
{ action: 'hold', ms: HOLD_BETWEEN_MS },
|
||||
{ action: 'exit', position: 'rightSide' },
|
||||
{ action: 'enter', position: 'left' },
|
||||
{ action: 'hold', ms: HOLD_BETWEEN_MS },
|
||||
{ action: 'exit', position: 'topLeft' },
|
||||
{ action: 'enter', position: 'topRight' },
|
||||
{ action: 'hold', ms: HOLD_BETWEEN_MS },
|
||||
{ action: 'enter', position: 'rightEdge' },
|
||||
{ action: 'hold', ms: HOLD_BETWEEN_MS },
|
||||
]
|
||||
|
||||
/**
|
||||
* Drives the block-cycling animation loop. Returns the current animation
|
||||
* state for every position so each component can be driven declaratively.
|
||||
*
|
||||
* Lifecycle:
|
||||
* 1. All three initial groups (topRight, left, rightEdge) enter together.
|
||||
* 2. After a hold period the cycle begins, processing each step in order.
|
||||
* 3. Repeats indefinitely, returning to the initial layout every cycle.
|
||||
*/
|
||||
export function useBlockCycle(): Record<BlockPosition, BlockAnimState> {
|
||||
const [states, setStates] = useState<Record<BlockPosition, BlockAnimState>>({
|
||||
topRight: 'entering',
|
||||
left: 'entering',
|
||||
rightEdge: 'entering',
|
||||
rightSide: 'hidden',
|
||||
topLeft: 'hidden',
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const cancelled = { current: false }
|
||||
const delay = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))
|
||||
|
||||
const run = async () => {
|
||||
const longestEnter = Math.max(
|
||||
enterTime('topRight'),
|
||||
enterTime('left'),
|
||||
enterTime('rightEdge')
|
||||
)
|
||||
await delay(longestEnter * 1000)
|
||||
if (cancelled.current) return
|
||||
|
||||
setStates({
|
||||
topRight: 'visible',
|
||||
left: 'visible',
|
||||
rightEdge: 'visible',
|
||||
rightSide: 'hidden',
|
||||
topLeft: 'hidden',
|
||||
})
|
||||
|
||||
await delay(INITIAL_HOLD_MS)
|
||||
if (cancelled.current) return
|
||||
|
||||
while (!cancelled.current) {
|
||||
for (const step of CYCLE_STEPS) {
|
||||
if (cancelled.current) return
|
||||
|
||||
if (step.action === 'exit') {
|
||||
setStates((prev) => ({ ...prev, [step.position]: 'exiting' }))
|
||||
await delay(exitTime(step.position) * 1000)
|
||||
if (cancelled.current) return
|
||||
setStates((prev) => ({ ...prev, [step.position]: 'hidden' }))
|
||||
await delay(TRANSITION_PAUSE_MS)
|
||||
} else if (step.action === 'enter') {
|
||||
setStates((prev) => ({ ...prev, [step.position]: 'entering' }))
|
||||
await delay(enterTime(step.position) * 1000)
|
||||
if (cancelled.current) return
|
||||
setStates((prev) => ({ ...prev, [step.position]: 'visible' }))
|
||||
await delay(TRANSITION_PAUSE_MS)
|
||||
} else {
|
||||
await delay(step.ms)
|
||||
}
|
||||
|
||||
if (cancelled.current) return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
run()
|
||||
return () => {
|
||||
cancelled.current = true
|
||||
}
|
||||
}, [])
|
||||
|
||||
return states
|
||||
}
|
||||
|
||||
interface AnimatedBlockProps {
|
||||
animState?: BlockAnimState
|
||||
reverseExit?: boolean
|
||||
}
|
||||
|
||||
/** Two-row horizontal strip at the top-right of the hero. */
|
||||
export function BlocksTopRightAnimated({
|
||||
animState = 'entering',
|
||||
reverseExit,
|
||||
}: AnimatedBlockProps) {
|
||||
return (
|
||||
<AnimatedBlocksSvg
|
||||
width={295}
|
||||
height={34}
|
||||
viewBox='0 0 295 34'
|
||||
rects={TOP_RIGHT_RECTS}
|
||||
animState={animState}
|
||||
reverseExit={reverseExit}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/** Two-row horizontal strip at the top-left of the hero. */
|
||||
export function BlocksTopLeftAnimated({ animState = 'entering', reverseExit }: AnimatedBlockProps) {
|
||||
return (
|
||||
<AnimatedBlocksSvg
|
||||
width={295}
|
||||
height={34}
|
||||
viewBox='0 0 295 34'
|
||||
rects={TOP_LEFT_RECTS}
|
||||
animState={animState}
|
||||
reverseExit={reverseExit}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/** Two-column vertical strip on the left edge of the screenshot. */
|
||||
export function BlocksLeftAnimated({ animState = 'entering', reverseExit }: AnimatedBlockProps) {
|
||||
return (
|
||||
<AnimatedBlocksSvg
|
||||
width={34}
|
||||
height={226}
|
||||
viewBox='0 0 34 226.021'
|
||||
rects={LEFT_RECTS}
|
||||
animState={animState}
|
||||
reverseExit={reverseExit}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/** Two-column vertical strip on the right edge of the screenshot. */
|
||||
export function BlocksRightSideAnimated({
|
||||
animState = 'entering',
|
||||
reverseExit,
|
||||
}: AnimatedBlockProps) {
|
||||
return (
|
||||
<AnimatedBlocksSvg
|
||||
width={34}
|
||||
height={226}
|
||||
viewBox='0 0 34 226.021'
|
||||
rects={RIGHT_SIDE_RECTS}
|
||||
animState={animState}
|
||||
reverseExit={reverseExit}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/** Two-column vertical strip at the far-right edge of the screen. */
|
||||
export function BlocksRightAnimated({ animState = 'entering', reverseExit }: AnimatedBlockProps) {
|
||||
return (
|
||||
<AnimatedBlocksSvg
|
||||
width={34}
|
||||
height={205}
|
||||
viewBox='0 0 34 204.769'
|
||||
rects={RIGHT_RECTS}
|
||||
animState={animState}
|
||||
reverseExit={reverseExit}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import dynamic from 'next/dynamic'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import {
|
||||
BlocksLeftAnimated,
|
||||
BlocksRightAnimated,
|
||||
BlocksRightSideAnimated,
|
||||
BlocksTopLeftAnimated,
|
||||
BlocksTopRightAnimated,
|
||||
useBlockCycle,
|
||||
} from '@/app/(home)/components/hero/components/animated-blocks'
|
||||
|
||||
const LandingPreview = dynamic(
|
||||
() =>
|
||||
import('@/app/(home)/components/landing-preview/landing-preview').then(
|
||||
(mod) => mod.LandingPreview
|
||||
),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => <div className='aspect-[1116/549] w-full rounded bg-[#1b1b1b]' />,
|
||||
}
|
||||
)
|
||||
|
||||
/** Shared base classes for CTA link buttons — matches Deploy/Run button styling in the preview panel. */
|
||||
const CTA_BASE =
|
||||
'inline-flex items-center h-[32px] rounded-[5px] border px-[10px] font-[430] font-season text-[14px]'
|
||||
|
||||
export default function Hero() {
|
||||
const blockStates = useBlockCycle()
|
||||
|
||||
return (
|
||||
<section
|
||||
id='hero'
|
||||
aria-labelledby='hero-heading'
|
||||
className='relative flex flex-col items-center overflow-hidden bg-[#1C1C1C] pt-[71px]'
|
||||
>
|
||||
<p className='sr-only'>
|
||||
Sim is the open-source platform to build AI agents and run your agentic workforce. Connect
|
||||
1,000+ integrations and LLMs — including OpenAI, Claude, Gemini, Mistral, and xAI — to
|
||||
deploy and orchestrate agentic workflows. Create agents, workflows, knowledge bases, tables,
|
||||
and docs. Trusted by over 100,000 builders at startups and Fortune 500 companies. SOC2 and
|
||||
HIPAA compliant.
|
||||
</p>
|
||||
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='pointer-events-none absolute top-[-0.7vw] left-[-2.8vw] z-0 aspect-[344/328] w-[23.9vw]'
|
||||
>
|
||||
<Image src='/landing/card-left.svg' alt='' fill className='object-contain' />
|
||||
</div>
|
||||
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='pointer-events-none absolute top-[-2.8vw] right-[0vw] z-0 aspect-[471/470] w-[32.7vw]'
|
||||
>
|
||||
<Image src='/landing/card-right.svg' alt='' fill className='object-contain' />
|
||||
</div>
|
||||
|
||||
<div className='relative z-10 flex flex-col items-center gap-[12px]'>
|
||||
<h1
|
||||
id='hero-heading'
|
||||
className='font-[430] font-season text-[64px] text-white leading-[100%] tracking-[-0.02em]'
|
||||
>
|
||||
Build Agents
|
||||
</h1>
|
||||
<p className='font-[430] font-season text-[#F6F6F6]/60 text-[16px] leading-[125%] tracking-[0.02em]'>
|
||||
Build and deploy agentic workflows
|
||||
</p>
|
||||
|
||||
<div className='mt-[12px] flex items-center gap-[8px]'>
|
||||
<Link
|
||||
href='/login'
|
||||
className={`${CTA_BASE} border-[#3d3d3d] text-[#ECECEC] transition-colors hover:bg-[#2A2A2A]`}
|
||||
aria-label='Log in'
|
||||
>
|
||||
Log in
|
||||
</Link>
|
||||
<Link
|
||||
href='/signup'
|
||||
className={`${CTA_BASE} gap-[8px] border-[#33C482] bg-[#33C482] text-black transition-[filter] hover:brightness-110`}
|
||||
aria-label='Get started with Sim'
|
||||
>
|
||||
Get started
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='pointer-events-none absolute top-0 right-[13.1vw] z-20 w-[calc(140px_+_10.76vw)] max-w-[295px]'
|
||||
>
|
||||
<BlocksTopRightAnimated animState={blockStates.topRight} />
|
||||
</div>
|
||||
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='pointer-events-none absolute top-0 left-[16vw] z-20 w-[calc(140px_+_10.76vw)] max-w-[295px]'
|
||||
>
|
||||
<BlocksTopLeftAnimated animState={blockStates.topLeft} />
|
||||
</div>
|
||||
|
||||
<div className='relative z-10 mx-auto mt-[2.4vw] w-[78.9vw] px-[1.4vw]'>
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='-translate-y-1/2 pointer-events-none absolute top-[50%] right-[calc(100%-1.41vw)] z-20 w-[calc(16px_+_1.25vw)] max-w-[34px]'
|
||||
>
|
||||
<BlocksLeftAnimated animState={blockStates.left} />
|
||||
</div>
|
||||
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='-translate-y-1/2 pointer-events-none absolute top-[50%] left-[calc(100%-1.41vw)] z-20 w-[calc(16px_+_1.25vw)] max-w-[34px] scale-x-[-1]'
|
||||
>
|
||||
<BlocksRightSideAnimated animState={blockStates.rightSide} />
|
||||
</div>
|
||||
|
||||
<div className='relative z-10 overflow-hidden rounded border border-[#2A2A2A]'>
|
||||
<LandingPreview />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='-translate-y-1/2 pointer-events-none absolute top-[50%] right-0 z-20 w-[calc(16px_+_1.25vw)] max-w-[34px]'
|
||||
>
|
||||
<BlocksRightAnimated animState={blockStates.rightEdge} />
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import Collaboration from '@/app/(home)/components/collaboration/collaboration'
|
||||
import Enterprise from '@/app/(home)/components/enterprise/enterprise'
|
||||
import Features from '@/app/(home)/components/features/features'
|
||||
import Footer from '@/app/(home)/components/footer/footer'
|
||||
import Hero from '@/app/(home)/components/hero/hero'
|
||||
import Navbar from '@/app/(home)/components/navbar/navbar'
|
||||
import Pricing from '@/app/(home)/components/pricing/pricing'
|
||||
import StructuredData from '@/app/(home)/components/structured-data'
|
||||
import Templates from '@/app/(home)/components/templates/templates'
|
||||
import Testimonials from '@/app/(home)/components/testimonials/testimonials'
|
||||
|
||||
export {
|
||||
Collaboration,
|
||||
Enterprise,
|
||||
Features,
|
||||
Footer,
|
||||
Hero,
|
||||
Navbar,
|
||||
Pricing,
|
||||
StructuredData,
|
||||
Templates,
|
||||
Testimonials,
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { memo, useCallback, useRef, useState } from 'react'
|
||||
import { ArrowUp } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { BubbleChatPreview, ChevronDown, MoreHorizontal, Play } from '@/components/emcn'
|
||||
import { LandingPromptStorage } from '@/lib/core/utils/browser-storage'
|
||||
|
||||
/**
|
||||
* Lightweight static panel replicating the real workspace panel styling.
|
||||
* The copilot tab is active with a functional user input.
|
||||
* When submitted, stores the prompt and redirects to /signup (same as landing hero).
|
||||
*
|
||||
* Structure mirrors the real Panel component:
|
||||
* aside > div.border-l.pt-[14px] > Header(px-8) > Tabs(px-8,pt-14) > Content(pt-12)
|
||||
* inside Content > Copilot > header-bar(mx-[-1px]) > UserInput(p-8)
|
||||
*/
|
||||
export const LandingPreviewPanel = memo(function LandingPreviewPanel() {
|
||||
const router = useRouter()
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
const [cursorPos, setCursorPos] = useState<{ x: number; y: number } | null>(null)
|
||||
|
||||
const isEmpty = inputValue.trim().length === 0
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
if (isEmpty) return
|
||||
LandingPromptStorage.store(inputValue)
|
||||
router.push('/signup')
|
||||
}, [isEmpty, inputValue, router])
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
handleSubmit()
|
||||
}
|
||||
},
|
||||
[handleSubmit]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className='flex h-full w-[280px] flex-shrink-0 flex-col bg-[#1e1e1e]'>
|
||||
<div className='flex h-full flex-col border-[#2c2c2c] border-l pt-[14px]'>
|
||||
{/* Header — More + Chat | Deploy + Run */}
|
||||
<div className='flex flex-shrink-0 items-center justify-between px-[8px]'>
|
||||
<div className='pointer-events-none flex gap-[6px]'>
|
||||
<div className='flex h-[30px] w-[30px] items-center justify-center rounded-[5px] border border-[#3d3d3d] bg-[#363636]'>
|
||||
<MoreHorizontal className='h-[14px] w-[14px] text-[#e6e6e6]' />
|
||||
</div>
|
||||
<div className='flex h-[30px] w-[30px] items-center justify-center rounded-[5px] border border-[#3d3d3d] bg-[#363636]'>
|
||||
<BubbleChatPreview className='h-[14px] w-[14px] text-[#e6e6e6]' />
|
||||
</div>
|
||||
</div>
|
||||
<Link
|
||||
href='/signup'
|
||||
className='flex gap-[6px]'
|
||||
onMouseMove={(e) => setCursorPos({ x: e.clientX, y: e.clientY })}
|
||||
onMouseLeave={() => setCursorPos(null)}
|
||||
>
|
||||
<div className='flex h-[30px] items-center rounded-[5px] bg-[#32bd7e] px-[10px] transition-[filter] hover:brightness-110'>
|
||||
<span className='font-medium text-[#1b1b1b] text-[12px]'>Deploy</span>
|
||||
</div>
|
||||
<div className='flex h-[30px] items-center gap-[8px] rounded-[5px] bg-[#32bd7e] px-[10px] transition-[filter] hover:brightness-110'>
|
||||
<Play className='h-[11.5px] w-[11.5px] text-[#1b1b1b]' />
|
||||
<span className='font-medium text-[#1b1b1b] text-[12px]'>Run</span>
|
||||
</div>
|
||||
</Link>
|
||||
{cursorPos &&
|
||||
createPortal(
|
||||
<div
|
||||
className='pointer-events-none fixed z-[9999]'
|
||||
style={{ left: cursorPos.x + 14, top: cursorPos.y + 14 }}
|
||||
>
|
||||
{/* Decorative color bars — mirrors hero top-right block sequence */}
|
||||
<div className='flex h-[4px]'>
|
||||
<div className='h-full w-[8px] bg-[#2ABBF8]' />
|
||||
<div className='h-full w-[14px] bg-[#2ABBF8] opacity-60' />
|
||||
<div className='h-full w-[8px] bg-[#00F701]' />
|
||||
<div className='h-full w-[16px] bg-[#00F701] opacity-60' />
|
||||
<div className='h-full w-[8px] bg-[#FFCC02]' />
|
||||
<div className='h-full w-[10px] bg-[#FFCC02] opacity-60' />
|
||||
<div className='h-full w-[8px] bg-[#FA4EDF]' />
|
||||
<div className='h-full w-[14px] bg-[#FA4EDF] opacity-60' />
|
||||
</div>
|
||||
<div className='flex items-center gap-[5px] bg-white px-[6px] py-[4px] font-medium text-[#1C1C1C] text-[11px]'>
|
||||
Get started
|
||||
<ChevronDown className='-rotate-90 h-[7px] w-[7px] text-[#1C1C1C]' />
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className='flex flex-shrink-0 items-center px-[8px] pt-[14px]'>
|
||||
<div className='pointer-events-none flex gap-[4px]'>
|
||||
<div className='flex h-[28px] items-center rounded-[6px] border border-[#3d3d3d] bg-[#363636] px-[8px] py-[5px]'>
|
||||
<span className='font-medium text-[#e6e6e6] text-[12.5px]'>Copilot</span>
|
||||
</div>
|
||||
<div className='flex h-[28px] items-center rounded-[6px] border border-transparent px-[8px] py-[5px]'>
|
||||
<span className='font-medium text-[#787878] text-[12.5px]'>Toolbar</span>
|
||||
</div>
|
||||
<div className='flex h-[28px] items-center rounded-[6px] border border-transparent px-[8px] py-[5px]'>
|
||||
<span className='font-medium text-[#787878] text-[12.5px]'>Editor</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab content — copilot */}
|
||||
<div className='flex flex-1 flex-col overflow-hidden pt-[12px]'>
|
||||
<div className='flex h-full flex-col'>
|
||||
{/* Copilot header bar — matches mx-[-1px] in real copilot */}
|
||||
<div className='pointer-events-none mx-[-1px] flex flex-shrink-0 items-center rounded-[4px] border border-[#2c2c2c] bg-[#292929] px-[12px] py-[6px]'>
|
||||
<span className='truncate font-medium text-[#e6e6e6] text-[14px]'>New Chat</span>
|
||||
</div>
|
||||
|
||||
{/* User input — matches real UserInput at p-[8px] inside copilot welcome state */}
|
||||
<div className='px-[8px] pt-[12px] pb-[8px]'>
|
||||
<div className='rounded-[4px] border border-[#3d3d3d] bg-[#292929] px-[6px] py-[6px]'>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder='Build an AI agent...'
|
||||
rows={2}
|
||||
className='mb-[6px] min-h-[48px] w-full cursor-text resize-none border-0 bg-transparent px-[2px] py-1 font-base text-[#e6e6e6] text-sm leading-[1.25rem] placeholder-[#787878] caret-[#e6e6e6] outline-none'
|
||||
/>
|
||||
<div className='flex items-center justify-end'>
|
||||
<button
|
||||
type='button'
|
||||
onClick={handleSubmit}
|
||||
disabled={isEmpty}
|
||||
className='flex h-[22px] w-[22px] items-center justify-center rounded-full border-0 p-0 transition-colors'
|
||||
style={{
|
||||
background: isEmpty ? '#808080' : '#e0e0e0',
|
||||
cursor: isEmpty ? 'not-allowed' : 'pointer',
|
||||
}}
|
||||
>
|
||||
<ArrowUp size={14} strokeWidth={2.25} color='#1b1b1b' />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
@@ -1,142 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Database, Layout, Search, Settings } from 'lucide-react'
|
||||
import { ChevronDown, Library } from '@/components/emcn'
|
||||
import type { PreviewWorkflow } from '@/app/(home)/components/landing-preview/components/landing-preview-workflow/workflow-data'
|
||||
|
||||
/**
|
||||
* Props for the LandingPreviewSidebar component
|
||||
*/
|
||||
interface LandingPreviewSidebarProps {
|
||||
workflows: PreviewWorkflow[]
|
||||
activeWorkflowId: string
|
||||
onSelectWorkflow: (id: string) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Static footer navigation items matching the real sidebar
|
||||
*/
|
||||
const FOOTER_NAV_ITEMS = [
|
||||
{ id: 'logs', label: 'Logs', icon: Library },
|
||||
{ id: 'templates', label: 'Templates', icon: Layout },
|
||||
{ id: 'knowledge-base', label: 'Knowledge Base', icon: Database },
|
||||
{ id: 'settings', label: 'Settings', icon: Settings },
|
||||
] as const
|
||||
|
||||
/**
|
||||
* Lightweight static sidebar replicating the real workspace sidebar styling.
|
||||
* Only workflow items are interactive — everything else is pointer-events-none.
|
||||
*
|
||||
* Colors sourced from the dark theme CSS variables:
|
||||
* --surface-1: #1e1e1e, --surface-5: #363636, --border: #2c2c2c, --border-1: #3d3d3d
|
||||
* --text-primary: #e6e6e6, --text-tertiary: #b3b3b3, --text-muted: #787878
|
||||
*/
|
||||
export function LandingPreviewSidebar({
|
||||
workflows,
|
||||
activeWorkflowId,
|
||||
onSelectWorkflow,
|
||||
}: LandingPreviewSidebarProps) {
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false)
|
||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const handleToggle = useCallback(() => {
|
||||
setIsDropdownOpen((prev) => !prev)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDropdownOpen) return
|
||||
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
|
||||
setIsDropdownOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||
}, [isDropdownOpen])
|
||||
|
||||
return (
|
||||
<div className='flex h-full w-[220px] flex-shrink-0 flex-col border-[#2c2c2c] border-r bg-[#1e1e1e]'>
|
||||
{/* Header */}
|
||||
<div className='relative flex-shrink-0 px-[14px] pt-[12px]' ref={dropdownRef}>
|
||||
<div className='flex items-center justify-between'>
|
||||
<button
|
||||
type='button'
|
||||
onClick={handleToggle}
|
||||
className='group -mx-[6px] flex cursor-pointer items-center gap-[8px] rounded-[6px] bg-transparent px-[6px] py-[4px] transition-colors hover:bg-[#363636]'
|
||||
>
|
||||
<span className='truncate font-base text-[#e6e6e6] text-[14px]'>My Workspace</span>
|
||||
<ChevronDown
|
||||
className={`h-[8px] w-[10px] flex-shrink-0 text-[#787878] transition-all duration-100 group-hover:text-[#cccccc] ${isDropdownOpen ? 'rotate-180' : ''}`}
|
||||
/>
|
||||
</button>
|
||||
<div className='pointer-events-none flex flex-shrink-0 items-center'>
|
||||
<Search className='h-[14px] w-[14px] text-[#787878]' />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Workspace switcher dropdown */}
|
||||
{isDropdownOpen && (
|
||||
<div className='absolute top-[42px] left-[8px] z-50 min-w-[160px] max-w-[160px] rounded-[6px] bg-[#242424] px-[6px] py-[6px] shadow-lg'>
|
||||
<div
|
||||
className='flex h-[26px] cursor-pointer items-center gap-[8px] rounded-[6px] bg-[#3d3d3d] px-[6px] font-base text-[#e6e6e6] text-[13px]'
|
||||
role='menuitem'
|
||||
onClick={() => setIsDropdownOpen(false)}
|
||||
>
|
||||
<span className='min-w-0 flex-1 truncate'>My Workspace</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Workflow items */}
|
||||
<div className='mt-[8px] space-y-[2px] overflow-x-hidden px-[8px]'>
|
||||
{workflows.map((workflow) => {
|
||||
const isActive = workflow.id === activeWorkflowId
|
||||
return (
|
||||
<button
|
||||
key={workflow.id}
|
||||
type='button'
|
||||
onClick={() => onSelectWorkflow(workflow.id)}
|
||||
className={`group flex h-[26px] w-full items-center gap-[8px] rounded-[8px] px-[6px] text-[14px] transition-colors ${
|
||||
isActive ? 'bg-[#363636]' : 'bg-transparent hover:bg-[#363636]'
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className='h-[14px] w-[14px] flex-shrink-0 rounded-[4px]'
|
||||
style={{ backgroundColor: workflow.color }}
|
||||
/>
|
||||
<div className='min-w-0 flex-1'>
|
||||
<div
|
||||
className={`min-w-0 truncate text-left font-medium ${
|
||||
isActive ? 'text-[#e6e6e6]' : 'text-[#b3b3b3] group-hover:text-[#e6e6e6]'
|
||||
}`}
|
||||
>
|
||||
{workflow.name}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Footer navigation — static */}
|
||||
<div className='pointer-events-none mt-auto flex flex-shrink-0 flex-col gap-[2px] border-[#2c2c2c] border-t px-[7.75px] pt-[8px] pb-[8px]'>
|
||||
{FOOTER_NAV_ITEMS.map((item) => {
|
||||
const Icon = item.icon
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className='flex h-[26px] items-center gap-[8px] rounded-[8px] px-[6px] text-[14px]'
|
||||
>
|
||||
<Icon className='h-[14px] w-[14px] flex-shrink-0 text-[#b3b3b3]' />
|
||||
<span className='truncate font-medium text-[#b3b3b3] text-[13px]'>{item.label}</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import ReactFlow, {
|
||||
applyEdgeChanges,
|
||||
applyNodeChanges,
|
||||
type Edge,
|
||||
type EdgeProps,
|
||||
type EdgeTypes,
|
||||
getSmoothStepPath,
|
||||
type Node,
|
||||
type NodeTypes,
|
||||
type OnEdgesChange,
|
||||
type OnNodesChange,
|
||||
ReactFlowProvider,
|
||||
} from 'reactflow'
|
||||
import 'reactflow/dist/style.css'
|
||||
import { PreviewBlockNode } from '@/app/(home)/components/landing-preview/components/landing-preview-workflow/preview-block-node'
|
||||
import {
|
||||
EASE_OUT,
|
||||
type PreviewWorkflow,
|
||||
toReactFlowElements,
|
||||
} from '@/app/(home)/components/landing-preview/components/landing-preview-workflow/workflow-data'
|
||||
|
||||
interface FitViewOptions {
|
||||
padding?: number
|
||||
maxZoom?: number
|
||||
}
|
||||
|
||||
interface LandingPreviewWorkflowProps {
|
||||
workflow: PreviewWorkflow
|
||||
animate?: boolean
|
||||
fitViewOptions?: FitViewOptions
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom edge that draws left-to-right on initial load via stroke animation.
|
||||
* Falls back to a static path when `data.animate` is false.
|
||||
*/
|
||||
function PreviewEdge({
|
||||
id,
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
style,
|
||||
data,
|
||||
}: EdgeProps) {
|
||||
const [edgePath] = getSmoothStepPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
})
|
||||
|
||||
if (data?.animate) {
|
||||
return (
|
||||
<motion.path
|
||||
id={id}
|
||||
className='react-flow__edge-path'
|
||||
d={edgePath}
|
||||
style={{ ...style, fill: 'none' }}
|
||||
initial={{ pathLength: 0, opacity: 0 }}
|
||||
animate={{ pathLength: 1, opacity: 1 }}
|
||||
transition={{
|
||||
pathLength: { duration: 0.4, delay: data.delay ?? 0, ease: EASE_OUT },
|
||||
opacity: { duration: 0.15, delay: data.delay ?? 0 },
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<path
|
||||
id={id}
|
||||
className='react-flow__edge-path'
|
||||
d={edgePath}
|
||||
style={{ ...style, fill: 'none' }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const NODE_TYPES: NodeTypes = { previewBlock: PreviewBlockNode }
|
||||
const EDGE_TYPES: EdgeTypes = { previewEdge: PreviewEdge }
|
||||
const PRO_OPTIONS = { hideAttribution: true }
|
||||
const DEFAULT_FIT_VIEW_OPTIONS = { padding: 0.3, maxZoom: 1 } as const
|
||||
|
||||
/**
|
||||
* Inner flow component. Keyed on workflow ID by the parent so it remounts
|
||||
* cleanly on workflow switch — fitView fires on mount with zero delay.
|
||||
*/
|
||||
function PreviewFlow({ workflow, animate = false, fitViewOptions }: LandingPreviewWorkflowProps) {
|
||||
const { nodes: initialNodes, edges: initialEdges } = useMemo(
|
||||
() => toReactFlowElements(workflow, animate),
|
||||
[workflow, animate]
|
||||
)
|
||||
|
||||
const [nodes, setNodes] = useState<Node[]>(initialNodes)
|
||||
const [edges, setEdges] = useState<Edge[]>(initialEdges)
|
||||
|
||||
const onNodesChange: OnNodesChange = useCallback(
|
||||
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
|
||||
[]
|
||||
)
|
||||
|
||||
const onEdgesChange: OnEdgesChange = useCallback(
|
||||
(changes) => setEdges((eds) => applyEdgeChanges(changes, eds)),
|
||||
[]
|
||||
)
|
||||
|
||||
const resolvedFitViewOptions = fitViewOptions ?? DEFAULT_FIT_VIEW_OPTIONS
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
nodeTypes={NODE_TYPES}
|
||||
edgeTypes={EDGE_TYPES}
|
||||
defaultEdgeOptions={{ type: 'previewEdge' }}
|
||||
elementsSelectable={false}
|
||||
nodesDraggable
|
||||
nodesConnectable={false}
|
||||
zoomOnScroll={false}
|
||||
zoomOnDoubleClick={false}
|
||||
panOnScroll={false}
|
||||
zoomOnPinch={false}
|
||||
panOnDrag
|
||||
preventScrolling={false}
|
||||
autoPanOnNodeDrag={false}
|
||||
proOptions={PRO_OPTIONS}
|
||||
fitView
|
||||
fitViewOptions={resolvedFitViewOptions}
|
||||
className='h-full w-full bg-[#1b1b1b]'
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Lightweight ReactFlow canvas displaying an interactive workflow preview.
|
||||
* The key on workflow.id forces a clean remount on switch — instant fitView,
|
||||
* no timers, no flicker.
|
||||
*/
|
||||
export function LandingPreviewWorkflow({
|
||||
workflow,
|
||||
animate = false,
|
||||
fitViewOptions,
|
||||
}: LandingPreviewWorkflowProps) {
|
||||
return (
|
||||
<div className='h-full w-full'>
|
||||
<ReactFlowProvider key={workflow.id}>
|
||||
<PreviewFlow workflow={workflow} animate={animate} fitViewOptions={fitViewOptions} />
|
||||
</ReactFlowProvider>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,307 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { memo } from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { Database } from 'lucide-react'
|
||||
import { Handle, type NodeProps, Position } from 'reactflow'
|
||||
import {
|
||||
AgentIcon,
|
||||
AnthropicIcon,
|
||||
FirecrawlIcon,
|
||||
GeminiIcon,
|
||||
GithubIcon,
|
||||
GmailIcon,
|
||||
GoogleCalendarIcon,
|
||||
GoogleSheetsIcon,
|
||||
JiraIcon,
|
||||
LinearIcon,
|
||||
LinkedInIcon,
|
||||
MistralIcon,
|
||||
NotionIcon,
|
||||
OpenAIIcon,
|
||||
RedditIcon,
|
||||
ReductoIcon,
|
||||
ScheduleIcon,
|
||||
SlackIcon,
|
||||
StartIcon,
|
||||
SupabaseIcon,
|
||||
TelegramIcon,
|
||||
TextractIcon,
|
||||
WebhookIcon,
|
||||
xAIIcon,
|
||||
xIcon,
|
||||
YouTubeIcon,
|
||||
} from '@/components/icons'
|
||||
import {
|
||||
BLOCK_STAGGER,
|
||||
EASE_OUT,
|
||||
type PreviewTool,
|
||||
} from '@/app/(home)/components/landing-preview/components/landing-preview-workflow/workflow-data'
|
||||
|
||||
/** Map block type strings to their icon components. */
|
||||
const BLOCK_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {
|
||||
starter: StartIcon,
|
||||
start_trigger: StartIcon,
|
||||
agent: AgentIcon,
|
||||
slack: SlackIcon,
|
||||
jira: JiraIcon,
|
||||
x: xIcon,
|
||||
youtube: YouTubeIcon,
|
||||
schedule: ScheduleIcon,
|
||||
telegram: TelegramIcon,
|
||||
knowledge_base: Database,
|
||||
webhook: WebhookIcon,
|
||||
github: GithubIcon,
|
||||
supabase: SupabaseIcon,
|
||||
google_calendar: GoogleCalendarIcon,
|
||||
gmail: GmailIcon,
|
||||
google_sheets: GoogleSheetsIcon,
|
||||
linear: LinearIcon,
|
||||
firecrawl: FirecrawlIcon,
|
||||
reddit: RedditIcon,
|
||||
notion: NotionIcon,
|
||||
reducto: ReductoIcon,
|
||||
textract: TextractIcon,
|
||||
linkedin: LinkedInIcon,
|
||||
}
|
||||
|
||||
/** Model prefix → provider icon for the "Model" row in agent blocks. */
|
||||
const MODEL_PROVIDER_ICONS: Array<{
|
||||
prefix: string
|
||||
icon: React.ComponentType<{ className?: string }>
|
||||
size?: string
|
||||
}> = [
|
||||
{ prefix: 'gpt-', icon: OpenAIIcon },
|
||||
{ prefix: 'o3', icon: OpenAIIcon },
|
||||
{ prefix: 'o4', icon: OpenAIIcon },
|
||||
{ prefix: 'claude-', icon: AnthropicIcon },
|
||||
{ prefix: 'gemini-', icon: GeminiIcon },
|
||||
{ prefix: 'grok-', icon: xAIIcon, size: 'h-[17px] w-[17px]' },
|
||||
{ prefix: 'mistral-', icon: MistralIcon },
|
||||
]
|
||||
|
||||
function getModelIconEntry(modelValue: string) {
|
||||
const lower = modelValue.toLowerCase()
|
||||
return MODEL_PROVIDER_ICONS.find((m) => lower.startsWith(m.prefix)) ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
* Data shape for preview block nodes
|
||||
*/
|
||||
interface PreviewBlockData {
|
||||
name: string
|
||||
blockType: string
|
||||
bgColor: string
|
||||
rows: Array<{ title: string; value: string }>
|
||||
tools?: PreviewTool[]
|
||||
markdown?: string
|
||||
hideTargetHandle?: boolean
|
||||
hideSourceHandle?: boolean
|
||||
index?: number
|
||||
animate?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle styling matching the real WorkflowBlock handles.
|
||||
* --workflow-edge in dark mode: #454545
|
||||
*/
|
||||
const HANDLE_BASE = '!z-[10] !border-none !bg-[#454545]'
|
||||
const HANDLE_LEFT = `${HANDLE_BASE} !left-[-8px] !h-5 !w-[7px] !rounded-r-none !rounded-l-[2px]`
|
||||
const HANDLE_RIGHT = `${HANDLE_BASE} !right-[-8px] !h-5 !w-[7px] !rounded-l-none !rounded-r-[2px]`
|
||||
|
||||
/**
|
||||
* Static preview block node matching the real WorkflowBlock styling.
|
||||
* Renders a block header with icon + name, sub-block rows, and tool chips.
|
||||
*
|
||||
* Colors sourced from dark theme CSS variables:
|
||||
* --surface-2: #232323, --border-1: #3d3d3d
|
||||
* --text-primary: #e6e6e6, --text-tertiary: #b3b3b3
|
||||
*/
|
||||
export const PreviewBlockNode = memo(function PreviewBlockNode({
|
||||
data,
|
||||
}: NodeProps<PreviewBlockData>) {
|
||||
const {
|
||||
name,
|
||||
blockType,
|
||||
bgColor,
|
||||
rows,
|
||||
tools,
|
||||
markdown,
|
||||
hideTargetHandle,
|
||||
hideSourceHandle,
|
||||
index = 0,
|
||||
animate = false,
|
||||
} = data
|
||||
const Icon = BLOCK_ICONS[blockType]
|
||||
const delay = animate ? index * BLOCK_STAGGER : 0
|
||||
|
||||
if (blockType === 'note' && markdown) {
|
||||
return (
|
||||
<motion.div
|
||||
className='relative'
|
||||
initial={animate ? { opacity: 0 } : false}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.45, delay, ease: EASE_OUT }}
|
||||
>
|
||||
<div className='w-[280px] select-none rounded-[8px] border border-[#3d3d3d] bg-[#232323]'>
|
||||
<div className='border-[#3d3d3d] border-b p-[8px]'>
|
||||
<span className='font-medium text-[#e6e6e6] text-[16px]'>Note</span>
|
||||
</div>
|
||||
<div className='p-[10px]'>
|
||||
<NoteMarkdown content={markdown} />
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
const hasContent = rows.length > 0 || (tools && tools.length > 0)
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className='relative'
|
||||
initial={animate ? { opacity: 0 } : false}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.45, delay, ease: EASE_OUT }}
|
||||
>
|
||||
<div className='relative z-[20] w-[250px] select-none rounded-[8px] border border-[#3d3d3d] bg-[#232323]'>
|
||||
{/* Target handle (left side) */}
|
||||
{!hideTargetHandle && (
|
||||
<Handle
|
||||
type='target'
|
||||
position={Position.Left}
|
||||
id='target'
|
||||
className={HANDLE_LEFT}
|
||||
style={{ top: '20px', transform: 'translateY(-50%)' }}
|
||||
isConnectableStart={false}
|
||||
isConnectableEnd={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Header */}
|
||||
<div
|
||||
className={`flex items-center justify-between p-[8px] ${hasContent ? 'border-[#3d3d3d] border-b' : ''}`}
|
||||
>
|
||||
<div className='relative z-10 flex min-w-0 flex-1 items-center gap-[10px]'>
|
||||
<div
|
||||
className='flex h-[24px] w-[24px] flex-shrink-0 items-center justify-center rounded-[6px]'
|
||||
style={{ background: bgColor }}
|
||||
>
|
||||
{Icon && <Icon className='h-[16px] w-[16px] text-white' />}
|
||||
</div>
|
||||
<span className='truncate font-medium text-[#e6e6e6] text-[16px]'>{name}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sub-block rows + tools */}
|
||||
{hasContent && (
|
||||
<div className='flex flex-col gap-[8px] p-[8px]'>
|
||||
{rows.map((row) => {
|
||||
const modelEntry = row.title === 'Model' ? getModelIconEntry(row.value) : null
|
||||
const ModelIcon = modelEntry?.icon
|
||||
return (
|
||||
<div key={row.title} className='flex items-center gap-[8px]'>
|
||||
<span className='flex-shrink-0 font-normal text-[#b3b3b3] text-[14px] capitalize'>
|
||||
{row.title}
|
||||
</span>
|
||||
{row.value && (
|
||||
<span className='flex min-w-0 flex-1 items-center justify-end gap-[5px] font-normal text-[#e6e6e6] text-[14px]'>
|
||||
{ModelIcon && (
|
||||
<ModelIcon
|
||||
className={`inline-block flex-shrink-0 text-[#e6e6e6] ${modelEntry.size ?? 'h-[14px] w-[14px]'}`}
|
||||
/>
|
||||
)}
|
||||
<span className='truncate'>{row.value}</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
{/* Tool chips — inline with label */}
|
||||
{tools && tools.length > 0 && (
|
||||
<div className='flex items-center gap-[8px]'>
|
||||
<span className='flex-shrink-0 font-normal text-[#b3b3b3] text-[14px]'>Tools</span>
|
||||
<div className='flex flex-1 flex-wrap items-center justify-end gap-[5px]'>
|
||||
{tools.map((tool) => {
|
||||
const ToolIcon = BLOCK_ICONS[tool.type]
|
||||
return (
|
||||
<div
|
||||
key={tool.type}
|
||||
className='flex items-center gap-[5px] rounded-[5px] border border-[#3d3d3d] bg-[#2a2a2a] px-[6px] py-[3px]'
|
||||
>
|
||||
<div
|
||||
className='flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center rounded-[4px]'
|
||||
style={{ background: tool.bgColor }}
|
||||
>
|
||||
{ToolIcon && <ToolIcon className='h-[10px] w-[10px] text-white' />}
|
||||
</div>
|
||||
<span className='font-normal text-[#e6e6e6] text-[12px]'>{tool.name}</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Source handle (right side) */}
|
||||
{!hideSourceHandle && (
|
||||
<Handle
|
||||
type='source'
|
||||
position={Position.Right}
|
||||
id='source'
|
||||
className={HANDLE_RIGHT}
|
||||
style={{ top: '20px', transform: 'translateY(-50%)' }}
|
||||
isConnectableStart={false}
|
||||
isConnectableEnd={false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* Renders lightweight markdown-like content for note blocks.
|
||||
* Supports ### headings, **bold**, _italic_, --- rules, and blank-line spacing.
|
||||
*/
|
||||
function NoteMarkdown({ content }: { content: string }) {
|
||||
const lines = content.split('\n')
|
||||
|
||||
return (
|
||||
<div className='flex flex-col gap-[4px]'>
|
||||
{lines.map((line, i) => {
|
||||
const trimmed = line.trim()
|
||||
if (!trimmed) return <div key={i} className='h-[4px]' />
|
||||
|
||||
if (trimmed === '---') {
|
||||
return <hr key={i} className='my-[4px] border-[#3d3d3d] border-t' />
|
||||
}
|
||||
|
||||
if (trimmed.startsWith('### ')) {
|
||||
return (
|
||||
<p key={i} className='font-semibold text-[#e6e6e6] text-[16px] leading-[1.3]'>
|
||||
{trimmed.slice(4)}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<p
|
||||
key={i}
|
||||
className='font-medium text-[#e6e6e6] text-[13px] leading-[1.5]'
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: trimmed
|
||||
.replace(/\*\*_(.+?)_\*\*/g, '<strong><em>$1</em></strong>')
|
||||
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/_"(.+?)"_/g, '<em>“$1”</em>')
|
||||
.replace(/_(.+?)_/g, '<em>$1</em>'),
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,226 +0,0 @@
|
||||
import type { Edge, Node } from 'reactflow'
|
||||
import { Position } from 'reactflow'
|
||||
|
||||
/**
|
||||
* Tool entry displayed as a chip on agent blocks
|
||||
*/
|
||||
export interface PreviewTool {
|
||||
name: string
|
||||
type: string
|
||||
bgColor: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Static block definition for preview workflow nodes
|
||||
*/
|
||||
export interface PreviewBlock {
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
bgColor: string
|
||||
rows: Array<{ title: string; value: string }>
|
||||
tools?: PreviewTool[]
|
||||
markdown?: string
|
||||
position: { x: number; y: number }
|
||||
hideTargetHandle?: boolean
|
||||
hideSourceHandle?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Workflow definition containing nodes, edges, and metadata
|
||||
*/
|
||||
export interface PreviewWorkflow {
|
||||
id: string
|
||||
name: string
|
||||
color: string
|
||||
blocks: PreviewBlock[]
|
||||
edges: Array<{ id: string; source: string; target: string }>
|
||||
}
|
||||
|
||||
/**
|
||||
* IT Service Management workflow — Slack Trigger -> Agent (KB tool) -> Jira
|
||||
*/
|
||||
const IT_SERVICE_WORKFLOW: PreviewWorkflow = {
|
||||
id: 'wf-it-service',
|
||||
name: 'IT Service Management',
|
||||
color: '#FF6B2C',
|
||||
blocks: [
|
||||
{
|
||||
id: 'slack-1',
|
||||
name: 'Slack',
|
||||
type: 'slack',
|
||||
bgColor: '#611f69',
|
||||
rows: [
|
||||
{ title: 'Channel', value: '#it-support' },
|
||||
{ title: 'Event', value: 'New Message' },
|
||||
],
|
||||
position: { x: 80, y: 140 },
|
||||
hideTargetHandle: true,
|
||||
},
|
||||
{
|
||||
id: 'agent-1',
|
||||
name: 'Agent',
|
||||
type: 'agent',
|
||||
bgColor: '#701ffc',
|
||||
rows: [
|
||||
{ title: 'Model', value: 'claude-sonnet-4.6' },
|
||||
{ title: 'System Prompt', value: 'Triage incoming IT...' },
|
||||
],
|
||||
tools: [{ name: 'Knowledge Base', type: 'knowledge_base', bgColor: '#10B981' }],
|
||||
position: { x: 420, y: 40 },
|
||||
},
|
||||
{
|
||||
id: 'jira-1',
|
||||
name: 'Jira',
|
||||
type: 'jira',
|
||||
bgColor: '#E0E0E0',
|
||||
rows: [
|
||||
{ title: 'Operation', value: 'Get Issues' },
|
||||
{ title: 'Project', value: 'IT-Support' },
|
||||
],
|
||||
position: { x: 420, y: 260 },
|
||||
hideSourceHandle: true,
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{ id: 'e-1', source: 'slack-1', target: 'agent-1' },
|
||||
{ id: 'e-2', source: 'slack-1', target: 'jira-1' },
|
||||
],
|
||||
}
|
||||
|
||||
/**
|
||||
* Content pipeline workflow — Schedule -> Agent (X + YouTube tools)
|
||||
*/
|
||||
const CONTENT_PIPELINE_WORKFLOW: PreviewWorkflow = {
|
||||
id: 'wf-content-pipeline',
|
||||
name: 'Content Pipeline',
|
||||
color: '#33C482',
|
||||
blocks: [
|
||||
{
|
||||
id: 'schedule-1',
|
||||
name: 'Schedule',
|
||||
type: 'schedule',
|
||||
bgColor: '#6366F1',
|
||||
rows: [
|
||||
{ title: 'Run Frequency', value: 'Daily' },
|
||||
{ title: 'Time', value: '09:00 AM' },
|
||||
],
|
||||
position: { x: 80, y: 140 },
|
||||
hideTargetHandle: true,
|
||||
},
|
||||
{
|
||||
id: 'agent-2',
|
||||
name: 'Agent',
|
||||
type: 'agent',
|
||||
bgColor: '#701ffc',
|
||||
rows: [
|
||||
{ title: 'Model', value: 'grok-4' },
|
||||
{ title: 'System Prompt', value: 'Repurpose trending...' },
|
||||
],
|
||||
tools: [
|
||||
{ name: 'X', type: 'x', bgColor: '#000000' },
|
||||
{ name: 'YouTube', type: 'youtube', bgColor: '#FF0000' },
|
||||
],
|
||||
position: { x: 420, y: 180 },
|
||||
hideSourceHandle: true,
|
||||
},
|
||||
],
|
||||
edges: [{ id: 'e-3', source: 'schedule-1', target: 'agent-2' }],
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty "New Agent" workflow — a single note prompting the user to start building
|
||||
*/
|
||||
const NEW_AGENT_WORKFLOW: PreviewWorkflow = {
|
||||
id: 'wf-new-agent',
|
||||
name: 'New Agent',
|
||||
color: '#787878',
|
||||
blocks: [
|
||||
{
|
||||
id: 'note-1',
|
||||
name: '',
|
||||
type: 'note',
|
||||
bgColor: 'transparent',
|
||||
rows: [],
|
||||
markdown: '### What will you build?\n\n_"Find Linear todos and send in Slack"_',
|
||||
position: { x: 0, y: 0 },
|
||||
hideTargetHandle: true,
|
||||
hideSourceHandle: true,
|
||||
},
|
||||
],
|
||||
edges: [],
|
||||
}
|
||||
|
||||
export const PREVIEW_WORKFLOWS: PreviewWorkflow[] = [
|
||||
CONTENT_PIPELINE_WORKFLOW,
|
||||
IT_SERVICE_WORKFLOW,
|
||||
NEW_AGENT_WORKFLOW,
|
||||
]
|
||||
|
||||
/** Stagger delay between each block appearing (seconds). */
|
||||
export const BLOCK_STAGGER = 0.12
|
||||
|
||||
/** Shared cubic-bezier easing — fast deceleration, gentle settle. */
|
||||
export const EASE_OUT: [number, number, number, number] = [0.16, 1, 0.3, 1]
|
||||
|
||||
/** Shared edge style applied to all preview workflow connections */
|
||||
const EDGE_STYLE = { stroke: '#454545', strokeWidth: 1.5 } as const
|
||||
|
||||
/**
|
||||
* Converts a PreviewWorkflow to React Flow nodes and edges.
|
||||
*
|
||||
* @param workflow - The workflow definition
|
||||
* @param animate - When true, node/edge data includes animation metadata
|
||||
*/
|
||||
export function toReactFlowElements(
|
||||
workflow: PreviewWorkflow,
|
||||
animate = false
|
||||
): {
|
||||
nodes: Node[]
|
||||
edges: Edge[]
|
||||
} {
|
||||
const blockIndexMap = new Map(workflow.blocks.map((b, i) => [b.id, i]))
|
||||
|
||||
const nodes: Node[] = workflow.blocks.map((block, index) => ({
|
||||
id: block.id,
|
||||
type: 'previewBlock',
|
||||
position: block.position,
|
||||
data: {
|
||||
name: block.name,
|
||||
blockType: block.type,
|
||||
bgColor: block.bgColor,
|
||||
rows: block.rows,
|
||||
tools: block.tools,
|
||||
markdown: block.markdown,
|
||||
hideTargetHandle: block.hideTargetHandle,
|
||||
hideSourceHandle: block.hideSourceHandle,
|
||||
index,
|
||||
animate,
|
||||
},
|
||||
draggable: true,
|
||||
selectable: false,
|
||||
connectable: false,
|
||||
sourcePosition: Position.Right,
|
||||
targetPosition: Position.Left,
|
||||
}))
|
||||
|
||||
const edges: Edge[] = workflow.edges.map((e) => {
|
||||
const sourceIndex = blockIndexMap.get(e.source) ?? 0
|
||||
return {
|
||||
id: e.id,
|
||||
source: e.source,
|
||||
target: e.target,
|
||||
type: 'previewEdge',
|
||||
animated: false,
|
||||
style: EDGE_STYLE,
|
||||
sourceHandle: 'source',
|
||||
targetHandle: 'target',
|
||||
data: {
|
||||
animate,
|
||||
delay: animate ? sourceIndex * BLOCK_STAGGER + BLOCK_STAGGER : 0,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
return { nodes, edges }
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { motion, type Variants } from 'framer-motion'
|
||||
import { LandingPreviewPanel } from '@/app/(home)/components/landing-preview/components/landing-preview-panel/landing-preview-panel'
|
||||
import { LandingPreviewSidebar } from '@/app/(home)/components/landing-preview/components/landing-preview-sidebar/landing-preview-sidebar'
|
||||
import { LandingPreviewWorkflow } from '@/app/(home)/components/landing-preview/components/landing-preview-workflow/landing-preview-workflow'
|
||||
import {
|
||||
EASE_OUT,
|
||||
PREVIEW_WORKFLOWS,
|
||||
} from '@/app/(home)/components/landing-preview/components/landing-preview-workflow/workflow-data'
|
||||
|
||||
const containerVariants: Variants = {
|
||||
hidden: {},
|
||||
visible: {
|
||||
transition: { staggerChildren: 0.15 },
|
||||
},
|
||||
}
|
||||
|
||||
const sidebarVariants: Variants = {
|
||||
hidden: { opacity: 0, x: -12 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
transition: {
|
||||
x: { duration: 0.25, ease: EASE_OUT },
|
||||
opacity: { duration: 0.25, ease: EASE_OUT },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const panelVariants: Variants = {
|
||||
hidden: { opacity: 0, x: 12 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
transition: {
|
||||
x: { duration: 0.25, ease: EASE_OUT },
|
||||
opacity: { duration: 0.25, ease: EASE_OUT },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Interactive workspace preview for the hero section.
|
||||
*
|
||||
* Renders a lightweight replica of the Sim workspace with:
|
||||
* - A sidebar with two selectable workflows
|
||||
* - A ReactFlow canvas showing the active workflow's blocks and edges
|
||||
* - A panel with a functional copilot input (stores prompt + redirects to /signup)
|
||||
*
|
||||
* Everything except the workflow items and the copilot input is non-interactive.
|
||||
* On mount the sidebar slides from left and the panel from right. The canvas
|
||||
* background stays fully opaque; individual block nodes animate in with a
|
||||
* staggered fade. Edges draw left-to-right. Animations only fire on initial
|
||||
* load — workflow switches render instantly.
|
||||
*/
|
||||
export function LandingPreview() {
|
||||
const [activeWorkflowId, setActiveWorkflowId] = useState(PREVIEW_WORKFLOWS[0].id)
|
||||
const isInitialMount = useRef(true)
|
||||
|
||||
useEffect(() => {
|
||||
isInitialMount.current = false
|
||||
}, [])
|
||||
|
||||
const activeWorkflow =
|
||||
PREVIEW_WORKFLOWS.find((w) => w.id === activeWorkflowId) ?? PREVIEW_WORKFLOWS[0]
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className='dark flex aspect-[1116/549] w-full overflow-hidden rounded bg-[#1b1b1b] antialiased'
|
||||
initial='hidden'
|
||||
animate='visible'
|
||||
variants={containerVariants}
|
||||
>
|
||||
<motion.div className='hidden lg:flex' variants={sidebarVariants}>
|
||||
<LandingPreviewSidebar
|
||||
workflows={PREVIEW_WORKFLOWS}
|
||||
activeWorkflowId={activeWorkflowId}
|
||||
onSelectWorkflow={setActiveWorkflowId}
|
||||
/>
|
||||
</motion.div>
|
||||
<div className='relative flex-1 overflow-hidden'>
|
||||
<LandingPreviewWorkflow workflow={activeWorkflow} animate={isInitialMount.current} />
|
||||
</div>
|
||||
<motion.div className='hidden lg:flex' variants={panelVariants}>
|
||||
<LandingPreviewPanel />
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { GithubOutlineIcon } from '@/components/icons'
|
||||
import { getFormattedGitHubStars } from '@/app/(landing)/actions/github'
|
||||
|
||||
const logger = createLogger('github-stars')
|
||||
|
||||
const INITIAL_STARS = '26.4k'
|
||||
|
||||
/**
|
||||
* Client component that displays GitHub stars count.
|
||||
*
|
||||
* Isolated as a client component to allow the parent Navbar to remain
|
||||
* a Server Component for optimal SEO/GEO crawlability.
|
||||
*/
|
||||
export function GitHubStars() {
|
||||
const [stars, setStars] = useState(INITIAL_STARS)
|
||||
|
||||
useEffect(() => {
|
||||
getFormattedGitHubStars()
|
||||
.then(setStars)
|
||||
.catch((error) => {
|
||||
logger.warn('Failed to fetch GitHub stars', error)
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<a
|
||||
href='https://github.com/simstudioai/sim'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='flex items-center gap-[8px] px-[12px]'
|
||||
aria-label={`GitHub repository — ${stars} stars`}
|
||||
>
|
||||
<GithubOutlineIcon className='h-[14px] w-[14px]' />
|
||||
<span aria-live='polite'>{stars}</span>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { ChevronDown } from '@/components/emcn'
|
||||
import { GitHubStars } from '@/app/(home)/components/navbar/components/github-stars'
|
||||
|
||||
interface NavLink {
|
||||
label: string
|
||||
href: string
|
||||
external?: boolean
|
||||
icon?: 'chevron'
|
||||
}
|
||||
|
||||
const NAV_LINKS: NavLink[] = [
|
||||
{ label: 'Docs', href: '/docs', icon: 'chevron' },
|
||||
{ label: 'Pricing', href: '/pricing' },
|
||||
{ label: 'Careers', href: '/careers' },
|
||||
{ label: 'Enterprise', href: '/enterprise' },
|
||||
]
|
||||
|
||||
/** Logo and nav edge: horizontal padding (px) for left/right symmetry. */
|
||||
const LOGO_CELL = 'flex items-center px-[20px]'
|
||||
|
||||
/** Links: even spacing between items. */
|
||||
const LINK_CELL = 'flex items-center px-[14px]'
|
||||
|
||||
export default function Navbar() {
|
||||
return (
|
||||
<nav
|
||||
aria-label='Primary navigation'
|
||||
className='flex h-[52px] border-[#2A2A2A] border-b-[1px] bg-[#1C1C1C] font-[430] font-season text-[#ECECEC] text-[14px]'
|
||||
itemScope
|
||||
itemType='https://schema.org/SiteNavigationElement'
|
||||
>
|
||||
{/* Logo */}
|
||||
<Link href='/' className={LOGO_CELL} aria-label='Sim home' itemProp='url'>
|
||||
<span itemProp='name' className='sr-only'>
|
||||
Sim
|
||||
</span>
|
||||
<Image
|
||||
src='/logo/sim-landing.svg'
|
||||
alt='Sim'
|
||||
width={71}
|
||||
height={22}
|
||||
className='h-[22px] w-auto'
|
||||
priority
|
||||
/>
|
||||
</Link>
|
||||
|
||||
{/* Links */}
|
||||
<ul className='mt-[0.75px] flex'>
|
||||
{NAV_LINKS.map(({ label, href, external, icon }) => (
|
||||
<li key={label} className='flex'>
|
||||
{external ? (
|
||||
<a href={href} target='_blank' rel='noopener noreferrer' className={LINK_CELL}>
|
||||
{label}
|
||||
</a>
|
||||
) : (
|
||||
<Link
|
||||
href={href}
|
||||
className={icon ? `${LINK_CELL} gap-[8px]` : LINK_CELL}
|
||||
aria-label={label}
|
||||
>
|
||||
{label}
|
||||
{icon === 'chevron' && (
|
||||
<ChevronDown className='mt-[1.75px] h-[10px] w-[10px] flex-shrink-0 text-[#ECECEC]' />
|
||||
)}
|
||||
</Link>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
<li className='flex'>
|
||||
<GitHubStars />
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div className='flex-1' />
|
||||
|
||||
{/* CTAs */}
|
||||
<div className='flex items-center gap-[8px] px-[20px]'>
|
||||
<Link
|
||||
href='/login'
|
||||
className='inline-flex h-[30px] items-center rounded-[5px] border border-[#3d3d3d] px-[9px] text-[#ECECEC] text-[13.5px] transition-colors hover:bg-[#2A2A2A]'
|
||||
aria-label='Log in'
|
||||
>
|
||||
Log in
|
||||
</Link>
|
||||
<Link
|
||||
href='/signup'
|
||||
className='inline-flex h-[30px] items-center gap-[7px] rounded-[5px] border border-[#33C482] bg-[#33C482] px-[9px] text-[13.5px] text-black transition-[filter] hover:brightness-110'
|
||||
aria-label='Get started with Sim'
|
||||
>
|
||||
Get started
|
||||
</Link>
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
import Link from 'next/link'
|
||||
import { Badge } from '@/components/emcn'
|
||||
|
||||
interface PricingTier {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
price: string
|
||||
billingPeriod?: string
|
||||
color: string
|
||||
features: string[]
|
||||
cta: { label: string; href: string }
|
||||
}
|
||||
|
||||
const PRICING_TIERS: PricingTier[] = [
|
||||
{
|
||||
id: 'community',
|
||||
name: 'Community',
|
||||
description: 'For individuals getting started with AI agents',
|
||||
price: 'Free',
|
||||
color: '#2ABBF8',
|
||||
features: [
|
||||
'$20 usage limit',
|
||||
'5GB file storage',
|
||||
'5 min execution limit',
|
||||
'Limited log retention',
|
||||
'CLI/SDK Access',
|
||||
],
|
||||
cta: { label: 'Get started', href: '/signup' },
|
||||
},
|
||||
{
|
||||
id: 'professional',
|
||||
name: 'Professional',
|
||||
description: 'For professionals building production workflows',
|
||||
price: '$20',
|
||||
billingPeriod: 'per month',
|
||||
color: '#00F701',
|
||||
features: [
|
||||
'150 runs per minute (sync)',
|
||||
'1,000 runs per minute (async)',
|
||||
'50 min sync execution limit',
|
||||
'50GB file storage',
|
||||
'Unlimited invites',
|
||||
'Unlimited log retention',
|
||||
],
|
||||
cta: { label: 'Get started', href: '/signup' },
|
||||
},
|
||||
{
|
||||
id: 'team',
|
||||
name: 'Team',
|
||||
description: 'For teams collaborating on complex agents',
|
||||
price: '$40',
|
||||
billingPeriod: 'per month',
|
||||
color: '#FA4EDF',
|
||||
features: [
|
||||
'300 runs per minute (sync)',
|
||||
'2,500 runs per minute (async)',
|
||||
'500GB file storage (pooled)',
|
||||
'50 min sync execution limit',
|
||||
'Unlimited invites',
|
||||
'Unlimited log retention',
|
||||
'Dedicated Slack channel',
|
||||
],
|
||||
cta: { label: 'Get started', href: '/signup' },
|
||||
},
|
||||
{
|
||||
id: 'enterprise',
|
||||
name: 'Enterprise',
|
||||
description: 'For organizations needing security and scale',
|
||||
price: 'Custom',
|
||||
color: '#FFCC02',
|
||||
features: ['Custom rate limits', 'Custom file storage', 'SSO', 'SOC2', 'Dedicated support'],
|
||||
cta: { label: 'Book a demo', href: '/contact' },
|
||||
},
|
||||
]
|
||||
|
||||
function CheckIcon({ color }: { color: string }) {
|
||||
return (
|
||||
<svg width='14' height='14' viewBox='0 0 14 14' fill='none'>
|
||||
<path
|
||||
d='M2.5 7L5.5 10L11.5 4'
|
||||
stroke={color}
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
interface PricingCardProps {
|
||||
tier: PricingTier
|
||||
}
|
||||
|
||||
function PricingCard({ tier }: PricingCardProps) {
|
||||
const isEnterprise = tier.id === 'enterprise'
|
||||
const isProfessional = tier.id === 'professional'
|
||||
|
||||
return (
|
||||
<article className='flex flex-1 flex-col' aria-labelledby={`${tier.id}-heading`}>
|
||||
<div className='flex flex-1 flex-col gap-6 rounded-t-lg border border-[#E5E5E5] border-b-0 bg-white p-5'>
|
||||
<div className='flex flex-col'>
|
||||
<h3
|
||||
id={`${tier.id}-heading`}
|
||||
className='font-[430] font-season text-[#1C1C1C] text-[24px] leading-[100%] tracking-[-0.02em]'
|
||||
>
|
||||
{tier.name}
|
||||
</h3>
|
||||
<p className='mt-2 min-h-[44px] font-[430] font-season text-[#5c5c5c] text-[14px] leading-[125%] tracking-[0.02em]'>
|
||||
{tier.description}
|
||||
</p>
|
||||
<p className='mt-4 flex items-center gap-1.5 font-[430] font-season text-[#1C1C1C] text-[20px] leading-[100%] tracking-[-0.02em]'>
|
||||
{tier.price}
|
||||
{tier.billingPeriod && (
|
||||
<span className='text-[#737373] text-[16px]'>{tier.billingPeriod}</span>
|
||||
)}
|
||||
</p>
|
||||
<div className='mt-4'>
|
||||
{isEnterprise ? (
|
||||
<a
|
||||
href={tier.cta.href}
|
||||
className='flex h-[32px] w-full items-center justify-center rounded-[5px] border border-[#E5E5E5] px-[10px] font-[430] font-season text-[#1C1C1C] text-[14px] transition-colors hover:bg-[#F0F0F0]'
|
||||
>
|
||||
{tier.cta.label}
|
||||
</a>
|
||||
) : isProfessional ? (
|
||||
<Link
|
||||
href={tier.cta.href}
|
||||
className='flex h-[32px] w-full items-center justify-center rounded-[5px] border border-[#33C482] bg-[#33C482] px-[10px] font-[430] font-season text-[14px] text-white transition-[filter] hover:brightness-110'
|
||||
>
|
||||
{tier.cta.label}
|
||||
</Link>
|
||||
) : (
|
||||
<Link
|
||||
href={tier.cta.href}
|
||||
className='flex h-[32px] w-full items-center justify-center rounded-[5px] border border-[#E5E5E5] px-[10px] font-[430] font-season text-[#1C1C1C] text-[14px] transition-colors hover:bg-[#F0F0F0]'
|
||||
>
|
||||
{tier.cta.label}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul className='flex flex-col gap-2'>
|
||||
{tier.features.map((feature) => (
|
||||
<li key={feature} className='flex items-center gap-2'>
|
||||
<CheckIcon color='#404040' />
|
||||
<span className='font-[400] font-season text-[#5c5c5c] text-[14px] leading-[125%] tracking-[0.02em]'>
|
||||
{feature}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className='relative h-[6px]'>
|
||||
<div
|
||||
className='absolute inset-0 rounded-b-sm opacity-60'
|
||||
style={{ backgroundColor: tier.color }}
|
||||
/>
|
||||
<div
|
||||
className='absolute top-0 right-0 bottom-0 left-[12%] rounded-b-sm opacity-60'
|
||||
style={{ backgroundColor: tier.color }}
|
||||
/>
|
||||
<div
|
||||
className='absolute top-0 right-0 bottom-0 left-[25%] rounded-b-sm'
|
||||
style={{ backgroundColor: tier.color }}
|
||||
/>
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Pricing section — tiered pricing plans with feature comparison.
|
||||
*
|
||||
* SEO:
|
||||
* - `<section id="pricing" aria-labelledby="pricing-heading">`.
|
||||
* - `<h2 id="pricing-heading">` for the section title.
|
||||
* - Each tier: `<h3>` plan name + semantic `<ul>` feature list.
|
||||
* - Free tier CTA uses `<Link href="/signup">` (crawlable). Enterprise CTA uses `<a>`.
|
||||
*
|
||||
* GEO:
|
||||
* - Each plan has consistent structure: name, price, billing period, feature list.
|
||||
* - Lead with a summary: "Sim offers a free Community plan, $20/mo Pro, $40/mo Team, custom Enterprise."
|
||||
* - Prices must match the `Offer` items in structured-data.tsx exactly.
|
||||
*/
|
||||
export default function Pricing() {
|
||||
return (
|
||||
<section id='pricing' aria-labelledby='pricing-heading' className='bg-[#F6F6F6]'>
|
||||
<div className='px-4 pt-[100px] pb-8 sm:px-8 md:px-[80px]'>
|
||||
<div className='flex flex-col items-start gap-3 sm:gap-4 md:gap-[20px]'>
|
||||
<Badge
|
||||
variant='blue'
|
||||
size='md'
|
||||
dot
|
||||
className='bg-[#2ABBF8]/10 font-season text-[#2ABBF8] uppercase tracking-[0.02em]'
|
||||
>
|
||||
Pricing
|
||||
</Badge>
|
||||
|
||||
<h2
|
||||
id='pricing-heading'
|
||||
className='font-[430] font-season text-[#1C1C1C] text-[32px] leading-[100%] tracking-[-0.02em] sm:text-[36px] md:text-[40px]'
|
||||
>
|
||||
Pricing
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className='mt-12 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4'>
|
||||
{PRICING_TIERS.map((tier) => (
|
||||
<PricingCard key={tier.id} tier={tier} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -1,224 +0,0 @@
|
||||
/**
|
||||
* JSON-LD structured data for the landing page.
|
||||
*
|
||||
* Renders a `<script type="application/ld+json">` with Schema.org markup.
|
||||
* Single source of truth for machine-readable page metadata.
|
||||
*
|
||||
* Schemas: Organization, WebSite, WebPage, BreadcrumbList, WebApplication, FAQPage.
|
||||
*
|
||||
* AI crawler behavior (2025-2026):
|
||||
* - Google AI Overviews / Bing Copilot parse JSON-LD from their search indexes.
|
||||
* - GPTBot indexes JSON-LD during crawling (92% of LLM crawlers parse JSON-LD first).
|
||||
* - Perplexity / Claude prioritize visible HTML over JSON-LD during direct fetch.
|
||||
* - All claims here must also appear as visible text on the page.
|
||||
*
|
||||
* Maintenance:
|
||||
* - Offer prices must match the Pricing component exactly.
|
||||
* - `sameAs` links must match the Footer social links.
|
||||
* - Do not add `aggregateRating` without real, verifiable review data.
|
||||
*/
|
||||
export default function StructuredData() {
|
||||
const structuredData = {
|
||||
'@context': 'https://schema.org',
|
||||
'@graph': [
|
||||
{
|
||||
'@type': 'Organization',
|
||||
'@id': 'https://sim.ai/#organization',
|
||||
name: 'Sim',
|
||||
alternateName: 'Sim Studio',
|
||||
description:
|
||||
'Sim is the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows.',
|
||||
url: 'https://sim.ai',
|
||||
logo: {
|
||||
'@type': 'ImageObject',
|
||||
'@id': 'https://sim.ai/#logo',
|
||||
url: 'https://sim.ai/logo/b%26w/text/b%26w.svg',
|
||||
contentUrl: 'https://sim.ai/logo/b%26w/text/b%26w.svg',
|
||||
width: 49.78314,
|
||||
height: 24.276,
|
||||
caption: 'Sim Logo',
|
||||
},
|
||||
image: { '@id': 'https://sim.ai/#logo' },
|
||||
sameAs: [
|
||||
'https://x.com/simdotai',
|
||||
'https://github.com/simstudioai/sim',
|
||||
'https://www.linkedin.com/company/simstudioai/',
|
||||
'https://discord.gg/Hr4UWYEcTT',
|
||||
],
|
||||
contactPoint: {
|
||||
'@type': 'ContactPoint',
|
||||
contactType: 'customer support',
|
||||
availableLanguage: ['en'],
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'WebSite',
|
||||
'@id': 'https://sim.ai/#website',
|
||||
url: 'https://sim.ai',
|
||||
name: 'Sim — Build AI Agents & Run Your Agentic Workforce',
|
||||
description:
|
||||
'Sim is the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows. Join 100,000+ builders.',
|
||||
publisher: { '@id': 'https://sim.ai/#organization' },
|
||||
inLanguage: 'en-US',
|
||||
},
|
||||
{
|
||||
'@type': 'WebPage',
|
||||
'@id': 'https://sim.ai/#webpage',
|
||||
url: 'https://sim.ai',
|
||||
name: 'Sim — Build AI Agents & Run Your Agentic Workforce',
|
||||
isPartOf: { '@id': 'https://sim.ai/#website' },
|
||||
about: { '@id': 'https://sim.ai/#software' },
|
||||
datePublished: '2024-01-01T00:00:00+00:00',
|
||||
dateModified: new Date().toISOString(),
|
||||
description:
|
||||
'Sim is the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows. Create agents, workflows, knowledge bases, tables, and docs.',
|
||||
breadcrumb: { '@id': 'https://sim.ai/#breadcrumb' },
|
||||
inLanguage: 'en-US',
|
||||
potentialAction: [{ '@type': 'ReadAction', target: ['https://sim.ai'] }],
|
||||
},
|
||||
{
|
||||
'@type': 'BreadcrumbList',
|
||||
'@id': 'https://sim.ai/#breadcrumb',
|
||||
itemListElement: [
|
||||
{ '@type': 'ListItem', position: 1, name: 'Home', item: 'https://sim.ai' },
|
||||
],
|
||||
},
|
||||
{
|
||||
'@type': 'WebApplication',
|
||||
'@id': 'https://sim.ai/#software',
|
||||
url: 'https://sim.ai',
|
||||
name: 'Sim — Build AI Agents & Run Your Agentic Workforce',
|
||||
description:
|
||||
'Sim is the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows. Create agents, workflows, knowledge bases, tables, and docs. Trusted by over 100,000 builders. SOC2 and HIPAA compliant.',
|
||||
applicationCategory: 'DeveloperApplication',
|
||||
operatingSystem: 'Web',
|
||||
browserRequirements: 'Requires a modern browser with JavaScript enabled',
|
||||
offers: [
|
||||
{
|
||||
'@type': 'Offer',
|
||||
name: 'Community Plan',
|
||||
price: '0',
|
||||
priceCurrency: 'USD',
|
||||
availability: 'https://schema.org/InStock',
|
||||
},
|
||||
{
|
||||
'@type': 'Offer',
|
||||
name: 'Pro Plan',
|
||||
price: '20',
|
||||
priceCurrency: 'USD',
|
||||
priceSpecification: {
|
||||
'@type': 'UnitPriceSpecification',
|
||||
price: '20',
|
||||
priceCurrency: 'USD',
|
||||
unitText: 'MONTH',
|
||||
billingIncrement: 1,
|
||||
},
|
||||
availability: 'https://schema.org/InStock',
|
||||
},
|
||||
{
|
||||
'@type': 'Offer',
|
||||
name: 'Team Plan',
|
||||
price: '40',
|
||||
priceCurrency: 'USD',
|
||||
priceSpecification: {
|
||||
'@type': 'UnitPriceSpecification',
|
||||
price: '40',
|
||||
priceCurrency: 'USD',
|
||||
unitText: 'MONTH',
|
||||
billingIncrement: 1,
|
||||
},
|
||||
availability: 'https://schema.org/InStock',
|
||||
},
|
||||
],
|
||||
featureList: [
|
||||
'AI agent creation',
|
||||
'Agentic workflow orchestration',
|
||||
'1,000+ integrations',
|
||||
'LLM orchestration (OpenAI, Anthropic, Google, xAI, Mistral, Perplexity)',
|
||||
'Knowledge base creation',
|
||||
'Table creation',
|
||||
'Document creation',
|
||||
'API access',
|
||||
'Custom functions',
|
||||
'Scheduled workflows',
|
||||
'Event triggers',
|
||||
],
|
||||
review: [
|
||||
{
|
||||
'@type': 'Review',
|
||||
author: { '@type': 'Person', name: 'Hasan Toor' },
|
||||
reviewBody:
|
||||
'This startup just dropped the fastest way to build AI agents. This Figma-like canvas to build agents will blow your mind.',
|
||||
url: 'https://x.com/hasantoxr/status/1912909502036525271',
|
||||
},
|
||||
{
|
||||
'@type': 'Review',
|
||||
author: { '@type': 'Person', name: 'nizzy' },
|
||||
reviewBody:
|
||||
'This is the zapier of agent building. I always believed that building agents and using AI should not be limited to technical people. I think this solves just that.',
|
||||
url: 'https://x.com/nizzyabi/status/1907864421227180368',
|
||||
},
|
||||
{
|
||||
'@type': 'Review',
|
||||
author: { '@type': 'Organization', name: 'xyflow' },
|
||||
reviewBody: 'A very good looking agent workflow builder and open source!',
|
||||
url: 'https://x.com/xyflowdev/status/1909501499719438670',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'@type': 'FAQPage',
|
||||
'@id': 'https://sim.ai/#faq',
|
||||
mainEntity: [
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'What is Sim?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'Sim is the open-source platform to build AI agents and run your agentic workforce. Teams connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows. Create agents, workflows, knowledge bases, tables, and docs. Trusted by over 100,000 builders. SOC2 and HIPAA compliant.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'Which AI models does Sim support?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'Sim supports all major AI models including OpenAI (GPT-5, GPT-4o), Anthropic (Claude), Google (Gemini), xAI (Grok), Mistral, Perplexity, and many more. You can also connect to open-source models via Ollama.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'How much does Sim cost?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'Sim offers a free Community plan with $20 usage limit, a Pro plan at $20/month, a Team plan at $40/month, and custom Enterprise pricing. All plans include CLI/SDK access.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'Do I need coding skills to use Sim?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'No coding skills are required. Sim provides a visual interface for building AI agents and agentic workflows. Developers can also use custom functions, the API, and the CLI/SDK for advanced use cases.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'What enterprise features does Sim offer?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'Sim offers SOC2 and HIPAA compliance, SSO/SAML authentication, role-based access control, audit logs, dedicated support, custom SLAs, and on-premise deployment options for enterprise customers.',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
return (
|
||||
<script
|
||||
type='application/ld+json'
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,582 +0,0 @@
|
||||
import type { PreviewWorkflow } from '@/app/(home)/components/landing-preview/components/landing-preview-workflow/workflow-data'
|
||||
|
||||
/**
|
||||
* OCR Invoice to DB — Start → Agent (Textract) → Supabase
|
||||
* Pattern: Straight line (all blocks aligned at top)
|
||||
*/
|
||||
const OCR_INVOICE_WORKFLOW: PreviewWorkflow = {
|
||||
id: 'tpl-ocr-invoice',
|
||||
name: 'OCR Invoice to DB',
|
||||
color: '#2ABBF8',
|
||||
blocks: [
|
||||
{
|
||||
id: 'starter-1',
|
||||
name: 'Start',
|
||||
type: 'starter',
|
||||
bgColor: '#34B5FF',
|
||||
rows: [{ title: 'URL', value: 'invoice.pdf' }],
|
||||
position: { x: 40, y: 80 },
|
||||
hideTargetHandle: true,
|
||||
},
|
||||
{
|
||||
id: 'agent-1',
|
||||
name: 'Agent',
|
||||
type: 'agent',
|
||||
bgColor: '#701ffc',
|
||||
rows: [
|
||||
{ title: 'Model', value: 'gpt-5.2' },
|
||||
{ title: 'System Prompt', value: 'Extract invoice fields...' },
|
||||
],
|
||||
tools: [{ name: 'Textract', type: 'textract', bgColor: '#055F4E' }],
|
||||
position: { x: 400, y: 100 },
|
||||
},
|
||||
{
|
||||
id: 'supabase-1',
|
||||
name: 'Supabase',
|
||||
type: 'supabase',
|
||||
bgColor: '#1C1C1C',
|
||||
rows: [
|
||||
{ title: 'Table', value: 'invoices' },
|
||||
{ title: 'Operation', value: 'Insert Row' },
|
||||
],
|
||||
position: { x: 760, y: 80 },
|
||||
hideSourceHandle: true,
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{ id: 'e-1', source: 'starter-1', target: 'agent-1' },
|
||||
{ id: 'e-2', source: 'agent-1', target: 'supabase-1' },
|
||||
],
|
||||
}
|
||||
|
||||
/**
|
||||
* GitHub Release Agent — GitHub → Agent → Slack
|
||||
* Pattern: Convex (low → high → low)
|
||||
*/
|
||||
const GITHUB_RELEASE_WORKFLOW: PreviewWorkflow = {
|
||||
id: 'tpl-github-release',
|
||||
name: 'GitHub Release Agent',
|
||||
color: '#00F701',
|
||||
blocks: [
|
||||
{
|
||||
id: 'github-1',
|
||||
name: 'GitHub',
|
||||
type: 'github',
|
||||
bgColor: '#181C1E',
|
||||
rows: [
|
||||
{ title: 'Event', value: 'New Release' },
|
||||
{ title: 'Repository', value: 'org/repo' },
|
||||
],
|
||||
position: { x: 60, y: 140 },
|
||||
hideTargetHandle: true,
|
||||
},
|
||||
{
|
||||
id: 'agent-2',
|
||||
name: 'Agent',
|
||||
type: 'agent',
|
||||
bgColor: '#701ffc',
|
||||
rows: [
|
||||
{ title: 'Model', value: 'claude-sonnet-4.6' },
|
||||
{ title: 'System Prompt', value: 'Summarize changelog...' },
|
||||
],
|
||||
position: { x: 370, y: 50 },
|
||||
},
|
||||
{
|
||||
id: 'slack-1',
|
||||
name: 'Slack',
|
||||
type: 'slack',
|
||||
bgColor: '#611f69',
|
||||
rows: [
|
||||
{ title: 'Channel', value: '#releases' },
|
||||
{ title: 'Operation', value: 'Send Message' },
|
||||
],
|
||||
position: { x: 680, y: 140 },
|
||||
hideSourceHandle: true,
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{ id: 'e-1', source: 'github-1', target: 'agent-2' },
|
||||
{ id: 'e-2', source: 'agent-2', target: 'slack-1' },
|
||||
],
|
||||
}
|
||||
|
||||
/**
|
||||
* Meeting Follow-up Agent — Google Calendar → Agent → Gmail
|
||||
* Pattern: Concave (high → low → high)
|
||||
*/
|
||||
const MEETING_FOLLOWUP_WORKFLOW: PreviewWorkflow = {
|
||||
id: 'tpl-meeting-followup',
|
||||
name: 'Meeting Follow-up Agent',
|
||||
color: '#FFCC02',
|
||||
blocks: [
|
||||
{
|
||||
id: 'gcal-1',
|
||||
name: 'Google Calendar',
|
||||
type: 'google_calendar',
|
||||
bgColor: '#E0E0E0',
|
||||
rows: [
|
||||
{ title: 'Event', value: 'Meeting Ended' },
|
||||
{ title: 'Calendar', value: 'Work' },
|
||||
],
|
||||
position: { x: 60, y: 60 },
|
||||
hideTargetHandle: true,
|
||||
},
|
||||
{
|
||||
id: 'agent-3',
|
||||
name: 'Agent',
|
||||
type: 'agent',
|
||||
bgColor: '#701ffc',
|
||||
rows: [
|
||||
{ title: 'Model', value: 'gemini-2.5-pro' },
|
||||
{ title: 'System Prompt', value: 'Draft follow-up email...' },
|
||||
],
|
||||
position: { x: 370, y: 150 },
|
||||
},
|
||||
{
|
||||
id: 'gmail-1',
|
||||
name: 'Gmail',
|
||||
type: 'gmail',
|
||||
bgColor: '#E0E0E0',
|
||||
rows: [
|
||||
{ title: 'Operation', value: 'Send Email' },
|
||||
{ title: 'To', value: 'attendees' },
|
||||
],
|
||||
position: { x: 680, y: 60 },
|
||||
hideSourceHandle: true,
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{ id: 'e-1', source: 'gcal-1', target: 'agent-3' },
|
||||
{ id: 'e-2', source: 'agent-3', target: 'gmail-1' },
|
||||
],
|
||||
}
|
||||
|
||||
/**
|
||||
* CV/Resume Scanner — Start → Agent (Reducto) → Google Sheets
|
||||
* Pattern: Convex (low → high → low)
|
||||
*/
|
||||
const CV_SCANNER_WORKFLOW: PreviewWorkflow = {
|
||||
id: 'tpl-cv-scanner',
|
||||
name: 'CV/Resume Scanner',
|
||||
color: '#FA4EDF',
|
||||
blocks: [
|
||||
{
|
||||
id: 'starter-2',
|
||||
name: 'Start',
|
||||
type: 'starter',
|
||||
bgColor: '#34B5FF',
|
||||
rows: [{ title: 'File URL', value: 'resume.pdf' }],
|
||||
position: { x: 60, y: 145 },
|
||||
hideTargetHandle: true,
|
||||
},
|
||||
{
|
||||
id: 'agent-4',
|
||||
name: 'Agent',
|
||||
type: 'agent',
|
||||
bgColor: '#701ffc',
|
||||
rows: [
|
||||
{ title: 'Model', value: 'claude-opus-4.6' },
|
||||
{ title: 'System Prompt', value: 'Parse resume fields...' },
|
||||
],
|
||||
tools: [{ name: 'Reducto', type: 'reducto', bgColor: '#5c0c5c' }],
|
||||
position: { x: 370, y: 55 },
|
||||
},
|
||||
{
|
||||
id: 'gsheets-1',
|
||||
name: 'Google Sheets',
|
||||
type: 'google_sheets',
|
||||
bgColor: '#E0E0E0',
|
||||
rows: [
|
||||
{ title: 'Spreadsheet', value: 'Candidates' },
|
||||
{ title: 'Operation', value: 'Append Row' },
|
||||
],
|
||||
position: { x: 680, y: 145 },
|
||||
hideSourceHandle: true,
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{ id: 'e-1', source: 'starter-2', target: 'agent-4' },
|
||||
{ id: 'e-2', source: 'agent-4', target: 'gsheets-1' },
|
||||
],
|
||||
}
|
||||
|
||||
/**
|
||||
* Email Triage Agent — Gmail → Agent (KB) → fan-out to Slack + Linear
|
||||
* Pattern: Fan-out (input low → agent mid → outputs spread vertically)
|
||||
*/
|
||||
const EMAIL_TRIAGE_WORKFLOW: PreviewWorkflow = {
|
||||
id: 'tpl-email-triage',
|
||||
name: 'Email Triage Agent',
|
||||
color: '#FF6B2C',
|
||||
blocks: [
|
||||
{
|
||||
id: 'gmail-2',
|
||||
name: 'Gmail',
|
||||
type: 'gmail',
|
||||
bgColor: '#E0E0E0',
|
||||
rows: [
|
||||
{ title: 'Event', value: 'New Email' },
|
||||
{ title: 'Label', value: 'Inbox' },
|
||||
],
|
||||
position: { x: 60, y: 130 },
|
||||
hideTargetHandle: true,
|
||||
},
|
||||
{
|
||||
id: 'agent-5',
|
||||
name: 'Agent',
|
||||
type: 'agent',
|
||||
bgColor: '#701ffc',
|
||||
rows: [
|
||||
{ title: 'Model', value: 'gpt-5.2-mini' },
|
||||
{ title: 'System Prompt', value: 'Classify and route...' },
|
||||
],
|
||||
tools: [{ name: 'Knowledge Base', type: 'knowledge_base', bgColor: '#00B0B0' }],
|
||||
position: { x: 370, y: 100 },
|
||||
},
|
||||
{
|
||||
id: 'slack-2',
|
||||
name: 'Slack',
|
||||
type: 'slack',
|
||||
bgColor: '#611f69',
|
||||
rows: [
|
||||
{ title: 'Channel', value: '#urgent' },
|
||||
{ title: 'Operation', value: 'Send Message' },
|
||||
],
|
||||
position: { x: 680, y: 20 },
|
||||
hideSourceHandle: true,
|
||||
},
|
||||
{
|
||||
id: 'linear-1',
|
||||
name: 'Linear',
|
||||
type: 'linear',
|
||||
bgColor: '#5E6AD2',
|
||||
rows: [
|
||||
{ title: 'Project', value: 'Support' },
|
||||
{ title: 'Operation', value: 'Create Issue' },
|
||||
],
|
||||
position: { x: 680, y: 200 },
|
||||
hideSourceHandle: true,
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{ id: 'e-1', source: 'gmail-2', target: 'agent-5' },
|
||||
{ id: 'e-2', source: 'agent-5', target: 'slack-2' },
|
||||
{ id: 'e-3', source: 'agent-5', target: 'linear-1' },
|
||||
],
|
||||
}
|
||||
|
||||
/**
|
||||
* Competitor Monitor — Schedule → Agent (Firecrawl) → Slack
|
||||
* Pattern: Concave (high → low → high)
|
||||
*/
|
||||
const COMPETITOR_MONITOR_WORKFLOW: PreviewWorkflow = {
|
||||
id: 'tpl-competitor-monitor',
|
||||
name: 'Competitor Monitor',
|
||||
color: '#6366F1',
|
||||
blocks: [
|
||||
{
|
||||
id: 'schedule-1',
|
||||
name: 'Schedule',
|
||||
type: 'schedule',
|
||||
bgColor: '#6366F1',
|
||||
rows: [
|
||||
{ title: 'Run Frequency', value: 'Daily' },
|
||||
{ title: 'Time', value: '08:00 AM' },
|
||||
],
|
||||
position: { x: 60, y: 50 },
|
||||
hideTargetHandle: true,
|
||||
},
|
||||
{
|
||||
id: 'agent-6',
|
||||
name: 'Agent',
|
||||
type: 'agent',
|
||||
bgColor: '#701ffc',
|
||||
rows: [
|
||||
{ title: 'Model', value: 'grok-4' },
|
||||
{ title: 'System Prompt', value: 'Monitor competitor...' },
|
||||
],
|
||||
tools: [{ name: 'Firecrawl', type: 'firecrawl', bgColor: '#181C1E' }],
|
||||
position: { x: 370, y: 150 },
|
||||
},
|
||||
{
|
||||
id: 'slack-3',
|
||||
name: 'Slack',
|
||||
type: 'slack',
|
||||
bgColor: '#611f69',
|
||||
rows: [
|
||||
{ title: 'Channel', value: '#competitive-intel' },
|
||||
{ title: 'Operation', value: 'Send Message' },
|
||||
],
|
||||
position: { x: 680, y: 50 },
|
||||
hideSourceHandle: true,
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{ id: 'e-1', source: 'schedule-1', target: 'agent-6' },
|
||||
{ id: 'e-2', source: 'agent-6', target: 'slack-3' },
|
||||
],
|
||||
}
|
||||
|
||||
/**
|
||||
* Social Listening Agent — Schedule → Agent (Reddit + X) → Notion
|
||||
* Pattern: Convex (low → high → low)
|
||||
*/
|
||||
const SOCIAL_LISTENING_WORKFLOW: PreviewWorkflow = {
|
||||
id: 'tpl-social-listening',
|
||||
name: 'Social Listening Agent',
|
||||
color: '#F43F5E',
|
||||
blocks: [
|
||||
{
|
||||
id: 'schedule-2',
|
||||
name: 'Schedule',
|
||||
type: 'schedule',
|
||||
bgColor: '#6366F1',
|
||||
rows: [{ title: 'Run Frequency', value: 'Hourly' }],
|
||||
position: { x: 60, y: 150 },
|
||||
hideTargetHandle: true,
|
||||
},
|
||||
{
|
||||
id: 'agent-7',
|
||||
name: 'Agent',
|
||||
type: 'agent',
|
||||
bgColor: '#701ffc',
|
||||
rows: [
|
||||
{ title: 'Model', value: 'gemini-2.5-flash' },
|
||||
{ title: 'System Prompt', value: 'Track brand mentions...' },
|
||||
],
|
||||
tools: [
|
||||
{ name: 'Reddit', type: 'reddit', bgColor: '#FF5700' },
|
||||
{ name: 'X', type: 'x', bgColor: '#000000' },
|
||||
],
|
||||
position: { x: 370, y: 55 },
|
||||
},
|
||||
{
|
||||
id: 'notion-1',
|
||||
name: 'Notion',
|
||||
type: 'notion',
|
||||
bgColor: '#181C1E',
|
||||
rows: [
|
||||
{ title: 'Database', value: 'Brand Mentions' },
|
||||
{ title: 'Operation', value: 'Create Page' },
|
||||
],
|
||||
position: { x: 680, y: 150 },
|
||||
hideSourceHandle: true,
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{ id: 'e-1', source: 'schedule-2', target: 'agent-7' },
|
||||
{ id: 'e-2', source: 'agent-7', target: 'notion-1' },
|
||||
],
|
||||
}
|
||||
|
||||
/**
|
||||
* Data Enrichment Pipeline — Start → Agent (LinkedIn) → Google Sheets
|
||||
* Pattern: Concave (high → low → high)
|
||||
*/
|
||||
const DATA_ENRICHMENT_WORKFLOW: PreviewWorkflow = {
|
||||
id: 'tpl-data-enrichment',
|
||||
name: 'Data Enrichment Pipeline',
|
||||
color: '#14B8A6',
|
||||
blocks: [
|
||||
{
|
||||
id: 'starter-3',
|
||||
name: 'Start',
|
||||
type: 'starter',
|
||||
bgColor: '#34B5FF',
|
||||
rows: [{ title: 'Email', value: 'lead@company.com' }],
|
||||
position: { x: 60, y: 55 },
|
||||
hideTargetHandle: true,
|
||||
},
|
||||
{
|
||||
id: 'agent-8',
|
||||
name: 'Agent',
|
||||
type: 'agent',
|
||||
bgColor: '#701ffc',
|
||||
rows: [
|
||||
{ title: 'Model', value: 'mistral-large' },
|
||||
{ title: 'System Prompt', value: 'Enrich lead data...' },
|
||||
],
|
||||
tools: [{ name: 'LinkedIn', type: 'linkedin', bgColor: '#0072B1' }],
|
||||
position: { x: 370, y: 145 },
|
||||
},
|
||||
{
|
||||
id: 'gsheets-2',
|
||||
name: 'Google Sheets',
|
||||
type: 'google_sheets',
|
||||
bgColor: '#E0E0E0',
|
||||
rows: [
|
||||
{ title: 'Spreadsheet', value: 'Lead Database' },
|
||||
{ title: 'Operation', value: 'Update Row' },
|
||||
],
|
||||
position: { x: 680, y: 55 },
|
||||
hideSourceHandle: true,
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{ id: 'e-1', source: 'starter-3', target: 'agent-8' },
|
||||
{ id: 'e-2', source: 'agent-8', target: 'gsheets-2' },
|
||||
],
|
||||
}
|
||||
|
||||
/**
|
||||
* Customer Feedback Digest — Schedule → Agent → Slack
|
||||
* Pattern: Convex (low → high → low)
|
||||
*/
|
||||
const FEEDBACK_DIGEST_WORKFLOW: PreviewWorkflow = {
|
||||
id: 'tpl-feedback-digest',
|
||||
name: 'Customer Feedback Digest',
|
||||
color: '#F59E0B',
|
||||
blocks: [
|
||||
{
|
||||
id: 'schedule-3',
|
||||
name: 'Schedule',
|
||||
type: 'schedule',
|
||||
bgColor: '#6366F1',
|
||||
rows: [
|
||||
{ title: 'Run Frequency', value: 'Daily' },
|
||||
{ title: 'Time', value: '09:00 AM' },
|
||||
],
|
||||
position: { x: 60, y: 145 },
|
||||
hideTargetHandle: true,
|
||||
},
|
||||
{
|
||||
id: 'agent-9',
|
||||
name: 'Agent',
|
||||
type: 'agent',
|
||||
bgColor: '#701ffc',
|
||||
rows: [
|
||||
{ title: 'Model', value: 'claude-sonnet-4.6' },
|
||||
{ title: 'System Prompt', value: 'Analyze customer feedback...' },
|
||||
],
|
||||
tools: [{ name: 'Airtable', type: 'airtable', bgColor: '#18BFFF' }],
|
||||
position: { x: 370, y: 50 },
|
||||
},
|
||||
{
|
||||
id: 'slack-4',
|
||||
name: 'Slack',
|
||||
type: 'slack',
|
||||
bgColor: '#611f69',
|
||||
rows: [
|
||||
{ title: 'Channel', value: '#product-feedback' },
|
||||
{ title: 'Operation', value: 'Send Message' },
|
||||
],
|
||||
position: { x: 680, y: 145 },
|
||||
hideSourceHandle: true,
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{ id: 'e-1', source: 'schedule-3', target: 'agent-9' },
|
||||
{ id: 'e-2', source: 'agent-9', target: 'slack-4' },
|
||||
],
|
||||
}
|
||||
|
||||
/**
|
||||
* PR Review Agent — GitHub → Agent → Slack
|
||||
* Pattern: Concave (high → low → high)
|
||||
*/
|
||||
const PR_REVIEW_WORKFLOW: PreviewWorkflow = {
|
||||
id: 'tpl-pr-review',
|
||||
name: 'PR Review Agent',
|
||||
color: '#06B6D4',
|
||||
blocks: [
|
||||
{
|
||||
id: 'github-2',
|
||||
name: 'GitHub',
|
||||
type: 'github',
|
||||
bgColor: '#181C1E',
|
||||
rows: [
|
||||
{ title: 'Event', value: 'Pull Request Opened' },
|
||||
{ title: 'Repository', value: 'org/repo' },
|
||||
],
|
||||
position: { x: 60, y: 60 },
|
||||
hideTargetHandle: true,
|
||||
},
|
||||
{
|
||||
id: 'agent-10',
|
||||
name: 'Agent',
|
||||
type: 'agent',
|
||||
bgColor: '#701ffc',
|
||||
rows: [
|
||||
{ title: 'Model', value: 'gpt-5.2' },
|
||||
{ title: 'System Prompt', value: 'Review code changes...' },
|
||||
],
|
||||
position: { x: 370, y: 155 },
|
||||
},
|
||||
{
|
||||
id: 'slack-5',
|
||||
name: 'Slack',
|
||||
type: 'slack',
|
||||
bgColor: '#611f69',
|
||||
rows: [
|
||||
{ title: 'Channel', value: '#code-reviews' },
|
||||
{ title: 'Operation', value: 'Send Message' },
|
||||
],
|
||||
position: { x: 680, y: 60 },
|
||||
hideSourceHandle: true,
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{ id: 'e-1', source: 'github-2', target: 'agent-10' },
|
||||
{ id: 'e-2', source: 'agent-10', target: 'slack-5' },
|
||||
],
|
||||
}
|
||||
|
||||
/**
|
||||
* Knowledge Base QA — Start → Agent (KB) → Response
|
||||
* Pattern: Convex (low → high → low)
|
||||
*/
|
||||
const KNOWLEDGE_QA_WORKFLOW: PreviewWorkflow = {
|
||||
id: 'tpl-knowledge-qa',
|
||||
name: 'Knowledge Base QA',
|
||||
color: '#84CC16',
|
||||
blocks: [
|
||||
{
|
||||
id: 'starter-4',
|
||||
name: 'Start',
|
||||
type: 'starter',
|
||||
bgColor: '#34B5FF',
|
||||
rows: [{ title: 'Question', value: 'How do I...' }],
|
||||
position: { x: 60, y: 140 },
|
||||
hideTargetHandle: true,
|
||||
},
|
||||
{
|
||||
id: 'agent-11',
|
||||
name: 'Agent',
|
||||
type: 'agent',
|
||||
bgColor: '#701ffc',
|
||||
rows: [
|
||||
{ title: 'Model', value: 'gemini-2.5-pro' },
|
||||
{ title: 'System Prompt', value: 'Answer using knowledge...' },
|
||||
],
|
||||
tools: [{ name: 'Knowledge Base', type: 'knowledge_base', bgColor: '#00B0B0' }],
|
||||
position: { x: 370, y: 50 },
|
||||
},
|
||||
{
|
||||
id: 'starter-5',
|
||||
name: 'Response',
|
||||
type: 'starter',
|
||||
bgColor: '#34B5FF',
|
||||
rows: [{ title: 'Answer', value: 'Based on your docs...' }],
|
||||
position: { x: 680, y: 140 },
|
||||
hideSourceHandle: true,
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{ id: 'e-1', source: 'starter-4', target: 'agent-11' },
|
||||
{ id: 'e-2', source: 'agent-11', target: 'starter-5' },
|
||||
],
|
||||
}
|
||||
|
||||
export const TEMPLATE_WORKFLOWS: PreviewWorkflow[] = [
|
||||
OCR_INVOICE_WORKFLOW,
|
||||
GITHUB_RELEASE_WORKFLOW,
|
||||
MEETING_FOLLOWUP_WORKFLOW,
|
||||
CV_SCANNER_WORKFLOW,
|
||||
EMAIL_TRIAGE_WORKFLOW,
|
||||
COMPETITOR_MONITOR_WORKFLOW,
|
||||
SOCIAL_LISTENING_WORKFLOW,
|
||||
DATA_ENRICHMENT_WORKFLOW,
|
||||
FEEDBACK_DIGEST_WORKFLOW,
|
||||
PR_REVIEW_WORKFLOW,
|
||||
KNOWLEDGE_QA_WORKFLOW,
|
||||
]
|
||||
@@ -1,549 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useRef, useState } from 'react'
|
||||
import { type MotionValue, motion, useScroll, useTransform } from 'framer-motion'
|
||||
import dynamic from 'next/dynamic'
|
||||
import Link from 'next/link'
|
||||
import { Badge, ChevronDown } from '@/components/emcn'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { TEMPLATE_WORKFLOWS } from '@/app/(home)/components/templates/template-workflows'
|
||||
|
||||
const LandingPreviewWorkflow = dynamic(
|
||||
() =>
|
||||
import(
|
||||
'@/app/(home)/components/landing-preview/components/landing-preview-workflow/landing-preview-workflow'
|
||||
).then((mod) => mod.LandingPreviewWorkflow),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => <div className='h-full w-full bg-[#1b1b1b]' />,
|
||||
}
|
||||
)
|
||||
|
||||
function hexToRgba(hex: string, alpha: number): string {
|
||||
const r = Number.parseInt(hex.slice(1, 3), 16)
|
||||
const g = Number.parseInt(hex.slice(3, 5), 16)
|
||||
const b = Number.parseInt(hex.slice(5, 7), 16)
|
||||
return `rgba(${r},${g},${b},${alpha})`
|
||||
}
|
||||
|
||||
const LEFT_WALL_CLIP = 'polygon(0 8px, 100% 0, 100% 100%, 0 100%)'
|
||||
const BOTTOM_WALL_CLIP = 'polygon(0 0, 100% 0, calc(100% - 8px) 100%, 0 100%)'
|
||||
|
||||
interface DepthConfig {
|
||||
color: string
|
||||
segments: readonly (readonly [opacity: number, width: number])[]
|
||||
}
|
||||
|
||||
/** Depth color and gradient segment pattern per template. Segments are `[opacity, width%]` tuples. */
|
||||
const DEPTH_CONFIGS: Record<string, DepthConfig> = {
|
||||
'tpl-ocr-invoice': {
|
||||
color: '#2ABBF8',
|
||||
segments: [
|
||||
[0.3, 10],
|
||||
[0.5, 8],
|
||||
[0.8, 6],
|
||||
[1, 5],
|
||||
[0.4, 12],
|
||||
[0.7, 8],
|
||||
[1, 6],
|
||||
[0.5, 10],
|
||||
[0.9, 7],
|
||||
[0.6, 12],
|
||||
[1, 8],
|
||||
[0.35, 8],
|
||||
],
|
||||
},
|
||||
'tpl-github-release': {
|
||||
color: '#00F701',
|
||||
segments: [
|
||||
[0.4, 8],
|
||||
[0.7, 6],
|
||||
[1, 5],
|
||||
[0.5, 14],
|
||||
[0.85, 8],
|
||||
[0.3, 12],
|
||||
[1, 6],
|
||||
[0.6, 10],
|
||||
[0.9, 7],
|
||||
[0.45, 8],
|
||||
[1, 8],
|
||||
[0.7, 8],
|
||||
],
|
||||
},
|
||||
'tpl-meeting-followup': {
|
||||
color: '#FFCC02',
|
||||
segments: [
|
||||
[0.5, 12],
|
||||
[0.8, 6],
|
||||
[0.35, 10],
|
||||
[1, 5],
|
||||
[0.6, 8],
|
||||
[0.9, 7],
|
||||
[0.4, 14],
|
||||
[1, 6],
|
||||
[0.7, 10],
|
||||
[0.5, 8],
|
||||
[1, 6],
|
||||
[0.3, 8],
|
||||
],
|
||||
},
|
||||
'tpl-cv-scanner': {
|
||||
color: '#FA4EDF',
|
||||
segments: [
|
||||
[0.35, 6],
|
||||
[0.6, 10],
|
||||
[0.9, 5],
|
||||
[1, 6],
|
||||
[0.4, 8],
|
||||
[0.75, 12],
|
||||
[0.5, 7],
|
||||
[1, 5],
|
||||
[0.3, 10],
|
||||
[0.8, 8],
|
||||
[0.6, 9],
|
||||
[1, 6],
|
||||
[0.45, 8],
|
||||
],
|
||||
},
|
||||
'tpl-email-triage': {
|
||||
color: '#FF6B2C',
|
||||
segments: [
|
||||
[0.4, 10],
|
||||
[0.7, 8],
|
||||
[1, 5],
|
||||
[0.5, 12],
|
||||
[0.85, 6],
|
||||
[0.3, 10],
|
||||
[1, 6],
|
||||
[0.6, 8],
|
||||
[0.9, 7],
|
||||
[0.4, 12],
|
||||
[1, 8],
|
||||
[0.65, 8],
|
||||
],
|
||||
},
|
||||
'tpl-competitor-monitor': {
|
||||
color: '#6366F1',
|
||||
segments: [
|
||||
[0.3, 8],
|
||||
[0.55, 10],
|
||||
[0.8, 6],
|
||||
[1, 5],
|
||||
[0.4, 12],
|
||||
[0.7, 7],
|
||||
[0.9, 8],
|
||||
[0.5, 10],
|
||||
[1, 6],
|
||||
[0.35, 8],
|
||||
[0.75, 6],
|
||||
[1, 6],
|
||||
[0.6, 8],
|
||||
],
|
||||
},
|
||||
'tpl-social-listening': {
|
||||
color: '#F43F5E',
|
||||
segments: [
|
||||
[0.5, 10],
|
||||
[0.8, 6],
|
||||
[0.4, 8],
|
||||
[1, 5],
|
||||
[0.6, 12],
|
||||
[0.35, 8],
|
||||
[0.9, 7],
|
||||
[1, 6],
|
||||
[0.5, 10],
|
||||
[0.75, 8],
|
||||
[0.4, 6],
|
||||
[1, 6],
|
||||
[0.65, 8],
|
||||
],
|
||||
},
|
||||
'tpl-data-enrichment': {
|
||||
color: '#14B8A6',
|
||||
segments: [
|
||||
[0.35, 8],
|
||||
[0.6, 6],
|
||||
[0.9, 5],
|
||||
[0.4, 12],
|
||||
[1, 6],
|
||||
[0.7, 10],
|
||||
[0.5, 7],
|
||||
[0.85, 8],
|
||||
[1, 5],
|
||||
[0.3, 10],
|
||||
[0.65, 8],
|
||||
[1, 7],
|
||||
[0.5, 8],
|
||||
],
|
||||
},
|
||||
'tpl-feedback-digest': {
|
||||
color: '#F59E0B',
|
||||
segments: [
|
||||
[0.4, 10],
|
||||
[0.65, 6],
|
||||
[0.9, 5],
|
||||
[0.5, 12],
|
||||
[1, 6],
|
||||
[0.35, 8],
|
||||
[0.75, 7],
|
||||
[1, 5],
|
||||
[0.6, 10],
|
||||
[0.85, 8],
|
||||
[0.45, 6],
|
||||
[1, 8],
|
||||
[0.55, 9],
|
||||
],
|
||||
},
|
||||
'tpl-pr-review': {
|
||||
color: '#06B6D4',
|
||||
segments: [
|
||||
[0.35, 8],
|
||||
[0.7, 7],
|
||||
[1, 5],
|
||||
[0.45, 10],
|
||||
[0.8, 6],
|
||||
[0.3, 12],
|
||||
[1, 6],
|
||||
[0.55, 8],
|
||||
[0.9, 7],
|
||||
[0.4, 10],
|
||||
[1, 6],
|
||||
[0.65, 8],
|
||||
[0.5, 7],
|
||||
],
|
||||
},
|
||||
'tpl-knowledge-qa': {
|
||||
color: '#84CC16',
|
||||
segments: [
|
||||
[0.5, 8],
|
||||
[0.75, 6],
|
||||
[0.4, 10],
|
||||
[1, 5],
|
||||
[0.6, 8],
|
||||
[0.85, 7],
|
||||
[0.35, 12],
|
||||
[1, 6],
|
||||
[0.7, 8],
|
||||
[0.45, 10],
|
||||
[0.9, 6],
|
||||
[1, 6],
|
||||
[0.55, 8],
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
const SCROLL_BLOCK_RX = '2.59574'
|
||||
|
||||
/**
|
||||
* Two-row horizontal block strip for the scroll-driven reveal in the templates section.
|
||||
* Same structural pattern as the hero's top-right blocks with matching colours:
|
||||
* blue (left) → pink (middle) → green (right).
|
||||
*/
|
||||
const SCROLL_BLOCK_RECTS = [
|
||||
{ opacity: 0.6, x: '-34.24', y: '0', width: '34.24', height: '16.86', fill: '#2ABBF8' },
|
||||
{ opacity: 1, x: '-17.38', y: '0', width: '16.86', height: '16.86', fill: '#2ABBF8' },
|
||||
{ opacity: 1, x: '0', y: '0', width: '16.86', height: '33.73', fill: '#2ABBF8' },
|
||||
{ opacity: 0.6, x: '0', y: '0', width: '85.34', height: '16.86', fill: '#2ABBF8' },
|
||||
{ opacity: 1, x: '0', y: '0', width: '16.86', height: '16.86', fill: '#2ABBF8' },
|
||||
{ opacity: 0.6, x: '34.24', y: '0', width: '34.24', height: '33.73', fill: '#2ABBF8' },
|
||||
{ opacity: 1, x: '34.24', y: '0', width: '16.86', height: '16.86', fill: '#2ABBF8' },
|
||||
{ opacity: 1, x: '51.62', y: '16.86', width: '16.86', height: '16.86', fill: '#2ABBF8' },
|
||||
{ opacity: 1, x: '68.48', y: '0', width: '54.65', height: '16.86', fill: '#FA4EDF' },
|
||||
{ opacity: 0.6, x: '106.27', y: '0', width: '34.24', height: '33.73', fill: '#FA4EDF' },
|
||||
{ opacity: 0.6, x: '106.27', y: '0', width: '51.10', height: '16.86', fill: '#FA4EDF' },
|
||||
{ opacity: 1, x: '123.65', y: '16.86', width: '16.86', height: '16.86', fill: '#FA4EDF' },
|
||||
{ opacity: 0.6, x: '157.37', y: '0', width: '34.24', height: '16.86', fill: '#FA4EDF' },
|
||||
{ opacity: 1, x: '157.37', y: '0', width: '16.86', height: '16.86', fill: '#FA4EDF' },
|
||||
{ opacity: 0.6, x: '209.0', y: '0', width: '68.48', height: '16.86', fill: '#00F701' },
|
||||
{ opacity: 0.6, x: '209.14', y: '0', width: '16.86', height: '33.73', fill: '#00F701' },
|
||||
{ opacity: 0.6, x: '243.23', y: '0', width: '34.24', height: '33.73', fill: '#00F701' },
|
||||
{ opacity: 1, x: '243.23', y: '0', width: '16.86', height: '16.86', fill: '#00F701' },
|
||||
{ opacity: 0.6, x: '260.10', y: '0', width: '34.04', height: '16.86', fill: '#00F701' },
|
||||
{ opacity: 1, x: '260.61', y: '16.86', width: '16.86', height: '16.86', fill: '#00F701' },
|
||||
] as const
|
||||
|
||||
const SCROLL_BLOCK_MAX_X = Math.max(...SCROLL_BLOCK_RECTS.map((r) => Number.parseFloat(r.x)))
|
||||
const SCROLL_REVEAL_START = 0.05
|
||||
const SCROLL_REVEAL_SPAN = 0.7
|
||||
const SCROLL_FADE_IN = 0.03
|
||||
|
||||
function getScrollBlockThreshold(x: string): number {
|
||||
const normalized = Number.parseFloat(x) / SCROLL_BLOCK_MAX_X
|
||||
return SCROLL_REVEAL_START + (1 - normalized) * SCROLL_REVEAL_SPAN
|
||||
}
|
||||
|
||||
interface ScrollBlockRectProps {
|
||||
scrollYProgress: MotionValue<number>
|
||||
rect: (typeof SCROLL_BLOCK_RECTS)[number]
|
||||
}
|
||||
|
||||
/** Renders a single SVG rect whose opacity is driven by scroll progress. */
|
||||
function ScrollBlockRect({ scrollYProgress, rect }: ScrollBlockRectProps) {
|
||||
const threshold = getScrollBlockThreshold(rect.x)
|
||||
const opacity = useTransform(
|
||||
scrollYProgress,
|
||||
[threshold, threshold + SCROLL_FADE_IN],
|
||||
[0, rect.opacity]
|
||||
)
|
||||
|
||||
return (
|
||||
<motion.rect
|
||||
x={rect.x}
|
||||
y={rect.y}
|
||||
width={rect.width}
|
||||
height={rect.height}
|
||||
rx={SCROLL_BLOCK_RX}
|
||||
fill={rect.fill}
|
||||
style={{ opacity }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function buildBottomWallStyle(config: DepthConfig) {
|
||||
let pos = 0
|
||||
const stops: string[] = []
|
||||
for (const [opacity, width] of config.segments) {
|
||||
const c = hexToRgba(config.color, opacity)
|
||||
stops.push(`${c} ${pos}%`, `${c} ${pos + width}%`)
|
||||
pos += width
|
||||
}
|
||||
return {
|
||||
clipPath: BOTTOM_WALL_CLIP,
|
||||
background: `linear-gradient(135deg, ${stops.join(', ')})`,
|
||||
}
|
||||
}
|
||||
|
||||
interface DotGridProps {
|
||||
className?: string
|
||||
cols: number
|
||||
rows: number
|
||||
gap?: number
|
||||
}
|
||||
|
||||
function DotGrid({ className, cols, rows, gap = 0 }: DotGridProps) {
|
||||
return (
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className={className}
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: `repeat(${cols}, 1fr)`,
|
||||
gap,
|
||||
placeItems: 'center',
|
||||
}}
|
||||
>
|
||||
{Array.from({ length: cols * rows }, (_, i) => (
|
||||
<div key={i} className='h-[2px] w-[2px] rounded-full bg-[#2A2A2A]' />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const TEMPLATES_PANEL_ID = 'templates-panel'
|
||||
|
||||
export default function Templates() {
|
||||
const sectionRef = useRef<HTMLDivElement>(null)
|
||||
const [activeIndex, setActiveIndex] = useState(0)
|
||||
|
||||
const { scrollYProgress } = useScroll({
|
||||
target: sectionRef,
|
||||
offset: ['start 0.9', 'start 0.2'],
|
||||
})
|
||||
|
||||
const activeWorkflow = TEMPLATE_WORKFLOWS[activeIndex]
|
||||
const activeDepth = DEPTH_CONFIGS[activeWorkflow.id]
|
||||
|
||||
return (
|
||||
<section
|
||||
ref={sectionRef}
|
||||
id='templates'
|
||||
aria-labelledby='templates-heading'
|
||||
className='mt-[40px] mb-[80px]'
|
||||
>
|
||||
<p className='sr-only'>
|
||||
Sim includes {TEMPLATE_WORKFLOWS.length} pre-built workflow templates covering OCR
|
||||
processing, release management, meeting follow-ups, resume scanning, email triage,
|
||||
competitor monitoring, social listening, data enrichment, feedback analysis, code review,
|
||||
and knowledge base Q&A. Each template connects real integrations and LLMs — pick one,
|
||||
customise it, and deploy in minutes.
|
||||
</p>
|
||||
|
||||
<div className='bg-[#1C1C1C]'>
|
||||
<DotGrid
|
||||
className='border-[#2A2A2A] border-y bg-[#1C1C1C] p-[6px]'
|
||||
cols={120}
|
||||
rows={1}
|
||||
gap={6}
|
||||
/>
|
||||
|
||||
<div className='relative overflow-hidden'>
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='pointer-events-none absolute top-0 right-0 z-20 hidden lg:block'
|
||||
>
|
||||
<svg
|
||||
width={329}
|
||||
height={34}
|
||||
viewBox='-34 0 329 34'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
className='h-auto w-full'
|
||||
>
|
||||
{SCROLL_BLOCK_RECTS.map((r, i) => (
|
||||
<ScrollBlockRect key={i} scrollYProgress={scrollYProgress} rect={r} />
|
||||
))}
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div className='px-[80px] pt-[100px]'>
|
||||
<div className='flex flex-col items-start gap-[20px]'>
|
||||
<Badge
|
||||
variant='blue'
|
||||
size='md'
|
||||
dot
|
||||
className='font-season uppercase tracking-[0.02em] transition-colors duration-200'
|
||||
style={{
|
||||
color: activeDepth.color,
|
||||
backgroundColor: hexToRgba(activeDepth.color, 0.1),
|
||||
}}
|
||||
>
|
||||
Templates
|
||||
</Badge>
|
||||
|
||||
<h2
|
||||
id='templates-heading'
|
||||
className='font-[430] font-season text-[40px] text-white leading-[100%] tracking-[-0.02em]'
|
||||
>
|
||||
Ship your agent in minutes
|
||||
</h2>
|
||||
|
||||
<p className='font-[430] font-season text-[#F6F6F0]/50 text-[16px] leading-[125%] tracking-[0.02em]'>
|
||||
Pre-built templates for every use case—pick one, swap <br />
|
||||
models and tools to fit your stack, and deploy.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mt-[73px] flex border-[#2A2A2A] border-y'>
|
||||
<DotGrid
|
||||
className='w-[80px] shrink-0 overflow-hidden border-[#2A2A2A] border-r p-[6px]'
|
||||
cols={6}
|
||||
rows={55}
|
||||
gap={6}
|
||||
/>
|
||||
|
||||
<div className='flex min-w-0 flex-1'>
|
||||
<div
|
||||
role='tablist'
|
||||
aria-label='Workflow templates'
|
||||
className='flex w-[300px] shrink-0 flex-col border-[#2A2A2A] border-r'
|
||||
>
|
||||
{TEMPLATE_WORKFLOWS.map((workflow, index) => {
|
||||
const isActive = index === activeIndex
|
||||
return (
|
||||
<button
|
||||
key={workflow.id}
|
||||
id={`template-tab-${index}`}
|
||||
type='button'
|
||||
role='tab'
|
||||
aria-selected={isActive}
|
||||
aria-controls={TEMPLATES_PANEL_ID}
|
||||
onClick={() => setActiveIndex(index)}
|
||||
className={cn(
|
||||
'relative text-left',
|
||||
isActive
|
||||
? 'z-10'
|
||||
: 'flex items-center px-[12px] py-[10px] shadow-[inset_0_-1px_0_0_#2A2A2A] last:shadow-none hover:bg-[#232323]/50'
|
||||
)}
|
||||
>
|
||||
{isActive ? (
|
||||
(() => {
|
||||
const depth = DEPTH_CONFIGS[workflow.id]
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className='absolute top-[-8px] bottom-0 left-0 w-2'
|
||||
style={{
|
||||
clipPath: LEFT_WALL_CLIP,
|
||||
backgroundColor: hexToRgba(depth.color, 0.63),
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className='absolute right-[-8px] bottom-0 left-2 h-2'
|
||||
style={buildBottomWallStyle(depth)}
|
||||
/>
|
||||
<div className='-translate-y-2 relative flex translate-x-2 items-center bg-[#242424] px-[12px] py-[10px] shadow-[inset_0_0_0_1.5px_#3E3E3E]'>
|
||||
<span className='flex-1 font-[430] font-season text-[16px] text-white'>
|
||||
{workflow.name}
|
||||
</span>
|
||||
<ChevronDown
|
||||
className='-rotate-90 h-[11px] w-[11px] shrink-0'
|
||||
style={{ color: depth.color }}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
})()
|
||||
) : (
|
||||
<span className='font-[430] font-season text-[#F6F6F0]/50 text-[16px]'>
|
||||
{workflow.name}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div
|
||||
id={TEMPLATES_PANEL_ID}
|
||||
role='tabpanel'
|
||||
aria-labelledby={`template-tab-${activeIndex}`}
|
||||
className='relative hidden flex-1 lg:block'
|
||||
>
|
||||
<div aria-hidden='true' className='h-full'>
|
||||
<LandingPreviewWorkflow
|
||||
key={activeIndex}
|
||||
workflow={activeWorkflow}
|
||||
animate
|
||||
fitViewOptions={{ padding: 0.15, maxZoom: 1.3 }}
|
||||
/>
|
||||
</div>
|
||||
<Link
|
||||
href='/signup'
|
||||
className='group/cta absolute top-[16px] right-[16px] z-10 inline-flex h-[32px] items-center gap-[6px] rounded-[5px] border border-[#33C482] bg-[#33C482] px-[10px] font-[430] font-season text-[14px] text-black transition-[filter] hover:brightness-110'
|
||||
>
|
||||
Use template
|
||||
<span className='relative h-[10px] w-[10px] shrink-0'>
|
||||
<ChevronDown className='-rotate-90 absolute inset-0 h-[10px] w-[10px] transition-opacity duration-150 group-hover/cta:opacity-0' />
|
||||
<svg
|
||||
className='absolute inset-0 h-[10px] w-[10px] opacity-0 transition-opacity duration-150 group-hover/cta:opacity-100'
|
||||
viewBox='0 0 10 10'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<path
|
||||
d='M1 5H8M5.5 2L8.5 5L5.5 8'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.33'
|
||||
strokeLinecap='square'
|
||||
strokeLinejoin='miter'
|
||||
fill='none'
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DotGrid
|
||||
className='w-[80px] shrink-0 overflow-hidden border-[#2A2A2A] border-l p-[6px]'
|
||||
cols={6}
|
||||
rows={55}
|
||||
gap={6}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
/**
|
||||
* Testimonials section — social proof via user quotes.
|
||||
*
|
||||
* SEO:
|
||||
* - `<section id="testimonials" aria-labelledby="testimonials-heading">`.
|
||||
* - `<h2 id="testimonials-heading">` for the section title.
|
||||
* - Each testimonial: `<blockquote cite="tweet-url">` with `<footer><cite>Author</cite></footer>`.
|
||||
* - Profile images use `loading="lazy"` (below the fold).
|
||||
*
|
||||
* GEO:
|
||||
* - Keep quote text as plain text in `<blockquote>` — not split across `<span>` elements.
|
||||
* - Include full author name + handle (LLMs weigh attributed quotes higher).
|
||||
* - Testimonials mentioning "Sim" by name carry more citation weight.
|
||||
* - Review data here aligns with `review` entries in structured-data.tsx.
|
||||
*/
|
||||
export default function Testimonials() {
|
||||
return null
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import { martianMono } from '@/app/_styles/fonts/martian-mono/martian-mono'
|
||||
import { season } from '@/app/_styles/fonts/season/season'
|
||||
import {
|
||||
Collaboration,
|
||||
Enterprise,
|
||||
Features,
|
||||
Hero,
|
||||
Navbar,
|
||||
Pricing,
|
||||
StructuredData,
|
||||
Templates,
|
||||
Testimonials,
|
||||
} from '@/app/(home)/components'
|
||||
import { Footer } from '@/app/(landing)/components'
|
||||
|
||||
/**
|
||||
* Landing page root component.
|
||||
*
|
||||
* ## SEO Architecture
|
||||
* - Single `<h1>` inside Hero (only one per page).
|
||||
* - Heading hierarchy: H1 (Hero) -> H2 (each section) -> H3 (sub-items).
|
||||
* - Semantic landmarks: `<header>`, `<main>`, `<footer>`.
|
||||
* - Every `<section>` has an `id` for anchor linking and `aria-labelledby` for accessibility.
|
||||
* - `StructuredData` emits JSON-LD before any visible content.
|
||||
*
|
||||
* ## GEO Architecture
|
||||
* - Above-fold content (Navbar, Hero) is statically rendered (Server Components where possible)
|
||||
* for immediate availability to AI crawlers.
|
||||
* - Section `id` attributes serve as fragment anchors for precise AI citations.
|
||||
* - Content ordering prioritizes answer-first patterns: definition (Hero) ->
|
||||
* examples (Templates) -> capabilities (Features) -> social proof (Collaboration, Testimonials) ->
|
||||
* pricing (Pricing) -> enterprise (Enterprise).
|
||||
*/
|
||||
export default async function Landing() {
|
||||
return (
|
||||
<div className={`${season.variable} ${martianMono.variable} min-h-screen bg-[#1C1C1C]`}>
|
||||
<StructuredData />
|
||||
<header>
|
||||
<Navbar />
|
||||
</header>
|
||||
<main>
|
||||
<Hero />
|
||||
<Templates />
|
||||
<Features />
|
||||
<Collaboration />
|
||||
<Pricing />
|
||||
<Enterprise />
|
||||
<Testimonials />
|
||||
</main>
|
||||
<Footer fullWidth={true} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { martianMono } from '@/app/_styles/fonts/martian-mono/martian-mono'
|
||||
import { season } from '@/app/_styles/fonts/season/season'
|
||||
|
||||
/**
|
||||
* Landing page route-group layout.
|
||||
*
|
||||
* Applies landing-specific font CSS variables to the subtree:
|
||||
* - `--font-season` (Season Sans): Headings and display text
|
||||
* - `--font-martian-mono` (Martian Mono): Code snippets and technical accents
|
||||
*
|
||||
* Available to child components via Tailwind (`font-season`, `font-martian-mono`).
|
||||
*
|
||||
* SEO metadata for the `/` route is exported from `app/page.tsx` — not here.
|
||||
* This layout only applies when a `page.tsx` exists inside the `(home)/` route group.
|
||||
*/
|
||||
export default function HomeLayout({ children }: { children: React.ReactNode }) {
|
||||
return <div className={`${season.variable} ${martianMono.variable}`}>{children}</div>
|
||||
}
|
||||
@@ -63,9 +63,7 @@ export default function StatusIndicator() {
|
||||
aria-label={`System status: ${message}`}
|
||||
>
|
||||
<StatusDotIcon status={status} className='h-[6px] w-[6px]' aria-hidden='true' />
|
||||
<span className='font-[family-name:var(--font-martian-mono)] font-medium uppercase tracking-[-0.24px]'>
|
||||
{message}
|
||||
</span>
|
||||
<span>{message}</span>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,231 +1,279 @@
|
||||
import Link from 'next/link'
|
||||
import { martianMono } from '@/app/_styles/fonts/martian-mono/martian-mono'
|
||||
import { season } from '@/app/_styles/fonts/season/season'
|
||||
import { SocialLinks, StatusIndicator } from '@/app/(landing)/components/footer/components'
|
||||
import { inter } from '@/app/_styles/fonts/inter/inter'
|
||||
import {
|
||||
ComplianceBadges,
|
||||
Logo,
|
||||
SocialLinks,
|
||||
StatusIndicator,
|
||||
} from '@/app/(landing)/components/footer/components'
|
||||
import { FOOTER_BLOCKS, FOOTER_TOOLS } from '@/app/(landing)/components/footer/consts'
|
||||
|
||||
const VISIBLE_COUNT = 9 as const
|
||||
const DOT_GRID_ROWS = 4 as const
|
||||
const DOT_GRID_GAP = 8 as const
|
||||
|
||||
const LINK_CLASS =
|
||||
'font-[family-name:var(--font-martian-mono)] text-[12px] font-medium uppercase tracking-[-0.24px] text-[#f6f6f0]/60 transition-colors hover:text-white' as const
|
||||
|
||||
interface FooterProps {
|
||||
fullWidth?: boolean
|
||||
}
|
||||
|
||||
export default function Footer({ fullWidth = false }: FooterProps) {
|
||||
return (
|
||||
<footer
|
||||
className={`${martianMono.variable} ${season.variable} relative w-full overflow-hidden bg-[#1C1C1C]`}
|
||||
>
|
||||
{/* Dot grid separator */}
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='border-[#2A2A2A] border-y bg-[#1C1C1C] p-[6px]'
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(120, 1fr)',
|
||||
gap: 6,
|
||||
placeItems: 'center',
|
||||
}}
|
||||
>
|
||||
{Array.from({ length: 120 * DOT_GRID_ROWS }, (_, i) => (
|
||||
<div key={i} className='h-[2px] w-[2px] rounded-full bg-[#2A2A2A]' />
|
||||
))}
|
||||
</div>
|
||||
<footer className={`${inter.className} relative w-full overflow-hidden bg-white`}>
|
||||
<div
|
||||
className={
|
||||
fullWidth
|
||||
? 'mx-auto max-w-[1440px] px-10 py-[48px] sm:px-[120px] sm:py-[56px]'
|
||||
: 'px-10 py-[48px] sm:px-[120px] sm:py-[56px]'
|
||||
? 'px-4 pt-[40px] pb-[40px] sm:px-4 sm:pt-[34px] sm:pb-[340px]'
|
||||
: 'px-4 pt-[40px] pb-[40px] sm:px-[50px] sm:pt-[34px] sm:pb-[340px]'
|
||||
}
|
||||
>
|
||||
<div className='flex flex-col gap-[48px]'>
|
||||
{/* Main content row */}
|
||||
<div className='flex flex-col gap-[48px] sm:flex-row sm:justify-between'>
|
||||
{/* Logo and status — left aligned */}
|
||||
<div className='flex flex-col gap-[24px]'>
|
||||
<Link href='/' aria-label='Sim home'>
|
||||
<svg
|
||||
width='71'
|
||||
height='22'
|
||||
viewBox='0 0 71 22'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<g transform='scale(0.07483)'>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
d='M142.793 124.175C142.793 128.925 140.913 133.487 137.577 136.846L137.099 137.327C133.765 140.696 129.236 142.579 124.519 142.579H17.8063C7.97854 142.579 0 150.605 0 160.503V275.91C0 285.808 7.97854 293.834 17.8063 293.834H132.383C142.211 293.834 150.179 285.808 150.179 275.91V167.858C150.179 163.453 151.914 159.226 155.009 156.109C158.095 153.001 162.292 151.253 166.666 151.253H275.166C284.994 151.253 292.962 143.229 292.962 133.33V17.9231C292.962 8.02512 284.994 0 275.166 0H160.588C150.761 0 142.793 8.02512 142.793 17.9231V124.175ZM177.564 24.5671H258.181C263.925 24.5671 268.57 29.2545 268.57 35.0301V116.224C268.57 121.998 263.925 126.687 258.181 126.687H177.564C171.83 126.687 167.175 121.998 167.175 116.224V35.0301C167.175 29.2545 171.83 24.5671 177.564 24.5671Z'
|
||||
fill='white'
|
||||
/>
|
||||
<path
|
||||
d='M275.293 171.578H190.106C179.779 171.578 171.406 180.01 171.406 190.412V275.162C171.406 285.564 179.779 293.996 190.106 293.996H275.293C285.621 293.996 293.994 285.564 293.994 275.162V190.412C293.994 180.01 285.621 171.578 275.293 171.578Z'
|
||||
fill='white'
|
||||
/>
|
||||
<path
|
||||
d='M275.293 171.18H190.106C179.779 171.18 171.406 179.612 171.406 190.014V274.763C171.406 285.165 179.779 293.596 190.106 293.596H275.293C285.621 293.596 293.994 285.165 293.994 274.763V190.014C293.994 179.612 285.621 171.18 275.293 171.18Z'
|
||||
fill='white'
|
||||
fillOpacity='0.2'
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d='M31.5718 15.845H34.1583C34.1583 16.5591 34.4169 17.1285 34.9342 17.5531C35.4515 17.9584 36.1508 18.1611 37.0321 18.1611C37.9901 18.1611 38.7277 17.9777 39.245 17.611C39.7623 17.225 40.021 16.7135 40.021 16.0766C40.021 15.6134 39.8773 15.2274 39.5899 14.9186C39.3217 14.6098 38.8235 14.3589 38.0955 14.1659L35.6239 13.5869C34.3786 13.2781 33.4494 12.8052 32.8363 12.1683C32.2423 11.5314 31.9454 10.6918 31.9454 9.64957C31.9454 8.78105 32.1657 8.02833 32.6064 7.39142C33.0662 6.7545 33.6889 6.26234 34.4744 5.91494C35.2791 5.56753 36.1987 5.39382 37.2333 5.39382C38.2679 5.39382 39.1588 5.57718 39.906 5.94389C40.6724 6.31059 41.2663 6.82206 41.6878 7.47827C42.1285 8.13449 42.3584 8.91615 42.3776 9.82327H39.7911C39.7719 9.08986 39.5324 8.52049 39.0726 8.11518C38.6128 7.70988 37.9709 7.50722 37.1471 7.50722C36.3041 7.50722 35.6527 7.69058 35.1929 8.05728C34.733 8.42399 34.5031 8.9258 34.5031 9.56272C34.5031 10.5084 35.1929 11.155 36.5723 11.5024L39.0439 12.1104C40.2317 12.3806 41.1226 12.8245 41.7166 13.4421C42.3105 14.0404 42.6075 14.8607 42.6075 15.9029C42.6075 16.7907 42.368 17.5724 41.889 18.2479C41.41 18.9041 40.749 19.4156 39.906 19.7823C39.0822 20.1297 38.1051 20.3034 36.9747 20.3034C35.327 20.3034 34.0146 19.8981 33.0375 19.0875C32.0603 18.2769 31.5718 17.196 31.5718 15.845Z'
|
||||
fill='white'
|
||||
/>
|
||||
<path
|
||||
d='M44.5096 19.956V5.79913C45.5868 6.19296 46.0617 6.19296 47.211 5.79913V19.956H44.5096ZM45.8316 4.86332C45.3526 4.86332 44.9311 4.68962 44.5671 4.34221C44.2222 3.9755 44.0498 3.55089 44.0498 3.06838C44.0498 2.56657 44.2222 2.14196 44.5671 1.79455C44.9311 1.44714 45.3526 1.27344 45.8316 1.27344C46.3297 1.27344 46.7512 1.44714 47.0961 1.79455C47.441 2.14196 47.6134 2.56657 47.6134 3.06838C47.6134 3.55089 47.441 3.9755 47.0961 4.34221C46.7512 4.68962 46.3297 4.86332 45.8316 4.86332Z'
|
||||
fill='white'
|
||||
/>
|
||||
<path
|
||||
d='M51.976 19.956H49.2746V5.79913H51.6887V8.18778C51.976 7.39647 52.5317 6.72555 53.298 6.20444C54.0835 5.66403 55.0319 5.39382 56.1432 5.39382C57.3885 5.39382 58.4231 5.73158 59.247 6.4071C60.0708 7.08261 60.6073 7.98008 60.8563 9.09951H60.3678C60.5594 7.98008 61.0862 7.08261 61.9484 6.4071C62.8106 5.73158 63.8739 5.39382 65.1384 5.39382C66.7478 5.39382 68.0123 5.86668 68.9319 6.8124C69.8516 7.75813 70.3114 9.05126 70.3114 10.6918V19.956H67.6674V11.3577C67.6674 10.2382 67.38 9.37936 66.8053 8.78105C66.2496 8.16344 65.4928 7.85463 64.5349 7.85463C63.8643 7.85463 63.2704 8.00903 62.7531 8.31784C62.2549 8.60735 61.8622 9.03196 61.5748 9.59167C61.2874 10.1514 61.1437 10.8076 61.1437 11.5603V19.956H58.471V11.3287C58.471 10.2093 58.1932 9.36006 57.6376 8.78105C57.082 8.18274 56.3252 7.88358 55.3672 7.88358C54.6966 7.88358 54.1027 8.03798 53.5854 8.34679C53.0873 8.6363 52.6945 9.06091 52.4071 9.62062C52.1197 10.161 51.976 10.8076 51.976 11.5603V19.956Z'
|
||||
fill='white'
|
||||
/>
|
||||
</svg>
|
||||
<div className={`flex gap-[80px] ${fullWidth ? 'justify-center' : ''}`}>
|
||||
{/* Logo and social links */}
|
||||
<div className='flex flex-col gap-[24px]'>
|
||||
<Logo />
|
||||
<SocialLinks />
|
||||
<ComplianceBadges />
|
||||
<StatusIndicator />
|
||||
</div>
|
||||
|
||||
{/* Links section */}
|
||||
<div>
|
||||
<h2 className='mb-[16px] font-medium text-[14px] text-foreground'>More Sim</h2>
|
||||
<div className='flex flex-col gap-[12px]'>
|
||||
<Link
|
||||
href='https://docs.sim.ai'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-[14px] text-muted-foreground transition-colors hover:text-foreground'
|
||||
>
|
||||
Docs
|
||||
</Link>
|
||||
<Link
|
||||
href='#pricing'
|
||||
className='text-[14px] text-muted-foreground transition-colors hover:text-foreground'
|
||||
>
|
||||
Pricing
|
||||
</Link>
|
||||
<Link
|
||||
href='https://form.typeform.com/to/jqCO12pF'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-[14px] text-muted-foreground transition-colors hover:text-foreground'
|
||||
>
|
||||
Enterprise
|
||||
</Link>
|
||||
<Link
|
||||
href='/studio'
|
||||
className='text-[14px] text-muted-foreground transition-colors hover:text-foreground'
|
||||
>
|
||||
Sim Studio
|
||||
</Link>
|
||||
<Link
|
||||
href='/changelog'
|
||||
className='text-[14px] text-muted-foreground transition-colors hover:text-foreground'
|
||||
>
|
||||
Changelog
|
||||
</Link>
|
||||
<Link
|
||||
href='https://status.sim.ai'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-[14px] text-muted-foreground transition-colors hover:text-foreground'
|
||||
>
|
||||
Status
|
||||
</Link>
|
||||
<Link
|
||||
href='/careers'
|
||||
className='text-[14px] text-muted-foreground transition-colors hover:text-foreground'
|
||||
>
|
||||
Careers
|
||||
</Link>
|
||||
<Link
|
||||
href='/privacy'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-[14px] text-muted-foreground transition-colors hover:text-foreground'
|
||||
>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
<Link
|
||||
href='/terms'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-[14px] text-muted-foreground transition-colors hover:text-foreground'
|
||||
>
|
||||
Terms of Service
|
||||
</Link>
|
||||
<div className='[&_a:hover]:text-white [&_a]:text-[#808080]'>
|
||||
<StatusIndicator />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Link columns — right aligned */}
|
||||
<div className='flex flex-col gap-[48px] sm:flex-row sm:gap-[80px]'>
|
||||
{/* Company links */}
|
||||
<div>
|
||||
<h2 className='mb-[24px] font-[family-name:var(--font-season)] font-medium text-[20px] text-white tracking-[-0.4px]'>
|
||||
Company
|
||||
</h2>
|
||||
<div className='flex flex-col gap-[10px]'>
|
||||
<Link
|
||||
href='https://docs.sim.ai'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className={LINK_CLASS}
|
||||
>
|
||||
Docs
|
||||
</Link>
|
||||
<Link href='#pricing' className={LINK_CLASS}>
|
||||
Pricing
|
||||
</Link>
|
||||
<Link
|
||||
href='https://form.typeform.com/to/jqCO12pF'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className={LINK_CLASS}
|
||||
>
|
||||
Enterprise
|
||||
</Link>
|
||||
<Link href='/studio' className={LINK_CLASS}>
|
||||
Sim Studio
|
||||
</Link>
|
||||
<Link href='/changelog' className={LINK_CLASS}>
|
||||
Changelog
|
||||
</Link>
|
||||
<Link
|
||||
href='https://status.sim.ai'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className={LINK_CLASS}
|
||||
>
|
||||
Status
|
||||
</Link>
|
||||
<Link href='/careers' className={LINK_CLASS}>
|
||||
Careers
|
||||
</Link>
|
||||
<Link
|
||||
href='/privacy'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className={LINK_CLASS}
|
||||
>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
<Link
|
||||
href='/terms'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className={LINK_CLASS}
|
||||
>
|
||||
Terms of Service
|
||||
</Link>
|
||||
<Link
|
||||
href='https://trust.delve.co/sim-studio'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className={LINK_CLASS}
|
||||
>
|
||||
Trust Center
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Blocks section */}
|
||||
<div className='hidden sm:block'>
|
||||
<h2 className='mb-[24px] font-[family-name:var(--font-season)] font-medium text-[20px] text-white tracking-[-0.4px]'>
|
||||
Blocks
|
||||
</h2>
|
||||
<div className='flex flex-col gap-[10px]'>
|
||||
{FOOTER_BLOCKS.slice(0, VISIBLE_COUNT).map((block) => (
|
||||
<Link
|
||||
key={block}
|
||||
href={`https://docs.sim.ai/blocks/${block.toLowerCase().replaceAll(' ', '-')}`}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className={LINK_CLASS}
|
||||
>
|
||||
{block}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<Link
|
||||
href='https://docs.sim.ai/blocks'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='mt-[24px] inline-block font-[family-name:var(--font-season)] font-medium text-[14px] text-white tracking-[-0.28px] transition-opacity hover:opacity-80'
|
||||
>
|
||||
View all Blocks →
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Tools section */}
|
||||
<div className='hidden sm:block'>
|
||||
<h2 className='mb-[24px] font-[family-name:var(--font-season)] font-medium text-[20px] text-white tracking-[-0.4px]'>
|
||||
Tools
|
||||
</h2>
|
||||
<div className='flex flex-col gap-[10px]'>
|
||||
{FOOTER_TOOLS.slice(0, VISIBLE_COUNT).map((tool) => (
|
||||
<Link
|
||||
key={tool}
|
||||
href={`https://docs.sim.ai/tools/${tool.toLowerCase().replace(/\s+/g, '_')}`}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className={`whitespace-nowrap ${LINK_CLASS}`}
|
||||
>
|
||||
{tool}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<Link
|
||||
href='https://docs.sim.ai/tools'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='mt-[24px] inline-block font-[family-name:var(--font-season)] font-medium text-[14px] text-white tracking-[-0.28px] transition-opacity hover:opacity-80'
|
||||
>
|
||||
View all Tools →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Social links — bottom */}
|
||||
<div className='[&_a:hover]:text-white [&_a]:text-[#808080]'>
|
||||
<SocialLinks />
|
||||
{/* Blocks section */}
|
||||
<div className='hidden sm:block'>
|
||||
<h2 className='mb-[16px] font-medium text-[14px] text-foreground'>Blocks</h2>
|
||||
<div className='flex flex-col gap-[12px]'>
|
||||
{FOOTER_BLOCKS.map((block) => (
|
||||
<Link
|
||||
key={block}
|
||||
href={`https://docs.sim.ai/blocks/${block.toLowerCase().replaceAll(' ', '-')}`}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-[14px] text-muted-foreground transition-colors hover:text-foreground'
|
||||
>
|
||||
{block}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tools section - split into columns */}
|
||||
<div className='hidden sm:block'>
|
||||
<h2 className='mb-[16px] font-medium text-[14px] text-foreground'>Tools</h2>
|
||||
<div className='flex gap-[80px]'>
|
||||
{/* First column */}
|
||||
<div className='flex flex-col gap-[12px]'>
|
||||
{FOOTER_TOOLS.slice(0, Math.ceil(FOOTER_TOOLS.length / 4)).map((tool) => (
|
||||
<Link
|
||||
key={tool}
|
||||
href={`https://docs.sim.ai/tools/${tool.toLowerCase().replace(/\s+/g, '_')}`}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='whitespace-nowrap text-[14px] text-muted-foreground transition-colors hover:text-foreground'
|
||||
>
|
||||
{tool}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
{/* Second column */}
|
||||
<div className='flex flex-col gap-[12px]'>
|
||||
{FOOTER_TOOLS.slice(
|
||||
Math.ceil(FOOTER_TOOLS.length / 4),
|
||||
Math.ceil((FOOTER_TOOLS.length * 2) / 4)
|
||||
).map((tool) => (
|
||||
<Link
|
||||
key={tool}
|
||||
href={`https://docs.sim.ai/tools/${tool.toLowerCase().replace(/\s+/g, '_')}`}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='whitespace-nowrap text-[14px] text-muted-foreground transition-colors hover:text-foreground'
|
||||
>
|
||||
{tool}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
{/* Third column */}
|
||||
<div className='flex flex-col gap-[12px]'>
|
||||
{FOOTER_TOOLS.slice(
|
||||
Math.ceil((FOOTER_TOOLS.length * 2) / 4),
|
||||
Math.ceil((FOOTER_TOOLS.length * 3) / 4)
|
||||
).map((tool) => (
|
||||
<Link
|
||||
key={tool}
|
||||
href={`https://docs.sim.ai/tools/${tool.toLowerCase().replace(/\s+/g, '_')}`}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='whitespace-nowrap text-[14px] text-muted-foreground transition-colors hover:text-foreground'
|
||||
>
|
||||
{tool}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
{/* Fourth column */}
|
||||
<div className='flex flex-col gap-[12px]'>
|
||||
{FOOTER_TOOLS.slice(Math.ceil((FOOTER_TOOLS.length * 3) / 4)).map((tool) => (
|
||||
<Link
|
||||
key={tool}
|
||||
href={`https://docs.sim.ai/tools/${tool.toLowerCase().replace(/\s+/g, '_')}`}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='whitespace-nowrap text-[14px] text-muted-foreground transition-colors hover:text-foreground'
|
||||
>
|
||||
{tool}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Large SIM logo at bottom - half cut off */}
|
||||
<div className='-translate-x-1/2 pointer-events-none absolute bottom-[-240px] left-1/2 hidden sm:block'>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width='1128'
|
||||
height='550'
|
||||
viewBox='0 0 1128 550'
|
||||
fill='none'
|
||||
>
|
||||
<g filter='url(#filter0_dd_122_4989)'>
|
||||
<path
|
||||
d='M3 420.942H77.9115C77.9115 441.473 85.4027 457.843 100.385 470.051C115.367 481.704 135.621 487.53 161.147 487.53C188.892 487.53 210.255 482.258 225.238 471.715C240.22 460.617 247.711 445.913 247.711 427.601C247.711 414.283 243.549 403.185 235.226 394.307C227.457 385.428 213.03 378.215 191.943 372.666L120.361 356.019C84.2929 347.14 57.3802 333.545 39.6234 315.234C22.4215 296.922 13.8206 272.784 13.8206 242.819C13.8206 217.849 20.2019 196.208 32.9646 177.896C46.2822 159.584 64.3165 145.434 87.0674 135.446C110.373 125.458 137.008 120.464 166.973 120.464C196.938 120.464 222.74 125.735 244.382 136.278C266.578 146.821 283.779 161.526 295.987 180.393C308.75 199.259 315.409 221.733 315.964 247.813H241.052C240.497 226.727 233.561 210.357 220.243 198.705C206.926 187.052 188.337 181.225 164.476 181.225C140.06 181.225 121.194 186.497 107.876 197.04C94.5585 207.583 87.8997 222.01 87.8997 240.322C87.8997 267.512 107.876 286.101 147.829 296.09L219.411 313.569C253.815 321.337 279.618 334.1 296.82 351.857C314.022 369.059 322.622 392.642 322.622 422.607C322.622 448.132 315.686 470.606 301.814 490.027C287.941 508.894 268.797 523.599 244.382 534.142C220.521 544.13 192.221 549.124 159.482 549.124C111.76 549.124 73.7498 537.471 45.4499 514.165C17.15 490.86 3 459.785 3 420.942Z'
|
||||
fill='#DCDCDC'
|
||||
/>
|
||||
<path
|
||||
d='M377.713 539.136V132.117C408.911 143.439 422.667 143.439 455.954 132.117V539.136H377.713ZM416.001 105.211C402.129 105.211 389.921 100.217 379.378 90.2291C369.39 79.686 364.395 67.4782 364.395 53.6057C364.395 39.1783 369.39 26.9705 379.378 16.9823C389.921 6.9941 402.129 2 416.001 2C430.428 2 442.636 6.9941 452.625 16.9823C462.613 26.9705 467.607 39.1783 467.607 53.6057C467.607 67.4782 462.613 79.686 452.625 90.2291C442.636 100.217 430.428 105.211 416.001 105.211Z'
|
||||
fill='#DCDCDC'
|
||||
/>
|
||||
<path
|
||||
d='M593.961 539.136H515.72V132.117H585.637V200.792C593.961 178.041 610.053 158.752 632.249 143.769C655 128.232 682.467 120.464 714.651 120.464C750.72 120.464 780.685 130.174 804.545 149.596C822.01 163.812 835.016 181.446 843.562 202.5C851.434 181.446 864.509 163.812 882.786 149.596C907.757 130.174 938.554 120.464 975.177 120.464C1021.79 120.464 1058.41 134.059 1085.05 161.249C1111.68 188.439 1125 225.617 1125 272.784V539.136H1048.42V291.928C1048.42 259.744 1040.1 235.051 1023.45 217.849C1007.36 200.092 985.443 191.213 957.698 191.213C938.276 191.213 921.074 195.653 906.092 204.531C891.665 212.855 880.289 225.062 871.966 241.154C863.642 257.247 859.48 276.113 859.48 297.754V539.136H782.072V291.095C782.072 258.911 774.026 234.496 757.934 217.849C741.841 200.647 719.923 192.046 692.178 192.046C672.756 192.046 655.555 196.485 640.572 205.363C626.145 213.687 614.769 225.895 606.446 241.987C598.122 257.524 593.961 276.113 593.961 297.754V539.136Z'
|
||||
fill='#DCDCDC'
|
||||
/>
|
||||
<path
|
||||
d='M166.973 121.105C196.396 121.105 221.761 126.201 243.088 136.367L244.101 136.855L244.106 136.858C265.86 147.191 282.776 161.528 294.876 179.865L295.448 180.741L295.455 180.753C308.032 199.345 314.656 221.475 315.306 247.171H241.675C240.996 226.243 234.012 209.899 220.666 198.222C207.196 186.435 188.437 180.583 164.476 180.583C139.977 180.583 120.949 185.871 107.478 196.536C93.9928 207.212 87.2578 221.832 87.2578 240.322C87.2579 254.096 92.3262 265.711 102.444 275.127C112.542 284.524 127.641 291.704 147.673 296.712L147.677 296.713L219.259 314.192L219.27 314.195C253.065 321.827 278.469 334.271 295.552 351.48L296.358 352.304L296.365 352.311C313.42 369.365 321.98 392.77 321.98 422.606C321.98 448.005 315.082 470.343 301.297 489.646C287.502 508.408 268.456 523.046 244.134 533.55C220.369 543.498 192.157 548.482 159.481 548.482C111.864 548.482 74.0124 536.855 45.8584 513.67C17.8723 490.623 3.80059 459.948 3.64551 421.584H77.2734C77.4285 441.995 84.9939 458.338 99.9795 470.549L99.9854 470.553L99.9912 470.558C115.12 482.324 135.527 488.172 161.146 488.172C188.96 488.172 210.474 482.889 225.607 472.24L225.613 472.236L225.619 472.231C240.761 461.015 248.353 446.12 248.353 427.601C248.352 414.145 244.145 402.89 235.709 393.884C227.81 384.857 213.226 377.603 192.106 372.045L192.098 372.043L192.089 372.04L120.507 355.394C84.5136 346.533 57.7326 332.983 40.0908 314.794H40.0918C23.0227 296.624 14.4629 272.654 14.4629 242.819C14.4629 217.969 20.8095 196.463 33.4834 178.273C46.7277 160.063 64.6681 145.981 87.3252 136.034L87.3242 136.033C110.536 126.086 137.081 121.106 166.973 121.105ZM975.177 121.105C1021.66 121.105 1058.1 134.658 1084.59 161.698C1111.08 188.741 1124.36 225.743 1124.36 272.784V538.494H1049.07V291.928C1049.07 259.636 1040.71 234.76 1023.92 217.402H1023.91C1007.68 199.5 985.584 190.571 957.697 190.571C938.177 190.571 920.862 195.034 905.771 203.975C891.228 212.365 879.77 224.668 871.396 240.859C863.017 257.059 858.838 276.03 858.838 297.754V538.494H782.714V291.096C782.714 258.811 774.641 234.209 758.395 217.402C742.16 200.053 720.062 191.404 692.178 191.404C673.265 191.404 656.422 195.592 641.666 203.985L640.251 204.808C625.711 213.196 614.254 225.497 605.88 241.684C597.496 257.333 593.318 276.031 593.318 297.754V538.494H516.361V132.759H584.995V200.792L586.24 201.013C594.51 178.408 610.505 159.221 632.607 144.302L632.61 144.3C655.238 128.847 682.574 121.105 714.651 121.105C750.599 121.105 780.413 130.781 804.14 150.094C821.52 164.241 834.461 181.787 842.967 202.741L843.587 204.268L844.163 202.725C851.992 181.786 864.994 164.248 883.181 150.103C908.021 130.782 938.673 121.106 975.177 121.105ZM455.312 538.494H378.354V133.027C393.534 138.491 404.652 141.251 416.05 141.251C427.46 141.251 439.095 138.485 455.312 133.009V538.494ZM416.001 2.6416C430.262 2.6416 442.306 7.57157 452.171 17.4365C462.036 27.3014 466.965 39.3445 466.965 53.6055C466.965 67.3043 462.04 79.3548 452.16 89.7842C442.297 99.6427 430.258 104.569 416.001 104.569C402.303 104.569 390.254 99.6452 379.825 89.7676C369.957 79.3421 365.037 67.2967 365.037 53.6055C365.037 39.3444 369.966 27.3005 379.831 17.4355C390.258 7.56247 402.307 2.64163 416.001 2.6416Z'
|
||||
stroke='#C1C1C1'
|
||||
strokeWidth='1.28396'
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter
|
||||
id='filter0_dd_122_4989'
|
||||
x='0'
|
||||
y='0'
|
||||
width='1128'
|
||||
height='550'
|
||||
filterUnits='userSpaceOnUse'
|
||||
colorInterpolationFilters='sRGB'
|
||||
>
|
||||
<feFlood floodOpacity='0' result='BackgroundImageFix' />
|
||||
<feColorMatrix
|
||||
in='SourceAlpha'
|
||||
type='matrix'
|
||||
values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0'
|
||||
result='hardAlpha'
|
||||
/>
|
||||
<feMorphology
|
||||
radius='1'
|
||||
operator='erode'
|
||||
in='SourceAlpha'
|
||||
result='effect1_dropShadow_122_4989'
|
||||
/>
|
||||
<feOffset dy='1' />
|
||||
<feGaussianBlur stdDeviation='1' />
|
||||
<feColorMatrix type='matrix' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0' />
|
||||
<feBlend
|
||||
mode='normal'
|
||||
in2='BackgroundImageFix'
|
||||
result='effect1_dropShadow_122_4989'
|
||||
/>
|
||||
<feColorMatrix
|
||||
in='SourceAlpha'
|
||||
type='matrix'
|
||||
values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0'
|
||||
result='hardAlpha'
|
||||
/>
|
||||
<feOffset dy='1' />
|
||||
<feGaussianBlur stdDeviation='1.5' />
|
||||
<feColorMatrix type='matrix' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0' />
|
||||
<feBlend
|
||||
mode='normal'
|
||||
in2='effect1_dropShadow_122_4989'
|
||||
result='effect2_dropShadow_122_4989'
|
||||
/>
|
||||
<feBlend
|
||||
mode='normal'
|
||||
in='SourceGraphic'
|
||||
in2='effect2_dropShadow_122_4989'
|
||||
result='shape'
|
||||
/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ export default function StructuredData() {
|
||||
name: 'Sim',
|
||||
alternateName: 'Sim',
|
||||
description:
|
||||
'Sim is the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows.',
|
||||
'Open-source AI agent workflow builder used by developers at trail-blazing startups to Fortune 500 companies',
|
||||
url: 'https://sim.ai',
|
||||
logo: {
|
||||
'@type': 'ImageObject',
|
||||
@@ -36,9 +36,9 @@ export default function StructuredData() {
|
||||
'@type': 'WebSite',
|
||||
'@id': 'https://sim.ai/#website',
|
||||
url: 'https://sim.ai',
|
||||
name: 'Sim — Build AI Agents & Run Your Agentic Workforce',
|
||||
name: 'Sim - AI Agent Workflow Builder',
|
||||
description:
|
||||
'Sim is the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows. Join 100,000+ builders.',
|
||||
'Open-source AI agent workflow builder. 70,000+ developers build and deploy agentic workflows. SOC2 and HIPAA compliant.',
|
||||
publisher: {
|
||||
'@id': 'https://sim.ai/#organization',
|
||||
},
|
||||
@@ -48,7 +48,7 @@ export default function StructuredData() {
|
||||
'@type': 'WebPage',
|
||||
'@id': 'https://sim.ai/#webpage',
|
||||
url: 'https://sim.ai',
|
||||
name: 'Sim — Build AI Agents & Run Your Agentic Workforce',
|
||||
name: 'Sim - Workflows for LLMs | Build AI Agent Workflows',
|
||||
isPartOf: {
|
||||
'@id': 'https://sim.ai/#website',
|
||||
},
|
||||
@@ -58,7 +58,7 @@ export default function StructuredData() {
|
||||
datePublished: '2024-01-01T00:00:00+00:00',
|
||||
dateModified: new Date().toISOString(),
|
||||
description:
|
||||
'Sim is the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows. Create agents, workflows, knowledge bases, tables, and docs.',
|
||||
'Build and deploy AI agent workflows with Sim. Visual drag-and-drop interface for creating powerful LLM-powered automations.',
|
||||
breadcrumb: {
|
||||
'@id': 'https://sim.ai/#breadcrumb',
|
||||
},
|
||||
@@ -85,9 +85,9 @@ export default function StructuredData() {
|
||||
{
|
||||
'@type': 'SoftwareApplication',
|
||||
'@id': 'https://sim.ai/#software',
|
||||
name: 'Sim — Build AI Agents & Run Your Agentic Workforce',
|
||||
name: 'Sim - AI Agent Workflow Builder',
|
||||
description:
|
||||
'Sim is the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows. Create agents, workflows, knowledge bases, tables, and docs. Trusted by over 100,000 builders. SOC2 and HIPAA compliant.',
|
||||
'Open-source AI agent workflow builder used by 70,000+ developers. Build agentic workflows with visual drag-and-drop interface. SOC2 and HIPAA compliant. Integrate with 100+ apps.',
|
||||
applicationCategory: 'DeveloperApplication',
|
||||
applicationSubCategory: 'AI Development Tools',
|
||||
operatingSystem: 'Web, Windows, macOS, Linux',
|
||||
@@ -159,13 +159,12 @@ export default function StructuredData() {
|
||||
worstRating: '1',
|
||||
},
|
||||
featureList: [
|
||||
'AI agent creation',
|
||||
'Agentic workflow orchestration',
|
||||
'1,000+ integrations',
|
||||
'LLM orchestration (OpenAI, Anthropic, Google, xAI, Mistral, Perplexity)',
|
||||
'Knowledge base creation',
|
||||
'Table creation',
|
||||
'Document creation',
|
||||
'Visual workflow builder',
|
||||
'Drag-and-drop interface',
|
||||
'100+ integrations',
|
||||
'AI model support (OpenAI, Anthropic, Google, xAI, Mistral, Perplexity)',
|
||||
'Real-time collaboration',
|
||||
'Version control',
|
||||
'API access',
|
||||
'Custom functions',
|
||||
'Scheduled workflows',
|
||||
@@ -175,7 +174,7 @@ export default function StructuredData() {
|
||||
{
|
||||
'@type': 'ImageObject',
|
||||
url: 'https://sim.ai/logo/426-240/primary/small.png',
|
||||
caption: 'Sim — build AI agents and run your agentic workforce',
|
||||
caption: 'Sim AI agent workflow builder interface',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -188,7 +187,7 @@ export default function StructuredData() {
|
||||
name: 'What is Sim?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'Sim is the open-source platform to build AI agents and run your agentic workforce. Teams connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows. Create agents, workflows, knowledge bases, tables, and docs. Trusted by over 100,000 builders. SOC2 and HIPAA compliant.',
|
||||
text: 'Sim is an open-source AI agent workflow builder used by 70,000+ developers at trail-blazing startups to Fortune 500 companies. It provides a visual drag-and-drop interface for building and deploying agentic workflows. Sim is SOC2 and HIPAA compliant.',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -204,7 +203,7 @@ export default function StructuredData() {
|
||||
name: 'Do I need coding skills to use Sim?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'No coding skills are required. Sim provides a visual interface for building AI agents and agentic workflows. Developers can also use custom functions, the API, and the CLI/SDK for advanced use cases.',
|
||||
text: 'No coding skills are required! Sim features a visual drag-and-drop interface that makes it easy to build AI workflows. However, developers can also use custom functions and our API for advanced use cases.',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -10,7 +10,7 @@ export function BackLink() {
|
||||
return (
|
||||
<Link
|
||||
href='/studio'
|
||||
className='group flex items-center gap-1 font-[430] font-season text-[#F6F6F0]/50 text-sm hover:text-[#F6F6F0]/80'
|
||||
className='group flex items-center gap-1 text-gray-600 text-sm hover:text-gray-900'
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
@@ -21,7 +21,7 @@ export function BackLink() {
|
||||
<ChevronLeft className='h-4 w-4' aria-hidden='true' />
|
||||
)}
|
||||
</span>
|
||||
All posts
|
||||
Back to Sim Studio
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { FAQ } from '@/lib/blog/faq'
|
||||
import { getAllPostMeta, getPostBySlug, getRelatedPosts } from '@/lib/blog/registry'
|
||||
import { buildArticleJsonLd, buildBreadcrumbJsonLd, buildPostMetadata } from '@/lib/blog/seo'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
|
||||
import { BackLink } from '@/app/(landing)/studio/[slug]/back-link'
|
||||
import { ShareButton } from '@/app/(landing)/studio/[slug]/share-button'
|
||||
|
||||
@@ -26,21 +27,6 @@ export async function generateMetadata({
|
||||
|
||||
export const revalidate = 86400
|
||||
|
||||
const PROSE_CLASSES = [
|
||||
'prose prose-lg prose-invert max-w-none',
|
||||
'prose-headings:font-season prose-headings:font-[430] prose-headings:text-white prose-headings:tracking-[-0.02em]',
|
||||
'prose-p:text-[#F6F6F0]/80',
|
||||
'prose-a:text-[#33C482] prose-a:no-underline hover:prose-a:text-[#33C482]/80',
|
||||
'prose-strong:text-white',
|
||||
'prose-blockquote:border-[#2A2A2A] prose-blockquote:text-[#F6F6F0]/60',
|
||||
'prose-hr:border-[#2A2A2A]',
|
||||
'prose-li:text-[#F6F6F0]/80',
|
||||
'prose-img:rounded-[10px] prose-img:border prose-img:border-[#2A2A2A]',
|
||||
'[&_code]:!bg-[#2A2A2A] [&_code]:!text-[#F6F6F0]/90 [&_code]:rounded [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:text-[0.875em]',
|
||||
'[&_pre]:!bg-[#222222] [&_pre]:border [&_pre]:border-[#2A2A2A] [&_pre]:rounded-[10px]',
|
||||
'[&_pre_code]:!bg-transparent [&_pre_code]:p-0',
|
||||
].join(' ')
|
||||
|
||||
export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
|
||||
const { slug } = await params
|
||||
const post = await getPostBySlug(slug)
|
||||
@@ -50,7 +36,11 @@ export default async function Page({ params }: { params: Promise<{ slug: string
|
||||
const related = await getRelatedPosts(slug, 3)
|
||||
|
||||
return (
|
||||
<article className='w-full' itemScope itemType='https://schema.org/BlogPosting'>
|
||||
<article
|
||||
className={`${soehne.className} w-full`}
|
||||
itemScope
|
||||
itemType='https://schema.org/BlogPosting'
|
||||
>
|
||||
<script
|
||||
type='application/ld+json'
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||
@@ -59,81 +49,98 @@ export default async function Page({ params }: { params: Promise<{ slug: string
|
||||
type='application/ld+json'
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbLd) }}
|
||||
/>
|
||||
<header className='mx-auto max-w-[1000px] px-6 pt-8 sm:px-8 sm:pt-12 md:px-12 md:pt-16'>
|
||||
<header className='mx-auto max-w-[1450px] px-6 pt-8 sm:px-8 sm:pt-12 md:px-12 md:pt-16'>
|
||||
<div className='mb-6'>
|
||||
<BackLink />
|
||||
</div>
|
||||
<div className='flex flex-col'>
|
||||
<h1
|
||||
className='font-[430] font-season text-[36px] text-white leading-tight tracking-[-0.02em] sm:text-[48px] md:text-[56px] lg:text-[64px]'
|
||||
itemProp='headline'
|
||||
>
|
||||
{post.title}
|
||||
</h1>
|
||||
<p className='mt-4 font-[430] font-season text-[#F6F6F0]/80 text-[16px] leading-[1.5] sm:text-[18px] md:text-[22px]'>
|
||||
{post.description}
|
||||
</p>
|
||||
<div className='mt-6 flex items-center justify-between'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<time
|
||||
className='font-[430] font-season text-[#F6F6F0]/50 text-[14px] leading-[1.5] sm:text-[16px]'
|
||||
dateTime={post.date}
|
||||
itemProp='datePublished'
|
||||
>
|
||||
{new Date(post.date).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</time>
|
||||
<meta itemProp='dateModified' content={post.updated ?? post.date} />
|
||||
<span className='text-[#F6F6F0]/30'>·</span>
|
||||
{(post.authors || [post.author]).map((a, idx) => (
|
||||
<div key={idx} className='flex items-center gap-2'>
|
||||
{a?.avatarUrl ? (
|
||||
<Avatar className='size-6'>
|
||||
<AvatarImage src={a.avatarUrl} alt={a.name} />
|
||||
<AvatarFallback>{a.name.slice(0, 2)}</AvatarFallback>
|
||||
</Avatar>
|
||||
) : null}
|
||||
<Link
|
||||
href={a?.url || '#'}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer author'
|
||||
className='font-[430] font-season text-[#F6F6F0]/50 text-[14px] leading-[1.5] hover:text-[#F6F6F0]/80 sm:text-[16px]'
|
||||
itemProp='author'
|
||||
itemScope
|
||||
itemType='https://schema.org/Person'
|
||||
>
|
||||
<span itemProp='name'>{a?.name}</span>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
<div className='flex flex-col gap-8 md:flex-row md:gap-12'>
|
||||
<div className='w-full flex-shrink-0 md:w-[450px]'>
|
||||
<div className='relative w-full overflow-hidden rounded-lg'>
|
||||
<Image
|
||||
src={post.ogImage}
|
||||
alt={post.title}
|
||||
width={450}
|
||||
height={360}
|
||||
className='h-auto w-full'
|
||||
sizes='(max-width: 768px) 100vw, 450px'
|
||||
priority
|
||||
itemProp='image'
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-1 flex-col justify-between'>
|
||||
<h1
|
||||
className='font-medium text-[36px] leading-tight tracking-tight sm:text-[48px] md:text-[56px] lg:text-[64px]'
|
||||
itemProp='headline'
|
||||
>
|
||||
{post.title}
|
||||
</h1>
|
||||
<div className='mt-4 flex items-center justify-between'>
|
||||
<div className='flex items-center gap-3'>
|
||||
{(post.authors || [post.author]).map((a, idx) => (
|
||||
<div key={idx} className='flex items-center gap-2'>
|
||||
{a?.avatarUrl ? (
|
||||
<Avatar className='size-6'>
|
||||
<AvatarImage src={a.avatarUrl} alt={a.name} />
|
||||
<AvatarFallback>{a.name.slice(0, 2)}</AvatarFallback>
|
||||
</Avatar>
|
||||
) : null}
|
||||
<Link
|
||||
href={a?.url || '#'}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer author'
|
||||
className='text-[14px] text-gray-600 leading-[1.5] hover:text-gray-900 sm:text-[16px]'
|
||||
itemProp='author'
|
||||
itemScope
|
||||
itemType='https://schema.org/Person'
|
||||
>
|
||||
<span itemProp='name'>{a?.name}</span>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<ShareButton url={`${getBaseUrl()}/studio/${slug}`} title={post.title} />
|
||||
</div>
|
||||
<ShareButton url={`${getBaseUrl()}/studio/${slug}`} title={post.title} />
|
||||
</div>
|
||||
</div>
|
||||
<hr className='mt-8 border-[#2A2A2A] border-t sm:mt-12' />
|
||||
<hr className='mt-8 border-gray-200 border-t sm:mt-12' />
|
||||
<div className='flex flex-col gap-6 py-8 sm:flex-row sm:items-start sm:justify-between sm:gap-8 sm:py-10'>
|
||||
<div className='flex flex-shrink-0 items-center gap-4'>
|
||||
<time
|
||||
className='block text-[14px] text-gray-600 leading-[1.5] sm:text-[16px]'
|
||||
dateTime={post.date}
|
||||
itemProp='datePublished'
|
||||
>
|
||||
{new Date(post.date).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</time>
|
||||
<meta itemProp='dateModified' content={post.updated ?? post.date} />
|
||||
</div>
|
||||
<div className='flex-1'>
|
||||
<p className='m-0 block translate-y-[-4px] font-[400] text-[18px] leading-[1.5] sm:text-[20px] md:text-[26px]'>
|
||||
{post.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div
|
||||
className='mx-auto max-w-[900px] px-6 py-10 pb-20 sm:px-8 md:px-12'
|
||||
itemProp='articleBody'
|
||||
>
|
||||
<div className={PROSE_CLASSES}>
|
||||
<div className='mx-auto max-w-[900px] px-6 pb-20 sm:px-8 md:px-12' itemProp='articleBody'>
|
||||
<div className='prose prose-lg max-w-none'>
|
||||
<Article />
|
||||
{post.faq && post.faq.length > 0 ? <FAQ items={post.faq} /> : null}
|
||||
</div>
|
||||
</div>
|
||||
{related.length > 0 && (
|
||||
<div className='mx-auto max-w-[900px] px-6 pb-24 sm:px-8 md:px-12'>
|
||||
<h2 className='mb-4 font-[430] font-season text-[24px] text-white tracking-[-0.02em]'>
|
||||
Related posts
|
||||
</h2>
|
||||
<h2 className='mb-4 font-medium text-[24px]'>Related posts</h2>
|
||||
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2 sm:gap-6 lg:grid-cols-3'>
|
||||
{related.map((p) => (
|
||||
<Link key={p.slug} href={`/studio/${p.slug}`} className='group'>
|
||||
<div className='overflow-hidden rounded-[10px] border border-[#2A2A2A] bg-[#222222] transition-all hover:border-[#3A3A3A]'>
|
||||
<div className='overflow-hidden rounded-lg border border-gray-200'>
|
||||
<Image
|
||||
src={p.ogImage}
|
||||
alt={p.title}
|
||||
@@ -145,16 +152,14 @@ export default async function Page({ params }: { params: Promise<{ slug: string
|
||||
unoptimized
|
||||
/>
|
||||
<div className='p-3'>
|
||||
<div className='mb-1 font-[430] font-season text-[#F6F6F0]/50 text-xs'>
|
||||
<div className='mb-1 text-gray-600 text-xs'>
|
||||
{new Date(p.date).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</div>
|
||||
<div className='font-[430] font-season text-sm text-white leading-tight'>
|
||||
{p.title}
|
||||
</div>
|
||||
<div className='font-medium text-sm leading-tight'>{p.title}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
@@ -2,33 +2,64 @@
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Share2 } from 'lucide-react'
|
||||
import { Popover, PopoverContent, PopoverItem, PopoverTrigger } from '@/components/emcn'
|
||||
|
||||
interface ShareButtonProps {
|
||||
url: string
|
||||
title: string
|
||||
}
|
||||
|
||||
export function ShareButton({ url }: ShareButtonProps) {
|
||||
export function ShareButton({ url, title }: ShareButtonProps) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const handleCopy = async () => {
|
||||
const handleCopyLink = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(url)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 1500)
|
||||
setTimeout(() => {
|
||||
setCopied(false)
|
||||
setOpen(false)
|
||||
}, 1000)
|
||||
} catch {
|
||||
/* noop */
|
||||
setOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleShareTwitter = () => {
|
||||
const tweetUrl = `https://twitter.com/intent/tweet?url=${encodeURIComponent(url)}&text=${encodeURIComponent(title)}`
|
||||
window.open(tweetUrl, '_blank', 'noopener,noreferrer')
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const handleShareLinkedIn = () => {
|
||||
const linkedInUrl = `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(url)}`
|
||||
window.open(linkedInUrl, '_blank', 'noopener,noreferrer')
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className='flex items-center gap-1.5 font-[430] font-season text-[#F6F6F0]/50 text-sm hover:text-[#F6F6F0]/80'
|
||||
aria-label='Copy link'
|
||||
<Popover
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
variant='secondary'
|
||||
size='sm'
|
||||
colorScheme='inverted'
|
||||
>
|
||||
<Share2 className='h-4 w-4' />
|
||||
<span>{copied ? 'Copied!' : 'Share'}</span>
|
||||
</button>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
className='flex items-center gap-1.5 text-gray-600 text-sm hover:text-gray-900'
|
||||
aria-label='Share this post'
|
||||
>
|
||||
<Share2 className='h-4 w-4' />
|
||||
<span>Share</span>
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align='end' minWidth={140}>
|
||||
<PopoverItem onClick={handleCopyLink}>{copied ? 'Copied!' : 'Copy link'}</PopoverItem>
|
||||
<PopoverItem onClick={handleShareTwitter}>Share on X</PopoverItem>
|
||||
<PopoverItem onClick={handleShareLinkedIn}>Share on LinkedIn</PopoverItem>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { getAllPostMeta } from '@/lib/blog/registry'
|
||||
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
|
||||
|
||||
export const revalidate = 3600
|
||||
|
||||
@@ -10,10 +11,8 @@ export default async function AuthorPage({ params }: { params: Promise<{ id: str
|
||||
const author = posts[0]?.author
|
||||
if (!author) {
|
||||
return (
|
||||
<main className='mx-auto max-w-[900px] px-6 py-10 sm:px-8 md:px-12'>
|
||||
<h1 className='font-[430] font-season text-[32px] text-white tracking-[-0.02em]'>
|
||||
Author not found
|
||||
</h1>
|
||||
<main className={`${soehne.className} mx-auto max-w-[900px] px-6 py-10 sm:px-8 md:px-12`}>
|
||||
<h1 className='font-medium text-[32px]'>Author not found</h1>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -26,7 +25,7 @@ export default async function AuthorPage({ params }: { params: Promise<{ id: str
|
||||
image: author.avatarUrl,
|
||||
}
|
||||
return (
|
||||
<main className='mx-auto max-w-[900px] px-6 py-10 sm:px-8 md:px-12'>
|
||||
<main className={`${soehne.className} mx-auto max-w-[900px] px-6 py-10 sm:px-8 md:px-12`}>
|
||||
<script
|
||||
type='application/ld+json'
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(personJsonLd) }}
|
||||
@@ -42,14 +41,12 @@ export default async function AuthorPage({ params }: { params: Promise<{ id: str
|
||||
unoptimized
|
||||
/>
|
||||
) : null}
|
||||
<h1 className='font-[430] font-season text-[32px] text-white leading-tight tracking-[-0.02em]'>
|
||||
{author.name}
|
||||
</h1>
|
||||
<h1 className='font-medium text-[32px] leading-tight'>{author.name}</h1>
|
||||
</div>
|
||||
<div className='grid grid-cols-1 gap-8 sm:grid-cols-2'>
|
||||
{posts.map((p) => (
|
||||
<Link key={p.slug} href={`/studio/${p.slug}`} className='group'>
|
||||
<div className='overflow-hidden rounded-[10px] border border-[#2A2A2A] bg-[#222222] transition-all hover:border-[#3A3A3A]'>
|
||||
<div className='overflow-hidden rounded-lg border border-gray-200'>
|
||||
<Image
|
||||
src={p.ogImage}
|
||||
alt={p.title}
|
||||
@@ -59,16 +56,14 @@ export default async function AuthorPage({ params }: { params: Promise<{ id: str
|
||||
unoptimized
|
||||
/>
|
||||
<div className='p-3'>
|
||||
<div className='mb-1 font-[430] font-season text-[#F6F6F0]/50 text-xs'>
|
||||
<div className='mb-1 text-gray-600 text-xs'>
|
||||
{new Date(p.date).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</div>
|
||||
<div className='font-[430] font-season text-sm text-white leading-tight'>
|
||||
{p.title}
|
||||
</div>
|
||||
<div className='font-medium text-sm leading-tight'>{p.title}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import { martianMono } from '@/app/_styles/fonts/martian-mono/martian-mono'
|
||||
import { season } from '@/app/_styles/fonts/season/season'
|
||||
import Navbar from '@/app/(home)/components/navbar/navbar'
|
||||
import { Footer } from '@/app/(landing)/components'
|
||||
import { Footer, Nav } from '@/app/(landing)/components'
|
||||
|
||||
export default function StudioLayout({ children }: { children: React.ReactNode }) {
|
||||
const orgJsonLd = {
|
||||
@@ -26,8 +23,7 @@ export default function StudioLayout({ children }: { children: React.ReactNode }
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${season.variable} ${martianMono.variable} relative min-h-screen`}>
|
||||
<div className='-z-50 pointer-events-none fixed inset-0 bg-[#1C1C1C]' />
|
||||
<div className='flex min-h-screen flex-col'>
|
||||
<script
|
||||
type='application/ld+json'
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(orgJsonLd) }}
|
||||
@@ -36,7 +32,7 @@ export default function StudioLayout({ children }: { children: React.ReactNode }
|
||||
type='application/ld+json'
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(websiteJsonLd) }}
|
||||
/>
|
||||
<Navbar />
|
||||
<Nav hideAuthButtons={false} variant='landing' />
|
||||
<main className='relative flex-1'>{children}</main>
|
||||
<Footer fullWidth={true} />
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Link from 'next/link'
|
||||
import { getAllPostMeta } from '@/lib/blog/registry'
|
||||
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
|
||||
import { PostGrid } from '@/app/(landing)/studio/post-grid'
|
||||
|
||||
export const revalidate = 3600
|
||||
@@ -28,7 +29,8 @@ export default async function StudioIndex({
|
||||
const totalPages = Math.max(1, Math.ceil(sorted.length / perPage))
|
||||
const start = (pageNum - 1) * perPage
|
||||
const posts = sorted.slice(start, start + perPage)
|
||||
|
||||
// Tag filter chips are intentionally disabled for now.
|
||||
// const tags = await getAllTags()
|
||||
const studioJsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Blog',
|
||||
@@ -38,48 +40,52 @@ export default async function StudioIndex({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='relative min-h-screen overflow-hidden'>
|
||||
<main className={`${soehne.className} mx-auto max-w-[1200px] px-6 py-12 sm:px-8 md:px-12`}>
|
||||
<script
|
||||
type='application/ld+json'
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(studioJsonLd) }}
|
||||
/>
|
||||
<h1 className='mb-3 font-medium text-[40px] leading-tight sm:text-[56px]'>Sim Studio</h1>
|
||||
<p className='mb-10 text-[18px] text-gray-700'>
|
||||
Announcements, insights, and guides for building AI agent workflows.
|
||||
</p>
|
||||
|
||||
<main className='relative z-10 mx-auto max-w-[1400px] px-4 py-16 sm:px-6 md:px-8 md:py-24'>
|
||||
<h1 className='mt-6 font-[430] font-season text-4xl text-white tracking-[-0.02em] sm:text-5xl'>
|
||||
Sim Studio
|
||||
</h1>
|
||||
<p className='mt-3 font-[430] font-season text-[#F6F6F0]/50 text-[14px] leading-[125%] tracking-[0.02em] sm:text-[16px]'>
|
||||
Announcements, insights, and guides for building AI agent workflows.
|
||||
</p>
|
||||
{/* Tag filter chips hidden until we have more posts */}
|
||||
{/* <div className='mb-10 flex flex-wrap gap-3'>
|
||||
<Link href='/studio' className={`rounded-full border px-3 py-1 text-sm ${!tag ? 'border-black bg-black text-white' : 'border-gray-300'}`}>All</Link>
|
||||
{tags.map((t) => (
|
||||
<Link key={t.tag} href={`/studio?tag=${encodeURIComponent(t.tag)}`} className={`rounded-full border px-3 py-1 text-sm ${tag === t.tag ? 'border-black bg-black text-white' : 'border-gray-300'}`}>
|
||||
{t.tag} ({t.count})
|
||||
</Link>
|
||||
))}
|
||||
</div> */}
|
||||
|
||||
<div className='mt-10'>
|
||||
<PostGrid posts={posts} />
|
||||
{/* Grid layout for consistent rows */}
|
||||
<PostGrid posts={posts} />
|
||||
|
||||
{totalPages > 1 && (
|
||||
<div className='mt-10 flex items-center justify-center gap-3'>
|
||||
{pageNum > 1 && (
|
||||
<Link
|
||||
href={`/studio?page=${pageNum - 1}${tag ? `&tag=${encodeURIComponent(tag)}` : ''}`}
|
||||
className='rounded border px-3 py-1 text-sm'
|
||||
>
|
||||
Previous
|
||||
</Link>
|
||||
)}
|
||||
<span className='text-gray-600 text-sm'>
|
||||
Page {pageNum} of {totalPages}
|
||||
</span>
|
||||
{pageNum < totalPages && (
|
||||
<Link
|
||||
href={`/studio?page=${pageNum + 1}${tag ? `&tag=${encodeURIComponent(tag)}` : ''}`}
|
||||
className='rounded border px-3 py-1 text-sm'
|
||||
>
|
||||
Next
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{totalPages > 1 && (
|
||||
<div className='mt-10 flex items-center justify-center gap-3'>
|
||||
{pageNum > 1 && (
|
||||
<Link
|
||||
href={`/studio?page=${pageNum - 1}${tag ? `&tag=${encodeURIComponent(tag)}` : ''}`}
|
||||
className='rounded-[5px] border border-[#2A2A2A] bg-[rgba(246,246,240,0.06)] px-3 py-1 font-[430] font-season text-[#F6F6F6] text-[14px] transition-all hover:bg-[rgba(246,246,240,0.1)]'
|
||||
>
|
||||
Previous
|
||||
</Link>
|
||||
)}
|
||||
<span className='font-[430] font-season text-[#F6F6F0]/50 text-[14px]'>
|
||||
Page {pageNum} of {totalPages}
|
||||
</span>
|
||||
{pageNum < totalPages && (
|
||||
<Link
|
||||
href={`/studio?page=${pageNum + 1}${tag ? `&tag=${encodeURIComponent(tag)}` : ''}`}
|
||||
className='rounded-[5px] border border-[#2A2A2A] bg-[rgba(246,246,240,0.06)] px-3 py-1 font-[430] font-season text-[#F6F6F6] text-[14px] transition-all hover:bg-[rgba(246,246,240,0.1)]'
|
||||
>
|
||||
Next
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/emcn'
|
||||
|
||||
interface Author {
|
||||
id: string
|
||||
@@ -23,78 +22,69 @@ interface Post {
|
||||
featured?: boolean
|
||||
}
|
||||
|
||||
const INITIAL_VISIBLE = 9
|
||||
|
||||
export function PostGrid({ posts }: { posts: Post[] }) {
|
||||
const [showAll, setShowAll] = useState(false)
|
||||
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null)
|
||||
const visiblePosts = showAll ? posts : posts.slice(0, INITIAL_VISIBLE)
|
||||
const hasMore = posts.length > INITIAL_VISIBLE
|
||||
|
||||
return (
|
||||
<div className='flex flex-col gap-10'>
|
||||
<div
|
||||
className='grid grid-cols-1 gap-8 md:grid-cols-2 md:gap-10 lg:grid-cols-3'
|
||||
onMouseLeave={() => setHoveredIndex(null)}
|
||||
>
|
||||
{visiblePosts.map((p, index) => {
|
||||
const authors = p.authors && p.authors.length > 0 ? p.authors : [p.author]
|
||||
const authorNames = authors.map((a) => a?.name).join(', ')
|
||||
const isHovered = hoveredIndex === index
|
||||
const isDimmed = hoveredIndex !== null && !isHovered
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={p.slug}
|
||||
href={`/studio/${p.slug}`}
|
||||
className={cn(
|
||||
'group flex flex-col overflow-hidden rounded-[10px] border border-[#2A2A2A] transition-[background-color] duration-200',
|
||||
isDimmed ? 'bg-transparent' : 'bg-[#222222] hover:border-[#3A3A3A]'
|
||||
)}
|
||||
onMouseEnter={() => setHoveredIndex(index)}
|
||||
>
|
||||
<div className='relative aspect-video w-full overflow-hidden bg-[#1C1C1C]'>
|
||||
<Image
|
||||
src={p.ogImage}
|
||||
alt={p.title}
|
||||
sizes='(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw'
|
||||
unoptimized
|
||||
priority={index < 6}
|
||||
loading={index < 6 ? undefined : 'lazy'}
|
||||
fill
|
||||
style={{ objectFit: 'cover' }}
|
||||
/>
|
||||
<div className='grid grid-cols-1 gap-4 md:grid-cols-2 md:gap-6 lg:grid-cols-3'>
|
||||
{posts.map((p, index) => (
|
||||
<Link key={p.slug} href={`/studio/${p.slug}`} className='group flex flex-col'>
|
||||
<div className='flex h-full flex-col overflow-hidden rounded-xl border border-gray-200 transition-colors duration-300 hover:border-gray-300'>
|
||||
{/* Image container with fixed aspect ratio to prevent layout shift */}
|
||||
<div className='relative aspect-video w-full overflow-hidden'>
|
||||
<Image
|
||||
src={p.ogImage}
|
||||
alt={p.title}
|
||||
sizes='(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw'
|
||||
unoptimized
|
||||
priority={index < 6}
|
||||
loading={index < 6 ? undefined : 'lazy'}
|
||||
fill
|
||||
style={{ objectFit: 'cover' }}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-1 flex-col p-4'>
|
||||
<div className='mb-2 text-gray-600 text-xs'>
|
||||
{new Date(p.date).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</div>
|
||||
<div className='flex flex-1 flex-col gap-2 p-4'>
|
||||
<h3 className='font-[430] font-season text-[17px] text-white leading-snug'>
|
||||
{p.title}
|
||||
</h3>
|
||||
<span className='font-[430] font-season text-[#F6F6F0]/50 text-[12px]'>
|
||||
{new Date(p.date).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})}
|
||||
<span className='mx-2'>•</span>
|
||||
{authorNames}
|
||||
<h3 className='shine-text mb-1 font-medium text-lg leading-tight'>{p.title}</h3>
|
||||
<p className='mb-3 line-clamp-3 flex-1 text-gray-700 text-sm'>{p.description}</p>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='-space-x-1.5 flex'>
|
||||
{(p.authors && p.authors.length > 0 ? p.authors : [p.author])
|
||||
.slice(0, 3)
|
||||
.map((author, idx) => (
|
||||
<Avatar key={idx} className='size-4 border border-white'>
|
||||
<AvatarImage src={author?.avatarUrl} alt={author?.name} />
|
||||
<AvatarFallback className='border border-white bg-gray-100 text-[10px] text-gray-600'>
|
||||
{author?.name.slice(0, 2)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
))}
|
||||
</div>
|
||||
<span className='text-gray-600 text-xs'>
|
||||
{(p.authors && p.authors.length > 0 ? p.authors : [p.author])
|
||||
.slice(0, 2)
|
||||
.map((a) => a?.name)
|
||||
.join(', ')}
|
||||
{(p.authors && p.authors.length > 0 ? p.authors : [p.author]).length > 2 && (
|
||||
<>
|
||||
{' '}
|
||||
and {(p.authors && p.authors.length > 0 ? p.authors : [p.author]).length - 2}{' '}
|
||||
other
|
||||
{(p.authors && p.authors.length > 0 ? p.authors : [p.author]).length - 2 > 1
|
||||
? 's'
|
||||
: ''}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{hasMore && !showAll && (
|
||||
<div className='flex justify-center'>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => setShowAll(true)}
|
||||
className='rounded-[5px] border border-[#2A2A2A] bg-[rgba(246,246,240,0.06)] px-4 py-2 font-[430] font-season text-[#F6F6F6] text-[14px] transition-all hover:bg-[rgba(246,246,240,0.1)]'
|
||||
>
|
||||
Show more
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
BlocksLeftAnimated,
|
||||
BlocksRightAnimated,
|
||||
BlocksRightSideAnimated,
|
||||
BlocksTopLeftAnimated,
|
||||
BlocksTopRightAnimated,
|
||||
useBlockCycle,
|
||||
} from '@/app/(home)/components/hero/components/animated-blocks'
|
||||
|
||||
export function StudioBlocks() {
|
||||
const blockStates = useBlockCycle()
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='pointer-events-none absolute top-0 right-[13.1vw] z-20 w-[calc(140px_+_10.76vw)] max-w-[295px]'
|
||||
>
|
||||
<BlocksTopRightAnimated animState={blockStates.topRight} />
|
||||
</div>
|
||||
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='pointer-events-none absolute top-0 left-[16vw] z-20 w-[calc(140px_+_10.76vw)] max-w-[295px]'
|
||||
>
|
||||
<BlocksTopLeftAnimated animState={blockStates.topLeft} />
|
||||
</div>
|
||||
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='-translate-y-1/2 pointer-events-none absolute top-[50%] left-0 z-20 w-[calc(16px_+_1.25vw)] max-w-[34px]'
|
||||
>
|
||||
<BlocksLeftAnimated animState={blockStates.left} />
|
||||
</div>
|
||||
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='-translate-y-1/2 pointer-events-none absolute top-[50%] right-0 z-20 w-[calc(16px_+_1.25vw)] max-w-[34px]'
|
||||
>
|
||||
<BlocksRightAnimated animState={blockStates.rightEdge} />
|
||||
</div>
|
||||
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='-translate-y-1/2 pointer-events-none absolute top-[50%] right-[3vw] z-20 w-[calc(16px_+_1.25vw)] max-w-[34px] scale-x-[-1]'
|
||||
>
|
||||
<BlocksRightSideAnimated animState={blockStates.rightSide} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -5,21 +5,16 @@ export default async function TagsIndex() {
|
||||
const tags = await getAllTags()
|
||||
return (
|
||||
<main className='mx-auto max-w-[900px] px-6 py-10 sm:px-8 md:px-12'>
|
||||
<h1 className='mb-6 font-[430] font-season text-[32px] text-white leading-tight tracking-[-0.02em]'>
|
||||
Browse by tag
|
||||
</h1>
|
||||
<h1 className='mb-6 font-medium text-[32px] leading-tight'>Browse by tag</h1>
|
||||
<div className='flex flex-wrap gap-3'>
|
||||
<Link
|
||||
href='/studio'
|
||||
className='rounded-full border border-[#2A2A2A] bg-[rgba(246,246,240,0.06)] px-3 py-1 font-[430] font-season text-[#F6F6F6] text-[14px] transition-all hover:bg-[rgba(246,246,240,0.1)]'
|
||||
>
|
||||
<Link href='/studio' className='rounded-full border border-gray-300 px-3 py-1 text-sm'>
|
||||
All
|
||||
</Link>
|
||||
{tags.map((t) => (
|
||||
<Link
|
||||
key={t.tag}
|
||||
href={`/studio?tag=${encodeURIComponent(t.tag)}`}
|
||||
className='rounded-full border border-[#2A2A2A] bg-[rgba(246,246,240,0.06)] px-3 py-1 font-[430] font-season text-[#F6F6F6] text-[14px] transition-all hover:bg-[rgba(246,246,240,0.1)]'
|
||||
className='rounded-full border border-gray-300 px-3 py-1 text-sm'
|
||||
>
|
||||
{t.tag} ({t.count})
|
||||
</Link>
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Martian_Mono } from 'next/font/google'
|
||||
|
||||
/**
|
||||
* Martian Mono font configuration
|
||||
* Monospaced variable font used for code snippets, technical content, and accent text
|
||||
* on the landing page. Supports weights 100-800.
|
||||
*/
|
||||
export const martianMono = Martian_Mono({
|
||||
subsets: ['latin'],
|
||||
display: 'swap',
|
||||
variable: '--font-martian-mono',
|
||||
weight: 'variable',
|
||||
fallback: ['ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', 'monospace'],
|
||||
})
|
||||
@@ -10,7 +10,7 @@
|
||||
* @see stores/constants.ts for the source of truth
|
||||
*/
|
||||
:root {
|
||||
--sidebar-width: 248px; /* SIDEBAR_WIDTH.DEFAULT */
|
||||
--sidebar-width: 232px; /* SIDEBAR_WIDTH.DEFAULT */
|
||||
--panel-width: 320px; /* PANEL_WIDTH.DEFAULT */
|
||||
--toolbar-triggers-height: 300px; /* TOOLBAR_TRIGGERS_HEIGHT.DEFAULT */
|
||||
--editor-connections-height: 172px; /* EDITOR_CONNECTIONS_HEIGHT.DEFAULT */
|
||||
@@ -93,14 +93,14 @@
|
||||
--border: #e0e0e0; /* primary border */
|
||||
--surface-5: #f3f3f3; /* inputs, form elements */
|
||||
--border-1: #e0e0e0; /* stronger border */
|
||||
--surface-6: #e5e5e5; /* popovers, elevated surfaces */
|
||||
--surface-7: #d9d9d9;
|
||||
--surface-6: #f0f0f0; /* popovers, elevated surfaces */
|
||||
--surface-7: #ececec;
|
||||
|
||||
--workflow-edge: #e0e0e0; /* workflow handles/edges - matches border-1 */
|
||||
|
||||
/* Text - neutral */
|
||||
--text-primary: #2d2d2d;
|
||||
--text-secondary: #4e4e4e;
|
||||
--text-secondary: #404040;
|
||||
--text-tertiary: #5c5c5c;
|
||||
--text-muted: #737373;
|
||||
--text-subtle: #8c8c8c;
|
||||
@@ -125,7 +125,7 @@
|
||||
|
||||
/* Font weights - lighter for light mode */
|
||||
--font-weight-base: 430;
|
||||
--font-weight-medium: 440;
|
||||
--font-weight-medium: 450;
|
||||
--font-weight-semibold: 500;
|
||||
|
||||
/* Extended palette */
|
||||
@@ -211,7 +211,7 @@
|
||||
--surface-5: #363636;
|
||||
--border-1: #3d3d3d;
|
||||
--surface-6: #454545;
|
||||
--surface-7: #505050;
|
||||
--surface-7: #454545;
|
||||
|
||||
--workflow-edge: #454545; /* workflow handles/edges - same as surface-6 in dark */
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import { validateUrlWithDNS } from '@/lib/core/security/input-validation.server'
|
||||
import { SSE_HEADERS } from '@/lib/core/utils/sse'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { markExecutionCancelled } from '@/lib/execution/cancellation'
|
||||
import { decrementSSEConnections, incrementSSEConnections } from '@/lib/monitoring/sse-connections'
|
||||
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
|
||||
import { getWorkspaceBilledAccountUserId } from '@/lib/workspaces/utils'
|
||||
import {
|
||||
@@ -630,9 +631,11 @@ async function handleMessageStream(
|
||||
}
|
||||
|
||||
const encoder = new TextEncoder()
|
||||
let messageStreamDecremented = false
|
||||
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
incrementSSEConnections('a2a-message')
|
||||
const sendEvent = (event: string, data: unknown) => {
|
||||
try {
|
||||
const jsonRpcResponse = {
|
||||
@@ -841,9 +844,19 @@ async function handleMessageStream(
|
||||
})
|
||||
} finally {
|
||||
await releaseLock(lockKey, lockValue)
|
||||
if (!messageStreamDecremented) {
|
||||
messageStreamDecremented = true
|
||||
decrementSSEConnections('a2a-message')
|
||||
}
|
||||
controller.close()
|
||||
}
|
||||
},
|
||||
cancel() {
|
||||
if (!messageStreamDecremented) {
|
||||
messageStreamDecremented = true
|
||||
decrementSSEConnections('a2a-message')
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return new NextResponse(stream, {
|
||||
@@ -1016,16 +1029,34 @@ async function handleTaskResubscribe(
|
||||
let pollTimeoutId: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
const abortSignal = request.signal
|
||||
abortSignal.addEventListener('abort', () => {
|
||||
abortSignal.addEventListener(
|
||||
'abort',
|
||||
() => {
|
||||
isCancelled = true
|
||||
if (pollTimeoutId) {
|
||||
clearTimeout(pollTimeoutId)
|
||||
pollTimeoutId = null
|
||||
}
|
||||
},
|
||||
{ once: true }
|
||||
)
|
||||
|
||||
let sseDecremented = false
|
||||
const cleanup = () => {
|
||||
isCancelled = true
|
||||
if (pollTimeoutId) {
|
||||
clearTimeout(pollTimeoutId)
|
||||
pollTimeoutId = null
|
||||
}
|
||||
})
|
||||
if (!sseDecremented) {
|
||||
sseDecremented = true
|
||||
decrementSSEConnections('a2a-resubscribe')
|
||||
}
|
||||
}
|
||||
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
incrementSSEConnections('a2a-resubscribe')
|
||||
const sendEvent = (event: string, data: unknown): boolean => {
|
||||
if (isCancelled || abortSignal.aborted) return false
|
||||
try {
|
||||
@@ -1041,14 +1072,6 @@ async function handleTaskResubscribe(
|
||||
}
|
||||
}
|
||||
|
||||
const cleanup = () => {
|
||||
isCancelled = true
|
||||
if (pollTimeoutId) {
|
||||
clearTimeout(pollTimeoutId)
|
||||
pollTimeoutId = null
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!sendEvent('status', {
|
||||
kind: 'status',
|
||||
@@ -1160,11 +1183,7 @@ async function handleTaskResubscribe(
|
||||
poll()
|
||||
},
|
||||
cancel() {
|
||||
isCancelled = true
|
||||
if (pollTimeoutId) {
|
||||
clearTimeout(pollTimeoutId)
|
||||
pollTimeoutId = null
|
||||
}
|
||||
cleanup()
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { createMockRequest, setupCommonApiMocks } from '@sim/testing'
|
||||
import { createMockRequest } from '@sim/testing'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const handlerMocks = vi.hoisted(() => ({
|
||||
@@ -14,6 +14,7 @@ const handlerMocks = vi.hoisted(() => ({
|
||||
session: { id: 'anon-session' },
|
||||
},
|
||||
})),
|
||||
isAuthDisabled: false,
|
||||
}))
|
||||
|
||||
vi.mock('better-auth/next-js', () => ({
|
||||
@@ -32,18 +33,22 @@ vi.mock('@/lib/auth/anonymous', () => ({
|
||||
createAnonymousGetSessionResponse: handlerMocks.createAnonymousGetSessionResponse,
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/core/config/feature-flags', () => ({
|
||||
get isAuthDisabled() {
|
||||
return handlerMocks.isAuthDisabled
|
||||
},
|
||||
}))
|
||||
|
||||
import { GET } from '@/app/api/auth/[...all]/route'
|
||||
|
||||
describe('auth catch-all route (DISABLE_AUTH get-session)', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
setupCommonApiMocks()
|
||||
handlerMocks.betterAuthGET.mockReset()
|
||||
handlerMocks.betterAuthPOST.mockReset()
|
||||
handlerMocks.ensureAnonymousUserExists.mockReset()
|
||||
handlerMocks.createAnonymousGetSessionResponse.mockClear()
|
||||
vi.clearAllMocks()
|
||||
handlerMocks.isAuthDisabled = false
|
||||
})
|
||||
|
||||
it('returns anonymous session in better-auth response envelope when auth is disabled', async () => {
|
||||
vi.doMock('@/lib/core/config/feature-flags', () => ({ isAuthDisabled: true }))
|
||||
handlerMocks.isAuthDisabled = true
|
||||
|
||||
const req = createMockRequest(
|
||||
'GET',
|
||||
@@ -51,7 +56,6 @@ describe('auth catch-all route (DISABLE_AUTH get-session)', () => {
|
||||
{},
|
||||
'http://localhost:3000/api/auth/get-session'
|
||||
)
|
||||
const { GET } = await import('@/app/api/auth/[...all]/route')
|
||||
|
||||
const res = await GET(req as any)
|
||||
const json = await res.json()
|
||||
@@ -67,10 +71,11 @@ describe('auth catch-all route (DISABLE_AUTH get-session)', () => {
|
||||
})
|
||||
|
||||
it('delegates to better-auth handler when auth is enabled', async () => {
|
||||
vi.doMock('@/lib/core/config/feature-flags', () => ({ isAuthDisabled: false }))
|
||||
handlerMocks.isAuthDisabled = false
|
||||
|
||||
const { NextResponse } = await import('next/server')
|
||||
handlerMocks.betterAuthGET.mockResolvedValueOnce(
|
||||
new (await import('next/server')).NextResponse(JSON.stringify({ data: { ok: true } }), {
|
||||
new NextResponse(JSON.stringify({ data: { ok: true } }), {
|
||||
headers: { 'content-type': 'application/json' },
|
||||
}) as any
|
||||
)
|
||||
@@ -81,7 +86,6 @@ describe('auth catch-all route (DISABLE_AUTH get-session)', () => {
|
||||
{},
|
||||
'http://localhost:3000/api/auth/get-session'
|
||||
)
|
||||
const { GET } = await import('@/app/api/auth/[...all]/route')
|
||||
|
||||
const res = await GET(req as any)
|
||||
const json = await res.json()
|
||||
|
||||
@@ -3,63 +3,45 @@
|
||||
*
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import {
|
||||
createMockRequest,
|
||||
mockConsoleLogger,
|
||||
mockCryptoUuid,
|
||||
mockDrizzleOrm,
|
||||
mockUuid,
|
||||
setupCommonApiMocks,
|
||||
} from '@sim/testing'
|
||||
import { createMockRequest } from '@sim/testing'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const { mockForgetPassword, mockLogger } = vi.hoisted(() => {
|
||||
const logger = {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
trace: vi.fn(),
|
||||
fatal: vi.fn(),
|
||||
child: vi.fn(),
|
||||
}
|
||||
return {
|
||||
mockForgetPassword: vi.fn(),
|
||||
mockLogger: logger,
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/lib/core/utils/urls', () => ({
|
||||
getBaseUrl: vi.fn(() => 'https://app.example.com'),
|
||||
}))
|
||||
|
||||
/** Setup auth API mocks for testing authentication routes */
|
||||
function setupAuthApiMocks(
|
||||
options: {
|
||||
operations?: {
|
||||
forgetPassword?: { success?: boolean; error?: string }
|
||||
resetPassword?: { success?: boolean; error?: string }
|
||||
}
|
||||
} = {}
|
||||
) {
|
||||
setupCommonApiMocks()
|
||||
mockUuid()
|
||||
mockCryptoUuid()
|
||||
mockConsoleLogger()
|
||||
mockDrizzleOrm()
|
||||
|
||||
const { operations = {} } = options
|
||||
const defaultOperations = {
|
||||
forgetPassword: { success: true, error: 'Forget password error', ...operations.forgetPassword },
|
||||
resetPassword: { success: true, error: 'Reset password error', ...operations.resetPassword },
|
||||
}
|
||||
|
||||
const createAuthMethod = (config: { success?: boolean; error?: string }) => {
|
||||
return vi.fn().mockImplementation(() => {
|
||||
if (config.success) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
return Promise.reject(new Error(config.error))
|
||||
})
|
||||
}
|
||||
|
||||
vi.doMock('@/lib/auth', () => ({
|
||||
auth: {
|
||||
api: {
|
||||
forgetPassword: createAuthMethod(defaultOperations.forgetPassword),
|
||||
resetPassword: createAuthMethod(defaultOperations.resetPassword),
|
||||
},
|
||||
vi.mock('@/lib/auth', () => ({
|
||||
auth: {
|
||||
api: {
|
||||
forgetPassword: mockForgetPassword,
|
||||
},
|
||||
}))
|
||||
}
|
||||
},
|
||||
}))
|
||||
vi.mock('@sim/logger', () => ({
|
||||
createLogger: vi.fn().mockReturnValue(mockLogger),
|
||||
}))
|
||||
|
||||
import { POST } from '@/app/api/auth/forget-password/route'
|
||||
|
||||
describe('Forget Password API Route', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
mockForgetPassword.mockResolvedValue(undefined)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
@@ -67,27 +49,18 @@ describe('Forget Password API Route', () => {
|
||||
})
|
||||
|
||||
it('should send password reset email successfully with same-origin redirectTo', async () => {
|
||||
setupAuthApiMocks({
|
||||
operations: {
|
||||
forgetPassword: { success: true },
|
||||
},
|
||||
})
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
email: 'test@example.com',
|
||||
redirectTo: 'https://app.example.com/reset',
|
||||
})
|
||||
|
||||
const { POST } = await import('@/app/api/auth/forget-password/route')
|
||||
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(data.success).toBe(true)
|
||||
|
||||
const auth = await import('@/lib/auth')
|
||||
expect(auth.auth.api.forgetPassword).toHaveBeenCalledWith({
|
||||
expect(mockForgetPassword).toHaveBeenCalledWith({
|
||||
body: {
|
||||
email: 'test@example.com',
|
||||
redirectTo: 'https://app.example.com/reset',
|
||||
@@ -97,50 +70,32 @@ describe('Forget Password API Route', () => {
|
||||
})
|
||||
|
||||
it('should reject external redirectTo URL', async () => {
|
||||
setupAuthApiMocks({
|
||||
operations: {
|
||||
forgetPassword: { success: true },
|
||||
},
|
||||
})
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
email: 'test@example.com',
|
||||
redirectTo: 'https://evil.com/phishing',
|
||||
})
|
||||
|
||||
const { POST } = await import('@/app/api/auth/forget-password/route')
|
||||
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
expect(data.message).toBe('Redirect URL must be a valid same-origin URL')
|
||||
|
||||
const auth = await import('@/lib/auth')
|
||||
expect(auth.auth.api.forgetPassword).not.toHaveBeenCalled()
|
||||
expect(mockForgetPassword).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should send password reset email without redirectTo', async () => {
|
||||
setupAuthApiMocks({
|
||||
operations: {
|
||||
forgetPassword: { success: true },
|
||||
},
|
||||
})
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
email: 'test@example.com',
|
||||
})
|
||||
|
||||
const { POST } = await import('@/app/api/auth/forget-password/route')
|
||||
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(data.success).toBe(true)
|
||||
|
||||
const auth = await import('@/lib/auth')
|
||||
expect(auth.auth.api.forgetPassword).toHaveBeenCalledWith({
|
||||
expect(mockForgetPassword).toHaveBeenCalledWith({
|
||||
body: {
|
||||
email: 'test@example.com',
|
||||
redirectTo: undefined,
|
||||
@@ -150,97 +105,64 @@ describe('Forget Password API Route', () => {
|
||||
})
|
||||
|
||||
it('should handle missing email', async () => {
|
||||
setupAuthApiMocks()
|
||||
|
||||
const req = createMockRequest('POST', {})
|
||||
|
||||
const { POST } = await import('@/app/api/auth/forget-password/route')
|
||||
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
expect(data.message).toBe('Email is required')
|
||||
|
||||
const auth = await import('@/lib/auth')
|
||||
expect(auth.auth.api.forgetPassword).not.toHaveBeenCalled()
|
||||
expect(mockForgetPassword).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle empty email', async () => {
|
||||
setupAuthApiMocks()
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
email: '',
|
||||
})
|
||||
|
||||
const { POST } = await import('@/app/api/auth/forget-password/route')
|
||||
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
expect(data.message).toBe('Please provide a valid email address')
|
||||
|
||||
const auth = await import('@/lib/auth')
|
||||
expect(auth.auth.api.forgetPassword).not.toHaveBeenCalled()
|
||||
expect(mockForgetPassword).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle auth service error with message', async () => {
|
||||
const errorMessage = 'User not found'
|
||||
|
||||
setupAuthApiMocks({
|
||||
operations: {
|
||||
forgetPassword: {
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
},
|
||||
},
|
||||
})
|
||||
mockForgetPassword.mockRejectedValue(new Error(errorMessage))
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
email: 'nonexistent@example.com',
|
||||
})
|
||||
|
||||
const { POST } = await import('@/app/api/auth/forget-password/route')
|
||||
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(500)
|
||||
expect(data.message).toBe(errorMessage)
|
||||
|
||||
const logger = await import('@sim/logger')
|
||||
const mockLogger = logger.createLogger('ForgetPasswordTest')
|
||||
expect(mockLogger.error).toHaveBeenCalledWith('Error requesting password reset:', {
|
||||
error: expect.any(Error),
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle unknown error', async () => {
|
||||
setupAuthApiMocks()
|
||||
|
||||
vi.doMock('@/lib/auth', () => ({
|
||||
auth: {
|
||||
api: {
|
||||
forgetPassword: vi.fn().mockRejectedValue('Unknown error'),
|
||||
},
|
||||
},
|
||||
}))
|
||||
mockForgetPassword.mockRejectedValue('Unknown error')
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
email: 'test@example.com',
|
||||
})
|
||||
|
||||
const { POST } = await import('@/app/api/auth/forget-password/route')
|
||||
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(500)
|
||||
expect(data.message).toBe('Failed to send password reset email. Please try again later.')
|
||||
|
||||
const logger = await import('@sim/logger')
|
||||
const mockLogger = logger.createLogger('ForgetPasswordTest')
|
||||
expect(mockLogger.error).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,52 +3,81 @@
|
||||
*
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { createMockLogger, createMockRequest } from '@sim/testing'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createMockRequest } from '@sim/testing'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
describe('OAuth Connections API Route', () => {
|
||||
const mockGetSession = vi.fn()
|
||||
const mockDb = {
|
||||
const {
|
||||
mockGetSession,
|
||||
mockDb,
|
||||
mockLogger,
|
||||
mockParseProvider,
|
||||
mockEvaluateScopeCoverage,
|
||||
mockJwtDecode,
|
||||
mockEq,
|
||||
} = vi.hoisted(() => {
|
||||
const db = {
|
||||
select: vi.fn().mockReturnThis(),
|
||||
from: vi.fn().mockReturnThis(),
|
||||
where: vi.fn().mockReturnThis(),
|
||||
limit: vi.fn(),
|
||||
}
|
||||
const mockLogger = createMockLogger()
|
||||
const mockParseProvider = vi.fn()
|
||||
const mockEvaluateScopeCoverage = vi.fn()
|
||||
const logger = {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
trace: vi.fn(),
|
||||
fatal: vi.fn(),
|
||||
child: vi.fn(),
|
||||
}
|
||||
return {
|
||||
mockGetSession: vi.fn(),
|
||||
mockDb: db,
|
||||
mockLogger: logger,
|
||||
mockParseProvider: vi.fn(),
|
||||
mockEvaluateScopeCoverage: vi.fn(),
|
||||
mockJwtDecode: vi.fn(),
|
||||
mockEq: vi.fn((field: unknown, value: unknown) => ({ field, value, type: 'eq' })),
|
||||
}
|
||||
})
|
||||
|
||||
const mockUUID = 'mock-uuid-12345678-90ab-cdef-1234-567890abcdef'
|
||||
vi.mock('@/lib/auth', () => ({
|
||||
getSession: mockGetSession,
|
||||
}))
|
||||
|
||||
vi.mock('@sim/db', () => ({
|
||||
db: mockDb,
|
||||
account: { userId: 'userId', providerId: 'providerId' },
|
||||
user: { email: 'email', id: 'id' },
|
||||
eq: mockEq,
|
||||
}))
|
||||
|
||||
vi.mock('drizzle-orm', () => ({
|
||||
eq: mockEq,
|
||||
}))
|
||||
|
||||
vi.mock('jwt-decode', () => ({
|
||||
jwtDecode: mockJwtDecode,
|
||||
}))
|
||||
|
||||
vi.mock('@sim/logger', () => ({
|
||||
createLogger: vi.fn().mockReturnValue(mockLogger),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/oauth/utils', () => ({
|
||||
parseProvider: mockParseProvider,
|
||||
evaluateScopeCoverage: mockEvaluateScopeCoverage,
|
||||
}))
|
||||
|
||||
import { GET } from '@/app/api/auth/oauth/connections/route'
|
||||
|
||||
describe('OAuth Connections API Route', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
|
||||
vi.stubGlobal('crypto', {
|
||||
randomUUID: vi.fn().mockReturnValue(mockUUID),
|
||||
})
|
||||
|
||||
vi.doMock('@/lib/auth', () => ({
|
||||
getSession: mockGetSession,
|
||||
}))
|
||||
|
||||
vi.doMock('@sim/db', () => ({
|
||||
db: mockDb,
|
||||
account: { userId: 'userId', providerId: 'providerId' },
|
||||
user: { email: 'email', id: 'id' },
|
||||
eq: vi.fn((field, value) => ({ field, value, type: 'eq' })),
|
||||
}))
|
||||
|
||||
vi.doMock('drizzle-orm', () => ({
|
||||
eq: vi.fn((field, value) => ({ field, value, type: 'eq' })),
|
||||
}))
|
||||
|
||||
vi.doMock('jwt-decode', () => ({
|
||||
jwtDecode: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.doMock('@sim/logger', () => ({
|
||||
createLogger: vi.fn().mockReturnValue(mockLogger),
|
||||
}))
|
||||
mockDb.select.mockReturnThis()
|
||||
mockDb.from.mockReturnThis()
|
||||
mockDb.where.mockReturnThis()
|
||||
|
||||
mockParseProvider.mockImplementation((providerId: string) => ({
|
||||
baseProvider: providerId.split('-')[0] || providerId,
|
||||
@@ -64,15 +93,6 @@ describe('OAuth Connections API Route', () => {
|
||||
requiresReauthorization: false,
|
||||
})
|
||||
)
|
||||
|
||||
vi.doMock('@/lib/oauth/utils', () => ({
|
||||
parseProvider: mockParseProvider,
|
||||
evaluateScopeCoverage: mockEvaluateScopeCoverage,
|
||||
}))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should return connections successfully', async () => {
|
||||
@@ -111,7 +131,6 @@ describe('OAuth Connections API Route', () => {
|
||||
mockDb.limit.mockResolvedValueOnce(mockUserRecord)
|
||||
|
||||
const req = createMockRequest('GET')
|
||||
const { GET } = await import('@/app/api/auth/oauth/connections/route')
|
||||
|
||||
const response = await GET(req)
|
||||
const data = await response.json()
|
||||
@@ -136,7 +155,6 @@ describe('OAuth Connections API Route', () => {
|
||||
mockGetSession.mockResolvedValueOnce(null)
|
||||
|
||||
const req = createMockRequest('GET')
|
||||
const { GET } = await import('@/app/api/auth/oauth/connections/route')
|
||||
|
||||
const response = await GET(req)
|
||||
const data = await response.json()
|
||||
@@ -161,7 +179,6 @@ describe('OAuth Connections API Route', () => {
|
||||
mockDb.limit.mockResolvedValueOnce([])
|
||||
|
||||
const req = createMockRequest('GET')
|
||||
const { GET } = await import('@/app/api/auth/oauth/connections/route')
|
||||
|
||||
const response = await GET(req)
|
||||
const data = await response.json()
|
||||
@@ -180,7 +197,6 @@ describe('OAuth Connections API Route', () => {
|
||||
mockDb.where.mockRejectedValueOnce(new Error('Database error'))
|
||||
|
||||
const req = createMockRequest('GET')
|
||||
const { GET } = await import('@/app/api/auth/oauth/connections/route')
|
||||
|
||||
const response = await GET(req)
|
||||
const data = await response.json()
|
||||
@@ -191,9 +207,6 @@ describe('OAuth Connections API Route', () => {
|
||||
})
|
||||
|
||||
it('should decode ID token for display name', async () => {
|
||||
const { jwtDecode } = await import('jwt-decode')
|
||||
const mockJwtDecode = jwtDecode as any
|
||||
|
||||
mockGetSession.mockResolvedValueOnce({
|
||||
user: { id: 'user-123' },
|
||||
})
|
||||
@@ -224,7 +237,6 @@ describe('OAuth Connections API Route', () => {
|
||||
mockDb.limit.mockResolvedValueOnce([])
|
||||
|
||||
const req = createMockRequest('GET')
|
||||
const { GET } = await import('@/app/api/auth/oauth/connections/route')
|
||||
|
||||
const response = await GET(req)
|
||||
const data = await response.json()
|
||||
|
||||
@@ -4,66 +4,89 @@
|
||||
* @vitest-environment node
|
||||
*/
|
||||
|
||||
import { createMockLogger } from '@sim/testing'
|
||||
import { NextRequest } from 'next/server'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const { mockCheckSessionOrInternalAuth, mockEvaluateScopeCoverage, mockLogger } = vi.hoisted(() => {
|
||||
const logger = {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
trace: vi.fn(),
|
||||
fatal: vi.fn(),
|
||||
child: vi.fn(),
|
||||
}
|
||||
return {
|
||||
mockCheckSessionOrInternalAuth: vi.fn(),
|
||||
mockEvaluateScopeCoverage: vi.fn(),
|
||||
mockLogger: logger,
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/lib/auth/hybrid', () => ({
|
||||
checkSessionOrInternalAuth: mockCheckSessionOrInternalAuth,
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/oauth', () => ({
|
||||
evaluateScopeCoverage: mockEvaluateScopeCoverage,
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/core/utils/request', () => ({
|
||||
generateRequestId: vi.fn().mockReturnValue('mock-request-id'),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/credentials/oauth', () => ({
|
||||
syncWorkspaceOAuthCredentialsForUser: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/workflows/utils', () => ({
|
||||
authorizeWorkflowByWorkspacePermission: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/workspaces/permissions/utils', () => ({
|
||||
checkWorkspaceAccess: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@sim/db/schema', () => ({
|
||||
account: {
|
||||
userId: 'userId',
|
||||
providerId: 'providerId',
|
||||
id: 'id',
|
||||
scope: 'scope',
|
||||
updatedAt: 'updatedAt',
|
||||
},
|
||||
credential: {
|
||||
id: 'id',
|
||||
workspaceId: 'workspaceId',
|
||||
type: 'type',
|
||||
displayName: 'displayName',
|
||||
providerId: 'providerId',
|
||||
accountId: 'accountId',
|
||||
},
|
||||
credentialMember: {
|
||||
id: 'id',
|
||||
credentialId: 'credentialId',
|
||||
userId: 'userId',
|
||||
status: 'status',
|
||||
},
|
||||
user: { email: 'email', id: 'id' },
|
||||
}))
|
||||
|
||||
vi.mock('@sim/logger', () => ({
|
||||
createLogger: vi.fn().mockReturnValue(mockLogger),
|
||||
}))
|
||||
|
||||
import { GET } from '@/app/api/auth/oauth/credentials/route'
|
||||
|
||||
describe('OAuth Credentials API Route', () => {
|
||||
const mockGetSession = vi.fn()
|
||||
const mockParseProvider = vi.fn()
|
||||
const mockEvaluateScopeCoverage = vi.fn()
|
||||
const mockDb = {
|
||||
select: vi.fn().mockReturnThis(),
|
||||
from: vi.fn().mockReturnThis(),
|
||||
where: vi.fn().mockReturnThis(),
|
||||
limit: vi.fn(),
|
||||
}
|
||||
const mockLogger = createMockLogger()
|
||||
|
||||
const mockUUID = 'mock-uuid-12345678-90ab-cdef-1234-567890abcdef'
|
||||
|
||||
function createMockRequestWithQuery(method = 'GET', queryParams = ''): NextRequest {
|
||||
const url = `http://localhost:3000/api/auth/oauth/credentials${queryParams}`
|
||||
return new NextRequest(new URL(url), { method })
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
|
||||
vi.stubGlobal('crypto', {
|
||||
randomUUID: vi.fn().mockReturnValue(mockUUID),
|
||||
})
|
||||
|
||||
vi.doMock('@/lib/auth', () => ({
|
||||
getSession: mockGetSession,
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/oauth/utils', () => ({
|
||||
parseProvider: mockParseProvider,
|
||||
evaluateScopeCoverage: mockEvaluateScopeCoverage,
|
||||
}))
|
||||
|
||||
vi.doMock('@sim/db', () => ({
|
||||
db: mockDb,
|
||||
}))
|
||||
|
||||
vi.doMock('@sim/db/schema', () => ({
|
||||
account: { userId: 'userId', providerId: 'providerId' },
|
||||
user: { email: 'email', id: 'id' },
|
||||
}))
|
||||
|
||||
vi.doMock('drizzle-orm', () => ({
|
||||
and: vi.fn((...conditions) => ({ conditions, type: 'and' })),
|
||||
eq: vi.fn((field, value) => ({ field, value, type: 'eq' })),
|
||||
}))
|
||||
|
||||
vi.doMock('@sim/logger', () => ({
|
||||
createLogger: vi.fn().mockReturnValue(mockLogger),
|
||||
}))
|
||||
|
||||
mockParseProvider.mockImplementation((providerId: string) => ({
|
||||
baseProvider: providerId.split('-')[0] || providerId,
|
||||
}))
|
||||
vi.clearAllMocks()
|
||||
|
||||
mockEvaluateScopeCoverage.mockImplementation(
|
||||
(_providerId: string, grantedScopes: string[]) => ({
|
||||
@@ -76,17 +99,14 @@ describe('OAuth Credentials API Route', () => {
|
||||
)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should handle unauthenticated user', async () => {
|
||||
mockGetSession.mockResolvedValueOnce(null)
|
||||
mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
|
||||
success: false,
|
||||
error: 'Authentication required',
|
||||
})
|
||||
|
||||
const req = createMockRequestWithQuery('GET', '?provider=google')
|
||||
|
||||
const { GET } = await import('@/app/api/auth/oauth/credentials/route')
|
||||
|
||||
const response = await GET(req)
|
||||
const data = await response.json()
|
||||
|
||||
@@ -96,14 +116,14 @@ describe('OAuth Credentials API Route', () => {
|
||||
})
|
||||
|
||||
it('should handle missing provider parameter', async () => {
|
||||
mockGetSession.mockResolvedValueOnce({
|
||||
user: { id: 'user-123' },
|
||||
mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
|
||||
success: true,
|
||||
userId: 'user-123',
|
||||
authType: 'session',
|
||||
})
|
||||
|
||||
const req = createMockRequestWithQuery('GET')
|
||||
|
||||
const { GET } = await import('@/app/api/auth/oauth/credentials/route')
|
||||
|
||||
const response = await GET(req)
|
||||
const data = await response.json()
|
||||
|
||||
@@ -113,22 +133,14 @@ describe('OAuth Credentials API Route', () => {
|
||||
})
|
||||
|
||||
it('should handle no credentials found', async () => {
|
||||
mockGetSession.mockResolvedValueOnce({
|
||||
user: { id: 'user-123' },
|
||||
mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
|
||||
success: true,
|
||||
userId: 'user-123',
|
||||
authType: 'session',
|
||||
})
|
||||
|
||||
mockParseProvider.mockReturnValueOnce({
|
||||
baseProvider: 'github',
|
||||
})
|
||||
|
||||
mockDb.select.mockReturnValueOnce(mockDb)
|
||||
mockDb.from.mockReturnValueOnce(mockDb)
|
||||
mockDb.where.mockResolvedValueOnce([])
|
||||
|
||||
const req = createMockRequestWithQuery('GET', '?provider=github')
|
||||
|
||||
const { GET } = await import('@/app/api/auth/oauth/credentials/route')
|
||||
|
||||
const response = await GET(req)
|
||||
const data = await response.json()
|
||||
|
||||
@@ -137,14 +149,14 @@ describe('OAuth Credentials API Route', () => {
|
||||
})
|
||||
|
||||
it('should return empty credentials when no workspace context', async () => {
|
||||
mockGetSession.mockResolvedValueOnce({
|
||||
user: { id: 'user-123' },
|
||||
mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
|
||||
success: true,
|
||||
userId: 'user-123',
|
||||
authType: 'session',
|
||||
})
|
||||
|
||||
const req = createMockRequestWithQuery('GET', '?provider=google-email')
|
||||
|
||||
const { GET } = await import('@/app/api/auth/oauth/credentials/route')
|
||||
|
||||
const response = await GET(req)
|
||||
const data = await response.json()
|
||||
|
||||
|
||||
@@ -3,76 +3,102 @@
|
||||
*
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { auditMock, createMockLogger, createMockRequest } from '@sim/testing'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createMockRequest } from '@sim/testing'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
describe('OAuth Disconnect API Route', () => {
|
||||
const mockGetSession = vi.fn()
|
||||
const mockSelectChain = {
|
||||
from: vi.fn().mockReturnThis(),
|
||||
innerJoin: vi.fn().mockReturnThis(),
|
||||
where: vi.fn().mockResolvedValue([]),
|
||||
}
|
||||
const mockDb = {
|
||||
delete: vi.fn().mockReturnThis(),
|
||||
where: vi.fn(),
|
||||
select: vi.fn().mockReturnValue(mockSelectChain),
|
||||
}
|
||||
const mockLogger = createMockLogger()
|
||||
const mockSyncAllWebhooksForCredentialSet = vi.fn().mockResolvedValue({})
|
||||
|
||||
const mockUUID = 'mock-uuid-12345678-90ab-cdef-1234-567890abcdef'
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
|
||||
vi.stubGlobal('crypto', {
|
||||
randomUUID: vi.fn().mockReturnValue(mockUUID),
|
||||
})
|
||||
|
||||
vi.doMock('@/lib/auth', () => ({
|
||||
getSession: mockGetSession,
|
||||
}))
|
||||
|
||||
vi.doMock('@sim/db', () => ({
|
||||
db: mockDb,
|
||||
}))
|
||||
|
||||
vi.doMock('@sim/db/schema', () => ({
|
||||
account: { userId: 'userId', providerId: 'providerId' },
|
||||
credentialSetMember: {
|
||||
id: 'id',
|
||||
credentialSetId: 'credentialSetId',
|
||||
userId: 'userId',
|
||||
status: 'status',
|
||||
},
|
||||
credentialSet: { id: 'id', providerId: 'providerId' },
|
||||
}))
|
||||
|
||||
vi.doMock('drizzle-orm', () => ({
|
||||
and: vi.fn((...conditions) => ({ conditions, type: 'and' })),
|
||||
eq: vi.fn((field, value) => ({ field, value, type: 'eq' })),
|
||||
like: vi.fn((field, value) => ({ field, value, type: 'like' })),
|
||||
or: vi.fn((...conditions) => ({ conditions, type: 'or' })),
|
||||
}))
|
||||
|
||||
vi.doMock('@sim/logger', () => ({
|
||||
createLogger: vi.fn().mockReturnValue(mockLogger),
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/core/utils/request', () => ({
|
||||
generateRequestId: vi.fn().mockReturnValue('test-request-id'),
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/webhooks/utils.server', () => ({
|
||||
syncAllWebhooksForCredentialSet: mockSyncAllWebhooksForCredentialSet,
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/audit/log', () => auditMock)
|
||||
const { mockGetSession, mockDb, mockSelectChain, mockLogger, mockSyncAllWebhooksForCredentialSet } =
|
||||
vi.hoisted(() => {
|
||||
const selectChain = {
|
||||
from: vi.fn().mockReturnThis(),
|
||||
innerJoin: vi.fn().mockReturnThis(),
|
||||
where: vi.fn().mockResolvedValue([]),
|
||||
}
|
||||
const db = {
|
||||
delete: vi.fn().mockReturnThis(),
|
||||
where: vi.fn(),
|
||||
select: vi.fn().mockReturnValue(selectChain),
|
||||
}
|
||||
const logger = {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
trace: vi.fn(),
|
||||
fatal: vi.fn(),
|
||||
child: vi.fn(),
|
||||
}
|
||||
return {
|
||||
mockGetSession: vi.fn(),
|
||||
mockDb: db,
|
||||
mockSelectChain: selectChain,
|
||||
mockLogger: logger,
|
||||
mockSyncAllWebhooksForCredentialSet: vi.fn().mockResolvedValue({}),
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.mock('@/lib/auth', () => ({
|
||||
getSession: mockGetSession,
|
||||
}))
|
||||
|
||||
vi.mock('@sim/db', () => ({
|
||||
db: mockDb,
|
||||
}))
|
||||
|
||||
vi.mock('@sim/db/schema', () => ({
|
||||
account: { userId: 'userId', providerId: 'providerId' },
|
||||
credentialSetMember: {
|
||||
id: 'id',
|
||||
credentialSetId: 'credentialSetId',
|
||||
userId: 'userId',
|
||||
status: 'status',
|
||||
},
|
||||
credentialSet: { id: 'id', providerId: 'providerId' },
|
||||
}))
|
||||
|
||||
vi.mock('drizzle-orm', () => ({
|
||||
and: vi.fn((...conditions: unknown[]) => ({ conditions, type: 'and' })),
|
||||
eq: vi.fn((field: unknown, value: unknown) => ({ field, value, type: 'eq' })),
|
||||
like: vi.fn((field: unknown, value: unknown) => ({ field, value, type: 'like' })),
|
||||
or: vi.fn((...conditions: unknown[]) => ({ conditions, type: 'or' })),
|
||||
}))
|
||||
|
||||
vi.mock('@sim/logger', () => ({
|
||||
createLogger: vi.fn().mockReturnValue(mockLogger),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/core/utils/request', () => ({
|
||||
generateRequestId: vi.fn().mockReturnValue('test-request-id'),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/webhooks/utils.server', () => ({
|
||||
syncAllWebhooksForCredentialSet: mockSyncAllWebhooksForCredentialSet,
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/audit/log', () => ({
|
||||
recordAudit: vi.fn(),
|
||||
AuditAction: {
|
||||
CREDENTIAL_SET_CREATED: 'credential_set.created',
|
||||
CREDENTIAL_SET_UPDATED: 'credential_set.updated',
|
||||
CREDENTIAL_SET_DELETED: 'credential_set.deleted',
|
||||
OAUTH_CONNECTED: 'oauth.connected',
|
||||
OAUTH_DISCONNECTED: 'oauth.disconnected',
|
||||
},
|
||||
AuditResourceType: {
|
||||
CREDENTIAL_SET: 'credential_set',
|
||||
OAUTH_CONNECTION: 'oauth_connection',
|
||||
},
|
||||
}))
|
||||
|
||||
import { POST } from '@/app/api/auth/oauth/disconnect/route'
|
||||
|
||||
describe('OAuth Disconnect API Route', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
mockDb.delete.mockReturnThis()
|
||||
mockSelectChain.from.mockReturnThis()
|
||||
mockSelectChain.innerJoin.mockReturnThis()
|
||||
mockSelectChain.where.mockResolvedValue([])
|
||||
})
|
||||
|
||||
it('should disconnect provider successfully', async () => {
|
||||
@@ -87,8 +113,6 @@ describe('OAuth Disconnect API Route', () => {
|
||||
provider: 'google',
|
||||
})
|
||||
|
||||
const { POST } = await import('@/app/api/auth/oauth/disconnect/route')
|
||||
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
@@ -110,8 +134,6 @@ describe('OAuth Disconnect API Route', () => {
|
||||
providerId: 'google-email',
|
||||
})
|
||||
|
||||
const { POST } = await import('@/app/api/auth/oauth/disconnect/route')
|
||||
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
@@ -127,8 +149,6 @@ describe('OAuth Disconnect API Route', () => {
|
||||
provider: 'google',
|
||||
})
|
||||
|
||||
const { POST } = await import('@/app/api/auth/oauth/disconnect/route')
|
||||
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
@@ -144,8 +164,6 @@ describe('OAuth Disconnect API Route', () => {
|
||||
|
||||
const req = createMockRequest('POST', {})
|
||||
|
||||
const { POST } = await import('@/app/api/auth/oauth/disconnect/route')
|
||||
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
@@ -166,8 +184,6 @@ describe('OAuth Disconnect API Route', () => {
|
||||
provider: 'google',
|
||||
})
|
||||
|
||||
const { POST } = await import('@/app/api/auth/oauth/disconnect/route')
|
||||
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
|
||||
@@ -3,48 +3,63 @@
|
||||
*
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { createMockLogger, createMockRequest, mockHybridAuth } from '@sim/testing'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createMockRequest } from '@sim/testing'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const {
|
||||
mockGetUserId,
|
||||
mockGetCredential,
|
||||
mockRefreshTokenIfNeeded,
|
||||
mockGetOAuthToken,
|
||||
mockAuthorizeCredentialUse,
|
||||
mockCheckSessionOrInternalAuth,
|
||||
mockLogger,
|
||||
} = vi.hoisted(() => {
|
||||
const logger = {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
trace: vi.fn(),
|
||||
fatal: vi.fn(),
|
||||
child: vi.fn(),
|
||||
}
|
||||
return {
|
||||
mockGetUserId: vi.fn(),
|
||||
mockGetCredential: vi.fn(),
|
||||
mockRefreshTokenIfNeeded: vi.fn(),
|
||||
mockGetOAuthToken: vi.fn(),
|
||||
mockAuthorizeCredentialUse: vi.fn(),
|
||||
mockCheckSessionOrInternalAuth: vi.fn(),
|
||||
mockLogger: logger,
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/app/api/auth/oauth/utils', () => ({
|
||||
getUserId: mockGetUserId,
|
||||
getCredential: mockGetCredential,
|
||||
refreshTokenIfNeeded: mockRefreshTokenIfNeeded,
|
||||
getOAuthToken: mockGetOAuthToken,
|
||||
}))
|
||||
|
||||
vi.mock('@sim/logger', () => ({
|
||||
createLogger: vi.fn().mockReturnValue(mockLogger),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/auth/credential-access', () => ({
|
||||
authorizeCredentialUse: mockAuthorizeCredentialUse,
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/auth/hybrid', () => ({
|
||||
checkHybridAuth: vi.fn(),
|
||||
checkSessionOrInternalAuth: mockCheckSessionOrInternalAuth,
|
||||
checkInternalAuth: vi.fn(),
|
||||
}))
|
||||
|
||||
import { GET, POST } from '@/app/api/auth/oauth/token/route'
|
||||
|
||||
describe('OAuth Token API Routes', () => {
|
||||
const mockGetUserId = vi.fn()
|
||||
const mockGetCredential = vi.fn()
|
||||
const mockRefreshTokenIfNeeded = vi.fn()
|
||||
const mockGetOAuthToken = vi.fn()
|
||||
const mockAuthorizeCredentialUse = vi.fn()
|
||||
let mockCheckSessionOrInternalAuth: ReturnType<typeof vi.fn>
|
||||
|
||||
const mockLogger = createMockLogger()
|
||||
|
||||
const mockUUID = 'mock-uuid-12345678-90ab-cdef-1234-567890abcdef'
|
||||
const mockRequestId = mockUUID.slice(0, 8)
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
|
||||
vi.stubGlobal('crypto', {
|
||||
randomUUID: vi.fn().mockReturnValue(mockUUID),
|
||||
})
|
||||
|
||||
vi.doMock('@/app/api/auth/oauth/utils', () => ({
|
||||
getUserId: mockGetUserId,
|
||||
getCredential: mockGetCredential,
|
||||
refreshTokenIfNeeded: mockRefreshTokenIfNeeded,
|
||||
getOAuthToken: mockGetOAuthToken,
|
||||
}))
|
||||
|
||||
vi.doMock('@sim/logger', () => ({
|
||||
createLogger: vi.fn().mockReturnValue(mockLogger),
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/auth/credential-access', () => ({
|
||||
authorizeCredentialUse: mockAuthorizeCredentialUse,
|
||||
}))
|
||||
|
||||
;({ mockCheckSessionOrInternalAuth } = mockHybridAuth())
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
@@ -75,8 +90,6 @@ describe('OAuth Token API Routes', () => {
|
||||
credentialId: 'credential-id',
|
||||
})
|
||||
|
||||
const { POST } = await import('@/app/api/auth/oauth/token/route')
|
||||
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
@@ -112,8 +125,6 @@ describe('OAuth Token API Routes', () => {
|
||||
workflowId: 'workflow-id',
|
||||
})
|
||||
|
||||
const { POST } = await import('@/app/api/auth/oauth/token/route')
|
||||
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
@@ -127,8 +138,6 @@ describe('OAuth Token API Routes', () => {
|
||||
it('should handle missing credentialId', async () => {
|
||||
const req = createMockRequest('POST', {})
|
||||
|
||||
const { POST } = await import('@/app/api/auth/oauth/token/route')
|
||||
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
@@ -150,8 +159,6 @@ describe('OAuth Token API Routes', () => {
|
||||
credentialId: 'credential-id',
|
||||
})
|
||||
|
||||
const { POST } = await import('@/app/api/auth/oauth/token/route')
|
||||
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
@@ -167,8 +174,6 @@ describe('OAuth Token API Routes', () => {
|
||||
workflowId: 'nonexistent-workflow-id',
|
||||
})
|
||||
|
||||
const { POST } = await import('@/app/api/auth/oauth/token/route')
|
||||
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
@@ -188,8 +193,6 @@ describe('OAuth Token API Routes', () => {
|
||||
credentialId: 'nonexistent-credential-id',
|
||||
})
|
||||
|
||||
const { POST } = await import('@/app/api/auth/oauth/token/route')
|
||||
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
@@ -217,8 +220,6 @@ describe('OAuth Token API Routes', () => {
|
||||
credentialId: 'credential-id',
|
||||
})
|
||||
|
||||
const { POST } = await import('@/app/api/auth/oauth/token/route')
|
||||
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
@@ -238,8 +239,6 @@ describe('OAuth Token API Routes', () => {
|
||||
providerId: 'google',
|
||||
})
|
||||
|
||||
const { POST } = await import('@/app/api/auth/oauth/token/route')
|
||||
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
@@ -260,8 +259,6 @@ describe('OAuth Token API Routes', () => {
|
||||
providerId: 'google',
|
||||
})
|
||||
|
||||
const { POST } = await import('@/app/api/auth/oauth/token/route')
|
||||
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
@@ -282,8 +279,6 @@ describe('OAuth Token API Routes', () => {
|
||||
providerId: 'google',
|
||||
})
|
||||
|
||||
const { POST } = await import('@/app/api/auth/oauth/token/route')
|
||||
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
@@ -305,8 +300,6 @@ describe('OAuth Token API Routes', () => {
|
||||
providerId: 'google',
|
||||
})
|
||||
|
||||
const { POST } = await import('@/app/api/auth/oauth/token/route')
|
||||
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
@@ -328,8 +321,6 @@ describe('OAuth Token API Routes', () => {
|
||||
providerId: 'nonexistent-provider',
|
||||
})
|
||||
|
||||
const { POST } = await import('@/app/api/auth/oauth/token/route')
|
||||
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
@@ -366,8 +357,6 @@ describe('OAuth Token API Routes', () => {
|
||||
'http://localhost:3000/api/auth/oauth/token?credentialId=credential-id'
|
||||
)
|
||||
|
||||
const { GET } = await import('@/app/api/auth/oauth/token/route')
|
||||
|
||||
const response = await GET(req as any)
|
||||
const data = await response.json()
|
||||
|
||||
@@ -382,8 +371,6 @@ describe('OAuth Token API Routes', () => {
|
||||
it('should handle missing credentialId', async () => {
|
||||
const req = new Request('http://localhost:3000/api/auth/oauth/token')
|
||||
|
||||
const { GET } = await import('@/app/api/auth/oauth/token/route')
|
||||
|
||||
const response = await GET(req as any)
|
||||
const data = await response.json()
|
||||
|
||||
@@ -402,8 +389,6 @@ describe('OAuth Token API Routes', () => {
|
||||
'http://localhost:3000/api/auth/oauth/token?credentialId=credential-id'
|
||||
)
|
||||
|
||||
const { GET } = await import('@/app/api/auth/oauth/token/route')
|
||||
|
||||
const response = await GET(req as any)
|
||||
const data = await response.json()
|
||||
|
||||
@@ -424,8 +409,6 @@ describe('OAuth Token API Routes', () => {
|
||||
'http://localhost:3000/api/auth/oauth/token?credentialId=nonexistent-credential-id'
|
||||
)
|
||||
|
||||
const { GET } = await import('@/app/api/auth/oauth/token/route')
|
||||
|
||||
const response = await GET(req as any)
|
||||
const data = await response.json()
|
||||
|
||||
@@ -451,8 +434,6 @@ describe('OAuth Token API Routes', () => {
|
||||
'http://localhost:3000/api/auth/oauth/token?credentialId=credential-id'
|
||||
)
|
||||
|
||||
const { GET } = await import('@/app/api/auth/oauth/token/route')
|
||||
|
||||
const response = await GET(req as any)
|
||||
const data = await response.json()
|
||||
|
||||
@@ -480,8 +461,6 @@ describe('OAuth Token API Routes', () => {
|
||||
'http://localhost:3000/api/auth/oauth/token?credentialId=credential-id'
|
||||
)
|
||||
|
||||
const { GET } = await import('@/app/api/auth/oauth/token/route')
|
||||
|
||||
const response = await GET(req as any)
|
||||
const data = await response.json()
|
||||
|
||||
|
||||
@@ -3,59 +3,42 @@
|
||||
*
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import {
|
||||
createMockRequest,
|
||||
mockConsoleLogger,
|
||||
mockCryptoUuid,
|
||||
mockDrizzleOrm,
|
||||
mockUuid,
|
||||
setupCommonApiMocks,
|
||||
} from '@sim/testing'
|
||||
import { createMockRequest } from '@sim/testing'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
/** Setup auth API mocks for testing authentication routes */
|
||||
function setupAuthApiMocks(
|
||||
options: {
|
||||
operations?: {
|
||||
forgetPassword?: { success?: boolean; error?: string }
|
||||
resetPassword?: { success?: boolean; error?: string }
|
||||
}
|
||||
} = {}
|
||||
) {
|
||||
setupCommonApiMocks()
|
||||
mockUuid()
|
||||
mockCryptoUuid()
|
||||
mockConsoleLogger()
|
||||
mockDrizzleOrm()
|
||||
|
||||
const { operations = {} } = options
|
||||
const defaultOperations = {
|
||||
forgetPassword: { success: true, error: 'Forget password error', ...operations.forgetPassword },
|
||||
resetPassword: { success: true, error: 'Reset password error', ...operations.resetPassword },
|
||||
const { mockResetPassword, mockLogger } = vi.hoisted(() => {
|
||||
const logger = {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
trace: vi.fn(),
|
||||
fatal: vi.fn(),
|
||||
child: vi.fn(),
|
||||
}
|
||||
|
||||
const createAuthMethod = (config: { success?: boolean; error?: string }) => {
|
||||
return vi.fn().mockImplementation(() => {
|
||||
if (config.success) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
return Promise.reject(new Error(config.error))
|
||||
})
|
||||
return {
|
||||
mockResetPassword: vi.fn(),
|
||||
mockLogger: logger,
|
||||
}
|
||||
})
|
||||
|
||||
vi.doMock('@/lib/auth', () => ({
|
||||
auth: {
|
||||
api: {
|
||||
forgetPassword: createAuthMethod(defaultOperations.forgetPassword),
|
||||
resetPassword: createAuthMethod(defaultOperations.resetPassword),
|
||||
},
|
||||
vi.mock('@/lib/auth', () => ({
|
||||
auth: {
|
||||
api: {
|
||||
resetPassword: mockResetPassword,
|
||||
},
|
||||
}))
|
||||
}
|
||||
},
|
||||
}))
|
||||
vi.mock('@sim/logger', () => ({
|
||||
createLogger: vi.fn().mockReturnValue(mockLogger),
|
||||
}))
|
||||
|
||||
import { POST } from '@/app/api/auth/reset-password/route'
|
||||
|
||||
describe('Reset Password API Route', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
mockResetPassword.mockResolvedValue(undefined)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
@@ -63,27 +46,18 @@ describe('Reset Password API Route', () => {
|
||||
})
|
||||
|
||||
it('should reset password successfully', async () => {
|
||||
setupAuthApiMocks({
|
||||
operations: {
|
||||
resetPassword: { success: true },
|
||||
},
|
||||
})
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
token: 'valid-reset-token',
|
||||
newPassword: 'newSecurePassword123!',
|
||||
})
|
||||
|
||||
const { POST } = await import('@/app/api/auth/reset-password/route')
|
||||
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(data.success).toBe(true)
|
||||
|
||||
const auth = await import('@/lib/auth')
|
||||
expect(auth.auth.api.resetPassword).toHaveBeenCalledWith({
|
||||
expect(mockResetPassword).toHaveBeenCalledWith({
|
||||
body: {
|
||||
token: 'valid-reset-token',
|
||||
newPassword: 'newSecurePassword123!',
|
||||
@@ -93,133 +67,92 @@ describe('Reset Password API Route', () => {
|
||||
})
|
||||
|
||||
it('should handle missing token', async () => {
|
||||
setupAuthApiMocks()
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
newPassword: 'newSecurePassword123',
|
||||
})
|
||||
|
||||
const { POST } = await import('@/app/api/auth/reset-password/route')
|
||||
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
expect(data.message).toBe('Token is required')
|
||||
|
||||
const auth = await import('@/lib/auth')
|
||||
expect(auth.auth.api.resetPassword).not.toHaveBeenCalled()
|
||||
expect(mockResetPassword).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle missing new password', async () => {
|
||||
setupAuthApiMocks()
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
token: 'valid-reset-token',
|
||||
})
|
||||
|
||||
const { POST } = await import('./route')
|
||||
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
expect(data.message).toBe('Password is required')
|
||||
|
||||
const auth = await import('@/lib/auth')
|
||||
expect(auth.auth.api.resetPassword).not.toHaveBeenCalled()
|
||||
expect(mockResetPassword).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle empty token', async () => {
|
||||
setupAuthApiMocks()
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
token: '',
|
||||
newPassword: 'newSecurePassword123',
|
||||
})
|
||||
|
||||
const { POST } = await import('@/app/api/auth/reset-password/route')
|
||||
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
expect(data.message).toBe('Token is required')
|
||||
|
||||
const auth = await import('@/lib/auth')
|
||||
expect(auth.auth.api.resetPassword).not.toHaveBeenCalled()
|
||||
expect(mockResetPassword).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle empty new password', async () => {
|
||||
setupAuthApiMocks()
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
token: 'valid-reset-token',
|
||||
newPassword: '',
|
||||
})
|
||||
|
||||
const { POST } = await import('@/app/api/auth/reset-password/route')
|
||||
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
expect(data.message).toBe('Password must be at least 8 characters long')
|
||||
|
||||
const auth = await import('@/lib/auth')
|
||||
expect(auth.auth.api.resetPassword).not.toHaveBeenCalled()
|
||||
expect(mockResetPassword).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle auth service error with message', async () => {
|
||||
const errorMessage = 'Invalid or expired token'
|
||||
|
||||
setupAuthApiMocks({
|
||||
operations: {
|
||||
resetPassword: {
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
},
|
||||
},
|
||||
})
|
||||
mockResetPassword.mockRejectedValue(new Error(errorMessage))
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
token: 'invalid-token',
|
||||
newPassword: 'newSecurePassword123!',
|
||||
})
|
||||
|
||||
const { POST } = await import('@/app/api/auth/reset-password/route')
|
||||
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(500)
|
||||
expect(data.message).toBe(errorMessage)
|
||||
|
||||
const logger = await import('@sim/logger')
|
||||
const mockLogger = logger.createLogger('PasswordResetAPI')
|
||||
expect(mockLogger.error).toHaveBeenCalledWith('Error during password reset:', {
|
||||
error: expect.any(Error),
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle unknown error', async () => {
|
||||
setupAuthApiMocks()
|
||||
|
||||
vi.doMock('@/lib/auth', () => ({
|
||||
auth: {
|
||||
api: {
|
||||
resetPassword: vi.fn().mockRejectedValue('Unknown error'),
|
||||
},
|
||||
},
|
||||
}))
|
||||
mockResetPassword.mockRejectedValue('Unknown error')
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
token: 'valid-reset-token',
|
||||
newPassword: 'newSecurePassword123!',
|
||||
})
|
||||
|
||||
const { POST } = await import('@/app/api/auth/reset-password/route')
|
||||
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
@@ -228,8 +161,6 @@ describe('Reset Password API Route', () => {
|
||||
'Failed to reset password. Please try again or request a new reset link.'
|
||||
)
|
||||
|
||||
const logger = await import('@sim/logger')
|
||||
const mockLogger = logger.createLogger('PasswordResetAPI')
|
||||
expect(mockLogger.error).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -18,7 +18,7 @@ const UpdateCostSchema = z.object({
|
||||
model: z.string().min(1, 'Model is required'),
|
||||
inputTokens: z.number().min(0).default(0),
|
||||
outputTokens: z.number().min(0).default(0),
|
||||
source: z.enum(['copilot', 'workspace-chat', 'mcp_copilot']).default('copilot'),
|
||||
source: z.enum(['copilot', 'mcp_copilot']).default('copilot'),
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -98,22 +98,19 @@ export async function POST(req: NextRequest) {
|
||||
return NextResponse.json({ error: 'User stats record not found' }, { status: 500 })
|
||||
}
|
||||
|
||||
const totalTokens = inputTokens + outputTokens
|
||||
|
||||
const updateFields: Record<string, unknown> = {
|
||||
totalCost: sql`total_cost + ${cost}`,
|
||||
currentPeriodCost: sql`current_period_cost + ${cost}`,
|
||||
totalCopilotCost: sql`total_copilot_cost + ${cost}`,
|
||||
currentPeriodCopilotCost: sql`current_period_copilot_cost + ${cost}`,
|
||||
totalCopilotCalls: sql`total_copilot_calls + 1`,
|
||||
totalCopilotTokens: sql`total_copilot_tokens + ${totalTokens}`,
|
||||
lastActive: new Date(),
|
||||
}
|
||||
|
||||
// Also increment MCP-specific counters when source is mcp_copilot
|
||||
if (isMcp) {
|
||||
updateFields.totalMcpCopilotCost = sql`total_mcp_copilot_cost + ${cost}`
|
||||
updateFields.currentPeriodMcpCopilotCost = sql`current_period_mcp_copilot_cost + ${cost}`
|
||||
updateFields.totalMcpCopilotCalls = sql`total_mcp_copilot_calls + 1`
|
||||
}
|
||||
|
||||
await db.update(userStats).set(updateFields).where(eq(userStats.userId, userId))
|
||||
@@ -124,10 +121,10 @@ export async function POST(req: NextRequest) {
|
||||
source,
|
||||
})
|
||||
|
||||
// Log usage for complete audit trail with the original source for visibility
|
||||
// Log usage for complete audit trail
|
||||
await logModelUsage({
|
||||
userId,
|
||||
source,
|
||||
source: isMcp ? 'mcp_copilot' : 'copilot',
|
||||
model,
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
|
||||
@@ -6,21 +6,38 @@
|
||||
import { NextRequest } from 'next/server'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
describe('Chat OTP API Route', () => {
|
||||
const mockEmail = 'test@example.com'
|
||||
const mockChatId = 'chat-123'
|
||||
const mockIdentifier = 'test-chat'
|
||||
const mockOTP = '123456'
|
||||
|
||||
const {
|
||||
mockRedisSet,
|
||||
mockRedisGet,
|
||||
mockRedisDel,
|
||||
mockGetRedisClient,
|
||||
mockRedisClient,
|
||||
mockDbSelect,
|
||||
mockDbInsert,
|
||||
mockDbDelete,
|
||||
mockSendEmail,
|
||||
mockRenderOTPEmail,
|
||||
mockAddCorsHeaders,
|
||||
mockCreateSuccessResponse,
|
||||
mockCreateErrorResponse,
|
||||
mockSetChatAuthCookie,
|
||||
mockGenerateRequestId,
|
||||
mockGetStorageMethod,
|
||||
mockZodParse,
|
||||
mockGetEnv,
|
||||
} = vi.hoisted(() => {
|
||||
const mockRedisSet = vi.fn()
|
||||
const mockRedisGet = vi.fn()
|
||||
const mockRedisDel = vi.fn()
|
||||
const mockRedisClient = {
|
||||
set: mockRedisSet,
|
||||
get: mockRedisGet,
|
||||
del: mockRedisDel,
|
||||
}
|
||||
const mockGetRedisClient = vi.fn()
|
||||
|
||||
const mockDbSelect = vi.fn()
|
||||
const mockDbInsert = vi.fn()
|
||||
const mockDbDelete = vi.fn()
|
||||
|
||||
const mockSendEmail = vi.fn()
|
||||
const mockRenderOTPEmail = vi.fn()
|
||||
const mockAddCorsHeaders = vi.fn()
|
||||
@@ -28,11 +45,152 @@ describe('Chat OTP API Route', () => {
|
||||
const mockCreateErrorResponse = vi.fn()
|
||||
const mockSetChatAuthCookie = vi.fn()
|
||||
const mockGenerateRequestId = vi.fn()
|
||||
const mockGetStorageMethod = vi.fn()
|
||||
const mockZodParse = vi.fn()
|
||||
const mockGetEnv = vi.fn()
|
||||
|
||||
let storageMethod: 'redis' | 'database' = 'redis'
|
||||
return {
|
||||
mockRedisSet,
|
||||
mockRedisGet,
|
||||
mockRedisDel,
|
||||
mockGetRedisClient,
|
||||
mockRedisClient,
|
||||
mockDbSelect,
|
||||
mockDbInsert,
|
||||
mockDbDelete,
|
||||
mockSendEmail,
|
||||
mockRenderOTPEmail,
|
||||
mockAddCorsHeaders,
|
||||
mockCreateSuccessResponse,
|
||||
mockCreateErrorResponse,
|
||||
mockSetChatAuthCookie,
|
||||
mockGenerateRequestId,
|
||||
mockGetStorageMethod,
|
||||
mockZodParse,
|
||||
mockGetEnv,
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/lib/core/config/redis', () => ({
|
||||
getRedisClient: mockGetRedisClient,
|
||||
}))
|
||||
|
||||
vi.mock('@sim/db', () => ({
|
||||
db: {
|
||||
select: mockDbSelect,
|
||||
insert: mockDbInsert,
|
||||
delete: mockDbDelete,
|
||||
transaction: vi.fn(async (callback: (tx: Record<string, unknown>) => unknown) => {
|
||||
return callback({
|
||||
select: mockDbSelect,
|
||||
insert: mockDbInsert,
|
||||
delete: mockDbDelete,
|
||||
})
|
||||
}),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@sim/db/schema', () => ({
|
||||
chat: {
|
||||
id: 'id',
|
||||
authType: 'authType',
|
||||
allowedEmails: 'allowedEmails',
|
||||
title: 'title',
|
||||
},
|
||||
verification: {
|
||||
id: 'id',
|
||||
identifier: 'identifier',
|
||||
value: 'value',
|
||||
expiresAt: 'expiresAt',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt',
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('drizzle-orm', () => ({
|
||||
eq: vi.fn((field: string, value: string) => ({ field, value, type: 'eq' })),
|
||||
and: vi.fn((...conditions: unknown[]) => ({ conditions, type: 'and' })),
|
||||
gt: vi.fn((field: string, value: string) => ({ field, value, type: 'gt' })),
|
||||
lt: vi.fn((field: string, value: string) => ({ field, value, type: 'lt' })),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/core/storage', () => ({
|
||||
getStorageMethod: mockGetStorageMethod,
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/messaging/email/mailer', () => ({
|
||||
sendEmail: mockSendEmail,
|
||||
}))
|
||||
|
||||
vi.mock('@/components/emails/render-email', () => ({
|
||||
renderOTPEmail: mockRenderOTPEmail,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/api/chat/utils', () => ({
|
||||
addCorsHeaders: mockAddCorsHeaders,
|
||||
setChatAuthCookie: mockSetChatAuthCookie,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/api/workflows/utils', () => ({
|
||||
createSuccessResponse: mockCreateSuccessResponse,
|
||||
createErrorResponse: mockCreateErrorResponse,
|
||||
}))
|
||||
|
||||
vi.mock('@sim/logger', () => ({
|
||||
createLogger: vi.fn().mockReturnValue({
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/core/config/env', () => ({
|
||||
env: {
|
||||
NEXT_PUBLIC_APP_URL: 'http://localhost:3000',
|
||||
NODE_ENV: 'test',
|
||||
},
|
||||
getEnv: mockGetEnv,
|
||||
isTruthy: vi.fn().mockReturnValue(false),
|
||||
isFalsy: vi.fn().mockReturnValue(true),
|
||||
}))
|
||||
|
||||
vi.mock('zod', () => {
|
||||
class ZodError extends Error {
|
||||
errors: Array<{ message: string }>
|
||||
constructor(issues: Array<{ message: string }>) {
|
||||
super('ZodError')
|
||||
this.errors = issues
|
||||
}
|
||||
}
|
||||
const mockStringReturnValue = {
|
||||
email: vi.fn().mockReturnThis(),
|
||||
length: vi.fn().mockReturnThis(),
|
||||
}
|
||||
return {
|
||||
z: {
|
||||
object: vi.fn().mockReturnValue({
|
||||
parse: mockZodParse,
|
||||
}),
|
||||
string: vi.fn().mockReturnValue(mockStringReturnValue),
|
||||
ZodError,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/lib/core/utils/request', () => ({
|
||||
generateRequestId: mockGenerateRequestId,
|
||||
}))
|
||||
|
||||
import { POST, PUT } from './route'
|
||||
|
||||
describe('Chat OTP API Route', () => {
|
||||
const mockEmail = 'test@example.com'
|
||||
const mockChatId = 'chat-123'
|
||||
const mockIdentifier = 'test-chat'
|
||||
const mockOTP = '123456'
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
|
||||
vi.spyOn(Math, 'random').mockReturnValue(0.123456)
|
||||
@@ -43,21 +201,12 @@ describe('Chat OTP API Route', () => {
|
||||
randomUUID: vi.fn().mockReturnValue('test-uuid-1234'),
|
||||
})
|
||||
|
||||
const mockRedisClient = {
|
||||
set: mockRedisSet,
|
||||
get: mockRedisGet,
|
||||
del: mockRedisDel,
|
||||
}
|
||||
mockGetRedisClient.mockReturnValue(mockRedisClient)
|
||||
mockRedisSet.mockResolvedValue('OK')
|
||||
mockRedisGet.mockResolvedValue(null)
|
||||
mockRedisDel.mockResolvedValue(1)
|
||||
|
||||
vi.doMock('@/lib/core/config/redis', () => ({
|
||||
getRedisClient: mockGetRedisClient,
|
||||
}))
|
||||
|
||||
const createDbChain = (result: any) => ({
|
||||
const createDbChain = (result: unknown) => ({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockResolvedValue(result),
|
||||
@@ -73,110 +222,26 @@ describe('Chat OTP API Route', () => {
|
||||
where: vi.fn().mockResolvedValue(undefined),
|
||||
}))
|
||||
|
||||
vi.doMock('@sim/db', () => ({
|
||||
db: {
|
||||
select: mockDbSelect,
|
||||
insert: mockDbInsert,
|
||||
delete: mockDbDelete,
|
||||
transaction: vi.fn(async (callback) => {
|
||||
return callback({
|
||||
select: mockDbSelect,
|
||||
insert: mockDbInsert,
|
||||
delete: mockDbDelete,
|
||||
})
|
||||
}),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.doMock('@sim/db/schema', () => ({
|
||||
chat: {
|
||||
id: 'id',
|
||||
authType: 'authType',
|
||||
allowedEmails: 'allowedEmails',
|
||||
title: 'title',
|
||||
},
|
||||
verification: {
|
||||
id: 'id',
|
||||
identifier: 'identifier',
|
||||
value: 'value',
|
||||
expiresAt: 'expiresAt',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt',
|
||||
},
|
||||
}))
|
||||
|
||||
vi.doMock('drizzle-orm', () => ({
|
||||
eq: vi.fn((field, value) => ({ field, value, type: 'eq' })),
|
||||
and: vi.fn((...conditions) => ({ conditions, type: 'and' })),
|
||||
gt: vi.fn((field, value) => ({ field, value, type: 'gt' })),
|
||||
lt: vi.fn((field, value) => ({ field, value, type: 'lt' })),
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/core/storage', () => ({
|
||||
getStorageMethod: vi.fn(() => storageMethod),
|
||||
}))
|
||||
mockGetStorageMethod.mockReturnValue('redis')
|
||||
|
||||
mockSendEmail.mockResolvedValue({ success: true })
|
||||
mockRenderOTPEmail.mockResolvedValue('<html>OTP Email</html>')
|
||||
|
||||
vi.doMock('@/lib/messaging/email/mailer', () => ({
|
||||
sendEmail: mockSendEmail,
|
||||
}))
|
||||
|
||||
vi.doMock('@/components/emails/render-email', () => ({
|
||||
renderOTPEmail: mockRenderOTPEmail,
|
||||
}))
|
||||
|
||||
mockAddCorsHeaders.mockImplementation((response) => response)
|
||||
mockCreateSuccessResponse.mockImplementation((data) => ({
|
||||
mockAddCorsHeaders.mockImplementation((response: unknown) => response)
|
||||
mockCreateSuccessResponse.mockImplementation((data: unknown) => ({
|
||||
json: () => Promise.resolve(data),
|
||||
status: 200,
|
||||
}))
|
||||
mockCreateErrorResponse.mockImplementation((message, status) => ({
|
||||
mockCreateErrorResponse.mockImplementation((message: string, status: number) => ({
|
||||
json: () => Promise.resolve({ error: message }),
|
||||
status,
|
||||
}))
|
||||
|
||||
vi.doMock('@/app/api/chat/utils', () => ({
|
||||
addCorsHeaders: mockAddCorsHeaders,
|
||||
setChatAuthCookie: mockSetChatAuthCookie,
|
||||
}))
|
||||
|
||||
vi.doMock('@/app/api/workflows/utils', () => ({
|
||||
createSuccessResponse: mockCreateSuccessResponse,
|
||||
createErrorResponse: mockCreateErrorResponse,
|
||||
}))
|
||||
|
||||
vi.doMock('@sim/logger', () => ({
|
||||
createLogger: vi.fn().mockReturnValue({
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/core/config/env', async () => {
|
||||
const { createEnvMock } = await import('@sim/testing')
|
||||
return createEnvMock()
|
||||
})
|
||||
|
||||
vi.doMock('zod', () => ({
|
||||
z: {
|
||||
object: vi.fn().mockReturnValue({
|
||||
parse: vi.fn().mockImplementation((data) => data),
|
||||
}),
|
||||
string: vi.fn().mockReturnValue({
|
||||
email: vi.fn().mockReturnThis(),
|
||||
length: vi.fn().mockReturnThis(),
|
||||
}),
|
||||
},
|
||||
}))
|
||||
|
||||
mockGenerateRequestId.mockReturnValue('req-123')
|
||||
vi.doMock('@/lib/core/utils/request', () => ({
|
||||
generateRequestId: mockGenerateRequestId,
|
||||
}))
|
||||
|
||||
mockZodParse.mockImplementation((data: unknown) => data)
|
||||
|
||||
mockGetEnv.mockReturnValue('http://localhost:3000')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
@@ -185,12 +250,10 @@ describe('Chat OTP API Route', () => {
|
||||
|
||||
describe('POST - Store OTP (Redis path)', () => {
|
||||
beforeEach(() => {
|
||||
storageMethod = 'redis'
|
||||
mockGetStorageMethod.mockReturnValue('redis')
|
||||
})
|
||||
|
||||
it('should store OTP in Redis when storage method is redis', async () => {
|
||||
const { POST } = await import('./route')
|
||||
|
||||
mockDbSelect.mockImplementationOnce(() => ({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
@@ -226,13 +289,11 @@ describe('Chat OTP API Route', () => {
|
||||
|
||||
describe('POST - Store OTP (Database path)', () => {
|
||||
beforeEach(() => {
|
||||
storageMethod = 'database'
|
||||
mockGetStorageMethod.mockReturnValue('database')
|
||||
mockGetRedisClient.mockReturnValue(null)
|
||||
})
|
||||
|
||||
it('should store OTP in database when storage method is database', async () => {
|
||||
const { POST } = await import('./route')
|
||||
|
||||
mockDbSelect.mockImplementationOnce(() => ({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
@@ -283,13 +344,11 @@ describe('Chat OTP API Route', () => {
|
||||
|
||||
describe('PUT - Verify OTP (Redis path)', () => {
|
||||
beforeEach(() => {
|
||||
storageMethod = 'redis'
|
||||
mockGetStorageMethod.mockReturnValue('redis')
|
||||
mockRedisGet.mockResolvedValue(mockOTP)
|
||||
})
|
||||
|
||||
it('should retrieve OTP from Redis and verify successfully', async () => {
|
||||
const { PUT } = await import('./route')
|
||||
|
||||
mockDbSelect.mockImplementationOnce(() => ({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
@@ -320,13 +379,11 @@ describe('Chat OTP API Route', () => {
|
||||
|
||||
describe('PUT - Verify OTP (Database path)', () => {
|
||||
beforeEach(() => {
|
||||
storageMethod = 'database'
|
||||
mockGetStorageMethod.mockReturnValue('database')
|
||||
mockGetRedisClient.mockReturnValue(null)
|
||||
})
|
||||
|
||||
it('should retrieve OTP from database and verify successfully', async () => {
|
||||
const { PUT } = await import('./route')
|
||||
|
||||
let selectCallCount = 0
|
||||
|
||||
mockDbSelect.mockImplementation(() => ({
|
||||
@@ -373,8 +430,6 @@ describe('Chat OTP API Route', () => {
|
||||
})
|
||||
|
||||
it('should reject expired OTP from database', async () => {
|
||||
const { PUT } = await import('./route')
|
||||
|
||||
let selectCallCount = 0
|
||||
|
||||
mockDbSelect.mockImplementation(() => ({
|
||||
@@ -412,12 +467,10 @@ describe('Chat OTP API Route', () => {
|
||||
|
||||
describe('DELETE OTP (Redis path)', () => {
|
||||
beforeEach(() => {
|
||||
storageMethod = 'redis'
|
||||
mockGetStorageMethod.mockReturnValue('redis')
|
||||
})
|
||||
|
||||
it('should delete OTP from Redis after verification', async () => {
|
||||
const { PUT } = await import('./route')
|
||||
|
||||
mockRedisGet.mockResolvedValue(mockOTP)
|
||||
|
||||
mockDbSelect.mockImplementationOnce(() => ({
|
||||
@@ -447,13 +500,11 @@ describe('Chat OTP API Route', () => {
|
||||
|
||||
describe('DELETE OTP (Database path)', () => {
|
||||
beforeEach(() => {
|
||||
storageMethod = 'database'
|
||||
mockGetStorageMethod.mockReturnValue('database')
|
||||
mockGetRedisClient.mockReturnValue(null)
|
||||
})
|
||||
|
||||
it('should delete OTP from database after verification', async () => {
|
||||
const { PUT } = await import('./route')
|
||||
|
||||
let selectCallCount = 0
|
||||
mockDbSelect.mockImplementation(() => ({
|
||||
from: vi.fn().mockReturnValue({
|
||||
@@ -490,11 +541,9 @@ describe('Chat OTP API Route', () => {
|
||||
|
||||
describe('Behavior consistency between Redis and Database', () => {
|
||||
it('should have same behavior for missing OTP in both storage methods', async () => {
|
||||
storageMethod = 'redis'
|
||||
mockGetStorageMethod.mockReturnValue('redis')
|
||||
mockRedisGet.mockResolvedValue(null)
|
||||
|
||||
const { PUT: PUTRedis } = await import('./route')
|
||||
|
||||
mockDbSelect.mockImplementation(() => ({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
@@ -508,7 +557,7 @@ describe('Chat OTP API Route', () => {
|
||||
body: JSON.stringify({ email: mockEmail, otp: mockOTP }),
|
||||
})
|
||||
|
||||
await PUTRedis(requestRedis, { params: Promise.resolve({ identifier: mockIdentifier }) })
|
||||
await PUT(requestRedis, { params: Promise.resolve({ identifier: mockIdentifier }) })
|
||||
|
||||
expect(mockCreateErrorResponse).toHaveBeenCalledWith(
|
||||
'No verification code found, request a new one',
|
||||
@@ -519,8 +568,7 @@ describe('Chat OTP API Route', () => {
|
||||
it('should have same OTP expiry time in both storage methods', async () => {
|
||||
const OTP_EXPIRY = 15 * 60
|
||||
|
||||
storageMethod = 'redis'
|
||||
const { POST: POSTRedis } = await import('./route')
|
||||
mockGetStorageMethod.mockReturnValue('redis')
|
||||
|
||||
mockDbSelect.mockImplementation(() => ({
|
||||
from: vi.fn().mockReturnValue({
|
||||
@@ -542,7 +590,7 @@ describe('Chat OTP API Route', () => {
|
||||
body: JSON.stringify({ email: mockEmail }),
|
||||
})
|
||||
|
||||
await POSTRedis(requestRedis, { params: Promise.resolve({ identifier: mockIdentifier }) })
|
||||
await POST(requestRedis, { params: Promise.resolve({ identifier: mockIdentifier }) })
|
||||
|
||||
expect(mockRedisSet).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
|
||||
@@ -51,6 +51,61 @@ const createMockStream = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const {
|
||||
mockDbSelect,
|
||||
mockAddCorsHeaders,
|
||||
mockValidateChatAuth,
|
||||
mockSetChatAuthCookie,
|
||||
mockValidateAuthToken,
|
||||
mockCreateErrorResponse,
|
||||
mockCreateSuccessResponse,
|
||||
} = vi.hoisted(() => ({
|
||||
mockDbSelect: vi.fn(),
|
||||
mockAddCorsHeaders: vi.fn().mockImplementation((response: Response) => response),
|
||||
mockValidateChatAuth: vi.fn().mockResolvedValue({ authorized: true }),
|
||||
mockSetChatAuthCookie: vi.fn(),
|
||||
mockValidateAuthToken: vi.fn().mockReturnValue(false),
|
||||
mockCreateErrorResponse: vi
|
||||
.fn()
|
||||
.mockImplementation((message: string, status: number, code?: string) => {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: code || 'Error',
|
||||
message,
|
||||
}),
|
||||
{ status }
|
||||
)
|
||||
}),
|
||||
mockCreateSuccessResponse: vi.fn().mockImplementation((data: unknown) => {
|
||||
return new Response(JSON.stringify(data), { status: 200 })
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@sim/db', () => ({
|
||||
db: { select: mockDbSelect },
|
||||
chat: {},
|
||||
workflow: {},
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/core/security/deployment', () => ({
|
||||
addCorsHeaders: mockAddCorsHeaders,
|
||||
validateAuthToken: mockValidateAuthToken,
|
||||
setDeploymentAuthCookie: vi.fn(),
|
||||
isEmailAllowed: vi.fn().mockReturnValue(false),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/api/chat/utils', () => ({
|
||||
validateChatAuth: mockValidateChatAuth,
|
||||
setChatAuthCookie: mockSetChatAuthCookie,
|
||||
}))
|
||||
|
||||
vi.mock('@sim/logger', () => loggerMock)
|
||||
|
||||
vi.mock('@/app/api/workflows/utils', () => ({
|
||||
createErrorResponse: mockCreateErrorResponse,
|
||||
createSuccessResponse: mockCreateSuccessResponse,
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/execution/preprocessing', () => ({
|
||||
preprocessExecution: vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
@@ -100,12 +155,11 @@ vi.mock('@/lib/core/security/encryption', () => ({
|
||||
decryptSecret: vi.fn().mockResolvedValue({ decrypted: 'test-password' }),
|
||||
}))
|
||||
|
||||
describe('Chat Identifier API Route', () => {
|
||||
const mockAddCorsHeaders = vi.fn().mockImplementation((response) => response)
|
||||
const mockValidateChatAuth = vi.fn().mockResolvedValue({ authorized: true })
|
||||
const mockSetChatAuthCookie = vi.fn()
|
||||
const mockValidateAuthToken = vi.fn().mockReturnValue(false)
|
||||
import { preprocessExecution } from '@/lib/execution/preprocessing'
|
||||
import { createStreamingResponse } from '@/lib/workflows/streaming/streaming'
|
||||
import { GET, POST } from '@/app/api/chat/[identifier]/route'
|
||||
|
||||
describe('Chat Identifier API Route', () => {
|
||||
const mockChatResult = [
|
||||
{
|
||||
id: 'chat-id',
|
||||
@@ -142,66 +196,42 @@ describe('Chat Identifier API Route', () => {
|
||||
]
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
|
||||
vi.doMock('@/lib/core/security/deployment', () => ({
|
||||
addCorsHeaders: mockAddCorsHeaders,
|
||||
validateAuthToken: mockValidateAuthToken,
|
||||
setDeploymentAuthCookie: vi.fn(),
|
||||
isEmailAllowed: vi.fn().mockReturnValue(false),
|
||||
}))
|
||||
mockAddCorsHeaders.mockImplementation((response: Response) => response)
|
||||
mockValidateChatAuth.mockResolvedValue({ authorized: true })
|
||||
mockValidateAuthToken.mockReturnValue(false)
|
||||
mockCreateErrorResponse.mockImplementation((message: string, status: number, code?: string) => {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: code || 'Error',
|
||||
message,
|
||||
}),
|
||||
{ status }
|
||||
)
|
||||
})
|
||||
mockCreateSuccessResponse.mockImplementation((data: unknown) => {
|
||||
return new Response(JSON.stringify(data), { status: 200 })
|
||||
})
|
||||
|
||||
vi.doMock('@/app/api/chat/utils', () => ({
|
||||
validateChatAuth: mockValidateChatAuth,
|
||||
setChatAuthCookie: mockSetChatAuthCookie,
|
||||
}))
|
||||
|
||||
// Mock logger - use loggerMock from @sim/testing
|
||||
vi.doMock('@sim/logger', () => loggerMock)
|
||||
|
||||
vi.doMock('@sim/db', () => {
|
||||
const mockSelect = vi.fn().mockImplementation((fields) => {
|
||||
if (fields && fields.isDeployed !== undefined) {
|
||||
return {
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockReturnValue(mockWorkflowResult),
|
||||
}),
|
||||
}),
|
||||
}
|
||||
}
|
||||
mockDbSelect.mockImplementation((fields: Record<string, unknown>) => {
|
||||
if (fields && fields.isDeployed !== undefined) {
|
||||
return {
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockReturnValue(mockChatResult),
|
||||
limit: vi.fn().mockReturnValue(mockWorkflowResult),
|
||||
}),
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
return {
|
||||
db: {
|
||||
select: mockSelect,
|
||||
},
|
||||
chat: {},
|
||||
workflow: {},
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockReturnValue(mockChatResult),
|
||||
}),
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
vi.doMock('@/app/api/workflows/utils', () => ({
|
||||
createErrorResponse: vi.fn().mockImplementation((message, status, code) => {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: code || 'Error',
|
||||
message,
|
||||
}),
|
||||
{ status }
|
||||
)
|
||||
}),
|
||||
createSuccessResponse: vi.fn().mockImplementation((data) => {
|
||||
return new Response(JSON.stringify(data), { status: 200 })
|
||||
}),
|
||||
}))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
@@ -213,8 +243,6 @@ describe('Chat Identifier API Route', () => {
|
||||
const req = createMockNextRequest('GET')
|
||||
const params = Promise.resolve({ identifier: 'test-chat' })
|
||||
|
||||
const { GET } = await import('@/app/api/chat/[identifier]/route')
|
||||
|
||||
const response = await GET(req, { params })
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
@@ -228,24 +256,19 @@ describe('Chat Identifier API Route', () => {
|
||||
})
|
||||
|
||||
it('should return 404 for non-existent identifier', async () => {
|
||||
vi.doMock('@sim/db', () => {
|
||||
const mockLimit = vi.fn().mockReturnValue([])
|
||||
const mockWhere = vi.fn().mockReturnValue({ limit: mockLimit })
|
||||
const mockFrom = vi.fn().mockReturnValue({ where: mockWhere })
|
||||
const mockSelect = vi.fn().mockReturnValue({ from: mockFrom })
|
||||
|
||||
mockDbSelect.mockImplementation(() => {
|
||||
return {
|
||||
db: {
|
||||
select: mockSelect,
|
||||
},
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockReturnValue([]),
|
||||
}),
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
const req = createMockNextRequest('GET')
|
||||
const params = Promise.resolve({ identifier: 'nonexistent' })
|
||||
|
||||
const { GET } = await import('@/app/api/chat/[identifier]/route')
|
||||
|
||||
const response = await GET(req, { params })
|
||||
|
||||
expect(response.status).toBe(404)
|
||||
@@ -256,30 +279,25 @@ describe('Chat Identifier API Route', () => {
|
||||
})
|
||||
|
||||
it('should return 403 for inactive chat', async () => {
|
||||
vi.doMock('@sim/db', () => {
|
||||
const mockLimit = vi.fn().mockReturnValue([
|
||||
{
|
||||
id: 'chat-id',
|
||||
isActive: false,
|
||||
authType: 'public',
|
||||
},
|
||||
])
|
||||
const mockWhere = vi.fn().mockReturnValue({ limit: mockLimit })
|
||||
const mockFrom = vi.fn().mockReturnValue({ where: mockWhere })
|
||||
const mockSelect = vi.fn().mockReturnValue({ from: mockFrom })
|
||||
|
||||
mockDbSelect.mockImplementation(() => {
|
||||
return {
|
||||
db: {
|
||||
select: mockSelect,
|
||||
},
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockReturnValue([
|
||||
{
|
||||
id: 'chat-id',
|
||||
isActive: false,
|
||||
authType: 'public',
|
||||
},
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
const req = createMockNextRequest('GET')
|
||||
const params = Promise.resolve({ identifier: 'inactive-chat' })
|
||||
|
||||
const { GET } = await import('@/app/api/chat/[identifier]/route')
|
||||
|
||||
const response = await GET(req, { params })
|
||||
|
||||
expect(response.status).toBe(403)
|
||||
@@ -290,17 +308,14 @@ describe('Chat Identifier API Route', () => {
|
||||
})
|
||||
|
||||
it('should return 401 when authentication is required', async () => {
|
||||
const originalValidateChatAuth = mockValidateChatAuth.getMockImplementation()
|
||||
mockValidateChatAuth.mockImplementationOnce(async () => ({
|
||||
mockValidateChatAuth.mockResolvedValueOnce({
|
||||
authorized: false,
|
||||
error: 'auth_required_password',
|
||||
}))
|
||||
})
|
||||
|
||||
const req = createMockNextRequest('GET')
|
||||
const params = Promise.resolve({ identifier: 'password-protected-chat' })
|
||||
|
||||
const { GET } = await import('@/app/api/chat/[identifier]/route')
|
||||
|
||||
const response = await GET(req, { params })
|
||||
|
||||
expect(response.status).toBe(401)
|
||||
@@ -308,10 +323,6 @@ describe('Chat Identifier API Route', () => {
|
||||
const data = await response.json()
|
||||
expect(data).toHaveProperty('error')
|
||||
expect(data).toHaveProperty('message', 'auth_required_password')
|
||||
|
||||
if (originalValidateChatAuth) {
|
||||
mockValidateChatAuth.mockImplementation(originalValidateChatAuth)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -320,8 +331,6 @@ describe('Chat Identifier API Route', () => {
|
||||
const req = createMockNextRequest('POST', { password: 'test-password' })
|
||||
const params = Promise.resolve({ identifier: 'password-protected-chat' })
|
||||
|
||||
const { POST } = await import('@/app/api/chat/[identifier]/route')
|
||||
|
||||
const response = await POST(req, { params })
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
@@ -336,8 +345,6 @@ describe('Chat Identifier API Route', () => {
|
||||
const req = createMockNextRequest('POST', {})
|
||||
const params = Promise.resolve({ identifier: 'test-chat' })
|
||||
|
||||
const { POST } = await import('@/app/api/chat/[identifier]/route')
|
||||
|
||||
const response = await POST(req, { params })
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
@@ -348,17 +355,14 @@ describe('Chat Identifier API Route', () => {
|
||||
})
|
||||
|
||||
it('should return 401 for unauthorized access', async () => {
|
||||
const originalValidateChatAuth = mockValidateChatAuth.getMockImplementation()
|
||||
mockValidateChatAuth.mockImplementationOnce(async () => ({
|
||||
mockValidateChatAuth.mockResolvedValueOnce({
|
||||
authorized: false,
|
||||
error: 'Authentication required',
|
||||
}))
|
||||
})
|
||||
|
||||
const req = createMockNextRequest('POST', { input: 'Hello' })
|
||||
const params = Promise.resolve({ identifier: 'protected-chat' })
|
||||
|
||||
const { POST } = await import('@/app/api/chat/[identifier]/route')
|
||||
|
||||
const response = await POST(req, { params })
|
||||
|
||||
expect(response.status).toBe(401)
|
||||
@@ -366,16 +370,9 @@ describe('Chat Identifier API Route', () => {
|
||||
const data = await response.json()
|
||||
expect(data).toHaveProperty('error')
|
||||
expect(data).toHaveProperty('message', 'Authentication required')
|
||||
|
||||
if (originalValidateChatAuth) {
|
||||
mockValidateChatAuth.mockImplementation(originalValidateChatAuth)
|
||||
}
|
||||
})
|
||||
|
||||
it('should return 503 when workflow is not available', async () => {
|
||||
const { preprocessExecution } = await import('@/lib/execution/preprocessing')
|
||||
const originalImplementation = vi.mocked(preprocessExecution).getMockImplementation()
|
||||
|
||||
vi.mocked(preprocessExecution).mockResolvedValueOnce({
|
||||
success: false,
|
||||
error: {
|
||||
@@ -388,8 +385,6 @@ describe('Chat Identifier API Route', () => {
|
||||
const req = createMockNextRequest('POST', { input: 'Hello' })
|
||||
const params = Promise.resolve({ identifier: 'test-chat' })
|
||||
|
||||
const { POST } = await import('@/app/api/chat/[identifier]/route')
|
||||
|
||||
const response = await POST(req, { params })
|
||||
|
||||
expect(response.status).toBe(403)
|
||||
@@ -397,10 +392,6 @@ describe('Chat Identifier API Route', () => {
|
||||
const data = await response.json()
|
||||
expect(data).toHaveProperty('error')
|
||||
expect(data).toHaveProperty('message', 'Workflow is not deployed')
|
||||
|
||||
if (originalImplementation) {
|
||||
vi.mocked(preprocessExecution).mockImplementation(originalImplementation)
|
||||
}
|
||||
})
|
||||
|
||||
it('should return streaming response for valid chat messages', async () => {
|
||||
@@ -410,9 +401,6 @@ describe('Chat Identifier API Route', () => {
|
||||
})
|
||||
const params = Promise.resolve({ identifier: 'test-chat' })
|
||||
|
||||
const { POST } = await import('@/app/api/chat/[identifier]/route')
|
||||
const { createStreamingResponse } = await import('@/lib/workflows/streaming/streaming')
|
||||
|
||||
const response = await POST(req, { params })
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
@@ -442,8 +430,6 @@ describe('Chat Identifier API Route', () => {
|
||||
const req = createMockNextRequest('POST', { input: 'Hello world' })
|
||||
const params = Promise.resolve({ identifier: 'test-chat' })
|
||||
|
||||
const { POST } = await import('@/app/api/chat/[identifier]/route')
|
||||
|
||||
const response = await POST(req, { params })
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
@@ -463,8 +449,6 @@ describe('Chat Identifier API Route', () => {
|
||||
})
|
||||
|
||||
it('should handle workflow execution errors gracefully', async () => {
|
||||
const { createStreamingResponse } = await import('@/lib/workflows/streaming/streaming')
|
||||
const originalStreamingResponse = vi.mocked(createStreamingResponse).getMockImplementation()
|
||||
vi.mocked(createStreamingResponse).mockImplementationOnce(async () => {
|
||||
throw new Error('Execution failed')
|
||||
})
|
||||
@@ -472,8 +456,6 @@ describe('Chat Identifier API Route', () => {
|
||||
const req = createMockNextRequest('POST', { input: 'Trigger error' })
|
||||
const params = Promise.resolve({ identifier: 'test-chat' })
|
||||
|
||||
const { POST } = await import('@/app/api/chat/[identifier]/route')
|
||||
|
||||
const response = await POST(req, { params })
|
||||
|
||||
expect(response.status).toBe(500)
|
||||
@@ -481,10 +463,6 @@ describe('Chat Identifier API Route', () => {
|
||||
const data = await response.json()
|
||||
expect(data).toHaveProperty('error')
|
||||
expect(data).toHaveProperty('message', 'Execution failed')
|
||||
|
||||
if (originalStreamingResponse) {
|
||||
vi.mocked(createStreamingResponse).mockImplementation(originalStreamingResponse)
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle invalid JSON in request body', async () => {
|
||||
@@ -496,8 +474,6 @@ describe('Chat Identifier API Route', () => {
|
||||
|
||||
const params = Promise.resolve({ identifier: 'test-chat' })
|
||||
|
||||
const { POST } = await import('@/app/api/chat/[identifier]/route')
|
||||
|
||||
const response = await POST(req, { params })
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
@@ -514,9 +490,6 @@ describe('Chat Identifier API Route', () => {
|
||||
})
|
||||
const params = Promise.resolve({ identifier: 'test-chat' })
|
||||
|
||||
const { POST } = await import('@/app/api/chat/[identifier]/route')
|
||||
const { createStreamingResponse } = await import('@/lib/workflows/streaming/streaming')
|
||||
|
||||
await POST(req, { params })
|
||||
|
||||
expect(createStreamingResponse).toHaveBeenCalledWith(
|
||||
@@ -533,9 +506,6 @@ describe('Chat Identifier API Route', () => {
|
||||
const req = createMockNextRequest('POST', { input: 'Hello world' })
|
||||
const params = Promise.resolve({ identifier: 'test-chat' })
|
||||
|
||||
const { POST } = await import('@/app/api/chat/[identifier]/route')
|
||||
const { createStreamingResponse } = await import('@/lib/workflows/streaming/streaming')
|
||||
|
||||
await POST(req, { params })
|
||||
|
||||
expect(createStreamingResponse).toHaveBeenCalledWith(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user