mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9fcd02fd3b | ||
|
|
ff7b5b528c | ||
|
|
30f2d1a0fc | ||
|
|
4bd0731871 | ||
|
|
4f3bc37fe4 | ||
|
|
84d6fdc423 | ||
|
|
4c12914d35 | ||
|
|
e9bdc57616 | ||
|
|
36612ae42a | ||
|
|
1c2c2c65d4 | ||
|
|
ecd3536a72 | ||
|
|
8c0a2e04b1 | ||
|
|
6586c5ce40 | ||
|
|
3ce947566d | ||
|
|
70c36cb7aa | ||
|
|
f1ec5fe824 | ||
|
|
e07e3c34cc | ||
|
|
0d2e6ff31d | ||
|
|
4fd0989264 | ||
|
|
67f8a687f6 | ||
|
|
af592349d3 | ||
|
|
0d86ea01f0 | ||
|
|
115f04e989 | ||
|
|
34d92fae89 | ||
|
|
67aa4bb332 | ||
|
|
15ace5e63f | ||
|
|
fdca73679d | ||
|
|
da46a387c9 | ||
|
|
b7e377ec4b |
@@ -4140,15 +4140,14 @@ export function IncidentioIcon(props: SVGProps<SVGSVGElement>) {
|
||||
|
||||
export function InfisicalIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='20 25 233 132' xmlns='http://www.w3.org/2000/svg'>
|
||||
<svg {...props} viewBox='0 0 273 182' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
d='m191.6 39.4c-20.3 0-37.15 13.21-52.9 30.61-12.99-16.4-29.8-30.61-51.06-30.61-27.74 0-50.44 23.86-50.44 51.33 0 26.68 21.43 51.8 48.98 51.8 20.55 0 37.07-13.86 51.32-31.81 12.69 16.97 29.1 31.41 53.2 31.41 27.13 0 49.85-22.96 49.85-51.4 0-27.12-20.44-51.33-48.95-51.33zm-104.3 77.94c-14.56 0-25.51-12.84-25.51-26.07 0-13.7 10.95-28.29 25.51-28.29 14.93 0 25.71 11.6 37.6 27.34-11.31 15.21-22.23 27.02-37.6 27.02zm104.4 0.25c-15 0-25.28-11.13-37.97-27.37 12.69-16.4 22.01-27.24 37.59-27.24 14.97 0 24.79 13.25 24.79 27.26 0 13-10.17 27.35-24.41 27.35z'
|
||||
fill='black'
|
||||
fill='currentColor'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function IntercomIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
@@ -4254,7 +4253,7 @@ export function ZoomIcon(props: SVGProps<SVGSVGElement>) {
|
||||
fill='currentColor'
|
||||
width='800px'
|
||||
height='800px'
|
||||
viewBox='-1 9.5 34 13'
|
||||
viewBox='0 0 32 32'
|
||||
version='1.1'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
|
||||
@@ -5,7 +5,7 @@ description: Manage users and groups in Azure AD (Microsoft Entra ID)
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
<BlockInfoCard
|
||||
type="microsoft_ad"
|
||||
color="#0078D4"
|
||||
/>
|
||||
|
||||
@@ -5,7 +5,7 @@ description: Manage users and groups in Okta
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
<BlockInfoCard
|
||||
type="okta"
|
||||
color="#191919"
|
||||
/>
|
||||
@@ -29,7 +29,6 @@ In Sim, the Okta integration enables your agents to automate identity management
|
||||
If you encounter issues with the Okta integration, contact us at [help@sim.ai](mailto:help@sim.ai)
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Okta identity management into your workflow. List, create, update, activate, suspend, and delete users. Reset passwords. Manage groups and group membership.
|
||||
|
||||
@@ -612,9 +612,7 @@ export default function Enterprise() {
|
||||
Ready for growth?
|
||||
</p>
|
||||
<Link
|
||||
href='https://form.typeform.com/to/jqCO12pF'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
href='/contact'
|
||||
className='group/cta inline-flex h-[32px] 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]'
|
||||
>
|
||||
Book a demo
|
||||
|
||||
@@ -23,7 +23,7 @@ const PRODUCT_LINKS: FooterItem[] = [
|
||||
|
||||
const RESOURCES_LINKS: FooterItem[] = [
|
||||
{ label: 'Blog', href: '/blog' },
|
||||
// { label: 'Templates', href: '/templates' },
|
||||
{ label: 'Templates', href: '/templates' },
|
||||
{ label: 'Docs', href: 'https://docs.sim.ai', external: true },
|
||||
{ label: 'Careers', href: 'https://jobs.ashbyhq.com/sim', external: true },
|
||||
{ label: 'Changelog', href: '/changelog' },
|
||||
|
||||
@@ -78,7 +78,7 @@ const PRICING_TIERS: PricingTier[] = [
|
||||
'SSO & SCIM · SOC2 & HIPAA',
|
||||
'Self hosting · Dedicated support',
|
||||
],
|
||||
cta: { label: 'Book a demo', href: 'https://form.typeform.com/to/jqCO12pF' },
|
||||
cta: { label: 'Book a demo', href: '/contact' },
|
||||
},
|
||||
]
|
||||
|
||||
@@ -125,14 +125,12 @@ function PricingCard({ tier }: PricingCardProps) {
|
||||
</p>
|
||||
<div className='mt-4'>
|
||||
{isEnterprise ? (
|
||||
<a
|
||||
<Link
|
||||
href={tier.cta.href}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='flex h-[32px] w-full items-center justify-center rounded-[5px] border border-[#E5E5E5] px-[10px] font-[430] font-season text-[#1C1C1C] text-[14px] transition-colors hover:bg-[#F0F0F0]'
|
||||
>
|
||||
{tier.cta.label}
|
||||
</a>
|
||||
</Link>
|
||||
) : isPro ? (
|
||||
<Link
|
||||
href={tier.cta.href}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getNavBlogPosts } from '@/lib/blog/registry'
|
||||
import { getAllPostMeta } from '@/lib/blog/registry'
|
||||
import { martianMono } from '@/app/_styles/fonts/martian-mono/martian-mono'
|
||||
import { season } from '@/app/_styles/fonts/season/season'
|
||||
import {
|
||||
@@ -33,7 +33,12 @@ import {
|
||||
* enterprise (Enterprise) -> pricing (Pricing) -> testimonials (Testimonials).
|
||||
*/
|
||||
export default async function Landing() {
|
||||
const blogPosts = await getNavBlogPosts()
|
||||
const allPosts = await getAllPostMeta()
|
||||
const featuredPost = allPosts.find((p) => p.featured) ?? allPosts[0]
|
||||
const recentPosts = allPosts.filter((p) => p !== featuredPost).slice(0, 4)
|
||||
const blogPosts = [featuredPost, ...recentPosts]
|
||||
.filter(Boolean)
|
||||
.map((p) => ({ slug: p.slug, title: p.title, ogImage: p.ogImage }))
|
||||
|
||||
return (
|
||||
<div className={`${season.variable} ${martianMono.variable} min-h-screen bg-[#1C1C1C]`}>
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { getNavBlogPosts } from '@/lib/blog/registry'
|
||||
import Footer from '@/app/(home)/components/footer/footer'
|
||||
import Navbar from '@/app/(home)/components/navbar/navbar'
|
||||
|
||||
export default async function StudioLayout({ children }: { children: React.ReactNode }) {
|
||||
const blogPosts = await getNavBlogPosts()
|
||||
export default function StudioLayout({ children }: { children: React.ReactNode }) {
|
||||
const orgJsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Organization',
|
||||
@@ -36,7 +34,7 @@ export default async function StudioLayout({ children }: { children: React.React
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(websiteJsonLd) }}
|
||||
/>
|
||||
<header>
|
||||
<Navbar blogPosts={blogPosts} />
|
||||
<Navbar />
|
||||
</header>
|
||||
<main className='relative flex-1'>{children}</main>
|
||||
<Footer />
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
|
||||
interface ExternalRedirectProps {
|
||||
url: string
|
||||
}
|
||||
|
||||
/** Redirects to an external URL when it is configured via an environment variable. */
|
||||
export default function ExternalRedirect({ url }: ExternalRedirectProps) {
|
||||
useEffect(() => {
|
||||
if (url?.startsWith('http')) {
|
||||
window.location.href = url
|
||||
}
|
||||
}, [url])
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import Background from '@/app/(landing)/components/background/background'
|
||||
import ExternalRedirect from '@/app/(landing)/components/external-redirect'
|
||||
import Footer from '@/app/(landing)/components/footer/footer'
|
||||
import Hero from '@/app/(landing)/components/hero/hero'
|
||||
import Integrations from '@/app/(landing)/components/integrations/integrations'
|
||||
@@ -21,5 +20,4 @@ export {
|
||||
Footer,
|
||||
StructuredData,
|
||||
LegalLayout,
|
||||
ExternalRedirect,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { getNavBlogPosts } from '@/lib/blog/registry'
|
||||
import { isHosted } from '@/lib/core/config/feature-flags'
|
||||
import Footer from '@/app/(home)/components/footer/footer'
|
||||
import Navbar from '@/app/(home)/components/navbar/navbar'
|
||||
@@ -8,13 +7,11 @@ interface LegalLayoutProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export default async function LegalLayout({ title, children }: LegalLayoutProps) {
|
||||
const blogPosts = await getNavBlogPosts()
|
||||
|
||||
export default function LegalLayout({ title, children }: LegalLayoutProps) {
|
||||
return (
|
||||
<main className='min-h-screen bg-[#1C1C1C] font-[430] font-season text-[#ECECEC]'>
|
||||
<header>
|
||||
<Navbar blogPosts={blogPosts} />
|
||||
<Navbar />
|
||||
</header>
|
||||
|
||||
<div className='mx-auto max-w-[800px] px-6 pt-[60px] pb-[80px] sm:px-12'>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { TEMPLATES } from '@/app/workspace/[workspaceId]/home/components/templat
|
||||
import { IntegrationIcon } from '../components/integration-icon'
|
||||
import { blockTypeToIconMap } from '../data/icon-mapping'
|
||||
import integrations from '../data/integrations.json'
|
||||
import { POPULAR_WORKFLOWS } from '../data/popular-workflows'
|
||||
import type { AuthType, FAQItem, Integration } from '../data/types'
|
||||
import { IntegrationFAQ } from './components/integration-faq'
|
||||
import { TemplateCardButton } from './components/template-card-button'
|
||||
@@ -13,52 +14,44 @@ const allIntegrations = integrations as Integration[]
|
||||
const INTEGRATION_COUNT = allIntegrations.length
|
||||
|
||||
/** Fast O(1) lookups — avoids repeated linear scans inside render loops. */
|
||||
const byName = new Map(allIntegrations.map((i) => [i.name, i]))
|
||||
const bySlug = new Map(allIntegrations.map((i) => [i.slug, i]))
|
||||
const byType = new Map(allIntegrations.map((i) => [i.type, i]))
|
||||
|
||||
/** Returns workflow pairs that feature the given integration on either side. */
|
||||
function getPairsFor(name: string) {
|
||||
return POPULAR_WORKFLOWS.filter((p) => p.from === name || p.to === name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns up to `limit` related integration slugs.
|
||||
*
|
||||
* Scoring (additive):
|
||||
* +3 per shared operation name — strongest signal (same capability)
|
||||
* +2 per shared operation word — weaker signal (e.g. both have "create" ops)
|
||||
* +1 same auth type — comparable setup experience
|
||||
* Scoring:
|
||||
* +100 — integration appears as a workflow pair partner (explicit editorial signal)
|
||||
* +N — N operation names shared with the current integration (semantic similarity)
|
||||
*
|
||||
* Every integration gets a score, so the sidebar always has suggestions.
|
||||
* Ties are broken by alphabetical slug order for determinism.
|
||||
* This means genuine partners always rank first; operation-similar integrations
|
||||
* (e.g. Slack → Teams → Discord for "Send Message") fill the rest organically.
|
||||
*/
|
||||
function getRelatedSlugs(
|
||||
name: string,
|
||||
slug: string,
|
||||
operations: Integration['operations'],
|
||||
authType: AuthType,
|
||||
limit = 6
|
||||
): string[] {
|
||||
const currentOpNames = new Set(operations.map((o) => o.name.toLowerCase()))
|
||||
const currentOpWords = new Set(
|
||||
operations.flatMap((o) =>
|
||||
o.name
|
||||
.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.filter((w) => w.length > 3)
|
||||
)
|
||||
)
|
||||
const partners = new Set(getPairsFor(name).map((p) => (p.from === name ? p.to : p.from)))
|
||||
const currentOps = new Set(operations.map((o) => o.name.toLowerCase()))
|
||||
|
||||
return allIntegrations
|
||||
.filter((i) => i.slug !== slug)
|
||||
.map((i) => {
|
||||
const sharedNames = i.operations.filter((o) =>
|
||||
currentOpNames.has(o.name.toLowerCase())
|
||||
).length
|
||||
const sharedWords = i.operations.filter((o) =>
|
||||
o.name
|
||||
.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.some((w) => w.length > 3 && currentOpWords.has(w))
|
||||
).length
|
||||
const sameAuth = i.authType === authType ? 1 : 0
|
||||
return { slug: i.slug, score: sharedNames * 3 + sharedWords * 2 + sameAuth }
|
||||
})
|
||||
.sort((a, b) => b.score - a.score || a.slug.localeCompare(b.slug))
|
||||
.map((i) => ({
|
||||
slug: i.slug,
|
||||
score:
|
||||
(partners.has(i.name) ? 100 : 0) +
|
||||
i.operations.filter((o) => currentOps.has(o.name.toLowerCase())).length,
|
||||
}))
|
||||
.filter(({ score }) => score > 0)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, limit)
|
||||
.map(({ slug: s }) => s)
|
||||
}
|
||||
@@ -77,6 +70,7 @@ function buildFAQs(integration: Integration): FAQItem[] {
|
||||
const { name, description, operations, triggers, authType } = integration
|
||||
const topOps = operations.slice(0, 5)
|
||||
const topOpNames = topOps.map((o) => o.name)
|
||||
const pairs = getPairsFor(name)
|
||||
const authStep = AUTH_STEP[authType]
|
||||
|
||||
const faqs: FAQItem[] = [
|
||||
@@ -95,10 +89,6 @@ function buildFAQs(integration: Integration): FAQItem[] {
|
||||
question: `How do I connect ${name} to Sim?`,
|
||||
answer: `Getting started takes under five minutes: (1) Create a free account at sim.ai. (2) Open a new workflow. (3) Drag a ${name} block onto the canvas. (4) ${authStep} (5) Choose the tool you want to use, wire it to the inputs you need, and click Run. Your automation is live.`,
|
||||
},
|
||||
{
|
||||
question: `Can I use ${name} as a tool inside an AI agent in Sim?`,
|
||||
answer: `Yes — this is one of Sim's core capabilities. Instead of hard-coding when and how ${name} is used, you give an AI agent access to ${name} tools and describe the goal in plain language. The agent decides which tools to call, in what order, and how to handle the results. This means your automation adapts to context rather than breaking when inputs change.`,
|
||||
},
|
||||
...(topOpNames.length >= 2
|
||||
? [
|
||||
{
|
||||
@@ -107,15 +97,19 @@ function buildFAQs(integration: Integration): FAQItem[] {
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(pairs.length > 0
|
||||
? [
|
||||
{
|
||||
question: `Can I connect ${name} to ${pairs[0].from === name ? pairs[0].to : pairs[0].from} with Sim?`,
|
||||
answer: `Yes. ${pairs[0].description} In Sim, you set this up by adding both a ${name} block and a ${pairs[0].from === name ? pairs[0].to : pairs[0].from} block to the same workflow and connecting them through an AI agent that orchestrates the logic between them.`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(triggers.length > 0
|
||||
? [
|
||||
{
|
||||
question: `How do I trigger a Sim workflow from ${name} automatically?`,
|
||||
answer: `In your Sim workflow, switch the ${name} block to Trigger mode and copy the generated webhook URL. Paste that URL into ${name}'s webhook settings and select the events you want to listen for (${triggers.map((t) => t.name).join(', ')}). From that point on, every matching event in ${name} instantly fires your workflow — no polling, no delay.`,
|
||||
},
|
||||
{
|
||||
question: `What data does Sim receive when a ${name} event triggers a workflow?`,
|
||||
answer: `When ${name} fires a webhook, Sim receives the full event payload that ${name} sends — typically the record or object that changed, along with metadata like the event type and timestamp. Inside your workflow, every field from that payload is available as a variable you can pass to AI agents, conditions, or other integrations.`,
|
||||
question: `Can ${name} trigger a Sim workflow automatically?`,
|
||||
answer: `Yes. ${name} supports ${triggers.length} webhook trigger${triggers.length === 1 ? '' : 's'} that can instantly start a Sim workflow: ${triggers.map((t) => t.name).join(', ')}. No polling needed — the workflow fires the moment the event occurs in ${name}.`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
@@ -196,10 +190,11 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
|
||||
|
||||
const IconComponent = blockTypeToIconMap[integration.type]
|
||||
const faqs = buildFAQs(integration)
|
||||
const relatedSlugs = getRelatedSlugs(slug, operations, authType)
|
||||
const relatedSlugs = getRelatedSlugs(name, slug, operations)
|
||||
const relatedIntegrations = relatedSlugs
|
||||
.map((s) => bySlug.get(s))
|
||||
.filter((i): i is Integration => i !== undefined)
|
||||
const featuredPairs = getPairsFor(name)
|
||||
const baseType = integration.type.replace(/_v\d+$/, '')
|
||||
const matchingTemplates = TEMPLATES.filter(
|
||||
(t) =>
|
||||
@@ -425,18 +420,15 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
|
||||
{triggers.length > 0 && (
|
||||
<section aria-labelledby='triggers-heading'>
|
||||
<h2 id='triggers-heading' className='mb-2 font-[500] text-[#ECECEC] text-[20px]'>
|
||||
Real-time triggers
|
||||
Triggers
|
||||
</h2>
|
||||
<p className='mb-4 text-[#999] text-[14px] leading-relaxed'>
|
||||
Connect a {name} webhook to Sim and your workflow fires the instant an event
|
||||
happens — no polling, no delay. Sim receives the full event payload and makes
|
||||
every field available as a variable inside your workflow.
|
||||
<p className='mb-6 text-[#999] text-[14px]'>
|
||||
These events in {name} can automatically start a Sim workflow — no polling
|
||||
required.
|
||||
</p>
|
||||
|
||||
{/* Event cards */}
|
||||
<ul
|
||||
className='grid grid-cols-1 gap-3 sm:grid-cols-2'
|
||||
aria-label={`${name} trigger events`}
|
||||
aria-label={`${name} triggers`}
|
||||
>
|
||||
{triggers.map((trigger) => (
|
||||
<li
|
||||
@@ -455,7 +447,7 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
|
||||
>
|
||||
<polygon points='13 2 3 14 12 14 11 22 21 10 12 10 13 2' />
|
||||
</svg>
|
||||
Event
|
||||
Trigger
|
||||
</span>
|
||||
</div>
|
||||
<p className='font-[500] text-[#ECECEC] text-[13px]'>{trigger.name}</p>
|
||||
@@ -470,6 +462,73 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Popular workflows featuring this integration */}
|
||||
{featuredPairs.length > 0 && (
|
||||
<section aria-labelledby='workflows-heading'>
|
||||
<h2 id='workflows-heading' className='mb-2 font-[500] text-[#ECECEC] text-[20px]'>
|
||||
Popular workflows with {name}
|
||||
</h2>
|
||||
<p className='mb-6 text-[#999] text-[14px]'>
|
||||
Common automation patterns teams build on Sim using {name}.
|
||||
</p>
|
||||
<ul
|
||||
className='grid grid-cols-1 gap-4 sm:grid-cols-2'
|
||||
aria-label='Popular workflow combinations'
|
||||
>
|
||||
{featuredPairs.map(({ from, to, headline, description: desc }) => {
|
||||
const fromInt = byName.get(from)
|
||||
const toInt = byName.get(to)
|
||||
const FromIcon = fromInt ? blockTypeToIconMap[fromInt.type] : undefined
|
||||
const ToIcon = toInt ? blockTypeToIconMap[toInt.type] : undefined
|
||||
const fromBg = fromInt?.bgColor ?? '#6B7280'
|
||||
const toBg = toInt?.bgColor ?? '#6B7280'
|
||||
|
||||
return (
|
||||
<li key={`${from}-${to}`}>
|
||||
<div className='h-full rounded-lg border border-[#2A2A2A] bg-[#242424] p-5'>
|
||||
<div className='mb-3 flex items-center gap-2 text-[12px]'>
|
||||
<span className='inline-flex items-center gap-1 rounded-[3px] bg-[#2A2A2A] px-1.5 py-0.5 font-[500] text-[#ECECEC]'>
|
||||
{FromIcon && (
|
||||
<IntegrationIcon
|
||||
bgColor={fromBg}
|
||||
name={from}
|
||||
Icon={FromIcon}
|
||||
as='span'
|
||||
className='h-3.5 w-3.5 rounded-[2px]'
|
||||
iconClassName='h-2.5 w-2.5'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
)}
|
||||
{from}
|
||||
</span>
|
||||
<span className='text-[#555]' aria-hidden='true'>
|
||||
→
|
||||
</span>
|
||||
<span className='inline-flex items-center gap-1 rounded-[3px] bg-[#2A2A2A] px-1.5 py-0.5 font-[500] text-[#ECECEC]'>
|
||||
{ToIcon && (
|
||||
<IntegrationIcon
|
||||
bgColor={toBg}
|
||||
name={to}
|
||||
Icon={ToIcon}
|
||||
as='span'
|
||||
className='h-3.5 w-3.5 rounded-[2px]'
|
||||
iconClassName='h-2.5 w-2.5'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
)}
|
||||
{to}
|
||||
</span>
|
||||
</div>
|
||||
<p className='mb-1 font-[500] text-[#ECECEC] text-[14px]'>{headline}</p>
|
||||
<p className='text-[#999] text-[13px] leading-relaxed'>{desc}</p>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Workflow templates */}
|
||||
{matchingTemplates.length > 0 && (
|
||||
<section aria-labelledby='templates-heading'>
|
||||
@@ -480,7 +539,7 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
|
||||
Ready-to-use workflows featuring {name}. Click any to build it instantly.
|
||||
</p>
|
||||
<ul
|
||||
className='grid grid-cols-1 gap-4 sm:grid-cols-2'
|
||||
className='grid grid-cols-1 gap-3 sm:grid-cols-2'
|
||||
aria-label='Workflow templates'
|
||||
>
|
||||
{matchingTemplates.map((template) => {
|
||||
@@ -492,49 +551,34 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
|
||||
return (
|
||||
<li key={template.title}>
|
||||
<TemplateCardButton prompt={template.prompt}>
|
||||
{/* Integration pills row */}
|
||||
<div className='mb-3 flex flex-wrap items-center gap-1.5 text-[12px]'>
|
||||
{allTypes.map((bt, idx) => {
|
||||
// Templates may use unversioned keys (e.g. "notion") while the
|
||||
// icon map has versioned keys ("notion_v2") — fall back to _v2.
|
||||
const resolvedBt = byType.get(bt)
|
||||
? bt
|
||||
: byType.get(`${bt}_v2`)
|
||||
? `${bt}_v2`
|
||||
: bt
|
||||
const int = byType.get(resolvedBt)
|
||||
{/* Integration icons */}
|
||||
<div className='mb-3 flex items-center gap-1.5'>
|
||||
{allTypes.map((bt) => {
|
||||
const int = byType.get(bt)
|
||||
const intName = int?.name ?? bt
|
||||
return (
|
||||
<span key={bt} className='inline-flex items-center gap-1.5'>
|
||||
{idx > 0 && (
|
||||
<span className='text-[#555]' aria-hidden='true'>
|
||||
→
|
||||
</span>
|
||||
)}
|
||||
<span className='inline-flex items-center gap-1 rounded-[3px] bg-[#2A2A2A] px-1.5 py-0.5 font-[500] text-[#ECECEC]'>
|
||||
<IntegrationIcon
|
||||
bgColor={int?.bgColor ?? '#6B7280'}
|
||||
name={intName}
|
||||
Icon={blockTypeToIconMap[resolvedBt]}
|
||||
as='span'
|
||||
className='h-3.5 w-3.5 rounded-[2px]'
|
||||
iconClassName='h-2.5 w-2.5'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
{intName}
|
||||
</span>
|
||||
</span>
|
||||
<IntegrationIcon
|
||||
key={bt}
|
||||
bgColor={int?.bgColor ?? '#6B7280'}
|
||||
name={intName}
|
||||
Icon={blockTypeToIconMap[bt]}
|
||||
className='h-6 w-6 rounded-[5px]'
|
||||
iconClassName='h-3.5 w-3.5'
|
||||
fallbackClassName='text-[9px]'
|
||||
title={intName}
|
||||
aria-label={intName}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<p className='mb-1 font-[500] text-[#ECECEC] text-[14px]'>
|
||||
<p className='mb-3 font-[500] text-[#ECECEC] text-[13px] leading-snug'>
|
||||
{template.title}
|
||||
</p>
|
||||
|
||||
<p className='mt-3 text-[#555] text-[13px] transition-colors group-hover:text-[#999]'>
|
||||
<span className='text-[#555] text-[12px] transition-colors group-hover:text-[#999]'>
|
||||
Try this workflow →
|
||||
</p>
|
||||
</span>
|
||||
</TemplateCardButton>
|
||||
</li>
|
||||
)
|
||||
@@ -616,34 +660,40 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
|
||||
<dd className='text-[#ECECEC]'>Free to start</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div className='mt-5 flex flex-col gap-2'>
|
||||
<a
|
||||
href='https://sim.ai'
|
||||
className='flex h-[32px] w-full items-center justify-center rounded-[5px] border border-[#FFFFFF] bg-[#FFFFFF] font-[430] font-season text-[#1C1C1C] text-[13px] transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
|
||||
<a
|
||||
href='https://sim.ai'
|
||||
className='mt-5 flex h-[32px] w-full items-center justify-center rounded-[5px] border border-[#FFFFFF] bg-[#FFFFFF] font-[430] font-season text-[#1C1C1C] text-[13px] transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
|
||||
>
|
||||
Get started free
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Docs */}
|
||||
<div className='rounded-lg border border-[#2A2A2A] bg-[#242424] p-5'>
|
||||
<h3 className='mb-2 font-[500] text-[#ECECEC] text-[14px]'>Documentation</h3>
|
||||
<p className='mb-4 text-[#999] text-[13px] leading-relaxed'>
|
||||
Full API reference, authentication setup, and usage examples for {name}.
|
||||
</p>
|
||||
<a
|
||||
href={docsUrl}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='inline-flex items-center gap-1.5 text-[#999] text-[13px] transition-colors hover:text-[#ECECEC]'
|
||||
>
|
||||
docs.sim.ai
|
||||
<svg
|
||||
aria-hidden='true'
|
||||
className='h-3 w-3'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth={2}
|
||||
viewBox='0 0 24 24'
|
||||
>
|
||||
Get started free
|
||||
</a>
|
||||
<a
|
||||
href={docsUrl}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='flex h-[32px] w-full items-center justify-center gap-1.5 rounded-[5px] border border-[#3d3d3d] font-[430] font-season text-[#ECECEC] text-[13px] transition-colors hover:bg-[#2A2A2A]'
|
||||
>
|
||||
View docs
|
||||
<svg
|
||||
aria-hidden='true'
|
||||
className='h-3 w-3'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth={2}
|
||||
viewBox='0 0 24 24'
|
||||
>
|
||||
<path d='M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6' />
|
||||
<polyline points='15 3 21 3 21 9' />
|
||||
<line x1='10' x2='21' y1='14' y2='3' />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<path d='M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6' />
|
||||
<polyline points='15 3 21 3 21 9' />
|
||||
<line x1='10' x2='21' y1='14' y2='3' />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Related integrations — internal linking for SEO */}
|
||||
@@ -688,43 +738,6 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
|
||||
aria-labelledby='cta-heading'
|
||||
className='mt-20 rounded-xl border border-[#2A2A2A] bg-[#242424] p-8 text-center sm:p-12'
|
||||
>
|
||||
{/* Logo pair: Sim × Integration */}
|
||||
<div className='mx-auto mb-6 flex items-center justify-center gap-3'>
|
||||
<img
|
||||
src='/brandbook/logo/small.png'
|
||||
alt='Sim'
|
||||
className='h-14 w-14 shrink-0 rounded-xl'
|
||||
/>
|
||||
<div className='flex items-center gap-2'>
|
||||
<span className='h-px w-5 bg-[#3d3d3d]' aria-hidden='true' />
|
||||
<span
|
||||
className='flex h-7 w-7 items-center justify-center rounded-full border border-[#3d3d3d]'
|
||||
aria-hidden='true'
|
||||
>
|
||||
<svg
|
||||
className='h-3.5 w-3.5 text-[#666]'
|
||||
viewBox='0 0 24 24'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth={2}
|
||||
strokeLinecap='round'
|
||||
>
|
||||
<path d='M5 12h14' />
|
||||
<path d='M12 5v14' />
|
||||
</svg>
|
||||
</span>
|
||||
<span className='h-px w-5 bg-[#3d3d3d]' aria-hidden='true' />
|
||||
</div>
|
||||
<IntegrationIcon
|
||||
bgColor={bgColor}
|
||||
name={name}
|
||||
Icon={IconComponent}
|
||||
className='h-14 w-14 rounded-xl'
|
||||
iconClassName='h-7 w-7'
|
||||
fallbackClassName='text-[22px]'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
</div>
|
||||
<h2
|
||||
id='cta-heading'
|
||||
className='mb-3 font-[500] text-[#ECECEC] text-[28px] sm:text-[34px]'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ComponentType, ElementType, HTMLAttributes, SVGProps } from 'react'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { isLightBg } from '@/app/(landing)/integrations/data/utils'
|
||||
|
||||
interface IntegrationIconProps extends HTMLAttributes<HTMLElement> {
|
||||
bgColor: string
|
||||
@@ -32,6 +33,9 @@ export function IntegrationIcon({
|
||||
as: Tag = 'div',
|
||||
...rest
|
||||
}: IntegrationIconProps) {
|
||||
const isLight = isLightBg(bgColor)
|
||||
const fgColor = isLight ? 'text-[#1C1C1C]' : 'text-white'
|
||||
|
||||
return (
|
||||
<Tag
|
||||
className={cn('flex shrink-0 items-center justify-center', className)}
|
||||
@@ -39,9 +43,9 @@ export function IntegrationIcon({
|
||||
{...rest}
|
||||
>
|
||||
{Icon ? (
|
||||
<Icon className={cn(iconClassName, 'text-white')} />
|
||||
<Icon className={cn(iconClassName, fgColor)} />
|
||||
) : (
|
||||
<span className={cn('font-[500] text-white leading-none', fallbackClassName)}>
|
||||
<span className={cn('font-[500] leading-none', fallbackClassName, fgColor)}>
|
||||
{name.charAt(0)}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useState } from 'react'
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Label,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
Textarea,
|
||||
} from '@/components/emcn'
|
||||
|
||||
type SubmitStatus = 'idle' | 'submitting' | 'success' | 'error'
|
||||
|
||||
export function RequestIntegrationModal() {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [status, setStatus] = useState<SubmitStatus>('idle')
|
||||
|
||||
const [integrationName, setIntegrationName] = useState('')
|
||||
const [email, setEmail] = useState('')
|
||||
const [useCase, setUseCase] = useState('')
|
||||
|
||||
const resetForm = useCallback(() => {
|
||||
setIntegrationName('')
|
||||
setEmail('')
|
||||
setUseCase('')
|
||||
setStatus('idle')
|
||||
}, [])
|
||||
|
||||
const handleOpenChange = useCallback(
|
||||
(nextOpen: boolean) => {
|
||||
setOpen(nextOpen)
|
||||
if (!nextOpen) resetForm()
|
||||
},
|
||||
[resetForm]
|
||||
)
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!integrationName.trim() || !email.trim()) return
|
||||
|
||||
setStatus('submitting')
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/help/integration-request', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
integrationName: integrationName.trim(),
|
||||
email: email.trim(),
|
||||
useCase: useCase.trim() || undefined,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!res.ok) throw new Error('Request failed')
|
||||
|
||||
setStatus('success')
|
||||
setTimeout(() => setOpen(false), 1500)
|
||||
} catch {
|
||||
setStatus('error')
|
||||
}
|
||||
},
|
||||
[integrationName, email, useCase]
|
||||
)
|
||||
|
||||
const canSubmit = integrationName.trim() && email.trim() && status === 'idle'
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => setOpen(true)}
|
||||
className='inline-flex h-[32px] shrink-0 items-center gap-[6px] rounded-[5px] border border-[#3d3d3d] px-[10px] font-[430] font-season text-[#ECECEC] text-[14px] transition-colors hover:bg-[#2A2A2A]'
|
||||
>
|
||||
Request an integration
|
||||
</button>
|
||||
|
||||
<Modal open={open} onOpenChange={handleOpenChange}>
|
||||
<ModalContent size='sm'>
|
||||
<ModalHeader>Request an Integration</ModalHeader>
|
||||
|
||||
{status === 'success' ? (
|
||||
<ModalBody>
|
||||
<div className='flex flex-col items-center gap-3 py-6 text-center'>
|
||||
<div className='flex h-10 w-10 items-center justify-center rounded-full bg-[#33C482]/10'>
|
||||
<svg
|
||||
className='h-5 w-5 text-[#33C482]'
|
||||
viewBox='0 0 24 24'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth={2}
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
>
|
||||
<polyline points='20 6 9 17 4 12' />
|
||||
</svg>
|
||||
</div>
|
||||
<p className='text-[14px] text-[var(--text-primary)]'>
|
||||
Request submitted — we'll follow up at{' '}
|
||||
<span className='font-medium'>{email}</span>.
|
||||
</p>
|
||||
</div>
|
||||
</ModalBody>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} className='flex min-h-0 flex-1 flex-col'>
|
||||
<ModalBody>
|
||||
<div className='space-y-[12px]'>
|
||||
<div className='flex flex-col gap-[4px]'>
|
||||
<Label htmlFor='integration-name'>Integration name</Label>
|
||||
<Input
|
||||
id='integration-name'
|
||||
placeholder='e.g. Stripe, HubSpot, Snowflake'
|
||||
value={integrationName}
|
||||
onChange={(e) => setIntegrationName(e.target.value)}
|
||||
maxLength={200}
|
||||
autoComplete='off'
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col gap-[4px]'>
|
||||
<Label htmlFor='requester-email'>Your email</Label>
|
||||
<Input
|
||||
id='requester-email'
|
||||
type='email'
|
||||
placeholder='you@company.com'
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
autoComplete='email'
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col gap-[4px]'>
|
||||
<Label htmlFor='use-case'>
|
||||
Use case <span className='text-[var(--text-tertiary)]'>(optional)</span>
|
||||
</Label>
|
||||
<Textarea
|
||||
id='use-case'
|
||||
placeholder='What would you automate with this integration?'
|
||||
value={useCase}
|
||||
onChange={(e) => setUseCase(e.target.value)}
|
||||
rows={3}
|
||||
maxLength={2000}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{status === 'error' && (
|
||||
<p className='text-[13px] text-[var(--text-error)]'>
|
||||
Something went wrong. Please try again.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
type='button'
|
||||
variant='default'
|
||||
onClick={() => setOpen(false)}
|
||||
disabled={status === 'submitting'}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type='submit' variant='primary' disabled={!canSubmit && status !== 'error'}>
|
||||
{status === 'submitting' ? 'Submitting...' : 'Submit request'}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
AsanaIcon,
|
||||
AshbyIcon,
|
||||
AttioIcon,
|
||||
AzureIcon,
|
||||
BoxCompanyIcon,
|
||||
BrainIcon,
|
||||
BrandfetchIcon,
|
||||
@@ -82,7 +81,6 @@ import {
|
||||
HunterIOIcon,
|
||||
ImageIcon,
|
||||
IncidentioIcon,
|
||||
InfisicalIcon,
|
||||
IntercomIcon,
|
||||
JinaAIIcon,
|
||||
JiraIcon,
|
||||
@@ -254,7 +252,6 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
image_generator: ImageIcon,
|
||||
imap: MailServerIcon,
|
||||
incidentio: IncidentioIcon,
|
||||
infisical: InfisicalIcon,
|
||||
intercom_v2: IntercomIcon,
|
||||
jina: JinaAIIcon,
|
||||
jira: JiraIcon,
|
||||
@@ -272,7 +269,6 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
mailgun: MailgunIcon,
|
||||
mem0: Mem0Icon,
|
||||
memory: BrainIcon,
|
||||
microsoft_ad: AzureIcon,
|
||||
microsoft_dataverse: MicrosoftDataverseIcon,
|
||||
microsoft_excel_v2: MicrosoftExcelIcon,
|
||||
microsoft_planner: MicrosoftPlannerIcon,
|
||||
|
||||
@@ -1122,75 +1122,6 @@
|
||||
"authType": "none",
|
||||
"category": "tools"
|
||||
},
|
||||
{
|
||||
"type": "microsoft_ad",
|
||||
"slug": "azure-ad",
|
||||
"name": "Azure AD",
|
||||
"description": "Manage users and groups in Azure AD (Microsoft Entra ID)",
|
||||
"longDescription": "Integrate Azure Active Directory into your workflows. List, create, update, and delete users and groups. Manage group memberships programmatically.",
|
||||
"bgColor": "#0078D4",
|
||||
"iconName": "AzureIcon",
|
||||
"docsUrl": "https://docs.sim.ai/tools/microsoft_ad",
|
||||
"operations": [
|
||||
{
|
||||
"name": "List Users",
|
||||
"description": "List users in Azure AD (Microsoft Entra ID)"
|
||||
},
|
||||
{
|
||||
"name": "Get User",
|
||||
"description": "Get a user by ID or user principal name from Azure AD"
|
||||
},
|
||||
{
|
||||
"name": "Create User",
|
||||
"description": "Create a new user in Azure AD (Microsoft Entra ID)"
|
||||
},
|
||||
{
|
||||
"name": "Update User",
|
||||
"description": "Update user properties in Azure AD (Microsoft Entra ID)"
|
||||
},
|
||||
{
|
||||
"name": "Delete User",
|
||||
"description": "Delete a user from Azure AD (Microsoft Entra ID). The user is moved to a temporary container and can be restored within 30 days."
|
||||
},
|
||||
{
|
||||
"name": "List Groups",
|
||||
"description": "List groups in Azure AD (Microsoft Entra ID)"
|
||||
},
|
||||
{
|
||||
"name": "Get Group",
|
||||
"description": "Get a group by ID from Azure AD (Microsoft Entra ID)"
|
||||
},
|
||||
{
|
||||
"name": "Create Group",
|
||||
"description": "Create a new group in Azure AD (Microsoft Entra ID)"
|
||||
},
|
||||
{
|
||||
"name": "Update Group",
|
||||
"description": "Update group properties in Azure AD (Microsoft Entra ID)"
|
||||
},
|
||||
{
|
||||
"name": "Delete Group",
|
||||
"description": "Delete a group from Azure AD (Microsoft Entra ID). Microsoft 365 and security groups can be restored within 30 days."
|
||||
},
|
||||
{
|
||||
"name": "List Group Members",
|
||||
"description": "List members of a group in Azure AD (Microsoft Entra ID)"
|
||||
},
|
||||
{
|
||||
"name": "Add Group Member",
|
||||
"description": "Add a member to a group in Azure AD (Microsoft Entra ID)"
|
||||
},
|
||||
{
|
||||
"name": "Remove Group Member",
|
||||
"description": "Remove a member from a group in Azure AD (Microsoft Entra ID)"
|
||||
}
|
||||
],
|
||||
"operationCount": 13,
|
||||
"triggers": [],
|
||||
"triggerCount": 0,
|
||||
"authType": "oauth",
|
||||
"category": "tools"
|
||||
},
|
||||
{
|
||||
"type": "box",
|
||||
"slug": "box",
|
||||
@@ -1986,31 +1917,31 @@
|
||||
"operations": [
|
||||
{
|
||||
"name": "Launch Agent",
|
||||
"description": "Start a new cloud agent to work on a GitHub repository with the given instructions."
|
||||
"description": "Cursor API key"
|
||||
},
|
||||
{
|
||||
"name": "Add Follow-up",
|
||||
"description": "Add a follow-up instruction to an existing cloud agent."
|
||||
"description": "Cursor API key"
|
||||
},
|
||||
{
|
||||
"name": "Get Agent Status",
|
||||
"description": "Retrieve the current status and results of a cloud agent."
|
||||
"description": "Cursor API key"
|
||||
},
|
||||
{
|
||||
"name": "Get Conversation",
|
||||
"description": "Retrieve the conversation history of a cloud agent, including all user prompts and assistant responses."
|
||||
"description": "Cursor API key"
|
||||
},
|
||||
{
|
||||
"name": "List Agents",
|
||||
"description": "List all cloud agents for the authenticated user with optional pagination."
|
||||
"description": "Cursor API key"
|
||||
},
|
||||
{
|
||||
"name": "Stop Agent",
|
||||
"description": "Stop a running cloud agent. This pauses the agent without deleting it."
|
||||
"description": "Cursor API key"
|
||||
},
|
||||
{
|
||||
"name": "Delete Agent",
|
||||
"description": "Permanently delete a cloud agent. This action cannot be undone."
|
||||
"description": "Cursor API key"
|
||||
}
|
||||
],
|
||||
"operationCount": 7,
|
||||
@@ -3950,7 +3881,7 @@
|
||||
"operations": [
|
||||
{
|
||||
"name": "List Files",
|
||||
"description": "List Google Drive files"
|
||||
"description": "List files and folders in Google Drive with complete metadata"
|
||||
},
|
||||
{
|
||||
"name": "Get File Info",
|
||||
@@ -5336,43 +5267,6 @@
|
||||
"authType": "api-key",
|
||||
"category": "tools"
|
||||
},
|
||||
{
|
||||
"type": "infisical",
|
||||
"slug": "infisical",
|
||||
"name": "Infisical",
|
||||
"description": "Manage secrets with Infisical",
|
||||
"longDescription": "Integrate Infisical into your workflow. List, get, create, update, and delete secrets across project environments.",
|
||||
"bgColor": "#F7FE62",
|
||||
"iconName": "InfisicalIcon",
|
||||
"docsUrl": "https://docs.sim.ai/tools/infisical",
|
||||
"operations": [
|
||||
{
|
||||
"name": "List Secrets",
|
||||
"description": "List all secrets in a project environment. Returns secret keys, values, comments, tags, and metadata."
|
||||
},
|
||||
{
|
||||
"name": "Get Secret",
|
||||
"description": "Retrieve a single secret by name from a project environment."
|
||||
},
|
||||
{
|
||||
"name": "Create Secret",
|
||||
"description": "Create a new secret in a project environment."
|
||||
},
|
||||
{
|
||||
"name": "Update Secret",
|
||||
"description": "Update an existing secret in a project environment."
|
||||
},
|
||||
{
|
||||
"name": "Delete Secret",
|
||||
"description": "Delete a secret from a project environment."
|
||||
}
|
||||
],
|
||||
"operationCount": 5,
|
||||
"triggers": [],
|
||||
"triggerCount": 0,
|
||||
"authType": "api-key",
|
||||
"category": "tools"
|
||||
},
|
||||
{
|
||||
"type": "intercom_v2",
|
||||
"slug": "intercom",
|
||||
@@ -5385,63 +5279,63 @@
|
||||
"operations": [
|
||||
{
|
||||
"name": "Create Contact",
|
||||
"description": "Create a new contact in Intercom with email, external_id, or role"
|
||||
"description": "Intercom API access token"
|
||||
},
|
||||
{
|
||||
"name": "Get Contact",
|
||||
"description": "Get a single contact by ID from Intercom"
|
||||
"description": "Intercom API access token"
|
||||
},
|
||||
{
|
||||
"name": "Update Contact",
|
||||
"description": "Update an existing contact in Intercom"
|
||||
"description": "Intercom API access token"
|
||||
},
|
||||
{
|
||||
"name": "List Contacts",
|
||||
"description": "List all contacts from Intercom with pagination support"
|
||||
"description": "Intercom API access token"
|
||||
},
|
||||
{
|
||||
"name": "Search Contacts",
|
||||
"description": "Search for contacts in Intercom using a query"
|
||||
"description": "Intercom API access token"
|
||||
},
|
||||
{
|
||||
"name": "Delete Contact",
|
||||
"description": "Delete a contact from Intercom by ID"
|
||||
"description": "Intercom API access token"
|
||||
},
|
||||
{
|
||||
"name": "Create Company",
|
||||
"description": "Create or update a company in Intercom"
|
||||
"description": "Intercom API access token"
|
||||
},
|
||||
{
|
||||
"name": "Get Company",
|
||||
"description": "Retrieve a single company by ID from Intercom"
|
||||
"description": "Intercom API access token"
|
||||
},
|
||||
{
|
||||
"name": "List Companies",
|
||||
"description": "List all companies from Intercom with pagination support. Note: This endpoint has a limit of 10,000 companies that can be returned using pagination. For datasets larger than 10,000 companies, use the Scroll API instead."
|
||||
"description": "Intercom API access token"
|
||||
},
|
||||
{
|
||||
"name": "Get Conversation",
|
||||
"description": "Retrieve a single conversation by ID from Intercom"
|
||||
"description": "Intercom API access token"
|
||||
},
|
||||
{
|
||||
"name": "List Conversations",
|
||||
"description": "List all conversations from Intercom with pagination support"
|
||||
"description": "Intercom API access token"
|
||||
},
|
||||
{
|
||||
"name": "Reply to Conversation",
|
||||
"description": "Reply to a conversation as an admin in Intercom"
|
||||
"description": "Intercom API access token"
|
||||
},
|
||||
{
|
||||
"name": "Search Conversations",
|
||||
"description": "Search for conversations in Intercom using a query"
|
||||
"description": "Intercom API access token"
|
||||
},
|
||||
{
|
||||
"name": "Create Ticket",
|
||||
"description": "Create a new ticket in Intercom"
|
||||
"description": "Intercom API access token"
|
||||
},
|
||||
{
|
||||
"name": "Get Ticket",
|
||||
"description": "Retrieve a single ticket by ID from Intercom"
|
||||
"description": "Intercom API access token"
|
||||
},
|
||||
{
|
||||
"name": "Update Ticket",
|
||||
@@ -5449,7 +5343,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Create Message",
|
||||
"description": "Create and send a new admin-initiated message in Intercom"
|
||||
"description": "Intercom API access token"
|
||||
},
|
||||
{
|
||||
"name": "List Admins",
|
||||
@@ -6956,15 +6850,15 @@
|
||||
"operations": [
|
||||
{
|
||||
"name": "Add Memories",
|
||||
"description": "Add memories to Mem0 for persistent storage and retrieval"
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "Search Memories",
|
||||
"description": "Search for memories in Mem0 using semantic search"
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "Get Memories",
|
||||
"description": "Retrieve memories from Mem0 by ID or filter criteria"
|
||||
"description": ""
|
||||
}
|
||||
],
|
||||
"operationCount": 3,
|
||||
@@ -6989,7 +6883,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Get All Memories",
|
||||
"description": "Retrieve all memories from the database"
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "Get Memory",
|
||||
@@ -8316,7 +8210,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Unsave",
|
||||
"description": "Remove a Reddit post or comment from your saved items"
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "Reply",
|
||||
@@ -8494,7 +8388,7 @@
|
||||
"operations": [
|
||||
{
|
||||
"name": "Send Email",
|
||||
"description": "Send an email using your own Resend API key and from address"
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "Get Email",
|
||||
|
||||
15
apps/sim/app/(landing)/integrations/data/utils.ts
Normal file
15
apps/sim/app/(landing)/integrations/data/utils.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Utility helpers for the integrations landing pages.
|
||||
* Shared across the listing grid, individual integration cards, and slug pages.
|
||||
*/
|
||||
|
||||
/** bgColor values that are visually light and require dark icon text. */
|
||||
const LIGHT_BG = new Set(['#e0e0e0', '#f5f5f5', '#ffffff', '#ececec', '#f0f0f0'])
|
||||
|
||||
/**
|
||||
* Returns true when `bgColor` is a light color that requires dark foreground text.
|
||||
* Handles gradient strings safely — they always use light foreground (white).
|
||||
*/
|
||||
export function isLightBg(bgColor: string): boolean {
|
||||
return LIGHT_BG.has(bgColor.toLowerCase())
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
import { getNavBlogPosts } from '@/lib/blog/registry'
|
||||
import Footer from '@/app/(home)/components/footer/footer'
|
||||
import Navbar from '@/app/(home)/components/navbar/navbar'
|
||||
|
||||
export default async function IntegrationsLayout({ children }: { children: React.ReactNode }) {
|
||||
const blogPosts = await getNavBlogPosts()
|
||||
export default function IntegrationsLayout({ children }: { children: React.ReactNode }) {
|
||||
const orgJsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Organization',
|
||||
@@ -36,7 +34,7 @@ export default async function IntegrationsLayout({ children }: { children: React
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(websiteJsonLd) }}
|
||||
/>
|
||||
<header>
|
||||
<Navbar blogPosts={blogPosts} />
|
||||
<Navbar />
|
||||
</header>
|
||||
<main className='relative flex-1'>{children}</main>
|
||||
<Footer />
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { Metadata } from 'next'
|
||||
import { IntegrationGrid } from './components/integration-grid'
|
||||
import { RequestIntegrationModal } from './components/request-integration-modal'
|
||||
import { blockTypeToIconMap } from './data/icon-mapping'
|
||||
import integrations from './data/integrations.json'
|
||||
import { POPULAR_WORKFLOWS } from './data/popular-workflows'
|
||||
@@ -139,7 +138,26 @@ export default function IntegrationsPage() {
|
||||
Let us know and we'll prioritize it.
|
||||
</p>
|
||||
</div>
|
||||
<RequestIntegrationModal />
|
||||
<a
|
||||
href='https://github.com/simstudioai/sim/issues/new?labels=integration+request&template=integration_request.md'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='inline-flex h-[32px] shrink-0 items-center gap-[6px] rounded-[5px] border border-[#3d3d3d] px-[10px] font-[430] font-season text-[#ECECEC] text-[14px] transition-colors hover:bg-[#2A2A2A]'
|
||||
>
|
||||
Request an integration
|
||||
<svg
|
||||
aria-hidden='true'
|
||||
className='h-3 w-3'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth={2}
|
||||
viewBox='0 0 24 24'
|
||||
>
|
||||
<path d='M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6' />
|
||||
<polyline points='15 3 21 3 21 9' />
|
||||
<line x1='10' x2='21' y1='14' y2='3' />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { getEnv } from '@/lib/core/config/env'
|
||||
import { ExternalRedirect, LegalLayout } from '@/app/(landing)/components'
|
||||
import { LegalLayout } from '@/app/(landing)/components'
|
||||
|
||||
export default function PrivacyPolicy() {
|
||||
useEffect(() => {
|
||||
const privacyUrl = getEnv('NEXT_PUBLIC_PRIVACY_URL')
|
||||
if (privacyUrl?.startsWith('http')) {
|
||||
window.location.href = privacyUrl
|
||||
}
|
||||
}, [])
|
||||
return (
|
||||
<LegalLayout title='Privacy Policy'>
|
||||
<ExternalRedirect url={getEnv('NEXT_PUBLIC_PRIVACY_URL') ?? ''} />
|
||||
<section>
|
||||
<p className='mb-4'>Last Updated: October 11, 2025</p>
|
||||
<p>
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { getEnv } from '@/lib/core/config/env'
|
||||
import { ExternalRedirect, LegalLayout } from '@/app/(landing)/components'
|
||||
import { LegalLayout } from '@/app/(landing)/components'
|
||||
|
||||
export default function TermsOfService() {
|
||||
useEffect(() => {
|
||||
const termsUrl = getEnv('NEXT_PUBLIC_TERMS_URL')
|
||||
if (termsUrl?.startsWith('http')) {
|
||||
window.location.href = termsUrl
|
||||
}
|
||||
}, [])
|
||||
return (
|
||||
<LegalLayout title='Terms of Service'>
|
||||
<ExternalRedirect url={getEnv('NEXT_PUBLIC_TERMS_URL') ?? ''} />
|
||||
<section>
|
||||
<p className='mb-4'>Last Updated: October 11, 2025</p>
|
||||
<p>
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { env } from '@/lib/core/config/env'
|
||||
import type { TokenBucketConfig } from '@/lib/core/rate-limiter'
|
||||
import { RateLimiter } from '@/lib/core/rate-limiter'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { getEmailDomain } from '@/lib/core/utils/urls'
|
||||
import { sendEmail } from '@/lib/messaging/email/mailer'
|
||||
import { getFromEmailAddress } from '@/lib/messaging/email/utils'
|
||||
|
||||
const logger = createLogger('IntegrationRequestAPI')
|
||||
|
||||
const rateLimiter = new RateLimiter()
|
||||
|
||||
const PUBLIC_ENDPOINT_RATE_LIMIT: TokenBucketConfig = {
|
||||
maxTokens: 10,
|
||||
refillRate: 5,
|
||||
refillIntervalMs: 60_000,
|
||||
}
|
||||
|
||||
const integrationRequestSchema = z.object({
|
||||
integrationName: z.string().min(1, 'Integration name is required').max(200),
|
||||
email: z.string().email('A valid email is required'),
|
||||
useCase: z.string().max(2000).optional(),
|
||||
})
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const ip = req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? 'unknown'
|
||||
const storageKey = `public:integration-request:${ip}`
|
||||
|
||||
const { allowed, remaining, resetAt } = await rateLimiter.checkRateLimitDirect(
|
||||
storageKey,
|
||||
PUBLIC_ENDPOINT_RATE_LIMIT
|
||||
)
|
||||
|
||||
if (!allowed) {
|
||||
logger.warn(`[${requestId}] Rate limit exceeded for IP ${ip}`, { remaining, resetAt })
|
||||
return NextResponse.json(
|
||||
{ error: 'Too many requests. Please try again later.' },
|
||||
{
|
||||
status: 429,
|
||||
headers: { 'Retry-After': String(Math.ceil((resetAt.getTime() - Date.now()) / 1000)) },
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const body = await req.json()
|
||||
|
||||
const validationResult = integrationRequestSchema.safeParse(body)
|
||||
if (!validationResult.success) {
|
||||
logger.warn(`[${requestId}] Invalid integration request data`, {
|
||||
errors: validationResult.error.format(),
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: validationResult.error.format() },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const { integrationName, email, useCase } = validationResult.data
|
||||
|
||||
logger.info(`[${requestId}] Processing integration request`, {
|
||||
integrationName,
|
||||
email: `${email.substring(0, 3)}***`,
|
||||
})
|
||||
|
||||
const emailText = `Integration: ${integrationName}
|
||||
From: ${email}
|
||||
Submitted: ${new Date().toISOString()}
|
||||
|
||||
${useCase ? `Use Case:\n${useCase}` : 'No use case provided.'}
|
||||
`
|
||||
|
||||
const emailResult = await sendEmail({
|
||||
to: [`help@${env.EMAIL_DOMAIN || getEmailDomain()}`],
|
||||
subject: `[INTEGRATION REQUEST] ${integrationName}`,
|
||||
text: emailText,
|
||||
from: getFromEmailAddress(),
|
||||
replyTo: email,
|
||||
emailType: 'transactional',
|
||||
})
|
||||
|
||||
if (!emailResult.success) {
|
||||
logger.error(`[${requestId}] Error sending integration request email`, emailResult.message)
|
||||
return NextResponse.json({ error: 'Failed to send request' }, { status: 500 })
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Integration request email sent successfully`)
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: true, message: 'Integration request submitted successfully' },
|
||||
{ status: 200 }
|
||||
)
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes('not configured')) {
|
||||
logger.error(`[${requestId}] Email service configuration error`, error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Email service is temporarily unavailable. Please try again later.' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
logger.error(`[${requestId}] Error processing integration request`, error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,14 @@
|
||||
import { getNavBlogPosts } from '@/lib/blog/registry'
|
||||
import { martianMono } from '@/app/_styles/fonts/martian-mono/martian-mono'
|
||||
import Footer from '@/app/(home)/components/footer/footer'
|
||||
import Navbar from '@/app/(home)/components/navbar/navbar'
|
||||
|
||||
export default async function ChangelogLayout({ children }: { children: React.ReactNode }) {
|
||||
const blogPosts = await getNavBlogPosts()
|
||||
export default function ChangelogLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div
|
||||
className={`${martianMono.variable} relative min-h-screen bg-[#1C1C1C] font-[430] font-season text-[#ECECEC]`}
|
||||
>
|
||||
<header>
|
||||
<Navbar blogPosts={blogPosts} />
|
||||
<Navbar />
|
||||
</header>
|
||||
{children}
|
||||
<Footer hideCTA />
|
||||
|
||||
@@ -12,6 +12,7 @@ Sim lets teams create agents, workflows, knowledge bases, tables, and docs. Over
|
||||
## Core Pages
|
||||
|
||||
- [Homepage](${baseUrl}): Product overview, features, and pricing
|
||||
- [Templates](${baseUrl}/templates): Pre-built workflow templates to get started quickly
|
||||
- [Changelog](${baseUrl}/changelog): Product updates and release notes
|
||||
- [Sim Blog](${baseUrl}/blog): Announcements, insights, and guides
|
||||
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
import Link from 'next/link'
|
||||
import { getNavBlogPosts } from '@/lib/blog/registry'
|
||||
import AuthBackground from '@/app/(auth)/components/auth-background'
|
||||
import Navbar from '@/app/(home)/components/navbar/navbar'
|
||||
|
||||
const CTA_BASE =
|
||||
'inline-flex items-center h-[32px] rounded-[5px] border px-[10px] font-[430] font-season text-[14px]'
|
||||
|
||||
export default async function NotFound() {
|
||||
const blogPosts = await getNavBlogPosts()
|
||||
export default function NotFound() {
|
||||
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]'>
|
||||
<Navbar blogPosts={blogPosts} />
|
||||
<Navbar />
|
||||
</header>
|
||||
<div className='relative z-30 flex flex-1 items-center justify-center px-4 pb-24'>
|
||||
<div className='flex flex-col items-center gap-[12px]'>
|
||||
|
||||
@@ -20,10 +20,10 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
url: `${baseUrl}/blog/tags`,
|
||||
lastModified: now,
|
||||
},
|
||||
// {
|
||||
// url: `${baseUrl}/templates`,
|
||||
// lastModified: now,
|
||||
// },
|
||||
{
|
||||
url: `${baseUrl}/templates`,
|
||||
lastModified: now,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/changelog`,
|
||||
lastModified: now,
|
||||
|
||||
@@ -1,20 +1,44 @@
|
||||
// import { db } from '@sim/db'
|
||||
// import { permissions, workspace } from '@sim/db/schema'
|
||||
// import { and, desc, eq } from 'drizzle-orm'
|
||||
// import { redirect } from 'next/navigation'
|
||||
// import { getSession } from '@/lib/auth'
|
||||
import { db } from '@sim/db'
|
||||
import { permissions, workspace } from '@sim/db/schema'
|
||||
import { and, desc, eq } from 'drizzle-orm'
|
||||
import { redirect } from 'next/navigation'
|
||||
import { getSession } from '@/lib/auth'
|
||||
|
||||
// export const dynamic = 'force-dynamic'
|
||||
// export const revalidate = 0
|
||||
export const dynamic = 'force-dynamic'
|
||||
export const revalidate = 0
|
||||
|
||||
interface TemplateLayoutProps {
|
||||
children: React.ReactNode
|
||||
params: Promise<{
|
||||
id: string
|
||||
}>
|
||||
}
|
||||
|
||||
/**
|
||||
* Template detail layout (public scope) — currently disabled.
|
||||
* Previously redirected authenticated users to the workspace-scoped template detail.
|
||||
* Template detail layout (public scope).
|
||||
* - If user is authenticated, redirect to workspace-scoped template detail.
|
||||
* - Otherwise render the public template detail children.
|
||||
*/
|
||||
export default function TemplateDetailLayout({ children }: TemplateLayoutProps) {
|
||||
export default async function TemplateDetailLayout({ children, params }: TemplateLayoutProps) {
|
||||
const { id } = await params
|
||||
const session = await getSession()
|
||||
|
||||
if (session?.user?.id) {
|
||||
const userWorkspaces = await db
|
||||
.select({
|
||||
workspace: workspace,
|
||||
})
|
||||
.from(permissions)
|
||||
.innerJoin(workspace, eq(permissions.entityId, workspace.id))
|
||||
.where(and(eq(permissions.userId, session.user.id), eq(permissions.entityType, 'workspace')))
|
||||
.orderBy(desc(workspace.createdAt))
|
||||
.limit(1)
|
||||
|
||||
if (userWorkspaces.length > 0) {
|
||||
const firstWorkspace = userWorkspaces[0].workspace
|
||||
redirect(`/workspace/${firstWorkspace.id}/templates/${id}`)
|
||||
}
|
||||
}
|
||||
|
||||
return children
|
||||
}
|
||||
|
||||
@@ -1,93 +1,92 @@
|
||||
import { notFound } from 'next/navigation'
|
||||
import { db } from '@sim/db'
|
||||
import { templateCreators, templates } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import type { Metadata } from 'next'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import TemplateDetails from '@/app/templates/[id]/template'
|
||||
|
||||
// import { db } from '@sim/db'
|
||||
// import { templateCreators, templates } from '@sim/db/schema'
|
||||
// import { createLogger } from '@sim/logger'
|
||||
// import { eq } from 'drizzle-orm'
|
||||
// import type { Metadata } from 'next'
|
||||
// import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
// import TemplateDetails from '@/app/templates/[id]/template'
|
||||
|
||||
// const logger = createLogger('TemplateMetadata')
|
||||
|
||||
// /**
|
||||
// * Generate dynamic metadata for template pages.
|
||||
// * This provides OpenGraph images for social media sharing.
|
||||
// */
|
||||
// export async function generateMetadata({
|
||||
// params,
|
||||
// }: {
|
||||
// params: Promise<{ id: string }>
|
||||
// }): Promise<Metadata> {
|
||||
// const { id } = await params
|
||||
//
|
||||
// try {
|
||||
// const result = await db
|
||||
// .select({
|
||||
// template: templates,
|
||||
// creator: templateCreators,
|
||||
// })
|
||||
// .from(templates)
|
||||
// .leftJoin(templateCreators, eq(templates.creatorId, templateCreators.id))
|
||||
// .where(eq(templates.id, id))
|
||||
// .limit(1)
|
||||
//
|
||||
// if (result.length === 0) {
|
||||
// return {
|
||||
// title: 'Template Not Found',
|
||||
// description: 'The requested template could not be found.',
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// const { template, creator } = result[0]
|
||||
// const baseUrl = getBaseUrl()
|
||||
//
|
||||
// const details = template.details as { tagline?: string; about?: string } | null
|
||||
// const description = details?.tagline || 'AI workflow template on Sim'
|
||||
//
|
||||
// const hasOgImage = !!template.ogImageUrl
|
||||
// const ogImageUrl = template.ogImageUrl || `${baseUrl}/logo/primary/rounded.png`
|
||||
//
|
||||
// return {
|
||||
// title: template.name,
|
||||
// description,
|
||||
// openGraph: {
|
||||
// title: template.name,
|
||||
// description,
|
||||
// type: 'website',
|
||||
// url: `${baseUrl}/templates/${id}`,
|
||||
// siteName: 'Sim',
|
||||
// images: [
|
||||
// {
|
||||
// url: ogImageUrl,
|
||||
// width: hasOgImage ? 1200 : 512,
|
||||
// height: hasOgImage ? 630 : 512,
|
||||
// alt: `${template.name} - Workflow Preview`,
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// twitter: {
|
||||
// card: hasOgImage ? 'summary_large_image' : 'summary',
|
||||
// title: template.name,
|
||||
// description,
|
||||
// images: [ogImageUrl],
|
||||
// creator: creator?.details
|
||||
// ? ((creator.details as Record<string, unknown>).xHandle as string) || undefined
|
||||
// : undefined,
|
||||
// },
|
||||
// }
|
||||
// } catch (error) {
|
||||
// logger.error('Failed to generate template metadata:', error)
|
||||
// return {
|
||||
// title: 'Template',
|
||||
// description: 'AI workflow template on Sim',
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
const logger = createLogger('TemplateMetadata')
|
||||
|
||||
/**
|
||||
* Public template detail page — currently disabled, returns 404.
|
||||
* Generate dynamic metadata for template pages.
|
||||
* This provides OpenGraph images for social media sharing.
|
||||
*/
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>
|
||||
}): Promise<Metadata> {
|
||||
const { id } = await params
|
||||
|
||||
try {
|
||||
const result = await db
|
||||
.select({
|
||||
template: templates,
|
||||
creator: templateCreators,
|
||||
})
|
||||
.from(templates)
|
||||
.leftJoin(templateCreators, eq(templates.creatorId, templateCreators.id))
|
||||
.where(eq(templates.id, id))
|
||||
.limit(1)
|
||||
|
||||
if (result.length === 0) {
|
||||
return {
|
||||
title: 'Template Not Found',
|
||||
description: 'The requested template could not be found.',
|
||||
}
|
||||
}
|
||||
|
||||
const { template, creator } = result[0]
|
||||
const baseUrl = getBaseUrl()
|
||||
|
||||
const details = template.details as { tagline?: string; about?: string } | null
|
||||
const description = details?.tagline || 'AI workflow template on Sim'
|
||||
|
||||
const hasOgImage = !!template.ogImageUrl
|
||||
const ogImageUrl = template.ogImageUrl || `${baseUrl}/logo/primary/rounded.png`
|
||||
|
||||
return {
|
||||
title: template.name,
|
||||
description,
|
||||
openGraph: {
|
||||
title: template.name,
|
||||
description,
|
||||
type: 'website',
|
||||
url: `${baseUrl}/templates/${id}`,
|
||||
siteName: 'Sim',
|
||||
images: [
|
||||
{
|
||||
url: ogImageUrl,
|
||||
width: hasOgImage ? 1200 : 512,
|
||||
height: hasOgImage ? 630 : 512,
|
||||
alt: `${template.name} - Workflow Preview`,
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: hasOgImage ? 'summary_large_image' : 'summary',
|
||||
title: template.name,
|
||||
description,
|
||||
images: [ogImageUrl],
|
||||
creator: creator?.details
|
||||
? ((creator.details as Record<string, unknown>).xHandle as string) || undefined
|
||||
: undefined,
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to generate template metadata:', error)
|
||||
return {
|
||||
title: 'Template',
|
||||
description: 'AI workflow template on Sim',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Public template detail page for unauthenticated users.
|
||||
* Authenticated-user redirect is handled in templates/[id]/layout.tsx.
|
||||
*/
|
||||
export default function TemplatePage() {
|
||||
notFound()
|
||||
return <TemplateDetails />
|
||||
}
|
||||
|
||||
@@ -1,79 +1,73 @@
|
||||
import { notFound } from 'next/navigation'
|
||||
import { db } from '@sim/db'
|
||||
import { permissions, templateCreators, templates, workspace } from '@sim/db/schema'
|
||||
import { and, desc, eq } from 'drizzle-orm'
|
||||
import type { Metadata } from 'next'
|
||||
import { redirect } from 'next/navigation'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import type { Template } from '@/app/templates/templates'
|
||||
import Templates from '@/app/templates/templates'
|
||||
|
||||
// import { db } from '@sim/db'
|
||||
// import { permissions, templateCreators, templates, workspace } from '@sim/db/schema'
|
||||
// import { and, desc, eq } from 'drizzle-orm'
|
||||
// import type { Metadata } from 'next'
|
||||
// import { redirect } from 'next/navigation'
|
||||
// import { getSession } from '@/lib/auth'
|
||||
// import type { Template } from '@/app/templates/templates'
|
||||
// import Templates from '@/app/templates/templates'
|
||||
|
||||
// export const metadata: Metadata = {
|
||||
// title: 'Templates',
|
||||
// description:
|
||||
// 'Browse pre-built workflow templates to get started quickly with AI agents, automations, and integrations.',
|
||||
// }
|
||||
export const metadata: Metadata = {
|
||||
title: 'Templates',
|
||||
description:
|
||||
'Browse pre-built workflow templates to get started quickly with AI agents, automations, and integrations.',
|
||||
}
|
||||
|
||||
/**
|
||||
* Public templates list page.
|
||||
* Currently disabled — returns 404.
|
||||
* Redirects authenticated users to their workspace-scoped templates page.
|
||||
* Allows unauthenticated users to view templates for SEO and discovery.
|
||||
*/
|
||||
export default function TemplatesPage() {
|
||||
notFound()
|
||||
export default async function TemplatesPage() {
|
||||
const session = await getSession()
|
||||
|
||||
// Redirects authenticated users to their workspace-scoped templates page.
|
||||
// Allows unauthenticated users to view templates for SEO and discovery.
|
||||
//
|
||||
// const session = await getSession()
|
||||
//
|
||||
// // Authenticated users: redirect to workspace-scoped templates
|
||||
// if (session?.user?.id) {
|
||||
// const userWorkspaces = await db
|
||||
// .select({
|
||||
// workspace: workspace,
|
||||
// })
|
||||
// .from(permissions)
|
||||
// .innerJoin(workspace, eq(permissions.entityId, workspace.id))
|
||||
// .where(and(eq(permissions.userId, session.user.id), eq(permissions.entityType, 'workspace')))
|
||||
// .orderBy(desc(workspace.createdAt))
|
||||
// .limit(1)
|
||||
//
|
||||
// if (userWorkspaces.length > 0) {
|
||||
// const firstWorkspace = userWorkspaces[0].workspace
|
||||
// redirect(`/workspace/${firstWorkspace.id}/templates`)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Unauthenticated users: show public templates
|
||||
// const templatesData = await db
|
||||
// .select({
|
||||
// id: templates.id,
|
||||
// workflowId: templates.workflowId,
|
||||
// name: templates.name,
|
||||
// details: templates.details,
|
||||
// creatorId: templates.creatorId,
|
||||
// creator: templateCreators,
|
||||
// views: templates.views,
|
||||
// stars: templates.stars,
|
||||
// status: templates.status,
|
||||
// tags: templates.tags,
|
||||
// requiredCredentials: templates.requiredCredentials,
|
||||
// state: templates.state,
|
||||
// createdAt: templates.createdAt,
|
||||
// updatedAt: templates.updatedAt,
|
||||
// })
|
||||
// .from(templates)
|
||||
// .leftJoin(templateCreators, eq(templates.creatorId, templateCreators.id))
|
||||
// .where(eq(templates.status, 'approved'))
|
||||
// .orderBy(desc(templates.views), desc(templates.createdAt))
|
||||
// .then((rows) => rows.map((row) => ({ ...row, isStarred: false })))
|
||||
//
|
||||
// return (
|
||||
// <Templates
|
||||
// initialTemplates={templatesData as unknown as Template[]}
|
||||
// currentUserId={null}
|
||||
// isSuperUser={false}
|
||||
// />
|
||||
// )
|
||||
// Authenticated users: redirect to workspace-scoped templates
|
||||
if (session?.user?.id) {
|
||||
const userWorkspaces = await db
|
||||
.select({
|
||||
workspace: workspace,
|
||||
})
|
||||
.from(permissions)
|
||||
.innerJoin(workspace, eq(permissions.entityId, workspace.id))
|
||||
.where(and(eq(permissions.userId, session.user.id), eq(permissions.entityType, 'workspace')))
|
||||
.orderBy(desc(workspace.createdAt))
|
||||
.limit(1)
|
||||
|
||||
if (userWorkspaces.length > 0) {
|
||||
const firstWorkspace = userWorkspaces[0].workspace
|
||||
redirect(`/workspace/${firstWorkspace.id}/templates`)
|
||||
}
|
||||
}
|
||||
|
||||
// Unauthenticated users: show public templates
|
||||
const templatesData = await db
|
||||
.select({
|
||||
id: templates.id,
|
||||
workflowId: templates.workflowId,
|
||||
name: templates.name,
|
||||
details: templates.details,
|
||||
creatorId: templates.creatorId,
|
||||
creator: templateCreators,
|
||||
views: templates.views,
|
||||
stars: templates.stars,
|
||||
status: templates.status,
|
||||
tags: templates.tags,
|
||||
requiredCredentials: templates.requiredCredentials,
|
||||
state: templates.state,
|
||||
createdAt: templates.createdAt,
|
||||
updatedAt: templates.updatedAt,
|
||||
})
|
||||
.from(templates)
|
||||
.leftJoin(templateCreators, eq(templates.creatorId, templateCreators.id))
|
||||
.where(eq(templates.status, 'approved'))
|
||||
.orderBy(desc(templates.views), desc(templates.createdAt))
|
||||
.then((rows) => rows.map((row) => ({ ...row, isStarred: false })))
|
||||
|
||||
return (
|
||||
<Templates
|
||||
initialTemplates={templatesData as unknown as Template[]}
|
||||
currentUserId={null}
|
||||
isSuperUser={false}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { getDocumentIcon } from '@/components/icons/document-icons'
|
||||
import { useSession } from '@/lib/auth/auth-client'
|
||||
import {
|
||||
LandingPromptStorage,
|
||||
LandingTemplateStorage,
|
||||
type LandingWorkflowSeed,
|
||||
LandingWorkflowSeedStorage,
|
||||
} from '@/lib/core/utils/browser-storage'
|
||||
@@ -117,12 +118,12 @@ export function Home({ chatId }: HomeProps = {}) {
|
||||
return
|
||||
}
|
||||
|
||||
// const templateId = LandingTemplateStorage.consume()
|
||||
// if (templateId) {
|
||||
// logger.info('Retrieved landing page template, redirecting to template detail')
|
||||
// router.replace(`/workspace/${workspaceId}/templates/${templateId}?use=true`)
|
||||
// return
|
||||
// }
|
||||
const templateId = LandingTemplateStorage.consume()
|
||||
if (templateId) {
|
||||
logger.info('Retrieved landing page template, redirecting to template detail')
|
||||
router.replace(`/workspace/${workspaceId}/templates/${templateId}?use=true`)
|
||||
return
|
||||
}
|
||||
|
||||
const prompt = LandingPromptStorage.consume()
|
||||
if (prompt) {
|
||||
|
||||
@@ -57,13 +57,13 @@ const Credentials = dynamic(
|
||||
),
|
||||
{ loading: () => <CredentialsSkeleton /> }
|
||||
)
|
||||
// const TemplateProfile = dynamic(
|
||||
// () =>
|
||||
// import(
|
||||
// '@/app/workspace/[workspaceId]/settings/components/template-profile/template-profile'
|
||||
// ).then((m) => m.TemplateProfile),
|
||||
// { loading: () => <SettingsSectionSkeleton /> }
|
||||
// )
|
||||
const TemplateProfile = dynamic(
|
||||
() =>
|
||||
import(
|
||||
'@/app/workspace/[workspaceId]/settings/components/template-profile/template-profile'
|
||||
).then((m) => m.TemplateProfile),
|
||||
{ loading: () => <SettingsSectionSkeleton /> }
|
||||
)
|
||||
const CredentialSets = dynamic(
|
||||
() =>
|
||||
import(
|
||||
@@ -177,7 +177,7 @@ export function SettingsPage({ section }: SettingsPageProps) {
|
||||
{effectiveSection === 'general' && <General />}
|
||||
{effectiveSection === 'integrations' && <Integrations />}
|
||||
{effectiveSection === 'secrets' && <Credentials />}
|
||||
{/* {effectiveSection === 'template-profile' && <TemplateProfile />} */}
|
||||
{effectiveSection === 'template-profile' && <TemplateProfile />}
|
||||
{effectiveSection === 'credential-sets' && <CredentialSets />}
|
||||
{effectiveSection === 'access-control' && <AccessControl />}
|
||||
{effectiveSection === 'apikeys' && <ApiKeys />}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
ShieldCheck,
|
||||
TerminalWindow,
|
||||
TrashOutline,
|
||||
User,
|
||||
Users,
|
||||
Wrench,
|
||||
} from '@/components/emcn'
|
||||
@@ -83,7 +84,7 @@ export const sectionConfig: { key: NavigationSection; title: string }[] = [
|
||||
|
||||
export const allNavigationItems: NavigationItem[] = [
|
||||
{ id: 'general', label: 'General', icon: Settings, section: 'account' },
|
||||
// { id: 'template-profile', label: 'Template Profile', icon: User, section: 'account' },
|
||||
{ id: 'template-profile', label: 'Template Profile', icon: User, section: 'account' },
|
||||
{
|
||||
id: 'access-control',
|
||||
label: 'Access Control',
|
||||
|
||||
@@ -1,103 +1,115 @@
|
||||
import { notFound } from 'next/navigation'
|
||||
import { db } from '@sim/db'
|
||||
import { templateCreators, templates } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import type { Metadata } from 'next'
|
||||
import { redirect } from 'next/navigation'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { verifyWorkspaceMembership } from '@/app/api/workflows/utils'
|
||||
import TemplateDetails from '@/app/templates/[id]/template'
|
||||
|
||||
// import { db } from '@sim/db'
|
||||
// import { templateCreators, templates } from '@sim/db/schema'
|
||||
// import { createLogger } from '@sim/logger'
|
||||
// import { eq } from 'drizzle-orm'
|
||||
// import type { Metadata } from 'next'
|
||||
// import { redirect } from 'next/navigation'
|
||||
// import { getSession } from '@/lib/auth'
|
||||
// import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
// import { verifyWorkspaceMembership } from '@/app/api/workflows/utils'
|
||||
// import TemplateDetails from '@/app/templates/[id]/template'
|
||||
const logger = createLogger('WorkspaceTemplateMetadata')
|
||||
|
||||
// const logger = createLogger('WorkspaceTemplateMetadata')
|
||||
|
||||
// interface TemplatePageProps {
|
||||
// params: Promise<{
|
||||
// workspaceId: string
|
||||
// id: string
|
||||
// }>
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Generate dynamic metadata for workspace template pages.
|
||||
// * This provides OpenGraph images for social media sharing.
|
||||
// */
|
||||
// export async function generateMetadata({
|
||||
// params,
|
||||
// }: {
|
||||
// params: Promise<{ workspaceId: string; id: string }>
|
||||
// }): Promise<Metadata> {
|
||||
// const { workspaceId, id } = await params
|
||||
//
|
||||
// try {
|
||||
// const result = await db
|
||||
// .select({
|
||||
// template: templates,
|
||||
// creator: templateCreators,
|
||||
// })
|
||||
// .from(templates)
|
||||
// .leftJoin(templateCreators, eq(templates.creatorId, templateCreators.id))
|
||||
// .where(eq(templates.id, id))
|
||||
// .limit(1)
|
||||
//
|
||||
// if (result.length === 0) {
|
||||
// return {
|
||||
// title: 'Template Not Found',
|
||||
// description: 'The requested template could not be found.',
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// const { template, creator } = result[0]
|
||||
// const baseUrl = getBaseUrl()
|
||||
//
|
||||
// const details = template.details as { tagline?: string; about?: string } | null
|
||||
// const description = details?.tagline || 'AI workflow template on Sim'
|
||||
//
|
||||
// const hasOgImage = !!template.ogImageUrl
|
||||
// const ogImageUrl = template.ogImageUrl || `${baseUrl}/logo/primary/rounded.png`
|
||||
//
|
||||
// return {
|
||||
// title: template.name,
|
||||
// description,
|
||||
// openGraph: {
|
||||
// title: template.name,
|
||||
// description,
|
||||
// type: 'website',
|
||||
// url: `${baseUrl}/workspace/${workspaceId}/templates/${id}`,
|
||||
// siteName: 'Sim',
|
||||
// images: [
|
||||
// {
|
||||
// url: ogImageUrl,
|
||||
// width: hasOgImage ? 1200 : 512,
|
||||
// height: hasOgImage ? 630 : 512,
|
||||
// alt: `${template.name} - Workflow Preview`,
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// twitter: {
|
||||
// card: hasOgImage ? 'summary_large_image' : 'summary',
|
||||
// title: template.name,
|
||||
// description,
|
||||
// images: [ogImageUrl],
|
||||
// creator: creator?.details
|
||||
// ? ((creator.details as Record<string, unknown>).xHandle as string) || undefined
|
||||
// : undefined,
|
||||
// },
|
||||
// }
|
||||
// } catch (error) {
|
||||
// logger.error('Failed to generate workspace template metadata:', error)
|
||||
// return {
|
||||
// title: 'Template',
|
||||
// description: 'AI workflow template on Sim',
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
interface TemplatePageProps {
|
||||
params: Promise<{
|
||||
workspaceId: string
|
||||
id: string
|
||||
}>
|
||||
}
|
||||
|
||||
/**
|
||||
* Workspace-scoped template detail page — currently disabled, returns 404.
|
||||
* Generate dynamic metadata for workspace template pages.
|
||||
* This provides OpenGraph images for social media sharing.
|
||||
*/
|
||||
export default function TemplatePage() {
|
||||
notFound()
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ workspaceId: string; id: string }>
|
||||
}): Promise<Metadata> {
|
||||
const { workspaceId, id } = await params
|
||||
|
||||
try {
|
||||
const result = await db
|
||||
.select({
|
||||
template: templates,
|
||||
creator: templateCreators,
|
||||
})
|
||||
.from(templates)
|
||||
.leftJoin(templateCreators, eq(templates.creatorId, templateCreators.id))
|
||||
.where(eq(templates.id, id))
|
||||
.limit(1)
|
||||
|
||||
if (result.length === 0) {
|
||||
return {
|
||||
title: 'Template Not Found',
|
||||
description: 'The requested template could not be found.',
|
||||
}
|
||||
}
|
||||
|
||||
const { template, creator } = result[0]
|
||||
const baseUrl = getBaseUrl()
|
||||
|
||||
const details = template.details as { tagline?: string; about?: string } | null
|
||||
const description = details?.tagline || 'AI workflow template on Sim'
|
||||
|
||||
const hasOgImage = !!template.ogImageUrl
|
||||
const ogImageUrl = template.ogImageUrl || `${baseUrl}/logo/primary/rounded.png`
|
||||
|
||||
return {
|
||||
title: template.name,
|
||||
description,
|
||||
openGraph: {
|
||||
title: template.name,
|
||||
description,
|
||||
type: 'website',
|
||||
url: `${baseUrl}/workspace/${workspaceId}/templates/${id}`,
|
||||
siteName: 'Sim',
|
||||
images: [
|
||||
{
|
||||
url: ogImageUrl,
|
||||
width: hasOgImage ? 1200 : 512,
|
||||
height: hasOgImage ? 630 : 512,
|
||||
alt: `${template.name} - Workflow Preview`,
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: hasOgImage ? 'summary_large_image' : 'summary',
|
||||
title: template.name,
|
||||
description,
|
||||
images: [ogImageUrl],
|
||||
creator: creator?.details
|
||||
? ((creator.details as Record<string, unknown>).xHandle as string) || undefined
|
||||
: undefined,
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to generate workspace template metadata:', error)
|
||||
return {
|
||||
title: 'Template',
|
||||
description: 'AI workflow template on Sim',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Workspace-scoped template detail page.
|
||||
* Requires authentication and workspace membership to access.
|
||||
* Uses the shared TemplateDetails component with workspace context.
|
||||
*/
|
||||
export default async function TemplatePage({ params }: TemplatePageProps) {
|
||||
const { workspaceId, id } = await params
|
||||
const session = await getSession()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
redirect(`/templates/${id}`)
|
||||
}
|
||||
|
||||
const hasPermission = await verifyWorkspaceMembership(session.user.id, workspaceId)
|
||||
if (!hasPermission) {
|
||||
redirect('/')
|
||||
}
|
||||
|
||||
return <TemplateDetails isWorkspaceContext={true} />
|
||||
}
|
||||
|
||||
@@ -1,29 +1,205 @@
|
||||
import { notFound } from 'next/navigation'
|
||||
import { db } from '@sim/db'
|
||||
import { settings, templateCreators, templateStars, templates, user } from '@sim/db/schema'
|
||||
import { and, desc, eq, sql } from 'drizzle-orm'
|
||||
import type { Metadata } from 'next'
|
||||
import { redirect } from 'next/navigation'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { verifyWorkspaceMembership } from '@/app/api/workflows/utils'
|
||||
import type { Template as WorkspaceTemplate } from '@/app/workspace/[workspaceId]/templates/templates'
|
||||
import Templates from '@/app/workspace/[workspaceId]/templates/templates'
|
||||
import { getUserPermissionConfig } from '@/ee/access-control/utils/permission-check'
|
||||
|
||||
// import { db } from '@sim/db'
|
||||
// import { settings, templateCreators, templateStars, templates, user } from '@sim/db/schema'
|
||||
// import { and, desc, eq, sql } from 'drizzle-orm'
|
||||
// import type { Metadata } from 'next'
|
||||
// import { redirect } from 'next/navigation'
|
||||
// import { getSession } from '@/lib/auth'
|
||||
// import { verifyWorkspaceMembership } from '@/app/api/workflows/utils'
|
||||
// import type { Template as WorkspaceTemplate } from '@/app/workspace/[workspaceId]/templates/templates'
|
||||
// import Templates from '@/app/workspace/[workspaceId]/templates/templates'
|
||||
// import { getUserPermissionConfig } from '@/ee/access-control/utils/permission-check'
|
||||
export const metadata: Metadata = {
|
||||
title: 'Templates',
|
||||
}
|
||||
|
||||
// export const metadata: Metadata = {
|
||||
// title: 'Templates',
|
||||
// }
|
||||
|
||||
// interface TemplatesPageProps {
|
||||
// params: Promise<{
|
||||
// workspaceId: string
|
||||
// }>
|
||||
// }
|
||||
interface TemplatesPageProps {
|
||||
params: Promise<{
|
||||
workspaceId: string
|
||||
}>
|
||||
}
|
||||
|
||||
/**
|
||||
* Workspace-scoped Templates page — currently disabled, returns 404.
|
||||
* Workspace-scoped Templates page.
|
||||
* Requires authentication and workspace membership to access.
|
||||
*/
|
||||
export default function TemplatesPage() {
|
||||
notFound()
|
||||
export default async function TemplatesPage({ params }: TemplatesPageProps) {
|
||||
const { workspaceId } = await params
|
||||
const session = await getSession()
|
||||
|
||||
// Redirect unauthenticated users to public templates page
|
||||
if (!session?.user?.id) {
|
||||
redirect('/templates')
|
||||
}
|
||||
|
||||
// Verify workspace membership
|
||||
const hasPermission = await verifyWorkspaceMembership(session.user.id, workspaceId)
|
||||
if (!hasPermission) {
|
||||
redirect('/')
|
||||
}
|
||||
|
||||
// Check permission group restrictions
|
||||
const permissionConfig = await getUserPermissionConfig(session.user.id)
|
||||
if (permissionConfig?.hideTemplates) {
|
||||
redirect(`/workspace/${workspaceId}`)
|
||||
}
|
||||
|
||||
// Determine effective super user (admin role AND UI mode enabled)
|
||||
const currentUser = await db
|
||||
.select({ role: user.role })
|
||||
.from(user)
|
||||
.where(eq(user.id, session.user.id))
|
||||
.limit(1)
|
||||
const userSettings = await db
|
||||
.select({ superUserModeEnabled: settings.superUserModeEnabled })
|
||||
.from(settings)
|
||||
.where(eq(settings.userId, session.user.id))
|
||||
.limit(1)
|
||||
|
||||
const isSuperUser = currentUser[0]?.role === 'admin'
|
||||
const superUserModeEnabled = userSettings[0]?.superUserModeEnabled ?? false
|
||||
const effectiveSuperUser = isSuperUser && superUserModeEnabled
|
||||
|
||||
// Load templates from database
|
||||
let rows:
|
||||
| Array<{
|
||||
id: string
|
||||
workflowId: string | null
|
||||
name: string
|
||||
details?: unknown
|
||||
creatorId: string | null
|
||||
creator: {
|
||||
id: string
|
||||
referenceType: 'user' | 'organization'
|
||||
referenceId: string
|
||||
name: string
|
||||
profileImageUrl?: string | null
|
||||
details?: unknown
|
||||
verified: boolean
|
||||
} | null
|
||||
views: number
|
||||
stars: number
|
||||
status: 'pending' | 'approved' | 'rejected'
|
||||
tags: string[]
|
||||
requiredCredentials: unknown
|
||||
state: unknown
|
||||
createdAt: Date | string
|
||||
updatedAt: Date | string
|
||||
isStarred?: boolean
|
||||
}>
|
||||
| undefined
|
||||
|
||||
if (session?.user?.id) {
|
||||
const whereCondition = effectiveSuperUser ? undefined : eq(templates.status, 'approved')
|
||||
rows = await db
|
||||
.select({
|
||||
id: templates.id,
|
||||
workflowId: templates.workflowId,
|
||||
name: templates.name,
|
||||
details: templates.details,
|
||||
creatorId: templates.creatorId,
|
||||
creator: templateCreators,
|
||||
views: templates.views,
|
||||
stars: templates.stars,
|
||||
status: templates.status,
|
||||
tags: templates.tags,
|
||||
requiredCredentials: templates.requiredCredentials,
|
||||
state: templates.state,
|
||||
createdAt: templates.createdAt,
|
||||
updatedAt: templates.updatedAt,
|
||||
isStarred: sql<boolean>`CASE WHEN ${templateStars.id} IS NOT NULL THEN true ELSE false END`,
|
||||
})
|
||||
.from(templates)
|
||||
.leftJoin(
|
||||
templateStars,
|
||||
and(eq(templateStars.templateId, templates.id), eq(templateStars.userId, session.user.id))
|
||||
)
|
||||
.leftJoin(templateCreators, eq(templates.creatorId, templateCreators.id))
|
||||
.where(whereCondition)
|
||||
.orderBy(desc(templates.views), desc(templates.createdAt))
|
||||
} else {
|
||||
rows = await db
|
||||
.select({
|
||||
id: templates.id,
|
||||
workflowId: templates.workflowId,
|
||||
name: templates.name,
|
||||
details: templates.details,
|
||||
creatorId: templates.creatorId,
|
||||
creator: templateCreators,
|
||||
views: templates.views,
|
||||
stars: templates.stars,
|
||||
status: templates.status,
|
||||
tags: templates.tags,
|
||||
requiredCredentials: templates.requiredCredentials,
|
||||
state: templates.state,
|
||||
createdAt: templates.createdAt,
|
||||
updatedAt: templates.updatedAt,
|
||||
})
|
||||
.from(templates)
|
||||
.leftJoin(templateCreators, eq(templates.creatorId, templateCreators.id))
|
||||
.where(eq(templates.status, 'approved'))
|
||||
.orderBy(desc(templates.views), desc(templates.createdAt))
|
||||
.then((r) => r.map((row) => ({ ...row, isStarred: false })))
|
||||
}
|
||||
|
||||
const initialTemplates: WorkspaceTemplate[] =
|
||||
rows?.map((row) => {
|
||||
const authorType = (row.creator?.referenceType as 'user' | 'organization') ?? 'user'
|
||||
const organizationId =
|
||||
row.creator?.referenceType === 'organization' ? row.creator.referenceId : null
|
||||
const userId =
|
||||
row.creator?.referenceType === 'user' ? row.creator.referenceId : '' /* no owner context */
|
||||
|
||||
return {
|
||||
// New structure fields
|
||||
id: row.id,
|
||||
workflowId: row.workflowId,
|
||||
name: row.name,
|
||||
details: row.details as { tagline?: string; about?: string } | null,
|
||||
creatorId: row.creatorId,
|
||||
creator: row.creator
|
||||
? {
|
||||
id: row.creator.id,
|
||||
name: row.creator.name,
|
||||
profileImageUrl: row.creator.profileImageUrl,
|
||||
details: row.creator.details as {
|
||||
about?: string
|
||||
xUrl?: string
|
||||
linkedinUrl?: string
|
||||
websiteUrl?: string
|
||||
contactEmail?: string
|
||||
} | null,
|
||||
referenceType: row.creator.referenceType,
|
||||
referenceId: row.creator.referenceId,
|
||||
verified: row.creator.verified,
|
||||
}
|
||||
: null,
|
||||
views: row.views,
|
||||
stars: row.stars,
|
||||
status: row.status,
|
||||
tags: row.tags,
|
||||
requiredCredentials: row.requiredCredentials,
|
||||
state: row.state as WorkspaceTemplate['state'],
|
||||
createdAt: row.createdAt,
|
||||
updatedAt: row.updatedAt,
|
||||
isStarred: row.isStarred ?? false,
|
||||
isSuperUser: effectiveSuperUser,
|
||||
// Legacy fields for backward compatibility
|
||||
userId,
|
||||
description: (row.details as any)?.tagline ?? null,
|
||||
author: row.creator?.name ?? 'Unknown',
|
||||
authorType,
|
||||
organizationId,
|
||||
color: '#3972F6', // default color for workspace cards
|
||||
icon: 'Workflow', // default icon for workspace cards
|
||||
}
|
||||
}) ?? []
|
||||
|
||||
return (
|
||||
<Templates
|
||||
initialTemplates={initialTemplates}
|
||||
currentUserId={session?.user?.id || ''}
|
||||
isSuperUser={effectiveSuperUser}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import type { GlobalCommand } from '@/app/workspace/[workspaceId]/providers/glob
|
||||
export type CommandId =
|
||||
| 'accept-diff-changes'
|
||||
| 'add-agent'
|
||||
// | 'goto-templates'
|
||||
| 'goto-templates'
|
||||
| 'goto-logs'
|
||||
| 'open-search'
|
||||
| 'run-workflow'
|
||||
@@ -52,11 +52,11 @@ export const COMMAND_DEFINITIONS: Record<CommandId, CommandDefinition> = {
|
||||
shortcut: 'Mod+Shift+A',
|
||||
allowInEditable: true,
|
||||
},
|
||||
// 'goto-templates': {
|
||||
// id: 'goto-templates',
|
||||
// shortcut: 'Mod+Y',
|
||||
// allowInEditable: true,
|
||||
// },
|
||||
'goto-templates': {
|
||||
id: 'goto-templates',
|
||||
shortcut: 'Mod+Y',
|
||||
allowInEditable: true,
|
||||
},
|
||||
'goto-logs': {
|
||||
id: 'goto-logs',
|
||||
shortcut: 'Mod+L',
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useCallback } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { Search } from 'lucide-react'
|
||||
import { Layout, Search } from 'lucide-react'
|
||||
import Image from 'next/image'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import { Button, Library } from '@/components/emcn'
|
||||
@@ -29,11 +29,11 @@ interface CommandItem {
|
||||
* Available commands list
|
||||
*/
|
||||
const commands: CommandItem[] = [
|
||||
// {
|
||||
// label: 'Templates',
|
||||
// icon: Layout,
|
||||
// shortcut: 'Y',
|
||||
// },
|
||||
{
|
||||
label: 'Templates',
|
||||
icon: Layout,
|
||||
shortcut: 'Y',
|
||||
},
|
||||
{
|
||||
label: 'New Agent',
|
||||
icon: AgentIcon,
|
||||
@@ -78,14 +78,14 @@ export function CommandList() {
|
||||
(label: string) => {
|
||||
try {
|
||||
switch (label) {
|
||||
// case 'Templates': {
|
||||
// if (!workspaceId) {
|
||||
// logger.warn('No workspace ID found, cannot navigate to templates from command list')
|
||||
// return
|
||||
// }
|
||||
// router.push(`/workspace/${workspaceId}/templates`)
|
||||
// return
|
||||
// }
|
||||
case 'Templates': {
|
||||
if (!workspaceId) {
|
||||
logger.warn('No workspace ID found, cannot navigate to templates from command list')
|
||||
return
|
||||
}
|
||||
router.push(`/workspace/${workspaceId}/templates`)
|
||||
return
|
||||
}
|
||||
case 'New Agent': {
|
||||
const event = new CustomEvent('add-block-from-toolbar', {
|
||||
detail: { type: 'agent', enableTriggerMode: false },
|
||||
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
useDeployWorkflow,
|
||||
useUndeployWorkflow,
|
||||
} from '@/hooks/queries/deployments'
|
||||
// import { useTemplateByWorkflow } from '@/hooks/queries/templates'
|
||||
import { useTemplateByWorkflow } from '@/hooks/queries/templates'
|
||||
import { useWorkflowMcpServers } from '@/hooks/queries/workflow-mcp-servers'
|
||||
import { useWorkspaceSettings } from '@/hooks/queries/workspace'
|
||||
import { usePermissionConfig } from '@/hooks/use-permission-config'
|
||||
@@ -73,7 +73,7 @@ interface WorkflowDeploymentInfoUI {
|
||||
isPublicApi: boolean
|
||||
}
|
||||
|
||||
type TabView = 'general' | 'api' | 'chat' | /* 'template' | */ 'mcp' | 'form' | 'a2a'
|
||||
type TabView = 'general' | 'api' | 'chat' | 'template' | 'mcp' | 'form' | 'a2a'
|
||||
|
||||
export function DeployModal({
|
||||
open,
|
||||
@@ -102,8 +102,8 @@ export function DeployModal({
|
||||
const [selectedStreamingOutputs, setSelectedStreamingOutputs] = useState<string[]>([])
|
||||
|
||||
const [showUndeployConfirm, setShowUndeployConfirm] = useState(false)
|
||||
// const [templateFormValid, setTemplateFormValid] = useState(false)
|
||||
// const [templateSubmitting, setTemplateSubmitting] = useState(false)
|
||||
const [templateFormValid, setTemplateFormValid] = useState(false)
|
||||
const [templateSubmitting, setTemplateSubmitting] = useState(false)
|
||||
const [mcpToolSubmitting, setMcpToolSubmitting] = useState(false)
|
||||
const [mcpToolCanSave, setMcpToolCanSave] = useState(false)
|
||||
const [a2aSubmitting, setA2aSubmitting] = useState(false)
|
||||
@@ -159,17 +159,17 @@ export function DeployModal({
|
||||
const hasA2aAgent = !!existingA2aAgent
|
||||
const isA2aPublished = existingA2aAgent?.isPublished ?? false
|
||||
|
||||
// const { data: existingTemplate } = useTemplateByWorkflow(workflowId || '', {
|
||||
// enabled: !!workflowId,
|
||||
// })
|
||||
// const hasExistingTemplate = !!existingTemplate
|
||||
// const templateStatus = existingTemplate
|
||||
// ? {
|
||||
// status: existingTemplate.status as 'pending' | 'approved' | 'rejected' | null,
|
||||
// views: existingTemplate.views,
|
||||
// stars: existingTemplate.stars,
|
||||
// }
|
||||
// : null
|
||||
const { data: existingTemplate } = useTemplateByWorkflow(workflowId || '', {
|
||||
enabled: !!workflowId,
|
||||
})
|
||||
const hasExistingTemplate = !!existingTemplate
|
||||
const templateStatus = existingTemplate
|
||||
? {
|
||||
status: existingTemplate.status as 'pending' | 'approved' | 'rejected' | null,
|
||||
views: existingTemplate.views,
|
||||
stars: existingTemplate.stars,
|
||||
}
|
||||
: null
|
||||
|
||||
const deployMutation = useDeployWorkflow()
|
||||
const undeployMutation = useUndeployWorkflow()
|
||||
@@ -406,10 +406,10 @@ export function DeployModal({
|
||||
}
|
||||
}, [])
|
||||
|
||||
// const handleTemplateFormSubmit = useCallback(() => {
|
||||
// const form = document.getElementById('template-deploy-form') as HTMLFormElement
|
||||
// form?.requestSubmit()
|
||||
// }, [])
|
||||
const handleTemplateFormSubmit = useCallback(() => {
|
||||
const form = document.getElementById('template-deploy-form') as HTMLFormElement
|
||||
form?.requestSubmit()
|
||||
}, [])
|
||||
|
||||
const handleMcpToolFormSubmit = useCallback(() => {
|
||||
const form = document.getElementById('mcp-deploy-form') as HTMLFormElement
|
||||
@@ -453,11 +453,11 @@ export function DeployModal({
|
||||
setShowA2aDeleteConfirm(false)
|
||||
}, [])
|
||||
|
||||
// const handleTemplateDelete = useCallback(() => {
|
||||
// const form = document.getElementById('template-deploy-form')
|
||||
// const deleteTrigger = form?.querySelector('[data-template-delete-trigger]') as HTMLButtonElement
|
||||
// deleteTrigger?.click()
|
||||
// }, [])
|
||||
const handleTemplateDelete = useCallback(() => {
|
||||
const form = document.getElementById('template-deploy-form')
|
||||
const deleteTrigger = form?.querySelector('[data-template-delete-trigger]') as HTMLButtonElement
|
||||
deleteTrigger?.click()
|
||||
}, [])
|
||||
|
||||
const isSubmitting = deployMutation.isPending
|
||||
const isUndeploying = undeployMutation.isPending
|
||||
@@ -938,28 +938,28 @@ function StatusBadge({ isWarning }: StatusBadgeProps) {
|
||||
)
|
||||
}
|
||||
|
||||
// interface TemplateStatusBadgeProps {
|
||||
// status: 'pending' | 'approved' | 'rejected' | null
|
||||
// views?: number
|
||||
// stars?: number
|
||||
// }
|
||||
interface TemplateStatusBadgeProps {
|
||||
status: 'pending' | 'approved' | 'rejected' | null
|
||||
views?: number
|
||||
stars?: number
|
||||
}
|
||||
|
||||
// function TemplateStatusBadge({ status, views, stars }: TemplateStatusBadgeProps) {
|
||||
// const isPending = status === 'pending'
|
||||
// const label = isPending ? 'Under review' : 'Live'
|
||||
function TemplateStatusBadge({ status, views, stars }: TemplateStatusBadgeProps) {
|
||||
const isPending = status === 'pending'
|
||||
const label = isPending ? 'Under review' : 'Live'
|
||||
|
||||
// const statsText =
|
||||
// status === 'approved' && views !== undefined && views > 0
|
||||
// ? `${views} views${stars !== undefined && stars > 0 ? ` • ${stars} stars` : ''}`
|
||||
// : null
|
||||
const statsText =
|
||||
status === 'approved' && views !== undefined && views > 0
|
||||
? `${views} views${stars !== undefined && stars > 0 ? ` • ${stars} stars` : ''}`
|
||||
: null
|
||||
|
||||
// return (
|
||||
// <Badge variant={isPending ? 'amber' : 'green'} size='lg' dot>
|
||||
// {label}
|
||||
// {statsText && <span>• {statsText}</span>}
|
||||
// </Badge>
|
||||
// )
|
||||
// }
|
||||
return (
|
||||
<Badge variant={isPending ? 'amber' : 'green'} size='lg' dot>
|
||||
{label}
|
||||
{statsText && <span>• {statsText}</span>}
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
|
||||
interface GeneralFooterProps {
|
||||
isDeployed?: boolean
|
||||
|
||||
@@ -197,10 +197,7 @@ export function Preview({
|
||||
const childTraceSpans = extractChildTraceSpans(blockExecution)
|
||||
const childBlockExecutions = buildBlockExecutions(childTraceSpans)
|
||||
|
||||
const workflowName =
|
||||
childWorkflowState.metadata?.name ||
|
||||
(blockExecution?.output as { childWorkflowName?: string } | undefined)?.childWorkflowName ||
|
||||
'Nested Workflow'
|
||||
const workflowName = childWorkflowState.metadata?.name || 'Nested Workflow'
|
||||
|
||||
setWorkflowStack((prev) => [
|
||||
...prev,
|
||||
|
||||
@@ -4,13 +4,11 @@ import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/emcn'
|
||||
import { Plus } from '@/components/emcn/icons'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import type { useHoverMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks'
|
||||
import type { FolderTreeNode } from '@/stores/folders/types'
|
||||
@@ -23,8 +21,6 @@ interface CollapsedSidebarMenuProps {
|
||||
ariaLabel?: string
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
createLabel?: string
|
||||
onCreateClick?: () => void
|
||||
}
|
||||
|
||||
export function CollapsedSidebarMenu({
|
||||
@@ -34,8 +30,6 @@ export function CollapsedSidebarMenu({
|
||||
ariaLabel,
|
||||
children,
|
||||
className,
|
||||
createLabel,
|
||||
onCreateClick,
|
||||
}: CollapsedSidebarMenuProps) {
|
||||
return (
|
||||
<div className={cn('flex flex-col px-[8px]', className)}>
|
||||
@@ -60,15 +54,6 @@ export function CollapsedSidebarMenu({
|
||||
</DropdownMenuTrigger>
|
||||
</div>
|
||||
<DropdownMenuContent side='right' align='start' sideOffset={8} {...hover.contentProps}>
|
||||
{createLabel && onCreateClick && (
|
||||
<>
|
||||
<DropdownMenuItem onSelect={onCreateClick}>
|
||||
<Plus className='h-[14px] w-[14px]' />
|
||||
<span>{createLabel}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
{children}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { usePathname, useRouter } from 'next/navigation'
|
||||
import { useLeaveWorkspace } from '@/hooks/queries/invitations'
|
||||
import {
|
||||
useCreateWorkspace,
|
||||
@@ -33,6 +33,7 @@ export function useWorkspaceManagement({
|
||||
sessionUserId,
|
||||
}: UseWorkspaceManagementProps) {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const queryClient = useQueryClient()
|
||||
const switchToWorkspace = useWorkflowRegistry((state) => state.switchToWorkspace)
|
||||
|
||||
@@ -49,10 +50,12 @@ export function useWorkspaceManagement({
|
||||
|
||||
const workspaceIdRef = useRef<string>(workspaceId)
|
||||
const routerRef = useRef<ReturnType<typeof useRouter>>(router)
|
||||
const pathnameRef = useRef<string | null>(pathname || null)
|
||||
const hasValidatedRef = useRef<boolean>(false)
|
||||
|
||||
workspaceIdRef.current = workspaceId
|
||||
routerRef.current = router
|
||||
pathnameRef.current = pathname || null
|
||||
|
||||
const activeWorkspace = useMemo(() => {
|
||||
if (!workspaces.length) return null
|
||||
@@ -111,7 +114,16 @@ export function useWorkspaceManagement({
|
||||
|
||||
try {
|
||||
await switchToWorkspace(workspace.id)
|
||||
routerRef.current?.push(`/workspace/${workspace.id}/home`)
|
||||
const currentPath = pathnameRef.current || ''
|
||||
const templateDetailMatch = currentPath.match(/^\/workspace\/[^/]+\/templates\/([^/]+)$/)
|
||||
if (templateDetailMatch) {
|
||||
const templateId = templateDetailMatch[1]
|
||||
routerRef.current?.push(`/workspace/${workspace.id}/templates/${templateId}`)
|
||||
} else if (/^\/workspace\/[^/]+\/templates$/.test(currentPath)) {
|
||||
routerRef.current?.push(`/workspace/${workspace.id}/templates`)
|
||||
} else {
|
||||
routerRef.current?.push(`/workspace/${workspace.id}/home`)
|
||||
}
|
||||
logger.info(`Switched to workspace: ${workspace.name} (${workspace.id})`)
|
||||
} catch (error) {
|
||||
logger.error('Error switching workspace:', error)
|
||||
|
||||
@@ -919,22 +919,22 @@ export const Sidebar = memo(function Sidebar() {
|
||||
}
|
||||
},
|
||||
},
|
||||
// {
|
||||
// id: 'goto-templates',
|
||||
// handler: () => {
|
||||
// try {
|
||||
// const pathWorkspaceId = resolveWorkspaceIdFromPath()
|
||||
// if (pathWorkspaceId) {
|
||||
// navigateToPage(`/workspace/${pathWorkspaceId}/templates`)
|
||||
// logger.info('Navigated to templates', { workspaceId: pathWorkspaceId })
|
||||
// } else {
|
||||
// logger.warn('No workspace ID found, cannot navigate to templates')
|
||||
// }
|
||||
// } catch (err) {
|
||||
// logger.error('Failed to navigate to templates', { err })
|
||||
// }
|
||||
// },
|
||||
// },
|
||||
{
|
||||
id: 'goto-templates',
|
||||
handler: () => {
|
||||
try {
|
||||
const pathWorkspaceId = resolveWorkspaceIdFromPath()
|
||||
if (pathWorkspaceId) {
|
||||
navigateToPage(`/workspace/${pathWorkspaceId}/templates`)
|
||||
logger.info('Navigated to templates', { workspaceId: pathWorkspaceId })
|
||||
} else {
|
||||
logger.warn('No workspace ID found, cannot navigate to templates')
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('Failed to navigate to templates', { err })
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'goto-logs',
|
||||
handler: () => {
|
||||
@@ -1109,52 +1109,6 @@ export const Sidebar = memo(function Sidebar() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick-create button (collapsed only) */}
|
||||
{isCollapsed && showCollapsedContent && (
|
||||
<div className='flex flex-shrink-0 flex-col px-[8px] pt-[8px]'>
|
||||
<DropdownMenu>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type='button'
|
||||
aria-label='Create new'
|
||||
className='mx-[2px] flex h-[30px] items-center justify-center rounded-[8px] px-[8px] hover:bg-[var(--surface-active)]'
|
||||
>
|
||||
<Plus className='h-[16px] w-[16px] flex-shrink-0 text-[var(--text-icon)]' />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side='right'>
|
||||
<p>Create new</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
<DropdownMenuContent side='right' align='start' sideOffset={8}>
|
||||
<DropdownMenuItem
|
||||
onSelect={() => navigateToPage(`/workspace/${workspaceId}/home`)}
|
||||
>
|
||||
<Blimp className='h-[14px] w-[14px]' />
|
||||
<span>New task</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onSelect={handleCreateWorkflow}
|
||||
disabled={!canEdit || isCreatingWorkflow}
|
||||
>
|
||||
<div
|
||||
className='h-[14px] w-[14px] flex-shrink-0 rounded-[3px] border-[2px]'
|
||||
style={{
|
||||
backgroundColor: 'var(--text-icon)',
|
||||
borderColor: 'color-mix(in srgb, var(--text-icon) 60%, transparent)',
|
||||
backgroundClip: 'padding-box',
|
||||
}}
|
||||
/>
|
||||
<span>{isCreatingWorkflow ? 'Creating...' : 'New workflow'}</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Scrollable Tasks + Workflows */}
|
||||
<div
|
||||
ref={isCollapsed ? undefined : scrollContainerRef}
|
||||
@@ -1195,8 +1149,6 @@ export const Sidebar = memo(function Sidebar() {
|
||||
onClick={() => navigateToPage(`/workspace/${workspaceId}/home`)}
|
||||
ariaLabel='Tasks'
|
||||
className='mt-[6px]'
|
||||
createLabel='New task'
|
||||
onCreateClick={() => navigateToPage(`/workspace/${workspaceId}/home`)}
|
||||
>
|
||||
{tasksLoading ? (
|
||||
<DropdownMenuItem disabled>
|
||||
@@ -1366,8 +1318,6 @@ export const Sidebar = memo(function Sidebar() {
|
||||
onClick={handleCreateWorkflow}
|
||||
ariaLabel='Workflows'
|
||||
className='mt-[6px]'
|
||||
createLabel='New workflow'
|
||||
onCreateClick={canEdit ? handleCreateWorkflow : undefined}
|
||||
>
|
||||
{workflowsLoading && regularWorkflows.length === 0 ? (
|
||||
<DropdownMenuItem disabled>
|
||||
|
||||
@@ -18,9 +18,6 @@ const AUTO_DISMISS_MS = 0
|
||||
const EXIT_ANIMATION_MS = 200
|
||||
const MAX_VISIBLE = 20
|
||||
|
||||
const RING_RADIUS = 5.5
|
||||
const RING_CIRCUMFERENCE = 2 * Math.PI * RING_RADIUS
|
||||
|
||||
type ToastVariant = 'default' | 'success' | 'error'
|
||||
|
||||
interface ToastAction {
|
||||
@@ -31,6 +28,7 @@ interface ToastAction {
|
||||
interface ToastData {
|
||||
id: string
|
||||
message: string
|
||||
description?: string
|
||||
variant: ToastVariant
|
||||
action?: ToastAction
|
||||
duration: number
|
||||
@@ -38,6 +36,7 @@ interface ToastData {
|
||||
|
||||
type ToastInput = {
|
||||
message: string
|
||||
description?: string
|
||||
variant?: ToastVariant
|
||||
action?: ToastAction
|
||||
duration?: number
|
||||
@@ -91,31 +90,12 @@ export function useToast() {
|
||||
return ctx
|
||||
}
|
||||
|
||||
function CountdownRing({ duration }: { duration: number }) {
|
||||
return (
|
||||
<svg
|
||||
width='14'
|
||||
height='14'
|
||||
viewBox='0 0 16 16'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
style={{ transform: 'rotate(-90deg) scaleX(-1)' }}
|
||||
>
|
||||
<circle cx='8' cy='8' r={RING_RADIUS} stroke='var(--border)' strokeWidth='1.5' />
|
||||
<circle
|
||||
cx='8'
|
||||
cy='8'
|
||||
r={RING_RADIUS}
|
||||
stroke='var(--text-icon)'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
strokeDasharray={RING_CIRCUMFERENCE}
|
||||
style={{
|
||||
animation: `notification-countdown ${duration}ms linear forwards`,
|
||||
}}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
const VARIANT_STYLES: Record<ToastVariant, string> = {
|
||||
default: 'border-[var(--border)] bg-[var(--bg)] text-[var(--text-primary)]',
|
||||
success:
|
||||
'border-emerald-200 bg-emerald-50 text-emerald-900 dark:border-emerald-800/40 dark:bg-emerald-950/30 dark:text-emerald-200',
|
||||
error:
|
||||
'border-red-200 bg-red-50 text-red-900 dark:border-red-800/40 dark:bg-red-950/30 dark:text-red-200',
|
||||
}
|
||||
|
||||
function ToastItem({ toast: t, onDismiss }: { toast: ToastData; onDismiss: (id: string) => void }) {
|
||||
@@ -137,48 +117,38 @@ function ToastItem({ toast: t, onDismiss }: { toast: ToastData; onDismiss: (id:
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'pointer-events-auto w-[240px] overflow-hidden rounded-[8px] border border-[var(--border)] bg-[var(--bg)] shadow-sm',
|
||||
'pointer-events-auto flex w-[320px] items-start gap-[8px] rounded-[8px] border px-[12px] py-[10px] shadow-md transition-all',
|
||||
VARIANT_STYLES[t.variant],
|
||||
exiting
|
||||
? 'animate-[toast-exit_200ms_ease-in_forwards]'
|
||||
: 'animate-[toast-enter_200ms_ease-out_forwards]'
|
||||
)}
|
||||
>
|
||||
<div className='flex flex-col gap-[8px] p-[8px]'>
|
||||
<div className='flex items-start gap-[8px]'>
|
||||
<div className='line-clamp-2 min-w-0 flex-1 font-medium text-[12px] text-[var(--text-body)]'>
|
||||
{t.variant === 'error' && (
|
||||
<span className='mr-[8px] mb-[2px] inline-block h-[8px] w-[8px] rounded-[2px] bg-[var(--text-error)] align-middle' />
|
||||
)}
|
||||
{t.variant === 'success' && (
|
||||
<span className='mr-[8px] mb-[2px] inline-block h-[8px] w-[8px] rounded-[2px] bg-[var(--text-success)] align-middle' />
|
||||
)}
|
||||
{t.message}
|
||||
</div>
|
||||
<div className='flex shrink-0 items-start gap-[2px]'>
|
||||
{t.duration > 0 && <CountdownRing duration={t.duration} />}
|
||||
<button
|
||||
type='button'
|
||||
onClick={dismiss}
|
||||
aria-label='Dismiss notification'
|
||||
className='-m-[2px] shrink-0 rounded-[5px] p-[4px] hover:bg-[var(--surface-active)]'
|
||||
>
|
||||
<X className='h-[14px] w-[14px] text-[var(--text-icon)]' />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{t.action && (
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => {
|
||||
t.action!.onClick()
|
||||
dismiss()
|
||||
}}
|
||||
className='w-full rounded-[5px] bg-[var(--surface-active)] px-[8px] py-[4px] font-medium text-[12px] hover:bg-[var(--surface-hover)]'
|
||||
>
|
||||
{t.action.label}
|
||||
</button>
|
||||
<div className='min-w-0 flex-1'>
|
||||
<p className='font-medium text-[13px] leading-[18px]'>{t.message}</p>
|
||||
{t.description && (
|
||||
<p className='mt-[2px] text-[12px] leading-[16px] opacity-80'>{t.description}</p>
|
||||
)}
|
||||
</div>
|
||||
{t.action && (
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => {
|
||||
t.action!.onClick()
|
||||
dismiss()
|
||||
}}
|
||||
className='shrink-0 font-medium text-[13px] underline underline-offset-2 opacity-90 hover:opacity-100'
|
||||
>
|
||||
{t.action.label}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type='button'
|
||||
onClick={dismiss}
|
||||
className='shrink-0 rounded-[4px] p-[2px] opacity-60 hover:opacity-100'
|
||||
>
|
||||
<X className='h-[14px] w-[14px]' />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -205,6 +175,7 @@ export function ToastProvider({ children }: { children?: ReactNode }) {
|
||||
const data: ToastData = {
|
||||
id,
|
||||
message: input.message,
|
||||
description: input.description,
|
||||
variant: input.variant ?? 'default',
|
||||
action: input.action,
|
||||
duration: input.duration ?? AUTO_DISMISS_MS,
|
||||
|
||||
@@ -4140,15 +4140,14 @@ export function IncidentioIcon(props: SVGProps<SVGSVGElement>) {
|
||||
|
||||
export function InfisicalIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='20 25 233 132' xmlns='http://www.w3.org/2000/svg'>
|
||||
<svg {...props} viewBox='0 0 273 182' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
d='m191.6 39.4c-20.3 0-37.15 13.21-52.9 30.61-12.99-16.4-29.8-30.61-51.06-30.61-27.74 0-50.44 23.86-50.44 51.33 0 26.68 21.43 51.8 48.98 51.8 20.55 0 37.07-13.86 51.32-31.81 12.69 16.97 29.1 31.41 53.2 31.41 27.13 0 49.85-22.96 49.85-51.4 0-27.12-20.44-51.33-48.95-51.33zm-104.3 77.94c-14.56 0-25.51-12.84-25.51-26.07 0-13.7 10.95-28.29 25.51-28.29 14.93 0 25.71 11.6 37.6 27.34-11.31 15.21-22.23 27.02-37.6 27.02zm104.4 0.25c-15 0-25.28-11.13-37.97-27.37 12.69-16.4 22.01-27.24 37.59-27.24 14.97 0 24.79 13.25 24.79 27.26 0 13-10.17 27.35-24.41 27.35z'
|
||||
fill='black'
|
||||
fill='currentColor'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function IntercomIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
@@ -4254,7 +4253,7 @@ export function ZoomIcon(props: SVGProps<SVGSVGElement>) {
|
||||
fill='currentColor'
|
||||
width='800px'
|
||||
height='800px'
|
||||
viewBox='-1 9.5 34 13'
|
||||
viewBox='0 0 32 32'
|
||||
version='1.1'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
|
||||
@@ -363,10 +363,6 @@ export class WorkflowBlockHandler implements BlockHandler {
|
||||
const workflowStateWithVariables = {
|
||||
...workflowState,
|
||||
variables: workflowVariables,
|
||||
metadata: {
|
||||
...(workflowState.metadata || {}),
|
||||
name: workflowData.name || DEFAULTS.WORKFLOW_NAME,
|
||||
},
|
||||
}
|
||||
|
||||
if (Object.keys(workflowVariables).length > 0) {
|
||||
@@ -448,18 +444,13 @@ export class WorkflowBlockHandler implements BlockHandler {
|
||||
)
|
||||
|
||||
const workflowVariables = (wfData?.variables as Record<string, any>) || {}
|
||||
const childName = wfData?.name || DEFAULTS.WORKFLOW_NAME
|
||||
const workflowStateWithVariables = {
|
||||
...deployedState,
|
||||
variables: workflowVariables,
|
||||
metadata: {
|
||||
...(deployedState.metadata || {}),
|
||||
name: childName,
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
name: childName,
|
||||
name: wfData?.name || DEFAULTS.WORKFLOW_NAME,
|
||||
serializedState: serializedWorkflow,
|
||||
variables: workflowVariables,
|
||||
workflowState: workflowStateWithVariables,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
import { cache } from 'react'
|
||||
import matter from 'gray-matter'
|
||||
import { compileMDX } from 'next-mdx-remote/rsc'
|
||||
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
|
||||
@@ -91,20 +90,6 @@ export async function getAllPostMeta(): Promise<BlogMeta[]> {
|
||||
return (await scanFrontmatters()).filter((p) => !p.draft)
|
||||
}
|
||||
|
||||
export const getNavBlogPosts = cache(
|
||||
async (): Promise<Pick<BlogMeta, 'slug' | 'title' | 'ogImage'>[]> => {
|
||||
const allPosts = await getAllPostMeta()
|
||||
const featuredPost = allPosts.find((p) => p.featured) ?? allPosts[0]
|
||||
if (!featuredPost) return []
|
||||
const recentPosts = allPosts.filter((p) => p.slug !== featuredPost.slug).slice(0, 4)
|
||||
return [featuredPost, ...recentPosts].map((p) => ({
|
||||
slug: p.slug,
|
||||
title: p.title,
|
||||
ogImage: p.ogImage,
|
||||
}))
|
||||
}
|
||||
)
|
||||
|
||||
export async function getAllTags(): Promise<TagWithCount[]> {
|
||||
const posts = await getAllPostMeta()
|
||||
const counts: Record<string, number> = {}
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
insertRow,
|
||||
queryRows,
|
||||
renameColumn,
|
||||
renameTable,
|
||||
updateColumnConstraints,
|
||||
updateColumnType,
|
||||
updateRow,
|
||||
@@ -879,36 +878,6 @@ export const userTableServerTool: BaseServerTool<UserTableArgs, UserTableResult>
|
||||
}
|
||||
}
|
||||
|
||||
case 'rename': {
|
||||
if (!args.tableId) {
|
||||
return { success: false, message: 'Table ID is required' }
|
||||
}
|
||||
const newName = (args as Record<string, unknown>).newName as string | undefined
|
||||
if (!newName) {
|
||||
return { success: false, message: 'newName is required for renaming a table' }
|
||||
}
|
||||
if (!workspaceId) {
|
||||
return { success: false, message: 'Workspace ID is required' }
|
||||
}
|
||||
|
||||
const table = await getTableById(args.tableId)
|
||||
if (!table) {
|
||||
return { success: false, message: `Table not found: ${args.tableId}` }
|
||||
}
|
||||
if (table.workspaceId !== workspaceId) {
|
||||
return { success: false, message: 'Table not found' }
|
||||
}
|
||||
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
const renamed = await renameTable(args.tableId, newName, requestId)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Renamed table to "${renamed.name}"`,
|
||||
data: { table: { id: renamed.id, name: renamed.name } },
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return { success: false, message: `Unknown operation: ${operation}` }
|
||||
}
|
||||
|
||||
@@ -127,7 +127,6 @@ export const UserTableArgsSchema = z.object({
|
||||
'rename_column',
|
||||
'delete_column',
|
||||
'update_column',
|
||||
'rename',
|
||||
]),
|
||||
args: z
|
||||
.object({
|
||||
|
||||
@@ -164,34 +164,6 @@ export class RateLimiter {
|
||||
}
|
||||
}
|
||||
|
||||
async checkRateLimitDirect(
|
||||
storageKey: string,
|
||||
config: { maxTokens: number; refillRate: number; refillIntervalMs: number }
|
||||
): Promise<RateLimitResult> {
|
||||
try {
|
||||
const result = await this.storage.consumeTokens(storageKey, 1, config)
|
||||
if (!result.allowed) {
|
||||
logger.info('Rate limit exceeded', { storageKey, tokensRemaining: result.tokensRemaining })
|
||||
}
|
||||
return {
|
||||
allowed: result.allowed,
|
||||
remaining: result.tokensRemaining,
|
||||
resetAt: result.resetAt,
|
||||
retryAfterMs: result.retryAfterMs,
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Rate limit storage error - failing open (allowing request)', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
storageKey,
|
||||
})
|
||||
return {
|
||||
allowed: true,
|
||||
remaining: 1,
|
||||
resetAt: new Date(Date.now() + RATE_LIMIT_WINDOW_MS),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async resetRateLimit(rateLimitKey: string): Promise<void> {
|
||||
try {
|
||||
await Promise.all([
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 708 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 213 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 51 KiB |
@@ -24,10 +24,10 @@
|
||||
"!**/public/workbox-*.js",
|
||||
"!**/public/worker-*.js",
|
||||
"!**/public/fallback-*.js",
|
||||
"!**/apps/docs/.source",
|
||||
"!**/venv",
|
||||
"!**/.venv",
|
||||
"!**/uploads"
|
||||
"!**/apps/docs/.source/**",
|
||||
"!**/venv/**",
|
||||
"!**/.venv/**",
|
||||
"!**/uploads/**"
|
||||
]
|
||||
},
|
||||
"formatter": {
|
||||
|
||||
@@ -317,48 +317,26 @@ function extractOperationsFromContent(blockContent: string): { label: string; id
|
||||
* Scan all tool files under apps/sim/tools/ and build a map from tool ID to description.
|
||||
* Used to enrich operation entries with descriptions.
|
||||
*/
|
||||
interface ToolMaps {
|
||||
desc: Map<string, string>
|
||||
name: Map<string, string>
|
||||
}
|
||||
|
||||
async function buildToolDescriptionMap(): Promise<ToolMaps> {
|
||||
async function buildToolDescriptionMap(): Promise<Map<string, string>> {
|
||||
const toolsDir = path.join(rootDir, 'apps/sim/tools')
|
||||
const desc = new Map<string, string>()
|
||||
const name = new Map<string, string>()
|
||||
const map = new Map<string, string>()
|
||||
try {
|
||||
const toolFiles = await glob(`${toolsDir}/**/*.ts`)
|
||||
for (const file of toolFiles) {
|
||||
if (file.endsWith('index.ts') || file.endsWith('types.ts')) continue
|
||||
const content = fs.readFileSync(file, 'utf-8')
|
||||
|
||||
// Find every `id: 'tool_id'` occurrence in the file. For each, search
|
||||
// the next ~600 characters for `name:` and `description:` fields, cutting
|
||||
// off at the first `params:` block within that window. This handles both
|
||||
// the simple inline pattern (id → description → params in one object) and
|
||||
// the two-step pattern (base object holds params, ToolConfig export holds
|
||||
// id + description after the base object).
|
||||
const idRegex = /\bid\s*:\s*['"]([^'"]+)['"]/g
|
||||
let idMatch: RegExpExecArray | null
|
||||
while ((idMatch = idRegex.exec(content)) !== null) {
|
||||
const toolId = idMatch[1]
|
||||
if (desc.has(toolId)) continue
|
||||
const windowStart = idMatch.index
|
||||
const windowEnd = Math.min(windowStart + 600, content.length)
|
||||
const window = content.substring(windowStart, windowEnd)
|
||||
// Stop before any params block so we don't pick up param-level values
|
||||
const paramsOffset = window.search(/\bparams\s*:\s*\{/)
|
||||
const searchWindow = paramsOffset > 0 ? window.substring(0, paramsOffset) : window
|
||||
const descMatch = searchWindow.match(/\bdescription\s*:\s*['"]([^'"]{5,})['"]/)
|
||||
const nameMatch = searchWindow.match(/\bname\s*:\s*['"]([^'"]+)['"]/)
|
||||
if (descMatch) desc.set(toolId, descMatch[1])
|
||||
if (nameMatch) name.set(toolId, nameMatch[1])
|
||||
// Match top-level id + description fields in ToolConfig objects
|
||||
const idMatches = [...content.matchAll(/\bid\s*:\s*['"]([^'"]+)['"]/g)]
|
||||
const descMatches = [...content.matchAll(/\bdescription\s*:\s*['"]([^'"]{5,})['"]/g)]
|
||||
if (idMatches.length > 0 && descMatches.length > 0) {
|
||||
// The first id match and first description match are the tool's own fields
|
||||
map.set(idMatches[0][1], descMatches[0][1])
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Non-fatal: descriptions will be empty strings
|
||||
}
|
||||
return { desc, name }
|
||||
return map
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -503,7 +481,7 @@ async function writeIntegrationsJson(iconMapping: Record<string, string>): Promi
|
||||
}
|
||||
|
||||
const triggerRegistry = await buildTriggerRegistry()
|
||||
const { desc: toolDescMap, name: toolNameMap } = await buildToolDescriptionMap()
|
||||
const toolDescMap = await buildToolDescriptionMap()
|
||||
const integrations: IntegrationEntry[] = []
|
||||
const seenBaseTypes = new Set<string>()
|
||||
const blockFiles = (await glob(`${BLOCKS_PATH}/*.ts`)).sort()
|
||||
@@ -539,27 +517,11 @@ async function writeIntegrationsJson(iconMapping: Record<string, string>): Promi
|
||||
const iconName = (config as any).iconName || iconMapping[blockType] || ''
|
||||
const rawOps: { label: string; id: string }[] = (config as any).operations || []
|
||||
|
||||
// Enrich each operation with a description from the tool registry.
|
||||
// Primary lookup: derive toolId as `{baseType}_{operationId}` and check
|
||||
// the map directly. Fallback: some blocks use short op IDs that don't
|
||||
// match tool IDs (e.g. Slack uses "send" while the tool ID is
|
||||
// "slack_message"). In that case, find the tool in tools.access whose
|
||||
// name exactly matches the operation label.
|
||||
const toolsAccess: string[] = (config as any).tools?.access || []
|
||||
// Enrich each operation with a description from the tool registry
|
||||
const operations: OperationInfo[] = rawOps.map(({ label, id }) => {
|
||||
const toolId = `${baseType}_${id}`
|
||||
let opDesc = toolDescMap.get(toolId) || toolDescMap.get(id) || ''
|
||||
|
||||
if (!opDesc && toolsAccess.length > 0) {
|
||||
for (const tId of toolsAccess) {
|
||||
if (toolNameMap.get(tId)?.toLowerCase() === label.toLowerCase()) {
|
||||
opDesc = toolDescMap.get(tId) || ''
|
||||
if (opDesc) break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { name: label, description: opDesc }
|
||||
const desc = toolDescMap.get(toolId) || toolDescMap.get(id) || ''
|
||||
return { name: label, description: desc }
|
||||
})
|
||||
|
||||
const triggerIds: string[] = (config as any).triggerIds || []
|
||||
|
||||
Reference in New Issue
Block a user