mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-10 14:45:16 -05:00
Compare commits
3 Commits
sim-609-to
...
feat/landi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e68ec1065c | ||
|
|
622d0cad22 | ||
|
|
c74922997c |
@@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* Collaboration section — team workflows and real-time collaboration.
|
||||||
|
*
|
||||||
|
* SEO:
|
||||||
|
* - `<section id="collaboration" aria-labelledby="collaboration-heading">`.
|
||||||
|
* - `<h2 id="collaboration-heading">` for the section title.
|
||||||
|
* - Product visuals use `<figure>` with `<figcaption>` and descriptive `alt` text.
|
||||||
|
*
|
||||||
|
* GEO:
|
||||||
|
* - Name specific capabilities (version control, shared workspaces, RBAC, audit logs).
|
||||||
|
* - Lead with a summary so AI can answer "Does Sim support team collaboration?".
|
||||||
|
* - Reference "Sim" by name per capability ("Sim's real-time collaboration").
|
||||||
|
*/
|
||||||
|
export default function Collaboration() {
|
||||||
|
return null
|
||||||
|
}
|
||||||
17
apps/sim/app/(home)/components/enterprise/enterprise.tsx
Normal file
17
apps/sim/app/(home)/components/enterprise/enterprise.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* Enterprise section — compliance, scale, and security messaging.
|
||||||
|
*
|
||||||
|
* SEO:
|
||||||
|
* - `<section id="enterprise" aria-labelledby="enterprise-heading">`.
|
||||||
|
* - `<h2 id="enterprise-heading">` for the section title.
|
||||||
|
* - Compliance certs (SOC2, HIPAA) as visible `<strong>` text.
|
||||||
|
* - Enterprise CTA links to contact form via `<a>` with `rel="noopener noreferrer"`.
|
||||||
|
*
|
||||||
|
* GEO:
|
||||||
|
* - Entity-rich: "Sim is SOC2 and HIPAA compliant" — not "We are compliant."
|
||||||
|
* - `<ul>` checklist of features (SSO, RBAC, audit logs, SLA, on-premise deployment)
|
||||||
|
* as an atomic answer block for "What enterprise features does Sim offer?".
|
||||||
|
*/
|
||||||
|
export default function Enterprise() {
|
||||||
|
return null
|
||||||
|
}
|
||||||
17
apps/sim/app/(home)/components/features/features.tsx
Normal file
17
apps/sim/app/(home)/components/features/features.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* Features section — Sim's core product capabilities.
|
||||||
|
*
|
||||||
|
* SEO:
|
||||||
|
* - `<section id="features" aria-labelledby="features-heading">`.
|
||||||
|
* - `<h2 id="features-heading">` for the section title.
|
||||||
|
* - Each feature: `<h3>` + `<p>` + `<ul>` capability list. Strict H2 -> H3 -> H4 hierarchy.
|
||||||
|
* - Feature lists map to `WebApplication.featureList` in structured-data.tsx — keep in sync.
|
||||||
|
*
|
||||||
|
* GEO:
|
||||||
|
* - Each feature block is independently extractable (atomic answer block pattern).
|
||||||
|
* - Include specific numbers ("100+ integrations", "15+ AI providers", "SOC2 compliant").
|
||||||
|
* - Always use "Sim" by name — never "the platform" or "our tool" (entity consistency).
|
||||||
|
*/
|
||||||
|
export default function Features() {
|
||||||
|
return null
|
||||||
|
}
|
||||||
18
apps/sim/app/(home)/components/footer/footer.tsx
Normal file
18
apps/sim/app/(home)/components/footer/footer.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* Landing page footer — navigation, legal links, and entity reinforcement.
|
||||||
|
*
|
||||||
|
* SEO:
|
||||||
|
* - `<footer role="contentinfo">` with `<nav aria-label="Footer navigation">`.
|
||||||
|
* - Link groups under semantic headings (`<h3>`). All links are `<Link>` or `<a>` with `href`.
|
||||||
|
* - External links include `rel="noopener noreferrer"`.
|
||||||
|
* - Legal links (Privacy, Terms) must be crawlable (trust signals).
|
||||||
|
*
|
||||||
|
* GEO:
|
||||||
|
* - Include "Sim — Open-source AI agent workflow builder" as visible text (entity reinforcement).
|
||||||
|
* - Social links (X, GitHub, LinkedIn, Discord) must match `sameAs` in structured-data.tsx.
|
||||||
|
* - Link to all major pages: Docs, Pricing, Enterprise, Careers, Changelog (internal link graph).
|
||||||
|
* - Display compliance badges (SOC2, HIPAA) and status page link as visible trust signals.
|
||||||
|
*/
|
||||||
|
export default function Footer() {
|
||||||
|
return null
|
||||||
|
}
|
||||||
18
apps/sim/app/(home)/components/hero/hero.tsx
Normal file
18
apps/sim/app/(home)/components/hero/hero.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* Hero section — above-the-fold value proposition.
|
||||||
|
*
|
||||||
|
* SEO:
|
||||||
|
* - `<section id="hero" aria-labelledby="hero-heading">`.
|
||||||
|
* - Contains the page's only `<h1>`. Text aligns with the `<title>` tag keyword.
|
||||||
|
* - Subtitle `<p>` expands the H1 into a full sentence with the primary keyword.
|
||||||
|
* - Primary CTA is a `<Link href="/signup">` (crawlable), not a `<button>`.
|
||||||
|
* - Canvas/animations wrapped in `aria-hidden="true"` with a text alternative.
|
||||||
|
*
|
||||||
|
* GEO:
|
||||||
|
* - H1 + subtitle answer "What is Sim?" in two sentences (answer-first pattern).
|
||||||
|
* - First 150 chars of visible text explicitly name "Sim", "AI agent", "workflow builder".
|
||||||
|
* - Include a `<p className="sr-only">` product summary (60-80 words) rich in entities.
|
||||||
|
*/
|
||||||
|
export default function Hero() {
|
||||||
|
return null
|
||||||
|
}
|
||||||
23
apps/sim/app/(home)/components/index.ts
Normal file
23
apps/sim/app/(home)/components/index.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import Collaboration from '@/app/(home)/components/collaboration/collaboration'
|
||||||
|
import Enterprise from '@/app/(home)/components/enterprise/enterprise'
|
||||||
|
import Features from '@/app/(home)/components/features/features'
|
||||||
|
import Footer from '@/app/(home)/components/footer/footer'
|
||||||
|
import Hero from '@/app/(home)/components/hero/hero'
|
||||||
|
import Navbar from '@/app/(home)/components/navbar/navbar'
|
||||||
|
import Pricing from '@/app/(home)/components/pricing/pricing'
|
||||||
|
import StructuredData from '@/app/(home)/components/structured-data'
|
||||||
|
import Templates from '@/app/(home)/components/templates/templates'
|
||||||
|
import Testimonials from '@/app/(home)/components/testimonials/testimonials'
|
||||||
|
|
||||||
|
export {
|
||||||
|
Collaboration,
|
||||||
|
Enterprise,
|
||||||
|
Features,
|
||||||
|
Footer,
|
||||||
|
Hero,
|
||||||
|
Navbar,
|
||||||
|
Pricing,
|
||||||
|
StructuredData,
|
||||||
|
Templates,
|
||||||
|
Testimonials,
|
||||||
|
}
|
||||||
17
apps/sim/app/(home)/components/navbar/navbar.tsx
Normal file
17
apps/sim/app/(home)/components/navbar/navbar.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* Landing page navigation bar.
|
||||||
|
*
|
||||||
|
* SEO:
|
||||||
|
* - `<nav aria-label="Primary navigation">` with `itemScope itemType="https://schema.org/SiteNavigationElement"`.
|
||||||
|
* - All routes use `<Link>` (crawlable). External links include `rel="noopener noreferrer"`.
|
||||||
|
* - Logo link contains `<span className="sr-only">` with the brand name.
|
||||||
|
* - Logo `<Image>` uses `priority` and `loading="eager"` (LCP element).
|
||||||
|
*
|
||||||
|
* GEO:
|
||||||
|
* - "Sim" as the canonical entity name everywhere.
|
||||||
|
* - Navigation items in semantic `<ul>/<li>` with descriptive anchor text.
|
||||||
|
* - Descriptive `aria-label` on interactive elements for AI intent extraction.
|
||||||
|
*/
|
||||||
|
export default function Navbar() {
|
||||||
|
return null
|
||||||
|
}
|
||||||
17
apps/sim/app/(home)/components/pricing/pricing.tsx
Normal file
17
apps/sim/app/(home)/components/pricing/pricing.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* Pricing section — tiered pricing plans with feature comparison.
|
||||||
|
*
|
||||||
|
* SEO:
|
||||||
|
* - `<section id="pricing" aria-labelledby="pricing-heading">`.
|
||||||
|
* - `<h2 id="pricing-heading">` for the section title.
|
||||||
|
* - Each tier: `<h3>` plan name + semantic `<ul>` feature list.
|
||||||
|
* - Free tier CTA uses `<Link href="/signup">` (crawlable). Enterprise CTA uses `<a>`.
|
||||||
|
*
|
||||||
|
* GEO:
|
||||||
|
* - Each plan has consistent structure: name, price, billing period, feature list.
|
||||||
|
* - Lead with a summary: "Sim offers a free Community plan, $20/mo Pro, $40/mo Team, custom Enterprise."
|
||||||
|
* - Prices must match the `Offer` items in structured-data.tsx exactly.
|
||||||
|
*/
|
||||||
|
export default function Pricing() {
|
||||||
|
return null
|
||||||
|
}
|
||||||
222
apps/sim/app/(home)/components/structured-data.tsx
Normal file
222
apps/sim/app/(home)/components/structured-data.tsx
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
/**
|
||||||
|
* JSON-LD structured data for the landing page.
|
||||||
|
*
|
||||||
|
* Renders a `<script type="application/ld+json">` with Schema.org markup.
|
||||||
|
* Single source of truth for machine-readable page metadata.
|
||||||
|
*
|
||||||
|
* Schemas: Organization, WebSite, WebPage, BreadcrumbList, WebApplication, FAQPage.
|
||||||
|
*
|
||||||
|
* AI crawler behavior (2025-2026):
|
||||||
|
* - Google AI Overviews / Bing Copilot parse JSON-LD from their search indexes.
|
||||||
|
* - GPTBot indexes JSON-LD during crawling (92% of LLM crawlers parse JSON-LD first).
|
||||||
|
* - Perplexity / Claude prioritize visible HTML over JSON-LD during direct fetch.
|
||||||
|
* - All claims here must also appear as visible text on the page.
|
||||||
|
*
|
||||||
|
* Maintenance:
|
||||||
|
* - Offer prices must match the Pricing component exactly.
|
||||||
|
* - `sameAs` links must match the Footer social links.
|
||||||
|
* - Do not add `aggregateRating` without real, verifiable review data.
|
||||||
|
*/
|
||||||
|
export default function StructuredData() {
|
||||||
|
const structuredData = {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@graph': [
|
||||||
|
{
|
||||||
|
'@type': 'Organization',
|
||||||
|
'@id': 'https://sim.ai/#organization',
|
||||||
|
name: 'Sim',
|
||||||
|
alternateName: 'Sim Studio',
|
||||||
|
description:
|
||||||
|
'Open-source AI agent workflow builder used by developers at trail-blazing startups to Fortune 500 companies',
|
||||||
|
url: 'https://sim.ai',
|
||||||
|
logo: {
|
||||||
|
'@type': 'ImageObject',
|
||||||
|
'@id': 'https://sim.ai/#logo',
|
||||||
|
url: 'https://sim.ai/logo/b&w/text/b&w.svg',
|
||||||
|
contentUrl: 'https://sim.ai/logo/b&w/text/b&w.svg',
|
||||||
|
width: 49.78314,
|
||||||
|
height: 24.276,
|
||||||
|
caption: 'Sim Logo',
|
||||||
|
},
|
||||||
|
image: { '@id': 'https://sim.ai/#logo' },
|
||||||
|
sameAs: [
|
||||||
|
'https://x.com/simdotai',
|
||||||
|
'https://github.com/simstudioai/sim',
|
||||||
|
'https://www.linkedin.com/company/simstudioai/',
|
||||||
|
'https://discord.gg/Hr4UWYEcTT',
|
||||||
|
],
|
||||||
|
contactPoint: {
|
||||||
|
'@type': 'ContactPoint',
|
||||||
|
contactType: 'customer support',
|
||||||
|
availableLanguage: ['en'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@type': 'WebSite',
|
||||||
|
'@id': 'https://sim.ai/#website',
|
||||||
|
url: 'https://sim.ai',
|
||||||
|
name: 'Sim — AI Agent Workflow Builder',
|
||||||
|
description:
|
||||||
|
'Open-source AI agent workflow builder. 60,000+ developers build and deploy agentic workflows. SOC2 and HIPAA compliant.',
|
||||||
|
publisher: { '@id': 'https://sim.ai/#organization' },
|
||||||
|
inLanguage: 'en-US',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@type': 'WebPage',
|
||||||
|
'@id': 'https://sim.ai/#webpage',
|
||||||
|
url: 'https://sim.ai',
|
||||||
|
name: 'Sim — Workflows for LLMs | Build AI Agent Workflows',
|
||||||
|
isPartOf: { '@id': 'https://sim.ai/#website' },
|
||||||
|
about: { '@id': 'https://sim.ai/#software' },
|
||||||
|
datePublished: '2024-01-01T00:00:00+00:00',
|
||||||
|
dateModified: new Date().toISOString(),
|
||||||
|
description:
|
||||||
|
'Build and deploy AI agent workflows with Sim. Visual drag-and-drop interface for creating powerful LLM-powered automations.',
|
||||||
|
breadcrumb: { '@id': 'https://sim.ai/#breadcrumb' },
|
||||||
|
inLanguage: 'en-US',
|
||||||
|
potentialAction: [{ '@type': 'ReadAction', target: ['https://sim.ai'] }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@type': 'BreadcrumbList',
|
||||||
|
'@id': 'https://sim.ai/#breadcrumb',
|
||||||
|
itemListElement: [
|
||||||
|
{ '@type': 'ListItem', position: 1, name: 'Home', item: 'https://sim.ai' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@type': 'WebApplication',
|
||||||
|
'@id': 'https://sim.ai/#software',
|
||||||
|
name: 'Sim — AI Agent Workflow Builder',
|
||||||
|
description:
|
||||||
|
'Open-source AI agent workflow builder used by 60,000+ developers. Build agentic workflows with visual drag-and-drop interface. SOC2 and HIPAA compliant. Integrate with 100+ apps.',
|
||||||
|
applicationCategory: 'DeveloperApplication',
|
||||||
|
operatingSystem: 'Web',
|
||||||
|
browserRequirements: 'Requires a modern browser with JavaScript enabled',
|
||||||
|
offers: [
|
||||||
|
{
|
||||||
|
'@type': 'Offer',
|
||||||
|
name: 'Community Plan',
|
||||||
|
price: '0',
|
||||||
|
priceCurrency: 'USD',
|
||||||
|
availability: 'https://schema.org/InStock',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@type': 'Offer',
|
||||||
|
name: 'Pro Plan',
|
||||||
|
price: '20',
|
||||||
|
priceCurrency: 'USD',
|
||||||
|
priceSpecification: {
|
||||||
|
'@type': 'UnitPriceSpecification',
|
||||||
|
price: '20',
|
||||||
|
priceCurrency: 'USD',
|
||||||
|
unitText: 'MONTH',
|
||||||
|
billingIncrement: 1,
|
||||||
|
},
|
||||||
|
availability: 'https://schema.org/InStock',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@type': 'Offer',
|
||||||
|
name: 'Team Plan',
|
||||||
|
price: '40',
|
||||||
|
priceCurrency: 'USD',
|
||||||
|
priceSpecification: {
|
||||||
|
'@type': 'UnitPriceSpecification',
|
||||||
|
price: '40',
|
||||||
|
priceCurrency: 'USD',
|
||||||
|
unitText: 'MONTH',
|
||||||
|
billingIncrement: 1,
|
||||||
|
},
|
||||||
|
availability: 'https://schema.org/InStock',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
featureList: [
|
||||||
|
'Visual workflow builder',
|
||||||
|
'Drag-and-drop interface',
|
||||||
|
'100+ integrations',
|
||||||
|
'AI model support (OpenAI, Anthropic, Google, xAI, Mistral, Perplexity)',
|
||||||
|
'Real-time collaboration',
|
||||||
|
'Version control',
|
||||||
|
'API access',
|
||||||
|
'Custom functions',
|
||||||
|
'Scheduled workflows',
|
||||||
|
'Event triggers',
|
||||||
|
],
|
||||||
|
review: [
|
||||||
|
{
|
||||||
|
'@type': 'Review',
|
||||||
|
author: { '@type': 'Person', name: 'Hasan Toor' },
|
||||||
|
reviewBody:
|
||||||
|
'This startup just dropped the fastest way to build AI agents. This Figma-like canvas to build agents will blow your mind.',
|
||||||
|
url: 'https://x.com/hasantoxr/status/1912909502036525271',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@type': 'Review',
|
||||||
|
author: { '@type': 'Person', name: 'nizzy' },
|
||||||
|
reviewBody:
|
||||||
|
'This is the zapier of agent building. I always believed that building agents and using AI should not be limited to technical people. I think this solves just that.',
|
||||||
|
url: 'https://x.com/nizzyabi/status/1907864421227180368',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@type': 'Review',
|
||||||
|
author: { '@type': 'Organization', name: 'xyflow' },
|
||||||
|
reviewBody: 'A very good looking agent workflow builder and open source!',
|
||||||
|
url: 'https://x.com/xyflowdev/status/1909501499719438670',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@type': 'FAQPage',
|
||||||
|
'@id': 'https://sim.ai/#faq',
|
||||||
|
mainEntity: [
|
||||||
|
{
|
||||||
|
'@type': 'Question',
|
||||||
|
name: 'What is Sim?',
|
||||||
|
acceptedAnswer: {
|
||||||
|
'@type': 'Answer',
|
||||||
|
text: 'Sim is an open-source AI agent workflow builder used by 60,000+ developers at trail-blazing startups to Fortune 500 companies. It provides a visual drag-and-drop interface for building and deploying agentic workflows. Sim is SOC2 and HIPAA compliant.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@type': 'Question',
|
||||||
|
name: 'Which AI models does Sim support?',
|
||||||
|
acceptedAnswer: {
|
||||||
|
'@type': 'Answer',
|
||||||
|
text: 'Sim supports all major AI models including OpenAI (GPT-5, GPT-4o), Anthropic (Claude), Google (Gemini), xAI (Grok), Mistral, Perplexity, and many more. You can also connect to open-source models via Ollama.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@type': 'Question',
|
||||||
|
name: 'How much does Sim cost?',
|
||||||
|
acceptedAnswer: {
|
||||||
|
'@type': 'Answer',
|
||||||
|
text: 'Sim offers a free Community plan with $20 usage limit, a Pro plan at $20/month, a Team plan at $40/month, and custom Enterprise pricing. All plans include CLI/SDK access.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@type': 'Question',
|
||||||
|
name: 'Do I need coding skills to use Sim?',
|
||||||
|
acceptedAnswer: {
|
||||||
|
'@type': 'Answer',
|
||||||
|
text: 'No coding skills are required. Sim features a visual drag-and-drop interface that makes it easy to build AI workflows. However, developers can also use custom functions and the API for advanced use cases.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@type': 'Question',
|
||||||
|
name: 'What enterprise features does Sim offer?',
|
||||||
|
acceptedAnswer: {
|
||||||
|
'@type': 'Answer',
|
||||||
|
text: 'Sim offers SOC2 and HIPAA compliance, SSO/SAML authentication, role-based access control, audit logs, dedicated support, custom SLAs, and on-premise deployment options for enterprise customers.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<script
|
||||||
|
type='application/ld+json'
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
16
apps/sim/app/(home)/components/templates/templates.tsx
Normal file
16
apps/sim/app/(home)/components/templates/templates.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* Templates section — pre-built workflow templates and use cases.
|
||||||
|
*
|
||||||
|
* SEO:
|
||||||
|
* - `<section id="templates" aria-labelledby="templates-heading">`.
|
||||||
|
* - `<h2 id="templates-heading">` for the section title.
|
||||||
|
* - Each template: `<article>` with `<h3>`, `<p>` description, `<Link>` to signup.
|
||||||
|
*
|
||||||
|
* GEO:
|
||||||
|
* - Each card is an atomic answer block: `<h3>Name</h3><p>1-2 sentences</p><ul>Features</ul>`.
|
||||||
|
* - Name specific integrations (Slack, Gmail, Pinecone) and AI models (GPT-5, Claude).
|
||||||
|
* - Add `id` anchors per card (e.g., `id="template-slack-summarizer"`) for fragment citations.
|
||||||
|
*/
|
||||||
|
export default function Templates() {
|
||||||
|
return null
|
||||||
|
}
|
||||||
18
apps/sim/app/(home)/components/testimonials/testimonials.tsx
Normal file
18
apps/sim/app/(home)/components/testimonials/testimonials.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* Testimonials section — social proof via user quotes.
|
||||||
|
*
|
||||||
|
* SEO:
|
||||||
|
* - `<section id="testimonials" aria-labelledby="testimonials-heading">`.
|
||||||
|
* - `<h2 id="testimonials-heading">` for the section title.
|
||||||
|
* - Each testimonial: `<blockquote cite="tweet-url">` with `<footer><cite>Author</cite></footer>`.
|
||||||
|
* - Profile images use `loading="lazy"` (below the fold).
|
||||||
|
*
|
||||||
|
* GEO:
|
||||||
|
* - Keep quote text as plain text in `<blockquote>` — not split across `<span>` elements.
|
||||||
|
* - Include full author name + handle (LLMs weigh attributed quotes higher).
|
||||||
|
* - Testimonials mentioning "Sim" by name carry more citation weight.
|
||||||
|
* - Review data here aligns with `review` entries in structured-data.tsx.
|
||||||
|
*/
|
||||||
|
export default function Testimonials() {
|
||||||
|
return null
|
||||||
|
}
|
||||||
51
apps/sim/app/(home)/landing.tsx
Normal file
51
apps/sim/app/(home)/landing.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import {
|
||||||
|
Collaboration,
|
||||||
|
Enterprise,
|
||||||
|
Features,
|
||||||
|
Footer,
|
||||||
|
Hero,
|
||||||
|
Navbar,
|
||||||
|
Pricing,
|
||||||
|
StructuredData,
|
||||||
|
Templates,
|
||||||
|
Testimonials,
|
||||||
|
} from '@/app/(home)/components'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Landing page root component.
|
||||||
|
*
|
||||||
|
* ## SEO Architecture
|
||||||
|
* - Single `<h1>` inside Hero (only one per page).
|
||||||
|
* - Heading hierarchy: H1 (Hero) -> H2 (each section) -> H3 (sub-items).
|
||||||
|
* - Semantic landmarks: `<header>`, `<main>`, `<footer>`.
|
||||||
|
* - Every `<section>` has an `id` for anchor linking and `aria-labelledby` for accessibility.
|
||||||
|
* - `StructuredData` emits JSON-LD before any visible content.
|
||||||
|
*
|
||||||
|
* ## GEO Architecture
|
||||||
|
* - Above-fold content (Navbar, Hero) is statically rendered (Server Components where possible)
|
||||||
|
* for immediate availability to AI crawlers.
|
||||||
|
* - Section `id` attributes serve as fragment anchors for precise AI citations.
|
||||||
|
* - Content ordering prioritizes answer-first patterns: definition (Hero) ->
|
||||||
|
* examples (Templates) -> capabilities (Features) -> social proof (Collaboration, Testimonials) ->
|
||||||
|
* pricing (Pricing) -> enterprise (Enterprise).
|
||||||
|
*/
|
||||||
|
export default function Landing() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<StructuredData />
|
||||||
|
<header>
|
||||||
|
<Navbar />
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<Hero />
|
||||||
|
<Templates />
|
||||||
|
<Features />
|
||||||
|
<Collaboration />
|
||||||
|
<Pricing />
|
||||||
|
<Enterprise />
|
||||||
|
<Testimonials />
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
87
apps/sim/app/(home)/layout.tsx
Normal file
87
apps/sim/app/(home)/layout.tsx
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import type { Metadata } from 'next'
|
||||||
|
import { martianMono } from '@/app/_styles/fonts/martian-mono/martian-mono'
|
||||||
|
import { season } from '@/app/_styles/fonts/season/season'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SEO metadata for the landing page.
|
||||||
|
*
|
||||||
|
* - `title` contains the primary keyword and brand name.
|
||||||
|
* - `description` is 150-160 chars, front-loaded with the value proposition.
|
||||||
|
* - `openGraph` provides social sharing metadata with a dedicated OG image.
|
||||||
|
* - `robots` ensures the landing page is fully indexable.
|
||||||
|
* - `alternates.canonical` prevents duplicate content issues.
|
||||||
|
*
|
||||||
|
* GEO note: The description is written as a direct answer to "What is Sim?"
|
||||||
|
* so LLMs can extract it as a cited definition.
|
||||||
|
*/
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
metadataBase: new URL('https://sim.ai'),
|
||||||
|
title: 'Sim — Workflows for LLMs | Build AI Agent Workflows',
|
||||||
|
description:
|
||||||
|
'Sim is an open-source AI agent workflow builder. Build and deploy agentic workflows with a visual drag-and-drop interface. 100+ integrations. SOC2 and HIPAA compliant.',
|
||||||
|
manifest: '/manifest.json',
|
||||||
|
icons: {
|
||||||
|
icon: '/favicon.ico',
|
||||||
|
apple: '/apple-icon.png',
|
||||||
|
},
|
||||||
|
openGraph: {
|
||||||
|
type: 'website',
|
||||||
|
locale: 'en_US',
|
||||||
|
url: 'https://sim.ai',
|
||||||
|
siteName: 'Sim',
|
||||||
|
title: 'Sim — Workflows for LLMs | Build AI Agent Workflows',
|
||||||
|
description:
|
||||||
|
'Open-source AI agent workflow builder used by 60,000+ developers. Visual drag-and-drop interface for building agentic workflows.',
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: '/og-image.png',
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: 'Sim — AI Agent Workflow Builder',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: 'summary_large_image',
|
||||||
|
site: '@simdotai',
|
||||||
|
creator: '@simdotai',
|
||||||
|
title: 'Sim — Workflows for LLMs',
|
||||||
|
description:
|
||||||
|
'Open-source AI agent workflow builder. Build and deploy agentic workflows with a visual drag-and-drop interface.',
|
||||||
|
images: ['/og-image.png'],
|
||||||
|
},
|
||||||
|
robots: {
|
||||||
|
index: true,
|
||||||
|
follow: true,
|
||||||
|
googleBot: {
|
||||||
|
index: true,
|
||||||
|
follow: true,
|
||||||
|
'max-video-preview': -1,
|
||||||
|
'max-image-preview': 'large',
|
||||||
|
'max-snippet': -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
alternates: {
|
||||||
|
canonical: 'https://sim.ai',
|
||||||
|
},
|
||||||
|
other: {
|
||||||
|
'msapplication-TileColor': '#000000',
|
||||||
|
'theme-color': '#ffffff',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Landing page layout.
|
||||||
|
*
|
||||||
|
* Applies landing-specific font CSS variables to the subtree:
|
||||||
|
* - `--font-season` (Season Sans): Headings and display text
|
||||||
|
* - `--font-martian-mono` (Martian Mono): Code snippets and technical accents
|
||||||
|
*
|
||||||
|
* These CSS variables are available to all child components via Tailwind classes
|
||||||
|
* (`font-season`, `font-martian-mono`) or direct `var()` usage.
|
||||||
|
*
|
||||||
|
* The layout is a Server Component — no `'use client'` needed.
|
||||||
|
*/
|
||||||
|
export default function HomeLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
return <div className={`${season.variable} ${martianMono.variable}`}>{children}</div>
|
||||||
|
}
|
||||||
14
apps/sim/app/_styles/fonts/martian-mono/martian-mono.ts
Normal file
14
apps/sim/app/_styles/fonts/martian-mono/martian-mono.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { Martian_Mono } from 'next/font/google'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Martian Mono font configuration
|
||||||
|
* Monospaced variable font used for code snippets, technical content, and accent text
|
||||||
|
* on the landing page. Supports weights 100-800.
|
||||||
|
*/
|
||||||
|
export const martianMono = Martian_Mono({
|
||||||
|
subsets: ['latin'],
|
||||||
|
display: 'swap',
|
||||||
|
variable: '--font-martian-mono',
|
||||||
|
weight: 'variable',
|
||||||
|
fallback: ['ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', 'monospace'],
|
||||||
|
})
|
||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||||
import { getInputFormatExample as getInputFormatExampleUtil } from '@/lib/workflows/operations/deployment-utils'
|
import { getInputFormatExample as getInputFormatExampleUtil } from '@/lib/workflows/operations/deployment-utils'
|
||||||
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
|
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
|
||||||
|
import { runPreDeployChecks } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-predeploy-checks'
|
||||||
import { CreateApiKeyModal } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/api-keys/components'
|
import { CreateApiKeyModal } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/api-keys/components'
|
||||||
import { startsWithUuid } from '@/executor/constants'
|
import { startsWithUuid } from '@/executor/constants'
|
||||||
import { useA2AAgentByWorkflow } from '@/hooks/queries/a2a/agents'
|
import { useA2AAgentByWorkflow } from '@/hooks/queries/a2a/agents'
|
||||||
@@ -38,6 +39,7 @@ import { useWorkspaceSettings } from '@/hooks/queries/workspace'
|
|||||||
import { usePermissionConfig } from '@/hooks/use-permission-config'
|
import { usePermissionConfig } from '@/hooks/use-permission-config'
|
||||||
import { useSettingsModalStore } from '@/stores/modals/settings/store'
|
import { useSettingsModalStore } from '@/stores/modals/settings/store'
|
||||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||||
|
import { mergeSubblockState } from '@/stores/workflows/utils'
|
||||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||||
import type { WorkflowState } from '@/stores/workflows/workflow/types'
|
import type { WorkflowState } from '@/stores/workflows/workflow/types'
|
||||||
import { A2aDeploy } from './components/a2a/a2a'
|
import { A2aDeploy } from './components/a2a/a2a'
|
||||||
@@ -335,6 +337,20 @@ export function DeployModal({
|
|||||||
setDeployError(null)
|
setDeployError(null)
|
||||||
setDeployWarnings([])
|
setDeployWarnings([])
|
||||||
|
|
||||||
|
const { blocks, edges, loops, parallels } = useWorkflowStore.getState()
|
||||||
|
const liveBlocks = mergeSubblockState(blocks, workflowId)
|
||||||
|
const checkResult = runPreDeployChecks({
|
||||||
|
blocks: liveBlocks,
|
||||||
|
edges,
|
||||||
|
loops,
|
||||||
|
parallels,
|
||||||
|
workflowId,
|
||||||
|
})
|
||||||
|
if (!checkResult.passed) {
|
||||||
|
setDeployError(checkResult.error || 'Pre-deploy validation failed')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await deployMutation.mutateAsync({ workflowId, deployChatEnabled: false })
|
const result = await deployMutation.mutateAsync({ workflowId, deployChatEnabled: false })
|
||||||
if (result.warnings && result.warnings.length > 0) {
|
if (result.warnings && result.warnings.length > 0) {
|
||||||
|
|||||||
@@ -2,9 +2,6 @@ import { useCallback, useState } from 'react'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { useNotificationStore } from '@/stores/notifications'
|
import { useNotificationStore } from '@/stores/notifications'
|
||||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||||
import { mergeSubblockState } from '@/stores/workflows/utils'
|
|
||||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
|
||||||
import { runPreDeployChecks } from './use-predeploy-checks'
|
|
||||||
|
|
||||||
const logger = createLogger('useDeployment')
|
const logger = createLogger('useDeployment')
|
||||||
|
|
||||||
@@ -25,36 +22,16 @@ export function useDeployment({
|
|||||||
const [isDeploying, setIsDeploying] = useState(false)
|
const [isDeploying, setIsDeploying] = useState(false)
|
||||||
const setDeploymentStatus = useWorkflowRegistry((state) => state.setDeploymentStatus)
|
const setDeploymentStatus = useWorkflowRegistry((state) => state.setDeploymentStatus)
|
||||||
const addNotification = useNotificationStore((state) => state.addNotification)
|
const addNotification = useNotificationStore((state) => state.addNotification)
|
||||||
const blocks = useWorkflowStore((state) => state.blocks)
|
|
||||||
const edges = useWorkflowStore((state) => state.edges)
|
|
||||||
const loops = useWorkflowStore((state) => state.loops)
|
|
||||||
const parallels = useWorkflowStore((state) => state.parallels)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle deploy button click
|
* Handle deploy button click
|
||||||
* First deploy: calls API to deploy, then opens modal on success
|
* First deploy: calls API to deploy, then opens modal on success
|
||||||
* Redeploy: validates client-side, then opens modal if valid
|
* Already deployed: opens modal directly (validation happens on Update in modal)
|
||||||
*/
|
*/
|
||||||
const handleDeployClick = useCallback(async () => {
|
const handleDeployClick = useCallback(async () => {
|
||||||
if (!workflowId) return { success: false, shouldOpenModal: false }
|
if (!workflowId) return { success: false, shouldOpenModal: false }
|
||||||
|
|
||||||
if (isDeployed) {
|
if (isDeployed) {
|
||||||
const liveBlocks = mergeSubblockState(blocks, workflowId)
|
|
||||||
const checkResult = runPreDeployChecks({
|
|
||||||
blocks: liveBlocks,
|
|
||||||
edges,
|
|
||||||
loops,
|
|
||||||
parallels,
|
|
||||||
workflowId,
|
|
||||||
})
|
|
||||||
if (!checkResult.passed) {
|
|
||||||
addNotification({
|
|
||||||
level: 'error',
|
|
||||||
message: checkResult.error || 'Pre-deploy validation failed',
|
|
||||||
workflowId,
|
|
||||||
})
|
|
||||||
return { success: false, shouldOpenModal: false }
|
|
||||||
}
|
|
||||||
return { success: true, shouldOpenModal: true }
|
return { success: true, shouldOpenModal: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,17 +78,7 @@ export function useDeployment({
|
|||||||
} finally {
|
} finally {
|
||||||
setIsDeploying(false)
|
setIsDeploying(false)
|
||||||
}
|
}
|
||||||
}, [
|
}, [workflowId, isDeployed, refetchDeployedState, setDeploymentStatus, addNotification])
|
||||||
workflowId,
|
|
||||||
isDeployed,
|
|
||||||
blocks,
|
|
||||||
edges,
|
|
||||||
loops,
|
|
||||||
parallels,
|
|
||||||
refetchDeployedState,
|
|
||||||
setDeploymentStatus,
|
|
||||||
addNotification,
|
|
||||||
])
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isDeploying,
|
isDeploying,
|
||||||
|
|||||||
@@ -1,410 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import type React from 'react'
|
|
||||||
import { useCallback, useEffect, useRef } from 'react'
|
|
||||||
import { Combobox, Switch } from '@/components/emcn'
|
|
||||||
import {
|
|
||||||
CheckboxList,
|
|
||||||
Code,
|
|
||||||
DocumentSelector,
|
|
||||||
DocumentTagEntry,
|
|
||||||
FileSelectorInput,
|
|
||||||
FileUpload,
|
|
||||||
FolderSelectorInput,
|
|
||||||
KnowledgeBaseSelector,
|
|
||||||
KnowledgeTagFilters,
|
|
||||||
LongInput,
|
|
||||||
ProjectSelectorInput,
|
|
||||||
SheetSelectorInput,
|
|
||||||
ShortInput,
|
|
||||||
SlackSelectorInput,
|
|
||||||
SliderInput,
|
|
||||||
Table,
|
|
||||||
TimeInput,
|
|
||||||
WorkflowSelectorInput,
|
|
||||||
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components'
|
|
||||||
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
|
|
||||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
|
||||||
import type { WandControlHandlers } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block'
|
|
||||||
import type { SubBlockConfig as BlockSubBlockConfig } from '@/blocks/types'
|
|
||||||
import { isPasswordParameter } from '@/tools/params'
|
|
||||||
|
|
||||||
interface ToolSubBlockRendererProps {
|
|
||||||
blockId: string
|
|
||||||
subBlockId: string
|
|
||||||
toolIndex: number
|
|
||||||
subBlock: BlockSubBlockConfig
|
|
||||||
effectiveParamId: string
|
|
||||||
toolParams: Record<string, string> | undefined
|
|
||||||
onParamChange: (toolIndex: number, paramId: string, value: string) => void
|
|
||||||
disabled: boolean
|
|
||||||
previewContextValues?: Record<string, unknown>
|
|
||||||
wandControlRef?: React.MutableRefObject<WandControlHandlers | null>
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders a subblock component inside tool-input by bridging the subblock store
|
|
||||||
* with StoredTool.params via a synthetic store key.
|
|
||||||
*
|
|
||||||
* Replaces the 17+ individual SyncWrapper components that previously existed.
|
|
||||||
* Components read/write to the store at a synthetic ID, and two effects
|
|
||||||
* handle bidirectional sync with tool.params.
|
|
||||||
*/
|
|
||||||
export function ToolSubBlockRenderer({
|
|
||||||
blockId,
|
|
||||||
subBlockId,
|
|
||||||
toolIndex,
|
|
||||||
subBlock,
|
|
||||||
effectiveParamId,
|
|
||||||
toolParams,
|
|
||||||
onParamChange,
|
|
||||||
disabled,
|
|
||||||
previewContextValues,
|
|
||||||
wandControlRef,
|
|
||||||
}: ToolSubBlockRendererProps) {
|
|
||||||
const syntheticId = `${subBlockId}-tool-${toolIndex}-${effectiveParamId}`
|
|
||||||
const [storeValue, setStoreValue] = useSubBlockValue(blockId, syntheticId)
|
|
||||||
|
|
||||||
// Gate the component using the same dependsOn logic as SubBlock
|
|
||||||
const { finalDisabled } = useDependsOnGate(blockId, subBlock, {
|
|
||||||
disabled,
|
|
||||||
previewContextValues,
|
|
||||||
})
|
|
||||||
|
|
||||||
const toolParamValue = toolParams?.[effectiveParamId] ?? ''
|
|
||||||
|
|
||||||
/** Tracks the last value we wrote to the store from tool.params to avoid echo loops */
|
|
||||||
const lastInitRef = useRef<string>(toolParamValue)
|
|
||||||
/** Tracks the last value we synced back to tool.params from the store */
|
|
||||||
const lastSyncRef = useRef<string>(toolParamValue)
|
|
||||||
|
|
||||||
// Init effect: push tool.params value into the store when it changes externally
|
|
||||||
useEffect(() => {
|
|
||||||
if (toolParamValue !== lastInitRef.current) {
|
|
||||||
lastInitRef.current = toolParamValue
|
|
||||||
lastSyncRef.current = toolParamValue
|
|
||||||
setStoreValue(toolParamValue)
|
|
||||||
}
|
|
||||||
}, [toolParamValue, setStoreValue])
|
|
||||||
|
|
||||||
// Sync effect: when the store changes (user interaction), push back to tool.params
|
|
||||||
useEffect(() => {
|
|
||||||
if (storeValue == null) return
|
|
||||||
const stringValue = typeof storeValue === 'string' ? storeValue : JSON.stringify(storeValue)
|
|
||||||
if (stringValue !== lastSyncRef.current) {
|
|
||||||
lastSyncRef.current = stringValue
|
|
||||||
lastInitRef.current = stringValue
|
|
||||||
onParamChange(toolIndex, effectiveParamId, stringValue)
|
|
||||||
}
|
|
||||||
}, [storeValue, toolIndex, effectiveParamId, onParamChange])
|
|
||||||
|
|
||||||
// Initialize the store on first mount
|
|
||||||
const hasInitializedRef = useRef(false)
|
|
||||||
useEffect(() => {
|
|
||||||
if (!hasInitializedRef.current && toolParamValue) {
|
|
||||||
hasInitializedRef.current = true
|
|
||||||
setStoreValue(toolParamValue)
|
|
||||||
}
|
|
||||||
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
|
||||||
|
|
||||||
const configWithSyntheticId = { ...subBlock, id: syntheticId }
|
|
||||||
|
|
||||||
return renderSubBlockComponent({
|
|
||||||
blockId,
|
|
||||||
syntheticId,
|
|
||||||
config: configWithSyntheticId,
|
|
||||||
subBlock,
|
|
||||||
disabled: finalDisabled,
|
|
||||||
previewContextValues,
|
|
||||||
wandControlRef,
|
|
||||||
toolParamValue,
|
|
||||||
onParamChange: useCallback(
|
|
||||||
(value: string) => onParamChange(toolIndex, effectiveParamId, value),
|
|
||||||
[toolIndex, effectiveParamId, onParamChange]
|
|
||||||
),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RenderContext {
|
|
||||||
blockId: string
|
|
||||||
syntheticId: string
|
|
||||||
config: BlockSubBlockConfig
|
|
||||||
subBlock: BlockSubBlockConfig
|
|
||||||
disabled: boolean
|
|
||||||
previewContextValues?: Record<string, unknown>
|
|
||||||
wandControlRef?: React.MutableRefObject<WandControlHandlers | null>
|
|
||||||
toolParamValue: string
|
|
||||||
onParamChange: (value: string) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders the appropriate component for a subblock type.
|
|
||||||
* Mirrors the switch cases in SubBlock's renderInput(), using
|
|
||||||
* the same component props pattern.
|
|
||||||
*/
|
|
||||||
function renderSubBlockComponent(ctx: RenderContext): React.ReactNode {
|
|
||||||
const {
|
|
||||||
blockId,
|
|
||||||
syntheticId,
|
|
||||||
config,
|
|
||||||
subBlock,
|
|
||||||
disabled,
|
|
||||||
previewContextValues,
|
|
||||||
wandControlRef,
|
|
||||||
toolParamValue,
|
|
||||||
onParamChange,
|
|
||||||
} = ctx
|
|
||||||
|
|
||||||
switch (subBlock.type) {
|
|
||||||
case 'short-input':
|
|
||||||
return (
|
|
||||||
<ShortInput
|
|
||||||
blockId={blockId}
|
|
||||||
subBlockId={syntheticId}
|
|
||||||
placeholder={subBlock.placeholder}
|
|
||||||
password={subBlock.password || isPasswordParameter(subBlock.id)}
|
|
||||||
config={config}
|
|
||||||
disabled={disabled}
|
|
||||||
wandControlRef={wandControlRef}
|
|
||||||
hideInternalWand={true}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
case 'long-input':
|
|
||||||
return (
|
|
||||||
<LongInput
|
|
||||||
blockId={blockId}
|
|
||||||
subBlockId={syntheticId}
|
|
||||||
placeholder={subBlock.placeholder}
|
|
||||||
rows={subBlock.rows}
|
|
||||||
config={config}
|
|
||||||
disabled={disabled}
|
|
||||||
wandControlRef={wandControlRef}
|
|
||||||
hideInternalWand={true}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
case 'dropdown':
|
|
||||||
return (
|
|
||||||
<Combobox
|
|
||||||
options={
|
|
||||||
(subBlock.options as { label: string; id: string }[] | undefined)
|
|
||||||
?.filter((option) => option.id !== '')
|
|
||||||
.map((option) => ({
|
|
||||||
label: option.label,
|
|
||||||
value: option.id,
|
|
||||||
})) || []
|
|
||||||
}
|
|
||||||
value={toolParamValue}
|
|
||||||
onChange={onParamChange}
|
|
||||||
placeholder={subBlock.placeholder || 'Select option'}
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
case 'switch':
|
|
||||||
return (
|
|
||||||
<Switch
|
|
||||||
checked={toolParamValue === 'true' || toolParamValue === 'True'}
|
|
||||||
onCheckedChange={(checked) => onParamChange(checked ? 'true' : 'false')}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
case 'code':
|
|
||||||
return (
|
|
||||||
<Code
|
|
||||||
blockId={blockId}
|
|
||||||
subBlockId={syntheticId}
|
|
||||||
placeholder={subBlock.placeholder}
|
|
||||||
language={subBlock.language}
|
|
||||||
generationType={subBlock.generationType}
|
|
||||||
value={typeof subBlock.value === 'function' ? subBlock.value({}) : undefined}
|
|
||||||
disabled={disabled}
|
|
||||||
wandConfig={
|
|
||||||
subBlock.wandConfig || {
|
|
||||||
enabled: false,
|
|
||||||
prompt: '',
|
|
||||||
placeholder: '',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
wandControlRef={wandControlRef}
|
|
||||||
hideInternalWand={true}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
case 'channel-selector':
|
|
||||||
case 'user-selector':
|
|
||||||
return (
|
|
||||||
<SlackSelectorInput
|
|
||||||
blockId={blockId}
|
|
||||||
subBlock={config}
|
|
||||||
disabled={disabled}
|
|
||||||
previewContextValues={previewContextValues}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
case 'project-selector':
|
|
||||||
return (
|
|
||||||
<ProjectSelectorInput
|
|
||||||
blockId={blockId}
|
|
||||||
subBlock={config}
|
|
||||||
disabled={disabled}
|
|
||||||
previewContextValues={previewContextValues}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
case 'file-selector':
|
|
||||||
return (
|
|
||||||
<FileSelectorInput
|
|
||||||
blockId={blockId}
|
|
||||||
subBlock={config}
|
|
||||||
disabled={disabled}
|
|
||||||
previewContextValues={previewContextValues}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
case 'sheet-selector':
|
|
||||||
return (
|
|
||||||
<SheetSelectorInput
|
|
||||||
blockId={blockId}
|
|
||||||
subBlock={config}
|
|
||||||
disabled={disabled}
|
|
||||||
previewContextValues={previewContextValues}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
case 'folder-selector':
|
|
||||||
return (
|
|
||||||
<FolderSelectorInput
|
|
||||||
blockId={blockId}
|
|
||||||
subBlock={config}
|
|
||||||
disabled={disabled}
|
|
||||||
previewContextValues={previewContextValues}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
case 'knowledge-base-selector':
|
|
||||||
return <KnowledgeBaseSelector blockId={blockId} subBlock={config} disabled={disabled} />
|
|
||||||
|
|
||||||
case 'document-selector':
|
|
||||||
return (
|
|
||||||
<DocumentSelector
|
|
||||||
blockId={blockId}
|
|
||||||
subBlock={config}
|
|
||||||
disabled={disabled}
|
|
||||||
previewContextValues={previewContextValues}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
case 'document-tag-entry':
|
|
||||||
return (
|
|
||||||
<DocumentTagEntry
|
|
||||||
blockId={blockId}
|
|
||||||
subBlock={config}
|
|
||||||
disabled={disabled}
|
|
||||||
previewContextValues={previewContextValues}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
case 'knowledge-tag-filters':
|
|
||||||
return (
|
|
||||||
<KnowledgeTagFilters
|
|
||||||
blockId={blockId}
|
|
||||||
subBlock={config}
|
|
||||||
disabled={disabled}
|
|
||||||
previewContextValues={previewContextValues}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
case 'table':
|
|
||||||
return (
|
|
||||||
<Table
|
|
||||||
blockId={blockId}
|
|
||||||
subBlockId={syntheticId}
|
|
||||||
columns={subBlock.columns ?? []}
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
case 'slider':
|
|
||||||
return (
|
|
||||||
<SliderInput
|
|
||||||
blockId={blockId}
|
|
||||||
subBlockId={syntheticId}
|
|
||||||
min={subBlock.min}
|
|
||||||
max={subBlock.max}
|
|
||||||
step={subBlock.step}
|
|
||||||
integer={subBlock.integer}
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
case 'checkbox-list':
|
|
||||||
return (
|
|
||||||
<CheckboxList
|
|
||||||
blockId={blockId}
|
|
||||||
subBlockId={syntheticId}
|
|
||||||
title={subBlock.title ?? ''}
|
|
||||||
options={subBlock.options as { label: string; id: string }[]}
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
case 'time-input':
|
|
||||||
return (
|
|
||||||
<TimeInput
|
|
||||||
blockId={blockId}
|
|
||||||
subBlockId={syntheticId}
|
|
||||||
placeholder={subBlock.placeholder}
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
case 'file-upload':
|
|
||||||
return (
|
|
||||||
<FileUpload
|
|
||||||
blockId={blockId}
|
|
||||||
subBlockId={syntheticId}
|
|
||||||
acceptedTypes={subBlock.acceptedTypes || '*'}
|
|
||||||
multiple={subBlock.multiple === true}
|
|
||||||
maxSize={subBlock.maxSize}
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
case 'combobox':
|
|
||||||
return (
|
|
||||||
<Combobox
|
|
||||||
options={((subBlock.options as { label: string; id: string }[] | undefined) || []).map(
|
|
||||||
(opt) => ({
|
|
||||||
label: opt.label,
|
|
||||||
value: opt.id,
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
value={toolParamValue}
|
|
||||||
onChange={onParamChange}
|
|
||||||
placeholder={subBlock.placeholder || 'Select option'}
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
case 'workflow-selector':
|
|
||||||
return <WorkflowSelectorInput blockId={blockId} subBlock={config} disabled={disabled} />
|
|
||||||
|
|
||||||
case 'oauth-input':
|
|
||||||
// OAuth inputs are handled separately by ToolCredentialSelector in the parent
|
|
||||||
return null
|
|
||||||
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
<ShortInput
|
|
||||||
blockId={blockId}
|
|
||||||
subBlockId={syntheticId}
|
|
||||||
placeholder={subBlock.placeholder}
|
|
||||||
password={isPasswordParameter(subBlock.id)}
|
|
||||||
config={config}
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -196,8 +196,6 @@ export interface SubBlockConfig {
|
|||||||
type: SubBlockType
|
type: SubBlockType
|
||||||
mode?: 'basic' | 'advanced' | 'both' | 'trigger' // Default is 'both' if not specified. 'trigger' means only shown in trigger mode
|
mode?: 'basic' | 'advanced' | 'both' | 'trigger' // Default is 'both' if not specified. 'trigger' means only shown in trigger mode
|
||||||
canonicalParamId?: string
|
canonicalParamId?: string
|
||||||
/** Controls parameter visibility in agent/tool-input context */
|
|
||||||
paramVisibility?: 'user-or-llm' | 'user-only' | 'llm-only' | 'hidden'
|
|
||||||
required?:
|
required?:
|
||||||
| boolean
|
| boolean
|
||||||
| {
|
| {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Environment utility functions for consistent environment detection across the application
|
* Environment utility functions for consistent environment detection across the application
|
||||||
*/
|
*/
|
||||||
import { env, getEnv, isFalsy, isTruthy } from './env'
|
import { env, isFalsy, isTruthy } from './env'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is the application running in production mode
|
* Is the application running in production mode
|
||||||
@@ -21,9 +21,10 @@ export const isTest = env.NODE_ENV === 'test'
|
|||||||
/**
|
/**
|
||||||
* Is this the hosted version of the application
|
* Is this the hosted version of the application
|
||||||
*/
|
*/
|
||||||
export const isHosted =
|
// export const isHosted =
|
||||||
getEnv('NEXT_PUBLIC_APP_URL') === 'https://www.sim.ai' ||
|
// getEnv('NEXT_PUBLIC_APP_URL') === 'https://www.sim.ai' ||
|
||||||
getEnv('NEXT_PUBLIC_APP_URL') === 'https://www.staging.sim.ai'
|
// getEnv('NEXT_PUBLIC_APP_URL') === 'https://www.staging.sim.ai'
|
||||||
|
export const isHosted = true
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is billing enforcement enabled
|
* Is billing enforcement enabled
|
||||||
|
|||||||
@@ -111,6 +111,16 @@ function isFieldRequired(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolveTriggerId(block: BlockState): string | undefined {
|
function resolveTriggerId(block: BlockState): string | undefined {
|
||||||
|
const blockConfig = getBlock(block.type)
|
||||||
|
|
||||||
|
if (blockConfig?.category === 'triggers' && isTriggerValid(block.type)) {
|
||||||
|
return block.type
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!block.triggerMode) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
const selectedTriggerId = getSubBlockValue(block, 'selectedTriggerId')
|
const selectedTriggerId = getSubBlockValue(block, 'selectedTriggerId')
|
||||||
if (typeof selectedTriggerId === 'string' && isTriggerValid(selectedTriggerId)) {
|
if (typeof selectedTriggerId === 'string' && isTriggerValid(selectedTriggerId)) {
|
||||||
return selectedTriggerId
|
return selectedTriggerId
|
||||||
@@ -121,12 +131,7 @@ function resolveTriggerId(block: BlockState): string | undefined {
|
|||||||
return storedTriggerId
|
return storedTriggerId
|
||||||
}
|
}
|
||||||
|
|
||||||
const blockConfig = getBlock(block.type)
|
if (blockConfig?.triggers?.enabled) {
|
||||||
if (blockConfig?.category === 'triggers' && isTriggerValid(block.type)) {
|
|
||||||
return block.type
|
|
||||||
}
|
|
||||||
|
|
||||||
if (block.triggerMode && blockConfig?.triggers?.enabled) {
|
|
||||||
const configuredTriggerId =
|
const configuredTriggerId =
|
||||||
typeof selectedTriggerId === 'string' ? selectedTriggerId : undefined
|
typeof selectedTriggerId === 'string' ? selectedTriggerId : undefined
|
||||||
if (configuredTriggerId && isTriggerValid(configuredTriggerId)) {
|
if (configuredTriggerId && isTriggerValid(configuredTriggerId)) {
|
||||||
|
|||||||
@@ -62,6 +62,45 @@ const shouldSkipEntry = (output: any): boolean => {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface NotifyBlockErrorParams {
|
||||||
|
error: unknown
|
||||||
|
blockName: string
|
||||||
|
workflowId?: string
|
||||||
|
logContext: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends an error notification for a block failure if error notifications are enabled.
|
||||||
|
*/
|
||||||
|
const notifyBlockError = ({ error, blockName, workflowId, logContext }: NotifyBlockErrorParams) => {
|
||||||
|
const settings = getQueryClient().getQueryData<GeneralSettings>(generalSettingsKeys.settings())
|
||||||
|
const isErrorNotificationsEnabled = settings?.errorNotificationsEnabled ?? true
|
||||||
|
|
||||||
|
if (!isErrorNotificationsEnabled) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const errorMessage = String(error)
|
||||||
|
const displayName = blockName || 'Unknown Block'
|
||||||
|
const displayMessage = `${displayName}: ${errorMessage}`
|
||||||
|
const copilotMessage = `${errorMessage}\n\nError in ${displayName}.\n\nPlease fix this.`
|
||||||
|
|
||||||
|
useNotificationStore.getState().addNotification({
|
||||||
|
level: 'error',
|
||||||
|
message: displayMessage,
|
||||||
|
workflowId,
|
||||||
|
action: {
|
||||||
|
type: 'copilot',
|
||||||
|
message: copilotMessage,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (notificationError) {
|
||||||
|
logger.error('Failed to create block error notification', {
|
||||||
|
...logContext,
|
||||||
|
error: notificationError,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const useTerminalConsoleStore = create<ConsoleStore>()(
|
export const useTerminalConsoleStore = create<ConsoleStore>()(
|
||||||
devtools(
|
devtools(
|
||||||
persist(
|
persist(
|
||||||
@@ -154,35 +193,12 @@ export const useTerminalConsoleStore = create<ConsoleStore>()(
|
|||||||
const newEntry = get().entries[0]
|
const newEntry = get().entries[0]
|
||||||
|
|
||||||
if (newEntry?.error) {
|
if (newEntry?.error) {
|
||||||
const settings = getQueryClient().getQueryData<GeneralSettings>(
|
notifyBlockError({
|
||||||
generalSettingsKeys.settings()
|
error: newEntry.error,
|
||||||
)
|
blockName: newEntry.blockName || 'Unknown Block',
|
||||||
const isErrorNotificationsEnabled = settings?.errorNotificationsEnabled ?? true
|
workflowId: entry.workflowId,
|
||||||
|
logContext: { entryId: newEntry.id },
|
||||||
if (isErrorNotificationsEnabled) {
|
})
|
||||||
try {
|
|
||||||
const errorMessage = String(newEntry.error)
|
|
||||||
const blockName = newEntry.blockName || 'Unknown Block'
|
|
||||||
const displayMessage = `${blockName}: ${errorMessage}`
|
|
||||||
|
|
||||||
const copilotMessage = `${errorMessage}\n\nError in ${blockName}.\n\nPlease fix this.`
|
|
||||||
|
|
||||||
useNotificationStore.getState().addNotification({
|
|
||||||
level: 'error',
|
|
||||||
message: displayMessage,
|
|
||||||
workflowId: entry.workflowId,
|
|
||||||
action: {
|
|
||||||
type: 'copilot',
|
|
||||||
message: copilotMessage,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (notificationError) {
|
|
||||||
logger.error('Failed to create block error notification', {
|
|
||||||
entryId: newEntry.id,
|
|
||||||
error: notificationError,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return newEntry
|
return newEntry
|
||||||
@@ -376,6 +392,18 @@ export const useTerminalConsoleStore = create<ConsoleStore>()(
|
|||||||
|
|
||||||
return { entries: updatedEntries }
|
return { entries: updatedEntries }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (typeof update === 'object' && update.error) {
|
||||||
|
const matchingEntry = get().entries.find(
|
||||||
|
(e) => e.blockId === blockId && e.executionId === executionId
|
||||||
|
)
|
||||||
|
notifyBlockError({
|
||||||
|
error: update.error,
|
||||||
|
blockName: matchingEntry?.blockName || 'Unknown Block',
|
||||||
|
workflowId: matchingEntry?.workflowId,
|
||||||
|
logContext: { blockId },
|
||||||
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
cancelRunningEntries: (workflowId: string) => {
|
cancelRunningEntries: (workflowId: string) => {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
buildCanonicalIndex,
|
buildCanonicalIndex,
|
||||||
type CanonicalIndex,
|
type CanonicalIndex,
|
||||||
type CanonicalModeOverrides,
|
|
||||||
evaluateSubBlockCondition,
|
evaluateSubBlockCondition,
|
||||||
getCanonicalValues,
|
getCanonicalValues,
|
||||||
isCanonicalPair,
|
isCanonicalPair,
|
||||||
@@ -13,10 +12,7 @@ import type { SubBlockConfig as BlockSubBlockConfig } from '@/blocks/types'
|
|||||||
export {
|
export {
|
||||||
buildCanonicalIndex,
|
buildCanonicalIndex,
|
||||||
type CanonicalIndex,
|
type CanonicalIndex,
|
||||||
type CanonicalModeOverrides,
|
|
||||||
evaluateSubBlockCondition,
|
evaluateSubBlockCondition,
|
||||||
isCanonicalPair,
|
|
||||||
resolveCanonicalMode,
|
|
||||||
type SubBlockCondition,
|
type SubBlockCondition,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { extractInputFieldsFromBlocks } from '@/lib/workflows/input-format'
|
import { extractInputFieldsFromBlocks } from '@/lib/workflows/input-format'
|
||||||
import {
|
import {
|
||||||
buildCanonicalIndex,
|
|
||||||
type CanonicalModeOverrides,
|
|
||||||
evaluateSubBlockCondition,
|
evaluateSubBlockCondition,
|
||||||
isCanonicalPair,
|
|
||||||
resolveCanonicalMode,
|
|
||||||
type SubBlockCondition,
|
type SubBlockCondition,
|
||||||
} from '@/lib/workflows/subblocks/visibility'
|
} from '@/lib/workflows/subblocks/visibility'
|
||||||
import type { SubBlockConfig as BlockSubBlockConfig, GenerationType } from '@/blocks/types'
|
import type { SubBlockConfig as BlockSubBlockConfig } from '@/blocks/types'
|
||||||
import { safeAssign } from '@/tools/safe-assign'
|
import { safeAssign } from '@/tools/safe-assign'
|
||||||
import { isEmptyTagValue } from '@/tools/shared/tags'
|
import { isEmptyTagValue } from '@/tools/shared/tags'
|
||||||
import type { OAuthConfig, ParameterVisibility, ToolConfig } from '@/tools/types'
|
import type { ParameterVisibility, ToolConfig } from '@/tools/types'
|
||||||
import { getTool } from '@/tools/utils'
|
import { getTool } from '@/tools/utils'
|
||||||
|
|
||||||
const logger = createLogger('ToolsParams')
|
const logger = createLogger('ToolsParams')
|
||||||
@@ -68,14 +64,6 @@ export interface UIComponentConfig {
|
|||||||
mode?: 'basic' | 'advanced' | 'both' | 'trigger'
|
mode?: 'basic' | 'advanced' | 'both' | 'trigger'
|
||||||
/** The actual subblock ID this config was derived from */
|
/** The actual subblock ID this config was derived from */
|
||||||
actualSubBlockId?: string
|
actualSubBlockId?: string
|
||||||
/** Wand configuration for AI assistance */
|
|
||||||
wandConfig?: {
|
|
||||||
enabled: boolean
|
|
||||||
prompt: string
|
|
||||||
generationType?: GenerationType
|
|
||||||
placeholder?: string
|
|
||||||
maintainHistory?: boolean
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SubBlockConfig {
|
export interface SubBlockConfig {
|
||||||
@@ -339,7 +327,6 @@ export function getToolParametersConfig(
|
|||||||
canonicalParamId: subBlock.canonicalParamId,
|
canonicalParamId: subBlock.canonicalParamId,
|
||||||
mode: subBlock.mode,
|
mode: subBlock.mode,
|
||||||
actualSubBlockId: subBlock.id,
|
actualSubBlockId: subBlock.id,
|
||||||
wandConfig: subBlock.wandConfig,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -824,196 +811,3 @@ export function formatParameterLabel(paramId: string): string {
|
|||||||
// Simple case - just capitalize first letter
|
// Simple case - just capitalize first letter
|
||||||
return paramId.charAt(0).toUpperCase() + paramId.slice(1)
|
return paramId.charAt(0).toUpperCase() + paramId.slice(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* SubBlock IDs that are "structural" — they control tool routing or auth,
|
|
||||||
* not user-facing parameters. These are excluded from tool-input rendering
|
|
||||||
* unless they have an explicit paramVisibility set.
|
|
||||||
*/
|
|
||||||
const STRUCTURAL_SUBBLOCK_IDS = new Set(['operation', 'authMethod', 'destinationType'])
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SubBlock types that represent auth/credential inputs handled separately
|
|
||||||
* by the tool-input OAuth credential selector.
|
|
||||||
*/
|
|
||||||
const AUTH_SUBBLOCK_TYPES = new Set(['oauth-input'])
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SubBlock types that should never appear in tool-input context.
|
|
||||||
*/
|
|
||||||
const EXCLUDED_SUBBLOCK_TYPES = new Set([
|
|
||||||
'tool-input',
|
|
||||||
'skill-input',
|
|
||||||
'condition-input',
|
|
||||||
'eval-input',
|
|
||||||
'webhook-config',
|
|
||||||
'schedule-info',
|
|
||||||
'trigger-save',
|
|
||||||
'input-format',
|
|
||||||
'response-format',
|
|
||||||
'mcp-server-selector',
|
|
||||||
'mcp-tool-selector',
|
|
||||||
'mcp-dynamic-args',
|
|
||||||
'input-mapping',
|
|
||||||
'variables-input',
|
|
||||||
'messages-input',
|
|
||||||
'router-input',
|
|
||||||
'text',
|
|
||||||
])
|
|
||||||
|
|
||||||
export interface SubBlocksForToolInput {
|
|
||||||
toolConfig: ToolConfig
|
|
||||||
subBlocks: BlockSubBlockConfig[]
|
|
||||||
oauthConfig?: OAuthConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns filtered SubBlockConfig[] for rendering in tool-input context.
|
|
||||||
* Uses subblock definitions as the primary source of UI metadata,
|
|
||||||
* getting all features (wandConfig, rich conditions, dependsOn, etc.) for free.
|
|
||||||
*
|
|
||||||
* For blocks without paramVisibility annotations, falls back to inferring
|
|
||||||
* visibility from the tool's param definitions.
|
|
||||||
*/
|
|
||||||
export function getSubBlocksForToolInput(
|
|
||||||
toolId: string,
|
|
||||||
blockType: string,
|
|
||||||
currentValues?: Record<string, unknown>,
|
|
||||||
canonicalModeOverrides?: CanonicalModeOverrides
|
|
||||||
): SubBlocksForToolInput | null {
|
|
||||||
try {
|
|
||||||
const toolConfig = getTool(toolId)
|
|
||||||
if (!toolConfig) {
|
|
||||||
logger.warn(`Tool not found: ${toolId}`)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const blockConfigs = getBlockConfigurations()
|
|
||||||
const blockConfig = blockConfigs[blockType]
|
|
||||||
if (!blockConfig?.subBlocks?.length) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const allSubBlocks = blockConfig.subBlocks as BlockSubBlockConfig[]
|
|
||||||
const canonicalIndex = buildCanonicalIndex(allSubBlocks)
|
|
||||||
|
|
||||||
// Build values for condition evaluation
|
|
||||||
const values = currentValues || {}
|
|
||||||
const valuesWithOperation = { ...values }
|
|
||||||
if (valuesWithOperation.operation === undefined) {
|
|
||||||
const parts = toolId.split('_')
|
|
||||||
valuesWithOperation.operation =
|
|
||||||
parts.length >= 3 ? parts.slice(2).join('_') : parts[parts.length - 1]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build a set of param IDs from the tool config for fallback visibility inference
|
|
||||||
const toolParamIds = new Set(Object.keys(toolConfig.params || {}))
|
|
||||||
const toolParamVisibility: Record<string, ParameterVisibility> = {}
|
|
||||||
for (const [paramId, param] of Object.entries(toolConfig.params || {})) {
|
|
||||||
toolParamVisibility[paramId] =
|
|
||||||
param.visibility ?? (param.required ? 'user-or-llm' : 'user-only')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track which canonical groups we've already included (to avoid duplicates)
|
|
||||||
const includedCanonicalIds = new Set<string>()
|
|
||||||
|
|
||||||
const filtered: BlockSubBlockConfig[] = []
|
|
||||||
|
|
||||||
for (const sb of allSubBlocks) {
|
|
||||||
// Skip excluded types
|
|
||||||
if (EXCLUDED_SUBBLOCK_TYPES.has(sb.type)) continue
|
|
||||||
|
|
||||||
// Skip trigger-mode-only subblocks
|
|
||||||
if (sb.mode === 'trigger') continue
|
|
||||||
|
|
||||||
// Determine the effective param ID (canonical or subblock id)
|
|
||||||
const effectiveParamId = sb.canonicalParamId || sb.id
|
|
||||||
|
|
||||||
// Resolve paramVisibility: explicit > inferred from tool params > skip
|
|
||||||
let visibility = sb.paramVisibility
|
|
||||||
if (!visibility) {
|
|
||||||
// Infer from structural checks
|
|
||||||
if (STRUCTURAL_SUBBLOCK_IDS.has(sb.id)) {
|
|
||||||
visibility = 'hidden'
|
|
||||||
} else if (AUTH_SUBBLOCK_TYPES.has(sb.type)) {
|
|
||||||
visibility = 'hidden'
|
|
||||||
} else if (
|
|
||||||
sb.password &&
|
|
||||||
(sb.id === 'botToken' || sb.id === 'accessToken' || sb.id === 'apiKey')
|
|
||||||
) {
|
|
||||||
// Auth tokens without explicit paramVisibility are hidden
|
|
||||||
// (they're handled by the OAuth credential selector or structurally)
|
|
||||||
// But only if they don't have a matching tool param
|
|
||||||
if (!toolParamIds.has(sb.id)) {
|
|
||||||
visibility = 'hidden'
|
|
||||||
} else {
|
|
||||||
visibility = toolParamVisibility[sb.id] || 'user-or-llm'
|
|
||||||
}
|
|
||||||
} else if (toolParamIds.has(effectiveParamId)) {
|
|
||||||
// Fallback: infer from tool param visibility
|
|
||||||
visibility = toolParamVisibility[effectiveParamId]
|
|
||||||
} else if (toolParamIds.has(sb.id)) {
|
|
||||||
visibility = toolParamVisibility[sb.id]
|
|
||||||
} else {
|
|
||||||
// SubBlock has no corresponding tool param — skip it
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter by visibility: exclude hidden and llm-only
|
|
||||||
if (visibility === 'hidden' || visibility === 'llm-only') continue
|
|
||||||
|
|
||||||
// Evaluate condition against current values
|
|
||||||
if (sb.condition) {
|
|
||||||
const conditionMet = evaluateSubBlockCondition(
|
|
||||||
sb.condition as SubBlockCondition,
|
|
||||||
valuesWithOperation
|
|
||||||
)
|
|
||||||
if (!conditionMet) continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle canonical pairs: only include the active mode variant
|
|
||||||
const canonicalId = canonicalIndex.canonicalIdBySubBlockId[sb.id]
|
|
||||||
if (canonicalId) {
|
|
||||||
const group = canonicalIndex.groupsById[canonicalId]
|
|
||||||
if (group && isCanonicalPair(group)) {
|
|
||||||
if (includedCanonicalIds.has(canonicalId)) continue
|
|
||||||
includedCanonicalIds.add(canonicalId)
|
|
||||||
|
|
||||||
// Determine active mode
|
|
||||||
const mode = resolveCanonicalMode(group, valuesWithOperation, canonicalModeOverrides)
|
|
||||||
if (mode === 'advanced') {
|
|
||||||
// Find the advanced variant
|
|
||||||
const advancedSb = allSubBlocks.find((s) => group.advancedIds.includes(s.id))
|
|
||||||
if (advancedSb) {
|
|
||||||
filtered.push(advancedSb)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Include basic variant (current sb if it's the basic one)
|
|
||||||
if (group.basicId === sb.id) {
|
|
||||||
filtered.push(sb)
|
|
||||||
} else {
|
|
||||||
const basicSb = allSubBlocks.find((s) => s.id === group.basicId)
|
|
||||||
if (basicSb) {
|
|
||||||
filtered.push(basicSb)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Non-canonical, non-hidden, condition-passing subblock
|
|
||||||
filtered.push(sb)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
toolConfig,
|
|
||||||
subBlocks: filtered,
|
|
||||||
oauthConfig: toolConfig.oauth,
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error getting subblocks for tool input:', error)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user