Compare commits

...

127 Commits

Author SHA1 Message Date
Waleed
6d00d6bf2c fix(modals): center modals in visible content area and remove open/close animation (#3937)
* fix(modals): center modals in visible content area accounting for sidebar and panel

* fix(modals): address pr feedback — comment clarity and document panel assumption

* fix(modals): remove open/close animation from modal content
2026-04-03 20:06:10 -07:00
Waleed
3267d8cc24 fix(modals): center modals in visible content area accounting for sidebar and panel (#3934)
* fix(modals): center modals in visible content area accounting for sidebar and panel

* fix(modals): address pr feedback — comment clarity and document panel assumption
2026-04-03 19:19:36 -07:00
Theodore Li
2e69f85364 Fix "fix in copilot" button (#3931)
* Fix "fix in copilot" button

* Auto send message to copilot for fix in copilot

---------

Co-authored-by: Theodore Li <theo@sim.ai>
2026-04-03 22:11:45 -04:00
Waleed
57e5bac121 fix(mcp): resolve userId before JWT generation for agent block auth (#3932)
* fix(mcp): resolve userId before JWT generation for agent block auth

* test(mcp): add regression test for agent block JWT userId resolution
2026-04-03 19:05:10 -07:00
Theodore Li
8ce0299400 fix(ui) Fix oauth redirect on connector modal (#3926)
* Fix oauth redirect on connector modal

* Fix lint

---------

Co-authored-by: Theodore Li <theo@sim.ai>
2026-04-03 21:58:42 -04:00
Vikhyath Mondreti
a0796f088b improvement(mothership): workflow edits via sockets (#3927)
* improvement(mothership): workflow edits via sockets

* make embedded view join room

* fix cursor positioning bug
2026-04-03 18:44:14 -07:00
Waleed
98fe4cd40b refactor(stores): consolidate variables stores into stores/variables/ (#3930)
* refactor(stores): consolidate variables stores into stores/variables/

Move variable data store from stores/panel/variables/ to stores/variables/
since the panel variables tab no longer exists. Rename the modal UI store
to useVariablesModalStore to eliminate naming collision with the data store.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: remove unused workflowId variable in deleteVariable

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-03 18:43:47 -07:00
Waleed
34d210c66c chore(stores): remove Zustand environment store and dead init scaffolding (#3929) 2026-04-03 17:54:49 -07:00
Waleed
2334f2dca4 fix(loading): remove jarring workflow loading spinners (#3928)
* fix(loading): remove jarring workflow loading spinners

* fix(loading): remove home page skeleton loading state

* fix(loading): remove plain spinner loading states from task and file view
2026-04-03 17:45:30 -07:00
Waleed
65fc138bfc improvement(stores): remove deployment state from Zustand in favor of React Query (#3923) 2026-04-03 17:44:10 -07:00
Waleed
e8f7fe0989 v0.6.22: agentmail, rootly, landing fixes, analytics, credentials block 2026-04-03 01:14:36 -07:00
Waleed
ace87791d8 feat(analytics): add PostHog product analytics (#3910)
* feat(analytics): add PostHog product analytics

* fix(posthog): fix workspace group via URL params, type errors, and clean up comments

* fix(posthog): address PR review - fix pre-tx event, auth_method, paused executions, enterprise cancellation, settings double-fire

* chore(posthog): remove unused identifyServerPerson

* fix(posthog): isolate processQueuedResumes errors, simplify settings posthog deps

* fix(posthog): correctly classify SSO auth_method, fix phantom empty-string workspace groups

* fix(posthog): remove usePostHog from memo'd TemplateCard, fix copilot chat phantom workspace group

* fix(posthog): eliminate all remaining phantom empty-string workspace groups

* fix(posthog): fix cancel route phantom group, remove redundant workspaceId shadow in catch block

* fix(posthog): use ids.length for block_removed guard to handle container blocks with descendants

* chore(posthog): remove unused removedBlockTypes variable

* fix(posthog): remove phantom $set person properties from subscription events

* fix(posthog): add passedKnowledgeBaseName to knowledge_base_opened effect deps

* fix(posthog): capture currentWorkflowId synchronously before async import to avoid stale closure

* fix(posthog): add typed captureEvent wrapper for React components, deduplicate copilot_panel_opened

* feat(posthog): add task_created and task_message_sent events, remove copilot_panel_opened

* feat(posthog): track task_renamed, task_deleted, task_marked_read, task_marked_unread

* feat(analytics): expand posthog event coverage with source tracking and lifecycle events

* fix(analytics): flush posthog events on SIGTERM before ECS task termination

* fix(analytics): fix posthog in useCallback deps and fire block events for bulk operations
2026-04-03 01:00:35 -07:00
Waleed
74af452175 feat(blocks): add Credential block (#3907)
* feat(blocks): add Credential block

* fix(blocks): explicit workspaceId guard in credential handler, clarify hasOAuthSelection

* feat(credential): add list operation with type/provider filters

* feat(credential): restrict to OAuth only, remove env vars and service accounts

* docs(credential): update screenshots

* fix(credential): remove stale isServiceAccount dep from overlayContent memo

* fix(credential): filter to oauth-only in handleComboboxChange matchedCred lookup
2026-04-02 23:15:15 -07:00
Waleed
ec51f73596 feat(email): abandoned checkout email, 80% free tier warning, credits exhausted email (#3908)
* feat(email): send plain personal email on abandoned checkout

* feat(email): lower free tier warning to 80% and add credits exhausted email

* feat(email): use wordmark in email header instead of icon-only logo

* fix(email): restore accidentally deleted social icons in email footer

* fix(email): prevent double email for free users at 80%, fix subject line

* improvement(emails): extract shared plain email styles and proFeatures constant, fix double email on 100% usage

* fix(email): filter subscription-mode checkout, skip already-subscribed users, fix preview text

* fix(email): use notifications type for onboarding followup to respect unsubscribe preferences

* fix(email): use limit instead of currentUsage in credits exhausted email body

* fix(email): use notifications type for abandoned checkout, clarify crosses80 comment

* chore(email): rename _constants.ts to constants.ts

* fix(email): use isProPlan to catch org-level subscriptions in abandoned checkout guard

* fix(email): align onboarding followup delay to 5 days for email/password users
2026-04-02 19:31:29 -07:00
Theodore Li
6866da590c fix(tools) Directly query db for custom tool id (#3875)
* Directly query db for custom tool id

* Switch back to inline imports

* Fix lint

* Fix test

* Fix greptile comments

* Fix lint

* Make userId and workspaceId required

* Add back nullable userId and workspaceId fields

---------

Co-authored-by: Theodore Li <theo@sim.ai>
2026-04-02 22:13:37 -04:00
Waleed
b0c0ee29a8 feat(email): send onboarding followup email 3 days after signup (#3906)
* feat(email): send onboarding followup email 3 days after signup

* fix(email): add trigger guard, idempotency key, and shared task ID constant

* fix(email): increase onboarding followup delay from 3 to 5 days
2026-04-02 18:08:14 -07:00
Waleed
20c05644ab fix(enterprise): smooth audit log list animation (#3905) 2026-04-02 16:03:03 -07:00
Waleed
f9d73db65c feat(rootly): expand Rootly integration from 14 to 27 tools (#3902)
* feat(rootly): expand Rootly integration from 14 to 27 tools

Add 13 new tools: delete_incident, get_alert, update_alert,
acknowledge_alert, resolve_alert, create_action_item, list_action_items,
list_users, list_on_calls, list_schedules, list_escalation_policies,
list_causes, list_playbooks. Includes tool files, types, registry,
block definition with subBlocks/conditions/params, and docs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(rootly): handle 204 No Content response for delete_incident

DELETE /v1/incidents/{id} returns 204 with empty body. Avoid calling
response.json() on success — return success/message instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(rootly): remove non-TSDoc comments, add empty body to acknowledge_alert

Remove all inline section comments from block definition per CLAUDE.md
guidelines. Add explicit empty JSON:API body to acknowledge_alert POST
to prevent potential 400 from servers expecting a body with Content-Type.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(rootly): send empty body on resolve_alert, guard assignedToUserId parse

resolve_alert now sends { data: {} } instead of undefined when no
optional params are provided, matching the acknowledge_alert fix.
create_action_item now validates assignedToUserId is numeric before
parseInt to avoid silent NaN coercion.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(rootly): extract on-call relationships from JSON:API relationships/included

On-call user, schedule, and escalation policy are exposed as JSON:API
relationships, not flat attributes. Now extracts IDs from
item.relationships and looks up names from the included array.
Adds ?include=user,schedule,escalation_policy to the request URL.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(rootly): remove last non-TSDoc comment from block definition

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 13:40:45 -07:00
Waleed
e2e53aba76 feat(agentmail): add AgentMail integration with 21 tools (#3901)
* feat(agentmail): add AgentMail integration with 21 tools

* fix(agentmail): clear stale to field when switching to reply_message operation

* fix(agentmail): guard messageId and label remappings with operation checks

* fix(agentmail): clean up subBlock titles

* fix(agentmail): guard replyTo and thread label remappings with operation checks

* fix(agentmail): guard inboxIdParam remapping with operation check

* fix(agentmail): guard permanent, replyAll, and draftInReplyTo with operation checks
2026-04-02 13:40:23 -07:00
Waleed
f0d1950477 v0.6.21: concurrency FF, blog theme 2026-04-02 13:08:59 -07:00
Waleed
727bb1cadb fix(bullmq): restore CONCURRENCY_CONTROL_ENABLED flag guard (#3903) 2026-04-02 13:07:50 -07:00
Waleed
e2e29cefd7 fix(blog): use landing theme variables in MDX components (#3900) 2026-04-02 12:34:38 -07:00
Waleed
0fdd8ffb55 v0.6.20: oauth default credential name, models pages, new models, rippling and rootly integrations 2026-04-02 11:44:24 -07:00
Waleed
45f053a383 feat(rootly): add Rootly incident management integration with 14 tools (#3899)
* feat(rootly): add Rootly incident management integration with 14 tools

* fix(rootly): address PR review feedback - PATCH method, totalCount, environmentIds

- Changed update_incident HTTP method from PUT to PATCH per Rootly API spec
- Fixed totalCount in all 9 list tools to use data.meta?.total_count from API response
- Added missing updateEnvironmentIds subBlock and params mapping for update_incident

* fix(rootly): add id to PATCH body and unchanged option to update status dropdown

- Include incident id in JSON:API PATCH body per spec requirement
- Add 'Unchanged' empty option to updateStatus dropdown to avoid accidental overwrites

* icon update

* improvement(rootly): complete block-tool alignment and fix validation gaps

- Add missing get_incident output fields (private, shortUrl, closedAt)
- Add missing block subBlocks: createPrivate, alertStatus, alertExternalId, listAlertsServices
- Add pageNumber subBlocks for all 9 list operations
- Add teams/environments filter subBlocks for list_incidents and list_alerts
- Add environmentIds subBlock for create_alert
- Add empty default options to all optional dropdowns (createStatus, createKind, listIncidentsSort, eventVisibility)
- Wire all new subBlocks in tools.config.params and inputs
- Regenerate docs

* fix(rootly): align tools with OpenAPI spec

- list_incident_types: use filter[name] instead of unsupported filter[search]
- list_severities: add missing search param (filter[search])
- create_incident: title is optional per API (auto-generated if null)
- update_incident: add kind, private, labels, incidentTypeIds,
  functionalityIds, cancellationMessage params
- create/update/list incidents: add scheduled, in_progress, completed
  status values
- create_alert: fix status description (only open/triggered on create)
- add_incident_event: add updatedAt to response
- block: add matching subBlocks and params for all new tool fields

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(rootly): final validation fixes from OpenAPI spec audit

- update_incident: change PATCH to PUT per OpenAPI spec
- index.ts: add types re-export
- types.ts: fix id fields to string | null (matches ?? null runtime)
- block: add value initializers to 4 dropdowns missing them
- registry: fix alphabetical order (incident_types before incidents)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* reorg

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 11:40:40 -07:00
Waleed
225d5d551a improvement(models): update default to claude-sonnet-4-6 and reorganize OpenAI models (#3898)
* improvement(models): update default to claude-sonnet-4-6 and reorganize OpenAI models

* fix(tests): update stale claude-sonnet-4-5 references to claude-sonnet-4-6

* fix(combobox): rename misleading claudeSonnet45 variable to defaultModelOption
2026-04-02 10:51:27 -07:00
Waleed
a78f3f9c2e fix(credential): fix service_account migration to avoid unsafe enum usage in same transaction (#3897) 2026-04-02 10:16:08 -07:00
Waleed
080a0a6123 feat(rippling): expand Rippling integration from to 86 tools, landing updates (#3886)
* feat(rippling): expand Rippling integration from 16 to 86 tools

* fix(rippling): add required constraints on name and data subBlocks for create operations

* fix(rippling): add subblock ID migrations for removed legacy fields

* fix(docs): add MANUAL-CONTENT markers to tailscale docs and regenerate

* fix(rippling): add missing response fields to tool transforms

Add fields found missing by validation agents:
- list_companies: physical_address
- list/get_supergroups: sub_group_type, read_only, parent, mutually_exclusive_key, cumulatively_exhaustive_default, include_terminated
- list/get/create/update_custom_object: native_category_id, managed_package_install_id, owner_id
- list/get/create/update_custom_app: icon, pages
- list/get/create/update_custom_object_field: managed_package_install_id

* fix(rippling): add missing block outputs and required data conditions

- Add 17 missing collection output keys (titles, workLocations, supergroups, etc.)
- Add delete/bulk/report output keys (deleted, results, report_id, etc.)
- Mark data subBlock required for create_business_partner, create_custom_app,
  and create_custom_object_field (all have required params via data JSON spread)
- Add optional: true to get_current_user work_email and company_id outputs

* fix(rippling): add missing supergroup fields and fix validation issues

- Add 5 missing supergroup fields (allow_non_employees, can_override_role_states, priority, is_invisible, ignore_prov_group_matching) to types, list, and get tools
- Fix ok fallback from true to false in supergroup inclusion/exclusion member update tools
- Fix truthy check to null check for description param in create_custom_object_field

* fix(rippling): add missing custom page fields and structured custom setting responses

- Add 5 missing CustomPage fields (components, actions, canvas_actions, variables, media) to types and all page tools
- Replace opaque data blob with structured field mapping in create/update custom setting transforms
- Fix secret_value type cast consistency in list_custom_settings

* fix(rippling): add missing response fields, fix truthy checks, and improve UX

- Add 9 missing Worker fields (location, gender, date_of_birth, race, ethnicity, citizenship, termination_details, custom_fields, country_fields)
- Add 5 missing User fields (name, emails, phone_numbers, addresses, photos)
- Add worker expandable field to GroupMember types and all 3 member list tools
- Add 5 optional params to trigger_report_run (includeObjectIds, includeTotalRows, formatDateFields, formatCurrencyFields, outputType)
- Fix truthy checks to null checks in create_department, create/update_work_location
- Fix customObjectId subBlock label to say "API Name" instead of "ID"

* update docs

* fix(rippling): fix truthy checks, add missing fields, and regenerate docs

- Replace all `if (params.x)` with `if (params.x != null)` across 30+ tool files to prevent empty string/false/zero suppression
- Add expandable `parent` and `department_hierarchy` fields to department tools
- Add expandable `parent` field to team tools
- Add `company` expandable field to get_current_user
- Add `addressType` param to create/update work location tools
- Fix `secret_value` output type from 'json' to 'string' in list_custom_settings
- Regenerate docs for all 86 tools from current definitions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(rippling): add all remaining spec fields and regenerate docs

- Add 6 advanced params to create_custom_object_field: required, rqlDefinition,
  formulaAttrMetas, section, derivedFieldFormula, derivedAggregatedField
- Add 6 advanced params to update_custom_object_field: required, rqlDefinition,
  formulaAttrMetas, section, derivedFieldFormula, nameFieldDetails
- Add 4 record output fields to all custom object record tools: created_by,
  last_modified_by, owner_role, system_updated_at
- Add cursor param to get_current_user
- Add __meta response field to get_report_run
- Regenerate docs for all 86 tools

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(rippling): align all tools with OpenAPI spec

- Add __meta to 14 GET-by-ID tools (MetaResponse pattern)
- Fix supergroup tools: add filter to list_supergroups, remove invalid
  cursor from 4 list endpoints, revert update members to PATCH with
  Operations body
- Fix query_custom_object_records: use query/limit/cursor body params,
  return cursor instead of nextLink
- Fix bulk_create: use rows_to_write per spec
- Fix create/update record body wrappers with externalId support
- Update types.ts param interfaces and block config mappings
- Add limit param mapping with Number() conversion in block config
- Regenerate docs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(rippling): address PR review comments — add dedicated subBlocks, fix data duplication, expand externalId condition

- Add dedicated apiName, businessPartnerGroupId, workerId, dataType subBlocks so required params are no longer hidden behind opaque data JSON
- Narrow `data: item` in custom object record tools to only include dynamic fields, avoiding duplication of enumerated fields
- Expand externalId subBlock condition to include create/update custom object record operations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(rippling): remove data JSON required for ops with dedicated subBlocks

create_business_partner, create_custom_app, and create_custom_object_field
now have dedicated subBlocks for their required params, so the data JSON
field is supplementary (not required) for those operations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(rippling): use rest-destructuring for all custom object record data output

The spec uses additionalProperties for custom fields at the top level,
not a nested `data` sub-object. Use the same rest-destructuring pattern
across all 6 custom object record tools so `data` only contains dynamic
fields, not duplicates of enumerated standard fields.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(rippling): make update_custom_object_record data param optional in type

Matches the tool's `required: false` — users may update only external_id
without changing data.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(rippling): add dedicated streetAddress subBlock for create_work_location

streetAddress is required by the tool but had no dedicated subBlock —
users had to include it in the data JSON. Now has its own required
subBlock matching the pattern used by all other required params.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(rippling): add allOrNothing subBlock for bulk operations

The bulk create/update/delete tools accept an optional allOrNothing
boolean param, but it had no subBlock and no way to be passed through
the block UI. Added as an advanced-mode dropdown with boolean coercion.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(rippling): derive spreadOps from DATA_OPS to prevent divergence

Replace the hardcoded spreadOps array with a derivation from the
file-level DATA_OPS constant minus non-spread operations. This ensures
new create/update operations added to DATA_OPS automatically get
spread behavior without needing a second manual update.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* updated

* fix(rippling): replace generic JSON outputs with specific fields per API spec

- Extract file_url, expires_at, output_type from report run result blob
- Rename bulk create/update outputs to createdRecords/updatedRecords
- Fix list_custom_settings output key mismatch (settings → customSettings)
- Make data optional for update_custom_object_record in block
- Update block outputs to match new tool output fields

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix landing

* restore FF

* fix(rippling): add wandConfig, clean titles, and migrate legacy operation values

- Remove "(JSON)" suffix from all subBlock titles
- Add wandConfig with AI prompts for filter, expand, orderBy, query, data, records, and dataType fields
- Add OPERATION_VALUE_MIGRATIONS to migrate old operation values (list_employees → list_workers, etc.) preventing runtime errors on saved workflows

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(rippling): fix grammar typos and revert unnecessary migration

- Fix "a object" → "an object" in update/delete object category descriptions
- Revert OPERATION_VALUE_MIGRATIONS (unnecessary for low-usage integration)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(landing): add interactive workspace preview tabs

Adds Tables, Files, Knowledge Base, Logs, and Scheduled Tasks preview
components to the landing hero, with sidebar nav items that switch to each view.

* test updates

* refactor(landing): clean up code quality issues in preview components

- Replace widthMultiplier with explicit width on PreviewColumn
- Replace key={i} with key={Icon.name} in connectorIcons
- Scope --c-active CSS variable to sidebar container, eliminating hardcoded #363636 duplication
- Replace '-  -  -' fallback with em dash
- Type onSelectNav as (id: SidebarView) removing the unsafe cast

* fix(landing): use stable index key in connectorIcons to avoid minification breakage

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 01:30:43 -07:00
Theodore Li
fc6fe193fa fix(credential) fix credential migration (#3896)
* fix(credential) fix credential migration

* Fix lint

---------

Co-authored-by: Theodore Li <theo@sim.ai>
2026-04-02 04:29:01 -04:00
Theodore Li
bbc704fe05 feat(credentials) Add google service account support (#3828)
* feat(auth): allow google service account

* Add gmail support for google services

* Refresh creds on typing in impersonated email

* Switch to adding subblock impersonateUserEmail conditionally

* Directly pass subblock for impersonateUserEmail

* Fix lint

* Update documentation for google service accounts

* Fix lint

* Address comments

* Remove hardcoded scopes, remove orphaned migration script

* Simplify subblocks for google service account

* Fix lint

* Fix build error

* Fix documentation scopes listed for google service accounts

* Fix issue with credential selector, remove bigquery and ad support

* create credentialCondition

* Shift conditional render out of subblock

* Simplify sublock values

* Fix security message

* Handle tool service accounts

* Address bugbot

* Fix lint

* Fix manual credential input not showing impersonate

* Fix tests

* Allow watching param id and subblock ids

* Fix bad test

---------

Co-authored-by: Theodore Li <theo@sim.ai>
2026-04-02 03:08:13 -04:00
Theodore Li
c016537564 fix(blog): Fix blog not loading (#3895)
* Fix blog not loading

* Use emcn icon

---------

Co-authored-by: Theodore Li <theo@sim.ai>
2026-04-02 00:53:31 -04:00
Waleed
4c94f3cf78 improvement(providers): audit and update all provider model definitions (#3893)
* improvement(providers): audit and update all provider model definitions

* fix(providers): add maxOutputTokens to azure/o3 and azure/o4-mini

* fix(providers): move maxOutputTokens inside capabilities for azure models
2026-04-01 19:32:16 -07:00
Vikhyath Mondreti
27a11a269d improvement(workflow): seed start block on server side (#3890)
* improvement(workflow): seed start block on server side

* add creating state machine for optimistic switch

* fix worksapce switch

* address comments

* address error handling at correct level
2026-04-01 19:04:34 -07:00
Waleed
2c174ca4f6 feat(landing): added models pages (#3888)
* feat(landing): added models pages

* fix(models): address PR review feedback

Correct model structured-data price bounds, remove dead code in the models catalog helpers, and harden OG font loading with graceful fallbacks.

Made-with: Cursor

* relative imports, build fix

* lint

* fix(models): remove dead og-utils exports, fix formatTokenCount null guard
2026-04-01 18:23:35 -07:00
Waleed
ac831b85b2 chore(bun): update bunfig.toml (#3889)
* chore(bun): update bunfig.toml

* outdated bun lock

* chore(deps): downgrade @aws-sdk/client-secrets-manager to 3.940.0
2026-04-01 17:21:00 -07:00
Waleed
8527ae5d3b feat(providers): server-side credential hiding for Azure and Bedrock (#3884)
* fix: allow Bedrock provider to use AWS SDK default credential chain

Remove hard requirement for explicit AWS credentials in Bedrock provider.
When access key and secret key are not provided, the AWS SDK automatically
falls back to its default credential chain (env vars, instance profile,
ECS task role, EKS IRSA, SSO).

Closes #3694

Signed-off-by: majiayu000 <1835304752@qq.com>

* fix: add partial credential guard for Bedrock provider

Reject configurations where only one of bedrockAccessKeyId or
bedrockSecretKey is provided, preventing silent fallback to the
default credential chain with a potentially different identity.

Add tests covering all credential configuration scenarios.

Signed-off-by: majiayu000 <1835304752@qq.com>

* fix: clean up bedrock test lint and dead code

Remove unused config parameter and dead _lastConfig assignment
from mock factory. Break long mockReturnValue chain to satisfy
biome line-length rule.

Signed-off-by: majiayu000 <1835304752@qq.com>

* fix: address greptile review feedback on PR #3708

Use BedrockRuntimeClientConfig from SDK instead of inline type.
Add default return value for prepareToolsWithUsageControl mock.

Signed-off-by: majiayu000 <1835304752@qq.com>

* feat(providers): server-side credential hiding for Azure and Bedrock

* fix(providers): revert Bedrock credential fields to required with original placeholders

* fix(blocks): add hideWhenEnvSet to getProviderCredentialSubBlocks for Azure and Bedrock

* fix(agent): use getProviderCredentialSubBlocks() instead of duplicating credential subblocks

* fix(blocks): consolidate Vertex credential into shared factory with basic/advanced mode

* fix(types): resolve pre-existing TypeScript errors across auth, secrets, and copilot

* lint

* improvement(blocks): make Vertex AI project ID a password field

* fix(blocks): preserve vertexCredential subblock ID for backwards compatibility

* fix(blocks): follow canonicalParamId pattern correctly for vertex credential subblocks

* fix(blocks): keep vertexCredential subblock ID stable to preserve saved workflow state

* fix(blocks): add canonicalParamId to vertexCredential basic subblock to complete the swap pair

* fix types

* more types

---------

Signed-off-by: majiayu000 <1835304752@qq.com>
Co-authored-by: majiayu000 <1835304752@qq.com>
Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
2026-04-01 16:27:54 -07:00
Waleed
076c835ba2 improvement(credentials): consolidate OAuth modals and auto-fill credential name (#3887)
* improvement(credentials): consolidate OAuth modals and auto-fill credential name

* fix(credentials): context-aware subtitle for KB vs workflow
2026-04-01 15:48:49 -07:00
Vikhyath Mondreti
df6ceb61a4 fix(envvar): remove dead env var 2026-04-01 14:01:13 -07:00
Vikhyath Mondreti
2ede12aa0e fix(cost): worker crash incremenental case (#3885) 2026-04-01 11:42:19 -07:00
Waleed
42fb434354 fix(encryption): specify authTagLength on all AES-GCM cipher/decipher calls (#3883)
* fix: specify authTagLength in AES-GCM decipheriv calls

Fixes missing authTagLength parameter in createDecipheriv calls using
AES-256-GCM mode. Without explicit tag length specification, the
application may be tricked into accepting shorter authentication tags,
potentially allowing ciphertext spoofing.

CWE-310: Cryptographic Issues (gcm-no-tag-length)

* fix: specify authTagLength on createCipheriv calls for AES-GCM consistency

Complements #3881 by adding explicit authTagLength: 16 to the encrypt
side as well, ensuring both cipher and decipher specify the tag length.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: clean up crypto modules

- Fix error: any → error: unknown with proper type guard in encryption.ts
- Eliminate duplicate iv.toString('hex') calls in both encrypt functions
- Remove redundant string split in decryptApiKey (was splitting twice)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* new turborepo version

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Lakee Sivaraya <71339072+lakeesiv@users.noreply.github.com>
Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
Co-authored-by: Siddharth Ganesan <33737564+Sg312@users.noreply.github.com>
Co-authored-by: NLmejiro <kuroda.k1021@gmail.com>
2026-04-01 10:46:58 -07:00
Waleed
d581009099 v0.6.19: vllm fixes, loading improevments, reactquery standardization, new gpt 5.4 models, fireworks provider support, launchdarkly, tailscale, extend integrations 2026-03-31 20:17:00 -07:00
Waleed
dcebe3ae97 improvement(triggers): add tags to all trigger.dev task invocations (#3878)
* improvement(triggers): add tags to all trigger.dev task invocations

* fix(triggers): prefix unused type param in buildTags

* fix(triggers): remove unused type param from buildTags
2026-03-31 19:52:33 -07:00
Waleed
e39c534ee3 feat(providers): add Fireworks AI provider integration (#3873)
* feat(providers): add Fireworks AI provider integration

* fix(providers): remove unused logger and dead modelInfo from fireworks

* lint

* feat(providers): add Fireworks BYOK support and official icon

* fix(providers): add workspace membership check and remove shared fetch cache for fireworks models
2026-03-31 19:22:04 -07:00
Vikhyath Mondreti
b95a0491a0 fix(kb): chunking config persistence (#3877)
* fix(kb): persist chunking config correctly

* fix kb config as sot

* remove dead code

* fix doc req bodies

* add defaults for async for legacy docs
2026-03-31 19:16:23 -07:00
Waleed
a79c8a75ce fix(chat): align floating chat send button colors with home/mothership chat (#3876) 2026-03-31 18:11:02 -07:00
Vikhyath Mondreti
282ec8c58c fix(reorder): drag and drop hook (#3874)
* fix(reorder): drag and drop hook

* fix custom tool dropdown color

* fix mcp server url change propagation
2026-03-31 17:33:08 -07:00
Waleed
e45fbe0184 improvement(attio): validate integration, fix event bug, add missing tool and triggers (#3872)
* improvement(attio): validate integration, fix event bug, add missing tool and triggers

* fix(attio): wire new trigger extractors into dispatcher, trim targetUrl

Add extractAttioListData and extractAttioWorkspaceMemberData dispatch
branches in utils.server.ts so the four new triggers return correct
outputs instead of falling through to generic extraction.

Also add missing .trim() on targetUrl in update_webhook.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 16:44:49 -07:00
Waleed
512558dcb3 feat(launchdarkly): add LaunchDarkly integration for feature flag management (#3870)
* feat(launchdarkly): add LaunchDarkly integration for feature flag management

* fix(launchdarkly): guard empty instructions array, trim apiKey in auth header

* lint
2026-03-31 16:40:06 -07:00
Waleed
35411e465e feat(models): add gpt-5.4-mini and gpt-5.4-nano (#3871) 2026-03-31 16:32:24 -07:00
Waleed
72e28baa07 feat(extend): add Extend AI document processing integration (#3869)
* feat(extend): add Extend AI document processing integration

* fix(extend): cast json response to fix type error

* fix(extend): correct API request body structure per Extend docs

* fix(extend): address PR review comments

* fix(extend): sync integrations.json bgColor to #000000

* lint
2026-03-31 16:26:34 -07:00
Waleed
d99dd86bf2 feat(tailscale): add Tailscale integration with 20 API operations (#3868)
* feat(tailscale): add Tailscale integration with 20 API operations

* fix(tailscale): fix transformResponse signatures and block output types

* fix(tailscale): safe response.json() pattern, trim apiKey, guard expirySeconds
2026-03-31 16:26:17 -07:00
Waleed
7898e5d75f improvement(workflows): replace Zustand workflow sync with React Query as single source of truth (#3860)
* improvement(workflows): replace Zustand workflow sync with React Query as single source of truth

* fix(workflows): address PR review feedback — sandbox execution, hydration deadlock, test mock, copy casing

* lint

* improvement(workflows): adopt skipToken over enabled+as-string for type-safe conditional queries

* improvement(workflows): remove dead complexity, fix mutation edge cases

- Throw on state PUT failure in useCreateWorkflow instead of swallowing
- Use Map for O(1) lookups in duplicate/export loops (3 hooks)
- Broaden invalidation scope in update/delete mutations to lists()
- Switch workflow-block to useWorkflowMap for direct ID lookup
- Consolidate use-workflow-operations to single useWorkflowMap hook
- Remove workspace transition guard (sync body, unreachable timeout)
- Make switchToWorkspace synchronous (remove async/try-catch/finally)

* fix(workflows): resolve cold-start deadlock on direct URL navigation

loadWorkflowState used hydration.workspaceId (null on cold start) to
look up the RQ cache, causing "Workflow not found" even when the
workflow exists in the DB. Now falls back to getWorkspaceIdFromUrl()
and skips the cache guard when the cache is empty (letting the API
fetch proceed).

Also removes the redundant isRegistryReady guard in workflow.tsx that
blocked setActiveWorkflow when hydration.workspaceId was null.

* fix(ui): prevent flash of empty state while workflows query is pending

Dashboard and EmbeddedWorkflow checked workflow list length before
the RQ query resolved, briefly showing "No workflows" or "Workflow
not found" on initial load. Now gates on isPending first.

* fix(workflows): address PR review — await description update, revert state PUT throw

- api-info-modal: use mutateAsync for description update so errors
  are caught by the surrounding try/catch instead of silently swallowed
- useCreateWorkflow: revert state PUT to log-only — the workflow is
  already created in the DB, throwing rolls back the optimistic entry
  and makes it appear the creation failed when it actually succeeded

* move folders over to react query native, restructure passage of data

* pass signal correctly

* fix types

* fix workspace id

* address comment

* soft deletion accuring

---------

Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
2026-03-31 15:49:16 -07:00
Waleed
df62502903 feat(infra): add dev environment support (#3867)
* feat(infra): add dev environment support

* fix(ci): push :dev ECR tag when building from dev branch

* fix(feature-flags): simplify isHosted subdomain check

* fix(ci,feature-flags): guard URL parse, fix dev AWS creds in images.yml
2026-03-31 15:36:57 -07:00
Waleed
1a2aa6949e feat(secrets-manager): add AWS Secrets Manager integration (#3866)
* feat(secrets-manager): add AWS Secrets Manager integration

* fix(secrets-manager): address PR review feedback

- Conditional delete message based on forceDelete flag
- Add binary secret detection in getSecretValue

* fix(secrets-manager): handle boolean forceDelete and validate numeric inputs

- Accept both string 'true' and boolean true for forceDelete
- Guard parseInt results with isNaN check for maxResults and recoveryWindowInDays
2026-03-31 15:26:03 -07:00
Waleed
4544fd4519 improvement(ui): fix nav loading flash, skeleton mismatches, and React anti-patterns across resource pages (#3864)
* improvement(ui): fix nav loading flash, skeleton mismatches, and React anti-patterns across resource pages

- Convert knowledge, files, tables, scheduled-tasks, and home page.tsx files from async server components to simple client re-exports, eliminating the loading.tsx flash on every navigation
- Add client-side permission redirects (usePermissionConfig) to knowledge, files, and tables components to replace server-side checks
- Fix knowledge loading.tsx skeleton column count (6→7) and tables loading.tsx (remove phantom checkbox column)
- Fix connector document live updates: use isConnectorSyncingOrPending instead of status === 'syncing' so polling activates immediately after connector creation
- Remove dead chunk-switch useEffect in ChunkEditor (redundant with key prop remount)
- Replace useState+useEffect debounce with useDebounce hook in document.tsx
- Replace useRef+useEffect URL init with lazy useState initializers in document.tsx and logs.tsx
- Make handleToggleEnabled optimistic in document.tsx (cache first, onError rollback)
- Replace mutate+new Promise wrapper with mutateAsync+try/catch in base.tsx
- Fix schedule-modal.tsx: replace 15-setter useEffect with useState lazy initializers + key prop remount; wrap parseCronToScheduleType in useMemo
- Fix logs search: eliminate mount-only useEffect with eslint-disable by passing initialQuery to useSearchState; parse query once via shared initialParsed state
- Add useWorkspaceFileRecord hook to workspace-files.ts; refactor FileViewer to self-fetch
- Fix value: any → value: string in useTagSelection and collaborativeSetTagSelection
- Fix knowledge-tag-filters.tsx: pass '' instead of null when filters are cleared (type safety)

* fix(kb): use active scope in useWorkspaceFileRecord to share cache with useWorkspaceFiles

* fix(logs,kb,tasks): lazy-init useRef for URL param, add cold-path docs to useWorkspaceFileRecord, document key remount requirement in ScheduleModal

* fix(files): redirect to files list when file record not found in viewer

* revert(files): remove useEffect redirect from file-viewer, keep simple null return

* fix(scheduled-tasks): correct useMemo dep from schedule?.cronExpression to schedule
2026-03-31 11:56:58 -07:00
Geonwoo Kim
019630bdc8 fix(vllm): pass env.VLLM_API_KEY to chat requests (#3865) 2026-03-31 10:33:44 -07:00
Waleed
7d0fdefb22 v0.6.18: file operations block, profound integration, edge connection improvements, copy logs, knowledgebase robustness 2026-03-30 21:35:41 -07:00
Vikhyath Mondreti
90f592797a fix(file): use file-upload subblock (#3862)
* fix(file): use file-upload subblock

* fix preview + logs url for notifs

* fix color for profound

* remove canonical param from desc
2026-03-30 21:29:01 -07:00
Waleed
d091441e39 feat(logs): add copy link and deep-link support for log entries (#3863)
* feat(logs): add copy link and deep link support for log entries

* fix(logs): move Link icon to emcn and handle clipboard rejections

* feat(notifications): use executionId deep-link for View Log URLs

Switch buildLogUrl from ?search= to ?executionId= so email and Slack
'View Log' buttons open the logs page with the specific execution
auto-selected and the details panel expanded.
2026-03-30 21:17:58 -07:00
Waleed
7d4dd26760 fix(knowledge): fix document processing stuck in processing state (#3857)
* fix(knowledge): fix document processing stuck in processing state

* fix(knowledge): use Promise.allSettled for document dispatch and fix Copilot OAuth context

- Change Promise.all to Promise.allSettled in processDocumentsWithQueue so
  one failed dispatch doesn't abort the entire batch
- Add writeOAuthReturnContext before showing LazyOAuthRequiredModal from
  Copilot tools so useOAuthReturnForWorkflow can handle the return
- Add consumeOAuthReturnContext on modal close to clean up stale context

* fix(knowledge): fix type error in useCredentialRefreshTriggers call

Pass empty string instead of undefined for connectorProviderId fallback
to match the hook's string parameter type.

* upgrade turbo

* fix(knowledge): fix type error in connectors-section useCredentialRefreshTriggers call

Same string narrowing fix as add-connector-modal — pass empty string
fallback for providerId.
2026-03-30 20:35:08 -07:00
Vikhyath Mondreti
0abeac77e1 improvement(platform): standardize perms, audit logging, lifecycle across admin, copilot, ui actions (#3858)
* improvement(platform): standardize perms, audit logging, lifecycle mgmt across admin, copilot, ui actions

* address comments

* improve error codes

* address bugbot comments

* fix test
2026-03-30 20:25:38 -07:00
Waleed
e9c94fa462 feat(logs): add copy link and deep link support for log entries (#3855)
* feat(logs): add copy link and deep link support for log entries

* fix(logs): fetch next page when deep linked log is beyond initial page

* fix(logs): move Link icon to emcn and handle clipboard rejections

* fix(logs): track isFetching reactively and drop empty-list early-return

- Remove  guard that prevented clearing the
  pending ref when filters return no results
- Use  directly in the condition and add it to
  the effect deps so the effect re-triggers after a background refetch

* fix(logs): guard deep-link ref clear until query has succeeded

Only clear pendingExecutionIdRef when the query status is 'success',
preventing premature clearing before the initial fetch completes.
On mount, the query is disabled (isInitialized.current starts false),
so hasNextPage is false but no data has loaded yet — the ref was being
cleared in the same effect pass that set it.

* fix(logs): guard fetchNextPage call until query has succeeded

Add logsQuery.status === 'success' to the fetchNextPage branch so it
mirrors the clear branch. On mount the query is disabled (isFetching is
false, status is pending), causing the effect to call fetchNextPage()
before the query is initialized — now both branches require success.
2026-03-30 18:42:27 -07:00
Waleed
72eea64bf6 improvement(tour): align product tour tooltip styling with emcn and fix spotlight overflow (#3854) 2026-03-30 16:59:53 -07:00
Waleed
27460f847c fix(atlassian): harden cloud ID resolution for Confluence and Jira (#3853) 2026-03-30 16:54:45 -07:00
Vikhyath Mondreti
c7643198dc fix(mothership): hang condition (#3852) 2026-03-30 16:47:54 -07:00
Waleed
e5aef6184a feat(profound): add Profound AI visibility and analytics integration (#3849)
* feat(profound): add Profound AI visibility and analytics integration

* fix(profound): fix import ordering and JSON formatting for CI lint

* fix(profound): gate metrics mapping on current operation to prevent stale overrides

* fix(profound): guard JSON.parse on filters, fix offset=0 falsy check, remove duplicate prompt_answers in FILTER_OPS

* lint

* fix(docs): fix import ordering and trailing newline for docs lint

* fix(scripts): sort generated imports to match Biome's organizeImports order

* fix(profound): use != null checks for limit param across all tools

* fix(profound): flatten block output type to 'json' to pass block validation test

* fix(profound): remove invalid 'required' field from block inputs (not part of ParamConfig)

* fix(profound): rename tool files from kebab-case to snake_case for docs generator compatibility

* lint

* fix(docs): let biome auto-fix import order, revert custom sort in generator

* fix(landing): fix import order in sim icon-mapping via biome

* fix(scripts): match Biome's exact import sort order in docs generator

* fix(generate-docs): produce Biome-compatible JSON output

The generator wrote multi-line arrays for short string arrays (like tags)
and omitted trailing newlines, causing Biome format check failures in CI.
Post-process integrations.json to collapse short arrays onto single lines
and add trailing newlines to both integrations.json and meta.json.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 16:30:06 -07:00
Waleed
4ae5b1b620 improvement(workflow): use DOM hit-testing for edge drop-on-block detection (#3851) 2026-03-30 16:20:30 -07:00
Waleed
5c334874eb fix(auth): use standard 'Unauthorized' error in hybrid auth responses (#3850) 2026-03-30 16:11:04 -07:00
Theodore Li
d3d58a9615 Feat/improved logging (#3833)
* feat(logs): add additional metadata for workflow execution logs

* Revert "Feat(logs) upgrade mothership chat messages to error (#3772)"

This reverts commit 9d1b9763c5.

* Fix lint, address greptile comments

* improvement(sidebar): expand sidebar by hovering and clicking the edge (#3830)

* improvement(sidebar): expand sidebar by hovering and clicking the edge

* improvement(sidebar): add keyboard shortcuts for new workflow/task, center search modal, fix edge ARIA

* improvement(sidebar): use Tooltip.Shortcut for inline shortcut display

* fix(sidebar): change new workflow shortcut from Mod+Shift+W to Mod+Shift+P to avoid browser close-window conflict

* fix(hotkeys): fall back to event.code for international keyboard layout compatibility

* fix(sidebar): guard add-workflow shortcut with canEdit and isCreatingWorkflow checks

* feat(ui): handle image paste (#3826)

* feat(ui): handle image paste

* Fix lint

* Fix type error

---------

Co-authored-by: Theodore Li <theo@sim.ai>

* feat(files): interactive markdown checkbox toggling in preview (#3829)

* feat(files): interactive markdown checkbox toggling in preview

* fix(files): handle ordered-list checkboxes and fix index drift

* lint

* fix(files): remove counter offset that prevented checkbox toggling

* fix(files): apply task-list styling to ordered lists too

* fix(files): render single pass when interactive to avoid index drift

* fix(files): move useMemo above conditional return to fix Rules of Hooks

* fix(files): pass content directly to preview when not streaming to avoid stale frame

* improvement(home): position @ mention popup at caret and fix icon consistency (#3831)

* improvement(home): position @ mention popup at caret and fix icon consistency

* fix(home): pin mirror div to document origin and guard button anchor

* chore(auth): restore hybrid.ts to staging

* improvement(ui): sidebar (#3832)

* Fix logger tests

* Add metadata to mothership logs

---------

Co-authored-by: Theodore Li <theo@sim.ai>
Co-authored-by: Waleed <walif6@gmail.com>
Co-authored-by: Theodore Li <theo@sim.ai>
2026-03-30 19:02:17 -04:00
Waleed
1d59eca90a fix(analytics): use getBaseDomain for Profound host field (#3848)
request.url resolves to internal ALB hostname on ECS, not the public domain
2026-03-30 15:36:41 -07:00
Theodore Li
e1359b09d6 feat(block) add block write and append operations (#3665)
* Add file write and delete operations

* Add file block write operation

* Fix lint

* Allow loop-in-loop workflow edits

* Fix type error

* Remove file id input, output link correctly

* Add append tool

* fix lint

* Address feedback

* Handle writing to same file name gracefully

* Removed  mime type from append block

* Add lock for file append operation

---------

Co-authored-by: Theodore Li <theo@sim.ai>
2026-03-30 18:08:51 -04:00
Waleed
35b3646330 fix(sidebar): cmd+click opens in new tab, shift+click for range select (#3846)
* fix(sidebar): cmd+click opens in new tab, shift+click for range select

* comment cleanup

* fix(sidebar): drop stale metaKey param from workflow and task selection hooks
2026-03-30 11:02:33 -07:00
Waleed
73e00f53e1 v0.6.17: trigger.dev CI, workers FF 2026-03-30 09:33:30 -07:00
Waleed
5c47ea58f8 chore(trigger): update @trigger.dev/sdk and @trigger.dev/build to 4.4.3 (#3843)
* chore(trigger): update @trigger.dev/sdk and @trigger.dev/build to 4.4.3

* fix(webhooks): execute non-polling webhooks inline when BullMQ is disabled
2026-03-30 09:27:15 -07:00
Vikhyath Mondreti
1d7ae906bc v0.6.16: bullmq optionality 2026-03-30 00:12:21 -07:00
Vikhyath Mondreti
c4f4e6b48c fix(bullmq): disable temporarily (#3841) 2026-03-30 00:04:59 -07:00
Waleed
560fa75155 v0.6.15: workers, security hardening, sidebar improvements, chat fixes, profound 2026-03-29 23:02:19 -07:00
Waleed
1728c370de improvement(landing): lighthouse performance and accessibility fixes (#3837)
* improvement(landing): lighthouse performance and accessibility fixes

* improvement(landing): extract FeatureToggleItem to deduplicate accessibility logic

* lint

* fix(landing): ensure explicit delay prop takes precedence over transition spread
2026-03-29 22:33:34 -07:00
Waleed
82e58a5082 fix(academy): hide academy pages until content is ready (#3839)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 22:18:13 -07:00
Waleed
336c065234 fix(viewer): image pan/zoom, sort fixes, sidebar dot fixes (#3836)
* feat(file-viewer): add pan and zoom to image preview

* fix(viewer): fix sort key mapping, disable load-more on sort, hide status dots when menu open

* fix(file-viewer): prevent scroll bleed and zoom button micro-pans

* fix(file-viewer): use exponential zoom formula to prevent zero/negative multiplier
2026-03-29 12:40:29 -07:00
Waleed
b3713642b2 feat(resources): add sort and filter to all resource list pages (#3834)
* improvement(tables): improve table filtering UX

- Replace popover filter with persistent inline panel below toolbar
- Add AND/OR toggle between filter rules (shown in Where label slot)
- Sync filter panel state from applied filter on open
- Show filter button active state when filter is applied or panel is open
- Use readable operator labels matching dropdown options
- Add Clear filters button (shown only when filter is active)
- Close filter panel when last rule is removed via X
- Fix empty gap rows appearing in filtered results by skipping position gap rendering when filter is active
- Add toggle mode to ResourceOptionsBar for inline panel pattern
- Memoize FilterRuleRow for perf, fix filterTags key collision, remove dead filterActiveCount prop

* fix(table-filter): use ref to stabilize handleRemove/handleApply callbacks

Reading rules via ref instead of closure eliminates rules from useCallback
dependency arrays, keeping callbacks stable across rule edits and preserving
the memo() benefit on FilterRuleRow.

* improvement(tables,kb): remove hacky patterns, fix KB filter popover width

- Remove non-TSDoc comment from table-filter (rulesRef pattern is self-evident)
- Simplify SearchSection: remove setState-during-render anti-pattern; controlled
  input binds directly to search.value/onChange (simpler and correct)
- Reduce KB filter popover from w-[320px] to w-[200px]; tag filter uses vertical
  layout so narrow width works; Status-only case is now appropriately compact

* feat(knowledge): add sort and filter to KB list page

Sort dropdown: name, documents, tokens, created, last updated — pre-sorted
externally before passing rows to Resource. Active sort highlights the Sort
button; clear resets to default (created desc).

Filter popover: filter by connector status (All / With connectors /
Without connectors). Active filter shown as a removable tag in the toolbar.

* feat(files): add sort and filter to files list page

* feat(scheduled-tasks): add sort and filter to scheduled tasks page

* fix(table-filter): use explicit close handler instead of toggle

* improvement(files,knowledge): replace manual debounce with useDebounce hook and use type guards for file filtering

* fix(resource): prevent popover from inheriting anchor min-width

* feat(tables): add sort to tables list page

* feat(knowledge): add content and owner filters to KB list

* feat(scheduled-tasks): add status and health filters

* feat(files): add size and uploaded-by filters to files list

* feat(tables): add row count, owner, and column type filters

* improvement(scheduled-tasks): use combobox filter panel matching logs UI style

* improvement(knowledge): use combobox filter panel matching logs UI style

* improvement(files): use combobox filter panel matching logs UI style

Replaces button-list filters with Combobox-based multi-select sections for file type, size, and uploaded-by filters, aligning the panel with the logs page filter UI.

* improvement(tables): use combobox filter panel matching logs UI style

* feat(settings): add sort to recently deleted page

Add a sort dropdown next to the search bar allowing users to sort by deletion date (default, newest first), name (A–Z), or type (A–Z).

* feat(logs): add sort to logs page

* improvement(knowledge): upgrade document list filter to combobox style

* fix(resources): fix missing imports, memoization, and stale refs across resource pages

* improvement(tables): remove column type filter

* fix(resources): fix filter/sort correctness issues from audit

* fix(chunks): add server-side sort to document chunks API

Chunk sort was previously done client-side on a single page of
server-paginated data, which only reordered the current page.
Now sort params (sortBy, sortOrder) flow through the full stack:
types → service → API route → query hook → useDocumentChunks → document.tsx.

* perf(resources): memoize filterContent JSX across all resource pages

Resource is wrapped in React.memo, so an unstable filterContent reference
on every parent re-render defeats the memo. Wrap filterContent in useMemo
with correct deps in all 6 pages (files, tables, scheduled-tasks, knowledge,
base, document).

* fix(resources): add missing sort options for all visible columns

Every column visible in a resource table should be sortable. Three pages
had visible columns with no sort support:
- files.tsx: add 'owner' sort (member name lookup)
- scheduled-tasks.tsx: add 'schedule' sort (localeCompare on description)
- knowledge.tsx: add 'connectors' (count) and 'owner' (member name) sorts

Also add 'members' to processedKBs deps in knowledge.tsx since owner
sort now reads member names inside the memo.

* whitelabeling updates, sidebar fixes, files bug

* increased type safety

* pr fixes
2026-03-28 23:31:54 -07:00
Waleed
b9b930bb63 feat(analytics): add Profound web traffic tracking (#3835)
* feat(analytics): add Profound web traffic tracking

* fix(analytics): address PR review — add endpoint check and document trade-offs

* chore(analytics): remove implementation comments

* fix(analytics): guard sendToProfound with try-catch and align check with isProfoundEnabled

* fix(analytics): strip sensitive query params and remove redundant guard

* chore(analytics): remove unnecessary query param filtering
2026-03-28 22:09:23 -07:00
Vikhyath Mondreti
f1ead2ed55 fix docker image build 2026-03-28 20:58:56 -07:00
Waleed
30377d775b improvement(ui): sidebar (#3832) 2026-03-28 15:24:07 -07:00
Waleed
d013132d0e improvement(home): position @ mention popup at caret and fix icon consistency (#3831)
* improvement(home): position @ mention popup at caret and fix icon consistency

* fix(home): pin mirror div to document origin and guard button anchor

* chore(auth): restore hybrid.ts to staging
2026-03-28 14:48:41 -07:00
Waleed
7b0ce8064a feat(files): interactive markdown checkbox toggling in preview (#3829)
* feat(files): interactive markdown checkbox toggling in preview

* fix(files): handle ordered-list checkboxes and fix index drift

* lint

* fix(files): remove counter offset that prevented checkbox toggling

* fix(files): apply task-list styling to ordered lists too

* fix(files): render single pass when interactive to avoid index drift

* fix(files): move useMemo above conditional return to fix Rules of Hooks

* fix(files): pass content directly to preview when not streaming to avoid stale frame
2026-03-28 14:26:29 -07:00
Theodore Li
0ea73263df feat(ui): handle image paste (#3826)
* feat(ui): handle image paste

* Fix lint

* Fix type error

---------

Co-authored-by: Theodore Li <theo@sim.ai>
2026-03-28 17:15:33 -04:00
Waleed
edc502384b improvement(sidebar): expand sidebar by hovering and clicking the edge (#3830)
* improvement(sidebar): expand sidebar by hovering and clicking the edge

* improvement(sidebar): add keyboard shortcuts for new workflow/task, center search modal, fix edge ARIA

* improvement(sidebar): use Tooltip.Shortcut for inline shortcut display

* fix(sidebar): change new workflow shortcut from Mod+Shift+W to Mod+Shift+P to avoid browser close-window conflict

* fix(hotkeys): fall back to event.code for international keyboard layout compatibility

* fix(sidebar): guard add-workflow shortcut with canEdit and isCreatingWorkflow checks
2026-03-28 14:06:30 -07:00
Waleed
e2be99263c feat(academy): Sim Academy — interactive partner certification platform (#3824)
* fix(import): dedup workflow name (#3813)

* feat(concurrency): bullmq based concurrency control system (#3605)

* feat(concurrency): bullmq based queueing system

* fix bun lock

* remove manual execs off queues

* address comments

* fix legacy team limits

* cleanup enterprise typing code

* inline child triggers

* fix status check

* address more comments

* optimize reconciler scan

* remove dead code

* add to landing page

* Add load testing framework

* update bullmq

* fix

* fix headless path

---------

Co-authored-by: Theodore Li <teddy@zenobiapay.com>

* fix(linear): add default null for after cursor (#3814)

* fix(knowledge): reject non-alphanumeric file extensions from document names (#3816)

* fix(knowledge): reject non-alphanumeric file extensions from document names

* fix(knowledge): improve error message when extension is non-alphanumeric

* fix(security): SSRF, access control, and info disclosure (#3815)

* fix(security): scope copilot feedback GET endpoint to authenticated user

Add WHERE clause to filter feedback records by the authenticated user's
ID, preventing any authenticated user from reading all users' copilot
interactions, queries, and workflow YAML (IDOR / CWE-639).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(smtp): add SSRF validation and genericize network error messages

Prevent SSRF via user-controlled smtpHost by validating with
validateDatabaseHost before creating the nodemailer transporter.
Collapse distinct network error messages (ECONNREFUSED, ECONNRESET,
ETIMEDOUT) into a single generic message to prevent port-state leakage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): add SSRF validation to SFTP/SSH and access control to workspace invitations

Add `validateDatabaseHost` checks to SFTP and SSH connection utilities to
block connections to private/reserved IPs and localhost, matching the
existing pattern used by all database tools. Add authorization check to
the workspace invitation GET endpoint so only the invitee or a workspace
admin can view invitation details.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(smtp): restore SMTP response code handling for post-connection errors

SMTP 4xx/5xx response codes are application-level errors (invalid
recipient, mailbox full, server error) unrelated to the SSRF hardening
goal. Restore response code differentiation and logging to preserve
actionable user-facing error messages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): use session email directly instead of extra DB query

Addresses PR review feedback — align with the workspace invitation
route pattern by using session.user.email instead of re-fetching
from the database.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* lint

* fix(auth): revert lint autofix that broke hasExternalApiCredentials return type

Biome auto-fixed `return auth !== null && auth.startsWith(...)` to
`return auth?.startsWith(...)` which returns `boolean | undefined`,
not `boolean`, causing a TypeScript build failure.

* fix(smtp): pin resolved IP to prevent DNS rebinding (TOCTOU)

Use the pre-resolved IP from validateDatabaseHost instead of the
original hostname when creating the nodemailer transporter. Set
servername to the original hostname to preserve TLS SNI validation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(security): extract createPinnedLookup helper for DNS rebinding prevention

Extract reusable createPinnedLookup from secureFetchWithPinnedIP so
non-HTTP transports (SSH, SFTP, IMAP) can pin resolved IPs at the
socket level. SMTP route uses host+servername pinning instead since
nodemailer doesn't reliably pass lookup to both secure/plaintext paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): pin IMAP connections to validated resolved IP

Pass the resolved IP from validateDatabaseHost to ImapFlow as host,
with the original hostname as servername for TLS SNI verification.
Closes the DNS TOCTOU rebinding window.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* lint

* fix(auth): revert lint autofix on hasExternalApiCredentials return type

Also pin SFTP/SSH connections to validated resolved IP to prevent DNS rebinding.

* fix(security): short-circuit admin check when caller is invitee

Skip the hasWorkspaceAdminAccess DB query when the caller is already
the invitee, avoiding an unnecessary round-trip. Aligns with the org
invitation route pattern.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* fix(worker): dockerfile + helm updates (#3818)

* fix(worker): dockerfile + helm updates

* address comments

* update dockerfile (#3819)

* fix dockerfile

* fix(security): pentest remediation — condition escaping, SSRF hardening, ReDoS protection (#3820)

* fix(executor): escape newline characters in condition expression strings

Unescaped newline/carriage-return characters in resolved string values
cause unterminated string literals in generated JS, crashing condition
evaluation with a SyntaxError.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): prevent ReDoS in guardrails regex validation

Add safe-regex2 to reject catastrophic backtracking patterns before
execution and cap input length at 10k characters.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): SSRF localhost hardening and regex DoS protection

Block localhost/loopback URLs in hosted environments using isHosted flag
instead of allowHttp. Add safe-regex2 validation and input length limits
to regex guardrails to prevent catastrophic backtracking.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): validate regex syntax before safety check

Move new RegExp() before safe() so invalid patterns get a proper syntax
error instead of a misleading "catastrophic backtracking" message.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): address PR review feedback

- Hoist isLocalhost && isHosted guard to single early-return before
  protocol checks, removing redundant duplicate block
- Move regex syntax validation (new RegExp) before safe-regex2 check
  so invalid patterns get proper syntax error instead of misleading
  "catastrophic backtracking" message

* fix(security): remove input length cap from regex validation

The 10k character cap would block legitimate guardrail checks on long
LLM outputs. Input length doesn't affect ReDoS risk — the safe-regex2
pattern check already prevents catastrophic backtracking.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(tests): mock isHosted in input-validation and function-execute tests

Tests that assert self-hosted localhost behavior need isHosted=false,
which is not guaranteed in CI where NEXT_PUBLIC_APP_URL is set to the
hosted domain.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* improvement(worker): configuration defaults (#3821)

* improvement(worker): configuration defaults

* update readmes

* realtime curl import

* improvement(tour): remove auto-start, only trigger on explicit user action (#3823)

* fix(mcp): use correct modal for creating workflow MCP servers in deploy (#3822)

* fix(mcp): use correct modal for creating workflow MCP servers in deploy

* fix(mcp): show workflows field during loading and when empty

* mock course

* fix(db): use bigint for token counter columns in user_stats (#3755)

* mock course

* updates

* updated X handle for emir

* cleanup: audit and clean academy implementation

* fix(academy): add label to ValidationRule, fix quiz gating, simplify getRuleMessage

* cleanup: remove unnecessary comments across academy files

* refactor(academy): simplify abstractions and fix perf issues

* perf(academy): convert course detail page to server component with client island

* fix(academy): null-safe canAdvance, render exercise instructions, remove stale comments

* fix(academy): remove orphaned migration, fix getCourseById, clean up comments

- Delete 0181_academy_certificate.sql (orphaned duplicate not in journal)
- Add getCourseById() to content/index.ts; use it in certificates API
  (was using getCourse which searches by slug, not stable id)
- Remove JSX comments from catalog page
- Remove redundant `passed` recomputation in LessonQuiz

* chore(db): regenerate academy_certificate migration with drizzle-kit

* chore: include blog mdx and components changes

* fix(blog): correct cn import path

* fix(academy): constrain progress bar to max-w-3xl with proper padding

* feat(academy): show back-to-course button on first lesson

* fix(academy): force dark theme on all /academy routes

* content(academy): rewrite sim-foundations course with full 6-module curriculum

* fix(academy): correct edge handles, quiz explanation, and starter mock outputs

- Fix Exercise 2 initial edge handles: 'starter-1-source'/'agent-1-target' → 'source'/'target' (React Flow actual IDs)
- Fix M1-L4 Q4 quiz explanation: remove non-existent Ctrl/Cmd+D and Alt+drag shortcuts
- Add starter mock output to all exercises so run animation shows feedback on the first block

* refine(academy): fix inaccurate content and improve exercise clarity

- Fix Exercise 3: replace hardcoded <agent-1.content> (invalid UUID-based ref) with reference picker instructions
- Fix M4 Quiz Q5: Loop block (subflow container) is correct answer, not the Workflow block
- Fix M4 Quiz Q4: clarify fan-out vs Parallel block distinction in explanation
- Fix M4-L2 video description: accurately describe Loop and Parallel subflow blocks
- Fix M2 Quiz Q3: make response format question conceptual rather than syntax-specific
- Improve Exercise 4 branching instructions: clarify top=true / bottom=false output handles
- Improve Final Project instructions: step-by-step numbered flow

* fix(academy): remove double border on quiz question cards

* fix(academy): single scroll container on lesson pages — remove nested flex scroll

* fix(academy): remove min-h-screen from root layout — fixes double scrollbar on lesson pages

* fix(academy): use fixed inset-0 on lesson page to eliminate document-level scrollbar

* fix(academy): replace sr-only radio/checkbox inputs with buttons to prevent scroll-on-focus; restore layout min-h-screen

* improvement(academy): polish, security hardening, and certificate claim UI

- Replace raw localStorage with BrowserStorage utility in local-progress
- Pre-compute slug/id Maps in content/index for O(1) course lookups
- Move blockMap construction into edge_exists branch only in validation
- Extract navBtnClass constant and MetaRow/formatDate helpers in UI
- Add rate limiting, server-side completion verification, audit logging, and nanoid cert numbers to certificate issuance endpoint
- Add useIssueCertificate mutation hook with completedLessonIds
- Wire certificate claim UI into CourseProgress: sign-in prompt, claim button with loading state, and post-issuance view with link to certificate page
- Fix lesson page scroll container and quiz scroll-on-focus bug

* fix(academy): validate condition branch handles in edge_exists rules

- Add sourceHandle field to edge_exists ValidationRule type
- Check sourceHandle in validation.ts when specified
- Require both condition-if and condition-else branches to be connected in the branching and final project exercises

* fix(academy): address PR review — isHosted regression, stuck isExecuting, revoked cert 500, certificate SSR

- Restore env-var-based isHosted check (was hardcoded true, breaking self-hosted deployments)
- Fix isExecuting stuck at true when mock run fails validation — set isMockRunningRef immediately and reset both flags on early exit
- Fix revoked/expired certificate causing 500 — any existing record (not just active) now returns 409 instead of falling through to INSERT
- Convert certificate verification page from client component to server component — direct DB fetch, notFound() on missing cert, generateMetadata for SEO/social previews

* fix(auth): restore hybrid.ts from staging to fix CI type error

* fix(academy): mark video lessons complete on visit and fix sign-in path

* fix(academy): replace useEffect+setState with lazy useState initializer in CourseProgress

* fix(academy): reset exerciseComplete on lesson navigation, remove unused useAcademyCertificate hook

* fix(academy): useState for slug-change reset, cache() for cert page, handleMockRunRef for stale closure

* fix(academy): replace shadcn theme vars with explicit hex in LessonVideo fallback

* fix(academy): reset completedRef on exercise change, conditional verified badge, multi-select empty guard

* fix(academy): type safety fixes — null metadata fallbacks, returning() guard, exhaustive union, empty catch

* fix(academy): reset ExerciseView completed banner on nav; fix CourseProgress hydration mismatch

* fix(lightbox): guard effect body with isOpen to prevent spurious overflow reset

* fix(academy): reset LessonQuiz state on lesson change to prevent stale answers persisting

* fix(academy): course not-found metadata title; try-finally guard in mock run loop

* fix(academy): type safety, cert persistence, regex guard, mixed-lesson video, shorts support

- Derive AcademyCertificate from db $inferSelect to prevent schema drift
- Add useCourseCertificate query hook; GET /api/academy/certificates now accepts courseId for authenticated lookup
- Use useCourseCertificate in CourseProgress so certificate state survives page refresh
- Guard new RegExp(valuePattern) in validation.ts with try/catch; log warn on invalid pattern
- Add logger.warn for custom validation rules so content authors are alerted
- Add YouTube Shorts URL support to LessonVideo (youtube.com/shorts/VIDEO_ID)
- Fix mixed-lesson video gap: render videoUrl above quiz when mixed has quiz but no exercise
- Add academy-scoped not-found.tsx with link back to /academy

* fix(academy): reset hintIndex when exercise changes

* chore: remove ban-spam-accounts script (wrong branch)

* fix(academy): enforce availableBlocks in toolbar; fix mixed exercise+quiz rendering

- Add useSandboxBlockConstraints context; SandboxCanvasProvider provides exerciseConfig.availableBlocks so the toolbar only shows permitted block types. Empty array hides all blocks (configure-only exercises); non-null array restricts to listed types; triggers always hidden in sandbox.
- Fix mixed lesson with both exerciseConfig and quizConfig: exercise renders first, quiz reveals after exercise completes (sequential pedagogy). canAdvance now requires both exerciseComplete && quizComplete when both are present.

* chore(academy): remove extraneous inline comments

* fix(academy): blank mixed lesson, quiz canAdvance flag, empty-array valueNotEmpty

* prep for merge

* chore(db): regenerate academy certificate migration after staging merge

* fix(academy): disable auto-connect in sandbox mode

* fix(academy): render video in mixed lesson with no exercise or quiz

* fix(academy): mark mixed video-only lessons complete; handle cert insert race

* fix(canvas): add sandbox and embedded to nodes useMemo deps

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Lakee Sivaraya <71339072+lakeesiv@users.noreply.github.com>
Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
Co-authored-by: Siddharth Ganesan <33737564+Sg312@users.noreply.github.com>
Co-authored-by: Theodore Li <teddy@zenobiapay.com>
2026-03-28 12:46:04 -07:00
Waleed
f6b461ad47 fix(readme): restore readme gifs (#3827)
The static GIFs referenced by README.md were removed in #3803.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-28 12:36:23 -07:00
Waleed
e4d35735b1 fix(knowledge): give users choice to keep or delete documents when removing connector (#3825)
* fix(knowledge): give users choice to keep or delete documents when removing connector

* refactor(knowledge): clean up connector delete and extract shared extension validator

- Extract `isAlphanumericExtension` helper to deduplicate regex across parser-extension.ts and validation.ts
- Extract `closeDeleteModal` callback to eliminate 4x scattered state resets
- Add archivedAt/deletedAt filters to UPDATE query in keep-docs delete path
- Parallelize storage file cleanup and tag definition cleanup with Promise.all
- Deduplicate URL construction in delete connector hook

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(knowledge): remove duplicate extension list from parser-extension

Use SUPPORTED_DOCUMENT_EXTENSIONS and isSupportedExtension from
validation.ts instead of maintaining a separate identical list.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(db): change document.connectorId FK from cascade to set null

The cascade behavior meant deleting a connector would always delete
its documents, contradicting the "keep documents" option. With set null,
the database automatically nullifies connectorId when a connector is
removed, and we only need explicit deletion when the user opts in.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore(db): add migration metadata for connectorId FK change

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(knowledge): fix connector delete test and use URL-safe searchParams

Use `new URL(request.url).searchParams` instead of `request.nextUrl.searchParams`
for compatibility with test mocks. Add missing `connectorType` to test fixture.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* spacing

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-28 12:04:26 -07:00
Waleed
b4064c57fb fix(mcp): use correct modal for creating workflow MCP servers in deploy (#3822)
* fix(mcp): use correct modal for creating workflow MCP servers in deploy

* fix(mcp): show workflows field during loading and when empty
2026-03-27 20:22:30 -07:00
Waleed
eac41ca105 improvement(tour): remove auto-start, only trigger on explicit user action (#3823) 2026-03-27 19:59:47 -07:00
Vikhyath Mondreti
d2c3c1c39e improvement(worker): configuration defaults (#3821)
* improvement(worker): configuration defaults

* update readmes

* realtime curl import
2026-03-27 19:54:56 -07:00
Waleed
8f3e864751 fix(security): pentest remediation — condition escaping, SSRF hardening, ReDoS protection (#3820)
* fix(executor): escape newline characters in condition expression strings

Unescaped newline/carriage-return characters in resolved string values
cause unterminated string literals in generated JS, crashing condition
evaluation with a SyntaxError.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): prevent ReDoS in guardrails regex validation

Add safe-regex2 to reject catastrophic backtracking patterns before
execution and cap input length at 10k characters.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): SSRF localhost hardening and regex DoS protection

Block localhost/loopback URLs in hosted environments using isHosted flag
instead of allowHttp. Add safe-regex2 validation and input length limits
to regex guardrails to prevent catastrophic backtracking.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): validate regex syntax before safety check

Move new RegExp() before safe() so invalid patterns get a proper syntax
error instead of a misleading "catastrophic backtracking" message.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): address PR review feedback

- Hoist isLocalhost && isHosted guard to single early-return before
  protocol checks, removing redundant duplicate block
- Move regex syntax validation (new RegExp) before safe-regex2 check
  so invalid patterns get proper syntax error instead of misleading
  "catastrophic backtracking" message

* fix(security): remove input length cap from regex validation

The 10k character cap would block legitimate guardrail checks on long
LLM outputs. Input length doesn't affect ReDoS risk — the safe-regex2
pattern check already prevents catastrophic backtracking.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(tests): mock isHosted in input-validation and function-execute tests

Tests that assert self-hosted localhost behavior need isHosted=false,
which is not guaranteed in CI where NEXT_PUBLIC_APP_URL is set to the
hosted domain.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 19:54:00 -07:00
Vikhyath Mondreti
23c3072784 fix dockerfile 2026-03-27 19:11:02 -07:00
Vikhyath Mondreti
33fdb11396 update dockerfile (#3819) 2026-03-27 18:50:57 -07:00
Vikhyath Mondreti
21156dd54a fix(worker): dockerfile + helm updates (#3818)
* fix(worker): dockerfile + helm updates

* address comments
2026-03-27 18:28:36 -07:00
Waleed
c05e2e0fc8 fix(security): SSRF, access control, and info disclosure (#3815)
* fix(security): scope copilot feedback GET endpoint to authenticated user

Add WHERE clause to filter feedback records by the authenticated user's
ID, preventing any authenticated user from reading all users' copilot
interactions, queries, and workflow YAML (IDOR / CWE-639).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(smtp): add SSRF validation and genericize network error messages

Prevent SSRF via user-controlled smtpHost by validating with
validateDatabaseHost before creating the nodemailer transporter.
Collapse distinct network error messages (ECONNREFUSED, ECONNRESET,
ETIMEDOUT) into a single generic message to prevent port-state leakage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): add SSRF validation to SFTP/SSH and access control to workspace invitations

Add `validateDatabaseHost` checks to SFTP and SSH connection utilities to
block connections to private/reserved IPs and localhost, matching the
existing pattern used by all database tools. Add authorization check to
the workspace invitation GET endpoint so only the invitee or a workspace
admin can view invitation details.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(smtp): restore SMTP response code handling for post-connection errors

SMTP 4xx/5xx response codes are application-level errors (invalid
recipient, mailbox full, server error) unrelated to the SSRF hardening
goal. Restore response code differentiation and logging to preserve
actionable user-facing error messages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): use session email directly instead of extra DB query

Addresses PR review feedback — align with the workspace invitation
route pattern by using session.user.email instead of re-fetching
from the database.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* lint

* fix(auth): revert lint autofix that broke hasExternalApiCredentials return type

Biome auto-fixed `return auth !== null && auth.startsWith(...)` to
`return auth?.startsWith(...)` which returns `boolean | undefined`,
not `boolean`, causing a TypeScript build failure.

* fix(smtp): pin resolved IP to prevent DNS rebinding (TOCTOU)

Use the pre-resolved IP from validateDatabaseHost instead of the
original hostname when creating the nodemailer transporter. Set
servername to the original hostname to preserve TLS SNI validation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(security): extract createPinnedLookup helper for DNS rebinding prevention

Extract reusable createPinnedLookup from secureFetchWithPinnedIP so
non-HTTP transports (SSH, SFTP, IMAP) can pin resolved IPs at the
socket level. SMTP route uses host+servername pinning instead since
nodemailer doesn't reliably pass lookup to both secure/plaintext paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): pin IMAP connections to validated resolved IP

Pass the resolved IP from validateDatabaseHost to ImapFlow as host,
with the original hostname as servername for TLS SNI verification.
Closes the DNS TOCTOU rebinding window.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* lint

* fix(auth): revert lint autofix on hasExternalApiCredentials return type

Also pin SFTP/SSH connections to validated resolved IP to prevent DNS rebinding.

* fix(security): short-circuit admin check when caller is invitee

Skip the hasWorkspaceAdminAccess DB query when the caller is already
the invitee, avoiding an unnecessary round-trip. Aligns with the org
invitation route pattern.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 18:10:47 -07:00
Waleed
a7c1e510e6 fix(knowledge): reject non-alphanumeric file extensions from document names (#3816)
* fix(knowledge): reject non-alphanumeric file extensions from document names

* fix(knowledge): improve error message when extension is non-alphanumeric
2026-03-27 16:58:07 -07:00
Vikhyath Mondreti
271624a402 fix(linear): add default null for after cursor (#3814) 2026-03-27 15:57:40 -07:00
Vikhyath Mondreti
dda012eae9 feat(concurrency): bullmq based concurrency control system (#3605)
* feat(concurrency): bullmq based queueing system

* fix bun lock

* remove manual execs off queues

* address comments

* fix legacy team limits

* cleanup enterprise typing code

* inline child triggers

* fix status check

* address more comments

* optimize reconciler scan

* remove dead code

* add to landing page

* Add load testing framework

* update bullmq

* fix

* fix headless path

---------

Co-authored-by: Theodore Li <teddy@zenobiapay.com>
2026-03-27 13:11:35 -07:00
Vikhyath Mondreti
2dd6d3d1e6 fix(import): dedup workflow name (#3813) 2026-03-27 13:09:49 -07:00
Waleed
14089f7dbb v0.6.14: performance improvements, connectors UX, collapsed sidebar actions 2026-03-27 13:07:59 -07:00
Waleed
b90bb75cda fix(knowledge): connector spinner race condition + connectors column (#3812)
* fix(knowledge): scope sync/update state per-connector to prevent race conditions

* feat(knowledge): add connectors column to knowledge base list

* refactor(knowledge): extract set helpers, handleTogglePause, and filter-before-map

* refactor(knowledge): use onSettled for syncingIds cleanup, consistent with updatingIds
2026-03-27 12:54:14 -07:00
Waleed
fb233d003d fix(flyout): align inline rename with non-rename styling (#3811) 2026-03-27 12:39:23 -07:00
Waleed
34df3333d1 fix(knowledge): fix search input flicker on clear and plan display name fallback (#3810) 2026-03-27 12:23:41 -07:00
Waleed
23677d41a0 improvement(sidebar): collapsed sidebar UX, quick-create, hover consistency, and UI polish (#3807)
* improvement(sidebar): collapsed sidebar UX, quick-create, hover consistency, and UI polish

Made-with: Cursor

* fix(sidebar): use stable handlers for root workflow items instead of inline lambdas

Made-with: Cursor

* fix(sidebar): reset actionsOpen state before triggering rename in collapsed dropdown

Made-with: Cursor
2026-03-27 12:08:17 -07:00
Waleed
a489f91085 fix(knowledge): show spinner on connector chip while syncing (#3808)
* fix(knowledge): show spinner on connector chip while syncing

* fix(knowledge): scope sync spinner to mutation lifetime, not cooldown
2026-03-27 12:04:11 -07:00
Adithya Krishna
ed6e7845cc chore: fix rerenders on files (#3805)
* chore: fix rerenders on files

* chore: fix review changes
2026-03-27 11:48:51 -07:00
Adithya Krishna
e698f9fe14 chore: remove font antialiasing (#3806)
* chore: fix antialiasing

* chore: remove antialiasing
2026-03-27 11:29:37 -07:00
Adithya Krishna
db1798267e feat: update sidebar and knowledge (#3804)
* feat: update sidebar and knowledge

* chore: fix rernders on knowledge

* chore: fix review changes

* chore: fix review changes
2026-03-27 09:39:41 -07:00
Waleed
e615816dce v0.6.13: emcn standardization, granola and ketch integrations, security hardening, connectors improvements 2026-03-27 00:16:37 -07:00
Waleed
5f1d5e0618 feat(generic): add generic resource tab, refactor home structure, and UI polish (#3803)
* feat(generic): add generic resource tab, refactor home structure, and UI polish

* reverted hardcoded ff

* fix build

* styling consistency

* styling

* fix(auth): extract shared auth button class and align SSO primary style

- Extract AUTH_SUBMIT_BTN constant to (auth)/components/auth-button-classes.ts,
  replacing 10 copy-pasted identical className strings across 7 files
- Update SSOLoginButton primary variant to use AUTH_SUBMIT_BTN instead of
  hardcoded purple gradient, making it consistent with all other auth form
  submit buttons
- Fix missing isEphemeralResource import in lib/copilot/resources.ts
  (was re-exported but not available in local scope)

* fix(auth): replace inline button class in chat auth components with AUTH_SUBMIT_BTN

* fix send button hover state
2026-03-27 00:13:41 -07:00
Vikhyath Mondreti
ed5645166e improvement(terminal): performance improvements (#3796)
* improvement(terminal): prevent canvas crashes

* checkpoint

* make persistence execution scoped not debounce

* revert feature flags

* address bugbot comments

* fix test

* fix

* fix type

* fix abortion of manual run

* fix type errors

* fix diff comment

* fix chat history query

* fix comment

* Ignore previously executed tool call ids

* fix eval input color

* fix copilot run workflow

* fix tests

---------

Co-authored-by: Theodore Li <theo@sim.ai>
2026-03-26 22:52:23 -04:00
Theodore Li
50e42c2041 fix(ui): Change modal field to be company size (#3801)
* Change modal field to be company size

* Adjust dropdown options

* Fix lint

---------

Co-authored-by: Theodore Li <theo@sim.ai>
2026-03-26 22:47:59 -04:00
Waleed
e70e1ec8c5 feat(search): add tables, files, knowledge bases, and jobs to cmd-k search (#3800)
* feat(search): add tables, files, knowledge bases, and jobs to cmd-k search

* fix(search): address PR feedback — drop files/jobs, add onSelect to memo

* fix(search): add files back with per-file deep links, keep jobs out

* fix(search): remove onSelect from memo comparator to match existing pattern
2026-03-26 19:18:21 -07:00
Waleed
4c474e03c1 fix(connectors): contentDeferred pattern + validation fixes across all connectors (#3793)
* fix(knowledge): enqueue connector docs per-batch to survive sync timeouts

* fix(connectors): convert all connectors to contentDeferred pattern and fix validation issues

All 10 connectors now use contentDeferred: true in listDocuments, returning
lightweight metadata stubs instead of downloading content during listing.
Content is fetched lazily via getDocument only for new/changed documents,
preventing Trigger.dev task timeouts on large syncs.

Connector-specific fixes from validation audit:
- Google Drive: metadata-based contentHash, orderBy for deterministic pagination,
  precise maxFiles, byte-length size check with truncation warning
- OneDrive: metadata-based contentHash, orderBy for deterministic pagination
- SharePoint: metadata-based contentHash, byte-length size check
- Dropbox: metadata-based contentHash using content_hash field
- Notion: code/equation block extraction, empty page fallback to title,
  reduced CHILD_PAGE_CONCURRENCY to 5, syncContext parameter
- Confluence: syncContext caching for cloudId, reduced label concurrency to 5
- Gmail: use joinTagArray for label tags
- Obsidian: syncRunId-based stub hash for forced re-fetch, mtime-based hash
  in getDocument, .trim() on vaultUrl, lightweight validateConfig
- Evernote: retryOptions threaded through apiFindNotesMetadata and apiGetNote
- GitHub: added contentDeferred: false to getDocument, syncContext parameter

Infrastructure:
- sync-engine: added syncRunId to syncContext for Obsidian change detection
- confluence/utils: replaced raw fetch with fetchWithRetry, added retryOptions
- oauth: added supportsRefreshTokenRotation: false for Dropbox
- Updated add-connector and validate-connector skills with contentDeferred docs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(connectors): address PR review comments - metadata merge, retryOptions, UTF-8 safety

- Sync engine: merge metadata from getDocument during deferred hydration,
  so Gmail/Obsidian/Confluence tags and metadata survive the stub→full transition
- Evernote: pass retryOptions {retries:3, backoff:500} from listDocuments and
  getDocument callers into apiFindNotesMetadata and apiGetNote
- Google Drive + SharePoint: safe UTF-8 truncation that walks back to the last
  complete character boundary instead of splitting multi-byte chars

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(evernote): use correct RetryOptions property names

maxRetries/initialDelayMs instead of retries/backoff to match the
RetryOptions interface from lib/knowledge/documents/utils.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(sync-engine): merge title from getDocument and skip unchanged docs after hydration

- Merge title from getDocument during deferred hydration so Gmail
  documents get the email Subject header instead of the snippet text
- After hydration, compare the hydrated contentHash against the stored
  DB hash — if they match, skip the update. This prevents Obsidian
  (and any connector with a force-refresh stub hash) from re-uploading
  and re-processing unchanged documents every sync

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(sync-engine): dedup externalIds, enable deletion reconciliation, merge sourceUrl

Three sync engine gaps identified during audit:

1. Duplicate externalId guard: if a connector returns the same externalId
   across pages (pagination overlap), skip the second occurrence to prevent
   unique constraint violations on add and double-uploads on update.

2. Deletion reconciliation: previously required explicit fullSync or
   syncMode='full', meaning docs deleted from the source accumulated in
   the KB forever. Now runs on all non-incremental syncs (which return
   ALL docs). Includes a safety threshold: if >50% of existing docs
   (and >5 docs) would be deleted, skip and warn — protects against
   partial listing failures. Explicit fullSync bypasses the threshold.

3. sourceUrl merge: hydration now picks up sourceUrl from getDocument,
   falling back to the stub's sourceUrl if getDocument doesn't set one.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* lint

* fix(connectors): confluence version metadata fallback and google drive maxFiles guard

- Confluence: use `version?.number` directly (undefined) in metadata instead
  of `?? ''` (empty string) to prevent Number('') = 0 passing NaN check in
  mapTags. Hash still uses `?? ''` for string interpolation.
- Google Drive: add early return when previouslyFetched >= maxFiles to prevent
  effectivePageSize <= 0 which violates the API's pageSize requirement (1-1000).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(connectors): blogpost labels and capped listing deletion reconciliation

- Confluence: fetchLabelsForPages now tries both /pages/{id}/labels and
  /blogposts/{id}/labels, preventing label loss when getDocument hydrates
  blogpost content (previously returned empty labels on 404).
- Sync engine: skip deletion reconciliation when listing was capped
  (maxFiles/maxThreads). Connectors signal this via syncContext.listingCapped.
  Prevents incorrect deletion of docs beyond the cap that still exist in source.
  fullSync override still forces deletion for explicit cleanup.
- Google Drive & Gmail: set syncContext.listingCapped = true when cap is hit.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(connectors): set syncContext.listingCapped in all connectors with caps

OneDrive, Dropbox, SharePoint, Confluence (v2 + CQL), and Notion (3 listing
functions) now set syncContext.listingCapped = true when their respective
maxFiles/maxPages limit is hit. Without this, the sync engine's deletion
reconciliation would run against an incomplete listing and incorrectly
hard-delete documents that exist in the source but fell outside the cap window.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(evernote): thread retryOptions through apiListTags and apiListNotebooks

All calls to apiListTags and apiListNotebooks in both listDocuments and
getDocument now pass retryOptions for consistent retry protection across
all Thrift RPC calls.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-26 19:04:54 -07:00
Vikhyath Mondreti
b0980b1e09 fix(light): tag dropdown, code highlight (#3799)
* fix(light): tag dropdown, code highlight

* remove more hardcoded vals
2026-03-26 18:15:52 -07:00
Waleed
66ce673629 fix(security): harden auth, SSRF, injection, and CORS across API routes (#3792)
* fix: prevent auth bypass via user-controlled context query param in file serve

The /api/files/serve endpoint trusted a user-supplied `context` query
parameter to skip authentication. An attacker could append
`?context=profile-pictures` to any file URL and download files without
auth. Now the public access gate checks the key prefix instead of the
query param, and `og-images/` is added to `inferContextFromKey`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use randomized heredoc delimiter in SSH execute-script route

Prevents accidental heredoc termination if script content contains
the delimiter string on its own line.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: escape workingDirectory in SSH execute-command route

Use escapeShellArg() with single quotes for the workingDirectory
parameter, consistent with all other SSH routes (execute-script,
create-directory, delete-file, move-rename).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: harden chat/form deployment auth (OTP brute-force, CSPRNG, HMAC tokens)

- Add brute-force protection to OTP verification with attempt tracking (CWE-307)
- Replace Math.random() with crypto.randomInt() for OTP generation (CWE-338)
- Replace unsigned Base64 auth tokens with HMAC-SHA256 signed tokens (CWE-327)
- Use shared isEmailAllowed utility in OTP route instead of inline duplicate
- Simplify Redis OTP update to single KEEPTTL call

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: harden SSRF protections and input validation across API routes

Add DNS-based SSRF validation for MCP server URLs, secure OIDC discovery
with IP-pinned fetch, strengthen OTP/chat/form input validation, sanitize
1Password vault parameters, and tighten deployment security checks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* lint

* fix(file-serve): remove user-controlled context param from authenticated path

The `?context` query param was still being passed to `handleCloudProxy`
in the authenticated code path, allowing any logged-in user to spoof
context as `profile-pictures` and bypass ownership checks in
`verifyFileAccess`. Now always use `inferContextFromKey` from the
server-controlled key prefix.

* fix: handle legacy OTP format in decodeOTPValue for deploy-time compat

Add guard for OTP values without colon separator (pre-deploy format)
to avoid misparse that would lock out users with in-flight OTPs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(mcp): distinguish DNS resolution failures from SSRF policy blocks

DNS lookup failures now throw McpDnsResolutionError (502) instead of
McpSsrfError (403), so transient DNS hiccups surface as retryable
upstream errors rather than confusing permission rejections.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: make OTP attempt counting atomic to prevent TOCTOU race

Redis path: use Lua script for atomic read-increment-conditional-delete.
DB path: use optimistic locking (UPDATE WHERE value = currentValue) with
re-read fallback on conflict. Prevents concurrent wrong guesses from
each counting as a single attempt.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: check attempt count before OTP comparison to prevent bypass

Reject OTPs that have already reached max failed attempts before
comparing the code, closing a race window where a correct guess
could bypass brute-force protection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: validate OIDC discovered endpoints against SSRF

The discovery URL itself was SSRF-validated, but endpoint URLs returned
in the discovery document (tokenEndpoint, userInfoEndpoint, jwksEndpoint)
were stored without validation. A malicious OIDC issuer on a public IP
could return internal network URLs in the discovery response.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: remove duplicate OIDC endpoint SSRF validation block

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: validate OIDC discovered endpoints and pin DNS for 1Password Connect

- SSRF-validate all endpoint URLs returned by OIDC discovery documents
  before storing them (authorization, token, userinfo, jwks endpoints)
- Pin DNS resolution in 1Password Connect requests using
  secureFetchWithPinnedIP to prevent TOCTOU DNS rebinding attacks

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* lint

* fix: replace KEEPTTL with TTL+EX for Redis <6.0 compat, add DB retry loop

- Lua script now reads TTL and uses SET...EX instead of KEEPTTL
- DB optimistic locking now retries up to 3 times on conflict

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address review feedback on OTP atomicity and 1Password fetch

- Replace Redis KEEPTTL with TTL+SET EX for Redis <6.0 compatibility
- Add retry loop to DB optimistic lock path so concurrent OTP attempts
  are actually counted instead of silently dropped
- Remove unreachable fallback fetch in 1Password Connect; make
  validateConnectServerUrl return non-nullable string

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: treat Lua nil return as locked when OTP key is missing

When the Redis key is deleted/expired between getOTP and
incrementOTPAttempts, the Lua script returns nil. Handle this
as 'locked' instead of silently treating it as 'incremented'.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: handle Lua nil as locked OTP and add SSRF check to MCP env resolution

- Treat Redis Lua nil return (expired/deleted key) as 'locked' instead
  of silently treating it as a successful increment
- Add validateMcpServerSsrf to MCP service resolveConfigEnvVars so
  env-var URLs are SSRF-validated after resolution at execution time

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: narrow resolvedIP type guard instead of non-null assertion

Replace urlValidation.resolvedIP! with proper type narrowing by adding
!urlValidation.resolvedIP to the guard clause, so TypeScript can infer
the string type without a fragile assertion.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: bind auth tokens to deployment password for immediate revocation

Include a SHA-256 hash of the encrypted password in the HMAC-signed
token payload. Changing the deployment password now immediately
invalidates all existing auth cookies, restoring the pre-HMAC behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: bind auth tokens to deployment password and remove resolvedIP non-null assertion

- Include SHA-256 hash of encryptedPassword in HMAC token payload so
  changing a deployment's password immediately invalidates all sessions
- Pass encryptedPassword through setChatAuthCookie/setFormAuthCookie
  and validateAuthToken at all call sites
- Replace non-null assertion on resolvedIP with proper narrowing guard

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: update test assertions for new encryptedPassword parameter

Tests now expect the encryptedPassword arg passed to validateAuthToken
and setDeploymentAuthCookie after the password-binding change.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: format long lines in chat/form test assertions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: pass encryptedPassword through OTP route cookie generation

Select chat.password in PUT handler DB query and pass it to
setChatAuthCookie so OTP-issued tokens include the correct
password slot for subsequent validation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-26 16:32:23 -07:00
Waleed
f37e4b67c7 feat(ketch): add Ketch privacy consent integration (#3794)
* feat(ketch): add Ketch privacy consent integration

* fix(ketch): add response.ok guards and fix registry ordering

* fix(ketch): include errorMessage in error response output for all tools

* fix(ketch): wire optional purposes filter for get_consent operation
2026-03-26 15:49:58 -07:00
Waleed
7a1a46067d feat(granola): add Granola meeting notes integration (#3790)
* feat(granola): add Granola meeting notes integration

* fix(granola): use string comparison for includeTranscript to avoid truthy string bug

* fix(granola): add missing get_note output fields to block definition

* regen docs
2026-03-26 15:00:00 -07:00
Adithya Krishna
bf60670c0b chore: fix cn with tw-merge (#3789)
* chore: fix cn

* chore: fix scale
2026-03-26 13:30:21 -07:00
Waleed
8a481b612d chore(config): clean up bun, turbo, and next.js config (#3788)
* chore(config): clean up bun, turbo, and next.js config

* chore(ci): bump bun to 1.3.11 in dockerfiles and workflows
2026-03-26 13:04:23 -07:00
Adithya Krishna
bc4b7f5759 feat: fix rerenders on search input (#3784)
* chore: fix conflicts

* chore: update contents

* chore: fix review changes
2026-03-26 12:06:31 -07:00
Adithya Krishna
5aa0b4d5d4 fix: emcn component library design engineering polish (#3672)
* chore: fix conflicts

* chore: fix conflicts

* chore: pause marquee

* chore: fix conflicts

* chore: fix conflicts

* chore: address review comments

* chore: fix conflicts

* chore: other bug fixes

* chore: fix conflicts

* chore: fix radius

* chore: fix review changes

* chore: fix conflicts

* chore: revert other

* chore: fix conflicts

* chore: fix review changes

* chore: fix conflicts

* chore: fix review changes

* chore: fix button state

* chore: fix button state

* chore: fix review changes

* chore: fix lint

* chore: fix conflicts

* chore: add metadata

* chore: fix things

* chore: fix overwritten states

* chore: fix warnings

* chore: fix button state

* chore: fix review changes

* chore: fix review changes

* chore: fix hover state

* chore: fix popover

* chore: fix review changes

* chore: fix review changes
2026-03-26 11:40:23 -07:00
Waleed
3774f33d39 fix(landing): fix image rendering and navbar blog/docs navigation (#3785)
* fix(landing): fix image rendering and navbar blog/docs navigation

* fix(navbar): remove disclosure aria attributes from navigation links
2026-03-26 09:48:54 -07:00
Waleed
3597eacdb7 feat(demo-request): block personal email domains (#3786) 2026-03-26 09:47:03 -07:00
1523 changed files with 123315 additions and 27226 deletions

View File

@@ -71,12 +71,14 @@ export const {service}Connector: ConnectorConfig = {
],
listDocuments: async (accessToken, sourceConfig, cursor) => {
// Paginate via cursor, extract text, compute SHA-256 hash
// Return metadata stubs with contentDeferred: true (if per-doc content fetch needed)
// Or full documents with content (if list API returns content inline)
// Return { documents: ExternalDocument[], nextCursor?, hasMore }
},
getDocument: async (accessToken, sourceConfig, externalId) => {
// Return ExternalDocument or null
// Fetch full content for a single document
// Return ExternalDocument with contentDeferred: false, or null
},
validateConfig: async (accessToken, sourceConfig) => {
@@ -281,26 +283,110 @@ Every document returned from `listDocuments`/`getDocument` must include:
{
externalId: string // Source-specific unique ID
title: string // Document title
content: string // Extracted plain text
content: string // Extracted plain text (or '' if contentDeferred)
contentDeferred?: boolean // true = content will be fetched via getDocument
mimeType: 'text/plain' // Always text/plain (content is extracted)
contentHash: string // SHA-256 of content (change detection)
contentHash: string // Metadata-based hash for change detection
sourceUrl?: string // Link back to original (stored on document record)
metadata?: Record<string, unknown> // Source-specific data (fed to mapTags)
}
```
## Content Hashing (Required)
## Content Deferral (Required for file/content-download connectors)
The sync engine uses content hashes for change detection:
**All connectors that require per-document API calls to fetch content MUST use `contentDeferred: true`.** This is the standard pattern — `listDocuments` returns lightweight metadata stubs, and content is fetched lazily by the sync engine via `getDocument` only for new/changed documents.
This pattern is critical for reliability: the sync engine processes documents in batches and enqueues each batch for processing immediately. If a sync times out, all previously-batched documents are already queued. Without deferral, content downloads during listing can exhaust the sync task's time budget before any documents are saved.
### When to use `contentDeferred: true`
- The service's list API does NOT return document content (only metadata)
- Content requires a separate download/export API call per document
- Examples: Google Drive, OneDrive, SharePoint, Dropbox, Notion, Confluence, Gmail, Obsidian, Evernote, GitHub
### When NOT to use `contentDeferred`
- The list API already returns the full content inline (e.g., Slack messages, Reddit posts, HubSpot notes)
- No per-document API call is needed to get content
### Content Hash Strategy
Use a **metadata-based** `contentHash` — never a content-based hash. The hash must be derivable from the list response metadata alone, so the sync engine can detect changes without downloading content.
Good metadata hash sources:
- `modifiedTime` / `lastModifiedDateTime` — changes when file is edited
- Git blob SHA — unique per content version
- API-provided content hash (e.g., Dropbox `content_hash`)
- Version number (e.g., Confluence page version)
Format: `{service}:{id}:{changeIndicator}`
```typescript
async function computeContentHash(content: string): Promise<string> {
const data = new TextEncoder().encode(content)
const hashBuffer = await crypto.subtle.digest('SHA-256', data)
return Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, '0')).join('')
// Google Drive: modifiedTime changes on edit
contentHash: `gdrive:${file.id}:${file.modifiedTime ?? ''}`
// GitHub: blob SHA is a content-addressable hash
contentHash: `gitsha:${item.sha}`
// Dropbox: API provides content_hash
contentHash: `dropbox:${entry.id}:${entry.content_hash ?? entry.server_modified}`
// Confluence: version number increments on edit
contentHash: `confluence:${page.id}:${page.version.number}`
```
**Critical invariant:** The `contentHash` MUST be identical whether produced by `listDocuments` (stub) or `getDocument` (full doc). Both should use the same stub function to guarantee this.
### Implementation Pattern
```typescript
// 1. Create a stub function (sync, no API calls)
function fileToStub(file: ServiceFile): ExternalDocument {
return {
externalId: file.id,
title: file.name || 'Untitled',
content: '',
contentDeferred: true,
mimeType: 'text/plain',
sourceUrl: `https://service.com/file/${file.id}`,
contentHash: `service:${file.id}:${file.modifiedTime ?? ''}`,
metadata: { /* fields needed by mapTags */ },
}
}
// 2. listDocuments returns stubs (fast, metadata only)
listDocuments: async (accessToken, sourceConfig, cursor) => {
const response = await fetchWithRetry(listUrl, { ... })
const files = (await response.json()).files
const documents = files.map(fileToStub)
return { documents, nextCursor, hasMore }
}
// 3. getDocument fetches content and returns full doc with SAME contentHash
getDocument: async (accessToken, sourceConfig, externalId) => {
const metadata = await fetchWithRetry(metadataUrl, { ... })
const file = await metadata.json()
if (file.trashed) return null
try {
const content = await fetchContent(accessToken, file)
if (!content.trim()) return null
const stub = fileToStub(file)
return { ...stub, content, contentDeferred: false }
} catch (error) {
logger.warn(`Failed to fetch content for: ${file.name}`, { error })
return null
}
}
```
### Reference Implementations
- **Google Drive**: `connectors/google-drive/google-drive.ts` — file download/export with `modifiedTime` hash
- **GitHub**: `connectors/github/github.ts` — git blob SHA hash
- **Notion**: `connectors/notion/notion.ts` — blocks API with `last_edited_time` hash
- **Confluence**: `connectors/confluence/confluence.ts` — version number hash
## tagDefinitions — Declared Tag Definitions
Declare which tags the connector populates using semantic IDs. Shown in the add-connector modal as opt-out checkboxes.
@@ -409,7 +495,10 @@ export const CONNECTOR_REGISTRY: ConnectorRegistry = {
## Reference Implementations
- **OAuth**: `apps/sim/connectors/confluence/confluence.ts` — multiple config field types, `mapTags`, label fetching
- **OAuth + contentDeferred**: `apps/sim/connectors/google-drive/google-drive.ts` — file download with metadata-based hash, `orderBy` for deterministic pagination
- **OAuth + contentDeferred (blocks API)**: `apps/sim/connectors/notion/notion.ts` — complex block content extraction deferred to `getDocument`
- **OAuth + contentDeferred (git)**: `apps/sim/connectors/github/github.ts` — blob SHA hash, tree listing
- **OAuth + inline content**: `apps/sim/connectors/confluence/confluence.ts` — multiple config field types, `mapTags`, label fetching
- **API key**: `apps/sim/connectors/fireflies/fireflies.ts` — GraphQL API with Bearer token auth
## Checklist
@@ -425,7 +514,9 @@ export const CONNECTOR_REGISTRY: ConnectorRegistry = {
- `selectorKey` exists in `hooks/selectors/registry.ts`
- `dependsOn` references selector field IDs (not `canonicalParamId`)
- Dependency `canonicalParamId` values exist in `SELECTOR_CONTEXT_FIELDS`
- [ ] `listDocuments` handles pagination and computes content hashes
- [ ] `listDocuments` handles pagination with metadata-based content hashes
- [ ] `contentDeferred: true` used if content requires per-doc API calls (file download, export, blocks fetch)
- [ ] `contentHash` is metadata-based (not content-based) and identical between stub and `getDocument`
- [ ] `sourceUrl` set on each ExternalDocument (full URL, not relative)
- [ ] `metadata` includes source-specific data for tag mapping
- [ ] `tagDefinitions` declared for each semantic key returned by `mapTags`

View File

@@ -141,12 +141,24 @@ For each API endpoint the connector calls:
## Step 6: Validate Data Transformation
### Content Deferral (CRITICAL)
Connectors that require per-document API calls to fetch content (file download, export, blocks fetch) MUST use `contentDeferred: true`. This is the standard pattern for reliability — without it, content downloads during listing can exhaust the sync task's time budget before any documents are saved.
- [ ] If the connector downloads content per-doc during `listDocuments`, it MUST use `contentDeferred: true` instead
- [ ] `listDocuments` returns lightweight stubs with `content: ''` and `contentDeferred: true`
- [ ] `getDocument` fetches actual content and returns the full document with `contentDeferred: false`
- [ ] A shared stub function (e.g., `fileToStub`) is used by both `listDocuments` and `getDocument` to guarantee `contentHash` consistency
- [ ] `contentHash` is metadata-based (e.g., `service:{id}:{modifiedTime}`), NOT content-based — it must be derivable from list metadata alone
- [ ] The `contentHash` is identical whether produced by `listDocuments` or `getDocument`
Connectors where the list API already returns content inline (e.g., Slack messages, Reddit posts) do NOT need `contentDeferred`.
### ExternalDocument Construction
- [ ] `externalId` is a stable, unique identifier from the source API
- [ ] `title` is extracted from the correct field and has a sensible fallback (e.g., `'Untitled'`)
- [ ] `content` is plain text — HTML content is stripped using `htmlToPlainText` from `@/connectors/utils`
- [ ] `mimeType` is `'text/plain'`
- [ ] `contentHash` is computed using `computeContentHash` from `@/connectors/utils`
- [ ] `contentHash` uses a metadata-based format (e.g., `service:{id}:{modifiedTime}`) for connectors with `contentDeferred: true`, or `computeContentHash` from `@/connectors/utils` for inline-content connectors
- [ ] `sourceUrl` is a valid, complete URL back to the original resource (not relative)
- [ ] `metadata` contains all fields referenced by `mapTags` and `tagDefinitions`
@@ -200,6 +212,8 @@ For each API endpoint the connector calls:
- [ ] Fetches a single document by `externalId`
- [ ] Returns `null` for 404 / not found (does not throw)
- [ ] Returns the same `ExternalDocument` shape as `listDocuments`
- [ ] If `listDocuments` uses `contentDeferred: true`, `getDocument` MUST fetch actual content and return `contentDeferred: false`
- [ ] If `listDocuments` uses `contentDeferred: true`, `getDocument` MUST use the same stub function to ensure `contentHash` is identical
- [ ] Handles all content types that `listDocuments` can produce (e.g., if `listDocuments` returns both pages and blogposts, `getDocument` must handle both — not hardcode one endpoint)
- [ ] Forwards `syncContext` if it needs cached state (user names, field maps, etc.)
- [ ] Error handling is graceful (catches, logs, returns null or throws with context)
@@ -253,6 +267,8 @@ Group findings by severity:
- Missing error handling that would crash the sync
- `requiredScopes` not a subset of OAuth provider scopes
- Query/filter injection: user-controlled values interpolated into OData `$filter`, SOQL, or query strings without escaping
- Per-document content download in `listDocuments` without `contentDeferred: true` — causes sync timeouts for large document sets
- `contentHash` mismatch between `listDocuments` stub and `getDocument` return — causes unnecessary re-processing every sync
**Warning** (incorrect behavior, data quality issues, or convention violations):
- HTML content not stripped via `htmlToPlainText`
@@ -300,6 +316,7 @@ After fixing, confirm:
- [ ] Validated scopes are sufficient for all API endpoints the connector calls
- [ ] Validated token refresh config (`useBasicAuth`, `supportsRefreshTokenRotation`)
- [ ] Validated pagination: cursor names, page sizes, hasMore logic, no silent caps
- [ ] Validated content deferral: `contentDeferred: true` used when per-doc content fetch required, metadata-based `contentHash` consistent between stub and `getDocument`
- [ ] Validated data transformation: plain text extraction, HTML stripping, content hashing
- [ ] Validated tag definitions match mapTags output, correct fieldTypes
- [ ] Validated config fields: canonical pairs, selector keys, required flags

View File

@@ -192,7 +192,7 @@ In the block config (`blocks/blocks/{service}.ts`), add `hideWhenHosted: true` t
},
```
The visibility is controlled by `isSubBlockHiddenByHostedKey()` in `lib/workflows/subblocks/visibility.ts`, which checks the `isHosted` feature flag.
The visibility is controlled by `isSubBlockHidden()` in `lib/workflows/subblocks/visibility.ts`, which checks both the `isHosted` feature flag (`hideWhenHosted`) and optional env var conditions (`hideWhenEnvSet`).
### Excluding Specific Operations from Hosted Key Support

View File

@@ -1,4 +1,4 @@
FROM oven/bun:1.3.10-alpine
FROM oven/bun:1.3.11-alpine
# Install necessary packages for development
RUN apk add --no-cache \

View File

@@ -2,9 +2,9 @@ name: CI
on:
push:
branches: [main, staging]
branches: [main, staging, dev]
pull_request:
branches: [main, staging]
branches: [main, staging, dev]
concurrency:
group: ci-${{ github.ref }}
@@ -23,7 +23,7 @@ jobs:
detect-version:
name: Detect Version
runs-on: blacksmith-4vcpu-ubuntu-2404
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging')
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging' || github.ref == 'refs/heads/dev')
outputs:
version: ${{ steps.extract.outputs.version }}
is_release: ${{ steps.extract.outputs.is_release }}
@@ -49,7 +49,7 @@ jobs:
build-amd64:
name: Build AMD64
needs: [test-build, detect-version]
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging')
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging' || github.ref == 'refs/heads/dev')
runs-on: blacksmith-8vcpu-ubuntu-2404
permissions:
contents: read
@@ -75,8 +75,8 @@ jobs:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ github.ref == 'refs/heads/main' && secrets.AWS_ROLE_TO_ASSUME || secrets.STAGING_AWS_ROLE_TO_ASSUME }}
aws-region: ${{ github.ref == 'refs/heads/main' && secrets.AWS_REGION || secrets.STAGING_AWS_REGION }}
role-to-assume: ${{ github.ref == 'refs/heads/main' && secrets.AWS_ROLE_TO_ASSUME || github.ref == 'refs/heads/dev' && secrets.DEV_AWS_ROLE_TO_ASSUME || secrets.STAGING_AWS_ROLE_TO_ASSUME }}
aws-region: ${{ github.ref == 'refs/heads/main' && secrets.AWS_REGION || github.ref == 'refs/heads/dev' && secrets.DEV_AWS_REGION || secrets.STAGING_AWS_REGION }}
- name: Login to Amazon ECR
id: login-ecr
@@ -109,6 +109,8 @@ jobs:
# ECR tags (always build for ECR)
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
ECR_TAG="latest"
elif [ "${{ github.ref }}" = "refs/heads/dev" ]; then
ECR_TAG="dev"
else
ECR_TAG="staging"
fi

View File

@@ -20,7 +20,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.10
bun-version: 1.3.11
- name: Setup Node
uses: actions/setup-node@v4

View File

@@ -23,7 +23,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.10
bun-version: 1.3.11
- name: Cache Bun dependencies
uses: actions/cache@v4
@@ -122,7 +122,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.10
bun-version: 1.3.11
- name: Cache Bun dependencies
uses: actions/cache@v4

View File

@@ -36,8 +36,8 @@ jobs:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ github.ref == 'refs/heads/main' && secrets.AWS_ROLE_TO_ASSUME || secrets.STAGING_AWS_ROLE_TO_ASSUME }}
aws-region: ${{ github.ref == 'refs/heads/main' && secrets.AWS_REGION || secrets.STAGING_AWS_REGION }}
role-to-assume: ${{ github.ref == 'refs/heads/main' && secrets.AWS_ROLE_TO_ASSUME || github.ref == 'refs/heads/dev' && secrets.DEV_AWS_ROLE_TO_ASSUME || secrets.STAGING_AWS_ROLE_TO_ASSUME }}
aws-region: ${{ github.ref == 'refs/heads/main' && secrets.AWS_REGION || github.ref == 'refs/heads/dev' && secrets.DEV_AWS_REGION || secrets.STAGING_AWS_REGION }}
- name: Login to Amazon ECR
id: login-ecr
@@ -70,6 +70,8 @@ jobs:
# ECR tags (always build for ECR)
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
ECR_TAG="latest"
elif [ "${{ github.ref }}" = "refs/heads/dev" ]; then
ECR_TAG="dev"
else
ECR_TAG="staging"
fi

View File

@@ -19,7 +19,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.10
bun-version: 1.3.11
- name: Cache Bun dependencies
uses: actions/cache@v4

View File

@@ -19,7 +19,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.10
bun-version: 1.3.11
- name: Setup Node.js for npm publishing
uses: actions/setup-node@v4

View File

@@ -19,7 +19,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.10
bun-version: 1.3.11
- name: Setup Node.js for npm publishing
uses: actions/setup-node@v4

View File

@@ -19,7 +19,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.10
bun-version: 1.3.11
- name: Setup Node
uses: actions/setup-node@v4

View File

@@ -74,6 +74,10 @@ docker compose -f docker-compose.prod.yml up -d
Open [http://localhost:3000](http://localhost:3000)
#### Background worker note
The Docker Compose stack starts a dedicated worker container by default. If `REDIS_URL` is not configured, the worker will start, log that it is idle, and do no queue processing. This is expected. Queue-backed API, webhook, and schedule execution requires Redis; installs without Redis continue to use the inline execution path.
Sim also supports local models via [Ollama](https://ollama.ai) and [vLLM](https://docs.vllm.ai/) — see the [Docker self-hosting docs](https://docs.sim.ai/self-hosting/docker) for setup details.
### Self-hosted: Manual Setup
@@ -113,10 +117,12 @@ cd packages/db && bunx drizzle-kit migrate --config=./drizzle.config.ts
5. Start development servers:
```bash
bun run dev:full # Starts both Next.js app and realtime socket server
bun run dev:full # Starts Next.js app, realtime socket server, and the BullMQ worker
```
Or run separately: `bun run dev` (Next.js) and `cd apps/sim && bun run dev:sockets` (realtime).
If `REDIS_URL` is not configured, the worker will remain idle and execution continues inline.
Or run separately: `bun run dev` (Next.js), `cd apps/sim && bun run dev:sockets` (realtime), and `cd apps/sim && bun run worker` (BullMQ worker).
## Copilot API Keys

View File

@@ -8,8 +8,6 @@ export default function RootLayout({ children }: { children: ReactNode }) {
export const viewport: Viewport = {
width: 'device-width',
initialScale: 1,
maximumScale: 1,
userScalable: false,
themeColor: [
{ media: '(prefers-color-scheme: light)', color: '#ffffff' },
{ media: '(prefers-color-scheme: dark)', color: '#0c0c0c' },
@@ -20,7 +18,7 @@ export const metadata = {
metadataBase: new URL('https://docs.sim.ai'),
title: {
default: 'Sim Documentation — Build AI Agents & Run Your Agentic Workforce',
template: '%s',
template: '%s | Sim Docs',
},
description:
'Documentation for Sim — the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows.',

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,7 @@
import type { ComponentType, SVGProps } from 'react'
import {
A2AIcon,
AgentMailIcon,
AhrefsIcon,
AirtableIcon,
AirweaveIcon,
@@ -45,6 +46,7 @@ import {
EnrichSoIcon,
EvernoteIcon,
ExaAIIcon,
ExtendIcon,
EyeIcon,
FathomIcon,
FirecrawlIcon,
@@ -74,6 +76,7 @@ import {
GoogleVaultIcon,
GrafanaIcon,
GrainIcon,
GranolaIcon,
GreenhouseIcon,
GreptileIcon,
HexIcon,
@@ -88,7 +91,9 @@ import {
JiraIcon,
JiraServiceManagementIcon,
KalshiIcon,
KetchIcon,
LangsmithIcon,
LaunchDarklyIcon,
LemlistIcon,
LinearIcon,
LinkedInIcon,
@@ -124,6 +129,7 @@ import {
PolymarketIcon,
PostgresIcon,
PosthogIcon,
ProfoundIcon,
PulseIcon,
QdrantIcon,
QuiverIcon,
@@ -134,9 +140,11 @@ import {
ResendIcon,
RevenueCatIcon,
RipplingIcon,
RootlyIcon,
S3Icon,
SalesforceIcon,
SearchIcon,
SecretsManagerIcon,
SendgridIcon,
SentryIcon,
SerperIcon,
@@ -152,6 +160,7 @@ import {
StagehandIcon,
StripeIcon,
SupabaseIcon,
TailscaleIcon,
TavilyIcon,
TelegramIcon,
TextractIcon,
@@ -181,6 +190,7 @@ type IconComponent = ComponentType<SVGProps<SVGSVGElement>>
export const blockTypeToIconMap: Record<string, IconComponent> = {
a2a: A2AIcon,
agentmail: AgentMailIcon,
ahrefs: AhrefsIcon,
airtable: AirtableIcon,
airweave: AirweaveIcon,
@@ -218,6 +228,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
enrich: EnrichSoIcon,
evernote: EvernoteIcon,
exa: ExaAIIcon,
extend_v2: ExtendIcon,
fathom: FathomIcon,
file_v3: DocumentIcon,
firecrawl: FirecrawlIcon,
@@ -247,6 +258,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
google_vault: GoogleVaultIcon,
grafana: GrafanaIcon,
grain: GrainIcon,
granola: GranolaIcon,
greenhouse: GreenhouseIcon,
greptile: GreptileIcon,
hex: HexIcon,
@@ -262,8 +274,10 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
jira: JiraIcon,
jira_service_management: JiraServiceManagementIcon,
kalshi_v2: KalshiIcon,
ketch: KetchIcon,
knowledge: PackageSearchIcon,
langsmith: LangsmithIcon,
launchdarkly: LaunchDarklyIcon,
lemlist: LemlistIcon,
linear: LinearIcon,
linkedin: LinkedInIcon,
@@ -298,6 +312,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
polymarket: PolymarketIcon,
postgresql: PostgresIcon,
posthog: PosthogIcon,
profound: ProfoundIcon,
pulse_v2: PulseIcon,
qdrant: QdrantIcon,
quiver: QuiverIcon,
@@ -308,9 +323,11 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
resend: ResendIcon,
revenuecat: RevenueCatIcon,
rippling: RipplingIcon,
rootly: RootlyIcon,
s3: S3Icon,
salesforce: SalesforceIcon,
search: SearchIcon,
secrets_manager: SecretsManagerIcon,
sendgrid: SendgridIcon,
sentry: SentryIcon,
serper: SerperIcon,
@@ -327,6 +344,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
stripe: StripeIcon,
stt_v2: STTIcon,
supabase: SupabaseIcon,
tailscale: TailscaleIcon,
tavily: TavilyIcon,
telegram: TelegramIcon,
textract_v2: TextractIcon,

View File

@@ -131,7 +131,7 @@ Erkennt personenbezogene Daten mithilfe von Microsoft Presidio. Unterstützt üb
**Anwendungsfälle:**
- Blockieren von Inhalten mit sensiblen persönlichen Informationen
- Maskieren von personenbezogenen Daten vor der Protokollierung oder Speicherung
- Einhaltung der DSGVO, HIPAA und anderer Datenschutzbestimmungen
- Einhaltung der DSGVO und anderer Datenschutzbestimmungen
- Bereinigung von Benutzereingaben vor der Verarbeitung
## Konfiguration

View File

@@ -0,0 +1,150 @@
---
title: Credential
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
import { Image } from '@/components/ui/image'
import { FAQ } from '@/components/ui/faq'
The Credential block has two operations: **Select Credential** picks a single OAuth credential and outputs its ID reference for downstream blocks; **List Credentials** returns all OAuth credentials in the workspace (optionally filtered by provider) as an array for iteration.
<div className="flex justify-center">
<Image
src="/static/blocks/credential.png"
alt="Credential Block"
width={400}
height={300}
className="my-6"
/>
</div>
<Callout>
The Credential block outputs credential **ID references**, not secrets. Downstream blocks receive the ID and resolve the actual OAuth token securely during their own execution.
</Callout>
## Configuration Options
### Operation
| Value | Description |
|---|---|
| **Select Credential** | Pick one OAuth credential and output its reference — use this to wire a single credential into downstream blocks |
| **List Credentials** | Return all OAuth credentials in the workspace as an array — use this with a ForEach loop |
### Credential (Select operation)
Select an OAuth credential from your workspace. The dropdown shows all connected OAuth accounts (Google, GitHub, Slack, etc.).
In advanced mode, paste a credential ID directly. You can copy a credential ID from your workspace's Credentials settings page.
### Provider (List operation)
Filter the returned OAuth credentials by provider. Select one or more providers from the dropdown — only providers you have credentials for will appear. Leave empty to return all OAuth credentials.
| Example | Returns |
|---|---|
| Gmail | Gmail credentials only |
| Slack | Slack credentials only |
| Gmail + Slack | Gmail and Slack credentials |
## Outputs
<Tabs items={['Select Credential', 'List Credentials']}>
<Tab>
| Output | Type | Description |
|---|---|---|
| `credentialId` | `string` | The credential ID — pipe this into other blocks' credential fields |
| `displayName` | `string` | Human-readable name (e.g. "waleed@company.com") |
| `providerId` | `string` | OAuth provider ID (e.g. `google-email`, `slack`) |
</Tab>
<Tab>
| Output | Type | Description |
|---|---|---|
| `credentials` | `json` | Array of OAuth credential objects (see shape below) |
| `count` | `number` | Number of credentials returned |
Each object in the `credentials` array:
| Field | Type | Description |
|---|---|---|
| `credentialId` | `string` | The credential ID |
| `displayName` | `string` | Human-readable name |
| `providerId` | `string` | OAuth provider ID |
</Tab>
</Tabs>
## Example Use Cases
**Shared credential across multiple blocks** — Define once, use everywhere
```
Credential (Select, Google) → Gmail (Send) & Google Drive (Upload) & Google Calendar (Create)
```
**Multi-account workflows** — Route to different credentials based on logic
```
Agent (Determine account) → Condition → Credential A or Credential B → Slack (Post)
```
**Iterate over all Gmail accounts**
```
Credential (List, Provider: Gmail) → ForEach Loop → Gmail (Send) using <loop.currentItem.credentialId>
```
<div className="flex justify-center">
<Image
src="/static/blocks/credential-loop.png"
alt="Credential List wired into a ForEach Loop"
width={900}
height={400}
className="my-6"
/>
</div>
## How to wire a Credential block
### Select Credential
1. Drop a **Credential** block and select your OAuth credential from the picker
2. In the downstream block, switch to **advanced mode** on its credential field
3. Enter `<credentialBlockName.credentialId>` as the value
<Tabs items={['Gmail', 'Slack']}>
<Tab>
In the Gmail block's credential field (advanced mode):
```
<myCredential.credentialId>
```
</Tab>
<Tab>
In the Slack block's credential field (advanced mode):
```
<myCredential.credentialId>
```
</Tab>
</Tabs>
### List Credentials
1. Drop a **Credential** block, set Operation to **List Credentials**
2. Optionally select one or more **Providers** to narrow results (only your connected providers appear)
3. Wire `<credentialBlockName.credentials>` into a **ForEach Loop** as the items source
4. Inside the loop, reference `<loop.currentItem.credentialId>` in downstream blocks' credential fields
## Best Practices
- **Define once, reference many times**: When five blocks use the same Google account, use one Credential block and wire all five to `<credential.credentialId>` instead of selecting the account five times
- **Outputs are safe to log**: The `credentialId` output is a UUID reference, not a secret. It is safe to inspect in execution logs
- **Use for environment switching**: Pair with a Condition block to route to a production or staging OAuth credential based on a workflow variable
- **Advanced mode is required**: Downstream blocks must be in advanced mode on their credential field to accept a dynamic reference
- **Use List + ForEach for fan-out**: When you need to run the same action across all accounts of a provider, List Credentials feeds naturally into a ForEach loop
- **Narrow by provider**: Use the Provider multiselect to filter to specific services — only providers you have credentials for are shown
<FAQ items={[
{ question: "Does the Credential block expose my secret or token?", answer: "No. The block outputs a credential ID (a UUID), not the actual OAuth token. Downstream blocks receive the ID and resolve the token securely in their own execution context. Secrets never appear in workflow state, logs, or the canvas." },
{ question: "What credential types does it support?", answer: "OAuth connected accounts only (Google, GitHub, Slack, etc.). Environment variables and service accounts cannot be resolved by ID in downstream blocks, so they are not supported." },
{ question: "How is Select different from just copying a credential ID into advanced mode?", answer: "Functionally identical — both pass the same credential ID to the downstream block. The Credential block adds value when you need to use one credential in many blocks (change it once), or when you want to select between credentials dynamically using a Condition block." },
{ question: "Can I list all OAuth credentials in my workspace?", answer: "Yes. Set the Operation to 'List Credentials'. Optionally filter by provider using the Provider multiselect. Wire the credentials output into a ForEach loop to process each credential individually." },
{ question: "Can I use a Credential block output in a Function block?", answer: "Yes. Reference <credential.credentialId> in your Function block's code. Note that the function will receive the raw UUID string — if you need the resolved token, the downstream block must handle the resolution (as integration blocks do). The Function block does not automatically resolve credential IDs." },
{ question: "What happens if the credential is deleted?", answer: "The Select operation will throw an error at execution time: 'Credential not found'. The List operation will simply omit the deleted credential from the results. Update the Credential block to select a valid credential before re-running." },
]} />

View File

@@ -132,7 +132,7 @@ Detects personally identifiable information using Microsoft Presidio. Supports o
**Use Cases:**
- Block content containing sensitive personal information
- Mask PII before logging or storing data
- Compliance with GDPR, HIPAA, and other privacy regulations
- Compliance with GDPR and other privacy regulations
- Sanitize user inputs before processing
## Configuration

View File

@@ -4,6 +4,7 @@
"agent",
"api",
"condition",
"credential",
"evaluator",
"function",
"guardrails",

View File

@@ -0,0 +1,206 @@
---
title: Google Service Accounts
description: Set up Google service accounts with domain-wide delegation for Gmail, Sheets, Drive, Calendar, and other Google services
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Step, Steps } from 'fumadocs-ui/components/steps'
import { Image } from '@/components/ui/image'
import { FAQ } from '@/components/ui/faq'
Google service accounts with domain-wide delegation let your workflows access Google APIs on behalf of users in your Google Workspace domain — without requiring each user to complete an OAuth consent flow. This is ideal for automated workflows that need to send emails, read spreadsheets, or manage files across your organization.
For example, you could build a workflow that iterates through a list of employees, impersonates each one to read their Google Docs, and uploads the contents to a shared knowledge base — all without requiring any of those users to sign in.
## Prerequisites
Before adding a service account to Sim, you need to configure it in the Google Cloud Console and Google Workspace Admin Console.
### 1. Create a Service Account in Google Cloud
<Steps>
<Step>
Go to the [Google Cloud Console](https://console.cloud.google.com/) and select your project (or create one)
</Step>
<Step>
Navigate to **IAM & Admin** → **Service Accounts**
</Step>
<Step>
Click **Create Service Account**, give it a name and description, then click **Create and Continue**
<div className="flex justify-center">
<Image
src="/static/credentials/gcp-create-service-account.png"
alt="Google Cloud Console — Create service account form"
width={700}
height={500}
className="my-4"
/>
</div>
</Step>
<Step>
Skip the optional role and user access steps and click **Done**
</Step>
<Step>
Click on the newly created service account, go to the **Keys** tab, and click **Add Key** → **Create new key**
</Step>
<Step>
Select **JSON** as the key type and click **Create**. A JSON key file will download — keep this safe
<div className="flex justify-center">
<Image
src="/static/credentials/gcp-create-private-key.png"
alt="Google Cloud Console — Create private key dialog with JSON selected"
width={700}
height={400}
className="my-4"
/>
</div>
</Step>
</Steps>
<Callout type="warn">
The JSON key file contains your service account's private key. Treat it like a password — do not commit it to source control or share it publicly.
</Callout>
### 2. Enable the Required APIs
In the Google Cloud Console, go to **APIs & Services** → **Library** and enable the APIs for the services your workflows will use. See the [scopes reference](#scopes-reference) below for the full list of APIs by service.
### 3. Set Up Domain-Wide Delegation
<Steps>
<Step>
In the Google Cloud Console, go to **IAM & Admin** → **Service Accounts**, click on your service account, and copy the **Client ID** (the numeric ID, not the email)
</Step>
<Step>
Open the [Google Workspace Admin Console](https://admin.google.com/) and navigate to **Security** → **Access and data control** → **API controls**
</Step>
<Step>
Click **Manage Domain Wide Delegation**, then click **Add new**
</Step>
<Step>
Paste the **Client ID** from your service account, then add the OAuth scopes for the services your workflows need. Copy the full scope URLs from the [scopes reference](#scopes-reference) below — only authorize scopes for services you plan to use.
<div className="flex justify-center">
<Image
src="/static/credentials/gcp-add-client-id.png"
alt="Google Workspace Admin Console — Add a new client ID with OAuth scopes"
width={350}
height={300}
className="my-4"
/>
</div>
</Step>
<Step>
Click **Authorize**
</Step>
</Steps>
<Callout type="info">
Domain-wide delegation must be configured by a Google Workspace admin. If you are not an admin, send the Client ID and required scopes to your admin.
</Callout>
### Scopes Reference
The table below lists every Google service that supports service account authentication in Sim, the API to enable in Google Cloud Console, and the delegation scopes to authorize. Copy the scope string for each service you need and paste it into the Google Workspace Admin Console.
<table>
<thead>
<tr>
<th className="whitespace-nowrap">Service</th>
<th className="whitespace-nowrap">API to Enable</th>
<th>Delegation Scopes</th>
</tr>
</thead>
<tbody>
<tr><td>Gmail</td><td>Gmail API</td><td><code>{'https://www.googleapis.com/auth/gmail.send'}</code><br/><code>{'https://www.googleapis.com/auth/gmail.modify'}</code><br/><code>{'https://www.googleapis.com/auth/gmail.labels'}</code></td></tr>
<tr><td>Google Sheets</td><td>Google Sheets API, Google Drive API</td><td><code>{'https://www.googleapis.com/auth/drive'}</code><br/><code>{'https://www.googleapis.com/auth/drive.file'}</code></td></tr>
<tr><td>Google Drive</td><td>Google Drive API</td><td><code>{'https://www.googleapis.com/auth/drive'}</code><br/><code>{'https://www.googleapis.com/auth/drive.file'}</code></td></tr>
<tr><td>Google Docs</td><td>Google Docs API, Google Drive API</td><td><code>{'https://www.googleapis.com/auth/drive'}</code><br/><code>{'https://www.googleapis.com/auth/drive.file'}</code></td></tr>
<tr><td>Google Slides</td><td>Google Slides API, Google Drive API</td><td><code>{'https://www.googleapis.com/auth/drive'}</code><br/><code>{'https://www.googleapis.com/auth/drive.file'}</code></td></tr>
<tr><td>Google Forms</td><td>Google Forms API, Google Drive API</td><td><code>{'https://www.googleapis.com/auth/drive'}</code><br/><code>{'https://www.googleapis.com/auth/forms.body'}</code><br/><code>{'https://www.googleapis.com/auth/forms.responses.readonly'}</code></td></tr>
<tr><td>Google Calendar</td><td>Google Calendar API</td><td><code>{'https://www.googleapis.com/auth/calendar'}</code></td></tr>
<tr><td>Google Contacts</td><td>People API</td><td><code>{'https://www.googleapis.com/auth/contacts'}</code></td></tr>
<tr><td>BigQuery</td><td>BigQuery API</td><td><code>{'https://www.googleapis.com/auth/bigquery'}</code></td></tr>
<tr><td>Google Tasks</td><td>Tasks API</td><td><code>{'https://www.googleapis.com/auth/tasks'}</code></td></tr>
<tr><td>Google Vault</td><td>Vault API, Cloud Storage API</td><td><code>{'https://www.googleapis.com/auth/ediscovery'}</code><br/><code>{'https://www.googleapis.com/auth/devstorage.read_only'}</code></td></tr>
<tr><td>Google Groups</td><td>Admin SDK API</td><td><code>{'https://www.googleapis.com/auth/admin.directory.group'}</code><br/><code>{'https://www.googleapis.com/auth/admin.directory.group.member'}</code></td></tr>
<tr><td>Google Meet</td><td>Google Meet API</td><td><code>{'https://www.googleapis.com/auth/meetings.space.created'}</code><br/><code>{'https://www.googleapis.com/auth/meetings.space.readonly'}</code></td></tr>
</tbody>
</table>
<Callout type="info">
You only need to enable APIs and authorize scopes for the services you plan to use. When authorizing multiple services, combine their scope strings with commas into a single entry in the Admin Console.
</Callout>
## Adding the Service Account to Sim
Once Google Cloud and Workspace are configured, add the service account as a credential in Sim.
<Steps>
<Step>
Open your workspace **Settings** and go to the **Integrations** tab
</Step>
<Step>
Search for "Google Service Account" and click **Connect**
<div className="flex justify-center">
<Image
src="/static/credentials/integrations-service-account.png"
alt="Integrations page showing Google Service Account"
width={800}
height={150}
className="my-4"
/>
</div>
</Step>
<Step>
Paste the full contents of your JSON key file into the text area
<div className="flex justify-center">
<Image
src="/static/credentials/add-service-account.png"
alt="Add Google Service Account dialog"
width={350}
height={420}
className="my-6"
/>
</div>
</Step>
<Step>
Give the credential a display name (the service account email is used by default)
</Step>
<Step>
Click **Save**
</Step>
</Steps>
The JSON key file is validated for the required fields (`type`, `client_email`, `private_key`, `project_id`) and encrypted before being stored.
## Using Delegated Access in Workflows
When you use a Google block (Gmail, Sheets, Drive, etc.) in a workflow and select a service account credential, an **Impersonate User Email** field appears below the credential selector.
Enter the email address of the Google Workspace user you want the service account to act as. For example, if you enter `alice@yourcompany.com`, the workflow will send emails from Alice's account, read her spreadsheets, or access her calendar — depending on the scopes you authorized.
<div className="flex justify-center">
<Image
src="/static/credentials/workflow-impersonated-account.png"
alt="Gmail block in a workflow showing the Impersonated Account field with a service account credential"
width={800}
height={350}
className="my-4"
/>
</div>
<Callout type="warn">
The impersonated email must belong to a user in the Google Workspace domain where you configured domain-wide delegation. Impersonating external email addresses will fail.
</Callout>
<FAQ items={[
{ question: "Can I use a service account without domain-wide delegation?", answer: "Yes, but it will only be able to access resources owned by the service account itself (e.g., spreadsheets shared directly with the service account email). Without delegation, you cannot impersonate users or access their personal data like Gmail." },
{ question: "What happens if the impersonation email field is left blank?", answer: "The service account will authenticate as itself. This works for accessing shared resources (like a Google Sheet shared with the service account email) but will fail for user-specific APIs like Gmail." },
{ question: "Can I use the same service account for multiple Google services?", answer: "Yes. A single service account can be used across Gmail, Sheets, Drive, Calendar, and other Google services — as long as the required API is enabled in Google Cloud and the corresponding scopes are authorized in the Workspace admin console." },
{ question: "How do I rotate the service account key?", answer: "Create a new JSON key in the Google Cloud Console under your service account's Keys tab, then update the credential in Sim with the new key. Delete the old key from Google Cloud once the new one is working." },
{ question: "Does the impersonated user need a Google Workspace license?", answer: "Yes. Domain-wide delegation only works with users who have a Google Workspace account in the domain. Consumer Gmail accounts (e.g., @gmail.com) cannot be impersonated." },
]} />

View File

@@ -0,0 +1,5 @@
{
"title": "Credentials",
"pages": ["index", "google-service-account"],
"defaultOpen": false
}

View File

@@ -195,6 +195,17 @@ By default, your usage is capped at the credits included in your plan. To allow
Max (individual) shares the same rate limits as team plans. Team plans (Pro or Max for Teams) use the Max-tier rate limits.
### Concurrent Execution Limits
| Plan | Concurrent Executions |
|------|----------------------|
| **Free** | 5 |
| **Pro** | 50 |
| **Max / Team** | 200 |
| **Enterprise** | 200 (customizable) |
Concurrent execution limits control how many workflow executions can run simultaneously within a workspace. When the limit is reached, new executions are queued and admitted as running executions complete. Manual runs from the editor are not subject to these limits.
### File Storage
| Plan | Storage |

View File

@@ -0,0 +1,592 @@
---
title: AgentMail
description: Manage email inboxes, threads, and messages with AgentMail
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="agentmail"
color="#000000"
/>
{/* MANUAL-CONTENT-START:intro */}
[AgentMail](https://agentmail.to/) is an API-first email platform built for agents and automation. AgentMail lets you create email inboxes on the fly, send and receive messages, reply to threads, manage drafts, and organize conversations with labels — all through a simple REST API designed for programmatic access.
**Why AgentMail?**
- **Agent-Native Email:** Purpose-built for AI agents and automation — create inboxes, send messages, and manage threads without human-facing UI overhead.
- **Full Email Lifecycle:** Send new messages, reply to threads, forward emails, manage drafts, and schedule sends — all from a single API.
- **Thread & Conversation Management:** Organize emails into threads with full read, reply, forward, and label support for structured conversation tracking.
- **Draft Workflow:** Compose drafts, update them, schedule sends, and dispatch when ready — perfect for review-before-send workflows.
- **Label Organization:** Tag threads and messages with custom labels for filtering, routing, and downstream automation.
**Using AgentMail in Sim**
Sim's AgentMail integration connects your agentic workflows directly to AgentMail using an API key. With 20 operations spanning inboxes, threads, messages, and drafts, you can build powerful email automations without writing backend code.
**Key benefits of using AgentMail in Sim:**
- **Dynamic inbox creation:** Spin up new inboxes on the fly for each agent, workflow, or customer — perfect for multi-tenant email handling.
- **Automated email processing:** List and read incoming messages, then trigger downstream actions based on content, sender, or labels.
- **Conversational email:** Reply to threads and forward messages to keep conversations flowing naturally within your automated workflows.
- **Draft and review workflows:** Create drafts, update them with AI-generated content, and send when approved — ideal for human-in-the-loop patterns.
- **Email organization:** Apply labels to threads and messages to categorize, filter, and route emails through your automation pipeline.
Whether you're building an AI email assistant, automating customer support replies, processing incoming leads, or managing multi-agent email workflows, AgentMail in Sim gives you direct, secure access to the full AgentMail API — no middleware required. Simply configure your API key, select the operation you need, and let Sim handle the rest.
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Integrate AgentMail into your workflow. Create and manage email inboxes, send and receive messages, reply to threads, manage drafts, and organize threads with labels. Requires API Key.
## Tools
### `agentmail_create_draft`
Create a new email draft in AgentMail
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | AgentMail API key |
| `inboxId` | string | Yes | ID of the inbox to create the draft in |
| `to` | string | No | Recipient email addresses \(comma-separated\) |
| `subject` | string | No | Draft subject line |
| `text` | string | No | Plain text draft body |
| `html` | string | No | HTML draft body |
| `cc` | string | No | CC recipient email addresses \(comma-separated\) |
| `bcc` | string | No | BCC recipient email addresses \(comma-separated\) |
| `inReplyTo` | string | No | ID of message being replied to |
| `sendAt` | string | No | ISO 8601 timestamp to schedule sending |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `draftId` | string | Unique identifier for the draft |
| `inboxId` | string | Inbox the draft belongs to |
| `subject` | string | Draft subject |
| `to` | array | Recipient email addresses |
| `cc` | array | CC email addresses |
| `bcc` | array | BCC email addresses |
| `text` | string | Plain text content |
| `html` | string | HTML content |
| `preview` | string | Draft preview text |
| `labels` | array | Labels assigned to the draft |
| `inReplyTo` | string | Message ID this draft replies to |
| `sendStatus` | string | Send status \(scheduled, sending, failed\) |
| `sendAt` | string | Scheduled send time |
| `createdAt` | string | Creation timestamp |
| `updatedAt` | string | Last updated timestamp |
### `agentmail_create_inbox`
Create a new email inbox with AgentMail
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | AgentMail API key |
| `username` | string | No | Username for the inbox email address |
| `domain` | string | No | Domain for the inbox email address |
| `displayName` | string | No | Display name for the inbox |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `inboxId` | string | Unique identifier for the inbox |
| `email` | string | Email address of the inbox |
| `displayName` | string | Display name of the inbox |
| `createdAt` | string | Creation timestamp |
| `updatedAt` | string | Last updated timestamp |
### `agentmail_delete_draft`
Delete an email draft in AgentMail
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | AgentMail API key |
| `inboxId` | string | Yes | ID of the inbox containing the draft |
| `draftId` | string | Yes | ID of the draft to delete |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `deleted` | boolean | Whether the draft was successfully deleted |
### `agentmail_delete_inbox`
Delete an email inbox in AgentMail
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | AgentMail API key |
| `inboxId` | string | Yes | ID of the inbox to delete |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `deleted` | boolean | Whether the inbox was successfully deleted |
### `agentmail_delete_thread`
Delete an email thread in AgentMail (moves to trash, or permanently deletes if already in trash)
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | AgentMail API key |
| `inboxId` | string | Yes | ID of the inbox containing the thread |
| `threadId` | string | Yes | ID of the thread to delete |
| `permanent` | boolean | No | Force permanent deletion instead of moving to trash |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `deleted` | boolean | Whether the thread was successfully deleted |
### `agentmail_forward_message`
Forward an email message to new recipients in AgentMail
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | AgentMail API key |
| `inboxId` | string | Yes | ID of the inbox containing the message |
| `messageId` | string | Yes | ID of the message to forward |
| `to` | string | Yes | Recipient email addresses \(comma-separated\) |
| `subject` | string | No | Override subject line |
| `text` | string | No | Additional plain text to prepend |
| `html` | string | No | Additional HTML to prepend |
| `cc` | string | No | CC recipient email addresses \(comma-separated\) |
| `bcc` | string | No | BCC recipient email addresses \(comma-separated\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `messageId` | string | ID of the forwarded message |
| `threadId` | string | ID of the thread |
### `agentmail_get_draft`
Get details of a specific email draft in AgentMail
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | AgentMail API key |
| `inboxId` | string | Yes | ID of the inbox the draft belongs to |
| `draftId` | string | Yes | ID of the draft to retrieve |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `draftId` | string | Unique identifier for the draft |
| `inboxId` | string | Inbox the draft belongs to |
| `subject` | string | Draft subject |
| `to` | array | Recipient email addresses |
| `cc` | array | CC email addresses |
| `bcc` | array | BCC email addresses |
| `text` | string | Plain text content |
| `html` | string | HTML content |
| `preview` | string | Draft preview text |
| `labels` | array | Labels assigned to the draft |
| `inReplyTo` | string | Message ID this draft replies to |
| `sendStatus` | string | Send status \(scheduled, sending, failed\) |
| `sendAt` | string | Scheduled send time |
| `createdAt` | string | Creation timestamp |
| `updatedAt` | string | Last updated timestamp |
### `agentmail_get_inbox`
Get details of a specific email inbox in AgentMail
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | AgentMail API key |
| `inboxId` | string | Yes | ID of the inbox to retrieve |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `inboxId` | string | Unique identifier for the inbox |
| `email` | string | Email address of the inbox |
| `displayName` | string | Display name of the inbox |
| `createdAt` | string | Creation timestamp |
| `updatedAt` | string | Last updated timestamp |
### `agentmail_get_message`
Get details of a specific email message in AgentMail
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | AgentMail API key |
| `inboxId` | string | Yes | ID of the inbox containing the message |
| `messageId` | string | Yes | ID of the message to retrieve |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `messageId` | string | Unique identifier for the message |
| `threadId` | string | ID of the thread this message belongs to |
| `from` | string | Sender email address |
| `to` | array | Recipient email addresses |
| `cc` | array | CC email addresses |
| `bcc` | array | BCC email addresses |
| `subject` | string | Message subject |
| `text` | string | Plain text content |
| `html` | string | HTML content |
| `createdAt` | string | Creation timestamp |
### `agentmail_get_thread`
Get details of a specific email thread including messages in AgentMail
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | AgentMail API key |
| `inboxId` | string | Yes | ID of the inbox containing the thread |
| `threadId` | string | Yes | ID of the thread to retrieve |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `threadId` | string | Unique identifier for the thread |
| `subject` | string | Thread subject |
| `senders` | array | List of sender email addresses |
| `recipients` | array | List of recipient email addresses |
| `messageCount` | number | Number of messages in the thread |
| `labels` | array | Labels assigned to the thread |
| `lastMessageAt` | string | Timestamp of last message |
| `createdAt` | string | Creation timestamp |
| `updatedAt` | string | Last updated timestamp |
| `messages` | array | Messages in the thread |
| ↳ `messageId` | string | Unique identifier for the message |
| ↳ `from` | string | Sender email address |
| ↳ `to` | array | Recipient email addresses |
| ↳ `cc` | array | CC email addresses |
| ↳ `bcc` | array | BCC email addresses |
| ↳ `subject` | string | Message subject |
| ↳ `text` | string | Plain text content |
| ↳ `html` | string | HTML content |
| ↳ `createdAt` | string | Creation timestamp |
### `agentmail_list_drafts`
List email drafts in an inbox in AgentMail
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | AgentMail API key |
| `inboxId` | string | Yes | ID of the inbox to list drafts from |
| `limit` | number | No | Maximum number of drafts to return |
| `pageToken` | string | No | Pagination token for next page of results |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `drafts` | array | List of drafts |
| ↳ `draftId` | string | Unique identifier for the draft |
| ↳ `inboxId` | string | Inbox the draft belongs to |
| ↳ `subject` | string | Draft subject |
| ↳ `to` | array | Recipient email addresses |
| ↳ `cc` | array | CC email addresses |
| ↳ `bcc` | array | BCC email addresses |
| ↳ `preview` | string | Draft preview text |
| ↳ `sendStatus` | string | Send status \(scheduled, sending, failed\) |
| ↳ `sendAt` | string | Scheduled send time |
| ↳ `createdAt` | string | Creation timestamp |
| ↳ `updatedAt` | string | Last updated timestamp |
| `count` | number | Total number of drafts |
| `nextPageToken` | string | Token for retrieving the next page |
### `agentmail_list_inboxes`
List all email inboxes in AgentMail
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | AgentMail API key |
| `limit` | number | No | Maximum number of inboxes to return |
| `pageToken` | string | No | Pagination token for next page of results |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `inboxes` | array | List of inboxes |
| ↳ `inboxId` | string | Unique identifier for the inbox |
| ↳ `email` | string | Email address of the inbox |
| ↳ `displayName` | string | Display name of the inbox |
| ↳ `createdAt` | string | Creation timestamp |
| ↳ `updatedAt` | string | Last updated timestamp |
| `count` | number | Total number of inboxes |
| `nextPageToken` | string | Token for retrieving the next page |
### `agentmail_list_messages`
List messages in an inbox in AgentMail
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | AgentMail API key |
| `inboxId` | string | Yes | ID of the inbox to list messages from |
| `limit` | number | No | Maximum number of messages to return |
| `pageToken` | string | No | Pagination token for next page of results |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `messages` | array | List of messages in the inbox |
| ↳ `messageId` | string | Unique identifier for the message |
| ↳ `from` | string | Sender email address |
| ↳ `to` | array | Recipient email addresses |
| ↳ `subject` | string | Message subject |
| ↳ `preview` | string | Message preview text |
| ↳ `createdAt` | string | Creation timestamp |
| `count` | number | Total number of messages |
| `nextPageToken` | string | Token for retrieving the next page |
### `agentmail_list_threads`
List email threads in AgentMail
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | AgentMail API key |
| `inboxId` | string | Yes | ID of the inbox to list threads from |
| `limit` | number | No | Maximum number of threads to return |
| `pageToken` | string | No | Pagination token for next page of results |
| `labels` | string | No | Comma-separated labels to filter threads by |
| `before` | string | No | Filter threads before this ISO 8601 timestamp |
| `after` | string | No | Filter threads after this ISO 8601 timestamp |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `threads` | array | List of email threads |
| ↳ `threadId` | string | Unique identifier for the thread |
| ↳ `subject` | string | Thread subject |
| ↳ `senders` | array | List of sender email addresses |
| ↳ `recipients` | array | List of recipient email addresses |
| ↳ `messageCount` | number | Number of messages in the thread |
| ↳ `lastMessageAt` | string | Timestamp of last message |
| ↳ `createdAt` | string | Creation timestamp |
| ↳ `updatedAt` | string | Last updated timestamp |
| `count` | number | Total number of threads |
| `nextPageToken` | string | Token for retrieving the next page |
### `agentmail_reply_message`
Reply to an existing email message in AgentMail
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | AgentMail API key |
| `inboxId` | string | Yes | ID of the inbox to reply from |
| `messageId` | string | Yes | ID of the message to reply to |
| `text` | string | No | Plain text reply body |
| `html` | string | No | HTML reply body |
| `to` | string | No | Override recipient email addresses \(comma-separated\) |
| `cc` | string | No | CC email addresses \(comma-separated\) |
| `bcc` | string | No | BCC email addresses \(comma-separated\) |
| `replyAll` | boolean | No | Reply to all recipients of the original message |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `messageId` | string | ID of the sent reply message |
| `threadId` | string | ID of the thread |
### `agentmail_send_draft`
Send an existing email draft in AgentMail
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | AgentMail API key |
| `inboxId` | string | Yes | ID of the inbox containing the draft |
| `draftId` | string | Yes | ID of the draft to send |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `messageId` | string | ID of the sent message |
| `threadId` | string | ID of the thread |
### `agentmail_send_message`
Send an email message from an AgentMail inbox
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | AgentMail API key |
| `inboxId` | string | Yes | ID of the inbox to send from |
| `to` | string | Yes | Recipient email address \(comma-separated for multiple\) |
| `subject` | string | Yes | Email subject line |
| `text` | string | No | Plain text email body |
| `html` | string | No | HTML email body |
| `cc` | string | No | CC recipient email addresses \(comma-separated\) |
| `bcc` | string | No | BCC recipient email addresses \(comma-separated\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `threadId` | string | ID of the created thread |
| `messageId` | string | ID of the sent message |
| `subject` | string | Email subject line |
| `to` | string | Recipient email address |
### `agentmail_update_draft`
Update an existing email draft in AgentMail
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | AgentMail API key |
| `inboxId` | string | Yes | ID of the inbox containing the draft |
| `draftId` | string | Yes | ID of the draft to update |
| `to` | string | No | Recipient email addresses \(comma-separated\) |
| `subject` | string | No | Draft subject line |
| `text` | string | No | Plain text draft body |
| `html` | string | No | HTML draft body |
| `cc` | string | No | CC recipient email addresses \(comma-separated\) |
| `bcc` | string | No | BCC recipient email addresses \(comma-separated\) |
| `sendAt` | string | No | ISO 8601 timestamp to schedule sending |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `draftId` | string | Unique identifier for the draft |
| `inboxId` | string | Inbox the draft belongs to |
| `subject` | string | Draft subject |
| `to` | array | Recipient email addresses |
| `cc` | array | CC email addresses |
| `bcc` | array | BCC email addresses |
| `text` | string | Plain text content |
| `html` | string | HTML content |
| `preview` | string | Draft preview text |
| `labels` | array | Labels assigned to the draft |
| `inReplyTo` | string | Message ID this draft replies to |
| `sendStatus` | string | Send status \(scheduled, sending, failed\) |
| `sendAt` | string | Scheduled send time |
| `createdAt` | string | Creation timestamp |
| `updatedAt` | string | Last updated timestamp |
### `agentmail_update_inbox`
Update the display name of an email inbox in AgentMail
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | AgentMail API key |
| `inboxId` | string | Yes | ID of the inbox to update |
| `displayName` | string | Yes | New display name for the inbox |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `inboxId` | string | Unique identifier for the inbox |
| `email` | string | Email address of the inbox |
| `displayName` | string | Display name of the inbox |
| `createdAt` | string | Creation timestamp |
| `updatedAt` | string | Last updated timestamp |
### `agentmail_update_message`
Add or remove labels on an email message in AgentMail
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | AgentMail API key |
| `inboxId` | string | Yes | ID of the inbox containing the message |
| `messageId` | string | Yes | ID of the message to update |
| `addLabels` | string | No | Comma-separated labels to add to the message |
| `removeLabels` | string | No | Comma-separated labels to remove from the message |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `messageId` | string | Unique identifier for the message |
| `labels` | array | Current labels on the message |
### `agentmail_update_thread`
Add or remove labels on an email thread in AgentMail
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | AgentMail API key |
| `inboxId` | string | Yes | ID of the inbox containing the thread |
| `threadId` | string | Yes | ID of the thread to update |
| `addLabels` | string | No | Comma-separated labels to add to the thread |
| `removeLabels` | string | No | Comma-separated labels to remove from the thread |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `threadId` | string | Unique identifier for the thread |
| `labels` | array | Current labels on the thread |

View File

@@ -359,6 +359,35 @@ List tasks in Attio, optionally filtered by record, assignee, or completion stat
| ↳ `createdAt` | string | When the task was created |
| `count` | number | Number of tasks returned |
### `attio_get_task`
Get a single task by ID from Attio
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `taskId` | string | Yes | The ID of the task to retrieve |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `taskId` | string | The task ID |
| `content` | string | The task content |
| `deadlineAt` | string | The task deadline |
| `isCompleted` | boolean | Whether the task is completed |
| `linkedRecords` | array | Records linked to this task |
| ↳ `targetObjectId` | string | The linked object ID |
| ↳ `targetRecordId` | string | The linked record ID |
| `assignees` | array | Task assignees |
| ↳ `type` | string | The assignee actor type \(e.g. workspace-member\) |
| ↳ `id` | string | The assignee actor ID |
| `createdByActor` | object | The actor who created this task |
| ↳ `type` | string | The actor type \(e.g. workspace-member, api-token, system\) |
| ↳ `id` | string | The actor ID |
| `createdAt` | string | When the task was created |
### `attio_create_task`
Create a task in Attio
@@ -1012,8 +1041,8 @@ Update a webhook in Attio (target URL and/or subscriptions)
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `webhookId` | string | Yes | The webhook ID to update |
| `targetUrl` | string | Yes | HTTPS target URL for webhook delivery |
| `subscriptions` | string | Yes | JSON array of subscriptions, e.g. \[\{"event_type":"note.created"\}\] |
| `targetUrl` | string | No | HTTPS target URL for webhook delivery |
| `subscriptions` | string | No | JSON array of subscriptions, e.g. \[\{"event_type":"note.created"\}\] |
#### Output

View File

@@ -0,0 +1,39 @@
---
title: Extend
description: Parse and extract content from documents
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="extend_v2"
color="#000000"
/>
## Usage Instructions
Integrate Extend AI into the workflow. Parse and extract structured content from documents or file references.
## Tools
### `extend_parser`
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `filePath` | string | No | URL to a document to be processed |
| `file` | file | No | Document file to be processed |
| `fileUpload` | object | No | File upload data from file-upload component |
| `outputFormat` | string | No | Target output format \(markdown or spatial\). Defaults to markdown. |
| `chunking` | string | No | Chunking strategy \(page, document, or section\). Defaults to page. |
| `engine` | string | No | Parsing engine \(parse_performance or parse_light\). Defaults to parse_performance. |
| `apiKey` | string | Yes | Extend API key |
#### Output
This tool does not produce any outputs.

View File

@@ -1,6 +1,6 @@
---
title: File
description: Read and parse multiple files
description: Read and write workspace files
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
@@ -27,7 +27,7 @@ The File Parser tool is particularly useful for scenarios where your agents need
## Usage Instructions
Upload files directly or import from external URLs to get UserFile objects for use in other blocks.
Read and parse files from uploads or URLs, write new workspace files, or append content to existing files.
@@ -52,4 +52,45 @@ Parse one or more uploaded files or files from URLs (text, PDF, CSV, images, etc
| `files` | file[] | Parsed files as UserFile objects |
| `combinedContent` | string | Combined content of all parsed files |
### `file_write`
Create a new workspace file. If a file with the same name already exists, a numeric suffix is added (e.g.,
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `fileName` | string | Yes | File name \(e.g., "data.csv"\). If a file with this name exists, a numeric suffix is added automatically. |
| `content` | string | Yes | The text content to write to the file. |
| `contentType` | string | No | MIME type for new files \(e.g., "text/plain"\). Auto-detected from file extension if omitted. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | File ID |
| `name` | string | File name |
| `size` | number | File size in bytes |
| `url` | string | URL to access the file |
### `file_append`
Append content to an existing workspace file. The file must already exist. Content is added to the end of the file.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `fileName` | string | Yes | Name of an existing workspace file to append to. |
| `content` | string | Yes | The text content to append to the file. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | File ID |
| `name` | string | File name |
| `size` | number | File size in bytes |
| `url` | string | URL to access the file |

View File

@@ -0,0 +1,92 @@
---
title: Granola
description: Access meeting notes and transcripts from Granola
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="granola"
color="#B2C147"
/>
## Usage Instructions
Integrate Granola into your workflow to retrieve meeting notes, summaries, attendees, and transcripts.
## Tools
### `granola_list_notes`
Lists meeting notes from Granola with optional date filters and pagination.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Granola API key |
| `createdBefore` | string | No | Return notes created before this date \(ISO 8601\) |
| `createdAfter` | string | No | Return notes created after this date \(ISO 8601\) |
| `updatedAfter` | string | No | Return notes updated after this date \(ISO 8601\) |
| `cursor` | string | No | Pagination cursor from a previous response |
| `pageSize` | number | No | Number of notes per page \(1-30, default 10\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `notes` | json | List of meeting notes |
| ↳ `id` | string | Note ID |
| ↳ `title` | string | Note title |
| ↳ `ownerName` | string | Note owner name |
| ↳ `ownerEmail` | string | Note owner email |
| ↳ `createdAt` | string | Creation timestamp |
| ↳ `updatedAt` | string | Last update timestamp |
| `hasMore` | boolean | Whether more notes are available |
| `cursor` | string | Pagination cursor for the next page |
### `granola_get_note`
Retrieves a specific meeting note from Granola by ID, including summary, attendees, calendar event details, and optionally the transcript.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Granola API key |
| `noteId` | string | Yes | The note ID \(e.g., not_1d3tmYTlCICgjy\) |
| `includeTranscript` | string | No | Whether to include the meeting transcript |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Note ID |
| `title` | string | Note title |
| `ownerName` | string | Note owner name |
| `ownerEmail` | string | Note owner email |
| `createdAt` | string | Creation timestamp |
| `updatedAt` | string | Last update timestamp |
| `summaryText` | string | Plain text summary of the meeting |
| `summaryMarkdown` | string | Markdown-formatted summary of the meeting |
| `attendees` | json | Meeting attendees |
| ↳ `name` | string | Attendee name |
| ↳ `email` | string | Attendee email |
| `folders` | json | Folders the note belongs to |
| ↳ `id` | string | Folder ID |
| ↳ `name` | string | Folder name |
| `calendarEventTitle` | string | Calendar event title |
| `calendarOrganiser` | string | Calendar event organiser email |
| `calendarEventId` | string | Calendar event ID |
| `scheduledStartTime` | string | Scheduled start time |
| `scheduledEndTime` | string | Scheduled end time |
| `invitees` | json | Calendar event invitee emails |
| `transcript` | json | Meeting transcript entries \(only if requested\) |
| ↳ `speaker` | string | Speaker source \(microphone or speaker\) |
| ↳ `text` | string | Transcript text |
| ↳ `startTime` | string | Segment start time |
| ↳ `endTime` | string | Segment end time |

View File

@@ -0,0 +1,149 @@
---
title: Ketch
description: Manage privacy consent, subscriptions, and data subject rights
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="ketch"
color="#9B5CFF"
/>
{/* MANUAL-CONTENT-START:intro */}
[Ketch](https://www.ketch.com/) is an AI-powered privacy, consent, and data governance platform that helps organizations automate compliance with global privacy regulations. It provides tools for managing consent preferences, handling data subject rights requests, and controlling subscription communications.
With Ketch, you can:
- **Retrieve consent status**: Query the current consent preferences for any data subject across configured purposes and legal bases
- **Update consent preferences**: Set or modify consent for specific purposes (e.g., analytics, marketing) with the appropriate legal basis (opt-in, opt-out, disclosure)
- **Manage subscriptions**: Get and update subscription topic preferences and global controls across contact methods like email and SMS
- **Invoke data subject rights**: Submit privacy rights requests including data access, deletion, correction, and processing restriction under regulations like GDPR and CCPA
To use Ketch, drop the Ketch block into your workflow and provide your organization code, property code, and environment code. The Ketch Web API is a public API — no API key or OAuth credentials are required. Identity is determined by the organization and property codes along with the data subject's identity (e.g., email address).
These capabilities let you automate privacy compliance workflows, respond to user consent changes in real time, and manage data subject rights requests as part of your broader automation pipelines.
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Integrate Ketch into the workflow. Retrieve and update consent preferences, manage subscription topics and controls, and submit data subject rights requests for access, deletion, correction, or processing restriction.
## Tools
### `ketch_get_consent`
Retrieve consent status for a data subject. Returns the current consent preferences for each configured purpose.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `organizationCode` | string | Yes | Ketch organization code |
| `propertyCode` | string | Yes | Digital property code defined in Ketch |
| `environmentCode` | string | Yes | Environment code defined in Ketch \(e.g., "production"\) |
| `jurisdictionCode` | string | No | Jurisdiction code \(e.g., "gdpr", "ccpa"\) |
| `identities` | json | Yes | Identity map \(e.g., \{"email": "user@example.com"\}\) |
| `purposes` | json | No | Optional purposes to filter the consent query |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `purposes` | object | Map of purpose codes to consent status and legal basis |
| ↳ `allowed` | string | Consent status for the purpose: "granted" or "denied" |
| ↳ `legalBasisCode` | string | Legal basis code \(e.g., "consent_optin", "consent_optout", "disclosure", "other"\) |
| `vendors` | object | Map of vendor consent statuses |
### `ketch_set_consent`
Update consent preferences for a data subject. Sets the consent status for specified purposes with the appropriate legal basis.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `organizationCode` | string | Yes | Ketch organization code |
| `propertyCode` | string | Yes | Digital property code defined in Ketch |
| `environmentCode` | string | Yes | Environment code defined in Ketch \(e.g., "production"\) |
| `jurisdictionCode` | string | No | Jurisdiction code \(e.g., "gdpr", "ccpa"\) |
| `identities` | json | Yes | Identity map \(e.g., \{"email": "user@example.com"\}\) |
| `purposes` | json | Yes | Map of purpose codes to consent settings \(e.g., \{"analytics": \{"allowed": "granted", "legalBasisCode": "consent_optin"\}\}\) |
| `collectedAt` | number | No | UNIX timestamp when consent was collected \(defaults to current time\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `purposes` | object | Updated consent status map of purpose codes to consent settings |
| ↳ `allowed` | string | Consent status for the purpose: "granted" or "denied" |
| ↳ `legalBasisCode` | string | Legal basis code \(e.g., "consent_optin", "consent_optout", "disclosure", "other"\) |
### `ketch_get_subscriptions`
Retrieve subscription preferences for a data subject. Returns the current subscription topic and control statuses.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `organizationCode` | string | Yes | Ketch organization code |
| `propertyCode` | string | Yes | Digital property code defined in Ketch |
| `environmentCode` | string | Yes | Environment code defined in Ketch \(e.g., "production"\) |
| `identities` | json | Yes | Identity map \(e.g., \{"email": "user@example.com"\}\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `topics` | object | Map of topic codes to contact method settings \(e.g., \{"newsletter": \{"email": \{"status": "granted"\}\}\}\) |
| `controls` | object | Map of control codes to settings \(e.g., \{"global_unsubscribe": \{"status": "denied"\}\}\) |
### `ketch_set_subscriptions`
Update subscription preferences for a data subject. Sets topic and control statuses for email, SMS, and other contact methods.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `organizationCode` | string | Yes | Ketch organization code |
| `propertyCode` | string | Yes | Digital property code defined in Ketch |
| `environmentCode` | string | Yes | Environment code defined in Ketch \(e.g., "production"\) |
| `identities` | json | Yes | Identity map \(e.g., \{"email": "user@example.com"\}\) |
| `topics` | json | No | Map of topic codes to contact method settings \(e.g., \{"newsletter": \{"email": \{"status": "granted"\}, "sms": \{"status": "denied"\}\}\}\) |
| `controls` | json | No | Map of control codes to settings \(e.g., \{"global_unsubscribe": \{"status": "denied"\}\}\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the subscription preferences were updated |
### `ketch_invoke_right`
Submit a data subject rights request (e.g., access, delete, correct, restrict processing). Initiates a privacy rights workflow in Ketch.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `organizationCode` | string | Yes | Ketch organization code |
| `propertyCode` | string | Yes | Digital property code defined in Ketch |
| `environmentCode` | string | Yes | Environment code defined in Ketch \(e.g., "production"\) |
| `jurisdictionCode` | string | Yes | Jurisdiction code \(e.g., "gdpr", "ccpa"\) |
| `rightCode` | string | Yes | Privacy right code to invoke \(e.g., "access", "delete", "correct", "restrict_processing"\) |
| `identities` | json | Yes | Identity map \(e.g., \{"email": "user@example.com"\}\) |
| `userData` | json | No | Optional data subject information \(e.g., \{"email": "user@example.com", "firstName": "John", "lastName": "Doe"\}\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the rights request was submitted |
| `message` | string | Response message from Ketch |

View File

@@ -0,0 +1,388 @@
---
title: LaunchDarkly
description: Manage feature flags with LaunchDarkly.
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="launchdarkly"
color="#191919"
/>
{/* MANUAL-CONTENT-START:intro */}
[LaunchDarkly](https://launchdarkly.com/) is a feature management platform that enables teams to safely deploy, control, and measure their software features at scale.
With the LaunchDarkly integration in Sim, you can:
- **Feature flag management** — List, create, update, toggle, and delete feature flags programmatically. Toggle flags on or off in specific environments using LaunchDarkly's semantic patch API.
- **Flag status monitoring** — Check whether a flag is active, inactive, new, or launched in a given environment. Track the last time a flag was evaluated.
- **Project and environment management** — List all projects and their environments to understand your LaunchDarkly organization structure.
- **User segments** — List user segments within a project and environment to understand how your audience is organized for targeting.
- **Team visibility** — List account members and their roles for auditing and access management workflows.
- **Audit log** — Retrieve recent audit log entries to track who changed what, when. Filter entries by resource type for targeted monitoring.
In Sim, the LaunchDarkly integration enables your agents to automate feature flag operations as part of their workflows. This allows for automation scenarios such as toggling flags on/off based on deployment pipeline events, monitoring flag status and alerting on stale or unused flags, auditing flag changes by querying the audit log after deployments, syncing flag metadata with your project management tools, and listing all feature flags across projects for governance.
## Authentication
This integration uses a LaunchDarkly API key. You can create personal access tokens or service tokens in the LaunchDarkly dashboard under **Account Settings > Authorization**. The API key is passed directly in the `Authorization` header (no `Bearer` prefix).
## Need Help?
If you encounter issues with the LaunchDarkly integration, contact us at [help@sim.ai](mailto:help@sim.ai)
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Integrate LaunchDarkly into your workflow. List, create, update, toggle, and delete feature flags. Manage projects, environments, segments, members, and audit logs. Requires API Key.
## Tools
### `launchdarkly_create_flag`
Create a new feature flag in a LaunchDarkly project.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | LaunchDarkly API key |
| `projectKey` | string | Yes | The project key to create the flag in |
| `name` | string | Yes | Human-readable name for the feature flag |
| `key` | string | Yes | Unique key for the feature flag \(used in code\) |
| `description` | string | No | Description of the feature flag |
| `tags` | string | No | Comma-separated list of tags |
| `temporary` | boolean | No | Whether the flag is temporary \(default true\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `key` | string | The unique key of the feature flag |
| `name` | string | The human-readable name of the feature flag |
| `kind` | string | The type of flag \(boolean or multivariate\) |
| `description` | string | Description of the feature flag |
| `temporary` | boolean | Whether the flag is temporary |
| `archived` | boolean | Whether the flag is archived |
| `deprecated` | boolean | Whether the flag is deprecated |
| `creationDate` | number | Unix timestamp in milliseconds when the flag was created |
| `tags` | array | Tags applied to the flag |
| `variations` | array | The variations for this feature flag |
| ↳ `value` | string | The variation value |
| ↳ `name` | string | The variation name |
| ↳ `description` | string | The variation description |
| `maintainerId` | string | The ID of the member who maintains this flag |
### `launchdarkly_delete_flag`
Delete a feature flag from a LaunchDarkly project.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | LaunchDarkly API key |
| `projectKey` | string | Yes | The project key |
| `flagKey` | string | Yes | The feature flag key to delete |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `deleted` | boolean | Whether the flag was successfully deleted |
### `launchdarkly_get_audit_log`
List audit log entries from your LaunchDarkly account.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | LaunchDarkly API key |
| `limit` | number | No | Maximum number of entries to return \(default 10, max 20\) |
| `spec` | string | No | Filter expression \(e.g., "resourceType:flag"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `entries` | array | List of audit log entries |
| ↳ `id` | string | The audit log entry ID |
| ↳ `date` | number | Unix timestamp in milliseconds |
| ↳ `kind` | string | The type of action performed |
| ↳ `name` | string | The name of the resource acted on |
| ↳ `description` | string | Full description of the action |
| ↳ `shortDescription` | string | Short description of the action |
| ↳ `memberEmail` | string | Email of the member who performed the action |
| ↳ `targetName` | string | Name of the target resource |
| ↳ `targetKind` | string | Kind of the target resource |
| `totalCount` | number | Total number of audit log entries |
### `launchdarkly_get_flag`
Get a single feature flag by key from a LaunchDarkly project.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | LaunchDarkly API key |
| `projectKey` | string | Yes | The project key |
| `flagKey` | string | Yes | The feature flag key |
| `environmentKey` | string | No | Filter flag configuration to a specific environment |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `key` | string | The unique key of the feature flag |
| `name` | string | The human-readable name of the feature flag |
| `kind` | string | The type of flag \(boolean or multivariate\) |
| `description` | string | Description of the feature flag |
| `temporary` | boolean | Whether the flag is temporary |
| `archived` | boolean | Whether the flag is archived |
| `deprecated` | boolean | Whether the flag is deprecated |
| `creationDate` | number | Unix timestamp in milliseconds when the flag was created |
| `tags` | array | Tags applied to the flag |
| `variations` | array | The variations for this feature flag |
| ↳ `value` | string | The variation value |
| ↳ `name` | string | The variation name |
| ↳ `description` | string | The variation description |
| `maintainerId` | string | The ID of the member who maintains this flag |
| `on` | boolean | Whether the flag is on in the requested environment \(null if no single environment was specified\) |
### `launchdarkly_get_flag_status`
Get the status of a feature flag across environments (active, inactive, launched, etc.).
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | LaunchDarkly API key |
| `projectKey` | string | Yes | The project key |
| `flagKey` | string | Yes | The feature flag key |
| `environmentKey` | string | Yes | The environment key |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `name` | string | The flag status \(new, active, inactive, launched\) |
| `lastRequested` | string | Timestamp of the last evaluation |
| `defaultVal` | string | The default variation value |
### `launchdarkly_list_environments`
List environments in a LaunchDarkly project.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | LaunchDarkly API key |
| `projectKey` | string | Yes | The project key to list environments for |
| `limit` | number | No | Maximum number of environments to return \(default 20\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `environments` | array | List of environments |
| ↳ `id` | string | The environment ID |
| ↳ `key` | string | The unique environment key |
| ↳ `name` | string | The environment name |
| ↳ `color` | string | The color assigned to this environment |
| ↳ `apiKey` | string | The server-side SDK key for this environment |
| ↳ `mobileKey` | string | The mobile SDK key for this environment |
| ↳ `tags` | array | Tags applied to the environment |
| `totalCount` | number | Total number of environments |
### `launchdarkly_list_flags`
List feature flags in a LaunchDarkly project.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | LaunchDarkly API key |
| `projectKey` | string | Yes | The project key to list flags for |
| `environmentKey` | string | No | Filter flag configurations to a specific environment |
| `tag` | string | No | Filter flags by tag name |
| `limit` | number | No | Maximum number of flags to return \(default 20\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `flags` | array | List of feature flags |
| ↳ `key` | string | The unique key of the feature flag |
| ↳ `name` | string | The human-readable name of the feature flag |
| ↳ `kind` | string | The type of flag \(boolean or multivariate\) |
| ↳ `description` | string | Description of the feature flag |
| ↳ `temporary` | boolean | Whether the flag is temporary |
| ↳ `archived` | boolean | Whether the flag is archived |
| ↳ `deprecated` | boolean | Whether the flag is deprecated |
| ↳ `creationDate` | number | Unix timestamp in milliseconds when the flag was created |
| ↳ `tags` | array | Tags applied to the flag |
| ↳ `variations` | array | The variations for this feature flag |
| ↳ `value` | string | The variation value |
| ↳ `name` | string | The variation name |
| ↳ `description` | string | The variation description |
| ↳ `maintainerId` | string | The ID of the member who maintains this flag |
| `totalCount` | number | Total number of flags |
### `launchdarkly_list_members`
List account members in your LaunchDarkly organization.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | LaunchDarkly API key |
| `limit` | number | No | Maximum number of members to return \(default 20\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `members` | array | List of account members |
| ↳ `id` | string | The member ID |
| ↳ `email` | string | The member email address |
| ↳ `firstName` | string | The member first name |
| ↳ `lastName` | string | The member last name |
| ↳ `role` | string | The member role \(reader, writer, admin, owner\) |
| ↳ `lastSeen` | number | Unix timestamp of last activity |
| ↳ `creationDate` | number | Unix timestamp when the member was created |
| ↳ `verified` | boolean | Whether the member email is verified |
| `totalCount` | number | Total number of members |
### `launchdarkly_list_projects`
List all projects in your LaunchDarkly account.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | LaunchDarkly API key |
| `limit` | number | No | Maximum number of projects to return \(default 20\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `projects` | array | List of projects |
| ↳ `id` | string | The project ID |
| ↳ `key` | string | The unique project key |
| ↳ `name` | string | The project name |
| ↳ `tags` | array | Tags applied to the project |
| `totalCount` | number | Total number of projects |
### `launchdarkly_list_segments`
List user segments in a LaunchDarkly project and environment.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | LaunchDarkly API key |
| `projectKey` | string | Yes | The project key |
| `environmentKey` | string | Yes | The environment key |
| `limit` | number | No | Maximum number of segments to return \(default 20\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `segments` | array | List of user segments |
| ↳ `key` | string | The unique segment key |
| ↳ `name` | string | The segment name |
| ↳ `description` | string | The segment description |
| ↳ `tags` | array | Tags applied to the segment |
| ↳ `creationDate` | number | Unix timestamp in milliseconds when the segment was created |
| ↳ `unbounded` | boolean | Whether this is an unbounded \(big\) segment |
| ↳ `included` | array | User keys explicitly included in the segment |
| ↳ `excluded` | array | User keys explicitly excluded from the segment |
| `totalCount` | number | Total number of segments |
### `launchdarkly_toggle_flag`
Toggle a feature flag on or off in a specific LaunchDarkly environment.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | LaunchDarkly API key |
| `projectKey` | string | Yes | The project key |
| `flagKey` | string | Yes | The feature flag key to toggle |
| `environmentKey` | string | Yes | The environment key to toggle the flag in |
| `enabled` | boolean | Yes | Whether to turn the flag on \(true\) or off \(false\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `key` | string | The unique key of the feature flag |
| `name` | string | The human-readable name of the feature flag |
| `kind` | string | The type of flag \(boolean or multivariate\) |
| `description` | string | Description of the feature flag |
| `temporary` | boolean | Whether the flag is temporary |
| `archived` | boolean | Whether the flag is archived |
| `deprecated` | boolean | Whether the flag is deprecated |
| `creationDate` | number | Unix timestamp in milliseconds when the flag was created |
| `tags` | array | Tags applied to the flag |
| `variations` | array | The variations for this feature flag |
| ↳ `value` | string | The variation value |
| ↳ `name` | string | The variation name |
| ↳ `description` | string | The variation description |
| `maintainerId` | string | The ID of the member who maintains this flag |
| `on` | boolean | Whether the flag is now on in the target environment |
### `launchdarkly_update_flag`
Update a feature flag metadata (name, description, tags, temporary, archived) using semantic patch.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | LaunchDarkly API key |
| `projectKey` | string | Yes | The project key |
| `flagKey` | string | Yes | The feature flag key to update |
| `updateName` | string | No | New name for the flag |
| `updateDescription` | string | No | New description for the flag |
| `addTags` | string | No | Comma-separated tags to add |
| `removeTags` | string | No | Comma-separated tags to remove |
| `archive` | boolean | No | Set to true to archive, false to restore |
| `comment` | string | No | Optional comment explaining the update |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `key` | string | The unique key of the feature flag |
| `name` | string | The human-readable name of the feature flag |
| `kind` | string | The type of flag \(boolean or multivariate\) |
| `description` | string | Description of the feature flag |
| `temporary` | boolean | Whether the flag is temporary |
| `archived` | boolean | Whether the flag is archived |
| `deprecated` | boolean | Whether the flag is deprecated |
| `creationDate` | number | Unix timestamp in milliseconds when the flag was created |
| `tags` | array | Tags applied to the flag |
| `variations` | array | The variations for this feature flag |
| ↳ `value` | string | The variation value |
| ↳ `name` | string | The variation name |
| ↳ `description` | string | The variation description |
| `maintainerId` | string | The ID of the member who maintains this flag |

View File

@@ -2,6 +2,7 @@
"pages": [
"index",
"a2a",
"agentmail",
"ahrefs",
"airtable",
"airweave",
@@ -39,6 +40,7 @@
"enrich",
"evernote",
"exa",
"extend",
"fathom",
"file",
"firecrawl",
@@ -68,6 +70,7 @@
"google_vault",
"grafana",
"grain",
"granola",
"greenhouse",
"greptile",
"hex",
@@ -83,8 +86,10 @@
"jira",
"jira_service_management",
"kalshi",
"ketch",
"knowledge",
"langsmith",
"launchdarkly",
"lemlist",
"linear",
"linkedin",
@@ -119,6 +124,7 @@
"polymarket",
"postgresql",
"posthog",
"profound",
"pulse",
"qdrant",
"quiver",
@@ -129,9 +135,11 @@
"resend",
"revenuecat",
"rippling",
"rootly",
"s3",
"salesforce",
"search",
"secrets_manager",
"sendgrid",
"sentry",
"serper",
@@ -149,6 +157,7 @@
"stt",
"supabase",
"table",
"tailscale",
"tavily",
"telegram",
"textract",

View File

@@ -0,0 +1,626 @@
---
title: Profound
description: AI visibility and analytics with Profound
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="profound"
color="#000000"
/>
{/* MANUAL-CONTENT-START:intro */}
[Profound](https://tryprofound.com/) is an AI visibility and analytics platform that helps brands understand how they appear across AI-powered search engines, chatbots, and assistants. It tracks mentions, citations, sentiment, bot traffic, and referral patterns across platforms like ChatGPT, Perplexity, Google AI Overviews, and more.
With the Profound integration in Sim, you can:
- **Monitor AI Visibility**: Track share of voice, visibility scores, and mention counts across AI platforms for your brand and competitors.
- **Analyze Sentiment**: Measure how positively or negatively your brand is discussed in AI-generated responses.
- **Track Citations**: See which URLs are being cited by AI models and your citation share relative to competitors.
- **Monitor Bot Traffic**: Analyze AI crawler activity on your domain, including GPTBot, ClaudeBot, and other AI agents, with hourly granularity.
- **Track Referral Traffic**: Monitor human visits arriving from AI platforms to your website.
- **Explore Prompt Data**: Access raw prompt-answer pairs, query fanouts, and prompt volume trends across AI platforms.
- **Optimize Content**: Get AEO (Answer Engine Optimization) scores and actionable recommendations to improve how AI models reference your content.
- **Manage Categories & Assets**: List and explore your tracked categories, assets (brands), topics, tags, personas, and regions.
These tools let your agents automate AI visibility monitoring, competitive intelligence, and content optimization workflows. To use the Profound integration, you'll need a Profound account with API access.
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Track how your brand appears across AI platforms. Monitor visibility scores, sentiment, citations, bot traffic, referrals, content optimization, and prompt volumes with Profound.
## Tools
### `profound_list_categories`
List all organization categories in Profound
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Profound API Key |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `categories` | json | List of organization categories |
| ↳ `id` | string | Category ID |
| ↳ `name` | string | Category name |
### `profound_list_regions`
List all organization regions in Profound
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Profound API Key |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `regions` | json | List of organization regions |
| ↳ `id` | string | Region ID \(UUID\) |
| ↳ `name` | string | Region name |
### `profound_list_models`
List all AI models/platforms tracked in Profound
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Profound API Key |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `models` | json | List of AI models/platforms |
| ↳ `id` | string | Model ID \(UUID\) |
| ↳ `name` | string | Model/platform name |
### `profound_list_domains`
List all organization domains in Profound
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Profound API Key |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `domains` | json | List of organization domains |
| ↳ `id` | string | Domain ID \(UUID\) |
| ↳ `name` | string | Domain name |
| ↳ `createdAt` | string | When the domain was added |
### `profound_list_assets`
List all organization assets (companies/brands) across all categories in Profound
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Profound API Key |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `assets` | json | List of organization assets with category info |
| ↳ `id` | string | Asset ID |
| ↳ `name` | string | Asset/company name |
| ↳ `website` | string | Asset website URL |
| ↳ `alternateDomains` | json | Alternate domain names |
| ↳ `isOwned` | boolean | Whether this asset is owned by the organization |
| ↳ `createdAt` | string | When the asset was created |
| ↳ `logoUrl` | string | URL of the asset logo |
| ↳ `categoryId` | string | Category ID the asset belongs to |
| ↳ `categoryName` | string | Category name |
### `profound_list_personas`
List all organization personas across all categories in Profound
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Profound API Key |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `personas` | json | List of organization personas with profile details |
| ↳ `id` | string | Persona ID |
| ↳ `name` | string | Persona name |
| ↳ `categoryId` | string | Category ID |
| ↳ `categoryName` | string | Category name |
| ↳ `persona` | json | Persona profile with behavior, employment, and demographics |
### `profound_category_topics`
List topics for a specific category in Profound
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Profound API Key |
| `categoryId` | string | Yes | Category ID \(UUID\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `topics` | json | List of topics in the category |
| ↳ `id` | string | Topic ID \(UUID\) |
| ↳ `name` | string | Topic name |
### `profound_category_tags`
List tags for a specific category in Profound
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Profound API Key |
| `categoryId` | string | Yes | Category ID \(UUID\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `tags` | json | List of tags in the category |
| ↳ `id` | string | Tag ID \(UUID\) |
| ↳ `name` | string | Tag name |
### `profound_category_prompts`
List prompts for a specific category in Profound
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Profound API Key |
| `categoryId` | string | Yes | Category ID \(UUID\) |
| `limit` | number | No | Maximum number of results \(default 10000, max 10000\) |
| `cursor` | string | No | Pagination cursor from previous response |
| `orderDir` | string | No | Sort direction: asc or desc \(default desc\) |
| `promptType` | string | No | Comma-separated prompt types to filter: visibility, sentiment |
| `topicId` | string | No | Comma-separated topic IDs \(UUIDs\) to filter by |
| `tagId` | string | No | Comma-separated tag IDs \(UUIDs\) to filter by |
| `regionId` | string | No | Comma-separated region IDs \(UUIDs\) to filter by |
| `platformId` | string | No | Comma-separated platform IDs \(UUIDs\) to filter by |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `totalRows` | number | Total number of prompts |
| `nextCursor` | string | Cursor for next page of results |
| `prompts` | json | List of prompts |
| ↳ `id` | string | Prompt ID |
| ↳ `prompt` | string | Prompt text |
| ↳ `promptType` | string | Prompt type \(visibility or sentiment\) |
| ↳ `topicId` | string | Topic ID |
| ↳ `topicName` | string | Topic name |
| ↳ `tags` | json | Associated tags |
| ↳ `regions` | json | Associated regions |
| ↳ `platforms` | json | Associated platforms |
| ↳ `createdAt` | string | When the prompt was created |
### `profound_category_assets`
List assets (companies/brands) for a specific category in Profound
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Profound API Key |
| `categoryId` | string | Yes | Category ID \(UUID\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `assets` | json | List of assets in the category |
| ↳ `id` | string | Asset ID |
| ↳ `name` | string | Asset/company name |
| ↳ `website` | string | Website URL |
| ↳ `alternateDomains` | json | Alternate domain names |
| ↳ `isOwned` | boolean | Whether the asset is owned by the organization |
| ↳ `createdAt` | string | When the asset was created |
| ↳ `logoUrl` | string | URL of the asset logo |
### `profound_category_personas`
List personas for a specific category in Profound
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Profound API Key |
| `categoryId` | string | Yes | Category ID \(UUID\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `personas` | json | List of personas in the category |
| ↳ `id` | string | Persona ID |
| ↳ `name` | string | Persona name |
| ↳ `persona` | json | Persona profile with behavior, employment, and demographics |
### `profound_visibility_report`
Query AI visibility report for a category in Profound
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Profound API Key |
| `categoryId` | string | Yes | Category ID \(UUID\) |
| `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) |
| `endDate` | string | Yes | End date \(YYYY-MM-DD or ISO 8601\) |
| `metrics` | string | Yes | Comma-separated metrics: share_of_voice, mentions_count, visibility_score, executions, average_position |
| `dimensions` | string | No | Comma-separated dimensions: date, region, topic, model, asset_name, prompt, tag, persona |
| `dateInterval` | string | No | Date interval: hour, day, week, month, year |
| `filters` | string | No | JSON array of filter objects, e.g. \[\{"field":"asset_name","operator":"is","value":"Company"\}\] |
| `limit` | number | No | Maximum number of results \(default 10000, max 50000\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `totalRows` | number | Total number of rows in the report |
| `data` | json | Report data rows with metrics and dimension values |
| ↳ `metrics` | json | Array of metric values matching requested metrics order |
| ↳ `dimensions` | json | Array of dimension values matching requested dimensions order |
### `profound_sentiment_report`
Query sentiment report for a category in Profound
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Profound API Key |
| `categoryId` | string | Yes | Category ID \(UUID\) |
| `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) |
| `endDate` | string | Yes | End date \(YYYY-MM-DD or ISO 8601\) |
| `metrics` | string | Yes | Comma-separated metrics: positive, negative, occurrences |
| `dimensions` | string | No | Comma-separated dimensions: theme, date, region, topic, model, asset_name, tag, prompt, sentiment_type, persona |
| `dateInterval` | string | No | Date interval: hour, day, week, month, year |
| `filters` | string | No | JSON array of filter objects, e.g. \[\{"field":"asset_name","operator":"is","value":"Company"\}\] |
| `limit` | number | No | Maximum number of results \(default 10000, max 50000\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `totalRows` | number | Total number of rows in the report |
| `data` | json | Report data rows with metrics and dimension values |
| ↳ `metrics` | json | Array of metric values matching requested metrics order |
| ↳ `dimensions` | json | Array of dimension values matching requested dimensions order |
### `profound_citations_report`
Query citations report for a category in Profound
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Profound API Key |
| `categoryId` | string | Yes | Category ID \(UUID\) |
| `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) |
| `endDate` | string | Yes | End date \(YYYY-MM-DD or ISO 8601\) |
| `metrics` | string | Yes | Comma-separated metrics: count, citation_share |
| `dimensions` | string | No | Comma-separated dimensions: hostname, path, date, region, topic, model, tag, prompt, url, root_domain, persona, citation_category |
| `dateInterval` | string | No | Date interval: hour, day, week, month, year |
| `filters` | string | No | JSON array of filter objects, e.g. \[\{"field":"hostname","operator":"is","value":"example.com"\}\] |
| `limit` | number | No | Maximum number of results \(default 10000, max 50000\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `totalRows` | number | Total number of rows in the report |
| `data` | json | Report data rows with metrics and dimension values |
| ↳ `metrics` | json | Array of metric values matching requested metrics order |
| ↳ `dimensions` | json | Array of dimension values matching requested dimensions order |
### `profound_query_fanouts`
Query fanout report showing how AI models expand prompts into sub-queries in Profound
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Profound API Key |
| `categoryId` | string | Yes | Category ID \(UUID\) |
| `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) |
| `endDate` | string | Yes | End date \(YYYY-MM-DD or ISO 8601\) |
| `metrics` | string | Yes | Comma-separated metrics: fanouts_per_execution, total_fanouts, share |
| `dimensions` | string | No | Comma-separated dimensions: prompt, query, model, region, date |
| `dateInterval` | string | No | Date interval: hour, day, week, month, year |
| `filters` | string | No | JSON array of filter objects |
| `limit` | number | No | Maximum number of results \(default 10000, max 50000\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `totalRows` | number | Total number of rows in the report |
| `data` | json | Report data rows with metrics and dimension values |
| ↳ `metrics` | json | Array of metric values matching requested metrics order |
| ↳ `dimensions` | json | Array of dimension values matching requested dimensions order |
### `profound_prompt_answers`
Get raw prompt answers data for a category in Profound
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Profound API Key |
| `categoryId` | string | Yes | Category ID \(UUID\) |
| `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) |
| `endDate` | string | Yes | End date \(YYYY-MM-DD or ISO 8601\) |
| `filters` | string | No | JSON array of filter objects, e.g. \[\{"field":"prompt_type","operator":"is","value":"visibility"\}\] |
| `limit` | number | No | Maximum number of results \(default 10000, max 50000\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `totalRows` | number | Total number of answer rows |
| `data` | json | Raw prompt answer data |
| ↳ `prompt` | string | The prompt text |
| ↳ `promptType` | string | Prompt type \(visibility or sentiment\) |
| ↳ `response` | string | AI model response text |
| ↳ `mentions` | json | Companies/assets mentioned in the response |
| ↳ `citations` | json | URLs cited in the response |
| ↳ `topic` | string | Topic name |
| ↳ `region` | string | Region name |
| ↳ `model` | string | AI model/platform name |
| ↳ `asset` | string | Asset name |
| ↳ `createdAt` | string | Timestamp when the answer was collected |
### `profound_bots_report`
Query bot traffic report with hourly granularity for a domain in Profound
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Profound API Key |
| `domain` | string | Yes | Domain to query bot traffic for \(e.g. example.com\) |
| `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) |
| `endDate` | string | No | End date \(YYYY-MM-DD or ISO 8601\). Defaults to now |
| `metrics` | string | Yes | Comma-separated metrics: count, citations, indexing, training, last_visit |
| `dimensions` | string | No | Comma-separated dimensions: date, hour, path, bot_name, bot_provider, bot_type |
| `dateInterval` | string | No | Date interval: hour, day, week, month, year |
| `filters` | string | No | JSON array of filter objects, e.g. \[\{"field":"bot_name","operator":"is","value":"GPTBot"\}\] |
| `limit` | number | No | Maximum number of results \(default 10000, max 50000\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `totalRows` | number | Total number of rows in the report |
| `data` | json | Report data rows with metrics and dimension values |
| ↳ `metrics` | json | Array of metric values matching requested metrics order |
| ↳ `dimensions` | json | Array of dimension values matching requested dimensions order |
### `profound_referrals_report`
Query human referral traffic report with hourly granularity for a domain in Profound
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Profound API Key |
| `domain` | string | Yes | Domain to query referral traffic for \(e.g. example.com\) |
| `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) |
| `endDate` | string | No | End date \(YYYY-MM-DD or ISO 8601\). Defaults to now |
| `metrics` | string | Yes | Comma-separated metrics: visits, last_visit |
| `dimensions` | string | No | Comma-separated dimensions: date, hour, path, referral_source, referral_type |
| `dateInterval` | string | No | Date interval: hour, day, week, month, year |
| `filters` | string | No | JSON array of filter objects, e.g. \[\{"field":"referral_source","operator":"is","value":"openai"\}\] |
| `limit` | number | No | Maximum number of results \(default 10000, max 50000\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `totalRows` | number | Total number of rows in the report |
| `data` | json | Report data rows with metrics and dimension values |
| ↳ `metrics` | json | Array of metric values matching requested metrics order |
| ↳ `dimensions` | json | Array of dimension values matching requested dimensions order |
### `profound_raw_logs`
Get raw traffic logs with filters for a domain in Profound
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Profound API Key |
| `domain` | string | Yes | Domain to query logs for \(e.g. example.com\) |
| `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) |
| `endDate` | string | No | End date \(YYYY-MM-DD or ISO 8601\). Defaults to now |
| `dimensions` | string | No | Comma-separated dimensions: timestamp, method, host, path, status_code, ip, user_agent, referer, bytes_sent, duration_ms, query_params |
| `filters` | string | No | JSON array of filter objects, e.g. \[\{"field":"path","operator":"contains","value":"/blog"\}\] |
| `limit` | number | No | Maximum number of results \(default 10000, max 50000\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `totalRows` | number | Total number of log entries |
| `data` | json | Log data rows with metrics and dimension values |
| ↳ `metrics` | json | Array of metric values \(count\) |
| ↳ `dimensions` | json | Array of dimension values matching requested dimensions order |
### `profound_bot_logs`
Get identified bot visit logs with filters for a domain in Profound
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Profound API Key |
| `domain` | string | Yes | Domain to query bot logs for \(e.g. example.com\) |
| `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) |
| `endDate` | string | No | End date \(YYYY-MM-DD or ISO 8601\). Defaults to now |
| `dimensions` | string | No | Comma-separated dimensions: timestamp, method, host, path, status_code, ip, user_agent, referer, bytes_sent, duration_ms, query_params, bot_name, bot_provider, bot_types |
| `filters` | string | No | JSON array of filter objects, e.g. \[\{"field":"bot_name","operator":"is","value":"GPTBot"\}\] |
| `limit` | number | No | Maximum number of results \(default 10000, max 50000\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `totalRows` | number | Total number of bot log entries |
| `data` | json | Bot log data rows with metrics and dimension values |
| ↳ `metrics` | json | Array of metric values \(count\) |
| ↳ `dimensions` | json | Array of dimension values matching requested dimensions order |
### `profound_list_optimizations`
List content optimization entries for an asset in Profound
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Profound API Key |
| `assetId` | string | Yes | Asset ID \(UUID\) |
| `limit` | number | No | Maximum number of results \(default 10000, max 50000\) |
| `offset` | number | No | Offset for pagination \(default 0\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `totalRows` | number | Total number of optimization entries |
| `optimizations` | json | List of content optimization entries |
| ↳ `id` | string | Optimization ID \(UUID\) |
| ↳ `title` | string | Content title |
| ↳ `createdAt` | string | When the optimization was created |
| ↳ `extractedInput` | string | Extracted input text |
| ↳ `type` | string | Content type: file, text, or url |
| ↳ `status` | string | Optimization status |
### `profound_optimization_analysis`
Get detailed content optimization analysis for a specific content item in Profound
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Profound API Key |
| `assetId` | string | Yes | Asset ID \(UUID\) |
| `contentId` | string | Yes | Content/optimization ID \(UUID\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | json | The analyzed content |
| ↳ `format` | string | Content format: markdown or html |
| ↳ `value` | string | Content text |
| `aeoContentScore` | json | AEO content score with target zone |
| ↳ `value` | number | AEO score value |
| ↳ `targetZone` | json | Target zone range |
| ↳ `low` | number | Low end of target range |
| ↳ `high` | number | High end of target range |
| `analysis` | json | Analysis breakdown by category |
| ↳ `breakdown` | json | Array of scoring breakdowns |
| ↳ `title` | string | Category title |
| ↳ `weight` | number | Category weight |
| ↳ `score` | number | Category score |
| `recommendations` | json | Content optimization recommendations |
| ↳ `title` | string | Recommendation title |
| ↳ `status` | string | Status: done or pending |
| ↳ `impact` | json | Impact details with section and score |
| ↳ `suggestion` | json | Suggestion text and rationale |
| ↳ `text` | string | Suggestion text |
| ↳ `rationale` | string | Why this recommendation matters |
### `profound_prompt_volume`
Query prompt volume data to understand search demand across AI platforms in Profound
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Profound API Key |
| `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) |
| `endDate` | string | Yes | End date \(YYYY-MM-DD or ISO 8601\) |
| `metrics` | string | Yes | Comma-separated metrics: volume, change |
| `dimensions` | string | No | Comma-separated dimensions: keyword, date, platform, country_code, matching_type, frequency |
| `dateInterval` | string | No | Date interval: hour, day, week, month, year |
| `filters` | string | No | JSON array of filter objects, e.g. \[\{"field":"keyword","operator":"contains","value":"best"\}\] |
| `limit` | number | No | Maximum number of results \(default 10000, max 50000\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `totalRows` | number | Total number of rows in the report |
| `data` | json | Volume data rows with metrics and dimension values |
| ↳ `metrics` | json | Array of metric values matching requested metrics order |
| ↳ `dimensions` | json | Array of dimension values matching requested dimensions order |
### `profound_citation_prompts`
Get prompts that cite a specific domain across AI platforms in Profound
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Profound API Key |
| `inputDomain` | string | Yes | Domain to look up citations for \(e.g. ramp.com\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `data` | json | Citation prompt data for the queried domain |

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,891 @@
---
title: Rootly
description: Manage incidents, alerts, and on-call with Rootly
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="rootly"
color="#6C72C8"
/>
{/* MANUAL-CONTENT-START:intro */}
[Rootly](https://rootly.com/) is an incident management platform that helps teams respond to, mitigate, and learn from incidents — all without leaving Slack or your existing tools. Rootly automates on-call alerting, incident workflows, status page updates, and retrospectives so engineering teams can resolve issues faster and reduce toil.
**Why Rootly?**
- **End-to-End Incident Management:** Create, track, update, and resolve incidents with full lifecycle support — from initial triage through retrospective.
- **On-Call Alerting:** Create and manage alerts with deduplication, routing, and escalation to ensure the right people are notified immediately.
- **Timeline Events:** Add structured timeline events to incidents for clear, auditable incident narratives.
- **Service Catalog:** Maintain a catalog of services and map them to incidents for precise impact tracking.
- **Severity & Prioritization:** Use configurable severity levels to prioritize incidents and drive appropriate response urgency.
- **Retrospectives:** Access post-incident retrospectives to identify root causes, capture learnings, and drive reliability improvements.
**Using Rootly in Sim**
Sim's Rootly integration connects your agentic workflows directly to your Rootly account using an API key. With operations spanning incidents, alerts, services, severities, teams, environments, functionalities, incident types, and retrospectives, you can build powerful incident management automations without writing backend code.
**Key benefits of using Rootly in Sim:**
- **Automated incident creation:** Trigger incident creation from monitoring alerts, customer reports, or anomaly detection workflows with full metadata including severity, services, and teams.
- **Incident lifecycle automation:** Automatically update incident status, add timeline events, and attach mitigation or resolution messages as your response progresses.
- **Alert management:** Create and list alerts with deduplication support to integrate Rootly into your existing monitoring and notification pipelines.
- **Organizational awareness:** Query services, severities, teams, environments, functionalities, and incident types to build context-aware incident workflows.
- **Retrospective insights:** List and filter retrospectives to feed post-incident learnings into continuous improvement workflows.
Whether you're automating incident response, building on-call alerting pipelines, or driving post-incident learning, Rootly in Sim gives you direct, secure access to the Rootly API — no middleware required. Simply configure your API key, select the operation you need, and let Sim handle the rest.
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Integrate Rootly incident management into workflows. Create and manage incidents, alerts, services, severities, and retrospectives.
## Tools
### `rootly_create_incident`
Create a new incident in Rootly with optional severity, services, and teams.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Rootly API key |
| `title` | string | No | The title of the incident \(auto-generated if not provided\) |
| `summary` | string | No | A summary of the incident |
| `severityId` | string | No | Severity ID to attach to the incident |
| `status` | string | No | Incident status \(in_triage, started, detected, acknowledged, mitigated, resolved, closed, cancelled, scheduled, in_progress, completed\) |
| `kind` | string | No | Incident kind \(normal, normal_sub, test, test_sub, example, example_sub, backfilled, scheduled, scheduled_sub\) |
| `serviceIds` | string | No | Comma-separated service IDs to attach |
| `environmentIds` | string | No | Comma-separated environment IDs to attach |
| `groupIds` | string | No | Comma-separated team/group IDs to attach |
| `incidentTypeIds` | string | No | Comma-separated incident type IDs to attach |
| `functionalityIds` | string | No | Comma-separated functionality IDs to attach |
| `labels` | string | No | Labels as JSON object, e.g. \{"platform":"osx","version":"1.29"\} |
| `private` | boolean | No | Create as a private incident \(cannot be undone\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `incident` | object | The created incident |
| ↳ `id` | string | Unique incident ID |
| ↳ `sequentialId` | number | Sequential incident number |
| ↳ `title` | string | Incident title |
| ↳ `slug` | string | Incident slug |
| ↳ `kind` | string | Incident kind |
| ↳ `summary` | string | Incident summary |
| ↳ `status` | string | Incident status |
| ↳ `private` | boolean | Whether the incident is private |
| ↳ `url` | string | URL to the incident |
| ↳ `shortUrl` | string | Short URL to the incident |
| ↳ `severityName` | string | Severity name |
| ↳ `severityId` | string | Severity ID |
| ↳ `createdAt` | string | Creation date |
| ↳ `updatedAt` | string | Last update date |
| ↳ `startedAt` | string | Start date |
| ↳ `mitigatedAt` | string | Mitigation date |
| ↳ `resolvedAt` | string | Resolution date |
| ↳ `closedAt` | string | Closed date |
### `rootly_get_incident`
Retrieve a single incident by ID from Rootly.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Rootly API key |
| `incidentId` | string | Yes | The ID of the incident to retrieve |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `incident` | object | The incident details |
| ↳ `id` | string | Unique incident ID |
| ↳ `sequentialId` | number | Sequential incident number |
| ↳ `title` | string | Incident title |
| ↳ `slug` | string | Incident slug |
| ↳ `kind` | string | Incident kind |
| ↳ `summary` | string | Incident summary |
| ↳ `status` | string | Incident status |
| ↳ `private` | boolean | Whether the incident is private |
| ↳ `url` | string | URL to the incident |
| ↳ `shortUrl` | string | Short URL to the incident |
| ↳ `severityName` | string | Severity name |
| ↳ `severityId` | string | Severity ID |
| ↳ `createdAt` | string | Creation date |
| ↳ `updatedAt` | string | Last update date |
| ↳ `startedAt` | string | Start date |
| ↳ `mitigatedAt` | string | Mitigation date |
| ↳ `resolvedAt` | string | Resolution date |
| ↳ `closedAt` | string | Closed date |
### `rootly_update_incident`
Update an existing incident in Rootly (status, severity, summary, etc.).
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Rootly API key |
| `incidentId` | string | Yes | The ID of the incident to update |
| `title` | string | No | Updated incident title |
| `summary` | string | No | Updated incident summary |
| `severityId` | string | No | Updated severity ID |
| `status` | string | No | Updated status \(in_triage, started, detected, acknowledged, mitigated, resolved, closed, cancelled, scheduled, in_progress, completed\) |
| `kind` | string | No | Incident kind \(normal, normal_sub, test, test_sub, example, example_sub, backfilled, scheduled, scheduled_sub\) |
| `private` | boolean | No | Set incident as private \(cannot be undone\) |
| `serviceIds` | string | No | Comma-separated service IDs |
| `environmentIds` | string | No | Comma-separated environment IDs |
| `groupIds` | string | No | Comma-separated team/group IDs |
| `incidentTypeIds` | string | No | Comma-separated incident type IDs to attach |
| `functionalityIds` | string | No | Comma-separated functionality IDs to attach |
| `labels` | string | No | Labels as JSON object, e.g. \{"platform":"osx","version":"1.29"\} |
| `mitigationMessage` | string | No | How was the incident mitigated? |
| `resolutionMessage` | string | No | How was the incident resolved? |
| `cancellationMessage` | string | No | Why was the incident cancelled? |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `incident` | object | The updated incident |
| ↳ `id` | string | Unique incident ID |
| ↳ `sequentialId` | number | Sequential incident number |
| ↳ `title` | string | Incident title |
| ↳ `slug` | string | Incident slug |
| ↳ `kind` | string | Incident kind |
| ↳ `summary` | string | Incident summary |
| ↳ `status` | string | Incident status |
| ↳ `private` | boolean | Whether the incident is private |
| ↳ `url` | string | URL to the incident |
| ↳ `shortUrl` | string | Short URL to the incident |
| ↳ `severityName` | string | Severity name |
| ↳ `severityId` | string | Severity ID |
| ↳ `createdAt` | string | Creation date |
| ↳ `updatedAt` | string | Last update date |
| ↳ `startedAt` | string | Start date |
| ↳ `mitigatedAt` | string | Mitigation date |
| ↳ `resolvedAt` | string | Resolution date |
| ↳ `closedAt` | string | Closed date |
### `rootly_list_incidents`
List incidents from Rootly with optional filtering by status, severity, and more.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Rootly API key |
| `status` | string | No | Filter by status \(in_triage, started, detected, acknowledged, mitigated, resolved, closed, cancelled, scheduled, in_progress, completed\) |
| `severity` | string | No | Filter by severity slug |
| `search` | string | No | Search term to filter incidents |
| `services` | string | No | Filter by service slugs \(comma-separated\) |
| `teams` | string | No | Filter by team slugs \(comma-separated\) |
| `environments` | string | No | Filter by environment slugs \(comma-separated\) |
| `sort` | string | No | Sort order \(e.g., -created_at, created_at, -started_at\) |
| `pageSize` | number | No | Number of items per page \(default: 20\) |
| `pageNumber` | number | No | Page number for pagination |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `incidents` | array | List of incidents |
| ↳ `id` | string | Unique incident ID |
| ↳ `sequentialId` | number | Sequential incident number |
| ↳ `title` | string | Incident title |
| ↳ `slug` | string | Incident slug |
| ↳ `kind` | string | Incident kind |
| ↳ `summary` | string | Incident summary |
| ↳ `status` | string | Incident status |
| ↳ `private` | boolean | Whether the incident is private |
| ↳ `url` | string | URL to the incident |
| ↳ `shortUrl` | string | Short URL to the incident |
| ↳ `severityName` | string | Severity name |
| ↳ `severityId` | string | Severity ID |
| ↳ `createdAt` | string | Creation date |
| ↳ `updatedAt` | string | Last update date |
| ↳ `startedAt` | string | Start date |
| ↳ `mitigatedAt` | string | Mitigation date |
| ↳ `resolvedAt` | string | Resolution date |
| ↳ `closedAt` | string | Closed date |
| `totalCount` | number | Total number of incidents returned |
### `rootly_create_alert`
Create a new alert in Rootly for on-call notification and routing.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Rootly API key |
| `summary` | string | Yes | The summary of the alert |
| `description` | string | No | A detailed description of the alert |
| `source` | string | Yes | The source of the alert \(e.g., api, manual, datadog, pagerduty\) |
| `status` | string | No | Alert status on creation \(open, triggered\) |
| `serviceIds` | string | No | Comma-separated service IDs to attach |
| `groupIds` | string | No | Comma-separated team/group IDs to attach |
| `environmentIds` | string | No | Comma-separated environment IDs to attach |
| `externalId` | string | No | External ID for the alert |
| `externalUrl` | string | No | External URL for the alert |
| `deduplicationKey` | string | No | Alerts sharing the same deduplication key are treated as a single alert |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `alert` | object | The created alert |
| ↳ `id` | string | Unique alert ID |
| ↳ `shortId` | string | Short alert ID |
| ↳ `summary` | string | Alert summary |
| ↳ `description` | string | Alert description |
| ↳ `source` | string | Alert source |
| ↳ `status` | string | Alert status |
| ↳ `externalId` | string | External ID |
| ↳ `externalUrl` | string | External URL |
| ↳ `deduplicationKey` | string | Deduplication key |
| ↳ `createdAt` | string | Creation date |
| ↳ `updatedAt` | string | Last update date |
| ↳ `startedAt` | string | Start date |
| ↳ `endedAt` | string | End date |
### `rootly_list_alerts`
List alerts from Rootly with optional filtering by status, source, and services.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Rootly API key |
| `status` | string | No | Filter by status \(open, triggered, acknowledged, resolved\) |
| `source` | string | No | Filter by source \(e.g., api, datadog, pagerduty\) |
| `services` | string | No | Filter by service slugs \(comma-separated\) |
| `environments` | string | No | Filter by environment slugs \(comma-separated\) |
| `groups` | string | No | Filter by team/group slugs \(comma-separated\) |
| `pageSize` | number | No | Number of items per page \(default: 20\) |
| `pageNumber` | number | No | Page number for pagination |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `alerts` | array | List of alerts |
| ↳ `id` | string | Unique alert ID |
| ↳ `shortId` | string | Short alert ID |
| ↳ `summary` | string | Alert summary |
| ↳ `description` | string | Alert description |
| ↳ `source` | string | Alert source |
| ↳ `status` | string | Alert status |
| ↳ `externalId` | string | External ID |
| ↳ `externalUrl` | string | External URL |
| ↳ `deduplicationKey` | string | Deduplication key |
| ↳ `createdAt` | string | Creation date |
| ↳ `updatedAt` | string | Last update date |
| ↳ `startedAt` | string | Start date |
| ↳ `endedAt` | string | End date |
| `totalCount` | number | Total number of alerts returned |
### `rootly_add_incident_event`
Add a timeline event to an existing incident in Rootly.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Rootly API key |
| `incidentId` | string | Yes | The ID of the incident to add the event to |
| `event` | string | Yes | The summary/description of the event |
| `visibility` | string | No | Event visibility \(internal or external\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `eventId` | string | The ID of the created event |
| `event` | string | The event summary |
| `visibility` | string | Event visibility \(internal or external\) |
| `occurredAt` | string | When the event occurred |
| `createdAt` | string | Creation date |
| `updatedAt` | string | Last update date |
### `rootly_list_services`
List services from Rootly with optional search filtering.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Rootly API key |
| `search` | string | No | Search term to filter services |
| `pageSize` | number | No | Number of items per page \(default: 20\) |
| `pageNumber` | number | No | Page number for pagination |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `services` | array | List of services |
| ↳ `id` | string | Unique service ID |
| ↳ `name` | string | Service name |
| ↳ `slug` | string | Service slug |
| ↳ `description` | string | Service description |
| ↳ `color` | string | Service color |
| ↳ `createdAt` | string | Creation date |
| ↳ `updatedAt` | string | Last update date |
| `totalCount` | number | Total number of services returned |
### `rootly_list_severities`
List severity levels configured in Rootly.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Rootly API key |
| `search` | string | No | Search term to filter severities |
| `pageSize` | number | No | Number of items per page \(default: 20\) |
| `pageNumber` | number | No | Page number for pagination |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `severities` | array | List of severity levels |
| ↳ `id` | string | Unique severity ID |
| ↳ `name` | string | Severity name |
| ↳ `slug` | string | Severity slug |
| ↳ `description` | string | Severity description |
| ↳ `severity` | string | Severity level \(critical, high, medium, low\) |
| ↳ `color` | string | Severity color |
| ↳ `position` | number | Display position |
| ↳ `createdAt` | string | Creation date |
| ↳ `updatedAt` | string | Last update date |
| `totalCount` | number | Total number of severities returned |
### `rootly_list_teams`
List teams (groups) configured in Rootly.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Rootly API key |
| `search` | string | No | Search term to filter teams |
| `pageSize` | number | No | Number of items per page \(default: 20\) |
| `pageNumber` | number | No | Page number for pagination |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `teams` | array | List of teams |
| ↳ `id` | string | Unique team ID |
| ↳ `name` | string | Team name |
| ↳ `slug` | string | Team slug |
| ↳ `description` | string | Team description |
| ↳ `color` | string | Team color |
| ↳ `createdAt` | string | Creation date |
| ↳ `updatedAt` | string | Last update date |
| `totalCount` | number | Total number of teams returned |
### `rootly_list_environments`
List environments configured in Rootly.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Rootly API key |
| `search` | string | No | Search term to filter environments |
| `pageSize` | number | No | Number of items per page \(default: 20\) |
| `pageNumber` | number | No | Page number for pagination |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `environments` | array | List of environments |
| ↳ `id` | string | Unique environment ID |
| ↳ `name` | string | Environment name |
| ↳ `slug` | string | Environment slug |
| ↳ `description` | string | Environment description |
| ↳ `color` | string | Environment color |
| ↳ `createdAt` | string | Creation date |
| ↳ `updatedAt` | string | Last update date |
| `totalCount` | number | Total number of environments returned |
### `rootly_list_incident_types`
List incident types configured in Rootly.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Rootly API key |
| `search` | string | No | Filter incident types by name |
| `pageSize` | number | No | Number of items per page \(default: 20\) |
| `pageNumber` | number | No | Page number for pagination |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `incidentTypes` | array | List of incident types |
| ↳ `id` | string | Unique incident type ID |
| ↳ `name` | string | Incident type name |
| ↳ `slug` | string | Incident type slug |
| ↳ `description` | string | Incident type description |
| ↳ `color` | string | Incident type color |
| ↳ `createdAt` | string | Creation date |
| ↳ `updatedAt` | string | Last update date |
| `totalCount` | number | Total number of incident types returned |
### `rootly_list_functionalities`
List functionalities configured in Rootly.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Rootly API key |
| `search` | string | No | Search term to filter functionalities |
| `pageSize` | number | No | Number of items per page \(default: 20\) |
| `pageNumber` | number | No | Page number for pagination |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `functionalities` | array | List of functionalities |
| ↳ `id` | string | Unique functionality ID |
| ↳ `name` | string | Functionality name |
| ↳ `slug` | string | Functionality slug |
| ↳ `description` | string | Functionality description |
| ↳ `color` | string | Functionality color |
| ↳ `createdAt` | string | Creation date |
| ↳ `updatedAt` | string | Last update date |
| `totalCount` | number | Total number of functionalities returned |
### `rootly_list_retrospectives`
List incident retrospectives (post-mortems) from Rootly.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Rootly API key |
| `status` | string | No | Filter by status \(draft, published\) |
| `search` | string | No | Search term to filter retrospectives |
| `pageSize` | number | No | Number of items per page \(default: 20\) |
| `pageNumber` | number | No | Page number for pagination |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `retrospectives` | array | List of retrospectives |
| ↳ `id` | string | Unique retrospective ID |
| ↳ `title` | string | Retrospective title |
| ↳ `status` | string | Status \(draft or published\) |
| ↳ `url` | string | URL to the retrospective |
| ↳ `startedAt` | string | Incident start date |
| ↳ `mitigatedAt` | string | Mitigation date |
| ↳ `resolvedAt` | string | Resolution date |
| ↳ `createdAt` | string | Creation date |
| ↳ `updatedAt` | string | Last update date |
| `totalCount` | number | Total number of retrospectives returned |
### `rootly_delete_incident`
Delete an incident by ID from Rootly.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Rootly API key |
| `incidentId` | string | Yes | The ID of the incident to delete |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the deletion succeeded |
| `message` | string | Result message |
### `rootly_get_alert`
Retrieve a single alert by ID from Rootly.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Rootly API key |
| `alertId` | string | Yes | The ID of the alert to retrieve |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `alert` | object | The alert details |
| ↳ `id` | string | Unique alert ID |
| ↳ `shortId` | string | Short alert ID |
| ↳ `summary` | string | Alert summary |
| ↳ `description` | string | Alert description |
| ↳ `source` | string | Alert source |
| ↳ `status` | string | Alert status |
| ↳ `externalId` | string | External ID |
| ↳ `externalUrl` | string | External URL |
| ↳ `deduplicationKey` | string | Deduplication key |
| ↳ `createdAt` | string | Creation date |
| ↳ `updatedAt` | string | Last update date |
| ↳ `startedAt` | string | Start date |
| ↳ `endedAt` | string | End date |
### `rootly_update_alert`
Update an existing alert in Rootly.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Rootly API key |
| `alertId` | string | Yes | The ID of the alert to update |
| `summary` | string | No | Updated alert summary |
| `description` | string | No | Updated alert description |
| `source` | string | No | Updated alert source |
| `serviceIds` | string | No | Comma-separated service IDs to attach |
| `groupIds` | string | No | Comma-separated team/group IDs to attach |
| `environmentIds` | string | No | Comma-separated environment IDs to attach |
| `externalId` | string | No | Updated external ID |
| `externalUrl` | string | No | Updated external URL |
| `deduplicationKey` | string | No | Updated deduplication key |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `alert` | object | The updated alert |
| ↳ `id` | string | Unique alert ID |
| ↳ `shortId` | string | Short alert ID |
| ↳ `summary` | string | Alert summary |
| ↳ `description` | string | Alert description |
| ↳ `source` | string | Alert source |
| ↳ `status` | string | Alert status |
| ↳ `externalId` | string | External ID |
| ↳ `externalUrl` | string | External URL |
| ↳ `deduplicationKey` | string | Deduplication key |
| ↳ `createdAt` | string | Creation date |
| ↳ `updatedAt` | string | Last update date |
| ↳ `startedAt` | string | Start date |
| ↳ `endedAt` | string | End date |
### `rootly_acknowledge_alert`
Acknowledge an alert in Rootly.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Rootly API key |
| `alertId` | string | Yes | The ID of the alert to acknowledge |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `alert` | object | The acknowledged alert |
| ↳ `id` | string | Unique alert ID |
| ↳ `shortId` | string | Short alert ID |
| ↳ `summary` | string | Alert summary |
| ↳ `description` | string | Alert description |
| ↳ `source` | string | Alert source |
| ↳ `status` | string | Alert status |
| ↳ `externalId` | string | External ID |
| ↳ `externalUrl` | string | External URL |
| ↳ `deduplicationKey` | string | Deduplication key |
| ↳ `createdAt` | string | Creation date |
| ↳ `updatedAt` | string | Last update date |
| ↳ `startedAt` | string | Start date |
| ↳ `endedAt` | string | End date |
### `rootly_resolve_alert`
Resolve an alert in Rootly.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Rootly API key |
| `alertId` | string | Yes | The ID of the alert to resolve |
| `resolutionMessage` | string | No | Message describing how the alert was resolved |
| `resolveRelatedIncidents` | boolean | No | Whether to also resolve related incidents |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `alert` | object | The resolved alert |
| ↳ `id` | string | Unique alert ID |
| ↳ `shortId` | string | Short alert ID |
| ↳ `summary` | string | Alert summary |
| ↳ `description` | string | Alert description |
| ↳ `source` | string | Alert source |
| ↳ `status` | string | Alert status |
| ↳ `externalId` | string | External ID |
| ↳ `externalUrl` | string | External URL |
| ↳ `deduplicationKey` | string | Deduplication key |
| ↳ `createdAt` | string | Creation date |
| ↳ `updatedAt` | string | Last update date |
| ↳ `startedAt` | string | Start date |
| ↳ `endedAt` | string | End date |
### `rootly_create_action_item`
Create a new action item for an incident in Rootly.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Rootly API key |
| `incidentId` | string | Yes | The ID of the incident to add the action item to |
| `summary` | string | Yes | The title of the action item |
| `description` | string | No | A detailed description of the action item |
| `kind` | string | No | The kind of action item \(task, follow_up\) |
| `priority` | string | No | Priority level \(high, medium, low\) |
| `status` | string | No | Action item status \(open, in_progress, cancelled, done\) |
| `assignedToUserId` | string | No | The user ID to assign the action item to |
| `dueDate` | string | No | Due date for the action item |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `actionItem` | object | The created action item |
| ↳ `id` | string | Unique action item ID |
| ↳ `summary` | string | Action item title |
| ↳ `description` | string | Action item description |
| ↳ `kind` | string | Action item kind \(task, follow_up\) |
| ↳ `priority` | string | Priority level |
| ↳ `status` | string | Action item status |
| ↳ `dueDate` | string | Due date |
| ↳ `createdAt` | string | Creation date |
| ↳ `updatedAt` | string | Last update date |
### `rootly_list_action_items`
List action items for an incident in Rootly.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Rootly API key |
| `incidentId` | string | Yes | The ID of the incident to list action items for |
| `pageSize` | number | No | Number of items per page \(default: 20\) |
| `pageNumber` | number | No | Page number for pagination |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `actionItems` | array | List of action items |
| ↳ `id` | string | Unique action item ID |
| ↳ `summary` | string | Action item title |
| ↳ `description` | string | Action item description |
| ↳ `kind` | string | Action item kind \(task, follow_up\) |
| ↳ `priority` | string | Priority level |
| ↳ `status` | string | Action item status |
| ↳ `dueDate` | string | Due date |
| ↳ `createdAt` | string | Creation date |
| ↳ `updatedAt` | string | Last update date |
| `totalCount` | number | Total number of action items returned |
### `rootly_list_users`
List users from Rootly with optional search and email filtering.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Rootly API key |
| `search` | string | No | Search term to filter users |
| `email` | string | No | Filter users by email address |
| `pageSize` | number | No | Number of items per page \(default: 20\) |
| `pageNumber` | number | No | Page number for pagination |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `users` | array | List of users |
| ↳ `id` | string | Unique user ID |
| ↳ `email` | string | User email address |
| ↳ `firstName` | string | User first name |
| ↳ `lastName` | string | User last name |
| ↳ `fullName` | string | User full name |
| ↳ `timeZone` | string | User time zone |
| ↳ `createdAt` | string | Creation date |
| ↳ `updatedAt` | string | Last update date |
| `totalCount` | number | Total number of users returned |
### `rootly_list_on_calls`
List current on-call entries from Rootly with optional filtering.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Rootly API key |
| `scheduleIds` | string | No | Comma-separated schedule IDs to filter by |
| `escalationPolicyIds` | string | No | Comma-separated escalation policy IDs to filter by |
| `userIds` | string | No | Comma-separated user IDs to filter by |
| `serviceIds` | string | No | Comma-separated service IDs to filter by |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `onCalls` | array | List of on-call entries |
| ↳ `id` | string | Unique on-call entry ID |
| ↳ `userId` | string | ID of the on-call user |
| ↳ `userName` | string | Name of the on-call user |
| ↳ `scheduleId` | string | ID of the associated schedule |
| ↳ `scheduleName` | string | Name of the associated schedule |
| ↳ `escalationPolicyId` | string | ID of the associated escalation policy |
| ↳ `startTime` | string | On-call start time |
| ↳ `endTime` | string | On-call end time |
| `totalCount` | number | Total number of on-call entries returned |
### `rootly_list_schedules`
List on-call schedules from Rootly with optional search filtering.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Rootly API key |
| `search` | string | No | Search term to filter schedules |
| `pageSize` | number | No | Number of items per page \(default: 20\) |
| `pageNumber` | number | No | Page number for pagination |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `schedules` | array | List of schedules |
| ↳ `id` | string | Unique schedule ID |
| ↳ `name` | string | Schedule name |
| ↳ `description` | string | Schedule description |
| ↳ `allTimeCoverage` | boolean | Whether schedule provides 24/7 coverage |
| ↳ `createdAt` | string | Creation date |
| ↳ `updatedAt` | string | Last update date |
| `totalCount` | number | Total number of schedules returned |
### `rootly_list_escalation_policies`
List escalation policies from Rootly with optional search filtering.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Rootly API key |
| `search` | string | No | Search term to filter escalation policies |
| `pageSize` | number | No | Number of items per page \(default: 20\) |
| `pageNumber` | number | No | Page number for pagination |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `escalationPolicies` | array | List of escalation policies |
| ↳ `id` | string | Unique escalation policy ID |
| ↳ `name` | string | Escalation policy name |
| ↳ `description` | string | Escalation policy description |
| ↳ `repeatCount` | number | Number of times to repeat escalation |
| ↳ `groupIds` | array | Associated group IDs |
| ↳ `serviceIds` | array | Associated service IDs |
| ↳ `createdAt` | string | Creation date |
| ↳ `updatedAt` | string | Last update date |
| `totalCount` | number | Total number of escalation policies returned |
### `rootly_list_causes`
List causes from Rootly with optional search filtering.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Rootly API key |
| `search` | string | No | Search term to filter causes |
| `pageSize` | number | No | Number of items per page \(default: 20\) |
| `pageNumber` | number | No | Page number for pagination |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `causes` | array | List of causes |
| ↳ `id` | string | Unique cause ID |
| ↳ `name` | string | Cause name |
| ↳ `slug` | string | Cause slug |
| ↳ `description` | string | Cause description |
| ↳ `position` | number | Cause position |
| ↳ `createdAt` | string | Creation date |
| ↳ `updatedAt` | string | Last update date |
| `totalCount` | number | Total number of causes returned |
### `rootly_list_playbooks`
List playbooks from Rootly with pagination support.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Rootly API key |
| `pageSize` | number | No | Number of items per page \(default: 20\) |
| `pageNumber` | number | No | Page number for pagination |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `playbooks` | array | List of playbooks |
| ↳ `id` | string | Unique playbook ID |
| ↳ `title` | string | Playbook title |
| ↳ `summary` | string | Playbook summary |
| ↳ `externalUrl` | string | External URL |
| ↳ `createdAt` | string | Creation date |
| ↳ `updatedAt` | string | Last update date |
| `totalCount` | number | Total number of playbooks returned |

View File

@@ -0,0 +1,157 @@
---
title: AWS Secrets Manager
description: Connect to AWS Secrets Manager
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="secrets_manager"
color="linear-gradient(45deg, #BD0816 0%, #FF5252 100%)"
/>
{/* MANUAL-CONTENT-START:intro */}
[AWS Secrets Manager](https://aws.amazon.com/secrets-manager/) is a secrets management service that helps you protect access to your applications, services, and IT resources. It enables you to rotate, manage, and retrieve database credentials, API keys, and other secrets throughout their lifecycle.
With AWS Secrets Manager, you can:
- **Securely store secrets**: Encrypt secrets at rest using AWS KMS encryption keys
- **Retrieve secrets programmatically**: Access secrets from your applications and workflows without hardcoding credentials
- **Rotate secrets automatically**: Configure automatic rotation for supported services like RDS, Redshift, and DocumentDB
- **Audit access**: Track secret access and changes through AWS CloudTrail integration
- **Control access with IAM**: Use fine-grained IAM policies to manage who can access which secrets
- **Replicate across regions**: Automatically replicate secrets to multiple AWS regions for disaster recovery
In Sim, the AWS Secrets Manager integration allows your workflows to securely retrieve credentials and configuration values at runtime, create and manage secrets as part of automation pipelines, and maintain a centralized secrets store that your agents can access. This is particularly useful for workflows that need to authenticate with external services, rotate credentials, or manage sensitive configuration across environments — all without exposing secrets in your workflow definitions.
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Integrate AWS Secrets Manager into the workflow. Can retrieve, create, update, list, and delete secrets.
## Tools
### `secrets_manager_get_secret`
Retrieve a secret value from AWS Secrets Manager
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
| `accessKeyId` | string | Yes | AWS access key ID |
| `secretAccessKey` | string | Yes | AWS secret access key |
| `secretId` | string | Yes | The name or ARN of the secret to retrieve |
| `versionId` | string | No | The unique identifier of the version to retrieve |
| `versionStage` | string | No | The staging label of the version to retrieve \(e.g., AWSCURRENT, AWSPREVIOUS\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `name` | string | Name of the secret |
| `secretValue` | string | The decrypted secret value |
| `arn` | string | ARN of the secret |
| `versionId` | string | Version ID of the secret |
| `versionStages` | array | Staging labels attached to this version |
| `createdDate` | string | Date the secret was created |
### `secrets_manager_list_secrets`
List secrets stored in AWS Secrets Manager
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
| `accessKeyId` | string | Yes | AWS access key ID |
| `secretAccessKey` | string | Yes | AWS secret access key |
| `maxResults` | number | No | Maximum number of secrets to return \(1-100, default 100\) |
| `nextToken` | string | No | Pagination token from a previous request |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `secrets` | json | List of secrets with name, ARN, description, and dates |
| `nextToken` | string | Pagination token for the next page of results |
| `count` | number | Number of secrets returned |
### `secrets_manager_create_secret`
Create a new secret in AWS Secrets Manager
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
| `accessKeyId` | string | Yes | AWS access key ID |
| `secretAccessKey` | string | Yes | AWS secret access key |
| `name` | string | Yes | Name of the secret to create |
| `secretValue` | string | Yes | The secret value \(plain text or JSON string\) |
| `description` | string | No | Description of the secret |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Operation status message |
| `name` | string | Name of the created secret |
| `arn` | string | ARN of the created secret |
| `versionId` | string | Version ID of the created secret |
### `secrets_manager_update_secret`
Update the value of an existing secret in AWS Secrets Manager
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
| `accessKeyId` | string | Yes | AWS access key ID |
| `secretAccessKey` | string | Yes | AWS secret access key |
| `secretId` | string | Yes | The name or ARN of the secret to update |
| `secretValue` | string | Yes | The new secret value \(plain text or JSON string\) |
| `description` | string | No | Updated description of the secret |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Operation status message |
| `name` | string | Name of the updated secret |
| `arn` | string | ARN of the updated secret |
| `versionId` | string | Version ID of the updated secret |
### `secrets_manager_delete_secret`
Delete a secret from AWS Secrets Manager
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
| `accessKeyId` | string | Yes | AWS access key ID |
| `secretAccessKey` | string | Yes | AWS secret access key |
| `secretId` | string | Yes | The name or ARN of the secret to delete |
| `recoveryWindowInDays` | number | No | Number of days before permanent deletion \(7-30, default 30\) |
| `forceDelete` | boolean | No | If true, immediately delete without recovery window |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Operation status message |
| `name` | string | Name of the deleted secret |
| `arn` | string | ARN of the deleted secret |
| `deletionDate` | string | Scheduled deletion date |

View File

@@ -0,0 +1,498 @@
---
title: Tailscale
description: Manage devices and network settings in your Tailscale tailnet
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="tailscale"
color="#2E2D2D"
/>
{/* MANUAL-CONTENT-START:intro */}
## Overview
[Tailscale](https://tailscale.com) is a zero-config mesh VPN built on WireGuard that makes it easy to connect devices, services, and users across any network. The Tailscale block lets you automate network management tasks like device provisioning, access control, route management, and DNS configuration directly from your Sim workflows.
## Authentication
The Tailscale block uses API key authentication. To get an API key:
1. Go to the [Tailscale admin console](https://login.tailscale.com/admin/settings/keys)
2. Navigate to **Settings > Keys**
3. Click **Generate API key**
4. Set an expiry (1-90 days) and copy the key (starts with `tskey-api-`)
You must have an **Owner**, **Admin**, **IT admin**, or **Network admin** role to generate API keys.
## Tailnet Identifier
Every operation requires a **tailnet** parameter. This is typically your organization's domain name (e.g., `example.com`). You can also use `"-"` to refer to your default tailnet.
## Common Use Cases
- **Device inventory**: List and monitor all devices connected to your network
- **Automated provisioning**: Create and manage auth keys to pre-authorize new devices
- **Access control**: Authorize or deauthorize devices, manage device tags for ACL policies
- **Route management**: View and enable subnet routes for devices acting as subnet routers
- **DNS management**: Configure nameservers, MagicDNS, and search paths
- **Key lifecycle**: Create, list, inspect, and revoke auth keys
- **User auditing**: List all users in the tailnet and their roles
- **Policy review**: Retrieve the current ACL policy for inspection or backup
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Interact with the Tailscale API to manage devices, DNS, ACLs, auth keys, users, and routes across your tailnet.
## Tools
### `tailscale_list_devices`
List all devices in the tailnet
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `devices` | array | List of devices in the tailnet |
| ↳ `id` | string | Device ID |
| ↳ `name` | string | Device name |
| ↳ `hostname` | string | Device hostname |
| ↳ `user` | string | Associated user |
| ↳ `os` | string | Operating system |
| ↳ `clientVersion` | string | Tailscale client version |
| ↳ `addresses` | array | Tailscale IP addresses |
| ↳ `tags` | array | Device tags |
| ↳ `authorized` | boolean | Whether the device is authorized |
| ↳ `blocksIncomingConnections` | boolean | Whether the device blocks incoming connections |
| ↳ `lastSeen` | string | Last seen timestamp |
| ↳ `created` | string | Creation timestamp |
| `count` | number | Total number of devices |
### `tailscale_get_device`
Get details of a specific device by ID
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
| `deviceId` | string | Yes | Device ID |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Device ID |
| `name` | string | Device name |
| `hostname` | string | Device hostname |
| `user` | string | Associated user |
| `os` | string | Operating system |
| `clientVersion` | string | Tailscale client version |
| `addresses` | array | Tailscale IP addresses |
| `tags` | array | Device tags |
| `authorized` | boolean | Whether the device is authorized |
| `blocksIncomingConnections` | boolean | Whether the device blocks incoming connections |
| `lastSeen` | string | Last seen timestamp |
| `created` | string | Creation timestamp |
| `isExternal` | boolean | Whether the device is external |
| `updateAvailable` | boolean | Whether an update is available |
| `machineKey` | string | Machine key |
| `nodeKey` | string | Node key |
### `tailscale_delete_device`
Remove a device from the tailnet
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
| `deviceId` | string | Yes | Device ID to delete |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the device was successfully deleted |
| `deviceId` | string | ID of the deleted device |
### `tailscale_authorize_device`
Authorize or deauthorize a device on the tailnet
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
| `deviceId` | string | Yes | Device ID to authorize |
| `authorized` | boolean | Yes | Whether to authorize \(true\) or deauthorize \(false\) the device |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the operation succeeded |
| `deviceId` | string | Device ID |
| `authorized` | boolean | Authorization status after the operation |
### `tailscale_set_device_tags`
Set tags on a device in the tailnet
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
| `deviceId` | string | Yes | Device ID |
| `tags` | string | Yes | Comma-separated list of tags \(e.g., "tag:server,tag:production"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the tags were successfully set |
| `deviceId` | string | Device ID |
| `tags` | array | Tags set on the device |
### `tailscale_get_device_routes`
Get the subnet routes for a device
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
| `deviceId` | string | Yes | Device ID |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `advertisedRoutes` | array | Subnet routes the device is advertising |
| `enabledRoutes` | array | Subnet routes that are approved/enabled |
### `tailscale_set_device_routes`
Set the enabled subnet routes for a device
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
| `deviceId` | string | Yes | Device ID |
| `routes` | string | Yes | Comma-separated list of subnet routes to enable \(e.g., "10.0.0.0/24,192.168.1.0/24"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `advertisedRoutes` | array | Subnet routes the device is advertising |
| `enabledRoutes` | array | Subnet routes that are now enabled |
### `tailscale_update_device_key`
Enable or disable key expiry on a device
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
| `deviceId` | string | Yes | Device ID |
| `keyExpiryDisabled` | boolean | Yes | Whether to disable key expiry \(true\) or enable it \(false\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the operation succeeded |
| `deviceId` | string | Device ID |
| `keyExpiryDisabled` | boolean | Whether key expiry is now disabled |
### `tailscale_list_dns_nameservers`
Get the DNS nameservers configured for the tailnet
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `dns` | array | List of DNS nameserver addresses |
| `magicDNS` | boolean | Whether MagicDNS is enabled |
### `tailscale_set_dns_nameservers`
Set the DNS nameservers for the tailnet
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
| `dns` | string | Yes | Comma-separated list of DNS nameserver IP addresses \(e.g., "8.8.8.8,8.8.4.4"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `dns` | array | Updated list of DNS nameserver addresses |
| `magicDNS` | boolean | Whether MagicDNS is enabled |
### `tailscale_get_dns_preferences`
Get the DNS preferences for the tailnet including MagicDNS status
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `magicDNS` | boolean | Whether MagicDNS is enabled |
### `tailscale_set_dns_preferences`
Set DNS preferences for the tailnet (enable/disable MagicDNS)
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
| `magicDNS` | boolean | Yes | Whether to enable \(true\) or disable \(false\) MagicDNS |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `magicDNS` | boolean | Updated MagicDNS status |
### `tailscale_get_dns_searchpaths`
Get the DNS search paths configured for the tailnet
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `searchPaths` | array | List of DNS search path domains |
### `tailscale_set_dns_searchpaths`
Set the DNS search paths for the tailnet
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
| `searchPaths` | string | Yes | Comma-separated list of DNS search path domains \(e.g., "corp.example.com,internal.example.com"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `searchPaths` | array | Updated list of DNS search path domains |
### `tailscale_list_users`
List all users in the tailnet
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `users` | array | List of users in the tailnet |
| ↳ `id` | string | User ID |
| ↳ `displayName` | string | Display name |
| ↳ `loginName` | string | Login name / email |
| ↳ `profilePicURL` | string | Profile picture URL |
| ↳ `role` | string | User role \(owner, admin, member, etc.\) |
| ↳ `status` | string | User status \(active, suspended, etc.\) |
| ↳ `type` | string | User type \(member, shared, tagged\) |
| ↳ `created` | string | Creation timestamp |
| ↳ `lastSeen` | string | Last seen timestamp |
| ↳ `deviceCount` | number | Number of devices owned by user |
| `count` | number | Total number of users |
### `tailscale_create_auth_key`
Create a new auth key for the tailnet to pre-authorize devices
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
| `reusable` | boolean | No | Whether the key can be used more than once |
| `ephemeral` | boolean | No | Whether devices authenticated with this key are ephemeral |
| `preauthorized` | boolean | No | Whether devices are pre-authorized \(skip manual approval\) |
| `tags` | string | No | Comma-separated list of tags for devices using this key \(e.g., "tag:server,tag:prod"\) |
| `description` | string | No | Description for the auth key |
| `expirySeconds` | number | No | Key expiry time in seconds \(default: 90 days\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Auth key ID |
| `key` | string | The auth key value \(only shown once at creation\) |
| `description` | string | Key description |
| `created` | string | Creation timestamp |
| `expires` | string | Expiration timestamp |
| `revoked` | string | Revocation timestamp \(empty if not revoked\) |
| `capabilities` | object | Key capabilities |
| ↳ `reusable` | boolean | Whether the key is reusable |
| ↳ `ephemeral` | boolean | Whether devices are ephemeral |
| ↳ `preauthorized` | boolean | Whether devices are pre-authorized |
| ↳ `tags` | array | Tags applied to devices using this key |
### `tailscale_list_auth_keys`
List all auth keys in the tailnet
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `keys` | array | List of auth keys |
| ↳ `id` | string | Auth key ID |
| ↳ `description` | string | Key description |
| ↳ `created` | string | Creation timestamp |
| ↳ `expires` | string | Expiration timestamp |
| ↳ `revoked` | string | Revocation timestamp |
| ↳ `capabilities` | object | Key capabilities |
| ↳ `reusable` | boolean | Whether the key is reusable |
| ↳ `ephemeral` | boolean | Whether devices are ephemeral |
| ↳ `preauthorized` | boolean | Whether devices are pre-authorized |
| ↳ `tags` | array | Tags applied to devices |
| `count` | number | Total number of auth keys |
### `tailscale_get_auth_key`
Get details of a specific auth key
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
| `keyId` | string | Yes | Auth key ID |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Auth key ID |
| `description` | string | Key description |
| `created` | string | Creation timestamp |
| `expires` | string | Expiration timestamp |
| `revoked` | string | Revocation timestamp |
| `capabilities` | object | Key capabilities |
| ↳ `reusable` | boolean | Whether the key is reusable |
| ↳ `ephemeral` | boolean | Whether devices are ephemeral |
| ↳ `preauthorized` | boolean | Whether devices are pre-authorized |
| ↳ `tags` | array | Tags applied to devices using this key |
### `tailscale_delete_auth_key`
Revoke and delete an auth key
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
| `keyId` | string | Yes | Auth key ID to delete |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the auth key was successfully deleted |
| `keyId` | string | ID of the deleted auth key |
### `tailscale_get_acl`
Get the current ACL policy for the tailnet
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `acl` | string | ACL policy as JSON string |
| `etag` | string | ETag for the current ACL version \(use with If-Match header for updates\) |

View File

@@ -131,7 +131,7 @@ Detecta información de identificación personal utilizando Microsoft Presidio.
**Casos de uso:**
- Bloquear contenido que contiene información personal sensible
- Enmascarar PII antes de registrar o almacenar datos
- Cumplimiento de GDPR, HIPAA y otras regulaciones de privacidad
- Cumplimiento de GDPR y otras regulaciones de privacidad
- Sanear entradas de usuario antes del procesamiento
## Configuración

View File

@@ -131,7 +131,7 @@ Détecte les informations personnelles identifiables à l'aide de Microsoft Pres
**Cas d'utilisation :**
- Bloquer le contenu contenant des informations personnelles sensibles
- Masquer les PII avant de journaliser ou stocker des données
- Conformité avec le RGPD, HIPAA et autres réglementations sur la confidentialité
- Conformité avec le RGPD et autres réglementations sur la confidentialité
- Assainir les entrées utilisateur avant traitement
## Configuration

View File

@@ -131,7 +131,7 @@ Microsoft Presidioを使用して個人を特定できる情報を検出しま
**ユースケース:**
- 機密性の高い個人情報を含むコンテンツをブロック
- データのログ記録や保存前にPIIをマスク
- GDPR、HIPAA、その他のプライバシー規制への準拠
- GDPR、その他のプライバシー規制への準拠
- 処理前のユーザー入力のサニタイズ
## 設定

View File

@@ -131,7 +131,7 @@ Guardrails 模块通过针对多种验证类型检查内容,验证并保护您
**使用场景:**
- 阻止包含敏感个人信息的内容
- 在记录或存储数据之前屏蔽 PII
- 符合 GDPR、HIPAA 和其他隐私法规
- 符合 GDPR 和其他隐私法规
- 在处理之前清理用户输入
## 配置

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

@@ -28,6 +28,15 @@ API_ENCRYPTION_KEY=your_api_encryption_key # Use `openssl rand -hex 32` to gener
# OLLAMA_URL=http://localhost:11434 # URL for local Ollama server - uncomment if using local models
# VLLM_BASE_URL=http://localhost:8000 # Base URL for your self-hosted vLLM (OpenAI-compatible)
# VLLM_API_KEY= # Optional bearer token if your vLLM instance requires auth
# FIREWORKS_API_KEY= # Optional Fireworks AI API key for model listing
# NEXT_PUBLIC_BEDROCK_DEFAULT_CREDENTIALS=true # Set when using AWS default credential chain (IAM roles, ECS task roles, IRSA). Hides credential fields in Agent block UI.
# AZURE_OPENAI_ENDPOINT= # Azure OpenAI endpoint (hides field in UI when set alongside NEXT_PUBLIC_AZURE_CONFIGURED)
# AZURE_OPENAI_API_KEY= # Azure OpenAI API key
# AZURE_OPENAI_API_VERSION= # Azure OpenAI API version
# AZURE_ANTHROPIC_ENDPOINT= # Azure Anthropic endpoint (AI Foundry)
# AZURE_ANTHROPIC_API_KEY= # Azure Anthropic API key
# AZURE_ANTHROPIC_API_VERSION= # Azure Anthropic API version (e.g., 2023-06-01)
# NEXT_PUBLIC_AZURE_CONFIGURED=true # Set when Azure credentials are pre-configured above. Hides endpoint/key/version fields in Agent block UI.
# Admin API (Optional - for self-hosted GitOps)
# ADMIN_API_KEY= # Use `openssl rand -hex 32` to generate. Enables admin API for workflow export/import.

View File

@@ -14,8 +14,8 @@ export default function AuthLayoutClient({ children }: { children: React.ReactNo
return (
<AuthBackground className='dark font-[430] font-season'>
<main className='relative flex min-h-full flex-col text-[#ECECEC]'>
<header className='shrink-0 bg-[#1C1C1C]'>
<main className='relative flex min-h-full flex-col text-[var(--landing-text)]'>
<header className='shrink-0 bg-[var(--landing-bg)]'>
<Navbar logoOnly />
</header>
<div className='relative z-30 flex flex-1 items-center justify-center px-4 pb-24'>

View File

@@ -9,7 +9,7 @@ type AuthBackgroundProps = {
export default function AuthBackground({ className, children }: AuthBackgroundProps) {
return (
<div className={cn('fixed inset-0 overflow-hidden', className)}>
<div className='-z-50 pointer-events-none absolute inset-0 bg-[#1C1C1C]' />
<div className='-z-50 pointer-events-none absolute inset-0 bg-[var(--landing-bg)]' />
<AuthBackgroundSVG />
<div className='relative z-20 h-full overflow-auto'>{children}</div>
</div>

View File

@@ -0,0 +1,6 @@
/** Shared className for primary auth/status CTA buttons on dark auth surfaces. */
export const AUTH_PRIMARY_CTA_BASE =
'inline-flex h-[32px] items-center justify-center gap-2 rounded-[5px] border border-[var(--auth-primary-btn-border)] bg-[var(--auth-primary-btn-bg)] px-2.5 font-[430] font-season text-[var(--auth-primary-btn-text)] text-sm transition-colors hover:border-[var(--auth-primary-btn-hover-border)] hover:bg-[var(--auth-primary-btn-hover-bg)] hover:text-[var(--auth-primary-btn-hover-text)] disabled:cursor-not-allowed disabled:opacity-50' as const
/** Full-width variant used for primary auth form submit buttons. */
export const AUTH_SUBMIT_BTN = `${AUTH_PRIMARY_CTA_BASE} w-full` as const

View File

@@ -1,102 +0,0 @@
'use client'
import { forwardRef, useState } from 'react'
import { ArrowRight, ChevronRight, Loader2 } from 'lucide-react'
import { cn } from '@/lib/core/utils/cn'
import { useBrandConfig } from '@/ee/whitelabeling'
export interface BrandedButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
loading?: boolean
loadingText?: string
showArrow?: boolean
fullWidth?: boolean
}
/**
* Branded button for auth and status pages.
* Default: white button matching the landing page "Get started" style.
* Whitelabel: uses the brand's primary color as background with white text.
*/
export const BrandedButton = forwardRef<HTMLButtonElement, BrandedButtonProps>(
(
{
children,
loading = false,
loadingText,
showArrow = true,
fullWidth = true,
className,
disabled,
onMouseEnter,
onMouseLeave,
...props
},
ref
) => {
const brand = useBrandConfig()
const hasCustomColor = brand.isWhitelabeled && Boolean(brand.theme?.primaryColor)
const [isHovered, setIsHovered] = useState(false)
const handleMouseEnter = (e: React.MouseEvent<HTMLButtonElement>) => {
setIsHovered(true)
onMouseEnter?.(e)
}
const handleMouseLeave = (e: React.MouseEvent<HTMLButtonElement>) => {
setIsHovered(false)
onMouseLeave?.(e)
}
return (
<button
ref={ref}
{...props}
disabled={disabled || loading}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
className={cn(
'group inline-flex h-[32px] items-center justify-center gap-[8px] rounded-[5px] border px-[10px] font-[430] font-season text-[14px] transition-colors disabled:cursor-not-allowed disabled:opacity-50',
!hasCustomColor &&
'border-[#FFFFFF] bg-[#FFFFFF] text-black hover:border-[#E0E0E0] hover:bg-[#E0E0E0]',
fullWidth && 'w-full',
className
)}
style={
hasCustomColor
? {
backgroundColor: isHovered
? (brand.theme?.primaryHoverColor ?? brand.theme?.primaryColor)
: brand.theme?.primaryColor,
borderColor: isHovered
? (brand.theme?.primaryHoverColor ?? brand.theme?.primaryColor)
: brand.theme?.primaryColor,
color: '#FFFFFF',
}
: undefined
}
>
{loading ? (
<span className='flex items-center gap-2'>
<Loader2 className='h-4 w-4 animate-spin' />
{loadingText ? `${loadingText}...` : children}
</span>
) : showArrow ? (
<span className='flex items-center gap-1'>
{children}
<span className='inline-flex transition-transform duration-200 group-hover:translate-x-0.5'>
{isHovered ? (
<ArrowRight className='h-4 w-4' aria-hidden='true' />
) : (
<ChevronRight className='h-4 w-4' aria-hidden='true' />
)}
</span>
</span>
) : (
children
)}
</button>
)
}
)
BrandedButton.displayName = 'BrandedButton'

View File

@@ -4,23 +4,18 @@ import { useRouter } from 'next/navigation'
import { Button } from '@/components/emcn'
import { getEnv, isTruthy } from '@/lib/core/config/env'
import { cn } from '@/lib/core/utils/cn'
import { AUTH_SUBMIT_BTN } from '@/app/(auth)/components/auth-button-classes'
interface SSOLoginButtonProps {
callbackURL?: string
className?: string
// Visual variant for button styling and placement contexts
// - 'primary' matches the main auth action button style
// - 'outline' matches social provider buttons
variant?: 'primary' | 'outline'
// Optional class used when variant is primary to match brand/gradient
primaryClassName?: string
}
export function SSOLoginButton({
callbackURL,
className,
variant = 'outline',
primaryClassName,
}: SSOLoginButtonProps) {
const router = useRouter()
@@ -33,11 +28,6 @@ export function SSOLoginButton({
router.push(ssoUrl)
}
const primaryBtnClasses = cn(
primaryClassName || 'branded-button-gradient',
'flex w-full items-center justify-center gap-2 rounded-[10px] border font-medium text-[15px] text-white transition-all duration-200'
)
const outlineBtnClasses = cn('w-full rounded-[10px]')
return (
@@ -45,7 +35,7 @@ export function SSOLoginButton({
type='button'
onClick={handleSSOClick}
variant={variant === 'outline' ? 'outline' : undefined}
className={cn(variant === 'outline' ? outlineBtnClasses : primaryBtnClasses, className)}
className={cn(variant === 'outline' ? outlineBtnClasses : AUTH_SUBMIT_BTN, className)}
>
Sign in with SSO
</Button>

View File

@@ -18,18 +18,18 @@ export function StatusPageLayout({
}: StatusPageLayoutProps) {
return (
<AuthBackground className='dark font-[430] font-season'>
<main className='relative flex min-h-full flex-col text-[#ECECEC]'>
<header className='shrink-0 bg-[#1C1C1C]'>
<main className='relative flex min-h-full flex-col text-[var(--landing-text)]'>
<header className='shrink-0 bg-[var(--landing-bg)]'>
<Navbar logoOnly />
</header>
<div className='relative z-30 flex flex-1 items-center justify-center px-4 pb-24'>
<div className='w-full max-w-lg px-4'>
<div className='flex flex-col items-center justify-center'>
<div className='space-y-1 text-center'>
<h1 className='font-[430] font-season text-[40px] text-white leading-[110%] tracking-[-0.02em]'>
<h1 className='text-balance font-[430] font-season text-[40px] text-white leading-[110%] tracking-[-0.02em]'>
{title}
</h1>
<p className='font-[430] font-season text-[#F6F6F6]/60 text-[18px] leading-[125%] tracking-[0.02em]'>
<p className='font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-subtle)_60%,transparent)] text-lg leading-[125%] tracking-[0.02em]'>
{description}
</p>
</div>

View File

@@ -11,12 +11,12 @@ export function SupportFooter({ position = 'fixed' }: SupportFooterProps) {
return (
<div
className={`right-0 bottom-0 left-0 z-50 pb-8 text-center font-[340] text-[#999] text-[13px] leading-relaxed ${position}`}
className={`right-0 bottom-0 left-0 z-50 pb-8 text-center font-[340] text-[var(--landing-text-muted)] text-small leading-relaxed ${position}`}
>
Need help?{' '}
<a
href={`mailto:${brandConfig.supportEmail}`}
className='text-[#999] underline-offset-4 transition hover:text-[#ECECEC] hover:underline'
className='text-[var(--landing-text-muted)] underline-offset-4 transition hover:text-[var(--landing-text)] hover:underline'
>
Contact support
</a>

View File

@@ -4,21 +4,21 @@ export default function LoginLoading() {
return (
<div className='flex flex-col items-center'>
<Skeleton className='h-[38px] w-[80px] rounded-[4px]' />
<div className='mt-[32px] w-full space-y-[8px]'>
<div className='mt-8 w-full space-y-2'>
<Skeleton className='h-[14px] w-[40px] rounded-[4px]' />
<Skeleton className='h-[44px] w-full rounded-[10px]' />
</div>
<div className='mt-[16px] w-full space-y-[8px]'>
<div className='mt-4 w-full space-y-2'>
<Skeleton className='h-[14px] w-[64px] rounded-[4px]' />
<Skeleton className='h-[44px] w-full rounded-[10px]' />
</div>
<Skeleton className='mt-[24px] h-[44px] w-full rounded-[10px]' />
<Skeleton className='mt-[24px] h-[1px] w-full rounded-[1px]' />
<div className='mt-[24px] flex w-full gap-[12px]'>
<Skeleton className='mt-6 h-[44px] w-full rounded-[10px]' />
<Skeleton className='mt-6 h-[1px] w-full rounded-[1px]' />
<div className='mt-6 flex w-full gap-3'>
<Skeleton className='h-[44px] flex-1 rounded-[10px]' />
<Skeleton className='h-[44px] flex-1 rounded-[10px]' />
</div>
<Skeleton className='mt-[24px] h-[14px] w-[200px] rounded-[4px]' />
<Skeleton className='mt-6 h-[14px] w-[200px] rounded-[4px]' />
</div>
)
}

View File

@@ -2,7 +2,7 @@
import { useRef, useState } from 'react'
import { createLogger } from '@sim/logger'
import { Eye, EyeOff } from 'lucide-react'
import { Eye, EyeOff, Loader2 } from 'lucide-react'
import Link from 'next/link'
import { useRouter, useSearchParams } from 'next/navigation'
import {
@@ -20,10 +20,9 @@ import { validateCallbackUrl } from '@/lib/core/security/input-validation'
import { cn } from '@/lib/core/utils/cn'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { quickValidateEmail } from '@/lib/messaging/email/validation'
import { BrandedButton } from '@/app/(auth)/components/branded-button'
import { AUTH_SUBMIT_BTN } from '@/app/(auth)/components/auth-button-classes'
import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons'
import { SSOLoginButton } from '@/app/(auth)/components/sso-login-button'
import { useBrandedButtonClass } from '@/hooks/use-branded-button-class'
const logger = createLogger('LoginForm')
@@ -87,8 +86,6 @@ export default function LoginPage({
const [passwordErrors, setPasswordErrors] = useState<string[]>([])
const [showValidationError, setShowValidationError] = useState(false)
const [formError, setFormError] = useState<string | null>(null)
const buttonClass = useBrandedButtonClass()
const callbackUrlParam = searchParams?.get('callbackUrl')
const isValidCallbackUrl = callbackUrlParam ? validateCallbackUrl(callbackUrlParam) : false
const invalidCallbackRef = useRef(false)
@@ -174,7 +171,7 @@ export default function LoginPage({
callbackURL: safeCallbackUrl,
},
{
onError: (ctx) => {
onError: (ctx: any) => {
logger.error('Login error:', ctx.error)
if (ctx.error.code?.includes('EMAIL_NOT_VERIFIED')) {
@@ -342,10 +339,10 @@ export default function LoginPage({
return (
<>
<div className='space-y-1 text-center'>
<h1 className='font-[430] font-season text-[40px] text-white leading-[110%] tracking-[-0.02em]'>
<h1 className='text-balance font-[430] font-season text-[40px] text-white leading-[110%] tracking-[-0.02em]'>
Sign in
</h1>
<p className='font-[430] font-season text-[#F6F6F6]/60 text-[18px] leading-[125%] tracking-[0.02em]'>
<p className='font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-subtle)_60%,transparent)] text-lg leading-[125%] tracking-[0.02em]'>
Enter your details
</p>
</div>
@@ -353,11 +350,7 @@ export default function LoginPage({
{/* SSO Login Button (primary top-only when it is the only method) */}
{showTopSSO && (
<div className='mt-8'>
<SSOLoginButton
callbackURL={callbackUrl}
variant='primary'
primaryClassName={buttonClass}
/>
<SSOLoginButton callbackURL={callbackUrl} variant='primary' />
</div>
)}
@@ -399,7 +392,7 @@ export default function LoginPage({
<button
type='button'
onClick={() => setForgotPasswordOpen(true)}
className='font-medium text-[#999] text-xs transition hover:text-[#ECECEC]'
className='font-medium text-[var(--landing-text-muted)] text-xs transition hover:text-[var(--landing-text)]'
>
Forgot password?
</button>
@@ -426,7 +419,7 @@ export default function LoginPage({
<button
type='button'
onClick={() => setShowPassword(!showPassword)}
className='-translate-y-1/2 absolute top-1/2 right-3 text-[#999] transition hover:text-[#ECECEC]'
className='-translate-y-1/2 absolute top-1/2 right-3 text-[var(--landing-text-muted)] transition hover:text-[var(--landing-text)]'
aria-label={showPassword ? 'Hide password' : 'Show password'}
>
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
@@ -454,14 +447,16 @@ export default function LoginPage({
</div>
)}
<BrandedButton
type='submit'
disabled={isLoading}
loading={isLoading}
loadingText='Signing in'
>
Sign in
</BrandedButton>
<button type='submit' disabled={isLoading} className={AUTH_SUBMIT_BTN}>
{isLoading ? (
<span className='flex items-center gap-2'>
<Loader2 className='h-4 w-4 animate-spin' />
Signing in...
</span>
) : (
'Sign in'
)}
</button>
</form>
)}
@@ -469,10 +464,12 @@ export default function LoginPage({
{showDivider && (
<div className='relative my-6 font-light'>
<div className='absolute inset-0 flex items-center'>
<div className='w-full border-[#2A2A2A] border-t' />
<div className='w-full border-[var(--landing-bg-elevated)] border-t' />
</div>
<div className='relative flex justify-center text-sm'>
<span className='bg-[#1C1C1C] px-4 font-[340] text-[#999]'>Or continue with</span>
<span className='bg-[var(--landing-bg)] px-4 font-[340] text-[var(--landing-text-muted)]'>
Or continue with
</span>
</div>
</div>
)}
@@ -486,11 +483,7 @@ export default function LoginPage({
callbackURL={callbackUrl}
>
{ssoEnabled && !hasOnlySSO && (
<SSOLoginButton
callbackURL={callbackUrl}
variant='outline'
primaryClassName={buttonClass}
/>
<SSOLoginButton callbackURL={callbackUrl} variant='outline' />
)}
</SocialLoginButtons>
</div>
@@ -502,20 +495,20 @@ export default function LoginPage({
<span className='font-normal'>Don't have an account? </span>
<Link
href={isInviteFlow ? `/signup?invite_flow=true&callbackUrl=${callbackUrl}` : '/signup'}
className='font-medium text-[#ECECEC] underline-offset-4 transition hover:text-white hover:underline'
className='font-medium text-[var(--landing-text)] underline-offset-4 transition hover:text-white hover:underline'
>
Sign up
</Link>
</div>
)}
<div className='absolute right-0 bottom-0 left-0 px-8 pb-8 text-center font-[340] text-[#999] text-[13px] leading-relaxed sm:px-8 md:px-[44px]'>
<div className='absolute right-0 bottom-0 left-0 px-8 pb-8 text-center font-[340] text-[13px] text-[var(--landing-text-muted)] leading-relaxed sm:px-8 md:px-11'>
By signing in, you agree to our{' '}
<Link
href='/terms'
target='_blank'
rel='noopener noreferrer'
className='text-[#999] underline-offset-4 transition hover:text-[#ECECEC] hover:underline'
className='text-[var(--landing-text-muted)] underline-offset-4 transition hover:text-[var(--landing-text)] hover:underline'
>
Terms of Service
</Link>{' '}
@@ -524,7 +517,7 @@ export default function LoginPage({
href='/privacy'
target='_blank'
rel='noopener noreferrer'
className='text-[#999] underline-offset-4 transition hover:text-[#ECECEC] hover:underline'
className='text-[var(--landing-text-muted)] underline-offset-4 transition hover:text-[var(--landing-text)] hover:underline'
>
Privacy Policy
</Link>
@@ -569,14 +562,16 @@ export default function LoginPage({
<p>{resetStatus.message}</p>
</div>
)}
<BrandedButton
type='submit'
disabled={isSubmittingReset}
loading={isSubmittingReset}
loadingText='Sending'
>
Send Reset Link
</BrandedButton>
<button type='submit' disabled={isSubmittingReset} className={AUTH_SUBMIT_BTN}>
{isSubmittingReset ? (
<span className='flex items-center gap-2'>
<Loader2 className='h-4 w-4 animate-spin' />
Sending...
</span>
) : (
'Send Reset Link'
)}
</button>
</div>
</form>
</ModalBody>

View File

@@ -3,16 +3,16 @@ import { Skeleton } from '@/components/emcn'
export default function OAuthConsentLoading() {
return (
<div className='flex flex-col items-center'>
<div className='flex items-center gap-[16px]'>
<div className='flex items-center gap-4'>
<Skeleton className='h-[48px] w-[48px] rounded-[12px]' />
<Skeleton className='h-[20px] w-[20px] rounded-[4px]' />
<Skeleton className='h-[48px] w-[48px] rounded-[12px]' />
</div>
<Skeleton className='mt-[24px] h-[38px] w-[220px] rounded-[4px]' />
<Skeleton className='mt-[8px] h-[14px] w-[280px] rounded-[4px]' />
<Skeleton className='mt-[24px] h-[56px] w-full rounded-[8px]' />
<Skeleton className='mt-[16px] h-[120px] w-full rounded-[8px]' />
<div className='mt-[24px] flex w-full max-w-[410px] gap-[12px]'>
<Skeleton className='mt-6 h-[38px] w-[220px] rounded-[4px]' />
<Skeleton className='mt-2 h-[14px] w-[280px] rounded-[4px]' />
<Skeleton className='mt-6 h-[56px] w-full rounded-[8px]' />
<Skeleton className='mt-4 h-[120px] w-full rounded-[8px]' />
<div className='mt-6 flex w-full max-w-[410px] gap-3'>
<Skeleton className='h-[44px] flex-1 rounded-[10px]' />
<Skeleton className='h-[44px] flex-1 rounded-[10px]' />
</div>

View File

@@ -1,12 +1,12 @@
'use client'
import { useCallback, useEffect, useState } from 'react'
import { ArrowLeftRight } from 'lucide-react'
import { ArrowLeftRight, Loader2 } from 'lucide-react'
import Image from 'next/image'
import { useRouter, useSearchParams } from 'next/navigation'
import { Button } from '@/components/emcn'
import { signOut, useSession } from '@/lib/auth/auth-client'
import { BrandedButton } from '@/app/(auth)/components/branded-button'
import { AUTH_SUBMIT_BTN } from '@/app/(auth)/components/auth-button-classes'
const SCOPE_DESCRIPTIONS: Record<string, string> = {
openid: 'Verify your identity',
@@ -127,10 +127,10 @@ export default function OAuthConsentPage() {
return (
<div className='flex flex-col items-center justify-center'>
<div className='space-y-1 text-center'>
<h1 className='font-[430] font-season text-[40px] text-white leading-[110%] tracking-[-0.02em]'>
<h1 className='text-balance font-[430] font-season text-[40px] text-white leading-[110%] tracking-[-0.02em]'>
Authorize Application
</h1>
<p className='font-[430] font-season text-[#F6F6F6]/60 text-[18px] leading-[125%] tracking-[0.02em]'>
<p className='font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-subtle)_60%,transparent)] text-lg leading-[125%] tracking-[0.02em]'>
Loading application details...
</p>
</div>
@@ -142,15 +142,17 @@ export default function OAuthConsentPage() {
return (
<div className='flex flex-col items-center justify-center'>
<div className='space-y-1 text-center'>
<h1 className='font-[430] font-season text-[40px] text-white leading-[110%] tracking-[-0.02em]'>
<h1 className='text-balance font-[430] font-season text-[40px] text-white leading-[110%] tracking-[-0.02em]'>
Authorization Error
</h1>
<p className='font-[430] font-season text-[#F6F6F6]/60 text-[18px] leading-[125%] tracking-[0.02em]'>
<p className='font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-subtle)_60%,transparent)] text-lg leading-[125%] tracking-[0.02em]'>
{error}
</p>
</div>
<div className='mt-8 w-full max-w-[410px] space-y-3'>
<BrandedButton onClick={() => router.push('/')}>Return to Home</BrandedButton>
<button onClick={() => router.push('/')} className={AUTH_SUBMIT_BTN}>
Return to Home
</button>
</div>
</div>
)
@@ -170,11 +172,11 @@ export default function OAuthConsentPage() {
className='rounded-[10px]'
/>
) : (
<div className='flex h-12 w-12 items-center justify-center rounded-[10px] bg-[#2A2A2A] font-medium text-[#999] text-[18px]'>
<div className='flex h-12 w-12 items-center justify-center rounded-[10px] bg-[var(--landing-bg-elevated)] font-medium text-[var(--landing-text-muted)] text-lg'>
{(clientName ?? '?').charAt(0).toUpperCase()}
</div>
)}
<ArrowLeftRight className='h-5 w-5 text-[#999]' />
<ArrowLeftRight className='h-5 w-5 text-[var(--landing-text-muted)]' />
<Image
src='/new/logo/colorized-bg.svg'
alt='Sim'
@@ -185,17 +187,17 @@ export default function OAuthConsentPage() {
</div>
<div className='space-y-1 text-center'>
<h1 className='font-[430] font-season text-[40px] text-white leading-[110%] tracking-[-0.02em]'>
<h1 className='text-balance font-[430] font-season text-[40px] text-white leading-[110%] tracking-[-0.02em]'>
Authorize Application
</h1>
<p className='font-[430] font-season text-[#F6F6F6]/60 text-[18px] leading-[125%] tracking-[0.02em]'>
<span className='font-medium text-[#ECECEC]'>{clientName}</span> is requesting access to
your account
<p className='font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-subtle)_60%,transparent)] text-lg leading-[125%] tracking-[0.02em]'>
<span className='font-medium text-[var(--landing-text)]'>{clientName}</span> is requesting
access to your account
</p>
</div>
{session?.user && (
<div className='mt-5 flex items-center gap-3 rounded-lg border border-[#2A2A2A] px-4 py-3'>
<div className='mt-5 flex items-center gap-3 rounded-lg border border-[var(--landing-bg-elevated)] px-4 py-3'>
{session.user.image ? (
<Image
src={session.user.image}
@@ -206,20 +208,22 @@ export default function OAuthConsentPage() {
unoptimized
/>
) : (
<div className='flex h-8 w-8 items-center justify-center rounded-full bg-[#2A2A2A] font-medium text-[#999] text-[13px]'>
<div className='flex h-8 w-8 items-center justify-center rounded-full bg-[var(--landing-bg-elevated)] font-medium text-[var(--landing-text-muted)] text-small'>
{(session.user.name ?? session.user.email ?? '?').charAt(0).toUpperCase()}
</div>
)}
<div className='min-w-0'>
{session.user.name && (
<p className='truncate font-medium text-[14px]'>{session.user.name}</p>
<p className='truncate font-medium text-sm'>{session.user.name}</p>
)}
<p className='truncate text-[#999] text-[13px]'>{session.user.email}</p>
<p className='truncate text-[var(--landing-text-muted)] text-small'>
{session.user.email}
</p>
</div>
<button
type='button'
onClick={handleSwitchAccount}
className='ml-auto text-[#999] text-[13px] underline-offset-2 transition-colors hover:text-[#ECECEC] hover:underline'
className='ml-auto text-[var(--landing-text-muted)] text-small underline-offset-2 transition-colors hover:text-[var(--landing-text)] hover:underline'
>
Switch
</button>
@@ -228,11 +232,14 @@ export default function OAuthConsentPage() {
{scopes.length > 0 && (
<div className='mt-5 w-full max-w-[410px]'>
<div className='rounded-lg border p-4'>
<p className='mb-3 font-medium text-[14px]'>This will allow the application to:</p>
<div className='rounded-lg border border-[var(--landing-bg-elevated)] p-4'>
<p className='mb-3 font-medium text-sm'>This will allow the application to:</p>
<ul className='space-y-2'>
{scopes.map((s) => (
<li key={s} className='flex items-start gap-2 font-normal text-[#999] text-[13px]'>
<li
key={s}
className='flex items-start gap-2 font-normal text-[var(--landing-text-muted)] text-small'
>
<span className='mt-0.5 text-green-500'>&#10003;</span>
<span>{SCOPE_DESCRIPTIONS[s] ?? s}</span>
</li>
@@ -252,15 +259,20 @@ export default function OAuthConsentPage() {
>
Deny
</Button>
<BrandedButton
fullWidth
showArrow={false}
loading={submitting}
loadingText='Authorizing'
<button
onClick={() => handleConsent(true)}
disabled={submitting}
className={AUTH_SUBMIT_BTN}
>
Allow
</BrandedButton>
{submitting ? (
<span className='flex items-center gap-2'>
<Loader2 className='h-4 w-4 animate-spin' />
Authorizing...
</span>
) : (
'Allow'
)}
</button>
</div>
</div>
)

View File

@@ -4,13 +4,13 @@ export default function ResetPasswordLoading() {
return (
<div className='flex flex-col items-center'>
<Skeleton className='h-[38px] w-[160px] rounded-[4px]' />
<Skeleton className='mt-[12px] h-[14px] w-[280px] rounded-[4px]' />
<div className='mt-[32px] w-full space-y-[8px]'>
<Skeleton className='mt-3 h-[14px] w-[280px] rounded-[4px]' />
<div className='mt-8 w-full space-y-2'>
<Skeleton className='h-[14px] w-[40px] rounded-[4px]' />
<Skeleton className='h-[44px] w-full rounded-[10px]' />
</div>
<Skeleton className='mt-[24px] h-[44px] w-full rounded-[10px]' />
<Skeleton className='mt-[24px] h-[14px] w-[120px] rounded-[4px]' />
<Skeleton className='mt-6 h-[44px] w-full rounded-[10px]' />
<Skeleton className='mt-6 h-[14px] w-[120px] rounded-[4px]' />
</div>
)
}

View File

@@ -69,10 +69,10 @@ function ResetPasswordContent() {
return (
<>
<div className='space-y-1 text-center'>
<h1 className='font-[430] font-season text-[40px] text-white leading-[110%] tracking-[-0.02em]'>
<h1 className='text-balance font-[430] font-season text-[40px] text-white leading-[110%] tracking-[-0.02em]'>
Reset your password
</h1>
<p className='font-[430] font-season text-[#F6F6F6]/60 text-[18px] leading-[125%] tracking-[0.02em]'>
<p className='font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-subtle)_60%,transparent)] text-lg leading-[125%] tracking-[0.02em]'>
Enter a new password for your account
</p>
</div>
@@ -87,10 +87,10 @@ function ResetPasswordContent() {
/>
</div>
<div className='pt-6 text-center font-light text-[14px]'>
<div className='pt-6 text-center font-light text-sm'>
<Link
href='/login'
className='font-medium text-[#ECECEC] underline-offset-4 transition hover:text-white hover:underline'
className='font-medium text-[var(--landing-text)] underline-offset-4 transition hover:text-white hover:underline'
>
Back to login
</Link>

View File

@@ -1,10 +1,10 @@
'use client'
import { useState } from 'react'
import { Eye, EyeOff } from 'lucide-react'
import { Eye, EyeOff, Loader2 } from 'lucide-react'
import { Input, Label } from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
import { BrandedButton } from '@/app/(auth)/components/branded-button'
import { AUTH_SUBMIT_BTN } from '@/app/(auth)/components/auth-button-classes'
interface RequestResetFormProps {
email: string
@@ -46,7 +46,7 @@ export function RequestResetForm({
disabled={isSubmitting}
required
/>
<p className='text-[#999] text-sm'>
<p className='text-[var(--landing-text-muted)] text-sm'>
We'll send a password reset link to this email address.
</p>
</div>
@@ -54,21 +54,26 @@ export function RequestResetForm({
{/* Status message display */}
{statusType && statusMessage && (
<div
className={cn('text-xs', statusType === 'success' ? 'text-[#4CAF50]' : 'text-red-400')}
className={cn(
'text-xs',
statusType === 'success' ? 'text-[var(--success)]' : 'text-red-400'
)}
>
<p>{statusMessage}</p>
</div>
)}
</div>
<BrandedButton
type='submit'
disabled={isSubmitting}
loading={isSubmitting}
loadingText='Sending'
>
Send Reset Link
</BrandedButton>
<button type='submit' disabled={isSubmitting} className={AUTH_SUBMIT_BTN}>
{isSubmitting ? (
<span className='flex items-center gap-2'>
<Loader2 className='h-4 w-4 animate-spin' />
Sending...
</span>
) : (
'Send Reset Link'
)}
</button>
</form>
)
}
@@ -162,7 +167,7 @@ export function SetNewPasswordForm({
<button
type='button'
onClick={() => setShowPassword(!showPassword)}
className='-translate-y-1/2 absolute top-1/2 right-3 text-[#999] transition hover:text-[#ECECEC]'
className='-translate-y-1/2 absolute top-1/2 right-3 text-[var(--landing-text-muted)] transition hover:text-[var(--landing-text)]'
aria-label={showPassword ? 'Hide password' : 'Show password'}
>
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
@@ -190,7 +195,7 @@ export function SetNewPasswordForm({
<button
type='button'
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
className='-translate-y-1/2 absolute top-1/2 right-3 text-[#999] transition hover:text-[#ECECEC]'
className='-translate-y-1/2 absolute top-1/2 right-3 text-[var(--landing-text-muted)] transition hover:text-[var(--landing-text)]'
aria-label={showConfirmPassword ? 'Hide password' : 'Show password'}
>
{showConfirmPassword ? <EyeOff size={18} /> : <Eye size={18} />}
@@ -208,7 +213,7 @@ export function SetNewPasswordForm({
<div
className={cn(
'mt-1 space-y-1 text-xs',
statusType === 'success' ? 'text-[#4CAF50]' : 'text-red-400'
statusType === 'success' ? 'text-[var(--success)]' : 'text-red-400'
)}
>
<p>{statusMessage}</p>
@@ -216,14 +221,16 @@ export function SetNewPasswordForm({
)}
</div>
<BrandedButton
type='submit'
disabled={isSubmitting || !token}
loading={isSubmitting}
loadingText='Resetting'
>
Reset Password
</BrandedButton>
<button type='submit' disabled={isSubmitting || !token} className={AUTH_SUBMIT_BTN}>
{isSubmitting ? (
<span className='flex items-center gap-2'>
<Loader2 className='h-4 w-4 animate-spin' />
Resetting...
</span>
) : (
'Reset Password'
)}
</button>
</form>
)
}

View File

@@ -4,25 +4,25 @@ export default function SignupLoading() {
return (
<div className='flex flex-col items-center'>
<Skeleton className='h-[38px] w-[100px] rounded-[4px]' />
<div className='mt-[32px] w-full space-y-[8px]'>
<div className='mt-8 w-full space-y-2'>
<Skeleton className='h-[14px] w-[40px] rounded-[4px]' />
<Skeleton className='h-[44px] w-full rounded-[10px]' />
</div>
<div className='mt-[16px] w-full space-y-[8px]'>
<div className='mt-4 w-full space-y-2'>
<Skeleton className='h-[14px] w-[40px] rounded-[4px]' />
<Skeleton className='h-[44px] w-full rounded-[10px]' />
</div>
<div className='mt-[16px] w-full space-y-[8px]'>
<div className='mt-4 w-full space-y-2'>
<Skeleton className='h-[14px] w-[64px] rounded-[4px]' />
<Skeleton className='h-[44px] w-full rounded-[10px]' />
</div>
<Skeleton className='mt-[24px] h-[44px] w-full rounded-[10px]' />
<Skeleton className='mt-[24px] h-[1px] w-full rounded-[1px]' />
<div className='mt-[24px] flex w-full gap-[12px]'>
<Skeleton className='mt-6 h-[44px] w-full rounded-[10px]' />
<Skeleton className='mt-6 h-[1px] w-full rounded-[1px]' />
<div className='mt-6 flex w-full gap-3'>
<Skeleton className='h-[44px] flex-1 rounded-[10px]' />
<Skeleton className='h-[44px] flex-1 rounded-[10px]' />
</div>
<Skeleton className='mt-[24px] h-[14px] w-[220px] rounded-[4px]' />
<Skeleton className='mt-6 h-[14px] w-[220px] rounded-[4px]' />
</div>
)
}

View File

@@ -1,20 +1,21 @@
'use client'
import { Suspense, useMemo, useRef, useState } from 'react'
import { Suspense, useEffect, useMemo, useRef, useState } from 'react'
import { Turnstile, type TurnstileInstance } from '@marsidev/react-turnstile'
import { createLogger } from '@sim/logger'
import { Eye, EyeOff } from 'lucide-react'
import { Eye, EyeOff, Loader2 } from 'lucide-react'
import Link from 'next/link'
import { useRouter, useSearchParams } from 'next/navigation'
import { usePostHog } from 'posthog-js/react'
import { Input, Label } from '@/components/emcn'
import { client, useSession } from '@/lib/auth/auth-client'
import { getEnv, isFalsy, isTruthy } from '@/lib/core/config/env'
import { cn } from '@/lib/core/utils/cn'
import { quickValidateEmail } from '@/lib/messaging/email/validation'
import { BrandedButton } from '@/app/(auth)/components/branded-button'
import { captureEvent } from '@/lib/posthog/client'
import { AUTH_SUBMIT_BTN } from '@/app/(auth)/components/auth-button-classes'
import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons'
import { SSOLoginButton } from '@/app/(auth)/components/sso-login-button'
import { useBrandedButtonClass } from '@/hooks/use-branded-button-class'
const logger = createLogger('SignupForm')
@@ -82,7 +83,12 @@ function SignupFormContent({
const router = useRouter()
const searchParams = useSearchParams()
const { refetch: refetchSession } = useSession()
const posthog = usePostHog()
const [isLoading, setIsLoading] = useState(false)
useEffect(() => {
captureEvent(posthog, 'signup_page_viewed', {})
}, [posthog])
const [showPassword, setShowPassword] = useState(false)
const [password, setPassword] = useState('')
const [passwordErrors, setPasswordErrors] = useState<string[]>([])
@@ -96,8 +102,6 @@ function SignupFormContent({
const captchaResolveRef = useRef<((token: string) => void) | null>(null)
const captchaRejectRef = useRef<((reason: Error) => void) | null>(null)
const turnstileSiteKey = useMemo(() => getEnv('NEXT_PUBLIC_TURNSTILE_SITE_KEY'), [])
const buttonClass = useBrandedButtonClass()
const redirectUrl = useMemo(
() => searchParams.get('redirect') || searchParams.get('callbackUrl') || '',
[searchParams]
@@ -363,10 +367,10 @@ function SignupFormContent({
return (
<>
<div className='space-y-1 text-center'>
<h1 className='font-[430] font-season text-[40px] text-white leading-[110%] tracking-[-0.02em]'>
<h1 className='text-balance font-[430] font-season text-[40px] text-white leading-[110%] tracking-[-0.02em]'>
Create an account
</h1>
<p className='font-[430] font-season text-[#F6F6F6]/60 text-[18px] leading-[125%] tracking-[0.02em]'>
<p className='font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-subtle)_60%,transparent)] text-lg leading-[125%] tracking-[0.02em]'>
Create an account or log in
</p>
</div>
@@ -380,115 +384,150 @@ function SignupFormContent({
return hasOnlySSO
})() && (
<div className='mt-8'>
<SSOLoginButton
callbackURL={redirectUrl || '/workspace'}
variant='primary'
primaryClassName={buttonClass}
/>
<SSOLoginButton callbackURL={redirectUrl || '/workspace'} variant='primary' />
</div>
)}
{/* Email/Password Form - show unless explicitly disabled */}
{!isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED')) && (
<form onSubmit={onSubmit} className='mt-8 space-y-8'>
<form onSubmit={onSubmit} className='mt-8 space-y-10'>
<div className='space-y-6'>
<div className='space-y-2'>
<div className='flex items-center justify-between'>
<Label htmlFor='name'>Full name</Label>
</div>
<Input
id='name'
name='name'
placeholder='Enter your name'
type='text'
autoCapitalize='words'
autoComplete='name'
title='Name can only contain letters, spaces, hyphens, and apostrophes'
value={name}
onChange={handleNameChange}
className={cn(
showNameValidationError &&
nameErrors.length > 0 &&
'border-red-500 focus:border-red-500'
)}
/>
{showNameValidationError && nameErrors.length > 0 && (
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{nameErrors.map((error, index) => (
<p key={index}>{error}</p>
))}
<div className='relative'>
<Input
id='name'
name='name'
placeholder='Enter your name'
type='text'
autoCapitalize='words'
autoComplete='name'
title='Name can only contain letters, spaces, hyphens, and apostrophes'
value={name}
onChange={handleNameChange}
className={cn(
showNameValidationError &&
nameErrors.length > 0 &&
'border-red-500 focus:border-red-500'
)}
/>
<div
className={cn(
'absolute right-0 left-0 z-10 grid transition-[grid-template-rows] duration-200 ease-out',
showNameValidationError && nameErrors.length > 0
? 'grid-rows-[1fr]'
: 'grid-rows-[0fr]'
)}
aria-live={showNameValidationError && nameErrors.length > 0 ? 'polite' : 'off'}
>
<div className='overflow-hidden'>
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{nameErrors.map((error, index) => (
<p key={index}>{error}</p>
))}
</div>
</div>
</div>
)}
</div>
</div>
<div className='space-y-2'>
<div className='flex items-center justify-between'>
<Label htmlFor='email'>Email</Label>
</div>
<Input
id='email'
name='email'
placeholder='Enter your email'
autoCapitalize='none'
autoComplete='email'
autoCorrect='off'
value={email}
onChange={handleEmailChange}
className={cn(
(emailError || (showEmailValidationError && emailErrors.length > 0)) &&
'border-red-500 focus:border-red-500'
)}
/>
{showEmailValidationError && emailErrors.length > 0 && (
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{emailErrors.map((error, index) => (
<p key={index}>{error}</p>
))}
<div className='relative'>
<Input
id='email'
name='email'
placeholder='Enter your email'
autoCapitalize='none'
autoComplete='email'
autoCorrect='off'
value={email}
onChange={handleEmailChange}
className={cn(
(emailError || (showEmailValidationError && emailErrors.length > 0)) &&
'border-red-500 focus:border-red-500'
)}
/>
<div
className={cn(
'absolute right-0 left-0 z-10 grid transition-[grid-template-rows] duration-200 ease-out',
(showEmailValidationError && emailErrors.length > 0) ||
(emailError && !showEmailValidationError)
? 'grid-rows-[1fr]'
: 'grid-rows-[0fr]'
)}
aria-live={
(showEmailValidationError && emailErrors.length > 0) ||
(emailError && !showEmailValidationError)
? 'polite'
: 'off'
}
>
<div className='overflow-hidden'>
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{showEmailValidationError && emailErrors.length > 0 ? (
emailErrors.map((error, index) => <p key={index}>{error}</p>)
) : emailError && !showEmailValidationError ? (
<p>{emailError}</p>
) : null}
</div>
</div>
</div>
)}
{emailError && !showEmailValidationError && (
<div className='mt-1 text-red-400 text-xs'>
<p>{emailError}</p>
</div>
)}
</div>
</div>
<div className='space-y-2'>
<div className='flex items-center justify-between'>
<Label htmlFor='password'>Password</Label>
</div>
<div className='relative'>
<Input
id='password'
name='password'
type={showPassword ? 'text' : 'password'}
autoCapitalize='none'
autoComplete='new-password'
placeholder='Enter your password'
autoCorrect='off'
value={password}
onChange={handlePasswordChange}
className={cn(
'pr-10',
showValidationError &&
passwordErrors.length > 0 &&
'border-red-500 focus:border-red-500'
)}
/>
<button
type='button'
onClick={() => setShowPassword(!showPassword)}
className='-translate-y-1/2 absolute top-1/2 right-3 text-[#999] transition hover:text-[#ECECEC]'
aria-label={showPassword ? 'Hide password' : 'Show password'}
>
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
</div>
{showValidationError && passwordErrors.length > 0 && (
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{passwordErrors.map((error, index) => (
<p key={index}>{error}</p>
))}
<div className='relative'>
<Input
id='password'
name='password'
type={showPassword ? 'text' : 'password'}
autoCapitalize='none'
autoComplete='new-password'
placeholder='Enter your password'
autoCorrect='off'
value={password}
onChange={handlePasswordChange}
className={cn(
'pr-10',
showValidationError &&
passwordErrors.length > 0 &&
'border-red-500 focus:border-red-500'
)}
/>
<button
type='button'
onClick={() => setShowPassword(!showPassword)}
className='-translate-y-1/2 absolute top-1/2 right-3 text-[var(--landing-text-muted)] transition hover:text-[var(--landing-text)]'
aria-label={showPassword ? 'Hide password' : 'Show password'}
>
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
</div>
)}
<div
className={cn(
'absolute right-0 left-0 z-10 grid transition-[grid-template-rows] duration-200 ease-out',
showValidationError && passwordErrors.length > 0
? 'grid-rows-[1fr]'
: 'grid-rows-[0fr]'
)}
aria-live={showValidationError && passwordErrors.length > 0 ? 'polite' : 'off'}
>
<div className='overflow-hidden'>
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{passwordErrors.map((error, index) => (
<p key={index}>{error}</p>
))}
</div>
</div>
</div>
</div>
</div>
</div>
@@ -509,14 +548,16 @@ function SignupFormContent({
</div>
)}
<BrandedButton
type='submit'
disabled={isLoading}
loading={isLoading}
loadingText='Creating account'
>
Create account
</BrandedButton>
<button type='submit' disabled={isLoading} className={cn('!mt-6', AUTH_SUBMIT_BTN)}>
{isLoading ? (
<span className='flex items-center gap-2'>
<Loader2 className='h-4 w-4 animate-spin' />
Creating account...
</span>
) : (
'Create account'
)}
</button>
</form>
)}
@@ -532,10 +573,12 @@ function SignupFormContent({
})() && (
<div className='relative my-6 font-light'>
<div className='absolute inset-0 flex items-center'>
<div className='w-full border-[#2A2A2A] border-t' />
<div className='w-full border-[var(--landing-bg-elevated)] border-t' />
</div>
<div className='relative flex justify-center text-sm'>
<span className='bg-[#1C1C1C] px-4 font-[340] text-[#999]'>Or continue with</span>
<span className='bg-[var(--landing-bg)] px-4 font-[340] text-[var(--landing-text-muted)]'>
Or continue with
</span>
</div>
</div>
)}
@@ -560,33 +603,29 @@ function SignupFormContent({
isProduction={isProduction}
>
{isTruthy(getEnv('NEXT_PUBLIC_SSO_ENABLED')) && (
<SSOLoginButton
callbackURL={redirectUrl || '/workspace'}
variant='outline'
primaryClassName={buttonClass}
/>
<SSOLoginButton callbackURL={redirectUrl || '/workspace'} variant='outline' />
)}
</SocialLoginButtons>
</div>
)}
<div className='pt-6 text-center font-light text-[14px]'>
<div className='pt-6 text-center font-light text-sm'>
<span className='font-normal'>Already have an account? </span>
<Link
href={isInviteFlow ? `/login?invite_flow=true&callbackUrl=${redirectUrl}` : '/login'}
className='font-medium text-[#ECECEC] underline-offset-4 transition hover:text-white hover:underline'
className='font-medium text-[var(--landing-text)] underline-offset-4 transition hover:text-white hover:underline'
>
Sign in
</Link>
</div>
<div className='absolute right-0 bottom-0 left-0 px-8 pb-8 text-center font-[340] text-[#999] text-[13px] leading-relaxed sm:px-8 md:px-[44px]'>
<div className='absolute right-0 bottom-0 left-0 px-8 pb-8 text-center font-[340] text-[var(--landing-text-muted)] text-small leading-relaxed sm:px-8 md:px-11'>
By creating an account, you agree to our{' '}
<Link
href='/terms'
target='_blank'
rel='noopener noreferrer'
className='text-[#999] underline-offset-4 transition hover:text-[#ECECEC] hover:underline'
className='text-[var(--landing-text-muted)] underline-offset-4 transition hover:text-[var(--landing-text)] hover:underline'
>
Terms of Service
</Link>{' '}
@@ -595,7 +634,7 @@ function SignupFormContent({
href='/privacy'
target='_blank'
rel='noopener noreferrer'
className='text-[#999] underline-offset-4 transition hover:text-[#ECECEC] hover:underline'
className='text-[var(--landing-text-muted)] underline-offset-4 transition hover:text-[var(--landing-text)] hover:underline'
>
Privacy Policy
</Link>

View File

@@ -4,13 +4,13 @@ export default function SSOLoading() {
return (
<div className='flex flex-col items-center'>
<Skeleton className='h-[38px] w-[120px] rounded-[4px]' />
<Skeleton className='mt-[12px] h-[14px] w-[260px] rounded-[4px]' />
<div className='mt-[32px] w-full space-y-[8px]'>
<Skeleton className='mt-3 h-[14px] w-[260px] rounded-[4px]' />
<div className='mt-8 w-full space-y-2'>
<Skeleton className='h-[14px] w-[80px] rounded-[4px]' />
<Skeleton className='h-[44px] w-full rounded-[10px]' />
</div>
<Skeleton className='mt-[24px] h-[44px] w-full rounded-[10px]' />
<Skeleton className='mt-[24px] h-[14px] w-[120px] rounded-[4px]' />
<Skeleton className='mt-6 h-[44px] w-full rounded-[10px]' />
<Skeleton className='mt-6 h-[14px] w-[120px] rounded-[4px]' />
</div>
)
}

View File

@@ -4,9 +4,9 @@ export default function VerifyLoading() {
return (
<div className='flex flex-col items-center'>
<Skeleton className='h-[38px] w-[180px] rounded-[4px]' />
<Skeleton className='mt-[12px] h-[14px] w-[300px] rounded-[4px]' />
<Skeleton className='mt-[4px] h-[14px] w-[240px] rounded-[4px]' />
<Skeleton className='mt-[32px] h-[44px] w-full rounded-[10px]' />
<Skeleton className='mt-3 h-[14px] w-[300px] rounded-[4px]' />
<Skeleton className='mt-1 h-[14px] w-[240px] rounded-[4px]' />
<Skeleton className='mt-8 h-[44px] w-full rounded-[10px]' />
</div>
)
}

View File

@@ -1,10 +1,11 @@
'use client'
import { Suspense, useEffect, useState } from 'react'
import { Loader2 } from 'lucide-react'
import { useRouter } from 'next/navigation'
import { InputOTP, InputOTPGroup, InputOTPSlot } from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
import { BrandedButton } from '@/app/(auth)/components/branded-button'
import { AUTH_SUBMIT_BTN } from '@/app/(auth)/components/auth-button-classes'
import { useVerification } from '@/app/(auth)/verify/use-verification'
interface VerifyContentProps {
@@ -59,10 +60,10 @@ function VerificationForm({
return (
<>
<div className='space-y-1 text-center'>
<h1 className='font-[430] font-season text-[40px] text-white leading-[110%] tracking-[-0.02em]'>
<h1 className='text-balance font-[430] font-season text-[40px] text-white leading-[110%] tracking-[-0.02em]'>
{isVerified ? 'Email Verified!' : 'Verify Your Email'}
</h1>
<p className='font-[430] font-season text-[#F6F6F6]/60 text-[18px] leading-[125%] tracking-[0.02em]'>
<p className='font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-subtle)_60%,transparent)] text-lg leading-[125%] tracking-[0.02em]'>
{isVerified
? 'Your email has been verified. Redirecting to dashboard...'
: !isEmailVerificationEnabled
@@ -78,7 +79,7 @@ function VerificationForm({
{!isVerified && isEmailVerificationEnabled && (
<div className='mt-8 space-y-8'>
<div className='space-y-6'>
<p className='text-center text-[#999] text-sm'>
<p className='text-center text-[var(--landing-text-muted)] text-sm'>
Enter the 6-digit code to verify your account.
{hasEmailService ? " If you don't see it in your inbox, check your spam folder." : ''}
</p>
@@ -110,27 +111,33 @@ function VerificationForm({
)}
</div>
<BrandedButton
<button
onClick={verifyCode}
disabled={!isOtpComplete || isLoading}
loading={isLoading}
loadingText='Verifying'
showArrow={false}
className={AUTH_SUBMIT_BTN}
>
Verify Email
</BrandedButton>
{isLoading ? (
<span className='flex items-center gap-2'>
<Loader2 className='h-4 w-4 animate-spin' />
Verifying...
</span>
) : (
'Verify Email'
)}
</button>
{hasEmailService && (
<div className='text-center'>
<p className='text-[#999] text-sm'>
<p className='text-[var(--landing-text-muted)] text-sm'>
Didn't receive a code?{' '}
{countdown > 0 ? (
<span>
Resend in <span className='font-medium text-[#ECECEC]'>{countdown}s</span>
Resend in{' '}
<span className='font-medium text-[var(--landing-text)]'>{countdown}s</span>
</span>
) : (
<button
className='font-medium text-[#ECECEC] underline-offset-4 transition hover:text-white hover:underline'
className='font-medium text-[var(--landing-text)] underline-offset-4 transition hover:text-white hover:underline'
onClick={handleResend}
disabled={isLoading || isResendDisabled}
>
@@ -141,7 +148,7 @@ function VerificationForm({
</div>
)}
<div className='text-center font-light text-[14px]'>
<div className='text-center font-light text-sm'>
<button
onClick={() => {
if (typeof window !== 'undefined') {
@@ -151,7 +158,7 @@ function VerificationForm({
}
router.push('/signup')
}}
className='font-medium text-[#ECECEC] underline-offset-4 transition hover:text-white hover:underline'
className='font-medium text-[var(--landing-text)] underline-offset-4 transition hover:text-white hover:underline'
>
Back to signup
</button>
@@ -166,8 +173,8 @@ function VerificationFormFallback() {
return (
<div className='text-center'>
<div className='animate-pulse'>
<div className='mx-auto mb-4 h-8 w-48 rounded bg-[#2A2A2A]' />
<div className='mx-auto h-4 w-64 rounded bg-[#2A2A2A]' />
<div className='mx-auto mb-4 h-8 w-48 rounded bg-[var(--surface-4)]' />
<div className='mx-auto h-4 w-64 rounded bg-[var(--surface-4)]' />
</div>
</div>
)

View File

@@ -25,7 +25,7 @@ function DotGrid({ className, cols, rows, gap = 0 }: DotGridProps) {
}}
>
{Array.from({ length: cols * rows }, (_, i) => (
<div key={i} className='h-[1.5px] w-[1.5px] rounded-full bg-[#2A2A2A]' />
<div key={i} className='h-[1.5px] w-[1.5px] rounded-full bg-[var(--landing-bg-elevated)]' />
))}
</div>
)
@@ -89,7 +89,7 @@ function VikhyathCursor() {
<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]'>
<div className='-left-[4px] absolute top-4.5 flex items-center rounded bg-[#2ABBF8] px-[5px] py-[3px] font-[420] font-season text-[var(--landing-text-dark)] text-sm leading-[100%] tracking-[-0.02em]'>
Vikhyath
</div>
</div>
@@ -113,7 +113,7 @@ function AlexaCursor() {
<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]'>
<div className='absolute top-4 left-[23px] flex items-center rounded bg-[#FFCC02] px-[5px] py-[3px] font-[420] font-season text-[var(--landing-text-dark)] text-sm leading-[100%] tracking-[-0.02em]'>
Alexa
</div>
</div>
@@ -143,7 +143,7 @@ function YouCursor({ x, y, visible }: YouCursorProps) {
<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]'>
<div className='absolute top-4 left-[23px] flex items-center rounded bg-[var(--brand-accent)] px-[5px] py-[3px] font-[420] font-season text-[var(--landing-text-dark)] text-sm leading-[100%] tracking-[-0.02em]'>
You
</div>
</div>
@@ -212,7 +212,7 @@ export default function Collaboration() {
ref={sectionRef}
id='collaboration'
aria-labelledby='collaboration-heading'
className='bg-[#1C1C1C]'
className='bg-[var(--landing-bg)]'
style={{ cursor: isHovering ? 'none' : 'auto' }}
onMouseMove={handleMouseMove}
onMouseEnter={handleMouseEnter}
@@ -222,7 +222,7 @@ export default function Collaboration() {
<style dangerouslySetInnerHTML={{ __html: CURSOR_KEYFRAMES }} />
<DotGrid
className='overflow-hidden border-[#2A2A2A] border-y bg-[#1C1C1C] p-[6px]'
className='overflow-hidden border-[var(--landing-bg-elevated)] border-y bg-[var(--landing-bg)] p-1.5'
cols={120}
rows={1}
gap={6}
@@ -230,33 +230,33 @@ export default function Collaboration() {
<div className='relative overflow-hidden'>
<div className='grid grid-cols-1 md:grid-cols-[auto_1fr]'>
<div className='flex flex-col items-start gap-3 px-4 pt-[60px] pb-8 sm:gap-4 sm:px-8 md:gap-[20px] md:px-[80px] md:pt-[100px]'>
<div className='flex flex-col items-start gap-3 px-4 pt-[60px] pb-8 sm:gap-4 sm:px-8 md:gap-5 md:px-20 md:pt-[100px]'>
<Badge
variant='blue'
size='md'
dot
className='bg-[#33C482]/10 font-season text-[#33C482] uppercase tracking-[0.02em]'
className='bg-[color-mix(in_srgb,var(--brand-accent)_10%,transparent)] font-season text-[var(--brand-accent)] 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]'
className='text-balance 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-[15px] leading-[150%] tracking-[0.02em] md:text-[18px]'>
<p className='font-[430] font-season text-[#F6F6F0]/50 text-base leading-[150%] tracking-[0.02em] md:text-lg'>
Grab your team. Build agents together <br className='hidden md:block' />
in real-time inside your workspace.
</p>
<Link
href='/signup'
className='group/cta mt-[12px] inline-flex h-[32px] cursor-none items-center gap-[6px] rounded-[5px] border border-[#FFFFFF] bg-[#FFFFFF] px-[10px] font-[430] font-season text-[14px] text-black transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
className='group/cta mt-3 inline-flex h-[32px] cursor-none items-center gap-1.5 rounded-[5px] border border-white bg-white px-2.5 font-[430] font-season text-black text-sm transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
>
Build together
<span className='relative h-[10px] w-[10px] shrink-0'>
@@ -288,7 +288,6 @@ export default function Collaboration() {
width={876}
height={480}
className='h-full w-auto object-left md:min-w-[100vw]'
priority
/>
</div>
<div className='hidden lg:block'>
@@ -306,16 +305,16 @@ export default function Collaboration() {
href='/blog/multiplayer'
target='_blank'
rel='noopener noreferrer'
className='relative mx-4 mb-6 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:mx-8 md:absolute md:bottom-10 md:left-[80px] md:z-20 md:mx-0 md:mb-0'
className='relative mx-4 mb-6 flex cursor-none items-center gap-3.5 rounded-[5px] border border-[var(--landing-bg-elevated)] bg-[var(--landing-bg)] px-3 py-2.5 transition-colors hover:border-[var(--landing-border-strong)] hover:bg-[var(--landing-bg-card)] sm:mx-8 md:absolute md:bottom-10 md:left-20 md:z-20 md:mx-0 md:mb-0'
>
<div className='relative h-7 w-11 shrink-0'>
<Image src='/landing/multiplayer-cursors.svg' alt='' fill className='object-contain' />
</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]'>
<div className='flex flex-col gap-0.5'>
<span className='font-[430] font-season text-[#F6F6F0]/50 text-caption uppercase leading-[100%] tracking-[0.08em]'>
Blog
</span>
<span className='font-[430] font-season text-[#F6F6F0] text-[14px] leading-[125%] tracking-[0.02em]'>
<span className='font-[430] font-season text-[#F6F6F0] text-sm leading-[125%] tracking-[0.02em]'>
How we built realtime collaboration
</span>
</div>
@@ -323,7 +322,7 @@ export default function Collaboration() {
</div>
<DotGrid
className='overflow-hidden border-[#2A2A2A] border-y bg-[#1C1C1C] p-[6px]'
className='overflow-hidden border-[var(--landing-bg-elevated)] border-y bg-[var(--landing-bg)] p-1.5'
cols={120}
rows={1}
gap={6}

View File

@@ -1,7 +1,10 @@
import freeEmailDomains from 'free-email-domains'
import { z } from 'zod'
import { NO_EMAIL_HEADER_CONTROL_CHARS_REGEX } from '@/lib/messaging/email/utils'
import { quickValidateEmail } from '@/lib/messaging/email/validation'
const FREE_EMAIL_DOMAINS = new Set(freeEmailDomains)
export const DEMO_REQUEST_REGION_VALUES = [
'north_america',
'europe',
@@ -11,13 +14,14 @@ export const DEMO_REQUEST_REGION_VALUES = [
'other',
] as const
export const DEMO_REQUEST_USER_COUNT_VALUES = [
export const DEMO_REQUEST_COMPANY_SIZE_VALUES = [
'1_10',
'11_50',
'51_200',
'201_500',
'501_1000',
'1000_plus',
'1001_10000',
'10000_plus',
] as const
export const DEMO_REQUEST_REGION_OPTIONS = [
@@ -29,13 +33,14 @@ export const DEMO_REQUEST_REGION_OPTIONS = [
{ value: 'other', label: 'Other' },
] as const
export const DEMO_REQUEST_USER_COUNT_OPTIONS = [
{ value: '1_10', label: '1-10' },
{ value: '11_50', label: '11-50' },
{ value: '51_200', label: '51-200' },
{ value: '201_500', label: '201-500' },
{ value: '501_1000', label: '501-1,000' },
{ value: '1000_plus', label: '1,000+' },
export const DEMO_REQUEST_COMPANY_SIZE_OPTIONS = [
{ value: '1_10', label: '110' },
{ value: '11_50', label: '1150' },
{ value: '51_200', label: '51200' },
{ value: '201_500', label: '201500' },
{ value: '501_1000', label: '5011,000' },
{ value: '1001_10000', label: '1,00110,000' },
{ value: '10000_plus', label: '10,000+' },
] as const
export const demoRequestSchema = z.object({
@@ -57,7 +62,11 @@ export const demoRequestSchema = z.object({
.min(1, 'Company email is required')
.max(320)
.transform((value) => value.toLowerCase())
.refine((value) => quickValidateEmail(value).isValid, 'Enter a valid work email'),
.refine((value) => quickValidateEmail(value).isValid, 'Enter a valid work email')
.refine((value) => {
const domain = value.split('@')[1]
return domain ? !FREE_EMAIL_DOMAINS.has(domain) : true
}, 'Please use your work email address'),
phoneNumber: z
.string()
.trim()
@@ -67,8 +76,8 @@ export const demoRequestSchema = z.object({
region: z.enum(DEMO_REQUEST_REGION_VALUES, {
errorMap: () => ({ message: 'Please select a region' }),
}),
userCount: z.enum(DEMO_REQUEST_USER_COUNT_VALUES, {
errorMap: () => ({ message: 'Please select the number of users' }),
companySize: z.enum(DEMO_REQUEST_COMPANY_SIZE_VALUES, {
errorMap: () => ({ message: 'Please select company size' }),
}),
details: z.string().trim().min(1, 'Details are required').max(2000),
})
@@ -79,6 +88,6 @@ export function getDemoRequestRegionLabel(value: DemoRequestPayload['region']):
return DEMO_REQUEST_REGION_OPTIONS.find((option) => option.value === value)?.label ?? value
}
export function getDemoRequestUserCountLabel(value: DemoRequestPayload['userCount']): string {
return DEMO_REQUEST_USER_COUNT_OPTIONS.find((option) => option.value === value)?.label ?? value
export function getDemoRequestCompanySizeLabel(value: DemoRequestPayload['companySize']): string {
return DEMO_REQUEST_COMPANY_SIZE_OPTIONS.find((option) => option.value === value)?.label ?? value
}

View File

@@ -16,8 +16,8 @@ import {
} from '@/components/emcn'
import { Check } from '@/components/emcn/icons'
import {
DEMO_REQUEST_COMPANY_SIZE_OPTIONS,
DEMO_REQUEST_REGION_OPTIONS,
DEMO_REQUEST_USER_COUNT_OPTIONS,
type DemoRequestPayload,
demoRequestSchema,
} from '@/app/(home)/components/demo-request/consts'
@@ -36,13 +36,13 @@ interface DemoRequestFormState {
companyEmail: string
phoneNumber: string
region: DemoRequestPayload['region'] | ''
userCount: DemoRequestPayload['userCount'] | ''
companySize: DemoRequestPayload['companySize'] | ''
details: string
}
const SUBMIT_SUCCESS_MESSAGE = "We'll be in touch soon!"
const COMBOBOX_REGIONS = [...DEMO_REQUEST_REGION_OPTIONS]
const COMBOBOX_USER_COUNTS = [...DEMO_REQUEST_USER_COUNT_OPTIONS]
const COMBOBOX_COMPANY_SIZES = [...DEMO_REQUEST_COMPANY_SIZE_OPTIONS]
const INITIAL_FORM_STATE: DemoRequestFormState = {
firstName: '',
@@ -50,7 +50,7 @@ const INITIAL_FORM_STATE: DemoRequestFormState = {
companyEmail: '',
phoneNumber: '',
region: '',
userCount: '',
companySize: '',
details: '',
}
@@ -118,7 +118,7 @@ export function DemoRequestModal({ children, theme = 'dark' }: DemoRequestModalP
companyEmail: fieldErrors.companyEmail?.[0],
phoneNumber: fieldErrors.phoneNumber?.[0],
region: fieldErrors.region?.[0],
userCount: fieldErrors.userCount?.[0],
companySize: fieldErrors.companySize?.[0],
details: fieldErrors.details?.[0],
})
return
@@ -235,13 +235,13 @@ export function DemoRequestModal({ children, theme = 'dark' }: DemoRequestModalP
filterOptions={false}
/>
</FormField>
<FormField htmlFor='userCount' label='Number of users' error={errors.userCount}>
<FormField htmlFor='companySize' label='Company size' error={errors.companySize}>
<Combobox
options={COMBOBOX_USER_COUNTS}
value={form.userCount}
selectedValue={form.userCount}
options={COMBOBOX_COMPANY_SIZES}
value={form.companySize}
selectedValue={form.companySize}
onChange={(value) =>
updateField('userCount', value as DemoRequestPayload['userCount'])
updateField('companySize', value as DemoRequestPayload['companySize'])
}
placeholder='Select'
editable={false}

View File

@@ -81,6 +81,56 @@ function ProviderPreviewIcon({ providerId }: { providerId?: string }) {
)
}
interface FeatureToggleItemProps {
feature: PermissionFeature
enabled: boolean
color: string
isInView: boolean
delay: number
textClassName: string
transition: Record<string, unknown>
onToggle: () => void
}
function FeatureToggleItem({
feature,
enabled,
color,
isInView,
delay,
textClassName,
transition,
onToggle,
}: FeatureToggleItemProps) {
return (
<motion.div
key={feature.key}
role='button'
tabIndex={0}
aria-label={`Toggle ${feature.name}`}
aria-pressed={enabled}
className='flex cursor-pointer items-center gap-2 rounded-[4px] py-0.5'
initial={{ opacity: 0, x: -6 }}
animate={isInView ? { opacity: 1, x: 0 } : {}}
transition={{ ...transition, delay }}
onClick={onToggle}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
onToggle()
}
}}
whileTap={{ scale: 0.98 }}
>
<CheckboxIcon checked={enabled} color={color} />
<ProviderPreviewIcon providerId={feature.providerId} />
<span className={textClassName} style={{ color: enabled ? '#F6F6F6AA' : '#F6F6F640' }}>
{feature.name}
</span>
</motion.div>
)
}
export function AccessControlPanel() {
const ref = useRef(null)
const isInView = useInView(ref, { once: true, margin: '-40px' })
@@ -97,39 +147,25 @@ export function AccessControlPanel() {
return (
<div key={category.label} className={catIdx > 0 ? 'mt-4' : ''}>
<span className='font-[430] font-season text-[#F6F6F6]/30 text-[10px] uppercase leading-none tracking-[0.08em]'>
<span className='font-[430] font-season text-[#F6F6F6]/55 text-[10px] uppercase leading-none tracking-[0.08em]'>
{category.label}
</span>
<div className='mt-[8px] grid grid-cols-2 gap-x-4 gap-y-[8px]'>
{category.features.map((feature, featIdx) => {
const enabled = accessState[feature.key]
return (
<motion.div
key={feature.key}
className='flex cursor-pointer items-center gap-[8px] rounded-[4px] py-[2px]'
initial={{ opacity: 0, x: -6 }}
animate={isInView ? { opacity: 1, x: 0 } : {}}
transition={{
delay: 0.05 + (offsetBefore + featIdx) * 0.04,
duration: 0.3,
}}
onClick={() =>
setAccessState((prev) => ({ ...prev, [feature.key]: !prev[feature.key] }))
}
whileTap={{ scale: 0.98 }}
>
<CheckboxIcon checked={enabled} color={category.color} />
<ProviderPreviewIcon providerId={feature.providerId} />
<span
className='truncate font-[430] font-season text-[13px] leading-none tracking-[0.02em]'
style={{ color: enabled ? '#F6F6F6AA' : '#F6F6F640' }}
>
{feature.name}
</span>
</motion.div>
)
})}
<div className='mt-2 grid grid-cols-2 gap-x-4 gap-y-2'>
{category.features.map((feature, featIdx) => (
<FeatureToggleItem
key={feature.key}
feature={feature}
enabled={accessState[feature.key]}
color={category.color}
isInView={isInView}
delay={0.05 + (offsetBefore + featIdx) * 0.04}
textClassName='truncate font-[430] font-season text-[13px] leading-none tracking-[0.02em]'
transition={{ duration: 0.3 }}
onToggle={() =>
setAccessState((prev) => ({ ...prev, [feature.key]: !prev[feature.key] }))
}
/>
))}
</div>
</div>
)
@@ -140,12 +176,11 @@ export function AccessControlPanel() {
<div className='hidden lg:block'>
{PERMISSION_CATEGORIES.map((category, catIdx) => (
<div key={category.label} className={catIdx > 0 ? 'mt-4' : ''}>
<span className='font-[430] font-season text-[#F6F6F6]/30 text-[10px] uppercase leading-none tracking-[0.08em]'>
<span className='font-[430] font-season text-[#F6F6F6]/55 text-[10px] uppercase leading-none tracking-[0.08em]'>
{category.label}
</span>
<div className='mt-[8px] grid grid-cols-2 gap-x-4 gap-y-[8px]'>
<div className='mt-2 grid grid-cols-2 gap-x-4 gap-y-2'>
{category.features.map((feature, featIdx) => {
const enabled = accessState[feature.key]
const currentIndex =
PERMISSION_CATEGORIES.slice(0, catIdx).reduce(
(sum, c) => sum + c.features.length,
@@ -153,30 +188,19 @@ export function AccessControlPanel() {
) + featIdx
return (
<motion.div
<FeatureToggleItem
key={feature.key}
className='flex cursor-pointer items-center gap-[8px] rounded-[4px] py-[2px]'
initial={{ opacity: 0, x: -6 }}
animate={isInView ? { opacity: 1, x: 0 } : {}}
transition={{
delay: 0.1 + currentIndex * 0.04,
duration: 0.3,
ease: [0.25, 0.46, 0.45, 0.94],
}}
onClick={() =>
feature={feature}
enabled={accessState[feature.key]}
color={category.color}
isInView={isInView}
delay={0.1 + currentIndex * 0.04}
textClassName='truncate font-[430] font-season text-[11px] leading-none tracking-[0.02em] transition-opacity duration-200'
transition={{ duration: 0.3, ease: [0.25, 0.46, 0.45, 0.94] }}
onToggle={() =>
setAccessState((prev) => ({ ...prev, [feature.key]: !prev[feature.key] }))
}
whileTap={{ scale: 0.98 }}
>
<CheckboxIcon checked={enabled} color={category.color} />
<ProviderPreviewIcon providerId={feature.providerId} />
<span
className='truncate font-[430] font-season text-[11px] leading-none tracking-[0.02em] transition-opacity duration-200'
style={{ color: enabled ? '#F6F6F6AA' : '#F6F6F640' }}
>
{feature.name}
</span>
</motion.div>
/>
)
})}
</div>

View File

@@ -125,7 +125,7 @@ function AuditRow({ entry, index }: AuditRowProps) {
const timeAgo = formatTimeAgo(entry.insertedAt)
return (
<div className='group relative overflow-hidden border-[#2A2A2A] border-b bg-[#1C1C1C] transition-colors duration-150 last:border-b-0 hover:bg-[#212121]'>
<div className='group relative overflow-hidden border-[var(--landing-border)] border-b bg-[var(--landing-bg)] transition-colors duration-150 last:border-b-0 hover:bg-[#212121]'>
{/* Left accent bar -- brightness encodes recency */}
<div
aria-hidden='true'
@@ -146,14 +146,14 @@ function AuditRow({ entry, index }: AuditRowProps) {
</div>
{/* Time */}
<span className='w-[56px] shrink-0 font-[430] font-season text-[#F6F6F6]/30 text-[11px] leading-none tracking-[0.02em]'>
<span className='w-[56px] shrink-0 font-[430] font-season text-[#F6F6F6]/55 text-[11px] leading-none tracking-[0.02em]'>
{timeAgo}
</span>
<span className='min-w-0 truncate font-[430] font-season text-[12px] leading-none tracking-[0.02em]'>
<span className='text-[#F6F6F6]/80'>{entry.actor}</span>
<span className='hidden sm:inline'>
<span className='text-[#F6F6F6]/40'> · </span>
<span className='text-[#F6F6F6]/60'> · </span>
<span className='text-[#F6F6F6]/55'>{entry.description}</span>
</span>
</span>
@@ -166,14 +166,14 @@ export function AuditLogPreview() {
const counterRef = useRef(ENTRY_TEMPLATES.length)
const templateIndexRef = useRef(6 % ENTRY_TEMPLATES.length)
const now = Date.now()
const [entries, setEntries] = useState<LogEntry[]>(() =>
ENTRY_TEMPLATES.slice(0, 6).map((t, i) => ({
const [entries, setEntries] = useState<LogEntry[]>(() => {
const now = Date.now()
return ENTRY_TEMPLATES.slice(0, 6).map((t, i) => ({
...t,
id: i,
insertedAt: now - INITIAL_OFFSETS_MS[i],
}))
)
})
const [, tick] = useState(0)
useEffect(() => {
@@ -208,10 +208,9 @@ export function AuditLogPreview() {
exit={{ opacity: 0 }}
transition={{
layout: {
type: 'spring',
stiffness: 380,
damping: 38,
mass: 0.8,
type: 'tween',
duration: 0.32,
ease: [0.25, 0.46, 0.45, 0.94],
},
y: { duration: 0.32, ease: [0.25, 0.46, 0.45, 0.94] },
opacity: { duration: 0.25 },

View File

@@ -4,11 +4,11 @@
* SEO:
* - `<section id="enterprise" aria-labelledby="enterprise-heading">`.
* - `<h2 id="enterprise-heading">` for the section title.
* - Compliance certs (SOC 2, HIPAA) as visible `<strong>` text.
* - Compliance cert (SOC 2) as visible `<strong>` text.
* - Enterprise CTA links to contact form via `<a>` with `rel="noopener noreferrer"`.
*
* GEO:
* - Entity-rich: "Sim is SOC 2 and HIPAA compliant" — not "We are compliant."
* - Entity-rich: "Sim is SOC 2 compliant" — not "We are compliant."
* - `<ul>` checklist of features (SSO, RBAC, audit logs, SLA, on-premise deployment)
* as an atomic answer block for "What enterprise features does Sim offer?".
*/
@@ -22,13 +22,27 @@ import { DemoRequestModal } from '@/app/(home)/components/demo-request/demo-requ
import { AccessControlPanel } from '@/app/(home)/components/enterprise/components/access-control-panel'
import { AuditLogPreview } from '@/app/(home)/components/enterprise/components/audit-log-preview'
const MARQUEE_KEYFRAMES = `
@keyframes marquee {
const ENTERPRISE_FEATURE_MARQUEE_STYLES = `
@keyframes enterprise-feature-marquee {
0% { transform: translateX(0); }
100% { transform: translateX(-25%); }
}
.enterprise-feature-marquee-track {
animation: enterprise-feature-marquee 30s linear infinite;
}
.enterprise-feature-marquee:hover .enterprise-feature-marquee-track {
animation-play-state: paused;
}
.enterprise-feature-marquee-tag {
transition: background-color 0.3s ease, color 0.3s ease;
}
@media (prefers-reduced-motion: reduce) {
@keyframes marquee { 0%, 100% { transform: none; } }
.enterprise-feature-marquee-track {
animation: none;
}
.enterprise-feature-marquee-tag {
transition: none;
}
}
`
@@ -51,13 +65,13 @@ const FEATURE_TAGS = [
function TrustStrip() {
return (
<div className='mx-6 mt-4 grid grid-cols-1 overflow-hidden rounded-[8px] border border-[#2A2A2A] sm:grid-cols-3 md:mx-8'>
{/* SOC 2 + HIPAA combined */}
<div className='mx-6 mt-4 grid grid-cols-1 overflow-hidden rounded-lg border border-[var(--landing-bg-elevated)] sm:grid-cols-3 md:mx-8'>
{/* SOC 2 */}
<Link
href='https://app.vanta.com/sim.ai/trust/v35ia0jil4l7dteqjgaktn'
target='_blank'
rel='noopener noreferrer'
className='group flex items-center gap-3 border-[#2A2A2A] border-b px-4 py-[14px] transition-colors hover:bg-[#212121] sm:border-r sm:border-b-0'
className='group flex items-center gap-3 border-[var(--landing-bg-elevated)] border-b px-4 py-3.5 transition-colors hover:bg-[#212121] sm:border-r sm:border-b-0'
>
<Image
src='/footer/soc2.png'
@@ -65,13 +79,14 @@ function TrustStrip() {
width={22}
height={22}
className='shrink-0 object-contain'
unoptimized
/>
<div className='flex flex-col gap-[3px]'>
<strong className='font-[430] font-season text-[13px] text-white leading-none'>
SOC 2 & HIPAA
<strong className='font-[430] font-season text-small text-white leading-none'>
SOC 2
</strong>
<span className='font-[430] font-season text-[#F6F6F6]/30 text-[11px] leading-none tracking-[0.02em] transition-colors group-hover:text-[#F6F6F6]/55'>
Type II · PHI protected
<span className='font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-subtle)_55%,transparent)] text-xs leading-none tracking-[0.02em] transition-colors group-hover:text-[color-mix(in_srgb,var(--landing-text-subtle)_75%,transparent)]'>
Type II
</span>
</div>
</Link>
@@ -81,31 +96,31 @@ function TrustStrip() {
href='https://github.com/simstudioai/sim'
target='_blank'
rel='noopener noreferrer'
className='group flex items-center gap-3 border-[#2A2A2A] border-b px-4 py-[14px] transition-colors hover:bg-[#212121] sm:border-r sm:border-b-0'
className='group flex items-center gap-3 border-[var(--landing-bg-elevated)] border-b px-4 py-3.5 transition-colors hover:bg-[#212121] sm:border-r sm:border-b-0'
>
<div className='flex h-[22px] w-[22px] shrink-0 items-center justify-center rounded-full bg-[#FFCC02]/10'>
<GithubIcon width={11} height={11} className='text-[#FFCC02]/75' />
</div>
<div className='flex flex-col gap-[3px]'>
<strong className='font-[430] font-season text-[13px] text-white leading-none'>
<strong className='font-[430] font-season text-small text-white leading-none'>
Open Source
</strong>
<span className='font-[430] font-season text-[#F6F6F6]/30 text-[11px] leading-none tracking-[0.02em] transition-colors group-hover:text-[#F6F6F6]/55'>
<span className='font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-subtle)_55%,transparent)] text-xs leading-none tracking-[0.02em] transition-colors group-hover:text-[color-mix(in_srgb,var(--landing-text-subtle)_75%,transparent)]'>
View on GitHub
</span>
</div>
</Link>
{/* SSO */}
<div className='flex items-center gap-3 px-4 py-[14px]'>
<div className='flex items-center gap-3 px-4 py-3.5'>
<div className='flex h-[22px] w-[22px] shrink-0 items-center justify-center rounded-full bg-[#2ABBF8]/10'>
<Lock className='h-[14px] w-[14px] text-[#2ABBF8]/75' />
</div>
<div className='flex flex-col gap-[3px]'>
<strong className='font-[430] font-season text-[13px] text-white leading-none'>
<strong className='font-[430] font-season text-small text-white leading-none'>
SSO & SCIM
</strong>
<span className='font-[430] font-season text-[#F6F6F6]/30 text-[11px] leading-none tracking-[0.02em]'>
<span className='font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-subtle)_55%,transparent)] text-xs leading-none tracking-[0.02em]'>
Okta, Azure AD, Google
</span>
</div>
@@ -116,9 +131,13 @@ function TrustStrip() {
export default function Enterprise() {
return (
<section id='enterprise' aria-labelledby='enterprise-heading' className='bg-[#F6F6F6]'>
<div className='px-4 pt-[60px] pb-[40px] sm:px-8 sm:pt-[80px] sm:pb-0 md:px-[80px] md:pt-[100px]'>
<div className='flex flex-col items-start gap-3 sm:gap-4 md:gap-[20px]'>
<section
id='enterprise'
aria-labelledby='enterprise-heading'
className='bg-[var(--landing-bg-section)]'
>
<div className='px-4 pt-[60px] pb-10 sm:px-8 sm:pt-20 sm:pb-0 md:px-20 md:pt-[100px]'>
<div className='flex flex-col items-start gap-3 sm:gap-4 md:gap-5'>
<Badge
variant='blue'
size='md'
@@ -130,7 +149,7 @@ export default function Enterprise() {
<h2
id='enterprise-heading'
className='max-w-[600px] font-[430] font-season text-[#1C1C1C] text-[32px] leading-[100%] tracking-[-0.02em] sm:text-[36px] md:text-[40px]'
className='max-w-[600px] text-balance font-[430] font-season text-[32px] text-[var(--landing-text-dark)] leading-[100%] tracking-[-0.02em] sm:text-[36px] md:text-[40px]'
>
Enterprise features for
<br />
@@ -138,15 +157,15 @@ export default function Enterprise() {
</h2>
</div>
<div className='mt-8 overflow-hidden rounded-[12px] bg-[#1C1C1C] sm:mt-10 md:mt-12'>
<div className='grid grid-cols-1 border-[#2A2A2A] border-b lg:grid-cols-[1fr_420px]'>
<div className='mt-8 overflow-hidden rounded-[12px] bg-[var(--landing-bg)] sm:mt-10 md:mt-12'>
<div className='grid grid-cols-1 border-[var(--landing-border)] border-b lg:grid-cols-[1fr_420px]'>
{/* Audit Trail */}
<div className='border-[#2A2A2A] lg:border-r'>
<div className='border-[var(--landing-border)] lg:border-r'>
<div className='px-6 pt-6 md:px-8 md:pt-8'>
<h3 className='font-[430] font-season text-[16px] text-white leading-[120%] tracking-[-0.01em]'>
Audit Trail
</h3>
<p className='mt-2 max-w-[480px] font-[430] font-season text-[#F6F6F6]/50 text-[14px] leading-[150%] tracking-[0.02em]'>
<p className='mt-2 max-w-[480px] font-[430] font-season text-[#F6F6F6]/70 text-[14px] leading-[150%] tracking-[0.02em]'>
Every action is captured with full actor attribution.
</p>
</div>
@@ -155,12 +174,12 @@ export default function Enterprise() {
</div>
{/* Access Control */}
<div className='border-[#2A2A2A] border-t lg:border-t-0'>
<div className='border-[var(--landing-border)] border-t lg:border-t-0'>
<div className='px-6 pt-6 md:px-8 md:pt-8'>
<h3 className='font-[430] font-season text-[16px] text-white leading-[120%] tracking-[-0.01em]'>
Access Control
</h3>
<p className='mt-[6px] font-[430] font-season text-[#F6F6F6]/50 text-[14px] leading-[150%] tracking-[0.02em]'>
<p className='mt-1.5 font-[430] font-season text-[#F6F6F6]/70 text-[14px] leading-[150%] tracking-[0.02em]'>
Restrict providers, surfaces, and tools per group.
</p>
</div>
@@ -172,27 +191,27 @@ export default function Enterprise() {
<TrustStrip />
{/* Scrolling feature ticker */}
<div className='relative mt-6 overflow-hidden border-[#2A2A2A] border-t'>
<style dangerouslySetInnerHTML={{ __html: MARQUEE_KEYFRAMES }} />
{/* Scrolling feature ticker — keyframe loop; pause on hover. Tags use transitions for hover. */}
<div className='enterprise-feature-marquee relative mt-6 overflow-hidden border-[var(--landing-bg-elevated)] border-t'>
<style dangerouslySetInnerHTML={{ __html: ENTERPRISE_FEATURE_MARQUEE_STYLES }} />
{/* Fade edges */}
<div
aria-hidden='true'
className='pointer-events-none absolute top-0 bottom-0 left-0 z-10 w-24'
style={{ background: 'linear-gradient(to right, #1C1C1C, transparent)' }}
style={{ background: 'linear-gradient(to right, var(--landing-bg), transparent)' }}
/>
<div
aria-hidden='true'
className='pointer-events-none absolute top-0 right-0 bottom-0 z-10 w-24'
style={{ background: 'linear-gradient(to left, #1C1C1C, transparent)' }}
style={{ background: 'linear-gradient(to left, var(--landing-bg), transparent)' }}
/>
{/* Duplicate tags for seamless loop */}
<div className='flex w-max' style={{ animation: 'marquee 30s linear infinite' }}>
<div className='enterprise-feature-marquee-track flex w-max'>
{[...FEATURE_TAGS, ...FEATURE_TAGS, ...FEATURE_TAGS, ...FEATURE_TAGS].map(
(tag, i) => (
<span
key={i}
className='whitespace-nowrap border-[#2A2A2A] border-r px-5 py-4 font-[430] font-season text-[#F6F6F6]/40 text-[13px] leading-none tracking-[0.02em]'
className='enterprise-feature-marquee-tag whitespace-nowrap border-[var(--landing-bg-elevated)] border-r px-5 py-4 font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-subtle)_60%,transparent)] text-small leading-none tracking-[0.02em] hover:bg-white/[0.04] hover:text-[color-mix(in_srgb,var(--landing-text-subtle)_80%,transparent)]'
>
{tag}
</span>
@@ -201,14 +220,14 @@ export default function Enterprise() {
</div>
</div>
<div className='flex items-center justify-between border-[#2A2A2A] border-t px-6 py-5 md:px-8 md:py-6'>
<p className='font-[430] font-season text-[#F6F6F6]/40 text-[15px] leading-[150%] tracking-[0.02em]'>
<div className='flex items-center justify-between border-[var(--landing-bg-elevated)] border-t px-6 py-5 md:px-8 md:py-6'>
<p className='font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-subtle)_60%,transparent)] text-base leading-[150%] tracking-[0.02em]'>
Ready for growth?
</p>
<DemoRequestModal>
<button
type='button'
className='group/cta inline-flex h-[32px] cursor-pointer items-center gap-[6px] rounded-[5px] border border-white bg-white px-[10px] font-[430] font-season text-[14px] text-black transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
className='group/cta inline-flex h-[32px] cursor-pointer items-center gap-1.5 rounded-[5px] border border-white bg-white px-2.5 font-[430] font-season text-[14px] text-black transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
>
Book a demo
<span className='relative h-[10px] w-[10px] shrink-0'>

View File

@@ -304,7 +304,7 @@ function WorkspacePreview({ activeTab, isActive }: { activeTab: number; isActive
function MockUserInput({ text }: { text: string }) {
return (
<div className='flex w-[380px] items-center gap-[6px] rounded-[16px] border border-[#E0E0E0] bg-white px-[10px] py-[8px] shadow-[0_2px_8px_rgba(0,0,0,0.06)]'>
<div className='flex w-[380px] items-center gap-1.5 rounded-[16px] border border-[#E0E0E0] bg-white px-2.5 py-2 shadow-[0_2px_8px_rgba(0,0,0,0.06)]'>
<div className='flex h-[24px] w-[24px] flex-shrink-0 items-center justify-center rounded-full border border-[#E8E8E8]'>
<svg width='12' height='12' viewBox='0 0 12 12' fill='none'>
<path d='M6 2.5v7M2.5 6h7' stroke='#999' strokeWidth='1.5' strokeLinecap='round' />
@@ -364,7 +364,7 @@ function MiniCardHeader({
color?: string
}) {
return (
<div className='flex items-center gap-[4px] border-[#F0F0F0] border-b px-[8px] py-[5px]'>
<div className='flex items-center gap-1 border-[#F0F0F0] border-b px-2 py-1.5'>
<MiniCardIcon variant={variant} color={color} />
<span className='truncate font-medium text-[#888] text-[7px] leading-none'>{label}</span>
</div>
@@ -419,7 +419,7 @@ function MiniCardBody({ variant, color }: { variant: CardVariant; color?: string
function PromptCardBody() {
return (
<div className='px-[8px] py-[6px]'>
<div className='px-2 py-1.5'>
<p className='break-words text-[#AAAAAA] text-[6.5px] leading-[10px]'>{TYPING_PROMPT}</p>
</div>
)
@@ -427,7 +427,7 @@ function PromptCardBody() {
function FileCardBody() {
return (
<div className='flex flex-col gap-[3px] px-[8px] py-[6px]'>
<div className='flex flex-col gap-[3px] px-2 py-1.5'>
<div className='h-[2px] w-[78%] rounded-full bg-[#E8E8E8]' />
<div className='h-[2px] w-[92%] rounded-full bg-[#E8E8E8]' />
<div className='h-[2px] w-[62%] rounded-full bg-[#E8E8E8]' />
@@ -450,7 +450,7 @@ const TABLE_ROW_WIDTHS = [
function TableCardBody() {
return (
<div className='flex flex-col'>
<div className='flex items-center gap-[4px] bg-[#FAFAFA] px-[6px] py-[3px]'>
<div className='flex items-center gap-1 bg-[#FAFAFA] px-1.5 py-[3px]'>
<div className='h-[2px] flex-1 rounded-full bg-[#D4D4D4]' />
<div className='h-[2px] flex-1 rounded-full bg-[#D4D4D4]' />
<div className='h-[2px] flex-1 rounded-full bg-[#D4D4D4]' />
@@ -458,7 +458,7 @@ function TableCardBody() {
{TABLE_ROW_WIDTHS.map((row, i) => (
<div
key={i}
className='flex items-center gap-[4px] border-[#F5F5F5] border-b px-[6px] py-[3.5px]'
className='flex items-center gap-1 border-[#F5F5F5] border-b px-1.5 py-[3.5px]'
>
<div className='h-[1.5px] rounded-full bg-[#EBEBEB]' style={{ width: `${row[0]}%` }} />
<div className='h-[1.5px] rounded-full bg-[#EBEBEB]' style={{ width: `${row[1]}%` }} />
@@ -472,17 +472,17 @@ function TableCardBody() {
function WorkflowCardBody({ color }: { color: string }) {
return (
<div className='relative h-full w-full'>
<div className='absolute top-[10px] left-[10px] h-[14px] w-[14px] rounded-[3px] border border-[#E0E0E0] bg-[#F8F8F8]' />
<div className='absolute top-2.5 left-[10px] h-[14px] w-[14px] rounded-[3px] border border-[#E0E0E0] bg-[#F8F8F8]' />
<div className='absolute top-[16px] left-[24px] h-[1px] w-[16px] bg-[#D8D8D8]' />
<div
className='absolute top-[10px] left-[40px] h-[14px] w-[14px] rounded-[3px] border-[2px]'
className='absolute top-2.5 left-[40px] h-[14px] w-[14px] rounded-[3px] border-[2px]'
style={{
backgroundColor: color,
borderColor: `${color}60`,
backgroundClip: 'padding-box',
}}
/>
<div className='absolute top-[24px] left-[46px] h-[12px] w-[1px] bg-[#D8D8D8]' />
<div className='absolute top-6 left-[46px] h-[12px] w-[1px] bg-[#D8D8D8]' />
<div className='absolute top-[36px] left-[40px] h-[14px] w-[14px] rounded-[3px] border border-[#E0E0E0] bg-[#F8F8F8]' />
<div className='absolute top-[42px] left-[54px] h-[1px] w-[14px] bg-[#D8D8D8]' />
<div
@@ -502,9 +502,9 @@ const KB_WIDTHS = [70, 85, 55, 80, 48] as const
function KnowledgeCardBody() {
return (
<div className='flex flex-col gap-[5px] px-[8px] py-[6px]'>
<div className='flex flex-col gap-[5px] px-2 py-1.5'>
{KB_WIDTHS.map((w, i) => (
<div key={i} className='flex items-center gap-[4px]'>
<div key={i} className='flex items-center gap-1'>
<div className='h-[3px] w-[3px] flex-shrink-0 rounded-full bg-[#D4D4D4]' />
<div className='h-[1.5px] rounded-full bg-[#E8E8E8]' style={{ width: `${w}%` }} />
</div>
@@ -524,9 +524,9 @@ const LOG_ENTRIES = [
function LogsCardBody() {
return (
<div className='flex flex-col gap-[3px] px-[6px] py-[4px]'>
<div className='flex flex-col gap-[3px] px-1.5 py-1'>
{LOG_ENTRIES.map((entry, i) => (
<div key={i} className='flex items-center gap-[4px] py-[1px]'>
<div key={i} className='flex items-center gap-1 py-[1px]'>
<div
className='h-[3px] w-[3px] flex-shrink-0 rounded-full'
style={{ backgroundColor: entry.color }}
@@ -620,8 +620,8 @@ const MD_COMPONENTS: Components = {
{children}
</h2>
),
ul: ({ children }) => <ul className='mb-3 list-disc pl-[24px]'>{children}</ul>,
ol: ({ children }) => <ol className='mb-3 list-decimal pl-[24px]'>{children}</ol>,
ul: ({ children }) => <ul className='mb-3 list-disc pl-6'>{children}</ul>,
ol: ({ children }) => <ol className='mb-3 list-decimal pl-6'>{children}</ol>,
li: ({ children }) => (
<li className='mb-1 text-[#1C1C1C] text-[14px] leading-[1.6]'>{children}</li>
),
@@ -633,8 +633,8 @@ function MockFullFiles() {
return (
<div className='flex h-full flex-col'>
<div className='flex h-[44px] shrink-0 items-center border-[#E5E5E5] border-b px-[24px]'>
<div className='flex items-center gap-[6px]'>
<div className='flex h-[44px] shrink-0 items-center border-[#E5E5E5] border-b px-6'>
<div className='flex items-center gap-1.5'>
<File className='h-[14px] w-[14px] text-[#999]' />
<span className='text-[#999] text-[13px]'>Files</span>
<span className='text-[#D4D4D4] text-[13px]'>/</span>
@@ -654,7 +654,7 @@ function MockFullFiles() {
onChange={(e) => setSource(e.target.value)}
spellCheck={false}
autoCorrect='off'
className='h-full w-full resize-none overflow-auto whitespace-pre-wrap bg-transparent p-[24px] font-[300] font-mono text-[#1C1C1C] text-[12px] leading-[1.7] outline-none'
className='h-full w-full resize-none overflow-auto whitespace-pre-wrap bg-transparent p-6 font-[300] font-mono text-[#1C1C1C] text-[12px] leading-[1.7] outline-none'
/>
</motion.div>
@@ -666,7 +666,7 @@ function MockFullFiles() {
animate={{ opacity: 1 }}
transition={{ duration: 0.4, delay: 0.5 }}
>
<div className='h-full overflow-auto p-[24px]'>
<div className='h-full overflow-auto p-6'>
<ReactMarkdown remarkPlugins={[remarkGfm]} components={MD_COMPONENTS}>
{source}
</ReactMarkdown>
@@ -686,8 +686,8 @@ const KB_STATUS_STYLES: Record<string, { bg: string; text: string; label: string
function MockFullKnowledgeBase({ revealedRows }: { revealedRows: number }) {
return (
<div className='flex h-full flex-col'>
<div className='flex h-[44px] shrink-0 items-center border-[#E5E5E5] border-b px-[24px]'>
<div className='flex items-center gap-[6px]'>
<div className='flex h-[44px] shrink-0 items-center border-[#E5E5E5] border-b px-6'>
<div className='flex items-center gap-1.5'>
<Database className='h-[14px] w-[14px] text-[#999]' />
<span className='text-[#999] text-[13px]'>Knowledge Base</span>
<span className='text-[#D4D4D4] text-[13px]'>/</span>
@@ -695,12 +695,12 @@ function MockFullKnowledgeBase({ revealedRows }: { revealedRows: number }) {
</div>
</div>
<div className='flex h-[36px] shrink-0 items-center border-[#E5E5E5] border-b px-[24px]'>
<div className='flex items-center gap-[6px]'>
<div className='flex h-[24px] items-center gap-[4px] rounded-[6px] border border-[#E5E5E5] px-[8px] text-[#999] text-[12px]'>
<div className='flex h-[36px] shrink-0 items-center border-[#E5E5E5] border-b px-6'>
<div className='flex items-center gap-1.5'>
<div className='flex h-[24px] items-center gap-1 rounded-[6px] border border-[#E5E5E5] px-2 text-[#999] text-[12px]'>
Sort
</div>
<div className='flex h-[24px] items-center gap-[4px] rounded-[6px] border border-[#E5E5E5] px-[8px] text-[#999] text-[12px]'>
<div className='flex h-[24px] items-center gap-1 rounded-[6px] border border-[#E5E5E5] px-2 text-[#999] text-[12px]'>
Filter
</div>
</div>
@@ -716,7 +716,7 @@ function MockFullKnowledgeBase({ revealedRows }: { revealedRows: number }) {
</colgroup>
<thead>
<tr>
<th className='border-[#E5E5E5] border-r border-b bg-[#FAFAFA] px-[4px] py-[7px] text-center align-middle'>
<th className='border-[#E5E5E5] border-r border-b bg-[#FAFAFA] px-1 py-[7px] text-center align-middle'>
<div className='flex items-center justify-center'>
<div className='h-[13px] w-[13px] rounded-[2px] border border-[#D4D4D4]' />
</div>
@@ -724,7 +724,7 @@ function MockFullKnowledgeBase({ revealedRows }: { revealedRows: number }) {
{MOCK_KB_COLUMNS.map((col) => (
<th
key={col}
className='border-[#E5E5E5] border-r border-b bg-[#FAFAFA] px-[8px] py-[7px] text-left align-middle'
className='border-[#E5E5E5] border-r border-b bg-[#FAFAFA] px-2 py-[7px] text-left align-middle'
>
<span className='font-base text-[#999] text-[13px]'>{col}</span>
</th>
@@ -742,11 +742,11 @@ function MockFullKnowledgeBase({ revealedRows }: { revealedRows: number }) {
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2, ease: 'easeOut' }}
>
<td className='border-[#E5E5E5] border-r border-b px-[4px] py-[7px] text-center align-middle'>
<td className='border-[#E5E5E5] border-r border-b px-1 py-[7px] text-center align-middle'>
<span className='text-[#999] text-[11px] tabular-nums'>{i + 1}</span>
</td>
<td className='border-[#E5E5E5] border-r border-b px-[8px] py-[7px] align-middle'>
<span className='flex items-center gap-[8px] text-[#1C1C1C] text-[13px]'>
<td className='border-[#E5E5E5] border-r border-b px-2 py-[7px] align-middle'>
<span className='flex items-center gap-2 text-[#1C1C1C] text-[13px]'>
<DocIcon className='h-[14px] w-[14px] shrink-0' />
<span className='truncate'>{row[0]}</span>
</span>
@@ -754,14 +754,14 @@ function MockFullKnowledgeBase({ revealedRows }: { revealedRows: number }) {
{row.slice(1, 4).map((cell, j) => (
<td
key={j}
className='border-[#E5E5E5] border-r border-b px-[8px] py-[7px] align-middle'
className='border-[#E5E5E5] border-r border-b px-2 py-[7px] align-middle'
>
<span className='text-[#999] text-[13px]'>{cell}</span>
</td>
))}
<td className='border-[#E5E5E5] border-r border-b px-[8px] py-[7px] align-middle'>
<td className='border-[#E5E5E5] border-r border-b px-2 py-[7px] align-middle'>
<span
className='inline-flex items-center rounded-full px-[8px] py-[2px] font-medium text-[11px]'
className='inline-flex items-center rounded-full px-2 py-0.5 font-medium text-[11px]'
style={{ backgroundColor: status.bg, color: status.text }}
>
{status.label}
@@ -885,8 +885,8 @@ function MockFullLogs({ revealedRows }: { revealedRows: number }) {
return (
<div className='relative flex h-full'>
<div className='flex min-w-0 flex-1 flex-col'>
<div className='flex h-[44px] shrink-0 items-center border-[#E5E5E5] border-b px-[24px]'>
<div className='flex items-center gap-[6px]'>
<div className='flex h-[44px] shrink-0 items-center border-[#E5E5E5] border-b px-6'>
<div className='flex items-center gap-1.5'>
<Library className='h-[14px] w-[14px] text-[#999]' />
<span className='font-medium text-[#1C1C1C] text-[13px]'>Logs</span>
</div>
@@ -902,7 +902,7 @@ function MockFullLogs({ revealedRows }: { revealedRows: number }) {
<thead className='shadow-[inset_0_-1px_0_#E5E5E5]'>
<tr>
{['Workflow', 'Date', 'Status', 'Cost', 'Trigger', 'Duration'].map((col) => (
<th key={col} className='h-10 px-[24px] py-[10px] text-left align-middle'>
<th key={col} className='h-10 px-6 py-2.5 text-left align-middle'>
<span className='font-base text-[#999] text-[13px]'>{col}</span>
</th>
))}
@@ -924,8 +924,8 @@ function MockFullLogs({ revealedRows }: { revealedRows: number }) {
)}
onClick={() => setSelectedRow(i)}
>
<td className='px-[24px] py-[10px] align-middle'>
<span className='flex items-center gap-[12px] font-medium text-[#1C1C1C] text-[14px]'>
<td className='px-6 py-2.5 align-middle'>
<span className='flex items-center gap-3 font-medium text-[#1C1C1C] text-[14px]'>
<div
className='h-[10px] w-[10px] shrink-0 rounded-[3px] border-[1.5px]'
style={{
@@ -937,26 +937,26 @@ function MockFullLogs({ revealedRows }: { revealedRows: number }) {
<span className='truncate'>{row[0]}</span>
</span>
</td>
<td className='px-[24px] py-[10px] align-middle'>
<td className='px-6 py-2.5 align-middle'>
<span className='font-medium text-[#999] text-[14px]'>{row[1]}</span>
</td>
<td className='px-[24px] py-[10px] align-middle'>
<td className='px-6 py-2.5 align-middle'>
<span
className='inline-flex items-center rounded-full px-[8px] py-[2px] font-medium text-[11px]'
className='inline-flex items-center rounded-full px-2 py-0.5 font-medium text-[11px]'
style={{ backgroundColor: statusStyle.bg, color: statusStyle.text }}
>
{statusStyle.label}
</span>
</td>
<td className='px-[24px] py-[10px] align-middle'>
<td className='px-6 py-2.5 align-middle'>
<span className='font-medium text-[#999] text-[14px]'>{row[3]}</span>
</td>
<td className='px-[24px] py-[10px] align-middle'>
<span className='rounded-[4px] bg-[#F5F5F5] px-[6px] py-[2px] text-[#666] text-[11px]'>
<td className='px-6 py-2.5 align-middle'>
<span className='rounded-[4px] bg-[#F5F5F5] px-1.5 py-0.5 text-[#666] text-[11px]'>
{row[4]}
</span>
</td>
<td className='px-[24px] py-[10px] align-middle'>
<td className='px-6 py-2.5 align-middle'>
<span className='font-medium text-[#999] text-[14px]'>{row[5]}</span>
</td>
</motion.tr>
@@ -1001,7 +1001,7 @@ function MockLogDetailsSidebar({ selectedRow, onPrev, onNext }: MockLogDetailsSi
const isNextDisabled = selectedRow === MOCK_LOG_DATA.length - 1
return (
<div className='flex h-full flex-col overflow-y-auto px-[14px] pt-[12px]'>
<div className='flex h-full flex-col overflow-y-auto px-3.5 pt-3'>
<div className='flex items-center justify-between'>
<span className='font-medium text-[#1C1C1C] text-[14px]'>Log Details</span>
<div className='flex items-center gap-[1px]'>
@@ -1030,18 +1030,18 @@ function MockLogDetailsSidebar({ selectedRow, onPrev, onNext }: MockLogDetailsSi
</div>
</div>
<div className='mt-[20px] flex flex-col gap-[10px]'>
<div className='flex items-center gap-[16px] px-[1px]'>
<div className='flex w-[120px] shrink-0 flex-col gap-[8px]'>
<div className='mt-5 flex flex-col gap-2.5'>
<div className='flex items-center gap-4 px-[1px]'>
<div className='flex w-[120px] shrink-0 flex-col gap-2'>
<span className='font-medium text-[#999] text-[12px]'>Timestamp</span>
<div className='flex items-center gap-[6px]'>
<div className='flex items-center gap-1.5'>
<span className='font-medium text-[#666] text-[13px]'>{date}</span>
<span className='font-medium text-[#666] text-[13px]'>{time}</span>
</div>
</div>
<div className='flex min-w-0 flex-1 flex-col gap-[8px]'>
<div className='flex min-w-0 flex-1 flex-col gap-2'>
<span className='font-medium text-[#999] text-[12px]'>Workflow</span>
<div className='flex items-center gap-[8px]'>
<div className='flex items-center gap-2'>
<div
className='h-[10px] w-[10px] shrink-0 rounded-[3px] border-[1.5px]'
style={{
@@ -1056,42 +1056,39 @@ function MockLogDetailsSidebar({ selectedRow, onPrev, onNext }: MockLogDetailsSi
</div>
<div className='flex flex-col'>
<div className='flex h-[42px] items-center justify-between border-[#E5E5E5] border-b px-[8px]'>
<div className='flex h-[42px] items-center justify-between border-[#E5E5E5] border-b px-2'>
<span className='font-medium text-[#999] text-[12px]'>Level</span>
<span
className='inline-flex items-center rounded-full px-[8px] py-[2px] font-medium text-[11px]'
className='inline-flex items-center rounded-full px-2 py-0.5 font-medium text-[11px]'
style={{ backgroundColor: statusStyle.bg, color: statusStyle.text }}
>
{statusStyle.label}
</span>
</div>
<div className='flex h-[42px] items-center justify-between border-[#E5E5E5] border-b px-[8px]'>
<div className='flex h-[42px] items-center justify-between border-[#E5E5E5] border-b px-2'>
<span className='font-medium text-[#999] text-[12px]'>Trigger</span>
<span className='rounded-[4px] bg-[#F5F5F5] px-[6px] py-[2px] text-[#666] text-[11px]'>
<span className='rounded-[4px] bg-[#F5F5F5] px-1.5 py-0.5 text-[#666] text-[11px]'>
{row[4]}
</span>
</div>
<div className='flex h-[42px] items-center justify-between px-[8px]'>
<div className='flex h-[42px] items-center justify-between px-2'>
<span className='font-medium text-[#999] text-[12px]'>Duration</span>
<span className='font-medium text-[#666] text-[13px]'>{row[5]}</span>
</div>
</div>
<div className='flex flex-col gap-[6px] rounded-[6px] border border-[#E5E5E5] bg-[#FAFAFA] px-[10px] py-[8px]'>
<div className='flex flex-col gap-1.5 rounded-[6px] border border-[#E5E5E5] bg-[#FAFAFA] px-2.5 py-2'>
<span className='font-medium text-[#999] text-[12px]'>Workflow Output</span>
<div className='rounded-[6px] bg-[#F0F0F0] p-[10px] font-mono text-[#555] text-[11px] leading-[1.5]'>
<div className='rounded-[6px] bg-[#F0F0F0] p-2.5 font-mono text-[#555] text-[11px] leading-[1.5]'>
{detail.output}
</div>
</div>
<div className='flex flex-col gap-[6px] rounded-[6px] border border-[#E5E5E5] bg-[#FAFAFA] px-[10px] py-[8px]'>
<div className='flex flex-col gap-1.5 rounded-[6px] border border-[#E5E5E5] bg-[#FAFAFA] px-2.5 py-2'>
<span className='font-medium text-[#999] text-[12px]'>Trace Spans</span>
<div className='flex flex-col gap-[6px]'>
<div className='flex flex-col gap-1.5'>
{detail.spans.map((span, i) => (
<div
key={i}
className={cn('flex flex-col gap-[3px]', span.depth === 1 && 'ml-[12px]')}
>
<div key={i} className={cn('flex flex-col gap-[3px]', span.depth === 1 && 'ml-3')}>
<div className='flex items-center justify-between'>
<span className='font-mono text-[#555] text-[11px]'>{span.name}</span>
<span className='font-medium text-[#999] text-[11px]'>{span.ms}ms</span>
@@ -1116,8 +1113,8 @@ function MockFullTable({ revealedRows }: { revealedRows: number }) {
return (
<div className='flex h-full flex-col'>
<div className='flex h-[44px] shrink-0 items-center border-[#E5E5E5] border-b px-[24px]'>
<div className='flex items-center gap-[6px]'>
<div className='flex h-[44px] shrink-0 items-center border-[#E5E5E5] border-b px-6'>
<div className='flex items-center gap-1.5'>
<Table className='h-[14px] w-[14px] text-[#999]' />
<span className='text-[#999] text-[13px]'>Tables</span>
<span className='text-[#D4D4D4] text-[13px]'>/</span>
@@ -1125,12 +1122,12 @@ function MockFullTable({ revealedRows }: { revealedRows: number }) {
</div>
</div>
<div className='flex h-[36px] shrink-0 items-center border-[#E5E5E5] border-b px-[24px]'>
<div className='flex items-center gap-[6px]'>
<div className='flex h-[24px] items-center gap-[4px] rounded-[6px] border border-[#E5E5E5] px-[8px] text-[#999] text-[12px]'>
<div className='flex h-[36px] shrink-0 items-center border-[#E5E5E5] border-b px-6'>
<div className='flex items-center gap-1.5'>
<div className='flex h-[24px] items-center gap-1 rounded-[6px] border border-[#E5E5E5] px-2 text-[#999] text-[12px]'>
Sort
</div>
<div className='flex h-[24px] items-center gap-[4px] rounded-[6px] border border-[#E5E5E5] px-[8px] text-[#999] text-[12px]'>
<div className='flex h-[24px] items-center gap-1 rounded-[6px] border border-[#E5E5E5] px-2 text-[#999] text-[12px]'>
Filter
</div>
</div>
@@ -1146,7 +1143,7 @@ function MockFullTable({ revealedRows }: { revealedRows: number }) {
</colgroup>
<thead>
<tr>
<th className='border-[#E5E5E5] border-r border-b bg-[#FAFAFA] px-[4px] py-[7px] text-center align-middle'>
<th className='border-[#E5E5E5] border-r border-b bg-[#FAFAFA] px-1 py-[7px] text-center align-middle'>
<div className='flex items-center justify-center'>
<div className='h-[13px] w-[13px] rounded-[2px] border border-[#D4D4D4]' />
</div>
@@ -1154,9 +1151,9 @@ function MockFullTable({ revealedRows }: { revealedRows: number }) {
{MOCK_TABLE_COLUMNS.map((col) => (
<th
key={col}
className='border-[#E5E5E5] border-r border-b bg-[#FAFAFA] px-[8px] py-[7px] text-left align-middle'
className='border-[#E5E5E5] border-r border-b bg-[#FAFAFA] px-2 py-[7px] text-left align-middle'
>
<div className='flex items-center gap-[6px]'>
<div className='flex items-center gap-1.5'>
<ColumnTypeIcon />
<span className='font-medium text-[#1C1C1C] text-[13px]'>{col}</span>
<ChevronDown className='ml-auto h-[7px] w-[9px] shrink-0 text-[#CCC]' />
@@ -1179,7 +1176,7 @@ function MockFullTable({ revealedRows }: { revealedRows: number }) {
>
<td
className={cn(
'border-[#E5E5E5] border-r border-b px-[4px] py-[7px] text-center align-middle',
'border-[#E5E5E5] border-r border-b px-1 py-[7px] text-center align-middle',
isSelected ? 'bg-[rgba(37,99,235,0.06)]' : 'hover:bg-[#FAFAFA]'
)}
>
@@ -1189,7 +1186,7 @@ function MockFullTable({ revealedRows }: { revealedRows: number }) {
<td
key={j}
className={cn(
'relative border-[#E5E5E5] border-r border-b px-[8px] py-[7px] align-middle',
'relative border-[#E5E5E5] border-r border-b px-2 py-[7px] align-middle',
isSelected ? 'bg-[rgba(37,99,235,0.06)]' : 'hover:bg-[#FAFAFA]'
)}
>
@@ -1311,13 +1308,13 @@ function DefaultPreview() {
return (
<motion.div
key={key}
className='absolute flex items-center justify-center rounded-xl border border-[#E5E5E5] bg-white p-[10px] shadow-[0_2px_4px_0_rgba(0,0,0,0.06)]'
className='absolute flex items-center justify-center rounded-xl border border-[#E5E5E5] bg-white p-2.5 shadow-[0_2px_4px_0_rgba(0,0,0,0.06)]'
initial={{ top: '50%', left: '50%', opacity: 0, scale: 0, x: '-50%', y: '-50%' }}
animate={inView ? { top, left, opacity: 1, scale: 1, x: '-50%', y: '-50%' } : undefined}
transition={{
type: 'spring',
stiffness: 50,
damping: 12,
stiffness: 80,
damping: 20,
delay: explodeDelay,
}}
style={{ color }}
@@ -1334,7 +1331,7 @@ function DefaultPreview() {
animate={inView ? { opacity: 1, x: '-50%', y: '-50%' } : undefined}
transition={{ duration: 0.4, ease: 'easeOut', delay: 0 }}
>
<div className='flex h-[36px] items-center gap-[8px] rounded-[8px] border border-[#E5E5E5] bg-white px-[10px] shadow-[0_2px_6px_0_rgba(0,0,0,0.08)]'>
<div className='flex h-[36px] items-center gap-2 rounded-[8px] border border-[#E5E5E5] bg-white px-2.5 shadow-[0_2px_6px_0_rgba(0,0,0,0.08)]'>
<div className='flex h-[22px] w-[22px] flex-shrink-0 items-center justify-center rounded-[5px] bg-[#1e1e1e]'>
<svg width='11' height='11' viewBox='0 0 10 10' fill='none'>
<path

View File

@@ -152,7 +152,7 @@ function DotGrid({
return (
<div
aria-hidden='true'
className={`h-full shrink-0 bg-[#F6F6F6] p-[6px] ${borderLeft ? 'border-[#E9E9E9] border-l' : ''}`}
className={`h-full shrink-0 bg-[var(--landing-bg-section)] p-1.5 ${borderLeft ? 'border-[var(--divider)] border-l' : ''}`}
style={{
width: width ? `${width}px` : undefined,
display: 'grid',
@@ -181,7 +181,7 @@ export default function Features() {
<section
id='features'
aria-labelledby='features-heading'
className='relative overflow-hidden bg-[#F6F6F6]'
className='relative overflow-hidden bg-[var(--landing-bg-section)]'
>
<div aria-hidden='true' className='absolute top-0 left-0 w-full'>
<Image
@@ -190,15 +190,11 @@ export default function Features() {
width={1440}
height={366}
className='h-auto w-full'
priority
/>
</div>
<div className='relative z-10 pt-[60px] lg:pt-[100px]'>
<div
ref={sectionRef}
className='flex flex-col items-start gap-[20px] px-[24px] lg:px-[80px]'
>
<div ref={sectionRef} className='flex flex-col items-start gap-5 px-6 lg:px-20'>
<Badge
variant='blue'
size='md'
@@ -216,31 +212,31 @@ export default function Features() {
</Badge>
<h2
id='features-heading'
className='max-w-[900px] font-[430] font-season text-[#1C1C1C] text-[28px] leading-[110%] tracking-[-0.02em] md:text-[40px]'
className='max-w-[900px] text-balance font-[430] font-season text-[28px] text-[var(--landing-text-dark)] leading-[110%] tracking-[-0.02em] md:text-[40px]'
>
{HEADING_LETTERS.map((char, i) => (
<ScrollLetter key={i} scrollYProgress={scrollYProgress} charIndex={i}>
{char}
</ScrollLetter>
))}
<span className='text-[#1C1C1C]/40'>
<span className='text-[color-mix(in_srgb,var(--landing-text-dark)_40%,transparent)]'>
Design powerful workflows, connect your data, and monitor every run all in one
platform.
</span>
</h2>
</div>
<div className='relative mt-[40px] pb-[40px] lg:mt-[73px] lg:pb-[80px]'>
<div className='relative mt-10 pb-10 lg:mt-[73px] lg:pb-20'>
<div
aria-hidden='true'
className='absolute top-0 bottom-0 left-[80px] z-20 hidden w-px bg-[#E9E9E9] lg:block'
className='absolute top-0 bottom-0 left-[80px] z-20 hidden w-px bg-[var(--divider)] lg:block'
/>
<div
aria-hidden='true'
className='absolute top-0 right-[80px] bottom-0 z-20 hidden w-px bg-[#E9E9E9] lg:block'
className='absolute top-0 right-[80px] bottom-0 z-20 hidden w-px bg-[var(--divider)] lg:block'
/>
<div className='flex h-[68px] border border-[#E9E9E9] lg:overflow-hidden'>
<div className='flex h-[68px] border border-[var(--divider)] lg:overflow-hidden'>
<div className='h-full shrink-0'>
<div className='h-full lg:hidden'>
<DotGrid cols={3} rows={8} width={24} />
@@ -258,7 +254,7 @@ export default function Features() {
role='tab'
aria-selected={index === activeTab}
onClick={() => setActiveTab(index)}
className={`relative h-full flex-1 items-center justify-center whitespace-nowrap px-[12px] font-medium font-season text-[#212121] text-[12px] uppercase lg:px-0 lg:text-[14px]${tab.hideOnMobile ? ' hidden lg:flex' : ' flex'}${index > 0 ? ' border-[#E9E9E9] border-l' : ''}`}
className={`relative h-full flex-1 items-center justify-center whitespace-nowrap px-3 font-medium font-season text-[var(--landing-text-dark)] text-caption uppercase lg:px-0 lg:text-sm${tab.hideOnMobile ? ' hidden lg:flex' : ' flex'}${index > 0 ? ' border-[var(--divider)] border-l' : ''}`}
style={{ backgroundColor: index === activeTab ? '#FDFDFD' : '#F6F6F6' }}
>
{tab.mobileLabel ? (
@@ -298,19 +294,19 @@ export default function Features() {
</div>
</div>
<div className='mt-[32px] flex flex-col gap-[24px] px-[24px] lg:mt-[60px] lg:grid lg:grid-cols-[1fr_2.8fr] lg:gap-[60px] lg:px-[120px]'>
<div className='flex flex-col items-start justify-between gap-[24px] pt-[20px] lg:h-[560px] lg:gap-0'>
<div className='flex flex-col items-start gap-[16px]'>
<h3 className='font-[430] font-season text-[#1C1C1C] text-[24px] leading-[120%] tracking-[-0.02em] lg:text-[28px]'>
<div className='mt-8 flex flex-col gap-6 px-6 lg:mt-[60px] lg:grid lg:grid-cols-[1fr_2.8fr] lg:gap-[60px] lg:px-[120px]'>
<div className='flex flex-col items-start justify-between gap-6 pt-5 lg:h-[560px] lg:gap-0'>
<div className='flex flex-col items-start gap-4'>
<h3 className='font-[430] font-season text-[24px] text-[var(--landing-text-dark)] leading-[120%] tracking-[-0.02em] lg:text-[28px]'>
{FEATURE_TABS[activeTab].title}
</h3>
<p className='font-[430] font-season text-[#1C1C1C]/50 text-[16px] leading-[150%] tracking-[0.02em] lg:text-[18px]'>
<p className='font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-dark)_50%,transparent)] text-md leading-[150%] tracking-[0.02em] lg:text-lg'>
{FEATURE_TABS[activeTab].description}
</p>
</div>
<Link
href='/signup'
className='group/cta inline-flex h-[32px] items-center gap-[6px] rounded-[5px] border border-[#1D1D1D] bg-[#1D1D1D] px-[10px] font-[430] font-season text-[14px] text-white transition-colors hover:border-[#2A2A2A] hover:bg-[#2A2A2A]'
className='group/cta inline-flex h-[32px] items-center gap-1.5 rounded-[5px] border border-[#1D1D1D] bg-[#1D1D1D] px-2.5 font-[430] font-season text-sm text-white transition-colors hover:border-[var(--landing-bg-elevated)] hover:bg-[var(--landing-bg-elevated)]'
>
{FEATURE_TABS[activeTab].cta}
<span className='relative h-[10px] w-[10px] shrink-0'>
@@ -336,7 +332,7 @@ export default function Features() {
<FeaturesPreview activeTab={activeTab} />
</div>
<div aria-hidden='true' className='mt-[60px] hidden h-px bg-[#E9E9E9] lg:block' />
<div aria-hidden='true' className='mt-[60px] hidden h-px bg-[var(--divider)] lg:block' />
</div>
</div>
</section>

View File

@@ -4,12 +4,12 @@ import { useCallback, useRef, useState } from 'react'
import { ArrowUp } from 'lucide-react'
import Link from 'next/link'
import { useLandingSubmit } from '@/app/(home)/components/landing-preview/components/landing-preview-panel/landing-preview-panel'
import { useAnimatedPlaceholder } from '@/app/workspace/[workspaceId]/home/hooks/use-animated-placeholder'
import { useAnimatedPlaceholder } from '@/hooks/use-animated-placeholder'
const MAX_HEIGHT = 120
const CTA_BUTTON =
'inline-flex items-center h-[32px] rounded-[5px] border px-[10px] font-[430] font-season text-[14px]'
'inline-flex items-center h-[32px] rounded-[5px] border px-2.5 font-[430] font-season text-sm'
export function FooterCTA() {
const landingSubmit = useLandingSubmit()
@@ -41,14 +41,14 @@ export function FooterCTA() {
}, [])
return (
<div className='flex flex-col items-center px-4 pt-[120px] pb-[100px] sm:px-8 md:px-[80px]'>
<h2 className='text-center font-[430] font-season text-[#1C1C1C] text-[28px] leading-[100%] tracking-[-0.02em] sm:text-[32px] md:text-[36px]'>
<div className='flex flex-col items-center px-4 pt-[120px] pb-[100px] sm:px-8 md:px-20'>
<h2 className='text-balance text-center font-[430] font-season text-[28px] text-[var(--landing-text-dark)] leading-[100%] tracking-[-0.02em] sm:text-[32px] md:text-[36px]'>
What should we get done?
</h2>
<div className='mt-8 w-full max-w-[42rem]'>
<div
className='cursor-text rounded-[20px] border border-[#E5E5E5] bg-white px-[10px] py-[8px] shadow-sm'
className='cursor-text rounded-[20px] border border-[var(--landing-bg-skeleton)] bg-white px-2.5 py-2 shadow-sm'
onClick={() => textareaRef.current?.focus()}
>
<textarea
@@ -59,7 +59,7 @@ export function FooterCTA() {
onInput={handleInput}
placeholder={animatedPlaceholder}
rows={2}
className='m-0 box-border min-h-[48px] w-full resize-none border-0 bg-transparent px-[4px] py-[4px] font-body text-[#1C1C1C] text-[15px] leading-[24px] tracking-[-0.015em] outline-none placeholder:font-[380] placeholder:text-[#999] focus-visible:ring-0'
className='m-0 box-border min-h-[48px] w-full resize-none border-0 bg-transparent px-1 py-1 font-body text-[var(--landing-text-dark)] text-base leading-[24px] tracking-[-0.015em] outline-none placeholder:font-[380] placeholder:text-[var(--landing-text-muted)] focus-visible:ring-0'
style={{ caretColor: '#1C1C1C', maxHeight: `${MAX_HEIGHT}px` }}
/>
<div className='flex items-center justify-end'>
@@ -67,6 +67,7 @@ export function FooterCTA() {
type='button'
onClick={handleSubmit}
disabled={isEmpty}
aria-label='Submit message'
className='flex h-[28px] w-[28px] items-center justify-center rounded-full border-0 p-0 transition-colors'
style={{
background: isEmpty ? '#C0C0C0' : '#1C1C1C',
@@ -79,18 +80,18 @@ export function FooterCTA() {
</div>
</div>
<div className='mt-8 flex gap-[8px]'>
<div className='mt-8 flex gap-2'>
<a
href='https://docs.sim.ai'
target='_blank'
rel='noopener noreferrer'
className={`${CTA_BUTTON} border-[#D4D4D4] text-[#1C1C1C] transition-colors hover:bg-[#E8E8E8]`}
className={`${CTA_BUTTON} border-[var(--landing-border-subtle)] text-[var(--landing-text-dark)] transition-colors hover:bg-[var(--landing-bg-skeleton)]`}
>
Docs
</a>
<Link
href='/signup'
className={`${CTA_BUTTON} gap-[8px] border-[#1C1C1C] bg-[#1C1C1C] text-white transition-colors hover:border-[#333] hover:bg-[#333]`}
className={`${CTA_BUTTON} gap-2 border-[var(--landing-bg)] bg-[var(--landing-bg)] text-white transition-colors hover:border-[var(--landing-bg-elevated)] hover:bg-[var(--landing-bg-elevated)]`}
>
Get started
</Link>

View File

@@ -2,7 +2,8 @@ import Image from 'next/image'
import Link from 'next/link'
import { FooterCTA } from '@/app/(home)/components/footer/footer-cta'
const LINK_CLASS = 'text-[14px] text-[#999] transition-colors hover:text-[#ECECEC]'
const LINK_CLASS =
'text-sm text-[var(--landing-text-muted)] transition-colors hover:text-[var(--landing-text)]'
interface FooterItem {
label: string
@@ -25,6 +26,9 @@ const RESOURCES_LINKS: FooterItem[] = [
{ label: 'Blog', href: '/blog' },
// { label: 'Templates', href: '/templates' },
{ label: 'Docs', href: 'https://docs.sim.ai', external: true },
{ label: 'Models', href: '/models' },
// { label: 'Academy', href: '/academy' },
{ label: 'Partners', href: '/partners' },
{ label: 'Careers', href: 'https://jobs.ashbyhq.com/sim', external: true },
{ label: 'Changelog', href: '/changelog' },
]
@@ -81,8 +85,8 @@ const LEGAL_LINKS: FooterItem[] = [
function FooterColumn({ title, items }: { title: string; items: FooterItem[] }) {
return (
<div>
<h3 className='mb-[16px] font-medium text-[#ECECEC] text-[14px]'>{title}</h3>
<div className='flex flex-col gap-[10px]'>
<h3 className='mb-4 font-medium text-[var(--landing-text)] text-sm'>{title}</h3>
<div className='flex flex-col gap-2.5'>
{items.map(({ label, href, external }) =>
external ? (
<a
@@ -113,16 +117,16 @@ export default function Footer({ hideCTA }: FooterProps) {
return (
<footer
role='contentinfo'
className={`bg-[#F6F6F6] pb-[40px] font-[430] font-season text-[14px]${hideCTA ? ' pt-[40px]' : ''}`}
className={`bg-[var(--landing-bg-section)] pb-10 font-[430] font-season text-sm${hideCTA ? ' pt-10' : ''}`}
>
{!hideCTA && <FooterCTA />}
<div className='px-4 sm:px-8 md:px-[80px]'>
<div className='relative overflow-hidden rounded-lg bg-[#1C1C1C] px-6 pt-[40px] pb-[32px] sm:px-10 sm:pt-[48px] sm:pb-[40px]'>
<div className='px-4 sm:px-8 md:px-20'>
<div className='relative overflow-hidden rounded-lg bg-[var(--landing-bg)] px-6 pt-10 pb-8 sm:px-10 sm:pt-12 sm:pb-10'>
<nav
aria-label='Footer navigation'
className='relative z-[1] grid grid-cols-2 gap-x-8 gap-y-10 sm:grid-cols-3 lg:grid-cols-7'
>
<div className='col-span-2 flex flex-col gap-[24px] sm:col-span-1'>
<div className='col-span-2 flex flex-col gap-6 sm:col-span-1'>
<Link href='/' aria-label='Sim home'>
<Image
src='/logo/sim-landing.svg'

View File

@@ -20,13 +20,13 @@ const LandingPreview = dynamic(
),
{
ssr: false,
loading: () => <div className='aspect-[1116/549] w-full rounded bg-[#1b1b1b]' />,
loading: () => <div className='aspect-[1116/549] w-full rounded bg-[var(--landing-bg)]' />,
}
)
/** Shared base classes for CTA link buttons — matches Deploy/Run button styling in the preview panel. */
const CTA_BASE =
'inline-flex items-center h-[32px] rounded-[5px] border px-[10px] font-[430] font-season text-[14px]'
'inline-flex items-center h-[32px] rounded-[5px] border px-2.5 font-[430] font-season text-sm'
export default function Hero() {
const blockStates = useBlockCycle()
@@ -35,14 +35,14 @@ export default function Hero() {
<section
id='hero'
aria-labelledby='hero-heading'
className='relative flex flex-col items-center overflow-hidden bg-[#1C1C1C] pt-[60px] pb-[12px] lg:pt-[100px]'
className='relative flex flex-col items-center overflow-hidden bg-[var(--landing-bg)] pt-[60px] pb-3 lg:pt-[100px]'
>
<p className='sr-only'>
Sim is the open-source platform to build AI agents and run your agentic workforce. Connect
1,000+ integrations and LLMs including OpenAI, Claude, Gemini, Mistral, and xAI to
deploy and orchestrate agentic workflows. Create agents, workflows, knowledge bases, tables,
and docs. Trusted by over 100,000 builders at startups and Fortune 500 companies. SOC2 and
HIPAA compliant.
SOC2 compliant.
</p>
<div
@@ -59,22 +59,22 @@ export default function Hero() {
<Image src='/landing/card-right.svg' alt='' fill className='object-contain' />
</div>
<div className='relative z-10 flex flex-col items-center gap-[12px]'>
<div className='relative z-10 flex flex-col items-center gap-3'>
<h1
id='hero-heading'
className='font-[430] font-season text-[36px] text-white leading-[100%] tracking-[-0.02em] sm:text-[48px] lg:text-[72px]'
className='text-balance font-[430] font-season text-[36px] text-white leading-[100%] tracking-[-0.02em] sm:text-[48px] lg:text-[72px]'
>
Build AI Agents
</h1>
<p className='font-[430] font-season text-[#F6F6F6]/60 text-[15px] leading-[125%] tracking-[0.02em] lg:text-[18px]'>
<p className='font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-subtle)_60%,transparent)] text-base leading-[125%] tracking-[0.02em] lg:text-lg'>
Sim is the AI Workspace for Agent Builders.
</p>
<div className='mt-[12px] flex items-center gap-[8px]'>
<div className='mt-3 flex items-center gap-2'>
<DemoRequestModal>
<button
type='button'
className={`${CTA_BASE} border-[#3d3d3d] bg-transparent text-[#ECECEC] transition-colors hover:bg-[#2A2A2A]`}
className={`${CTA_BASE} border-[var(--landing-border-strong)] bg-transparent text-[var(--landing-text)] transition-colors hover:bg-[var(--landing-bg-elevated)]`}
aria-label='Get a demo'
>
Get a demo
@@ -82,7 +82,7 @@ export default function Hero() {
</DemoRequestModal>
<Link
href='/signup'
className={`${CTA_BASE} gap-[8px] border-[#FFFFFF] bg-[#FFFFFF] text-black transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]`}
className={`${CTA_BASE} gap-2 border-white bg-white text-black transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]`}
aria-label='Get started with Sim'
>
Get started
@@ -119,7 +119,7 @@ export default function Hero() {
<BlocksRightSideAnimated animState={blockStates.rightSide} />
</div>
<div className='relative z-10 overflow-hidden rounded border border-[#2A2A2A]'>
<div className='relative z-10 overflow-hidden rounded border border-[var(--landing-bg-elevated)]'>
<LandingPreview />
</div>
</div>

View File

@@ -0,0 +1,157 @@
import { File } from '@/components/emcn/icons'
import { DocxIcon, PdfIcon } from '@/components/icons/document-icons'
import type {
PreviewColumn,
PreviewRow,
} from '@/app/(home)/components/landing-preview/components/landing-preview-resource/landing-preview-resource'
import {
LandingPreviewResource,
ownerCell,
} from '@/app/(home)/components/landing-preview/components/landing-preview-resource/landing-preview-resource'
/** Generic audio/zip icon using basic SVG since no dedicated component exists */
function AudioIcon({ className }: { className?: string }) {
return (
<svg
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='1.5'
strokeLinecap='round'
strokeLinejoin='round'
className={className}
>
<path d='M9 18V5l12-2v13' />
<circle cx='6' cy='18' r='3' />
<circle cx='18' cy='16' r='3' />
</svg>
)
}
function JsonlIcon({ className }: { className?: string }) {
return (
<svg
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='1.5'
strokeLinecap='round'
strokeLinejoin='round'
className={className}
>
<path d='M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z' />
<path d='M14 2v4a2 2 0 0 0 2 2h4' />
<path d='M10 9H8' />
<path d='M16 13H8' />
<path d='M16 17H8' />
</svg>
)
}
function ZipIcon({ className }: { className?: string }) {
return (
<svg
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='1.5'
strokeLinecap='round'
strokeLinejoin='round'
className={className}
>
<path d='M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z' />
<path d='M14 2v4a2 2 0 0 0 2 2h4' />
<path d='M10 6h1' />
<path d='M10 10h1' />
<path d='M10 14h1' />
<path d='M9 18h2v2h-2z' />
</svg>
)
}
const COLUMNS: PreviewColumn[] = [
{ id: 'name', header: 'Name' },
{ id: 'size', header: 'Size' },
{ id: 'type', header: 'Type' },
{ id: 'created', header: 'Created' },
{ id: 'owner', header: 'Owner' },
]
const ROWS: PreviewRow[] = [
{
id: '1',
cells: {
name: { icon: <PdfIcon className='h-[14px] w-[14px]' />, label: 'Q1 Performance Report.pdf' },
size: { label: '2.4 MB' },
type: { icon: <PdfIcon className='h-[14px] w-[14px]' />, label: 'PDF' },
created: { label: '3 hours ago' },
owner: ownerCell('T', 'Theo L.'),
},
},
{
id: '2',
cells: {
name: { icon: <ZipIcon className='h-[14px] w-[14px]' />, label: 'product-screenshots.zip' },
size: { label: '18.7 MB' },
type: { icon: <ZipIcon className='h-[14px] w-[14px]' />, label: 'ZIP' },
created: { label: '1 day ago' },
owner: ownerCell('A', 'Alex M.'),
},
},
{
id: '3',
cells: {
name: { icon: <JsonlIcon className='h-[14px] w-[14px]' />, label: 'training-dataset.jsonl' },
size: { label: '892 KB' },
type: { icon: <JsonlIcon className='h-[14px] w-[14px]' />, label: 'JSONL' },
created: { label: '3 days ago' },
owner: ownerCell('J', 'Jordan P.'),
},
},
{
id: '4',
cells: {
name: { icon: <PdfIcon className='h-[14px] w-[14px]' />, label: 'brand-guidelines.pdf' },
size: { label: '5.1 MB' },
type: { icon: <PdfIcon className='h-[14px] w-[14px]' />, label: 'PDF' },
created: { label: '1 week ago' },
owner: ownerCell('S', 'Sarah K.'),
},
},
{
id: '5',
cells: {
name: { icon: <AudioIcon className='h-[14px] w-[14px]' />, label: 'customer-interviews.mp3' },
size: { label: '45.2 MB' },
type: { icon: <AudioIcon className='h-[14px] w-[14px]' />, label: 'Audio' },
created: { label: 'March 20th, 2026' },
owner: ownerCell('V', 'Vik M.'),
},
},
{
id: '6',
cells: {
name: { icon: <DocxIcon className='h-[14px] w-[14px]' />, label: 'onboarding-playbook.docx' },
size: { label: '1.1 MB' },
type: { icon: <DocxIcon className='h-[14px] w-[14px]' />, label: 'DOCX' },
created: { label: 'March 14th, 2026' },
owner: ownerCell('S', 'Sarah K.'),
},
},
]
/**
* Static landing preview of the Files workspace page.
*/
export function LandingPreviewFiles() {
return (
<LandingPreviewResource
icon={File}
title='Files'
createLabel='Upload file'
searchPlaceholder='Search files...'
columns={COLUMNS}
rows={ROWS}
/>
)
}

View File

@@ -3,7 +3,7 @@
import { memo, useCallback, useRef, useState } from 'react'
import { ArrowUp } from 'lucide-react'
import { useLandingSubmit } from '@/app/(home)/components/landing-preview/components/landing-preview-panel/landing-preview-panel'
import { useAnimatedPlaceholder } from '@/app/workspace/[workspaceId]/home/hooks/use-animated-placeholder'
import { useAnimatedPlaceholder } from '@/hooks/use-animated-placeholder'
const C = {
SURFACE: '#292929',
@@ -48,10 +48,10 @@ export const LandingPreviewHome = memo(function LandingPreviewHome() {
}, [])
return (
<div className='flex min-w-0 flex-1 flex-col items-center justify-center px-[24px] pb-[2vh]'>
<div className='flex min-w-0 flex-1 flex-col items-center justify-center px-6 pb-[2vh]'>
<p
role='presentation'
className='mb-[24px] max-w-[42rem] font-[430] font-season text-[32px] tracking-[-0.02em]'
className='mb-6 max-w-[42rem] font-[430] font-season text-[32px] tracking-[-0.02em]'
style={{ color: C.TEXT_PRIMARY }}
>
What should we get done?
@@ -59,7 +59,7 @@ export const LandingPreviewHome = memo(function LandingPreviewHome() {
<div className='w-full max-w-[32rem]'>
<div
className='cursor-text rounded-[20px] border px-[10px] py-[8px]'
className='cursor-text rounded-[20px] border px-2.5 py-2'
style={{ borderColor: C.BORDER, backgroundColor: C.SURFACE }}
onClick={() => textareaRef.current?.focus()}
>
@@ -71,7 +71,7 @@ export const LandingPreviewHome = memo(function LandingPreviewHome() {
onInput={handleInput}
placeholder={animatedPlaceholder}
rows={1}
className='m-0 box-border min-h-[24px] w-full resize-none overflow-y-auto border-0 bg-transparent px-[4px] py-[4px] font-body text-[15px] leading-[24px] tracking-[-0.015em] outline-none placeholder:font-[380] placeholder:text-[#787878] focus-visible:ring-0'
className='m-0 box-border min-h-[24px] w-full resize-none overflow-y-auto border-0 bg-transparent px-1 py-1 font-body text-[15px] leading-[24px] tracking-[-0.015em] outline-none placeholder:font-[380] placeholder:text-[#787878] focus-visible:ring-0'
style={{
color: C.TEXT_PRIMARY,
caretColor: C.TEXT_PRIMARY,

View File

@@ -0,0 +1,105 @@
import { Database } from '@/components/emcn/icons'
import {
AirtableIcon,
AsanaIcon,
ConfluenceIcon,
GoogleDocsIcon,
GoogleDriveIcon,
JiraIcon,
SalesforceIcon,
SlackIcon,
ZendeskIcon,
} from '@/components/icons'
import type {
PreviewColumn,
PreviewRow,
} from '@/app/(home)/components/landing-preview/components/landing-preview-resource/landing-preview-resource'
import { LandingPreviewResource } from '@/app/(home)/components/landing-preview/components/landing-preview-resource/landing-preview-resource'
const DB_ICON = <Database className='h-[14px] w-[14px]' />
function connectorIcons(icons: React.ComponentType<{ className?: string }>[]) {
return {
content: (
<div className='flex items-center gap-1'>
{icons.map((Icon, i) => (
<Icon key={i} className='h-3.5 w-3.5 flex-shrink-0' />
))}
</div>
),
}
}
const COLUMNS: PreviewColumn[] = [
{ id: 'name', header: 'Name' },
{ id: 'documents', header: 'Documents' },
{ id: 'tokens', header: 'Tokens' },
{ id: 'connectors', header: 'Connectors' },
{ id: 'created', header: 'Created' },
]
const ROWS: PreviewRow[] = [
{
id: '1',
cells: {
name: { icon: DB_ICON, label: 'Product Documentation' },
documents: { label: '847' },
tokens: { label: '1,284,392' },
connectors: connectorIcons([AsanaIcon, GoogleDocsIcon]),
created: { label: '2 days ago' },
},
},
{
id: '2',
cells: {
name: { icon: DB_ICON, label: 'Customer Support KB' },
documents: { label: '234' },
tokens: { label: '892,104' },
connectors: connectorIcons([ZendeskIcon, SlackIcon]),
created: { label: '1 week ago' },
},
},
{
id: '3',
cells: {
name: { icon: DB_ICON, label: 'Engineering Wiki' },
documents: { label: '1,203' },
tokens: { label: '2,847,293' },
connectors: connectorIcons([ConfluenceIcon, JiraIcon]),
created: { label: 'March 12th, 2026' },
},
},
{
id: '4',
cells: {
name: { icon: DB_ICON, label: 'Marketing Assets' },
documents: { label: '189' },
tokens: { label: '634,821' },
connectors: connectorIcons([GoogleDriveIcon, AirtableIcon]),
created: { label: 'March 5th, 2026' },
},
},
{
id: '5',
cells: {
name: { icon: DB_ICON, label: 'Sales Playbook' },
documents: { label: '92' },
tokens: { label: '418,570' },
connectors: connectorIcons([SalesforceIcon]),
created: { label: 'February 28th, 2026' },
},
},
]
export function LandingPreviewKnowledge() {
return (
<LandingPreviewResource
icon={Database}
title='Knowledge Base'
createLabel='New base'
searchPlaceholder='Search knowledge bases...'
columns={COLUMNS}
rows={ROWS}
/>
)
}

View File

@@ -0,0 +1,321 @@
'use client'
import { useMemo, useState } from 'react'
import { Download } from 'lucide-react'
import { ArrowUpDown, Badge, Library, ListFilter, Search } from '@/components/emcn'
import type { BadgeProps } from '@/components/emcn/components/badge/badge'
import { cn } from '@/lib/core/utils/cn'
interface LogRow {
id: string
workflowName: string
workflowColor: string
date: string
status: 'completed' | 'error' | 'running'
cost: string
trigger: 'webhook' | 'api' | 'schedule' | 'manual' | 'mcp' | 'chat'
triggerLabel: string
duration: string
}
type BadgeVariant = BadgeProps['variant']
const STATUS_VARIANT: Record<LogRow['status'], BadgeVariant> = {
completed: 'gray',
error: 'red',
running: 'amber',
}
const STATUS_LABELS: Record<LogRow['status'], string> = {
completed: 'Completed',
error: 'Error',
running: 'Running',
}
const TRIGGER_VARIANT: Record<LogRow['trigger'], BadgeVariant> = {
webhook: 'orange',
api: 'blue',
schedule: 'green',
manual: 'gray-secondary',
mcp: 'cyan',
chat: 'purple',
}
const MOCK_LOGS: LogRow[] = [
{
id: '1',
workflowName: 'Customer Onboarding',
workflowColor: '#4f8ef7',
date: 'Apr 1 10:42 AM',
status: 'running',
cost: '-',
trigger: 'webhook',
triggerLabel: 'Webhook',
duration: '-',
},
{
id: '2',
workflowName: 'Lead Enrichment',
workflowColor: '#33C482',
date: 'Apr 1 09:15 AM',
status: 'error',
cost: '318 credits',
trigger: 'api',
triggerLabel: 'API',
duration: '2.7s',
},
{
id: '3',
workflowName: 'Email Campaign',
workflowColor: '#a855f7',
date: 'Apr 1 08:30 AM',
status: 'completed',
cost: '89 credits',
trigger: 'schedule',
triggerLabel: 'Schedule',
duration: '0.8s',
},
{
id: '4',
workflowName: 'Data Pipeline',
workflowColor: '#f97316',
date: 'Mar 31 10:14 PM',
status: 'completed',
cost: '241 credits',
trigger: 'webhook',
triggerLabel: 'Webhook',
duration: '4.1s',
},
{
id: '5',
workflowName: 'Invoice Processing',
workflowColor: '#ec4899',
date: 'Mar 31 08:45 PM',
status: 'completed',
cost: '112 credits',
trigger: 'manual',
triggerLabel: 'Manual',
duration: '0.9s',
},
{
id: '6',
workflowName: 'Support Triage',
workflowColor: '#0ea5e9',
date: 'Mar 31 07:22 PM',
status: 'completed',
cost: '197 credits',
trigger: 'api',
triggerLabel: 'API',
duration: '1.6s',
},
{
id: '7',
workflowName: 'Content Moderator',
workflowColor: '#f59e0b',
date: 'Mar 31 06:11 PM',
status: 'error',
cost: '284 credits',
trigger: 'schedule',
triggerLabel: 'Schedule',
duration: '3.2s',
},
]
type SortKey = 'workflowName' | 'date' | 'status' | 'cost' | 'trigger' | 'duration'
const COL_HEADERS: { key: SortKey; label: string }[] = [
{ key: 'workflowName', label: 'Workflow' },
{ key: 'date', label: 'Date' },
{ key: 'status', label: 'Status' },
{ key: 'cost', label: 'Cost' },
{ key: 'trigger', label: 'Trigger' },
{ key: 'duration', label: 'Duration' },
]
export function LandingPreviewLogs() {
const [search, setSearch] = useState('')
const [sortKey, setSortKey] = useState<SortKey | null>(null)
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc')
const [activeTab, setActiveTab] = useState<'logs' | 'dashboard'>('logs')
function handleSort(key: SortKey) {
if (sortKey === key) {
setSortDir((d) => (d === 'asc' ? 'desc' : 'asc'))
} else {
setSortKey(key)
setSortDir('asc')
}
}
const sorted = useMemo(() => {
const q = search.toLowerCase()
const filtered = q
? MOCK_LOGS.filter(
(log) =>
log.workflowName.toLowerCase().includes(q) ||
log.triggerLabel.toLowerCase().includes(q) ||
STATUS_LABELS[log.status].toLowerCase().includes(q)
)
: MOCK_LOGS
if (!sortKey) return filtered
return [...filtered].sort((a, b) => {
const av = sortKey === 'cost' ? a.cost.replace(/\D/g, '') : a[sortKey]
const bv = sortKey === 'cost' ? b.cost.replace(/\D/g, '') : b[sortKey]
const cmp = av.localeCompare(bv, undefined, { numeric: true, sensitivity: 'base' })
return sortDir === 'asc' ? cmp : -cmp
})
}, [search, sortKey, sortDir])
return (
<div className='flex h-full flex-1 flex-col overflow-hidden bg-[var(--bg)]'>
{/* Header */}
<div className='border-[var(--border)] border-b px-6 py-2.5'>
<div className='flex items-center justify-between'>
<div className='flex items-center gap-3'>
<Library className='h-[14px] w-[14px] text-[var(--text-icon)]' />
<h1 className='font-medium text-[var(--text-body)] text-sm'>Logs</h1>
</div>
<div className='flex items-center gap-1'>
<div className='flex cursor-default items-center rounded-md px-2 py-1 text-[var(--text-secondary)] text-caption'>
<Download className='mr-1.5 h-[14px] w-[14px] text-[var(--text-icon)]' />
Export
</div>
<button
type='button'
onClick={() => setActiveTab('logs')}
className='rounded-md px-2 py-1 text-caption transition-colors'
style={{
backgroundColor: activeTab === 'logs' ? 'var(--surface-active)' : 'transparent',
color: activeTab === 'logs' ? 'var(--text-body)' : 'var(--text-secondary)',
}}
>
Logs
</button>
<button
type='button'
onClick={() => setActiveTab('dashboard')}
className='rounded-md px-2 py-1 text-caption transition-colors'
style={{
backgroundColor:
activeTab === 'dashboard' ? 'var(--surface-active)' : 'transparent',
color: activeTab === 'dashboard' ? 'var(--text-body)' : 'var(--text-secondary)',
}}
>
Dashboard
</button>
</div>
</div>
</div>
{/* Options bar */}
<div className='border-[var(--border)] border-b px-6 py-2.5'>
<div className='flex items-center justify-between'>
<div className='flex flex-1 items-center gap-2.5'>
<Search className='h-[14px] w-[14px] flex-shrink-0 text-[var(--text-icon)]' />
<input
type='text'
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder='Search logs...'
className='flex-1 bg-transparent text-[var(--text-body)] text-caption outline-none placeholder:text-[var(--text-subtle)]'
/>
</div>
<div className='flex items-center gap-1.5'>
<div className='flex cursor-default items-center rounded-md px-2 py-1 text-[var(--text-secondary)] text-caption'>
<ListFilter className='mr-1.5 h-[14px] w-[14px] text-[var(--text-icon)]' />
Filter
</div>
<button
type='button'
onClick={() => handleSort(sortKey ?? 'workflowName')}
className='flex cursor-default items-center rounded-md px-2 py-1 text-[var(--text-secondary)] text-caption transition-colors hover-hover:bg-[var(--surface-3)]'
>
<ArrowUpDown className='mr-1.5 h-[14px] w-[14px] text-[var(--text-icon)]' />
Sort
</button>
</div>
</div>
</div>
{/* Table — uses <table> for pixel-perfect column alignment with headers */}
<div className='min-h-0 flex-1 overflow-hidden'>
<table className='w-full table-fixed text-sm'>
<colgroup>
<col style={{ width: '22%' }} />
<col style={{ width: '18%' }} />
<col style={{ width: '13%' }} />
<col style={{ width: '15%' }} />
<col style={{ width: '14%' }} />
<col style={{ width: '18%' }} />
</colgroup>
<thead className='shadow-[inset_0_-1px_0_var(--border)]'>
<tr>
{COL_HEADERS.map(({ key, label }) => (
<th
key={key}
className='h-10 px-6 py-1.5 text-left align-middle font-normal text-caption'
>
<button
type='button'
onClick={() => handleSort(key)}
className={cn(
'flex items-center gap-1 transition-colors hover-hover:text-[var(--text-secondary)]',
sortKey === key ? 'text-[var(--text-secondary)]' : 'text-[var(--text-muted)]'
)}
>
{label}
{sortKey === key && <ArrowUpDown className='h-[10px] w-[10px] opacity-60' />}
</button>
</th>
))}
</tr>
</thead>
<tbody>
{sorted.map((log) => (
<tr
key={log.id}
className='h-[44px] cursor-default transition-colors hover-hover:bg-[var(--surface-3)]'
>
<td className='px-6 align-middle'>
<div className='flex items-center gap-2'>
<div
className='h-[10px] w-[10px] flex-shrink-0 rounded-[3px] border-[1.5px]'
style={{
backgroundColor: log.workflowColor,
borderColor: `${log.workflowColor}60`,
backgroundClip: 'padding-box',
}}
/>
<span className='min-w-0 truncate font-medium text-[var(--text-primary)] text-caption'>
{log.workflowName}
</span>
</div>
</td>
<td className='px-6 align-middle text-[var(--text-secondary)] text-caption'>
{log.date}
</td>
<td className='px-6 align-middle'>
<Badge variant={STATUS_VARIANT[log.status]} size='sm' dot>
{STATUS_LABELS[log.status]}
</Badge>
</td>
<td className='px-6 align-middle text-[var(--text-secondary)] text-caption'>
{log.cost}
</td>
<td className='px-6 align-middle'>
<Badge variant={TRIGGER_VARIANT[log.trigger]} size='sm'>
{log.triggerLabel}
</Badge>
</td>
<td className='px-6 align-middle text-[var(--text-secondary)] text-caption'>
{log.duration}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)
}

View File

@@ -59,10 +59,10 @@ export const LandingPreviewPanel = memo(function LandingPreviewPanel() {
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]'>
<div className='flex h-full flex-col border-[#2c2c2c] border-l pt-3.5'>
{/* Header — More + Chat | Deploy + Run */}
<div className='flex flex-shrink-0 items-center justify-between px-[8px]'>
<div className='pointer-events-none flex gap-[6px]'>
<div className='flex flex-shrink-0 items-center justify-between px-2'>
<div className='pointer-events-none flex gap-1.5'>
<div className='flex h-[30px] w-[30px] items-center justify-center rounded-[5px] border border-[#3d3d3d] bg-[#363636]'>
<MoreHorizontal className='h-[14px] w-[14px] text-[#e6e6e6]' />
</div>
@@ -72,14 +72,14 @@ export const LandingPreviewPanel = memo(function LandingPreviewPanel() {
</div>
<Link
href='/signup'
className='flex gap-[6px]'
className='flex gap-1.5'
onMouseMove={(e) => setCursorPos({ x: e.clientX, y: e.clientY })}
onMouseLeave={() => setCursorPos(null)}
>
<div className='flex h-[30px] items-center rounded-[5px] bg-[#33C482] px-[10px] transition-colors hover:bg-[#2DAC72]'>
<div className='flex h-[30px] items-center rounded-[5px] bg-[#33C482] px-2.5 transition-colors hover:bg-[#2DAC72]'>
<span className='font-medium text-[#1b1b1b] text-[12px]'>Deploy</span>
</div>
<div className='flex h-[30px] items-center gap-[8px] rounded-[5px] bg-[#33C482] px-[10px] transition-colors hover:bg-[#2DAC72]'>
<div className='flex h-[30px] items-center gap-2 rounded-[5px] bg-[#33C482] px-2.5 transition-colors hover:bg-[#2DAC72]'>
<Play className='h-[11.5px] w-[11.5px] text-[#1b1b1b]' />
<span className='font-medium text-[#1b1b1b] text-[12px]'>Run</span>
</div>
@@ -101,7 +101,7 @@ export const LandingPreviewPanel = memo(function LandingPreviewPanel() {
<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]'>
<div className='flex items-center gap-[5px] bg-white px-1.5 py-1 font-medium text-[#1C1C1C] text-[11px]'>
Get started
<ChevronDown className='-rotate-90 h-[7px] w-[7px] text-[#1C1C1C]' />
</div>
@@ -111,31 +111,31 @@ export const LandingPreviewPanel = memo(function LandingPreviewPanel() {
</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]'>
<div className='flex flex-shrink-0 items-center px-2 pt-3.5'>
<div className='pointer-events-none flex gap-1'>
<div className='flex h-[28px] items-center rounded-[6px] border border-[#3d3d3d] bg-[#363636] px-2 py-[5px]'>
<span className='font-medium text-[#e6e6e6] text-[12.5px]'>Copilot</span>
</div>
<div className='flex h-[28px] items-center rounded-[6px] border border-transparent px-[8px] py-[5px]'>
<div className='flex h-[28px] items-center rounded-[6px] border border-transparent px-2 py-[5px]'>
<span className='font-medium text-[#787878] text-[12.5px]'>Toolbar</span>
</div>
<div className='flex h-[28px] items-center rounded-[6px] border border-transparent px-[8px] py-[5px]'>
<div className='flex h-[28px] items-center rounded-[6px] border border-transparent px-2 py-[5px]'>
<span className='font-medium text-[#787878] text-[12.5px]'>Editor</span>
</div>
</div>
</div>
{/* Tab content — copilot */}
<div className='flex flex-1 flex-col overflow-hidden pt-[12px]'>
<div className='flex flex-1 flex-col overflow-hidden pt-3'>
<div className='flex h-full flex-col'>
{/* Copilot header bar — matches mx-[-1px] in real copilot */}
<div className='pointer-events-none mx-[-1px] flex flex-shrink-0 items-center rounded-[4px] border border-[#2c2c2c] bg-[#292929] px-[12px] py-[6px]'>
<div className='pointer-events-none mx-[-1px] flex flex-shrink-0 items-center rounded-[4px] border border-[#2c2c2c] bg-[#292929] px-3 py-1.5'>
<span className='truncate font-medium text-[#e6e6e6] text-[14px]'>New Chat</span>
</div>
{/* User input — matches real UserInput at p-[8px] inside copilot welcome state */}
<div className='px-[8px] pt-[12px] pb-[8px]'>
<div className='rounded-[4px] border border-[#3d3d3d] bg-[#292929] px-[6px] py-[6px]'>
<div className='px-2 pt-3 pb-2'>
<div className='rounded-[4px] border border-[#3d3d3d] bg-[#292929] px-1.5 py-1.5'>
<textarea
ref={textareaRef}
value={inputValue}
@@ -143,7 +143,7 @@ export const LandingPreviewPanel = memo(function LandingPreviewPanel() {
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'
className='mb-1.5 min-h-[48px] w-full cursor-text resize-none border-0 bg-transparent px-0.5 py-1 font-base text-[#e6e6e6] text-sm leading-[1.25rem] placeholder-[#787878] caret-[#e6e6e6] outline-none'
/>
<div className='flex items-center justify-end'>
<button

View File

@@ -0,0 +1,211 @@
'use client'
import type { ReactNode } from 'react'
import { useMemo, useState } from 'react'
import { ArrowUpDown, ListFilter, Plus, Search } from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
export interface PreviewColumn {
id: string
header: string
width?: number
}
export interface PreviewCell {
icon?: ReactNode
label?: string
content?: ReactNode
}
export interface PreviewRow {
id: string
cells: Record<string, PreviewCell>
}
interface LandingPreviewResourceProps {
icon: React.ComponentType<{ className?: string }>
title: string
createLabel: string
searchPlaceholder: string
columns: PreviewColumn[]
rows: PreviewRow[]
onRowClick?: (id: string) => void
}
export function ownerCell(initial: string, name: string): PreviewCell {
return {
icon: (
<span className='flex h-[14px] w-[14px] flex-shrink-0 items-center justify-center rounded-full border border-[var(--border)] bg-[var(--surface-3)] font-medium text-[8px] text-[var(--text-secondary)]'>
{initial}
</span>
),
label: name,
}
}
export function LandingPreviewResource({
icon: Icon,
title,
createLabel,
searchPlaceholder,
columns,
rows,
onRowClick,
}: LandingPreviewResourceProps) {
const [search, setSearch] = useState('')
const [sortColId, setSortColId] = useState<string | null>(null)
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc')
function handleSortClick(colId: string) {
if (sortColId === colId) {
setSortDir((d) => (d === 'asc' ? 'desc' : 'asc'))
} else {
setSortColId(colId)
setSortDir('asc')
}
}
const sorted = useMemo(() => {
const q = search.toLowerCase()
const filtered = q
? rows.filter((row) =>
Object.values(row.cells).some((cell) => cell.label?.toLowerCase().includes(q))
)
: rows
if (!sortColId) return filtered
return [...filtered].sort((a, b) => {
const av = a.cells[sortColId]?.label ?? ''
const bv = b.cells[sortColId]?.label ?? ''
const cmp = av.localeCompare(bv, undefined, { numeric: true, sensitivity: 'base' })
return sortDir === 'asc' ? cmp : -cmp
})
}, [rows, search, sortColId, sortDir])
return (
<div className='flex h-full flex-1 flex-col overflow-hidden bg-[var(--bg)]'>
{/* Header */}
<div className='border-[var(--border)] border-b px-6 py-2.5'>
<div className='flex items-center justify-between'>
<div className='flex items-center gap-3'>
<Icon className='h-[14px] w-[14px] text-[var(--text-icon)]' />
<h1 className='font-medium text-[var(--text-body)] text-sm'>{title}</h1>
</div>
<div className='flex cursor-default items-center rounded-md px-2 py-1 text-[var(--text-secondary)] text-caption'>
<Plus className='mr-1.5 h-[14px] w-[14px] text-[var(--text-icon)]' />
{createLabel}
</div>
</div>
</div>
{/* Options bar */}
<div className='border-[var(--border)] border-b px-6 py-2.5'>
<div className='flex items-center justify-between'>
<div className='flex flex-1 items-center gap-2.5'>
<Search className='h-[14px] w-[14px] flex-shrink-0 text-[var(--text-icon)]' />
<input
type='text'
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder={searchPlaceholder}
className='flex-1 bg-transparent text-[var(--text-body)] text-caption outline-none placeholder:text-[var(--text-subtle)]'
/>
</div>
<div className='flex items-center gap-1.5'>
<div className='flex cursor-default items-center rounded-md px-2 py-1 text-[var(--text-secondary)] text-caption'>
<ListFilter className='mr-1.5 h-[14px] w-[14px] text-[var(--text-icon)]' />
Filter
</div>
<button
type='button'
onClick={() => handleSortClick(sortColId ?? columns[0]?.id)}
className='flex cursor-default items-center rounded-md px-2 py-1 text-[var(--text-secondary)] text-caption transition-colors hover-hover:bg-[var(--surface-3)]'
>
<ArrowUpDown className='mr-1.5 h-[14px] w-[14px] text-[var(--text-icon)]' />
Sort
</button>
</div>
</div>
</div>
{/* Table */}
<div className='min-h-0 flex-1 overflow-hidden'>
<table className='w-full table-fixed text-sm'>
<colgroup>
{columns.map((col, i) => (
<col
key={col.id}
style={i === 0 ? { minWidth: col.width ?? 200 } : { width: col.width ?? 160 }}
/>
))}
</colgroup>
<thead className='shadow-[inset_0_-1px_0_var(--border)]'>
<tr>
{columns.map((col) => (
<th
key={col.id}
className='h-10 px-6 py-1.5 text-left align-middle font-normal text-caption'
>
<button
type='button'
onClick={() => handleSortClick(col.id)}
className={cn(
'flex items-center gap-1 transition-colors hover-hover:text-[var(--text-secondary)]',
sortColId === col.id
? 'text-[var(--text-secondary)]'
: 'text-[var(--text-muted)]'
)}
>
{col.header}
{sortColId === col.id && (
<ArrowUpDown className='h-[10px] w-[10px] opacity-60' />
)}
</button>
</th>
))}
</tr>
</thead>
<tbody>
{sorted.map((row) => (
<tr
key={row.id}
onClick={() => onRowClick?.(row.id)}
className={cn(
'transition-colors hover-hover:bg-[var(--surface-3)]',
onRowClick && 'cursor-pointer'
)}
>
{columns.map((col, colIdx) => {
const cell = row.cells[col.id]
return (
<td key={col.id} className='px-6 py-2.5 align-middle'>
{cell?.content ? (
cell.content
) : (
<span
className={cn(
'flex min-w-0 items-center gap-3 font-medium text-sm',
colIdx === 0
? 'text-[var(--text-body)]'
: 'text-[var(--text-secondary)]'
)}
>
{cell?.icon && (
<span className='flex-shrink-0 text-[var(--text-icon)]'>
{cell.icon}
</span>
)}
<span className='truncate'>{cell?.label ?? '—'}</span>
</span>
)}
</td>
)
})}
</tr>
))}
</tbody>
</table>
</div>
</div>
)
}

View File

@@ -0,0 +1,88 @@
import { Calendar } from '@/components/emcn/icons'
import type {
PreviewColumn,
PreviewRow,
} from '@/app/(home)/components/landing-preview/components/landing-preview-resource/landing-preview-resource'
import { LandingPreviewResource } from '@/app/(home)/components/landing-preview/components/landing-preview-resource/landing-preview-resource'
const CAL_ICON = <Calendar className='h-[14px] w-[14px]' />
const COLUMNS: PreviewColumn[] = [
{ id: 'task', header: 'Task' },
{ id: 'schedule', header: 'Schedule', width: 240 },
{ id: 'nextRun', header: 'Next Run' },
{ id: 'lastRun', header: 'Last Run' },
]
const ROWS: PreviewRow[] = [
{
id: '1',
cells: {
task: { icon: CAL_ICON, label: 'Sync CRM contacts' },
schedule: { label: 'Recurring, every day at 9:00 AM' },
nextRun: { label: 'Tomorrow' },
lastRun: { label: '2 hours ago' },
},
},
{
id: '2',
cells: {
task: { icon: CAL_ICON, label: 'Generate weekly report' },
schedule: { label: 'Recurring, every Monday at 8:00 AM' },
nextRun: { label: 'In 5 days' },
lastRun: { label: '6 days ago' },
},
},
{
id: '3',
cells: {
task: { icon: CAL_ICON, label: 'Clean up stale files' },
schedule: { label: 'Recurring, every Sunday at midnight' },
nextRun: { label: 'In 2 days' },
lastRun: { label: '6 days ago' },
},
},
{
id: '4',
cells: {
task: { icon: CAL_ICON, label: 'Send performance digest' },
schedule: { label: 'Recurring, every Friday at 5:00 PM' },
nextRun: { label: 'In 3 days' },
lastRun: { label: '3 days ago' },
},
},
{
id: '5',
cells: {
task: { icon: CAL_ICON, label: 'Backup production data' },
schedule: { label: 'Recurring, every 4 hours' },
nextRun: { label: 'In 2 hours' },
lastRun: { label: '2 hours ago' },
},
},
{
id: '6',
cells: {
task: { icon: CAL_ICON, label: 'Scrape competitor pricing' },
schedule: { label: 'Recurring, every Tuesday at 6:00 AM' },
nextRun: { label: 'In 6 days' },
lastRun: { label: '1 week ago' },
},
},
]
/**
* Static landing preview of the Scheduled Tasks workspace page.
*/
export function LandingPreviewScheduledTasks() {
return (
<LandingPreviewResource
icon={Calendar}
title='Scheduled Tasks'
createLabel='New scheduled task'
searchPlaceholder='Search scheduled tasks...'
columns={COLUMNS}
rows={ROWS}
/>
)
}

View File

@@ -10,14 +10,25 @@ import {
Settings,
Table,
} from '@/components/emcn/icons'
import { cn } from '@/lib/core/utils/cn'
import type { PreviewWorkflow } from '@/app/(home)/components/landing-preview/components/landing-preview-workflow/workflow-data'
export type SidebarView =
| 'home'
| 'workflow'
| 'tables'
| 'files'
| 'knowledge'
| 'logs'
| 'scheduled-tasks'
interface LandingPreviewSidebarProps {
workflows: PreviewWorkflow[]
activeWorkflowId: string
activeView: 'home' | 'workflow'
activeView: SidebarView
onSelectWorkflow: (id: string) => void
onSelectHome: () => void
onSelectNav: (id: SidebarView) => void
}
/**
@@ -39,7 +50,7 @@ const C = {
const WORKSPACE_NAV = [
{ id: 'tables', label: 'Tables', icon: Table },
{ id: 'files', label: 'Files', icon: File },
{ id: 'knowledge-base', label: 'Knowledge Base', icon: Database },
{ id: 'knowledge', label: 'Knowledge Base', icon: Database },
{ id: 'scheduled-tasks', label: 'Scheduled Tasks', icon: Calendar },
{ id: 'logs', label: 'Logs', icon: Library },
] as const
@@ -49,20 +60,42 @@ const FOOTER_NAV = [
{ id: 'settings', label: 'Settings', icon: Settings },
] as const
function StaticNavItem({
function NavItem({
icon: Icon,
label,
isActive,
onClick,
}: {
icon: React.ComponentType<{ className?: string; style?: React.CSSProperties }>
label: string
isActive?: boolean
onClick?: () => void
}) {
if (!onClick) {
return (
<div className='pointer-events-none mx-0.5 flex h-[28px] items-center gap-2 rounded-[8px] px-2'>
<Icon className='h-[14px] w-[14px] flex-shrink-0' style={{ color: C.TEXT_ICON }} />
<span className='truncate text-[13px]' style={{ color: C.TEXT_BODY, fontWeight: 450 }}>
{label}
</span>
</div>
)
}
return (
<div className='pointer-events-none mx-[2px] flex h-[28px] items-center gap-[8px] rounded-[8px] px-[8px]'>
<button
type='button'
onClick={onClick}
className={cn(
'mx-0.5 flex h-[28px] items-center gap-2 rounded-[8px] px-2 transition-colors hover-hover:bg-[var(--c-active)]',
isActive && 'bg-[var(--c-active)]'
)}
>
<Icon className='h-[14px] w-[14px] flex-shrink-0' style={{ color: C.TEXT_ICON }} />
<span className='truncate text-[13px]' style={{ color: C.TEXT_BODY, fontWeight: 450 }}>
{label}
</span>
</div>
</button>
)
}
@@ -77,18 +110,21 @@ export function LandingPreviewSidebar({
activeView,
onSelectWorkflow,
onSelectHome,
onSelectNav,
}: LandingPreviewSidebarProps) {
const isHomeActive = activeView === 'home'
return (
<div
className='flex h-full w-[248px] flex-shrink-0 flex-col pt-[12px]'
style={{ backgroundColor: C.SURFACE_1 }}
className='flex h-full w-[248px] flex-shrink-0 flex-col pt-3'
style={
{ backgroundColor: C.SURFACE_1, '--c-active': C.SURFACE_ACTIVE } as React.CSSProperties
}
>
{/* Workspace Header */}
<div className='flex-shrink-0 px-[10px]'>
<div className='flex-shrink-0 px-2.5'>
<div
className='pointer-events-none flex h-[32px] w-full items-center gap-[8px] rounded-[8px] border pr-[8px] pl-[5px]'
className='pointer-events-none flex h-[32px] w-full items-center gap-2 rounded-[8px] border pr-2 pl-[5px]'
style={{ borderColor: C.BORDER, backgroundColor: C.SURFACE_2 }}
>
<div className='flex h-[20px] w-[20px] flex-shrink-0 items-center justify-center rounded-[4px] bg-white'>
@@ -112,51 +148,53 @@ export function LandingPreviewSidebar({
</div>
{/* Top Navigation: Home (interactive), Search (static) */}
<div className='mt-[10px] flex flex-shrink-0 flex-col gap-[2px] px-[8px]'>
<div className='mt-2.5 flex flex-shrink-0 flex-col gap-0.5 px-2'>
<button
type='button'
onClick={onSelectHome}
className='mx-[2px] flex h-[28px] items-center gap-[8px] rounded-[8px] px-[8px] transition-colors'
style={{ backgroundColor: isHomeActive ? C.SURFACE_ACTIVE : 'transparent' }}
onMouseEnter={(e) => {
if (!isHomeActive) e.currentTarget.style.backgroundColor = C.SURFACE_ACTIVE
}}
onMouseLeave={(e) => {
if (!isHomeActive) e.currentTarget.style.backgroundColor = 'transparent'
}}
className={cn(
'mx-0.5 flex h-[28px] items-center gap-2 rounded-[8px] px-2 transition-colors hover-hover:bg-[var(--c-active)]',
isHomeActive && 'bg-[var(--c-active)]'
)}
>
<Home className='h-[14px] w-[14px] flex-shrink-0' style={{ color: C.TEXT_ICON }} />
<span className='truncate text-[13px]' style={{ color: C.TEXT_BODY, fontWeight: 450 }}>
Home
</span>
</button>
<StaticNavItem icon={Search} label='Search' />
<NavItem icon={Search} label='Search' />
</div>
{/* Workspace */}
<div className='mt-[14px] flex flex-shrink-0 flex-col'>
<div className='px-[16px] pb-[6px]'>
<div className='mt-3.5 flex flex-shrink-0 flex-col'>
<div className='px-4 pb-1.5'>
<div className='font-base text-[12px]' style={{ color: C.TEXT_ICON }}>
Workspace
</div>
</div>
<div className='flex flex-col gap-[2px] px-[8px]'>
<div className='flex flex-col gap-0.5 px-2'>
{WORKSPACE_NAV.map((item) => (
<StaticNavItem key={item.id} icon={item.icon} label={item.label} />
<NavItem
key={item.id}
icon={item.icon}
label={item.label}
isActive={activeView === item.id}
onClick={() => onSelectNav(item.id)}
/>
))}
</div>
</div>
{/* Scrollable Tasks + Workflows */}
<div className='flex flex-1 flex-col overflow-y-auto overflow-x-hidden pt-[14px]'>
<div className='flex flex-1 flex-col overflow-y-auto overflow-x-hidden pt-3.5'>
{/* Workflows */}
<div className='flex flex-col'>
<div className='px-[16px]'>
<div className='px-4'>
<div className='font-base text-[12px]' style={{ color: C.TEXT_ICON }}>
Workflows
</div>
</div>
<div className='mt-[6px] flex flex-col gap-[2px] px-[8px]'>
<div className='mt-1.5 flex flex-col gap-0.5 px-2'>
{workflows.map((workflow) => {
const isActive = activeView === 'workflow' && workflow.id === activeWorkflowId
return (
@@ -164,14 +202,10 @@ export function LandingPreviewSidebar({
key={workflow.id}
type='button'
onClick={() => onSelectWorkflow(workflow.id)}
className='group mx-[2px] flex h-[28px] w-full items-center gap-[8px] rounded-[8px] px-[8px] transition-colors'
style={{ backgroundColor: isActive ? C.SURFACE_ACTIVE : 'transparent' }}
onMouseEnter={(e) => {
if (!isActive) e.currentTarget.style.backgroundColor = C.SURFACE_ACTIVE
}}
onMouseLeave={(e) => {
if (!isActive) e.currentTarget.style.backgroundColor = 'transparent'
}}
className={cn(
'mx-0.5 flex h-[28px] w-full items-center gap-2 rounded-[8px] px-2 transition-colors hover-hover:bg-[#363636]',
isActive && 'bg-[#363636]'
)}
>
<div
className='h-[14px] w-[14px] flex-shrink-0 rounded-[4px] border-[2.5px]'
@@ -195,9 +229,9 @@ export function LandingPreviewSidebar({
</div>
{/* Footer */}
<div className='flex flex-shrink-0 flex-col gap-[2px] px-[8px] pt-[9px] pb-[8px]'>
<div className='flex flex-shrink-0 flex-col gap-0.5 px-2 pt-[9px] pb-2'>
{FOOTER_NAV.map((item) => (
<StaticNavItem key={item.id} icon={item.icon} label={item.label} />
<NavItem key={item.id} icon={item.icon} label={item.label} />
))}
</div>
</div>

View File

@@ -0,0 +1,552 @@
'use client'
import { useState } from 'react'
import { Checkbox } from '@/components/emcn'
import {
ChevronDown,
Columns3,
Rows3,
Table,
TypeBoolean,
TypeNumber,
TypeText,
} from '@/components/emcn/icons'
import { cn } from '@/lib/core/utils/cn'
import type {
PreviewColumn,
PreviewRow,
} from '@/app/(home)/components/landing-preview/components/landing-preview-resource/landing-preview-resource'
import {
LandingPreviewResource,
ownerCell,
} from '@/app/(home)/components/landing-preview/components/landing-preview-resource/landing-preview-resource'
const CELL = 'border-[var(--border)] border-r border-b px-2 py-[7px] align-middle select-none'
const CELL_CHECKBOX =
'border-[var(--border)] border-r border-b px-1 py-[7px] align-middle select-none'
const CELL_HEADER =
'border-[var(--border)] border-r border-b bg-[var(--bg)] p-0 text-left align-middle'
const CELL_HEADER_CHECKBOX =
'border-[var(--border)] border-r border-b bg-[var(--bg)] px-1 py-[7px] text-center align-middle'
const CELL_CONTENT =
'relative min-h-[20px] min-w-0 overflow-clip text-ellipsis whitespace-nowrap text-small'
const SELECTION_OVERLAY =
'pointer-events-none absolute -top-px -right-px -bottom-px -left-px z-[5] border-[2px] border-[var(--selection)]'
const LIST_COLUMNS: PreviewColumn[] = [
{ id: 'name', header: 'Name' },
{ id: 'columns', header: 'Columns' },
{ id: 'rows', header: 'Rows' },
{ id: 'created', header: 'Created' },
{ id: 'owner', header: 'Owner' },
]
const TABLE_METAS: Record<string, string> = {
'1': 'Customer Leads',
'2': 'Product Catalog',
'3': 'Campaign Analytics',
'4': 'User Profiles',
'5': 'Invoice Records',
}
const TABLE_ICON = <Table className='h-[14px] w-[14px]' />
const COLUMNS_ICON = <Columns3 className='h-[14px] w-[14px]' />
const ROWS_ICON = <Rows3 className='h-[14px] w-[14px]' />
const LIST_ROWS: PreviewRow[] = [
{
id: '1',
cells: {
name: { icon: TABLE_ICON, label: 'Customer Leads' },
columns: { icon: COLUMNS_ICON, label: '8' },
rows: { icon: ROWS_ICON, label: '2,847' },
created: { label: '2 days ago' },
owner: ownerCell('S', 'Sarah K.'),
},
},
{
id: '2',
cells: {
name: { icon: TABLE_ICON, label: 'Product Catalog' },
columns: { icon: COLUMNS_ICON, label: '12' },
rows: { icon: ROWS_ICON, label: '1,203' },
created: { label: '5 days ago' },
owner: ownerCell('A', 'Alex M.'),
},
},
{
id: '3',
cells: {
name: { icon: TABLE_ICON, label: 'Campaign Analytics' },
columns: { icon: COLUMNS_ICON, label: '6' },
rows: { icon: ROWS_ICON, label: '534' },
created: { label: '1 week ago' },
owner: ownerCell('W', 'Emaan K.'),
},
},
{
id: '4',
cells: {
name: { icon: TABLE_ICON, label: 'User Profiles' },
columns: { icon: COLUMNS_ICON, label: '15' },
rows: { icon: ROWS_ICON, label: '18,492' },
created: { label: '2 weeks ago' },
owner: ownerCell('J', 'Jordan P.'),
},
},
{
id: '5',
cells: {
name: { icon: TABLE_ICON, label: 'Invoice Records' },
columns: { icon: COLUMNS_ICON, label: '9' },
rows: { icon: ROWS_ICON, label: '742' },
created: { label: 'March 15th, 2026' },
owner: ownerCell('S', 'Sarah K.'),
},
},
]
interface SpreadsheetColumn {
id: string
label: string
type: 'text' | 'number' | 'boolean'
width: number
}
interface SpreadsheetRow {
id: string
cells: Record<string, string>
}
const COLUMN_TYPE_ICONS = {
text: TypeText,
number: TypeNumber,
boolean: TypeBoolean,
} as const
const SPREADSHEET_DATA: Record<string, { columns: SpreadsheetColumn[]; rows: SpreadsheetRow[] }> = {
'1': {
columns: [
{ id: 'name', label: 'Name', type: 'text', width: 160 },
{ id: 'email', label: 'Email', type: 'text', width: 200 },
{ id: 'company', label: 'Company', type: 'text', width: 160 },
{ id: 'score', label: 'Score', type: 'number', width: 100 },
{ id: 'qualified', label: 'Qualified', type: 'boolean', width: 120 },
],
rows: [
{
id: '1',
cells: {
name: 'Alice Johnson',
email: 'alice@acme.com',
company: 'Acme Corp',
score: '87',
qualified: 'true',
},
},
{
id: '2',
cells: {
name: 'Bob Williams',
email: 'bob@techco.io',
company: 'TechCo',
score: '62',
qualified: 'false',
},
},
{
id: '3',
cells: {
name: 'Carol Davis',
email: 'carol@startup.co',
company: 'StartupCo',
score: '94',
qualified: 'true',
},
},
{
id: '4',
cells: {
name: 'Dan Miller',
email: 'dan@bigcorp.com',
company: 'BigCorp',
score: '71',
qualified: 'true',
},
},
{
id: '5',
cells: {
name: 'Eva Chen',
email: 'eva@design.io',
company: 'Design IO',
score: '45',
qualified: 'false',
},
},
{
id: '6',
cells: {
name: 'Frank Lee',
email: 'frank@ventures.co',
company: 'Ventures',
score: '88',
qualified: 'true',
},
},
],
},
'2': {
columns: [
{ id: 'sku', label: 'SKU', type: 'text', width: 120 },
{ id: 'name', label: 'Product Name', type: 'text', width: 200 },
{ id: 'price', label: 'Price', type: 'number', width: 100 },
{ id: 'stock', label: 'In Stock', type: 'number', width: 120 },
{ id: 'active', label: 'Active', type: 'boolean', width: 90 },
],
rows: [
{
id: '1',
cells: {
sku: 'PRD-001',
name: 'Wireless Headphones',
price: '79.99',
stock: '234',
active: 'true',
},
},
{
id: '2',
cells: { sku: 'PRD-002', name: 'USB-C Hub', price: '49.99', stock: '89', active: 'true' },
},
{
id: '3',
cells: {
sku: 'PRD-003',
name: 'Laptop Stand',
price: '39.99',
stock: '0',
active: 'false',
},
},
{
id: '4',
cells: {
sku: 'PRD-004',
name: 'Mechanical Keyboard',
price: '129.99',
stock: '52',
active: 'true',
},
},
{
id: '5',
cells: { sku: 'PRD-005', name: 'Webcam HD', price: '89.99', stock: '17', active: 'true' },
},
{
id: '6',
cells: {
sku: 'PRD-006',
name: 'Mouse Pad XL',
price: '24.99',
stock: '0',
active: 'false',
},
},
],
},
'3': {
columns: [
{ id: 'campaign', label: 'Campaign', type: 'text', width: 180 },
{ id: 'clicks', label: 'Clicks', type: 'number', width: 100 },
{ id: 'conversions', label: 'Conversions', type: 'number', width: 140 },
{ id: 'spend', label: 'Spend ($)', type: 'number', width: 130 },
{ id: 'active', label: 'Active', type: 'boolean', width: 90 },
],
rows: [
{
id: '1',
cells: {
campaign: 'Spring Sale 2026',
clicks: '12,847',
conversions: '384',
spend: '2,400',
active: 'true',
},
},
{
id: '2',
cells: {
campaign: 'Email Reactivation',
clicks: '3,201',
conversions: '97',
spend: '450',
active: 'false',
},
},
{
id: '3',
cells: {
campaign: 'Referral Program',
clicks: '8,923',
conversions: '210',
spend: '1,100',
active: 'true',
},
},
{
id: '4',
cells: {
campaign: 'Product Launch',
clicks: '24,503',
conversions: '891',
spend: '5,800',
active: 'true',
},
},
{
id: '5',
cells: {
campaign: 'Retargeting Q1',
clicks: '6,712',
conversions: '143',
spend: '980',
active: 'false',
},
},
],
},
'4': {
columns: [
{ id: 'username', label: 'Username', type: 'text', width: 140 },
{ id: 'email', label: 'Email', type: 'text', width: 200 },
{ id: 'plan', label: 'Plan', type: 'text', width: 120 },
{ id: 'seats', label: 'Seats', type: 'number', width: 100 },
{ id: 'active', label: 'Active', type: 'boolean', width: 100 },
],
rows: [
{
id: '1',
cells: {
username: 'alice_j',
email: 'alice@acme.com',
plan: 'Pro',
seats: '5',
active: 'true',
},
},
{
id: '2',
cells: {
username: 'bobw',
email: 'bob@techco.io',
plan: 'Starter',
seats: '1',
active: 'true',
},
},
{
id: '3',
cells: {
username: 'carol_d',
email: 'carol@startup.co',
plan: 'Enterprise',
seats: '25',
active: 'true',
},
},
{
id: '4',
cells: {
username: 'dan.m',
email: 'dan@bigcorp.com',
plan: 'Pro',
seats: '10',
active: 'false',
},
},
{
id: '5',
cells: {
username: 'eva_chen',
email: 'eva@design.io',
plan: 'Starter',
seats: '1',
active: 'true',
},
},
{
id: '6',
cells: {
username: 'frank_lee',
email: 'frank@ventures.co',
plan: 'Enterprise',
seats: '50',
active: 'true',
},
},
],
},
'5': {
columns: [
{ id: 'invoice', label: 'Invoice #', type: 'text', width: 140 },
{ id: 'client', label: 'Client', type: 'text', width: 160 },
{ id: 'amount', label: 'Amount ($)', type: 'number', width: 130 },
{ id: 'paid', label: 'Paid', type: 'boolean', width: 80 },
],
rows: [
{
id: '1',
cells: { invoice: 'INV-2026-001', client: 'Acme Corp', amount: '4,800.00', paid: 'true' },
},
{
id: '2',
cells: { invoice: 'INV-2026-002', client: 'TechCo', amount: '1,200.00', paid: 'true' },
},
{
id: '3',
cells: { invoice: 'INV-2026-003', client: 'StartupCo', amount: '750.00', paid: 'false' },
},
{
id: '4',
cells: { invoice: 'INV-2026-004', client: 'BigCorp', amount: '12,500.00', paid: 'true' },
},
{
id: '5',
cells: { invoice: 'INV-2026-005', client: 'Design IO', amount: '3,300.00', paid: 'false' },
},
],
},
}
interface SpreadsheetViewProps {
tableId: string
tableName: string
onBack: () => void
}
function SpreadsheetView({ tableId, tableName, onBack }: SpreadsheetViewProps) {
const data = SPREADSHEET_DATA[tableId] ?? SPREADSHEET_DATA['1']
const [selectedCell, setSelectedCell] = useState<{ row: string; col: string } | null>(null)
return (
<div className='flex h-full flex-1 flex-col overflow-hidden bg-[var(--bg)]'>
{/* Breadcrumb header — matches real ResourceHeader breadcrumb layout */}
<div className='border-[var(--border)] border-b px-4 py-[8.5px]'>
<div className='flex items-center gap-3'>
<button
type='button'
onClick={onBack}
className='inline-flex items-center px-2 py-1 font-medium text-[var(--text-secondary)] text-sm transition-colors hover-hover:text-[var(--text-body)]'
>
<Table className='mr-3 h-[14px] w-[14px] text-[var(--text-icon)]' />
Tables
</button>
<span className='select-none text-[var(--text-icon)] text-sm'>/</span>
<span className='inline-flex items-center px-2 py-1 font-medium text-[var(--text-body)] text-sm'>
{tableName}
<ChevronDown className='ml-2 h-[7px] w-[9px] shrink-0 text-[var(--text-muted)]' />
</span>
</div>
</div>
{/* Spreadsheet — matches exact real table editor structure */}
<div className='min-h-0 flex-1 overflow-auto overscroll-none'>
<table className='table-fixed border-separate border-spacing-0 text-small'>
<colgroup>
<col style={{ width: 40 }} />
{data.columns.map((col) => (
<col key={col.id} style={{ width: col.width }} />
))}
</colgroup>
<thead className='sticky top-0 z-10'>
<tr>
<th className={CELL_HEADER_CHECKBOX} />
{data.columns.map((col) => {
const Icon = COLUMN_TYPE_ICONS[col.type] ?? TypeText
return (
<th key={col.id} className={CELL_HEADER}>
<div className='flex h-full w-full min-w-0 items-center px-2 py-[7px]'>
<Icon className='h-3 w-3 shrink-0 text-[var(--text-icon)]' />
<span className='ml-1.5 min-w-0 overflow-clip text-ellipsis whitespace-nowrap font-medium text-[var(--text-primary)] text-small'>
{col.label}
</span>
<ChevronDown className='ml-auto h-[7px] w-[9px] shrink-0 text-[var(--text-muted)]' />
</div>
</th>
)
})}
</tr>
</thead>
<tbody>
{data.rows.map((row, rowIdx) => (
<tr key={row.id}>
<td className={cn(CELL_CHECKBOX, 'text-center')}>
<span className='text-[var(--text-tertiary)] text-xs tabular-nums'>
{rowIdx + 1}
</span>
</td>
{data.columns.map((col) => {
const isSelected = selectedCell?.row === row.id && selectedCell?.col === col.id
const cellValue = row.cells[col.id] ?? ''
return (
<td
key={col.id}
onClick={() => setSelectedCell({ row: row.id, col: col.id })}
className={cn(
CELL,
'relative cursor-default text-[var(--text-body)]',
isSelected && 'bg-[rgba(37,99,235,0.06)]'
)}
>
{isSelected && <div className={SELECTION_OVERLAY} />}
<div className={CELL_CONTENT}>
{col.type === 'boolean' ? (
<div className='flex min-h-[20px] items-center justify-center'>
<Checkbox
size='sm'
checked={cellValue === 'true'}
className='pointer-events-none'
/>
</div>
) : (
cellValue
)}
</div>
</td>
)
})}
</tr>
))}
</tbody>
</table>
</div>
</div>
)
}
export function LandingPreviewTables() {
const [openTableId, setOpenTableId] = useState<string | null>(null)
if (openTableId !== null) {
return (
<SpreadsheetView
tableId={openTableId}
tableName={TABLE_METAS[openTableId] ?? 'Table'}
onBack={() => setOpenTableId(null)}
/>
)
}
return (
<LandingPreviewResource
icon={Table}
title='Tables'
createLabel='New table'
searchPlaceholder='Search tables...'
columns={LIST_COLUMNS}
rows={LIST_ROWS}
onRowClick={(id) => setOpenTableId(id)}
/>
)
}

View File

@@ -137,7 +137,7 @@ function PreviewFlow({ workflow, animate = false, fitViewOptions }: LandingPrevi
proOptions={PRO_OPTIONS}
fitView
fitViewOptions={resolvedFitViewOptions}
className='h-full w-full bg-[#1b1b1b]'
className='h-full w-full bg-[var(--landing-bg)]'
/>
)
}

View File

@@ -150,10 +150,10 @@ export const PreviewBlockNode = memo(function PreviewBlockNode({
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]'>
<div className='border-[#3d3d3d] border-b p-2'>
<span className='font-medium text-[#e6e6e6] text-[16px]'>Note</span>
</div>
<div className='p-[10px]'>
<div className='p-2.5'>
<NoteMarkdown content={markdown} />
</div>
</div>
@@ -186,9 +186,9 @@ export const PreviewBlockNode = memo(function PreviewBlockNode({
{/* Header */}
<div
className={`flex items-center justify-between p-[8px] ${hasContent ? 'border-[#3d3d3d] border-b' : ''}`}
className={`flex items-center justify-between p-2 ${hasContent ? 'border-[#3d3d3d] border-b' : ''}`}
>
<div className='relative z-10 flex min-w-0 flex-1 items-center gap-[10px]'>
<div className='relative z-10 flex min-w-0 flex-1 items-center gap-2.5'>
<div
className='flex h-[24px] w-[24px] flex-shrink-0 items-center justify-center rounded-[6px]'
style={{ background: bgColor }}
@@ -201,17 +201,17 @@ export const PreviewBlockNode = memo(function PreviewBlockNode({
{/* Sub-block rows + tools */}
{hasContent && (
<div className='flex flex-col gap-[8px] p-[8px]'>
<div className='flex flex-col gap-2 p-2'>
{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]'>
<div key={row.title} className='flex items-center gap-2'>
<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]'>
<span className='flex min-w-0 flex-1 items-center justify-end gap-2 font-normal text-[#e6e6e6] text-[14px]'>
{ModelIcon && (
<ModelIcon
className={`inline-block flex-shrink-0 text-[#e6e6e6] ${modelEntry.size ?? 'h-[14px] w-[14px]'}`}
@@ -226,7 +226,7 @@ export const PreviewBlockNode = memo(function PreviewBlockNode({
{/* Tool chips — inline with label */}
{tools && tools.length > 0 && (
<div className='flex items-center gap-[8px]'>
<div className='flex items-center gap-2'>
<span className='flex-shrink-0 font-normal text-[#b3b3b3] text-[14px]'>Tools</span>
<div className='flex flex-1 flex-wrap items-center justify-end gap-[5px]'>
{tools.map((tool) => {
@@ -277,13 +277,13 @@ function NoteMarkdown({ content }: { content: string }) {
const lines = content.split('\n')
return (
<div className='flex flex-col gap-[4px]'>
<div className='flex flex-col gap-1'>
{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' />
return <hr key={i} className='my-1 border-[#3d3d3d] border-t' />
}
if (trimmed.startsWith('### ')) {

View File

@@ -127,6 +127,60 @@ const SELF_HEALING_CRM_WORKFLOW: PreviewWorkflow = {
edges: [{ id: 'e-3', source: 'schedule-1', target: 'mothership-1' }],
}
/**
* Customer Support Agent workflow — Gmail Trigger -> Agent (KB + Notion tools) -> Slack
*/
const CUSTOMER_SUPPORT_WORKFLOW: PreviewWorkflow = {
id: 'wf-customer-support',
name: 'Customer Support Agent',
color: '#0EA5E9',
blocks: [
{
id: 'gmail-1',
name: 'Gmail',
type: 'gmail',
bgColor: '#E0E0E0',
rows: [
{ title: 'Event', value: 'New Email' },
{ title: 'Label', value: 'Support' },
],
position: { x: 80, y: 140 },
hideTargetHandle: true,
},
{
id: 'agent-3',
name: 'Support Agent',
type: 'agent',
bgColor: '#701ffc',
rows: [
{ title: 'Model', value: 'gpt-5.4' },
{ title: 'System Prompt', value: 'Resolve customer issues...' },
],
tools: [
{ name: 'Knowledge', type: 'knowledge_base', bgColor: '#10B981' },
{ name: 'Notion', type: 'notion', bgColor: '#181C1E' },
],
position: { x: 420, y: 40 },
},
{
id: 'slack-3',
name: 'Slack',
type: 'slack',
bgColor: '#611f69',
rows: [
{ title: 'Channel', value: '#support' },
{ title: 'Operation', value: 'Send Message' },
],
position: { x: 420, y: 260 },
hideSourceHandle: true,
},
],
edges: [
{ id: 'e-cs-1', source: 'gmail-1', target: 'agent-3' },
{ id: 'e-cs-2', source: 'gmail-1', target: 'slack-3' },
],
}
/**
* Empty "New Agent" workflow — a single note prompting the user to start building
*/
@@ -153,6 +207,7 @@ const NEW_AGENT_WORKFLOW: PreviewWorkflow = {
export const PREVIEW_WORKFLOWS: PreviewWorkflow[] = [
SELF_HEALING_CRM_WORKFLOW,
IT_SERVICE_WORKFLOW,
CUSTOMER_SUPPORT_WORKFLOW,
NEW_AGENT_WORKFLOW,
]

View File

@@ -2,9 +2,15 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { motion, type Variants } from 'framer-motion'
import { LandingPreviewFiles } from '@/app/(home)/components/landing-preview/components/landing-preview-files/landing-preview-files'
import { LandingPreviewHome } from '@/app/(home)/components/landing-preview/components/landing-preview-home/landing-preview-home'
import { LandingPreviewKnowledge } from '@/app/(home)/components/landing-preview/components/landing-preview-knowledge/landing-preview-knowledge'
import { LandingPreviewLogs } from '@/app/(home)/components/landing-preview/components/landing-preview-logs/landing-preview-logs'
import { LandingPreviewPanel } from '@/app/(home)/components/landing-preview/components/landing-preview-panel/landing-preview-panel'
import { LandingPreviewScheduledTasks } from '@/app/(home)/components/landing-preview/components/landing-preview-scheduled-tasks/landing-preview-scheduled-tasks'
import type { SidebarView } from '@/app/(home)/components/landing-preview/components/landing-preview-sidebar/landing-preview-sidebar'
import { LandingPreviewSidebar } from '@/app/(home)/components/landing-preview/components/landing-preview-sidebar/landing-preview-sidebar'
import { LandingPreviewTables } from '@/app/(home)/components/landing-preview/components/landing-preview-tables/landing-preview-tables'
import { LandingPreviewWorkflow } from '@/app/(home)/components/landing-preview/components/landing-preview-workflow/landing-preview-workflow'
import {
EASE_OUT,
@@ -46,18 +52,16 @@ const panelVariants: Variants = {
* Interactive workspace preview for the hero section.
*
* Renders a lightweight replica of the Sim workspace with:
* - A sidebar with two selectable workflows
* - A sidebar with selectable workflows and workspace nav items
* - A ReactFlow canvas showing the active workflow's blocks and edges
* - Static previews of Tables, Files, Knowledge Base, Logs, and Scheduled Tasks
* - 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.
* Only workflow items, the home button, workspace nav items, and the copilot input
* are interactive. Animations only fire on initial load.
*/
export function LandingPreview() {
const [activeView, setActiveView] = useState<'home' | 'workflow'>('workflow')
const [activeView, setActiveView] = useState<SidebarView>('workflow')
const [activeWorkflowId, setActiveWorkflowId] = useState(PREVIEW_WORKFLOWS[0].id)
const isInitialMount = useRef(true)
@@ -74,14 +78,37 @@ export function LandingPreview() {
setActiveView('home')
}, [])
const handleSelectNav = useCallback((id: SidebarView) => {
setActiveView(id)
}, [])
const activeWorkflow =
PREVIEW_WORKFLOWS.find((w) => w.id === activeWorkflowId) ?? PREVIEW_WORKFLOWS[0]
const isWorkflowView = activeView === 'workflow'
function renderContent() {
switch (activeView) {
case 'workflow':
return <LandingPreviewWorkflow workflow={activeWorkflow} animate={isInitialMount.current} />
case 'home':
return <LandingPreviewHome />
case 'tables':
return <LandingPreviewTables />
case 'files':
return <LandingPreviewFiles />
case 'knowledge':
return <LandingPreviewKnowledge />
case 'logs':
return <LandingPreviewLogs />
case 'scheduled-tasks':
return <LandingPreviewScheduledTasks />
}
}
return (
<motion.div
className='dark flex aspect-[1116/549] w-full overflow-hidden rounded bg-[#1e1e1e] antialiased'
className='dark flex aspect-[1116/549] w-full overflow-hidden rounded bg-[var(--landing-bg-surface)] antialiased'
initial='hidden'
animate='visible'
variants={containerVariants}
@@ -93,10 +120,11 @@ export function LandingPreview() {
activeView={activeView}
onSelectWorkflow={handleSelectWorkflow}
onSelectHome={handleSelectHome}
onSelectNav={handleSelectNav}
/>
</motion.div>
<div className='flex min-w-0 flex-1 flex-col py-[8px] pr-[8px] pl-[8px] lg:pl-0'>
<div className='flex flex-1 overflow-hidden rounded-[8px] border border-[#2c2c2c] bg-[#1b1b1b]'>
<div className='flex min-w-0 flex-1 flex-col py-2 pr-2 pl-2 lg:pl-0'>
<div className='flex flex-1 overflow-hidden rounded-[8px] border border-[#2c2c2c] bg-[var(--landing-bg)]'>
<div
className={
isWorkflowView
@@ -104,11 +132,7 @@ export function LandingPreview() {
: 'relative flex min-w-0 flex-1 flex-col overflow-hidden'
}
>
{isWorkflowView ? (
<LandingPreviewWorkflow workflow={activeWorkflow} animate={isInitialMount.current} />
) : (
<LandingPreviewHome />
)}
{renderContent()}
</div>
<motion.div
className={isWorkflowView ? 'hidden lg:flex' : 'hidden'}

View File

@@ -29,7 +29,7 @@ function BlogCard({
<Link
href={`/blog/${slug}`}
className={cn(
'group/card flex flex-col overflow-hidden rounded-[5px] border border-[#2A2A2A] bg-[#1C1C1C] transition-colors hover:border-[#3D3D3D] hover:bg-[#2A2A2A]',
'group/card flex flex-col overflow-hidden rounded-[5px] border border-[var(--landing-bg-elevated)] bg-[var(--landing-bg)] transition-colors hover:border-[var(--landing-border-strong)] hover:bg-[var(--landing-bg-elevated)]',
className
)}
prefetch={false}
@@ -41,11 +41,12 @@ function BlogCard({
fill
sizes={sizes}
className='object-cover transition-transform duration-200 group-hover/card:scale-[1.02]'
unoptimized
/>
</div>
<div className='flex-shrink-0 px-[10px] py-[6px]'>
<div className='flex-shrink-0 px-2.5 py-1.5'>
<span
className='font-[430] font-season text-[#cdcdcd] leading-[140%]'
className='font-[430] font-season text-[var(--landing-text-body)] leading-[140%]'
style={{ fontSize: titleSize }}
>
{title}
@@ -65,8 +66,8 @@ export function BlogDropdown({ posts }: BlogDropdownProps) {
if (!featured) return null
return (
<div className='w-[560px] rounded-[5px] border border-[#2A2A2A] bg-[#1C1C1C] p-[16px] shadow-[0_16px_48px_rgba(0,0,0,0.4)]'>
<div className='grid grid-cols-3 gap-[8px]'>
<div className='w-[560px] rounded-[5px] border border-[var(--landing-bg-elevated)] bg-[var(--landing-bg)] p-4 shadow-overlay'>
<div className='grid grid-cols-3 gap-2'>
<BlogCard
slug={featured.slug}
image={featured.ogImage}

View File

@@ -37,15 +37,15 @@ const RESOURCE_CARDS = [
export function DocsDropdown() {
return (
<div className='w-[480px] rounded-[5px] border border-[#2A2A2A] bg-[#1C1C1C] p-[16px] shadow-[0_16px_48px_rgba(0,0,0,0.4)]'>
<div className='grid grid-cols-2 gap-[10px]'>
<div className='w-[480px] rounded-[5px] border border-[var(--landing-bg-elevated)] bg-[var(--landing-bg)] p-4 shadow-overlay'>
<div className='grid grid-cols-2 gap-2.5'>
{PREVIEW_CARDS.map((card) => (
<a
key={card.title}
href={card.href}
target='_blank'
rel='noopener noreferrer'
className='group/card overflow-hidden rounded-[5px] border border-[#2A2A2A] bg-[#1C1C1C] transition-colors hover:border-[#3D3D3D] hover:bg-[#2A2A2A]'
className='group/card overflow-hidden rounded-[5px] border border-[var(--landing-bg-elevated)] bg-[var(--landing-bg)] transition-colors hover:border-[var(--landing-border-strong)] hover:bg-[var(--landing-bg-elevated)]'
>
<div className='relative h-[120px] w-full overflow-hidden bg-[#141414]'>
<Image
@@ -54,10 +54,11 @@ export function DocsDropdown() {
fill
sizes='220px'
className='scale-[1.04] object-cover transition-transform duration-200 group-hover/card:scale-[1.06]'
unoptimized
/>
</div>
<div className='px-[10px] py-[8px]'>
<span className='font-[430] font-season text-[#cdcdcd] text-[13px]'>
<div className='px-2.5 py-2'>
<span className='font-[430] font-season text-[var(--landing-text-body)] text-small'>
{card.title}
</span>
</div>
@@ -65,7 +66,7 @@ export function DocsDropdown() {
))}
</div>
<div className='mt-[8px] grid grid-cols-3 gap-[8px]'>
<div className='mt-2 grid grid-cols-3 gap-2'>
{RESOURCE_CARDS.map((card) => {
const Icon = card.icon
return (
@@ -74,15 +75,15 @@ export function DocsDropdown() {
href={card.href}
target='_blank'
rel='noopener noreferrer'
className='flex flex-col gap-[4px] rounded-[5px] border border-[#2A2A2A] px-[10px] py-[8px] transition-colors hover:border-[#3D3D3D] hover:bg-[#232323]'
className='flex flex-col gap-1 rounded-[5px] border border-[var(--landing-bg-elevated)] px-2.5 py-2 transition-colors hover:border-[var(--landing-border-strong)] hover:bg-[var(--landing-bg-card)]'
>
<div className='flex items-center gap-[6px]'>
<Icon className='h-[13px] w-[13px] flex-shrink-0 text-[#939393]' />
<span className='font-[430] font-season text-[#cdcdcd] text-[12px]'>
<div className='flex items-center gap-1.5'>
<Icon className='h-[13px] w-[13px] flex-shrink-0 text-[var(--landing-text-icon)]' />
<span className='font-[430] font-season text-[var(--landing-text-body)] text-caption'>
{card.title}
</span>
</div>
<span className='font-season text-[#939393] text-[11px] leading-[130%]'>
<span className='font-season text-[var(--landing-text-icon)] text-xs leading-[130%]'>
{card.description}
</span>
</a>

View File

@@ -3,7 +3,7 @@
import { useEffect, useState } from 'react'
import { createLogger } from '@sim/logger'
import { GithubOutlineIcon } from '@/components/icons'
import { getFormattedGitHubStars } from '@/app/(landing)/actions/github'
import { getFormattedGitHubStars } from '@/app/(home)/actions/github'
const logger = createLogger('github-stars')
@@ -31,7 +31,7 @@ export function GitHubStars() {
href='https://github.com/simstudioai/sim'
target='_blank'
rel='noopener noreferrer'
className='flex items-center gap-[8px] px-[12px]'
className='flex items-center gap-2 px-3'
aria-label={`GitHub repository — ${stars} stars`}
>
<GithubOutlineIcon className='h-[14px] w-[14px]' />

View File

@@ -32,8 +32,8 @@ const NAV_LINKS: NavLink[] = [
{ label: 'Enterprise', href: 'https://form.typeform.com/to/jqCO12pF', external: true },
]
const LOGO_CELL = 'flex items-center pl-[20px] lg:pl-[80px] pr-[20px]'
const LINK_CELL = 'flex items-center px-[14px]'
const LOGO_CELL = 'flex items-center pl-5 lg:pl-20 pr-5'
const LINK_CELL = 'flex items-center px-3.5'
interface NavbarProps {
logoOnly?: boolean
@@ -96,7 +96,7 @@ export default function Navbar({ logoOnly = false, blogPosts = [] }: NavbarProps
return (
<nav
aria-label='Primary navigation'
className='relative flex h-[52px] border-[#2A2A2A] border-b-[1px] bg-[#1C1C1C] font-[430] font-season text-[#ECECEC] text-[14px]'
className='relative flex h-[52px] border-[var(--landing-bg-elevated)] border-b-[1px] bg-[var(--landing-bg)] font-[430] font-season text-[var(--landing-text)] text-sm'
itemScope
itemType='https://schema.org/SiteNavigationElement'
>
@@ -138,9 +138,9 @@ export default function Navbar({ logoOnly = false, blogPosts = [] }: NavbarProps
const isHighlighted = isActive || isThisHovered
const isDimmed = anyHighlighted && !isHighlighted
const linkClass = cn(
icon ? `${LINK_CELL} gap-[8px]` : LINK_CELL,
icon ? `${LINK_CELL} gap-2` : LINK_CELL,
'transition-colors duration-200',
isDimmed && 'text-[#F6F6F6]/60'
isDimmed && 'text-[color-mix(in_srgb,var(--landing-text-subtle)_60%,transparent)]'
)
const chevron = icon === 'chevron' && <NavChevron open={isActive} />
@@ -152,19 +152,26 @@ export default function Navbar({ logoOnly = false, blogPosts = [] }: NavbarProps
onMouseEnter={() => openDropdown(dropdown)}
onMouseLeave={scheduleClose}
>
<button
type='button'
className={cn(linkClass, 'h-full cursor-pointer')}
aria-expanded={isActive}
aria-haspopup='true'
>
{label}
{chevron}
</button>
{external ? (
<a
href={href}
target='_blank'
rel='noopener noreferrer'
className={cn(linkClass, 'h-full cursor-pointer')}
>
{label}
{chevron}
</a>
) : (
<Link href={href} className={cn(linkClass, 'h-full cursor-pointer')}>
{label}
{chevron}
</Link>
)}
<div
className={cn(
'-mt-[2px] absolute top-full left-0 z-50',
'-mt-0.5 absolute top-full left-0 z-50',
isActive
? 'pointer-events-auto opacity-100'
: 'pointer-events-none opacity-0'
@@ -218,14 +225,14 @@ export default function Navbar({ logoOnly = false, blogPosts = [] }: NavbarProps
<div
className={cn(
'hidden items-center gap-[8px] pr-[80px] pl-[20px] lg:flex',
'hidden items-center gap-2 pr-20 pl-5 lg:flex',
isSessionPending && 'invisible'
)}
>
{isAuthenticated ? (
<Link
href='/workspace'
className='inline-flex h-[30px] items-center gap-[7px] rounded-[5px] border border-[#FFFFFF] bg-[#FFFFFF] px-[9px] text-[13.5px] text-black transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
className='inline-flex h-[30px] items-center gap-[7px] rounded-[5px] border border-[var(--white)] bg-[var(--white)] px-[9px] text-[13.5px] text-black transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
aria-label='Go to app'
>
Go to App
@@ -234,14 +241,14 @@ export default function Navbar({ logoOnly = false, blogPosts = [] }: NavbarProps
<>
<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]'
className='inline-flex h-[30px] items-center rounded-[5px] border border-[var(--landing-border-strong)] px-[9px] text-[13.5px] text-[var(--landing-text)] transition-colors hover:bg-[var(--landing-bg-elevated)]'
aria-label='Log in'
>
Log in
</Link>
<Link
href='/signup'
className='inline-flex h-[30px] items-center gap-[7px] rounded-[5px] border border-[#FFFFFF] bg-[#FFFFFF] px-[9px] text-[13.5px] text-black transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
className='inline-flex h-[30px] items-center gap-[7px] rounded-[5px] border border-[var(--white)] bg-[var(--white)] px-2.5 text-[13.5px] text-black transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
aria-label='Get started with Sim'
>
Get started
@@ -250,10 +257,10 @@ export default function Navbar({ logoOnly = false, blogPosts = [] }: NavbarProps
)}
</div>
<div className='flex flex-1 items-center justify-end pr-[20px] lg:hidden'>
<div className='flex flex-1 items-center justify-end pr-5 lg:hidden'>
<button
type='button'
className='flex h-[32px] w-[32px] items-center justify-center rounded-[5px] transition-colors hover:bg-[#2A2A2A]'
className='flex h-[32px] w-[32px] items-center justify-center rounded-[5px] transition-colors hover:bg-[var(--landing-bg-elevated)]'
onClick={() => setMobileMenuOpen((prev) => !prev)}
aria-label={mobileMenuOpen ? 'Close menu' : 'Open menu'}
aria-expanded={mobileMenuOpen}
@@ -264,7 +271,7 @@ export default function Navbar({ logoOnly = false, blogPosts = [] }: NavbarProps
<div
className={cn(
'fixed inset-x-0 top-[52px] bottom-0 z-50 flex flex-col overflow-y-auto bg-[#1C1C1C] font-[430] font-season text-[14px] transition-all duration-200 lg:hidden',
'fixed inset-x-0 top-[52px] bottom-0 z-50 flex flex-col overflow-y-auto bg-[var(--landing-bg)] font-[430] font-season text-sm transition-all duration-200 lg:hidden',
mobileMenuOpen ? 'visible opacity-100' : 'invisible opacity-0'
)}
>
@@ -273,13 +280,13 @@ export default function Navbar({ logoOnly = false, blogPosts = [] }: NavbarProps
const href =
useHomeLinks && rawHref.startsWith('/#') ? `/?home${rawHref.slice(1)}` : rawHref
return (
<li key={label} className='border-[#2A2A2A] border-b'>
<li key={label} className='border-[var(--landing-border)] border-b'>
{external ? (
<a
href={href}
target='_blank'
rel='noopener noreferrer'
className='flex items-center justify-between px-[20px] py-[14px] text-[#ECECEC] transition-colors active:bg-[#2A2A2A]'
className='flex items-center justify-between px-5 py-3.5 text-[var(--landing-text)] transition-colors active:bg-[var(--landing-bg-elevated)]'
onClick={() => setMobileMenuOpen(false)}
>
{label}
@@ -288,7 +295,7 @@ export default function Navbar({ logoOnly = false, blogPosts = [] }: NavbarProps
) : (
<Link
href={href}
className='flex items-center px-[20px] py-[14px] text-[#ECECEC] transition-colors active:bg-[#2A2A2A]'
className='flex items-center px-5 py-3.5 text-[var(--landing-text)] transition-colors active:bg-[var(--landing-bg-elevated)]'
onClick={() => setMobileMenuOpen(false)}
>
{label}
@@ -297,12 +304,12 @@ export default function Navbar({ logoOnly = false, blogPosts = [] }: NavbarProps
</li>
)
})}
<li className='border-[#2A2A2A] border-b'>
<li className='border-[var(--landing-border)] border-b'>
<a
href='https://github.com/simstudioai/sim'
target='_blank'
rel='noopener noreferrer'
className='flex items-center gap-[8px] px-[20px] py-[14px] text-[#ECECEC] transition-colors active:bg-[#2A2A2A]'
className='flex items-center gap-2 px-5 py-3.5 text-[var(--landing-text)] transition-colors active:bg-[var(--landing-bg-elevated)]'
onClick={() => setMobileMenuOpen(false)}
>
<GithubOutlineIcon className='h-[14px] w-[14px]' />
@@ -312,15 +319,12 @@ export default function Navbar({ logoOnly = false, blogPosts = [] }: NavbarProps
</ul>
<div
className={cn(
'mt-auto flex flex-col gap-[10px] p-[20px]',
isSessionPending && 'invisible'
)}
className={cn('mt-auto flex flex-col gap-2.5 p-5', isSessionPending && 'invisible')}
>
{isAuthenticated ? (
<Link
href='/workspace'
className='flex h-[32px] items-center justify-center rounded-[5px] border border-[#FFFFFF] bg-[#FFFFFF] text-[14px] text-black transition-colors active:bg-[#E0E0E0]'
className='flex h-[32px] items-center justify-center rounded-[5px] border border-[var(--white)] bg-[var(--white)] text-[14px] text-black transition-colors active:bg-[#E0E0E0]'
onClick={() => setMobileMenuOpen(false)}
aria-label='Go to app'
>
@@ -330,7 +334,7 @@ export default function Navbar({ logoOnly = false, blogPosts = [] }: NavbarProps
<>
<Link
href='/login'
className='flex h-[32px] items-center justify-center rounded-[5px] border border-[#3d3d3d] text-[#ECECEC] text-[14px] transition-colors active:bg-[#2A2A2A]'
className='flex h-[32px] items-center justify-center rounded-[5px] border border-[var(--landing-border-strong)] text-[14px] text-[var(--landing-text)] transition-colors active:bg-[var(--landing-bg-elevated)]'
onClick={() => setMobileMenuOpen(false)}
aria-label='Log in'
>
@@ -338,7 +342,7 @@ export default function Navbar({ logoOnly = false, blogPosts = [] }: NavbarProps
</Link>
<Link
href='/signup'
className='flex h-[32px] items-center justify-center rounded-[5px] border border-[#FFFFFF] bg-[#FFFFFF] text-[14px] text-black transition-colors active:bg-[#E0E0E0]'
className='flex h-[32px] items-center justify-center rounded-[5px] border border-[var(--white)] bg-[var(--white)] text-[14px] text-black transition-colors active:bg-[#E0E0E0]'
onClick={() => setMobileMenuOpen(false)}
aria-label='Get started with Sim'
>
@@ -425,7 +429,13 @@ function MobileMenuIcon({ open }: { open: boolean }) {
function ExternalArrowIcon() {
return (
<svg width='12' height='12' viewBox='0 0 12 12' fill='none' className='text-[#666]'>
<svg
width='12'
height='12'
viewBox='0 0 12 12'
fill='none'
className='text-[var(--landing-text-secondary)]'
>
<path
d='M3.5 2.5H9.5V8.5M9 3L3 9'
stroke='currentColor'

View File

@@ -25,6 +25,7 @@ const PRICING_TIERS: PricingTier[] = [
'5GB file storage',
'3 tables · 1,000 rows each',
'5 min execution limit',
'5 concurrent/workspace',
'7-day log retention',
'CLI/SDK/MCP Access',
],
@@ -42,6 +43,7 @@ const PRICING_TIERS: PricingTier[] = [
'50GB file storage',
'25 tables · 5,000 rows each',
'50 min execution · 150 runs/min',
'50 concurrent/workspace',
'Unlimited log retention',
'CLI/SDK/MCP Access',
],
@@ -59,6 +61,7 @@ const PRICING_TIERS: PricingTier[] = [
'500GB file storage',
'25 tables · 5,000 rows each',
'50 min execution · 300 runs/min',
'200 concurrent/workspace',
'Unlimited log retention',
'CLI/SDK/MCP Access',
],
@@ -75,8 +78,9 @@ const PRICING_TIERS: PricingTier[] = [
'Custom file storage',
'10,000 tables · 1M rows each',
'Custom execution limits',
'Custom concurrency limits',
'Unlimited log retention',
'SSO & SCIM · SOC2 & HIPAA',
'SSO & SCIM · SOC2',
'Self hosting · Dedicated support',
],
cta: { label: 'Book a demo', action: 'demo-request' },
@@ -107,21 +111,21 @@ function PricingCard({ tier }: PricingCardProps) {
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-1 flex-col gap-6 rounded-t-lg border border-[var(--landing-border-light)] border-b-0 bg-white p-5'>
<div className='flex flex-col'>
<h3
id={`${tier.id}-heading`}
className='font-[430] font-season text-[#1C1C1C] text-[24px] leading-[100%] tracking-[-0.02em]'
className='font-[430] font-season text-[24px] text-[var(--landing-text-dark)] 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]'>
<p className='mt-2 min-h-[44px] font-[430] font-season text-[#5c5c5c] text-sm 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]'>
<p className='mt-4 flex items-center gap-1.5 font-[430] font-season text-[20px] text-[var(--landing-text-dark)] leading-[100%] tracking-[-0.02em]'>
{tier.price}
{tier.billingPeriod && (
<span className='text-[#737373] text-[16px]'>{tier.billingPeriod}</span>
<span className='text-[#737373] text-md'>{tier.billingPeriod}</span>
)}
</p>
<div className='mt-4'>
@@ -129,7 +133,7 @@ function PricingCard({ tier }: PricingCardProps) {
<DemoRequestModal theme='light'>
<button
type='button'
className='flex h-[32px] w-full items-center justify-center rounded-[5px] border border-[#E5E5E5] bg-transparent px-[10px] font-[430] font-season text-[#1C1C1C] text-[14px] transition-colors hover:bg-[#F0F0F0]'
className='flex h-[32px] w-full items-center justify-center rounded-[5px] border border-[var(--landing-border-light)] bg-transparent px-2.5 font-[430] font-season text-[14px] text-[var(--landing-text-dark)] transition-colors hover:bg-[var(--landing-bg-hover)]'
>
{tier.cta.label}
</button>
@@ -137,14 +141,14 @@ function PricingCard({ tier }: PricingCardProps) {
) : isPro ? (
<Link
href={tier.cta.href || '/signup'}
className='flex h-[32px] w-full items-center justify-center rounded-[5px] border border-[#1D1D1D] bg-[#1D1D1D] px-[10px] font-[430] font-season text-[14px] text-white transition-colors hover:border-[#2A2A2A] hover:bg-[#2A2A2A]'
className='flex h-[32px] w-full items-center justify-center rounded-[5px] border border-[#1D1D1D] bg-[#1D1D1D] px-2.5 font-[430] font-season text-[14px] text-white transition-colors hover:border-[var(--landing-border)] hover:bg-[var(--landing-bg-elevated)]'
>
{tier.cta.label}
</Link>
) : (
<Link
href={tier.cta.href || '/signup'}
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]'
className='flex h-[32px] w-full items-center justify-center rounded-[5px] border border-[var(--landing-border-light)] px-2.5 font-[430] font-season text-[14px] text-[var(--landing-text-dark)] transition-colors hover:bg-[var(--landing-bg-hover)]'
>
{tier.cta.label}
</Link>
@@ -156,7 +160,7 @@ function PricingCard({ tier }: PricingCardProps) {
{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]'>
<span className='font-[400] font-season text-[#5c5c5c] text-sm leading-[125%] tracking-[0.02em]'>
{feature}
</span>
</li>
@@ -187,9 +191,13 @@ function PricingCard({ tier }: PricingCardProps) {
*/
export default function Pricing() {
return (
<section id='pricing' aria-labelledby='pricing-heading' className='bg-[#F6F6F6]'>
<div className='px-4 pt-[60px] pb-[40px] sm:px-8 sm:pt-[80px] sm:pb-0 md:px-[80px] md:pt-[100px]'>
<div className='flex flex-col items-start gap-3 sm:gap-4 md:gap-[20px]'>
<section
id='pricing'
aria-labelledby='pricing-heading'
className='bg-[var(--landing-bg-section)]'
>
<div className='px-4 pt-[60px] pb-10 sm:px-8 sm:pt-20 sm:pb-0 md:px-20 md:pt-[100px]'>
<div className='flex flex-col items-start gap-3 sm:gap-4 md:gap-5'>
<Badge
variant='blue'
size='md'
@@ -201,7 +209,7 @@ export default function Pricing() {
<h2
id='pricing-heading'
className='font-[430] font-season text-[#1C1C1C] text-[32px] leading-[100%] tracking-[-0.02em] sm:text-[36px] md:text-[40px]'
className='text-balance font-[430] font-season text-[32px] text-[var(--landing-text-dark)] leading-[100%] tracking-[-0.02em] sm:text-[36px] md:text-[40px]'
>
Pricing
</h2>

View File

@@ -93,7 +93,7 @@ export default function StructuredData() {
url: 'https://sim.ai',
name: 'Sim — Build AI Agents & Run Your Agentic Workforce',
description:
'Sim is the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows. Create agents, workflows, knowledge bases, tables, and docs. Trusted by over 100,000 builders. SOC2 and HIPAA compliant.',
'Sim is the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows. Create agents, workflows, knowledge bases, tables, and docs. Trusted by over 100,000 builders. SOC2 compliant.',
applicationCategory: 'DeveloperApplication',
operatingSystem: 'Web',
browserRequirements: 'Requires a modern browser with JavaScript enabled',
@@ -179,7 +179,7 @@ export default function StructuredData() {
name: 'What is Sim?',
acceptedAnswer: {
'@type': 'Answer',
text: 'Sim is the open-source platform to build AI agents and run your agentic workforce. Teams connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows. Create agents, workflows, knowledge bases, tables, and docs. Trusted by over 100,000 builders. SOC2 and HIPAA compliant.',
text: 'Sim is the open-source platform to build AI agents and run your agentic workforce. Teams connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows. Create agents, workflows, knowledge bases, tables, and docs. Trusted by over 100,000 builders. SOC2 compliant.',
},
},
{
@@ -211,7 +211,7 @@ export default function StructuredData() {
name: 'What enterprise features does Sim offer?',
acceptedAnswer: {
'@type': 'Answer',
text: 'Sim offers SOC2 and HIPAA compliance, SSO/SAML authentication, role-based access control, audit logs, dedicated support, custom SLAs, and on-premise deployment options for enterprise customers.',
text: 'Sim offers SOC2 compliance, SSO/SAML authentication, role-based access control, audit logs, dedicated support, custom SLAs, and on-premise deployment options for enterprise customers.',
},
},
],

View File

@@ -19,7 +19,7 @@ const LandingPreviewWorkflow = dynamic(
).then((mod) => mod.LandingPreviewWorkflow),
{
ssr: false,
loading: () => <div className='h-full w-full bg-[#1b1b1b]' />,
loading: () => <div className='h-full w-full bg-[var(--landing-bg)]' />,
}
)
@@ -337,7 +337,7 @@ function DotGrid({ className, cols, rows, gap = 0 }: DotGridProps) {
}}
>
{Array.from({ length: cols * rows }, (_, i) => (
<div key={i} className='h-[1.5px] w-[1.5px] rounded-full bg-[#2A2A2A]' />
<div key={i} className='h-[1.5px] w-[1.5px] rounded-full bg-[var(--landing-bg-elevated)]' />
))}
</div>
)
@@ -412,7 +412,7 @@ export default function Templates() {
ref={sectionRef}
id='templates'
aria-labelledby='templates-heading'
className='mt-[40px] mb-[80px]'
className='mt-10 mb-20'
>
<p className='sr-only'>
Sim includes {TEMPLATE_WORKFLOWS.length} pre-built workflow templates covering OCR
@@ -422,9 +422,9 @@ export default function Templates() {
customise it, and deploy in minutes.
</p>
<div className='bg-[#1C1C1C]'>
<div className='bg-[var(--landing-bg)]'>
<DotGrid
className='overflow-hidden border-[#2A2A2A] border-y bg-[#1C1C1C] p-[6px]'
className='overflow-hidden border-[var(--landing-bg-elevated)] border-y bg-[var(--landing-bg)] p-1.5'
cols={160}
rows={1}
gap={6}
@@ -449,8 +449,8 @@ export default function Templates() {
</svg>
</div>
<div className='px-[20px] pt-[60px] lg:px-[80px] lg:pt-[100px]'>
<div className='flex flex-col items-start gap-[20px]'>
<div className='px-5 pt-[60px] lg:px-20 lg:pt-[100px]'>
<div className='flex flex-col items-start gap-5'>
<Badge
variant='blue'
size='md'
@@ -466,12 +466,12 @@ export default function Templates() {
<h2
id='templates-heading'
className='font-[430] font-season text-[28px] text-white leading-[100%] tracking-[-0.02em] lg:text-[40px]'
className='text-balance font-[430] font-season text-[28px] text-white leading-[100%] tracking-[-0.02em] lg:text-[40px]'
>
Ship your agent in minutes
</h2>
<p className='font-[430] font-season text-[#F6F6F0]/50 text-[15px] leading-[150%] tracking-[0.02em] lg:text-[18px]'>
<p className='font-[430] font-season text-[#F6F6F0]/50 text-base leading-[150%] tracking-[0.02em] lg:text-lg'>
Pre-built templates for every use casepick one, swap{' '}
<br className='hidden lg:inline' />
models and tools to fit your stack, and deploy.
@@ -479,11 +479,11 @@ export default function Templates() {
</div>
</div>
<div className='mt-[40px] flex border-[#2A2A2A] border-y lg:mt-[73px]'>
<div className='mt-10 flex border-[var(--landing-bg-elevated)] border-y lg:mt-[73px]'>
<div className='shrink-0'>
<div className='h-full lg:hidden'>
<DotGrid
className='h-full w-[24px] overflow-hidden border-[#2A2A2A] border-r p-[4px]'
className='h-full w-[24px] overflow-hidden border-[var(--landing-bg-elevated)] border-r p-1'
cols={2}
rows={55}
gap={4}
@@ -491,7 +491,7 @@ export default function Templates() {
</div>
<div className='hidden h-full lg:block'>
<DotGrid
className='h-full w-[80px] overflow-hidden border-[#2A2A2A] border-r p-[6px]'
className='h-full w-[80px] overflow-hidden border-[var(--landing-bg-elevated)] border-r p-1.5'
cols={8}
rows={55}
gap={6}
@@ -503,7 +503,7 @@ export default function Templates() {
<div
role='tablist'
aria-label='Workflow templates'
className='flex w-full shrink-0 flex-col border-[#2A2A2A] lg:w-[300px] lg:border-r'
className='flex w-full shrink-0 flex-col border-[var(--landing-bg-elevated)] lg:w-[300px] lg:border-r'
>
{TEMPLATE_WORKFLOWS.map((workflow, index) => {
const isActive = index === activeIndex
@@ -521,7 +521,7 @@ export default function Templates() {
isActive
? 'z-10'
: cn(
'flex items-center px-[12px] py-[10px] hover:bg-[#232323]/50',
'flex items-center px-3 py-2.5 hover:bg-[color-mix(in_srgb,var(--landing-bg-card)_50%,transparent)]',
index < TEMPLATE_WORKFLOWS.length - 1 &&
'shadow-[inset_0_-1px_0_0_#2A2A2A]'
)
@@ -543,8 +543,8 @@ export default function Templates() {
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'>
<div className='-translate-y-2 relative flex translate-x-2 items-center bg-[var(--landing-bg-card)] px-3 py-2.5 shadow-[inset_0_0_0_1.5px_#3E3E3E]'>
<span className='flex-1 font-[430] font-season text-md text-white'>
{workflow.name}
</span>
<ChevronDown
@@ -556,7 +556,7 @@ export default function Templates() {
)
})()
) : (
<span className='font-[430] font-season text-[#F6F6F0]/50 text-[16px]'>
<span className='font-[430] font-season text-[#F6F6F0]/50 text-md'>
{workflow.name}
</span>
)}
@@ -571,19 +571,19 @@ export default function Templates() {
transition={{ duration: 0.25, ease: [0.4, 0, 0.2, 1] }}
className='overflow-hidden'
>
<div className='aspect-[16/10] w-full border-[#2A2A2A] border-y bg-[#1b1b1b]'>
<div className='aspect-[16/10] w-full border-[var(--landing-bg-elevated)] border-y bg-[var(--landing-bg)]'>
<LandingPreviewWorkflow
workflow={workflow}
animate
fitViewOptions={{ padding: 0.15, maxZoom: 1.3 }}
/>
</div>
<div className='p-[12px]'>
<div className='p-3'>
<button
type='button'
onClick={handleUseTemplate}
disabled={isPreparingTemplate}
className='inline-flex h-[32px] w-full cursor-pointer items-center justify-center gap-[6px] rounded-[5px] border border-[#FFFFFF] bg-[#FFFFFF] font-[430] font-season text-[14px] text-black transition-colors active:bg-[#E0E0E0]'
className='inline-flex h-[32px] w-full cursor-pointer items-center justify-center gap-1.5 rounded-[5px] border border-white bg-white font-[430] font-season text-black text-sm transition-colors active:bg-[#E0E0E0]'
>
{isPreparingTemplate ? 'Preparing...' : 'Use template'}
</button>
@@ -614,7 +614,7 @@ export default function Templates() {
type='button'
onClick={handleUseTemplate}
disabled={isPreparingTemplate}
className='group/cta absolute top-[16px] right-[16px] z-10 inline-flex h-[32px] cursor-pointer items-center gap-[6px] rounded-[5px] border border-[#FFFFFF] bg-[#FFFFFF] px-[10px] font-[430] font-season text-[14px] text-black transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
className='group/cta absolute top-4 right-[16px] z-10 inline-flex h-[32px] cursor-pointer items-center gap-1.5 rounded-[5px] border border-white bg-white px-2.5 font-[430] font-season text-black text-sm transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
>
{isPreparingTemplate ? 'Preparing...' : 'Use template'}
<span className='relative h-[10px] w-[10px] shrink-0'>
@@ -642,7 +642,7 @@ export default function Templates() {
<div className='shrink-0'>
<div className='h-full lg:hidden'>
<DotGrid
className='h-full w-[24px] overflow-hidden border-[#2A2A2A] border-l p-[4px]'
className='h-full w-[24px] overflow-hidden border-[var(--landing-bg-elevated)] border-l p-1'
cols={2}
rows={55}
gap={4}
@@ -650,7 +650,7 @@ export default function Templates() {
</div>
<div className='hidden h-full lg:block'>
<DotGrid
className='h-full w-[80px] overflow-hidden border-[#2A2A2A] border-l p-[6px]'
className='h-full w-[80px] overflow-hidden border-[var(--landing-bg-elevated)] border-l p-1.5'
cols={8}
rows={55}
gap={6}

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