chore: import upstream snapshot with attribution
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
+43
View File
@@ -0,0 +1,43 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
# bun specific
.bun
bun.lockb
bun-debug.log*
# testing
/coverage
# next.js
/.next/
/out/
/build
# misc
.DS_Store
*.pem
# env files
.env
*.env
.env.local
.env.development
.env.test
.env.production
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# Fumadocs
/.source/
.plans/
# fumadocs generates .source dirs anywhere a source.config sits
**/.source/
+23
View File
@@ -0,0 +1,23 @@
# docs
This is a Next.js application generated with
[Create Fumadocs](https://github.com/fuma-nama/fumadocs).
Run development server:
```bash
bun run dev
```
Open http://localhost:3000 with your browser to see the result.
## Learn More
To learn more about Next.js and Fumadocs, take a look at the following
resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js
features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
- [Fumadocs](https://fumadocs.vercel.app) - learn about Fumadocs
- [Bun Documentation](https://bun.sh/docs) - learn about Bun features and API
+355
View File
@@ -0,0 +1,355 @@
import type React from 'react'
import type { Root } from 'fumadocs-core/page-tree'
import { findNeighbour } from 'fumadocs-core/page-tree'
import type { ApiPageProps } from 'fumadocs-openapi/ui'
import { createAPIPage } from 'fumadocs-openapi/ui'
import { Pre } from 'fumadocs-ui/components/codeblock'
import defaultMdxComponents from 'fumadocs-ui/mdx'
import { DocsBody, DocsPage, DocsTitle } from 'fumadocs-ui/page'
import { notFound } from 'next/navigation'
import { PageFooter } from '@/components/docs-layout/page-footer'
import { PageNavigationArrows } from '@/components/docs-layout/page-navigation-arrows'
import { LLMCopyButton } from '@/components/page-actions'
import { PageTypeBadge } from '@/components/page-type-badge'
import { StructuredData } from '@/components/structured-data'
import { CodeBlock } from '@/components/ui/code-block'
import { Heading } from '@/components/ui/heading'
import { ResponseSection } from '@/components/ui/response-section'
import { i18n } from '@/lib/i18n'
import { getApiSpecContent, openapi } from '@/lib/openapi'
import { type PageData, source } from '@/lib/source'
import { DOCS_BASE_URL } from '@/lib/urls'
const SUPPORTED_LANGUAGES: Set<string> = new Set(i18n.languages)
const BASE_URL = DOCS_BASE_URL
const OG_LOCALE_MAP: Record<string, string> = {
en: 'en_US',
es: 'es_ES',
fr: 'fr_FR',
de: 'de_DE',
ja: 'ja_JP',
zh: 'zh_CN',
}
function resolveLangAndSlug(params: { slug?: string[]; lang: string }) {
const isValidLang = SUPPORTED_LANGUAGES.has(params.lang)
const lang = isValidLang ? params.lang : 'en'
const slug = isValidLang ? params.slug : [params.lang, ...(params.slug ?? [])]
return { lang, slug }
}
const APIPage = createAPIPage(openapi, {
playground: { enabled: false },
content: {
renderOperationLayout: async (slots) => {
return (
<div className='flex @4xl:flex-row flex-col @4xl:items-start gap-x-6 gap-y-4'>
<div className='min-w-0 flex-1'>
{slots.header}
{slots.apiPlayground}
{slots.authSchemes && <div className='api-section-divider'>{slots.authSchemes}</div>}
{slots.parameters}
{slots.body && <div className='api-section-divider'>{slots.body}</div>}
<ResponseSection>{slots.responses}</ResponseSection>
{slots.callbacks}
</div>
<div className='@4xl:sticky @4xl:top-[calc(var(--fd-docs-row-1,2rem)+1rem)] @4xl:w-[400px]'>
{slots.apiExample}
</div>
</div>
)
},
},
})
export default async function Page(props: { params: Promise<{ slug?: string[]; lang: string }> }) {
const params = await props.params
const { lang, slug } = resolveLangAndSlug(params)
const page = source.getPage(slug, lang)
if (!page) notFound()
const data = page.data as unknown as PageData & {
_openapi?: { method?: string }
getAPIPageProps?: () => ApiPageProps
}
const isOpenAPI = '_openapi' in data && data._openapi != null
const isApiReference = slug?.some((s) => s === 'api-reference') ?? false
// Academy lessons are video-first: drop the "On this page" TOC and go full
// width so the lesson hero/video gets the room (chapters live in-page instead).
const isAcademy = slug?.[0] === 'academy'
const pageTreeRecord = source.pageTree as Record<string, Root>
const pageTree = pageTreeRecord[lang] ?? pageTreeRecord.en ?? Object.values(pageTreeRecord)[0]
const rawNeighbours = pageTree ? findNeighbour(pageTree, page.url) : null
// Academy and API Reference are self-contained sections; keep prev/next inside
// the section instead of spilling into the main documentation tree. Match both
// the section's pages (`/<slug>/...`) and its index (`/<slug>`).
const sectionSlug = isApiReference ? 'api-reference' : isAcademy ? 'academy' : null
const inSection = (url?: string) =>
url != null && (url.includes(`/${sectionSlug}/`) || url.endsWith(`/${sectionSlug}`))
const neighbours = sectionSlug
? {
previous: inSection(rawNeighbours?.previous?.url) ? rawNeighbours?.previous : undefined,
next: inSection(rawNeighbours?.next?.url) ? rawNeighbours?.next : undefined,
}
: rawNeighbours
const generateBreadcrumbs = () => {
const breadcrumbs: Array<{ name: string; url: string }> = [
{
name: 'Home',
url: BASE_URL,
},
]
const urlParts = page.url.split('/').filter(Boolean)
let currentPath = ''
urlParts.forEach((part, index) => {
if (index === 0 && SUPPORTED_LANGUAGES.has(part)) {
currentPath = `/${part}`
return
}
currentPath += `/${part}`
const name = part
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ')
if (index === urlParts.length - 1) {
breadcrumbs.push({
name: data.title,
url: `${BASE_URL}${page.url}`,
})
} else {
breadcrumbs.push({
name: name,
url: `${BASE_URL}${currentPath}`,
})
}
})
return breadcrumbs
}
const breadcrumbs = generateBreadcrumbs()
const footer = <PageFooter previous={neighbours?.previous} next={neighbours?.next} />
if (isOpenAPI && data.getAPIPageProps) {
const apiProps = data.getAPIPageProps()
const apiPageContent = getApiSpecContent(
data.title,
data.description,
apiProps.operations ?? []
)
return (
<>
<StructuredData
title={data.title}
description={data.description || ''}
url={`${BASE_URL}${page.url}`}
lang={lang}
breadcrumb={breadcrumbs}
/>
<DocsPage
toc={data.toc}
breadcrumb={{
enabled: false,
}}
tableOfContent={{
style: 'clerk',
enabled: false,
}}
tableOfContentPopover={{
style: 'clerk',
enabled: false,
}}
footer={{
enabled: true,
component: footer,
}}
>
<div className='api-page-header relative mt-6 sm:mt-0'>
<div className='absolute top-1 right-0 flex items-center gap-2'>
<div className='hidden sm:flex'>
<LLMCopyButton content={apiPageContent} />
</div>
<PageNavigationArrows previous={neighbours?.previous} next={neighbours?.next} />
</div>
<DocsTitle className='mb-2'>{data.title}</DocsTitle>
</div>
<DocsBody>
<APIPage {...apiProps} />
</DocsBody>
</DocsPage>
</>
)
}
const MDX = data.body
const markdownContent = await data.getText('processed')
return (
<>
<StructuredData
title={data.title}
description={data.description || ''}
url={`${BASE_URL}${page.url}`}
lang={lang}
breadcrumb={breadcrumbs}
/>
<DocsPage
toc={data.toc}
full={data.full || isAcademy}
breadcrumb={{
enabled: false,
}}
tableOfContent={{
style: 'clerk',
enabled: !isAcademy,
single: false,
}}
tableOfContentPopover={{
style: 'clerk',
enabled: !isAcademy,
}}
footer={{
enabled: true,
component: footer,
}}
>
<div className='relative mt-6 sm:mt-0'>
<div className='absolute top-1 right-0 flex items-center gap-2'>
<div className='hidden sm:flex'>
<LLMCopyButton content={markdownContent} />
</div>
<PageNavigationArrows previous={neighbours?.previous} next={neighbours?.next} />
</div>
{data.pageType && <PageTypeBadge type={data.pageType} className='mb-3' />}
<DocsTitle className='mb-2'>{data.title}</DocsTitle>
</div>
<DocsBody>
<MDX
components={{
...defaultMdxComponents,
pre: (props: React.HTMLAttributes<HTMLPreElement>) => (
<CodeBlock {...props}>
<Pre>{props.children}</Pre>
</CodeBlock>
),
h1: (props: React.HTMLAttributes<HTMLHeadingElement>) => (
<Heading as='h1' {...props} />
),
h2: (props: React.HTMLAttributes<HTMLHeadingElement>) => (
<Heading as='h2' {...props} />
),
h3: (props: React.HTMLAttributes<HTMLHeadingElement>) => (
<Heading as='h3' {...props} />
),
h4: (props: React.HTMLAttributes<HTMLHeadingElement>) => (
<Heading as='h4' {...props} />
),
h5: (props: React.HTMLAttributes<HTMLHeadingElement>) => (
<Heading as='h5' {...props} />
),
h6: (props: React.HTMLAttributes<HTMLHeadingElement>) => (
<Heading as='h6' {...props} />
),
}}
/>
</DocsBody>
</DocsPage>
</>
)
}
export async function generateStaticParams() {
return source.generateParams()
}
export async function generateMetadata(props: {
params: Promise<{ slug?: string[]; lang: string }>
}) {
const params = await props.params
const { lang, slug } = resolveLangAndSlug(params)
const page = source.getPage(slug, lang)
if (!page) notFound()
const data = page.data as unknown as PageData
const fullUrl = `${BASE_URL}${page.url}`
const ogImageUrl = `${BASE_URL}/api/og?title=${encodeURIComponent(data.title)}`
return {
title: data.title,
description:
data.description ||
'Documentation for Sim — the open-source AI workspace where teams build, deploy, and manage AI agents.',
keywords: [
'AI agents',
'AI workspace',
'AI agent builder',
'build AI agents',
'LLM orchestration',
'AI automation',
'knowledge base',
'AI integrations',
data.title?.toLowerCase().split(' '),
]
.flat()
.filter(Boolean),
authors: [{ name: 'Sim Team' }],
category: 'Developer Tools',
openGraph: {
title: data.title,
description:
data.description ||
'Documentation for Sim — the open-source AI workspace where teams build, deploy, and manage AI agents.',
url: fullUrl,
siteName: 'Sim Documentation',
type: 'article',
locale: OG_LOCALE_MAP[lang] ?? 'en_US',
alternateLocale: i18n.languages.reduce<string[]>((locales, l) => {
if (l !== lang) {
locales.push(OG_LOCALE_MAP[l] ?? 'en_US')
}
return locales
}, []),
images: [
{
url: ogImageUrl,
width: 1200,
height: 675,
alt: data.title,
},
],
},
twitter: {
card: 'summary_large_image',
title: data.title,
description:
data.description ||
'Documentation for Sim — the open-source AI workspace where teams build, deploy, and manage AI agents.',
images: [ogImageUrl],
creator: '@simdotai',
site: '@simdotai',
},
canonical: fullUrl,
alternates: {
canonical: fullUrl,
languages: {
'x-default': `${BASE_URL}${page.url.replace(`/${lang}`, '')}`,
en: `${BASE_URL}${page.url.replace(`/${lang}`, '')}`,
es: `${BASE_URL}/es${page.url.replace(`/${lang}`, '')}`,
fr: `${BASE_URL}/fr${page.url.replace(`/${lang}`, '')}`,
de: `${BASE_URL}/de${page.url.replace(`/${lang}`, '')}`,
ja: `${BASE_URL}/ja${page.url.replace(`/${lang}`, '')}`,
zh: `${BASE_URL}/zh${page.url.replace(`/${lang}`, '')}`,
},
},
}
}
+131
View File
@@ -0,0 +1,131 @@
import type { ReactNode } from 'react'
import { defineI18nUI } from 'fumadocs-ui/i18n'
import { DocsLayout } from 'fumadocs-ui/layouts/docs'
import { RootProvider } from 'fumadocs-ui/provider/next'
import { Geist_Mono, Inter } from 'next/font/google'
import { AskAI } from '@/components/ai/ask-ai'
import {
SidebarFolder,
SidebarItem,
SidebarSeparator,
} from '@/components/docs-layout/sidebar-components'
import { Footer } from '@/components/footer/footer'
import { Navbar } from '@/components/navbar/navbar'
import { SimWordmark } from '@/components/ui/sim-logo'
import { i18n } from '@/lib/i18n'
import { serializeJsonLd } from '@/lib/json-ld'
import { source } from '@/lib/source'
import { DOCS_BASE_URL } from '@/lib/urls'
import { season } from '@/app/fonts/season'
import '../global.css'
const inter = Inter({
subsets: ['latin'],
variable: '--font-geist-sans',
display: 'swap',
})
const geistMono = Geist_Mono({
subsets: ['latin'],
variable: '--font-geist-mono',
display: 'swap',
})
const { provider } = defineI18nUI(i18n, {
translations: {
en: {
displayName: 'English',
},
es: {
displayName: 'Español',
},
fr: {
displayName: 'Français',
},
de: {
displayName: 'Deutsch',
},
ja: {
displayName: '日本語',
},
zh: {
displayName: '简体中文',
},
},
})
type LayoutProps = {
children: ReactNode
params: Promise<{ lang: string }>
}
const SUPPORTED_LANGUAGES: Set<string> = new Set(i18n.languages)
export default async function Layout({ children, params }: LayoutProps) {
const { lang: rawLang } = await params
const lang = SUPPORTED_LANGUAGES.has(rawLang) ? rawLang : 'en'
const structuredData = {
'@context': 'https://schema.org',
'@type': 'WebSite',
name: 'Sim Documentation',
description:
'Documentation for Sim — the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM.',
url: DOCS_BASE_URL,
publisher: {
'@type': 'Organization',
name: 'Sim',
url: 'https://sim.ai',
logo: {
'@type': 'ImageObject',
url: `${DOCS_BASE_URL}/static/logo.png`,
},
},
inLanguage: lang,
}
return (
<html
lang={lang}
className={`${inter.variable} ${geistMono.variable} ${season.variable}`}
suppressHydrationWarning
>
<head>
<script
type='application/ld+json'
dangerouslySetInnerHTML={{ __html: serializeJsonLd(structuredData) }}
/>
</head>
<body className='flex min-h-screen flex-col font-sans'>
<RootProvider i18n={provider(lang)}>
<Navbar />
<DocsLayout
tree={source.pageTree[lang]}
nav={{
title: <SimWordmark className='h-[18px]' />,
}}
sidebar={{
tabs: false,
defaultOpenLevel: 0,
collapsible: false,
footer: null,
banner: null,
components: {
Item: SidebarItem,
Folder: SidebarFolder,
Separator: SidebarSeparator,
},
}}
containerProps={{
className: '!pt-0',
}}
>
{children}
</DocsLayout>
<Footer />
<AskAI locale={lang} />
</RootProvider>
</body>
</html>
)
}
+25
View File
@@ -0,0 +1,25 @@
import { ChipLink } from '@sim/emcn'
import { DocsPage } from 'fumadocs-ui/page'
export const metadata = {
title: 'Page Not Found',
}
export default function NotFound() {
return (
<DocsPage>
<div className='flex min-h-[70vh] flex-col items-center justify-center gap-4 text-center'>
<h1 className='bg-gradient-to-b from-[var(--brand-accent)] to-[var(--brand-accent-hover)] bg-clip-text font-semibold text-8xl text-transparent'>
404
</h1>
<h2 className='font-semibold text-2xl text-[var(--text-primary)]'>Page Not Found</h2>
<p className='text-[var(--text-muted)]'>
The page you're looking for doesn't exist or has been moved.
</p>
<ChipLink href='/' variant='primary'>
Go home
</ChipLink>
</div>
</DocsPage>
)
}
+344
View File
@@ -0,0 +1,344 @@
import { openai } from '@ai-sdk/openai'
import { convertToModelMessages, stepCountIs, streamText, tool, type UIMessage } from 'ai'
import { sql } from 'drizzle-orm'
import { z } from 'zod'
import { db, docsEmbeddings } from '@/lib/db'
import { generateSearchEmbedding } from '@/lib/embeddings'
export const runtime = 'nodejs'
export const maxDuration = 30
/** Model used for the Ask AI chat. Override with OPENAI_CHAT_MODEL in the environment. */
const CHAT_MODEL = process.env.OPENAI_CHAT_MODEL || 'gpt-5.4-mini'
/** Max documentation chunks returned per search to ground an answer. */
const SEARCH_LIMIT = 6
/** Candidates pulled before locale filtering, so a locale still yields SEARCH_LIMIT results. */
const SEARCH_CANDIDATES = SEARCH_LIMIT * 4
/** Minimum cosine similarity for an English vector match (mirrors the site search route). */
const SIMILARITY_THRESHOLD = 0.6
/** Locales the docs are published in (mirrors the site search route). */
const KNOWN_LOCALES = ['en', 'es', 'fr', 'de', 'ja', 'zh']
const DEFAULT_LOCALE = 'en'
/** Postgres full-text config per locale (mirrors the site search route). */
const TS_CONFIG: Record<string, string> = {
en: 'english',
es: 'spanish',
fr: 'french',
de: 'german',
ja: 'simple',
zh: 'simple',
}
/**
* Abuse guards. This endpoint proxies a paid LLM, so an unauthenticated public
* route is a target for scripted "free inference". These bounds cap the cost of
* any single request; an in-memory per-IP rate limit (below) caps volume on the
* hot path. A shared-store rate limit, a provider spend cap, and edge bot
* protection remain the durable controls (see the PR checklist).
*
* The size cap counts only user-authored text — NOT the conversation history,
* assistant turns, or retrieved doc chunks we add via the searchDocs tool, which
* legitimately grow large over a multi-turn chat.
*/
const MAX_MESSAGES = 200
const MAX_USER_INPUT_CHARS = 400_000
const MAX_OUTPUT_TOKENS = 4000
const MAX_STEPS = 6
/** Backstop on the sanitized model payload — bounds total LLM input (e.g. stuffed assistant text). */
const MAX_TOTAL_CHARS = 1_000_000
/**
* Per-IP rate limit. Fixed window, in-memory: this bounds volume from a single
* source on a warm instance without external infra. It is best-effort on
* serverless (state is per-instance, not shared across regions/cold starts);
* a shared store (e.g. Vercel KV) and an edge WAF remain the durable controls,
* but this closes the "no volume limit at all" gap on the hot path.
*/
const RATE_LIMIT_MAX = 20
const RATE_LIMIT_WINDOW_MS = 60_000
const rateLimitHits = new Map<string, { count: number; resetAt: number }>()
/** Resolve the client IP from forwarding headers, falling back to a shared bucket. */
function getClientIp(req: Request): string {
const forwarded = req.headers.get('x-forwarded-for')
if (forwarded) return forwarded.split(',')[0].trim()
return req.headers.get('x-real-ip') ?? 'unknown'
}
/** Fixed-window check. Returns retry-after seconds when the caller is over the limit, else null. */
function rateLimit(ip: string, now: number): number | null {
const entry = rateLimitHits.get(ip)
if (!entry || now >= entry.resetAt) {
rateLimitHits.set(ip, { count: 1, resetAt: now + RATE_LIMIT_WINDOW_MS })
return null
}
if (entry.count >= RATE_LIMIT_MAX) {
return Math.ceil((entry.resetAt - now) / 1000)
}
entry.count += 1
return null
}
/** Drop expired buckets so the Map doesn't grow unbounded on a long-lived instance. */
function sweepRateLimit(now: number): void {
if (rateLimitHits.size < 10_000) return
for (const [ip, entry] of rateLimitHits) {
if (now >= entry.resetAt) rateLimitHits.delete(ip)
}
}
/** A structurally valid UI message: has a role and a parts array. */
function isValidMessage(message: unknown): message is UIMessage {
return (
typeof message === 'object' &&
message !== null &&
typeof (message as { role?: unknown }).role === 'string' &&
Array.isArray((message as { parts?: unknown }).parts)
)
}
/** Total length of user-authored text across the conversation. */
function userInputChars(messages: UIMessage[]): number {
let total = 0
for (const message of messages) {
if (message.role !== 'user') continue
for (const part of message.parts) {
if (part.type === 'text' && typeof part.text === 'string') total += part.text.length
}
}
return total
}
/**
* Strip everything the model shouldn't trust from client-supplied history:
* drop `system` messages (client-injected instructions) and every non-text part
* (e.g. crafted tool results faking searchDocs output). Only user/assistant text
* survives, so grounding comes from the server-run searchDocs tool — not the
* client's payload.
*/
function sanitizeMessages(messages: UIMessage[]): UIMessage[] {
return messages
.filter((message) => message.role === 'user' || message.role === 'assistant')
.map((message) => ({
...message,
parts: message.parts.filter((part) => part.type === 'text' && typeof part.text === 'string'),
}))
.filter((message) => message.parts.length > 0)
}
/**
* Reject obvious cross-origin calls. Same-origin browser requests send an
* `Origin` header matching the host; we allow those, plus any host in
* DOCS_ALLOWED_ORIGINS (comma-separated). Requests with no Origin (e.g. curl)
* are allowed through to the cost caps rather than blocked, since Origin is
* trivially spoofable and is a filter, not a security boundary.
*/
function isAllowedOrigin(req: Request): boolean {
const origin = req.headers.get('origin')
if (!origin) return true
let originHost: string
try {
originHost = new URL(origin).host.toLowerCase()
} catch {
return false
}
const forwardedHost = req.headers.get('x-forwarded-host') ?? req.headers.get('host')
const requestHost = forwardedHost?.split(',')[0].trim().toLowerCase()
if (requestHost && originHost === requestHost) return true
const allowlist = (process.env.DOCS_ALLOWED_ORIGINS ?? '')
.split(',')
.map((value) => value.trim().toLowerCase())
.filter(Boolean)
return allowlist.includes(originHost)
}
const SYSTEM_PROMPT = `You are the documentation assistant for Sim — the open-source AI workspace where teams build, deploy, and manage AI agents.
Answer questions about Sim using the documentation. Always call the searchDocs tool before answering anything specific about Sim's features, configuration, or usage — do not answer from memory. Base your answer only on the returned documentation; if the docs do not cover the question, say so plainly rather than guessing.
Guidelines:
- Be direct and concrete. Lead with the answer, then the detail.
- Reference the relevant pages by their titles so the user knows where to read more.
- When you show configuration or code, keep it minimal and correct.
- The agent is called "Sim" and the chat surface is "Chat" — never say "Mothership" or "copilot".
- If a question is unrelated to Sim, briefly say it's outside the docs' scope.`
const SEARCH_COLUMNS = {
chunkId: docsEmbeddings.chunkId,
title: docsEmbeddings.headerText,
url: docsEmbeddings.sourceLink,
content: docsEmbeddings.chunkText,
sourceDocument: docsEmbeddings.sourceDocument,
}
/** Reciprocal-rank-fusion constant, matching the site search route. */
const RRF_K = 60
/**
* SQL predicate selecting only the locale's documents, so the row limit applies
* to matching rows: non-English docs are prefixed with their locale segment;
* English is everything not prefixed with another locale.
*/
function localeFilter(locale: string) {
const firstSegment = sql`split_part(${docsEmbeddings.sourceDocument}, '/', 1)`
if (locale === DEFAULT_LOCALE) {
const others = KNOWN_LOCALES.filter((l) => l !== DEFAULT_LOCALE)
return sql`${firstSegment} not in (${sql.join(
others.map((l) => sql`${l}`),
sql`, `
)})`
}
return sql`${firstSegment} = ${locale}`
}
type SearchRow = {
chunkId: string
title: string
url: string
content: string
sourceDocument: string
}
/**
* Retrieve candidate chunks for grounding, mirroring the site search route's
* hybrid strategy: Postgres full-text keyword search for every locale, plus
* vector similarity (thresholded) for English — fused by reciprocal rank so a
* page found by either signal can ground the answer.
*/
async function searchDocs(query: string, locale: string) {
const tsConfig = TS_CONFIG[locale] ?? 'simple'
// Each retrieval path is best-effort and independent: a failure in one still
// lets the other ground the answer (both empty just yields no grounding).
let keywordRows: SearchRow[] = []
try {
keywordRows = await db
.select(SEARCH_COLUMNS)
.from(docsEmbeddings)
.where(
sql`${docsEmbeddings.chunkTextTsv} @@ plainto_tsquery(${tsConfig}, ${query}) and ${localeFilter(locale)}`
)
.orderBy(
sql`ts_rank(${docsEmbeddings.chunkTextTsv}, plainto_tsquery(${tsConfig}, ${query})) DESC`
)
.limit(SEARCH_CANDIDATES)
} catch (error) {
console.error('Ask AI keyword search failed:', error)
}
let vectorRows: SearchRow[] = []
if (locale === DEFAULT_LOCALE) {
// Vector retrieval (embedding call + pgvector query) is best-effort: if it
// fails, fall back to the keyword rows already fetched rather than losing all
// grounding for the turn.
try {
const embedding = await generateSearchEmbedding(query)
const vectorLiteral = JSON.stringify(embedding)
vectorRows = await db
.select(SEARCH_COLUMNS)
.from(docsEmbeddings)
.where(
sql`1 - (${docsEmbeddings.embedding} <=> ${vectorLiteral}::vector) >= ${SIMILARITY_THRESHOLD} and ${localeFilter(locale)}`
)
.orderBy(sql`${docsEmbeddings.embedding} <=> ${vectorLiteral}::vector`)
.limit(SEARCH_CANDIDATES)
} catch (error) {
console.error('Ask AI vector search failed; using keyword results only:', error)
}
}
// Reciprocal rank fusion across the two rankings, deduped by chunk.
const scores = new Map<string, number>()
const rowById = new Map<string, SearchRow>()
for (const list of [vectorRows, keywordRows]) {
list.forEach((row, index) => {
scores.set(row.chunkId, (scores.get(row.chunkId) ?? 0) + 1 / (RRF_K + index + 1))
if (!rowById.has(row.chunkId)) rowById.set(row.chunkId, row)
})
}
return [...rowById.values()]
.sort((a, b) => (scores.get(b.chunkId) ?? 0) - (scores.get(a.chunkId) ?? 0))
.slice(0, SEARCH_LIMIT)
.map((row) => ({
title: row.title,
url: row.url,
content: row.content,
}))
}
export async function POST(req: Request) {
if (!isAllowedOrigin(req)) {
return new Response('Forbidden', { status: 403 })
}
const now = Date.now()
sweepRateLimit(now)
const retryAfter = rateLimit(getClientIp(req), now)
if (retryAfter !== null) {
return new Response('Too many requests', {
status: 429,
headers: { 'Retry-After': String(retryAfter) },
})
}
let body: { messages: UIMessage[]; locale?: string }
try {
body = await req.json()
} catch {
return new Response('Invalid JSON', { status: 400 })
}
const { messages } = body
const locale = KNOWN_LOCALES.includes(body.locale ?? '')
? (body.locale as string)
: DEFAULT_LOCALE
if (!Array.isArray(messages) || messages.length === 0 || messages.length > MAX_MESSAGES) {
return new Response('Invalid request', { status: 400 })
}
if (!messages.every(isValidMessage)) {
return new Response('Invalid request', { status: 400 })
}
if (userInputChars(messages) > MAX_USER_INPUT_CHARS) {
return new Response('Request too large', { status: 413 })
}
const modelMessages = sanitizeMessages(messages)
if (modelMessages.length === 0) {
return new Response('Invalid request', { status: 400 })
}
// Bound what actually reaches the model. Measured AFTER sanitization, so the
// prior searchDocs tool outputs that accumulate in client history (and are
// stripped here) don't count — only user/assistant text the model will see.
if (JSON.stringify(modelMessages).length > MAX_TOTAL_CHARS) {
return new Response('Request too large', { status: 413 })
}
const result = streamText({
model: openai(CHAT_MODEL),
system: SYSTEM_PROMPT,
messages: convertToModelMessages(modelMessages),
stopWhen: stepCountIs(MAX_STEPS),
maxOutputTokens: MAX_OUTPUT_TOKENS,
tools: {
searchDocs: tool({
description:
'Search the Sim documentation for relevant content. Use this before answering any question about Sim.',
inputSchema: z.object({
query: z.string().describe('A focused natural-language search query.'),
}),
execute: async ({ query }) => searchDocs(query, locale),
}),
},
})
return result.toUIMessageStreamResponse()
}
+232
View File
@@ -0,0 +1,232 @@
import type { CSSProperties } from 'react'
import { ImageResponse } from 'next/og'
import type { NextRequest } from 'next/server'
export const runtime = 'edge'
const TITLE_FONT_SIZE = {
large: 110,
medium: 96,
small: 85,
} as const
/** Average glyph width as a fraction of font size, for this weight/family — used to pack words into lines. */
const LATIN_CHAR_WIDTH_EM = 0.42
/** CJK glyphs (docs ships `ja`/`zh` locales) render near-square, roughly 2.4x a Latin glyph at this weight. */
const CJK_CHAR_WIDTH_EM = 1
const CJK_RANGE = /[\u3000-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uff00-\uffef]/
const TITLE_BOX_WIDTH = 1020
const FONT_CACHE_REVALIDATE_SECONDS = 60 * 60 * 24 * 30
/** Exact hex from a vector trace of the reference cover template, not an estimate off compressed JPEG pixels. */
const INK_COLOR = '#515151'
const OG_CONTAINER_STYLE = {
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
padding: '26px',
background: '#c1c1c1',
fontFamily: 'Soehne',
} satisfies CSSProperties
const OG_HEADER_STYLE = {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-start',
width: '100%',
} satisfies CSSProperties
const OG_TITLE_STYLE = {
display: 'flex',
flexDirection: 'column',
fontWeight: 500,
color: INK_COLOR,
lineHeight: 1.1,
width: `${TITLE_BOX_WIDTH}px`,
/** Compensates for Satori adding extra invisible leading below the last line instead of splitting it evenly. */
transform: 'translateY(14px)',
} satisfies CSSProperties
function getTitleFontSize(title: string): number {
if (title.length > 45) return TITLE_FONT_SIZE.small
if (title.length > 30) return TITLE_FONT_SIZE.medium
return TITLE_FONT_SIZE.large
}
function getTitleStyle(title: string): CSSProperties {
return {
...OG_TITLE_STYLE,
fontSize: getTitleFontSize(title),
}
}
/** Sums per-character em-widths rather than counting characters, so wide CJK glyphs (docs ships `ja`/`zh`) don't under-wrap. */
function estimateWidthEm(text: string): number {
let width = 0
for (const char of text) {
width += CJK_RANGE.test(char) ? CJK_CHAR_WIDTH_EM : LATIN_CHAR_WIDTH_EM
}
return width
}
/**
* Splits a single word wider than `maxWidthEm` into character-level chunks
* that each fit. CJK titles (docs ships `ja`/`zh` locales) are often
* space-free, so a whole run can arrive as one "word" from `wrapTitleLines`'
* space-based split. Breaking mid-word is correct for CJK, where each glyph
* is independently readable; Latin words never reach this path since they
* stay under `maxWidthEm` in practice.
*/
function splitOversizedWord(word: string, maxWidthEm: number): string[] {
const chunks: string[] = []
let chunk = ''
for (const char of word) {
const candidate = chunk + char
if (estimateWidthEm(candidate) > maxWidthEm && chunk) {
chunks.push(chunk)
chunk = char
} else {
chunk = candidate
}
}
if (chunk) chunks.push(chunk)
return chunks
}
/**
* Greedily packs words into lines that fit `TITLE_BOX_WIDTH` at `fontSize`,
* then joins each line with U+00A0 instead of a plain space. Satori
* (`next/og`'s renderer) has a text-measurement bug where the first plain
* space (U+0020) in a text node renders at roughly double width — a
* non-breaking space measures correctly and reads identically at this size,
* so it sidesteps the bug instead of fighting Satori's own line-wrapping
* (which is also disabled here — lines are pre-split, not auto-wrapped).
*/
function wrapTitleLines(title: string, fontSize: number): string[] {
const maxWidthEm = TITLE_BOX_WIDTH / fontSize
const words = title.split(' ')
const lines: string[] = []
let current = ''
for (const word of words) {
if (estimateWidthEm(word) > maxWidthEm) {
if (current) {
lines.push(current)
current = ''
}
const chunks = splitOversizedWord(word, maxWidthEm)
lines.push(...chunks.slice(0, -1))
current = chunks[chunks.length - 1] ?? ''
continue
}
const candidate = current ? `${current} ${word}` : word
if (estimateWidthEm(candidate) > maxWidthEm && current) {
lines.push(current)
current = word
} else {
current = candidate
}
}
if (current) lines.push(current)
return lines.map((line) => line.replace(/ /g, ' '))
}
/**
* Loads Söhne Kräftig (weight 500), the typeface used on the reference cover
* template this OG image matches. Converted to a plain TTF from the
* last-shipped `soehne-kraftig.woff2` since Satori (`next/og`'s renderer)
* can't parse WOFF2 or variable fonts. Fetched over HTTP since the edge
* runtime has no filesystem access — served from `/static/fonts/` (not
* `/fonts/`) so it isn't intercepted by the site's i18n proxy (`proxy.ts`),
* whose matcher excludes `static` but not `fonts`.
*/
async function loadTitleFont(baseUrl: string): Promise<ArrayBuffer> {
const response = await fetch(new URL('/static/fonts/Soehne-Kraftig.ttf', baseUrl), {
next: { revalidate: FONT_CACHE_REVALIDATE_SECONDS },
})
if (!response.ok) {
throw new Error(`Failed to load font data: ${response.status} ${response.statusText}`)
}
return await response.arrayBuffer()
}
/** "sim" wordmark, no icon — same brandbook workmark geometry as the docs navbar/landing OG cards. */
function SimWordmark() {
return (
<svg width='118' height='57' viewBox='0 0 800 386' fill='none'>
<path
d='M0 293.75h53.4128c0 14.748 5.3413 26.506 16.0239 35.275 10.6826 8.37 25.1238 12.555 43.3233 12.555 19.783 0 35.016-3.786 45.698-11.36 10.683-7.971 16.024-18.534 16.024-31.687 0-9.566-2.967-17.538-8.902-23.915-5.539-6.378-15.826-11.559-30.861-15.545l-51.0389-11.958c-25.7173-6.377-44.9063-16.142-57.5672-29.296-12.2651-13.153-18.39771-30.491-18.39771-52.015 0-17.936 4.55001-33.481 13.64991-46.635 9.4957-13.153 22.3543-23.3169 38.576-30.4914 16.6173-7.1745 35.6086-10.7619 56.9739-10.7619 21.365 0 39.763 3.7866 55.193 11.3598 15.826 7.5731 28.091 18.1355 36.796 31.6875 9.1 13.552 13.847 29.695 14.243 48.428h-53.413c-.395-15.146-5.341-26.904-14.837-35.275-9.495-8.37-22.75-12.555-39.763-12.555-17.4083 0-30.8604 3.786-40.356 11.36-9.4956 7.573-14.2434 17.936-14.2434 31.089 0 19.531 14.2434 32.884 42.7304 40.058l51.039 12.556c24.53 5.58 42.928 14.747 55.193 27.502 12.265 12.356 18.398 29.296 18.398 50.82 0 18.335-4.946 34.477-14.837 48.428-9.891 13.552-23.541 24.114-40.95 31.687-17.013 7.175-37.191 10.762-60.534 10.762-34.0265 0-61.1285-8.37-81.3067-25.111-20.1782-16.74-30.2673-39.061-30.2673-66.962z'
fill={INK_COLOR}
/>
<path
d='m267.175 385.826v-292.3631c22.244 8.1331 32.053 8.1331 55.787 0v292.3631zm27.3-311.6891c-9.891 0-18.596-3.5872-26.113-10.7618-7.122-7.5731-10.683-16.342-10.683-26.3067 0-10.3632 3.561-19.132 10.683-26.3066 7.517-7.17453 16.222-10.7618 26.113-10.7618 10.287 0 18.991 3.58727 26.113 10.7618 7.122 7.1746 10.682 15.9434 10.682 26.3066 0 9.9647-3.56 18.7336-10.682 26.3067-7.122 7.1746-15.826 10.7618-26.113 10.7618z'
fill={INK_COLOR}
/>
<path
d='m421.362 385.823h-55.786v-292.3624h49.852v49.3294c5.934-16.342 17.408-30.197 33.234-40.959 16.222-11.1605 35.807-16.7407 58.754-16.7407 25.718 0 47.083 6.9752 64.096 20.9257 17.013 13.951 28.091 32.485 33.234 55.603h-10.089c3.957-23.118 14.837-41.652 32.642-55.603 17.804-13.9505 39.762-20.9257 65.875-20.9257 33.235 0 59.348 9.7653 78.339 29.2957 18.991 19.531 28.487 46.236 28.487 80.116v191.321h-54.6v-177.57c0-23.118-5.934-40.855-17.804-53.211-11.474-12.755-27.102-19.132-46.885-19.132-13.847 0-26.113 3.189-36.795 9.566-10.287 5.979-18.398 14.748-24.333 26.307-5.934 11.559-8.902 25.111-8.902 40.655v173.385h-55.193v-178.168c0-23.118-5.737-40.655-17.211-52.613-11.474-12.356-27.102-18.534-46.885-18.534-13.847 0-26.112 3.189-36.795 9.566-10.287 5.979-18.398 14.748-24.333 26.307-5.934 11.16-8.902 24.513-8.902 40.057z'
fill={INK_COLOR}
/>
</svg>
)
}
/** Diagonal "open" arrow, top-right — square caps and a miter join to match the reference's sharp corners. */
function CornerArrow() {
return (
<svg width='58' height='58' viewBox='0 0 24 24' fill='none'>
<path
d='M2 22 22 2M22 2H12M22 2V12'
stroke={INK_COLOR}
strokeWidth={3.6}
strokeLinecap='square'
strokeLinejoin='miter'
/>
</svg>
)
}
/**
* Generates dynamic Open Graph images for documentation pages. Matches the
* site's library/blog cover template: light gray background, "sim" wordmark
* top-left, an open/diagonal arrow top-right, and the page title large and
* bold at the bottom-left.
*/
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const title = searchParams.get('title') || 'Documentation'
const fontData = await loadTitleFont(request.url)
const fontSize = getTitleFontSize(title)
const titleLines = wrapTitleLines(title, fontSize)
return new ImageResponse(
<div style={OG_CONTAINER_STYLE}>
<div style={OG_HEADER_STYLE}>
<SimWordmark />
<CornerArrow />
</div>
<div style={getTitleStyle(title)}>
{titleLines.map((line, index) => (
<span key={index}>{line}</span>
))}
</div>
</div>,
{
width: 1200,
height: 675,
fonts: [
{
name: 'Soehne',
data: fontData,
style: 'normal',
weight: 500,
},
],
}
)
}
+239
View File
@@ -0,0 +1,239 @@
import { sql } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { db, docsEmbeddings } from '@/lib/db'
import { generateSearchEmbedding } from '@/lib/embeddings'
export const runtime = 'nodejs'
export const revalidate = 0
const DEFAULT_SEARCH_LIMIT = 10
const MAX_SEARCH_LIMIT = 20
function getSearchLimit(value: unknown): number {
const limit = Number.parseInt(String(value ?? DEFAULT_SEARCH_LIMIT), 10)
if (!Number.isFinite(limit) || limit <= 0) {
return DEFAULT_SEARCH_LIMIT
}
return Math.min(limit, MAX_SEARCH_LIMIT)
}
function getSearchParams(request: NextRequest) {
const searchParams = request.nextUrl.searchParams
return {
query: searchParams.get('query') || searchParams.get('q') || '',
locale: searchParams.get('locale') || 'en',
limit: getSearchLimit(searchParams.get('limit')),
}
}
/**
* Hybrid search API endpoint
* - English: Vector embeddings + keyword search
* - Other languages: Keyword search only
*/
export async function GET(request: NextRequest) {
try {
const { query, locale, limit } = getSearchParams(request)
if (!query || query.trim().length === 0) {
return NextResponse.json([])
}
const candidateLimit = limit * 3
const similarityThreshold = 0.6
const localeMap: Record<string, string> = {
en: 'english',
es: 'spanish',
fr: 'french',
de: 'german',
ja: 'simple', // PostgreSQL doesn't have Japanese support, use simple
zh: 'simple', // PostgreSQL doesn't have Chinese support, use simple
}
const tsConfig = localeMap[locale] || 'simple'
const useVectorSearch = locale === 'en'
let vectorResults: Array<{
chunkId: string
chunkText: string
sourceDocument: string
sourceLink: string
headerText: string
headerLevel: number
similarity: number
searchType: string
}> = []
if (useVectorSearch) {
const queryEmbedding = await generateSearchEmbedding(query)
vectorResults = await db
.select({
chunkId: docsEmbeddings.chunkId,
chunkText: docsEmbeddings.chunkText,
sourceDocument: docsEmbeddings.sourceDocument,
sourceLink: docsEmbeddings.sourceLink,
headerText: docsEmbeddings.headerText,
headerLevel: docsEmbeddings.headerLevel,
similarity: sql<number>`1 - (${docsEmbeddings.embedding} <=> ${JSON.stringify(queryEmbedding)}::vector)`,
searchType: sql<string>`'vector'`,
})
.from(docsEmbeddings)
.where(
sql`1 - (${docsEmbeddings.embedding} <=> ${JSON.stringify(queryEmbedding)}::vector) >= ${similarityThreshold}`
)
.orderBy(sql`${docsEmbeddings.embedding} <=> ${JSON.stringify(queryEmbedding)}::vector`)
.limit(candidateLimit)
}
const keywordResults = await db
.select({
chunkId: docsEmbeddings.chunkId,
chunkText: docsEmbeddings.chunkText,
sourceDocument: docsEmbeddings.sourceDocument,
sourceLink: docsEmbeddings.sourceLink,
headerText: docsEmbeddings.headerText,
headerLevel: docsEmbeddings.headerLevel,
similarity: sql<number>`ts_rank(${docsEmbeddings.chunkTextTsv}, plainto_tsquery(${tsConfig}, ${query}))`,
searchType: sql<string>`'keyword'`,
})
.from(docsEmbeddings)
.where(sql`${docsEmbeddings.chunkTextTsv} @@ plainto_tsquery(${tsConfig}, ${query})`)
.orderBy(
sql`ts_rank(${docsEmbeddings.chunkTextTsv}, plainto_tsquery(${tsConfig}, ${query})) DESC`
)
.limit(candidateLimit)
const knownLocales = ['en', 'es', 'fr', 'de', 'ja', 'zh']
const vectorRankMap = new Map<string, number>()
vectorResults.forEach((r, idx) => vectorRankMap.set(r.chunkId, idx + 1))
const keywordRankMap = new Map<string, number>()
keywordResults.forEach((r, idx) => keywordRankMap.set(r.chunkId, idx + 1))
const resultByChunkId = new Map<string, (typeof vectorResults)[number]>()
keywordResults.forEach((result) => resultByChunkId.set(result.chunkId, result))
vectorResults.forEach((result) => resultByChunkId.set(result.chunkId, result))
const allChunkIds = new Set([
...vectorResults.map((r) => r.chunkId),
...keywordResults.map((r) => r.chunkId),
])
const k = 60
type ResultWithRRF = (typeof vectorResults)[0] & { rrfScore: number }
const scoredResults: ResultWithRRF[] = []
for (const chunkId of allChunkIds) {
const vectorRank = vectorRankMap.get(chunkId) ?? Number.POSITIVE_INFINITY
const keywordRank = keywordRankMap.get(chunkId) ?? Number.POSITIVE_INFINITY
const rrfScore = 1 / (k + vectorRank) + 1 / (k + keywordRank)
const result = resultByChunkId.get(chunkId)
if (result) {
scoredResults.push({ ...result, rrfScore })
}
}
scoredResults.sort((a, b) => b.rrfScore - a.rrfScore)
const localeFilteredResults = scoredResults.filter((result) => {
const firstPart = result.sourceDocument.split('/')[0]
if (knownLocales.includes(firstPart)) {
return firstPart === locale
}
return locale === 'en'
})
const queryLower = query.toLowerCase()
const getTitleBoost = (result: ResultWithRRF): number => {
const fileName = result.sourceDocument
.replace('.mdx', '')
.split('/')
.pop()
?.toLowerCase()
?.replace(/_/g, ' ')
if (fileName === queryLower) return 0.01
if (fileName?.includes(queryLower)) return 0.005
return 0
}
localeFilteredResults.sort((a, b) => {
return b.rrfScore + getTitleBoost(b) - (a.rrfScore + getTitleBoost(a))
})
const pageMap = new Map<string, ResultWithRRF>()
for (const result of localeFilteredResults) {
const pageKey = result.sourceDocument
const existing = pageMap.get(pageKey)
if (!existing || result.rrfScore > existing.rrfScore) {
pageMap.set(pageKey, result)
}
}
const deduplicatedResults = Array.from(pageMap.values())
.sort((a, b) => b.rrfScore + getTitleBoost(b) - (a.rrfScore + getTitleBoost(a)))
.slice(0, limit)
const searchResults = deduplicatedResults.map((result) => {
const title = result.headerText || result.sourceDocument.replace('.mdx', '')
const pathParts = result.sourceDocument
.replace('.mdx', '')
.split('/')
.reduce<string[]>((parts, part) => {
if (part === 'index' || knownLocales.includes(part)) {
return parts
}
parts.push(
part
.replace(/_/g, ' ')
.split(' ')
.map((word) => {
const acronyms = [
'api',
'mcp',
'sdk',
'url',
'http',
'json',
'xml',
'html',
'css',
'ai',
]
if (acronyms.includes(word.toLowerCase())) {
return word.toUpperCase()
}
return word.charAt(0).toUpperCase() + word.slice(1)
})
.join(' ')
)
return parts
}, [])
return {
id: result.chunkId,
type: 'page' as const,
url: result.sourceLink,
content: title,
breadcrumbs: pathParts,
}
})
return NextResponse.json(searchResults)
} catch (error) {
console.error('Semantic search error:', error)
return NextResponse.json([])
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.
+16
View File
@@ -0,0 +1,16 @@
import localFont from 'next/font/local'
/**
* Season Sans variable font — the platform's UI font, mirrored from
* `apps/sim/app/_styles/fonts/season/season.ts` so docs chip chrome renders
* with the same typeface as the main app. Variable font supports weights
* 300-800.
*/
export const season = localFont({
src: [{ path: './SeasonSansUprightsVF.woff2', weight: '300 800', style: 'normal' }],
display: 'swap',
preload: true,
variable: '--font-season',
fallback: ['system-ui', 'Segoe UI', 'Roboto', 'Helvetica Neue', 'Arial', 'Noto Sans'],
adjustFontFallback: 'Arial',
})
File diff suppressed because it is too large Load Diff
+112
View File
@@ -0,0 +1,112 @@
import type { ReactNode } from 'react'
import type { Viewport } from 'next'
import { DOCS_BASE_URL } from '@/lib/urls'
export default function RootLayout({ children }: { children: ReactNode }) {
return children
}
export const viewport: Viewport = {
width: 'device-width',
initialScale: 1,
themeColor: '#000000',
}
export const metadata = {
metadataBase: new URL(DOCS_BASE_URL),
title: {
default: 'Sim Documentation — The AI Workspace for Teams',
template: '%s | Sim Docs',
},
description:
'Documentation for Sim — the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM.',
applicationName: 'Sim Docs',
generator: 'Next.js',
referrer: 'origin-when-cross-origin' as const,
keywords: [
'AI workspace',
'AI agent builder',
'AI agents',
'build AI agents',
'open-source AI agents',
'LLM orchestration',
'AI integrations',
'knowledge base',
'AI automation',
'visual workflow builder',
'enterprise AI',
'AI agent deployment',
'AI tools',
],
authors: [{ name: 'Sim Team', url: 'https://sim.ai' }],
creator: 'Sim',
publisher: 'Sim',
category: 'Developer Tools',
classification: 'Developer Documentation',
manifest: '/favicon/site.webmanifest',
icons: {
icon: [{ url: '/icon.svg', type: 'image/svg+xml', sizes: 'any' }],
apple: '/favicon/apple-touch-icon.png',
},
appleWebApp: {
capable: true,
statusBarStyle: 'default',
title: 'Sim Docs',
},
formatDetection: {
telephone: false,
},
other: {
'msapplication-TileColor': '#000000',
},
openGraph: {
type: 'website',
locale: 'en_US',
alternateLocale: ['es_ES', 'fr_FR', 'de_DE', 'ja_JP', 'zh_CN'],
url: DOCS_BASE_URL,
siteName: 'Sim Documentation',
title: 'Sim Documentation — The AI Workspace for Teams',
description:
'Documentation for Sim — the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM.',
images: [
{
url: `${DOCS_BASE_URL}/api/og?title=Sim%20Documentation`,
width: 1200,
height: 675,
alt: 'Sim Documentation',
},
],
},
twitter: {
card: 'summary_large_image',
title: 'Sim Documentation — The AI Workspace for Teams',
description:
'Documentation for Sim — the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM.',
creator: '@simdotai',
site: '@simdotai',
images: [`${DOCS_BASE_URL}/api/og?title=Sim%20Documentation`],
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
'max-video-preview': -1,
'max-image-preview': 'large',
'max-snippet': -1,
},
},
alternates: {
canonical: DOCS_BASE_URL,
languages: {
'x-default': DOCS_BASE_URL,
en: DOCS_BASE_URL,
es: `${DOCS_BASE_URL}/es`,
fr: `${DOCS_BASE_URL}/fr`,
de: `${DOCS_BASE_URL}/de`,
ja: `${DOCS_BASE_URL}/ja`,
zh: `${DOCS_BASE_URL}/zh`,
},
},
}
+31
View File
@@ -0,0 +1,31 @@
import { getLLMText } from '@/lib/llms'
import { source } from '@/lib/source'
export const revalidate = false
export async function GET() {
try {
const pages = source.getPages().filter((page) => {
if (!page || !page.data || !page.url) return false
const pathParts = page.url.split('/').filter(Boolean)
const hasLangPrefix = pathParts[0] && ['es', 'fr', 'de', 'ja', 'zh'].includes(pathParts[0])
return !hasLangPrefix
})
const scan = pages.map((page) => getLLMText(page))
const scanned = await Promise.all(scan)
const filtered = scanned.filter((text) => text && text.length > 0)
return new Response(filtered.join('\n\n---\n\n'), {
headers: {
'Content-Type': 'text/plain; charset=utf-8',
},
})
} catch (error) {
console.error('Error generating LLM full text:', error)
return new Response('Error generating full documentation text', { status: 500 })
}
}
@@ -0,0 +1,35 @@
import { notFound } from 'next/navigation'
import { type NextRequest, NextResponse } from 'next/server'
import { i18n } from '@/lib/i18n'
import { getLLMText } from '@/lib/llms'
import { source } from '@/lib/source'
export const revalidate = false
export async function GET(
_request: NextRequest,
{ params }: { params: Promise<{ slug?: string[] }> }
) {
const { slug } = await params
let lang: (typeof i18n.languages)[number] = i18n.defaultLanguage
let pageSlug = slug
if (slug && slug.length > 0 && i18n.languages.includes(slug[0] as typeof lang)) {
lang = slug[0] as typeof lang
pageSlug = slug.slice(1)
}
const page = source.getPage(pageSlug, lang)
if (!page) notFound()
return new NextResponse(await getLLMText(page), {
headers: {
'Content-Type': 'text/markdown',
},
})
}
export function generateStaticParams() {
return source.generateParams()
}
+88
View File
@@ -0,0 +1,88 @@
import { source } from '@/lib/source'
import { DOCS_BASE_URL } from '@/lib/urls'
export const revalidate = false
export async function GET() {
const baseUrl = DOCS_BASE_URL
try {
const pages = source.getPages().filter((page) => {
if (!page || !page.data || !page.url) return false
const pathParts = page.url.split('/').filter(Boolean)
const hasLangPrefix = pathParts[0] && ['es', 'fr', 'de', 'ja', 'zh'].includes(pathParts[0])
return !hasLangPrefix
})
const sections: Record<string, Array<{ title: string; url: string; description?: string }>> = {}
pages.forEach((page) => {
const pathParts = page.url.split('/').filter(Boolean)
const section =
pathParts[0] && ['en', 'es', 'fr', 'de', 'ja', 'zh'].includes(pathParts[0])
? pathParts[1] || 'root'
: pathParts[0] || 'root'
if (!sections[section]) {
sections[section] = []
}
sections[section].push({
title: page.data.title || 'Untitled',
url: `${baseUrl}${page.url}`,
description: page.data.description,
})
})
const manifest = `# Sim Documentation
> The open-source AI workspace where teams build, deploy, and manage AI agents.
Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM to create agents that automate real work — visually, conversationally, or with code. Trusted by over 100,000 builders.
## Documentation Overview
This file provides an overview of our documentation. For full content of all pages, see [llms-full.txt](${baseUrl}/llms-full.txt).
## Main Sections
${Object.entries(sections)
.map(([section, items]) => {
const sectionTitle = section
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ')
return `### ${sectionTitle}\n\n${items.map((item) => `- [${item.title}](${item.url})${item.description ? `\n ${item.description}` : ''}`).join('\n')}`
})
.join('\n\n')}
## Additional Resources
- [Full documentation content](${baseUrl}/llms-full.txt)
- Individual page content: ${baseUrl}/llms.mdx/[page-path]
- [API documentation](${baseUrl}/api-reference/)
- [Tool integrations](${baseUrl}/tools/)
## Statistics
- Total pages: ${pages.length} (English only)
- Other languages available at: ${baseUrl}/[lang]/ (es, fr, de, ja, zh)
---
Generated: ${new Date().toISOString()}
Format: llms.txt v0.1.0
See: https://llmstxt.org for specification`
return new Response(manifest, {
headers: {
'Content-Type': 'text/plain; charset=utf-8',
},
})
} catch (error) {
console.error('Error generating LLM manifest:', error)
return new Response('Error generating documentation manifest', { status: 500 })
}
}
+35
View File
@@ -0,0 +1,35 @@
import { DOCS_BASE_URL } from '@/lib/urls'
export const revalidate = false
export async function GET() {
const baseUrl = DOCS_BASE_URL
const robotsTxt = `# Robots.txt for Sim Documentation
User-agent: *
Disallow: /.next/
Disallow: /api/internal/
Disallow: /_next/static/
Disallow: /admin/
Allow: /
Allow: /llms.txt
Allow: /llms-full.txt
Allow: /llms.mdx/
# Sitemaps
Sitemap: ${baseUrl}/sitemap.xml
# Additional resources for AI indexing
# See https://github.com/AnswerDotAI/llms-txt for more info
# LLM-friendly content:
# Manifest: ${baseUrl}/llms.txt
# Full content: ${baseUrl}/llms-full.txt
# Individual pages: ${baseUrl}/llms.mdx/[page-path]`
return new Response(robotsTxt, {
headers: {
'Content-Type': 'text/plain',
},
})
}
+42
View File
@@ -0,0 +1,42 @@
import type { MetadataRoute } from 'next'
import { i18n } from '@/lib/i18n'
import { source } from '@/lib/source'
import { DOCS_BASE_URL } from '@/lib/urls'
export const revalidate = 3600
export default function sitemap(): MetadataRoute.Sitemap {
const baseUrl = DOCS_BASE_URL
const languages = source.getLanguages()
const pagesBySlug = new Map<string, Map<string, string>>()
for (const { language, pages } of languages) {
for (const page of pages) {
const key = page.slugs.join('/')
if (!pagesBySlug.has(key)) {
pagesBySlug.set(key, new Map())
}
pagesBySlug.get(key)!.set(language, `${baseUrl}${page.url}`)
}
}
const entries: MetadataRoute.Sitemap = []
for (const [, localeMap] of pagesBySlug) {
const defaultUrl = localeMap.get(i18n.defaultLanguage)
if (!defaultUrl) continue
const langAlternates: Record<string, string> = {}
for (const [lang, url] of localeMap) {
langAlternates[lang] = url
}
langAlternates['x-default'] = defaultUrl
entries.push({
url: defaultUrl,
alternates: { languages: langAlternates },
})
}
return entries
}
+11
View File
@@ -0,0 +1,11 @@
{
"aliases": {
"uiDir": "./components/ui",
"componentsDir": "./components",
"blockDir": "./components",
"cssDir": "./styles",
"libDir": "./lib"
},
"baseDir": "",
"commands": {}
}
+231
View File
@@ -0,0 +1,231 @@
'use client'
import { type FormEvent, useEffect, useMemo, useRef, useState } from 'react'
import { useChat } from '@ai-sdk/react'
import { DefaultChatTransport } from 'ai'
import { ArrowUp, MessageCircle, Square, X } from 'lucide-react'
import { Streamdown } from 'streamdown'
import { cn } from '@/lib/utils'
import 'streamdown/styles.css'
interface DocSource {
title: string
url: string
}
/** Pull the deduped doc sources surfaced by the searchDocs tool out of a message's parts. */
function getSources(parts: ReadonlyArray<{ type: string; [key: string]: unknown }>): DocSource[] {
const seen = new Set<string>()
const sources: DocSource[] = []
for (const part of parts) {
if (part.type !== 'tool-searchDocs') continue
const output = (part as { output?: unknown }).output
if (!Array.isArray(output)) continue
for (const item of output as DocSource[]) {
if (!item?.url || seen.has(item.url)) continue
seen.add(item.url)
sources.push({ title: item.title, url: item.url })
}
}
return sources
}
/** Concatenate the streamed text parts of a message. */
function getText(parts: ReadonlyArray<{ type: string; [key: string]: unknown }>): string {
return parts
.filter((part) => part.type === 'text')
.map((part) => (part as unknown as { text: string }).text)
.join('')
}
interface AskAIProps {
/** Active docs locale, forwarded so retrieval is scoped to the reader's language. */
locale: string
}
export function AskAI({ locale }: AskAIProps) {
const [open, setOpen] = useState(false)
const [input, setInput] = useState('')
const scrollRef = useRef<HTMLDivElement>(null)
// Stable transport; the locale is sent per-message (below) so it stays current
// after a language switch instead of being frozen into the transport.
const transport = useMemo(() => new DefaultChatTransport({ api: '/api/chat' }), [])
const { messages, sendMessage, status, stop, error } = useChat({ transport })
const isBusy = status === 'submitted' || status === 'streaming'
// Jump to the bottom instantly when the panel opens (a mount transition).
useEffect(() => {
if (!open) return
scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight })
}, [open])
// Smooth-scroll as new messages stream in (an explicit re-orientation cue).
useEffect(() => {
scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight, behavior: 'smooth' })
}, [messages])
const handleSubmit = (event: FormEvent) => {
event.preventDefault()
const text = input.trim()
if (!text || isBusy) return
sendMessage({ text }, { body: { locale } })
setInput('')
}
return (
<>
{!open && (
<button
type='button'
aria-label='Ask Sim'
onClick={() => setOpen(true)}
className='fixed right-4 bottom-4 z-50 flex h-11 items-center gap-1.5 rounded-full border border-[var(--border-1)] bg-[var(--surface-5)] px-4 font-season text-[var(--text-body)] text-sm shadow-[var(--shadow-medium)] transition-colors hover:bg-[var(--surface-active)] dark:bg-[var(--surface-4)]'
>
<MessageCircle className='size-[16px] text-[var(--text-icon)]' />
Ask Sim
</button>
)}
{open && (
<div className='fixed right-4 bottom-4 z-50 flex h-[600px] max-h-[calc(100vh-2rem)] w-[400px] max-w-[calc(100vw-2rem)] flex-col overflow-hidden rounded-xl border border-[var(--border-1)] bg-[var(--surface-5)] shadow-[var(--shadow-medium)] dark:bg-[var(--surface-4)]'>
<div className='flex items-center justify-between border-[var(--border-1)] border-b px-4 py-3'>
<span className='flex items-center gap-1.5 font-season text-[var(--text-body)] text-sm'>
<MessageCircle className='size-[16px] text-[var(--text-icon)]' />
Ask Sim
</span>
<button
type='button'
aria-label='Close'
onClick={() => {
stop()
setOpen(false)
}}
className='flex size-7 items-center justify-center rounded-lg text-[var(--text-icon)] transition-colors hover:bg-[var(--surface-active)]'
>
<X className='size-[16px]' />
</button>
</div>
<div ref={scrollRef} className='flex-1 space-y-4 overflow-y-auto px-4 py-4'>
{messages.length === 0 && (
<p className='text-[var(--text-muted)] text-sm'>
Ask anything about building, deploying, and managing AI agents in Sim.
</p>
)}
{messages.map((message, index) => {
const text = getText(message.parts)
// Only the in-progress (last) message should show the loading state.
const isStreaming = isBusy && index === messages.length - 1
const sources = message.role === 'assistant' ? getSources(message.parts) : []
return (
<div
key={message.id}
className={cn(
'flex flex-col gap-1.5',
message.role === 'user' ? 'items-end' : 'items-start'
)}
>
{message.role === 'user' ? (
<div className='max-w-[85%] whitespace-pre-wrap rounded-[16px] bg-[var(--surface-5)] px-3 py-2 text-[var(--text-primary)] text-base leading-[23px]'>
{text}
</div>
) : (
<div className='max-w-full text-[var(--text-primary)] text-base'>
{text ? (
<Streamdown
className={cn(
'space-y-3 text-[var(--text-primary)] text-base leading-relaxed',
'[&_a]:text-[var(--text-primary)] [&_a]:underline [&_a]:decoration-dashed [&_a]:underline-offset-4',
'[&_strong]:font-[600]',
'[&_h1]:font-[600] [&_h2]:font-[600] [&_h3]:font-[600] [&_h4]:font-[600]',
'[&_li]:my-1 [&_ol]:my-3 [&_ol]:list-decimal [&_ol]:pl-5 [&_ul]:my-3 [&_ul]:list-disc [&_ul]:pl-5',
'[&_code]:font-mono [&_pre]:my-3 [&_pre]:overflow-x-auto [&_pre]:rounded-lg [&_pre]:bg-[var(--surface-5)] [&_pre]:p-3 [&_pre]:text-small'
)}
>
{text}
</Streamdown>
) : isStreaming ? (
'…'
) : sources.length === 0 ? (
<span className='text-[var(--text-muted)]'>No answer returned.</span>
) : null}
</div>
)}
{sources.length > 0 && (
<div className='flex max-w-[90%] flex-wrap gap-1.5'>
{sources.map((source) => (
<a
key={source.url}
href={source.url}
target='_blank'
rel='noopener noreferrer'
className='rounded-lg border border-[var(--border-1)] px-2 py-0.5 text-[var(--text-muted)] text-xs transition-colors hover:bg-[var(--surface-active)]'
>
{source.title || source.url}
</a>
))}
</div>
)}
</div>
)
})}
{error && (
<p className='text-[var(--text-muted)] text-sm'>
Something went wrong. Please try again.
</p>
)}
</div>
<form onSubmit={handleSubmit} className='px-3 pb-3'>
<div className='flex items-end gap-2 rounded-2xl border border-[var(--border-1)] bg-white px-2.5 py-1.5 dark:bg-[var(--surface-5)]'>
<textarea
value={input}
onChange={(event) => setInput(event.target.value)}
onKeyDown={(event) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault()
handleSubmit(event)
}
}}
rows={1}
placeholder='Ask Sim about the docs…'
className='max-h-32 flex-1 resize-none bg-transparent py-1 font-season text-[var(--text-body)] text-sm outline-none placeholder:text-[var(--text-muted)]'
/>
{isBusy ? (
<button
type='button'
aria-label='Stop'
onClick={() => stop()}
className='flex size-[28px] shrink-0 items-center justify-center rounded-full bg-[#383838] transition-colors hover:bg-[#575757] dark:bg-[#e0e0e0] dark:hover:bg-[#cfcfcf]'
>
<Square className='size-[12px] fill-white text-white dark:fill-black dark:text-black' />
</button>
) : (
<button
type='submit'
aria-label='Send'
disabled={!input.trim()}
className={cn(
'flex size-[28px] shrink-0 items-center justify-center rounded-full transition-colors',
input.trim()
? 'bg-[#383838] hover:bg-[#575757] dark:bg-[#e0e0e0] dark:hover:bg-[#cfcfcf]'
: 'bg-[#808080]'
)}
>
<ArrowUp className='size-[16px] text-white dark:text-black' />
</button>
)}
</div>
</form>
</div>
)}
</>
)
}
@@ -0,0 +1,92 @@
import type { ReactNode } from 'react'
import { ChevronLeft, ChevronRight } from 'lucide-react'
import Link from 'next/link'
interface PageNeighbour {
url: string
name: ReactNode
}
interface PageFooterProps {
previous?: PageNeighbour
next?: PageNeighbour
}
const SOCIAL_LINKS = [
{
href: 'https://x.com/simdotai',
label: 'X (Twitter)',
icon: 'M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z',
},
{
href: 'https://github.com/simstudioai/sim',
label: 'GitHub',
icon: 'M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z',
},
{
href: 'https://discord.gg/Hr4UWYEcTT',
label: 'Discord',
icon: 'M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z',
},
] as const
export function PageFooter({ previous, next }: PageFooterProps) {
return (
<div className='mt-12'>
<div className='h-px w-full bg-[var(--border)]' />
{(previous || next) && (
<div className='flex gap-2 py-3'>
{previous ? (
<Link
href={previous.url}
className='group flex flex-1 flex-col gap-1 rounded-lg px-3 py-3 transition-colors hover:bg-[var(--surface-3)]'
>
<span className='text-[var(--text-muted)] text-xs'>Previous</span>
<span className='flex items-center gap-1.5 text-[var(--text-body)] text-sm transition-colors group-hover:text-[var(--text-primary)]'>
<ChevronLeft className='size-3.5 shrink-0' />
{previous.name}
</span>
</Link>
) : (
<div className='flex-1' />
)}
{next ? (
<Link
href={next.url}
className='group flex flex-1 flex-col items-end gap-1 rounded-lg px-3 py-3 transition-colors hover:bg-[var(--surface-3)]'
>
<span className='text-[var(--text-muted)] text-xs'>Next</span>
<span className='flex items-center gap-1.5 text-[var(--text-body)] text-sm transition-colors group-hover:text-[var(--text-primary)]'>
{next.name}
<ChevronRight className='size-3.5 shrink-0' />
</span>
</Link>
) : (
<div className='flex-1' />
)}
</div>
)}
<div className='flex items-center gap-4 pt-4 pb-6'>
{SOCIAL_LINKS.map((link) => (
<Link
key={link.label}
href={link.href}
target='_blank'
rel='noopener noreferrer'
aria-label={link.label}
>
<svg
viewBox='0 0 24 24'
className='size-[16px] fill-[var(--text-muted)] transition-colors hover:fill-[var(--text-icon)]'
>
<path d={link.icon} />
</svg>
</Link>
))}
</div>
</div>
)
}
@@ -0,0 +1,40 @@
'use client'
import { ChevronLeft, ChevronRight } from 'lucide-react'
import Link from 'next/link'
interface PageNavigationArrowsProps {
previous?: {
url: string
}
next?: {
url: string
}
}
const ARROW_LINK_CLASS =
'flex size-[30px] items-center justify-center rounded-lg text-[var(--text-icon)] transition-colors hover:bg-[var(--surface-active)]'
export function PageNavigationArrows({ previous, next }: PageNavigationArrowsProps) {
if (!previous && !next) return null
return (
<div className='flex items-center gap-2'>
{previous && (
<Link
href={previous.url}
className={ARROW_LINK_CLASS}
aria-label='Previous page'
title='Previous page'
>
<ChevronLeft className='size-[16px]' />
</Link>
)}
{next && (
<Link href={next.url} className={ARROW_LINK_CLASS} aria-label='Next page' title='Next page'>
<ChevronRight className='size-[16px]' />
</Link>
)}
</div>
)
}
@@ -0,0 +1,235 @@
'use client'
import { type ReactNode, useState } from 'react'
import type { Folder, Item, Separator } from 'fumadocs-core/page-tree'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { i18n } from '@/lib/i18n'
import { cn } from '@/lib/utils'
function SidebarChevron({ open, className }: { open: boolean; className?: string }) {
return (
<svg
width='5'
height='8'
viewBox='0 0 6 10'
fill='none'
className={cn(
'flex-shrink-0 transition-transform duration-200',
open && 'rotate-90',
className
)}
>
<path
d='M1 1L5 5L1 9'
stroke='currentColor'
strokeWidth='1.33'
strokeLinecap='square'
strokeLinejoin='miter'
/>
</svg>
)
}
const LANG_PREFIXES = i18n.languages.map((l) => `/${l}`)
function stripLangPrefix(path: string): string {
for (const prefix of LANG_PREFIXES) {
if (path === prefix) return '/'
if (path.startsWith(`${prefix}/`)) return path.slice(prefix.length)
}
return path
}
function isActive(url: string, pathname: string, nested = true): boolean {
const normalizedPathname = stripLangPrefix(pathname)
const normalizedUrl = stripLangPrefix(url)
return (
normalizedUrl === normalizedPathname ||
(nested && normalizedPathname.startsWith(`${normalizedUrl}/`))
)
}
const ITEM_BASE =
'flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-[var(--text-muted)] text-sm transition-colors hover:bg-[var(--surface-active)] hover:text-[var(--text-body)]'
const ITEM_ACTIVE_MOBILE = 'bg-[var(--surface-active)] font-medium text-[var(--text-primary)]'
const ITEM_DESKTOP =
'lg:mb-[0.0625rem] lg:block lg:rounded-lg lg:px-2.5 lg:py-1.5 lg:font-normal lg:text-[13px] lg:leading-tight'
const ITEM_TEXT = 'lg:text-[var(--text-body)]'
const ITEM_HOVER = 'lg:hover:bg-[var(--surface-3)]'
const ITEM_ACTIVE = 'lg:bg-[var(--surface-active)] lg:font-normal lg:text-[var(--text-body)]'
const FOLDER_TEXT = 'lg:text-[var(--text-body)] lg:font-medium'
const FOLDER_HOVER = 'lg:hover:bg-[var(--surface-3)]'
const FOLDER_ACTIVE = 'lg:bg-[var(--surface-active)] lg:text-[var(--text-body)]'
export function SidebarItem({ item }: { item: Item }) {
const pathname = usePathname()
const active = isActive(item.url, pathname, false)
return (
<Link
href={item.url}
data-active={active}
className={cn(
ITEM_BASE,
active && ITEM_ACTIVE_MOBILE,
ITEM_DESKTOP,
ITEM_TEXT,
!active && ITEM_HOVER,
active && ITEM_ACTIVE
)}
>
{item.name}
</Link>
)
}
function isApiReferenceFolder(node: Folder): boolean {
if (node.index?.url.includes('/api-reference/')) return true
for (const child of node.children) {
if (child.type === 'page' && child.url.includes('/api-reference/')) return true
if (child.type === 'folder' && isApiReferenceFolder(child)) return true
}
return false
}
export function SidebarFolder({ item, children }: { item: Folder; children: ReactNode }) {
const pathname = usePathname()
const hasActiveChild = checkHasActiveChild(item, pathname)
const isApiRef = isApiReferenceFolder(item)
const isOnApiRefPage = stripLangPrefix(pathname).startsWith('/api-reference')
const hasChildren = item.children.length > 0
const defaultOpen = hasActiveChild || (isApiRef && isOnApiRefPage)
const [manualOpen, setManualOpen] = useState<{ pathname: string; open: boolean } | null>(null)
const open = manualOpen?.pathname === pathname ? manualOpen.open : defaultOpen
const toggleOpen = () => setManualOpen({ pathname, open: !open })
const active = item.index ? isActive(item.index.url, pathname, false) : false
if (item.index && !hasChildren) {
return (
<Link
href={item.index.url}
data-active={active}
className={cn(
ITEM_BASE,
active && ITEM_ACTIVE_MOBILE,
ITEM_DESKTOP,
ITEM_TEXT,
!active && ITEM_HOVER,
active && ITEM_ACTIVE
)}
>
{item.name}
</Link>
)
}
return (
<div className='flex flex-col lg:mb-[0.0625rem]'>
<div className='flex w-full items-center lg:gap-0.5'>
{item.index ? (
<>
<Link
href={item.index.url}
data-active={active}
className={cn(
'flex flex-1 items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors',
'text-[var(--text-muted)] hover:bg-[var(--surface-active)] hover:text-[var(--text-body)]',
active && ITEM_ACTIVE_MOBILE,
'lg:block lg:flex-1 lg:rounded-lg lg:px-2.5 lg:py-1.5 lg:text-[13px] lg:leading-tight',
FOLDER_TEXT,
!active && FOLDER_HOVER,
active && FOLDER_ACTIVE
)}
>
{item.name}
</Link>
{hasChildren && (
<button
onClick={toggleOpen}
className={cn(
'rounded-md p-1 hover:bg-[var(--surface-active)]',
'lg:cursor-pointer lg:rounded-md lg:p-1 lg:transition-colors lg:hover:bg-[var(--surface-3)]'
)}
aria-label={open ? 'Collapse' : 'Expand'}
>
<SidebarChevron open={open} className='text-[var(--text-icon)]' />
</button>
)}
</>
) : (
<button
onClick={toggleOpen}
className={cn(
'flex flex-1 items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors',
'text-[var(--text-muted)] hover:bg-[var(--surface-active)]',
'lg:flex lg:w-full lg:cursor-pointer lg:items-center lg:justify-between lg:rounded-lg lg:px-2.5 lg:py-1.5 lg:text-left lg:text-[13px] lg:leading-tight',
FOLDER_TEXT,
FOLDER_HOVER
)}
>
<span>{item.name}</span>
<SidebarChevron open={open} className='ml-auto text-[var(--text-icon)]' />
</button>
)}
</div>
{hasChildren && (
<div
className={cn(
'grid transition-[grid-template-rows,opacity] duration-200 ease-in-out',
open ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0'
)}
>
<div className='overflow-hidden'>
<div className='ml-4 flex flex-col gap-0.5 lg:hidden'>{children}</div>
<ul className='mt-0.5 ml-2 hidden space-y-[0.0625rem] border-[var(--surface-active)] border-l pl-2.5 lg:block'>
{children}
</ul>
</div>
</div>
)}
</div>
)
}
export function SidebarSeparator({ item }: { item: Separator }) {
return (
<div
data-separator
className={cn('mt-5 mb-1.5 px-2', 'lg:relative lg:mt-0 lg:mb-1.5 lg:px-[13px] lg:pt-0')}
>
<div className='separator-divider hidden'>
<div className='h-[20px]' />
<div className='h-px bg-[var(--surface-active)]' />
<div className='h-[20px]' />
</div>
<p
className={cn(
'font-medium text-[var(--text-muted)] text-xs',
'lg:font-semibold lg:text-[10px] lg:text-[var(--text-muted)] lg:uppercase lg:tracking-[0.06em]'
)}
>
{item.name}
</p>
</div>
)
}
function checkHasActiveChild(node: Folder, pathname: string): boolean {
if (node.index && isActive(node.index.url, pathname)) {
return true
}
for (const child of node.children) {
if (child.type === 'page' && isActive(child.url, pathname)) {
return true
}
if (child.type === 'folder' && checkHasActiveChild(child, pathname)) {
return true
}
}
return false
}
+160
View File
@@ -0,0 +1,160 @@
import Link from 'next/link'
import { SimWordmark } from '@/components/ui/sim-logo'
import { SIM_SITE_URL } from '@/lib/urls'
/**
* Docs footer - the same site link directory as the main app's landing
* footer (`apps/sim/app/(landing)/components/footer`), ported here so both
* apps share one consistent footer. Links that live on sim.ai are absolute
* (docs.sim.ai is a different origin); links that live on docs.sim.ai itself
* (Academy, API Reference, blocks, integrations guides, …) stay relative.
*/
const LINK_CLASS =
'text-sm text-[var(--text-muted)] transition-colors hover:text-[var(--text-primary)]'
interface FooterItem {
label: string
href: string
external?: boolean
}
const PRODUCT_LINKS: FooterItem[] = [
{ label: 'Enterprise', href: `${SIM_SITE_URL}/enterprise`, external: true },
{ label: 'Mothership', href: '/mothership' },
{ label: 'Workflows', href: '/introduction' },
{ label: 'Knowledge Base', href: '/knowledgebase' },
{ label: 'Tables', href: '/tables' },
{ label: 'MCP', href: '/agents/mcp' },
{ label: 'API', href: '/api-reference/getting-started' },
{ label: 'Self Hosting', href: '/platform/self-hosting' },
{ label: 'Status', href: 'https://status.sim.ai', external: true },
]
const RESOURCES_LINKS: FooterItem[] = [
{ label: 'Blog', href: `${SIM_SITE_URL}/blog`, external: true },
{ label: 'Academy', href: '/academy' },
{ label: 'Compare', href: `${SIM_SITE_URL}/comparison`, external: true },
{ label: 'Careers', href: `${SIM_SITE_URL}/careers`, external: true },
{ label: 'Changelog', href: `${SIM_SITE_URL}/changelog`, external: true },
{ label: 'Contact', href: `${SIM_SITE_URL}/contact`, external: true },
]
/** Top model providers — mirrors the landing footer's top 8 catalog providers. */
const MODEL_LINKS: FooterItem[] = [
{ label: 'All Models', href: `${SIM_SITE_URL}/models`, external: true },
{ label: 'OpenAI', href: `${SIM_SITE_URL}/models/openai`, external: true },
{ label: 'Anthropic', href: `${SIM_SITE_URL}/models/anthropic`, external: true },
{ label: 'Google', href: `${SIM_SITE_URL}/models/google`, external: true },
{ label: 'DeepSeek', href: `${SIM_SITE_URL}/models/deepseek`, external: true },
{ label: 'xAI', href: `${SIM_SITE_URL}/models/xai`, external: true },
{ label: 'Cerebras', href: `${SIM_SITE_URL}/models/cerebras`, external: true },
{ label: 'Groq', href: `${SIM_SITE_URL}/models/groq`, external: true },
{ label: 'Sakana AI', href: `${SIM_SITE_URL}/models/sakana`, external: true },
]
const BLOCK_LINKS: FooterItem[] = [
{ label: 'Agent', href: '/workflows/blocks/agent' },
{ label: 'Router', href: '/workflows/blocks/router' },
{ label: 'Function', href: '/workflows/blocks/function' },
{ label: 'Condition', href: '/workflows/blocks/condition' },
{ label: 'API Block', href: '/workflows/blocks/api' },
{ label: 'Workflow', href: '/workflows/blocks/workflow' },
{ label: 'Parallel', href: '/workflows/blocks/parallel' },
{ label: 'Guardrails', href: '/workflows/blocks/guardrails' },
{ label: 'Evaluator', href: '/workflows/blocks/evaluator' },
{ label: 'Loop', href: '/workflows/blocks/loop' },
]
const INTEGRATION_LINKS: FooterItem[] = [
{ label: 'All Integrations', href: `${SIM_SITE_URL}/integrations`, external: true },
{ label: 'Slack', href: '/integrations/slack' },
{ label: 'GitHub', href: '/integrations/github' },
{ label: 'Gmail', href: '/integrations/gmail' },
{ label: 'Notion', href: '/integrations/notion' },
{ label: 'Salesforce', href: '/integrations/salesforce' },
{ label: 'Jira', href: '/integrations/jira' },
{ label: 'Linear', href: '/integrations/linear' },
{ label: 'Supabase', href: '/integrations/supabase' },
{ label: 'Stripe', href: '/integrations/stripe' },
]
const SOCIAL_LINKS: FooterItem[] = [
{ label: 'X (Twitter)', href: 'https://x.com/simdotai', external: true },
{
label: 'LinkedIn',
href: 'https://www.linkedin.com/company/simstudioai/',
external: true,
},
{ label: 'Discord', href: 'https://discord.gg/Hr4UWYEcTT', external: true },
{
label: 'GitHub',
href: 'https://github.com/simstudioai/sim',
external: true,
},
]
const LEGAL_LINKS: FooterItem[] = [
{ label: 'Terms of Service', href: `${SIM_SITE_URL}/terms`, external: true },
{ label: 'Privacy Policy', href: `${SIM_SITE_URL}/privacy`, external: true },
]
function FooterColumn({ title, items }: { title: string; items: FooterItem[] }) {
return (
<div>
<h3 className='mb-4 text-[var(--text-primary)] text-sm'>{title}</h3>
<div className='flex flex-col gap-2.5'>
{items.map(({ label, href, external }) =>
external ? (
<a
key={label}
href={href}
target='_blank'
rel='noopener noreferrer'
className={LINK_CLASS}
>
{label}
</a>
) : (
<Link key={label} href={href} className={LINK_CLASS}>
{label}
</Link>
)
)}
</div>
</div>
)
}
export function Footer() {
return (
<footer className='mt-[120px] w-full border-[var(--border)] border-t bg-[var(--bg)] max-sm:mt-16 max-lg:mt-[88px]'>
<div className='mx-auto w-full max-w-[1460px] px-20 pt-16 pb-16 max-sm:px-5 max-lg:px-8 max-lg:pt-12 max-lg:pb-12'>
<nav
aria-label='Footer navigation'
itemScope
itemType='https://schema.org/SiteNavigationElement'
className='grid grid-cols-8 gap-x-8 gap-y-10 max-sm:grid-cols-2 max-sm:gap-y-8 max-lg:grid-cols-3'
>
<a
href={SIM_SITE_URL}
aria-label='Sim home'
className='flex h-[18px] items-center max-lg:col-span-full max-lg:mb-2'
>
<SimWordmark />
</a>
<FooterColumn title='Product' items={PRODUCT_LINKS} />
<FooterColumn title='Resources' items={RESOURCES_LINKS} />
<FooterColumn title='Blocks' items={BLOCK_LINKS} />
<FooterColumn title='Integrations' items={INTEGRATION_LINKS} />
<FooterColumn title='Models' items={MODEL_LINKS} />
<FooterColumn title='Socials' items={SOCIAL_LINKS} />
<FooterColumn title='Legal' items={LEGAL_LINKS} />
</nav>
<p className='mt-16 text-[var(--text-muted)] text-sm'>© 2026 Sim. All rights reserved.</p>
</div>
</footer>
)
}
File diff suppressed because one or more lines are too long
+95
View File
@@ -0,0 +1,95 @@
'use client'
import { ChipLink } from '@sim/emcn'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { LanguageDropdown } from '@/components/ui/language-dropdown'
import { SearchTrigger } from '@/components/ui/search-trigger'
import { SimWordmark } from '@/components/ui/sim-logo'
import { ThemeToggle } from '@/components/ui/theme-toggle'
import { cn } from '@/lib/utils'
const NAV_TABS = [
{
label: 'Documentation',
href: '/introduction',
match: (p: string) => !p.includes('/api-reference') && !p.includes('/academy'),
external: false,
},
{
label: 'Academy',
href: '/academy',
match: (p: string) => p.includes('/academy'),
external: false,
},
{
label: 'API Reference',
href: '/api-reference/getting-started',
match: (p: string) => p.includes('/api-reference'),
external: false,
},
] as const
export function Navbar() {
const pathname = usePathname()
return (
<nav className='sticky top-0 z-50 bg-[var(--bg)]/80 backdrop-blur-md backdrop-saturate-150'>
<div className='hidden w-full flex-col lg:flex'>
{/* Top row: logo, search, controls */}
<div
className='relative flex h-[52px] w-full items-center justify-between'
style={{
paddingLeft: 'calc(var(--sidebar-offset) + var(--nav-inset))',
paddingRight: 'calc(var(--toc-offset) + var(--nav-inset))',
}}
>
<Link href='/' className='flex items-center'>
<SimWordmark className='h-[18px]' />
</Link>
<div className='-translate-x-1/2 absolute left-1/2 flex items-center justify-center'>
<SearchTrigger />
</div>
<div className='flex items-center gap-2'>
<LanguageDropdown />
<ThemeToggle />
<ChipLink href='https://sim.ai' variant='primary'>
Get started
</ChipLink>
</div>
</div>
{/* Bottom row: navigation tabs — border on row, tabs overlap it */}
<div
className='flex h-[40px] items-stretch gap-6 border-[var(--border)]/20 border-b'
style={{
paddingLeft: 'calc(var(--sidebar-offset) + var(--nav-inset))',
}}
>
{NAV_TABS.map((tab) => {
const isActive = !tab.external && tab.match(pathname)
return (
<Link
key={tab.label}
href={tab.href}
{...(tab.external ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
className={cn(
'-mb-px relative flex items-center border-b text-[14px] tracking-[-0.01em] transition-colors',
isActive
? 'border-[var(--text-muted)] font-[480] text-[var(--text-primary)]'
: 'border-transparent font-[430] text-[var(--text-muted)] hover:border-[var(--border-1)] hover:text-[var(--text-secondary)]'
)}
>
{/* Invisible bold text reserves width to prevent layout shift */}
<span className='invisible font-[480]'>{tab.label}</span>
<span className='absolute'>{tab.label}</span>
</Link>
)
})}
</div>
</div>
</nav>
)
}
+19
View File
@@ -0,0 +1,19 @@
'use client'
import { Chip } from '@sim/emcn'
import { useCopyButton } from 'fumadocs-ui/utils/use-copy-button'
import { Check, Copy } from 'lucide-react'
export function LLMCopyButton({ content }: { content: string }) {
const [checked, onClick] = useCopyButton(() => navigator.clipboard.writeText(content))
return (
<Chip
onClick={onClick}
leftIcon={checked ? Check : Copy}
aria-label={checked ? 'Copied to clipboard' : 'Copy page content'}
>
{checked ? 'Copied' : 'Copy page'}
</Chip>
)
}
+53
View File
@@ -0,0 +1,53 @@
import type { DocsPageType } from '@/lib/source'
import { cn } from '@/lib/utils'
/**
* Status-color mapping mirrored from the emcn `Badge` variants
* (`apps/sim/components/emcn/components/badge/badge.tsx`) — `green`, `blue`,
* `purple`, and `amber` over the shared `--badge-*` tokens.
*/
const CONFIG = {
tutorial: {
label: 'Tutorial',
className: 'bg-[var(--badge-success-bg)] text-[var(--badge-success-text)]',
},
guide: {
label: 'Guide',
className: 'bg-[var(--badge-blue-bg)] text-[var(--badge-blue-text)]',
},
reference: {
label: 'Reference',
className: 'bg-[var(--badge-purple-bg)] text-[var(--badge-purple-text)]',
},
concept: {
label: 'Concept',
className: 'bg-[var(--badge-amber-bg)] text-[var(--badge-amber-text)]',
},
} as const satisfies Record<DocsPageType, { label: string; className: string }>
interface PageTypeBadgeProps {
type: DocsPageType
className?: string
}
/**
* Small label that tells the reader which Diátaxis mode a page is — learning,
* task, lookup, or understanding. Rendered only when a page declares `type`.
* Chrome matches the emcn `Badge` status variants (md size).
*/
export function PageTypeBadge({ type, className }: PageTypeBadgeProps) {
const config = CONFIG[type]
if (!config) return null
return (
<span
className={cn(
'inline-flex items-center gap-1.5 rounded-md px-[9px] py-0.5 font-medium font-season text-[12px] transition-colors',
config.className,
className
)}
>
{config.label}
</span>
)
}
+128
View File
@@ -0,0 +1,128 @@
import { serializeJsonLd } from '@/lib/json-ld'
import { DOCS_BASE_URL } from '@/lib/urls'
interface StructuredDataProps {
title: string
description: string
url: string
lang: string
dateModified?: string
breadcrumb?: Array<{ name: string; url: string }>
}
export function StructuredData({
title,
description,
url,
lang,
dateModified,
breadcrumb,
}: StructuredDataProps) {
const baseUrl = DOCS_BASE_URL
const articleStructuredData = {
'@context': 'https://schema.org',
'@type': 'TechArticle',
headline: title,
description: description,
url: url,
...(dateModified && { datePublished: dateModified }),
...(dateModified && { dateModified }),
author: {
'@type': 'Organization',
name: 'Sim Team',
url: baseUrl,
},
publisher: {
'@type': 'Organization',
name: 'Sim',
url: baseUrl,
logo: {
'@type': 'ImageObject',
url: `${baseUrl}/static/logo.png`,
},
},
mainEntityOfPage: {
'@type': 'WebPage',
'@id': url,
},
inLanguage: lang,
isPartOf: {
'@type': 'WebSite',
name: 'Sim Documentation',
url: baseUrl,
},
potentialAction: {
'@type': 'ReadAction',
target: url,
},
}
const breadcrumbStructuredData = breadcrumb && {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: breadcrumb.map((item, index) => ({
'@type': 'ListItem',
position: index + 1,
name: item.name,
item: item.url,
})),
}
const softwareStructuredData = {
'@context': 'https://schema.org',
'@type': 'SoftwareApplication',
name: 'Sim',
applicationCategory: 'BusinessApplication',
applicationSubCategory: 'AI Workspace',
operatingSystem: 'Any',
description:
'Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM to create agents that automate real work.',
url: baseUrl,
author: {
'@type': 'Organization',
name: 'Sim Team',
},
offers: {
'@type': 'Offer',
category: 'Developer Tools',
},
featureList: [
'AI workspace for teams',
'Mothership — natural language agent creation',
'Visual workflow builder',
'1,000+ integrations',
'LLM orchestration (OpenAI, Anthropic, Google, xAI, Mistral, Perplexity)',
'Knowledge base creation',
'Table creation',
'Document creation',
],
}
return (
<>
<script
type='application/ld+json'
dangerouslySetInnerHTML={{
__html: serializeJsonLd(articleStructuredData),
}}
/>
{breadcrumbStructuredData && (
<script
type='application/ld+json'
dangerouslySetInnerHTML={{
__html: serializeJsonLd(breadcrumbStructuredData),
}}
/>
)}
{url === baseUrl && (
<script
type='application/ld+json'
dangerouslySetInnerHTML={{
__html: serializeJsonLd(softwareStructuredData),
}}
/>
)}
</>
)
}
+114
View File
@@ -0,0 +1,114 @@
'use client'
import { useRef, useState } from 'react'
import { cn, getAssetUrl } from '@/lib/utils'
import { Lightbox } from './lightbox'
interface ActionImageProps {
src: string
alt: string
enableLightbox?: boolean
}
interface ActionVideoProps {
src: string
alt: string
enableLightbox?: boolean
}
export function ActionImage({ src, alt, enableLightbox = true }: ActionImageProps) {
const [isLightboxOpen, setIsLightboxOpen] = useState(false)
const openLightbox = () => setIsLightboxOpen(true)
const image = (
<img
src={src}
alt={alt}
className={cn(
'inline-block w-full max-w-[200px] rounded-lg border border-[var(--border-1)]',
enableLightbox && 'transition-opacity group-hover:opacity-90'
)}
/>
)
return (
<>
{enableLightbox ? (
<button
type='button'
onClick={openLightbox}
aria-label={`Open ${alt} in media viewer`}
className='group inline-block cursor-pointer rounded p-0 text-left'
>
{image}
</button>
) : (
image
)}
{enableLightbox && (
<Lightbox
isOpen={isLightboxOpen}
onClose={() => setIsLightboxOpen(false)}
src={src}
alt={alt}
type='image'
/>
)}
</>
)
}
export function ActionVideo({ src, alt, enableLightbox = true }: ActionVideoProps) {
const videoRef = useRef<HTMLVideoElement>(null)
const startTimeRef = useRef(0)
const [isLightboxOpen, setIsLightboxOpen] = useState(false)
const resolvedSrc = getAssetUrl(src)
const openLightbox = () => {
startTimeRef.current = videoRef.current?.currentTime ?? 0
setIsLightboxOpen(true)
}
const video = (
<video
ref={videoRef}
src={resolvedSrc}
autoPlay
loop
muted
playsInline
className={cn(
'inline-block w-full max-w-[200px] rounded-lg border border-[var(--border-1)]',
enableLightbox && 'transition-opacity group-hover:opacity-90'
)}
/>
)
return (
<>
{enableLightbox ? (
<button
type='button'
onClick={openLightbox}
aria-label={`Open ${alt} in media viewer`}
className='group inline-block cursor-pointer rounded p-0 text-left'
>
{video}
</button>
) : (
video
)}
{enableLightbox && (
<Lightbox
isOpen={isLightboxOpen}
onClose={() => setIsLightboxOpen(false)}
src={src}
alt={alt}
type='video'
startTime={startTimeRef.current}
/>
)}
</>
)
}
@@ -0,0 +1,61 @@
'use client'
import type * as React from 'react'
import { blockTypeToIconMap } from '@/components/ui/icon-mapping'
interface BlockInfoCardProps {
type: string
color: string
icon?: React.ComponentType<{ className?: string }>
}
/**
* Brightness above which a tile background is "clearly light" and a white
* foreground icon would wash out. Mirrors apps/sim's LIGHT_TILE_THRESHOLD
* (blocks/icon-color.ts) so monochrome `currentColor` icons (e.g. Daytona,
* Notion) stay legible on white/pale tiles instead of white-on-white.
*/
const LIGHT_TILE_THRESHOLD = 0.75
function isLightTileColor(color: string): boolean {
const hex = color.trim().replace('#', '').toLowerCase()
let r: number
let g: number
let b: number
if (/^[0-9a-f]{3}$/.test(hex)) {
r = Number.parseInt(hex[0] + hex[0], 16)
g = Number.parseInt(hex[1] + hex[1], 16)
b = Number.parseInt(hex[2] + hex[2], 16)
} else if (/^[0-9a-f]{6}$/.test(hex)) {
r = Number.parseInt(hex.slice(0, 2), 16)
g = Number.parseInt(hex.slice(2, 4), 16)
b = Number.parseInt(hex.slice(4, 6), 16)
} else {
return false
}
return (0.299 * r + 0.587 * g + 0.114 * b) / 255 > LIGHT_TILE_THRESHOLD
}
export function BlockInfoCard({
type,
color,
icon: IconComponent,
}: BlockInfoCardProps): React.ReactNode {
const ResolvedIcon = IconComponent || blockTypeToIconMap[type] || null
const iconColorClass = isLightTileColor(color) ? 'text-black' : 'text-white'
return (
<div
className='mb-6 flex items-center justify-center overflow-hidden rounded-lg p-8'
style={{ background: color }}
>
{ResolvedIcon ? (
<ResolvedIcon className={`size-10 ${iconColorClass}`} />
) : (
<div className={`font-mono text-xl opacity-70 ${iconColorClass}`}>
{type.substring(0, 2)}
</div>
)}
</div>
)
}
+44
View File
@@ -0,0 +1,44 @@
'use client'
import { useState } from 'react'
import { CodeBlock as FumadocsCodeBlock } from 'fumadocs-ui/components/codeblock'
import { Check, Copy } from 'lucide-react'
import { cn } from '@/lib/utils'
export function CodeBlock(props: React.ComponentProps<typeof FumadocsCodeBlock>) {
const [copied, setCopied] = useState(false)
const handleCopy = async (text: string) => {
await navigator.clipboard.writeText(text)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
return (
<FumadocsCodeBlock
{...props}
className={cn('!border !border-[var(--border)] !shadow-none', props.className)}
Actions={({ className }) => (
<div className={cn('empty:hidden', className)}>
<button
type='button'
aria-label={copied ? 'Copied Text' : 'Copy Text'}
onClick={(e) => {
const pre = (e.currentTarget as HTMLElement).closest('figure')?.querySelector('pre')
if (pre) handleCopy(pre.textContent || '')
}}
className='cursor-pointer rounded-md p-2 text-[var(--text-muted)] transition-colors hover:text-[var(--text-icon)]'
>
<span className='flex items-center justify-center'>
{copied ? (
<Check size={16} className='text-[var(--brand-accent)]' />
) : (
<Copy size={16} className='text-[var(--text-muted)]' />
)}
</span>
</button>
</div>
)}
/>
)
}
+99
View File
@@ -0,0 +1,99 @@
'use client'
import { useState } from 'react'
import { ChevronRight } from 'lucide-react'
import { serializeJsonLd } from '@/lib/json-ld'
import { cn } from '@/lib/utils'
interface FAQItem {
question: string
answer: string
}
interface FAQProps {
items: FAQItem[]
title?: string
}
function FAQItemRow({
item,
isOpen,
onToggle,
}: {
item: FAQItem
isOpen: boolean
onToggle: () => void
}) {
return (
<div>
<button
type='button'
onClick={onToggle}
aria-expanded={isOpen}
className='flex w-full cursor-pointer items-center gap-3 px-4 py-2.5 text-left font-[470] text-[0.875rem] text-[var(--text-body)] transition-colors hover:bg-[var(--surface-3)]'
>
<ChevronRight
className={cn(
'size-[14px] shrink-0 text-[var(--text-icon)] transition-transform duration-200',
isOpen && 'rotate-90'
)}
/>
{item.question}
</button>
<div
className='grid transition-[grid-template-rows,opacity] duration-200 ease-in-out'
style={{
gridTemplateRows: isOpen ? '1fr' : '0fr',
opacity: isOpen ? 1 : 0,
}}
>
<div className='overflow-hidden'>
<div className='px-4 pt-2 pb-2.5 pl-11 text-[0.875rem] text-[var(--text-secondary)] leading-relaxed'>
{item.answer}
</div>
</div>
</div>
</div>
)
}
export function FAQ({ items, title = 'Common Questions' }: FAQProps) {
const [openIndex, setOpenIndex] = useState<number | null>(null)
const faqSchema = {
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: items.map((item) => ({
'@type': 'Question',
name: item.question,
acceptedAnswer: {
'@type': 'Answer',
text: item.answer,
},
})),
}
return (
<div className='mt-12'>
<script
type='application/ld+json'
dangerouslySetInnerHTML={{ __html: serializeJsonLd(faqSchema) }}
/>
<h2 className='mb-4 font-[500] text-xl'>{title}</h2>
<div className='border-[var(--border)] border-t border-b'>
{items.map((item, index) => (
<div
key={item.question}
className={cn(index !== items.length - 1 && 'border-[var(--border)] border-b')}
>
<FAQItemRow
item={item}
isOpen={openIndex === index}
onToggle={() => setOpenIndex(openIndex === index ? null : index)}
/>
</div>
))}
</div>
</div>
)
}
+58
View File
@@ -0,0 +1,58 @@
'use client'
import { type ComponentPropsWithoutRef, useState } from 'react'
import { Check, Link } from 'lucide-react'
import { cn } from '@/lib/utils'
type HeadingTag = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
interface HeadingProps extends ComponentPropsWithoutRef<'h1'> {
as?: HeadingTag
}
export function Heading({ as, className, ...props }: HeadingProps) {
const [copied, setCopied] = useState(false)
const As = as ?? 'h1'
if (!props.id) {
return <As className={className} {...props} />
}
const copyHeadingLink = async (e: React.MouseEvent) => {
e.preventDefault()
const url = `${window.location.origin}${window.location.pathname}#${props.id}`
try {
await navigator.clipboard.writeText(url)
setCopied(true)
// Update URL hash without scrolling
window.history.pushState(null, '', `#${props.id}`)
setTimeout(() => setCopied(false), 2000)
} catch {
// Fallback: just navigate to the anchor
window.location.hash = props.id as string
}
}
return (
<As className={cn('group flex scroll-m-28 flex-row items-center gap-2', className)} {...props}>
<a href={`#${props.id}`} className='peer' onClick={copyHeadingLink}>
{props.children}
</a>
{copied ? (
<Check
aria-hidden
className='size-[14px] shrink-0 text-[var(--brand-accent)] opacity-100 transition-opacity'
/>
) : (
<Link
aria-hidden
className='size-[14px] shrink-0 text-[var(--text-icon)] opacity-0 transition-opacity group-hover:opacity-100 peer-hover:opacity-100'
/>
)}
</As>
)
}
+506
View File
@@ -0,0 +1,506 @@
// Auto-generated file - do not edit manually
// Generated by scripts/generate-docs.ts
// Maps block types to their icon component references
import type { ComponentType, SVGProps } from 'react'
import {
AgentMailIcon,
AgentPhoneIcon,
AgiloftIcon,
AhrefsIcon,
AirtableIcon,
AirweaveIcon,
AlgoliaIcon,
AmplitudeIcon,
ApifyIcon,
ApolloIcon,
AppConfigIcon,
ArxivIcon,
AsanaIcon,
AshbyIcon,
AthenaIcon,
AttioIcon,
AzureIcon,
BoxCompanyIcon,
BrainIcon,
BrandfetchIcon,
BrexIcon,
BrightDataIcon,
BrowserUseIcon,
CalComIcon,
CalendlyIcon,
CirclebackIcon,
ClayIcon,
ClerkIcon,
ClickHouseIcon,
CloudFormationIcon,
CloudflareIcon,
CloudWatchIcon,
CodePipelineIcon,
ConfluenceIcon,
ContextDevIcon,
ConvexIcon,
CrowdStrikeIcon,
CursorIcon,
DagsterIcon,
DatabricksIcon,
DatadogIcon,
DatagmaIcon,
DaytonaIcon,
DevinIcon,
DiscordIcon,
DocumentIcon,
DocuSignIcon,
DowndetectorIcon,
DropboxIcon,
DropcontactIcon,
DsPyIcon,
DubIcon,
DuckDuckGoIcon,
DynamoDBIcon,
ElasticsearchIcon,
ElevenLabsIcon,
EmailBisonIcon,
EnrichmentIcon,
EnrichSoIcon,
EnrowIcon,
EvernoteIcon,
ExaAIIcon,
ExtendIcon,
EyeIcon,
FathomIcon,
FindymailIcon,
FirecrawlIcon,
FirefliesIcon,
GammaIcon,
GithubIcon,
GitLabIcon,
GmailIcon,
GongIcon,
GoogleAdsIcon,
GoogleAppsheetIcon,
GoogleBigQueryIcon,
GoogleBooksIcon,
GoogleCalendarIcon,
GoogleContactsIcon,
GoogleDocsIcon,
GoogleDriveIcon,
GoogleFormsIcon,
GoogleGroupsIcon,
GoogleIcon,
GoogleMapsIcon,
GoogleMeetIcon,
GooglePagespeedIcon,
GoogleSheetsIcon,
GoogleSlidesIcon,
GoogleTasksIcon,
GoogleTranslateIcon,
GoogleVaultIcon,
GrafanaIcon,
GrainIcon,
GranolaIcon,
GreenhouseIcon,
GreptileIcon,
HexIcon,
HubspotIcon,
HuggingFaceIcon,
HunterIOIcon,
IAMIcon,
IcypeasIcon,
IdentityCenterIcon,
IncidentioIcon,
InfisicalIcon,
InstantlyIcon,
IntercomIcon,
JinaAIIcon,
JiraIcon,
JiraServiceManagementIcon,
JupyterIcon,
KalshiIcon,
KetchIcon,
LangsmithIcon,
LatexIcon,
LaunchDarklyIcon,
LeadMagicIcon,
LemlistIcon,
LinearIcon,
LinkedInIcon,
LinkupIcon,
LinqIcon,
LoopsIcon,
LumaIcon,
MailchimpIcon,
MailgunIcon,
MailServerIcon,
Mem0Icon,
MicrosoftDataverseIcon,
MicrosoftExcelIcon,
MicrosoftOneDriveIcon,
MicrosoftPlannerIcon,
MicrosoftSharepointIcon,
MicrosoftTeamsIcon,
MillionVerifierIcon,
MistralIcon,
MondayIcon,
MongoDBIcon,
MySQLIcon,
Neo4jIcon,
NeverBounceIcon,
NewRelicIcon,
NotionIcon,
ObsidianIcon,
OktaIcon,
OnePasswordIcon,
OpenAIIcon,
OutlookIcon,
PackageSearchIcon,
PagerDutyIcon,
ParallelIcon,
PeopleDataLabsIcon,
PerplexityIcon,
PersonaIcon,
PineconeIcon,
PipedriveIcon,
PolymarketIcon,
PostgresIcon,
PosthogIcon,
ProfoundIcon,
ProspeoIcon,
PulseIcon,
QdrantIcon,
QuartrIcon,
QuiverIcon,
RailwayIcon,
RB2BIcon,
RDSIcon,
RedditIcon,
RedisIcon,
ReductoIcon,
ResendIcon,
RevenueCatIcon,
RipplingIcon,
RootlyIcon,
S3Icon,
SalesforceIcon,
SapConcurIcon,
SapS4HanaIcon,
SESIcon,
SecretsManagerIcon,
SendblueIcon,
SendgridIcon,
SentryIcon,
SerperIcon,
ServiceNowIcon,
SftpIcon,
ShopifyIcon,
SimilarwebIcon,
SimTriggerIcon,
SixtyfourIcon,
SlackIcon,
SmtpIcon,
SportmonksIcon,
SQSIcon,
SquareIcon,
SshIcon,
STSIcon,
STTIcon,
StagehandIcon,
StripeIcon,
SupabaseIcon,
TailscaleIcon,
TavilyIcon,
TelegramIcon,
TemporalIcon,
TextractIcon,
ThriveIcon,
TinybirdIcon,
TrelloIcon,
TriggerDevIcon,
TwilioIcon,
TypeformIcon,
UpstashIcon,
UptimeRobotIcon,
VantaIcon,
VercelIcon,
VideoIcon,
WealthboxIcon,
WebflowIcon,
WhatsAppIcon,
WikipediaIcon,
WizaIcon,
WordpressIcon,
WorkdayIcon,
xIcon,
YouTubeIcon,
ZendeskIcon,
ZepIcon,
ZeroBounceIcon,
ZoomIcon,
ZoomInfoIcon,
} from '@/components/icons'
type IconComponent = ComponentType<SVGProps<SVGSVGElement>>
export const blockTypeToIconMap: Record<string, IconComponent> = {
agentmail: AgentMailIcon,
agentphone: AgentPhoneIcon,
agiloft: AgiloftIcon,
ahrefs: AhrefsIcon,
airtable: AirtableIcon,
airweave: AirweaveIcon,
algolia: AlgoliaIcon,
amplitude: AmplitudeIcon,
apify: ApifyIcon,
apollo: ApolloIcon,
appconfig: AppConfigIcon,
arxiv: ArxivIcon,
asana: AsanaIcon,
ashby: AshbyIcon,
athena: AthenaIcon,
attio: AttioIcon,
azure_devops: AzureIcon,
box: BoxCompanyIcon,
brandfetch: BrandfetchIcon,
brex: BrexIcon,
brightdata: BrightDataIcon,
browser_use: BrowserUseIcon,
calcom: CalComIcon,
calendly: CalendlyIcon,
circleback: CirclebackIcon,
clay: ClayIcon,
clerk: ClerkIcon,
clickhouse: ClickHouseIcon,
cloudflare: CloudflareIcon,
cloudformation: CloudFormationIcon,
cloudwatch: CloudWatchIcon,
codepipeline: CodePipelineIcon,
confluence: ConfluenceIcon,
confluence_v2: ConfluenceIcon,
context_dev: ContextDevIcon,
convex: ConvexIcon,
crowdstrike: CrowdStrikeIcon,
cursor: CursorIcon,
cursor_v2: CursorIcon,
dagster: DagsterIcon,
databricks: DatabricksIcon,
datadog: DatadogIcon,
datagma: DatagmaIcon,
daytona: DaytonaIcon,
devin: DevinIcon,
discord: DiscordIcon,
docusign: DocuSignIcon,
downdetector: DowndetectorIcon,
dropbox: DropboxIcon,
dropcontact: DropcontactIcon,
dspy: DsPyIcon,
dub: DubIcon,
duckduckgo: DuckDuckGoIcon,
dynamodb: DynamoDBIcon,
elasticsearch: ElasticsearchIcon,
elevenlabs: ElevenLabsIcon,
emailbison: EmailBisonIcon,
enrich: EnrichSoIcon,
enrichment: EnrichmentIcon,
enrow: EnrowIcon,
evernote: EvernoteIcon,
exa: ExaAIIcon,
extend: ExtendIcon,
extend_v2: ExtendIcon,
fathom: FathomIcon,
file: DocumentIcon,
file_v2: DocumentIcon,
file_v4: DocumentIcon,
file_v5: DocumentIcon,
findymail: FindymailIcon,
firecrawl: FirecrawlIcon,
fireflies: FirefliesIcon,
fireflies_v2: FirefliesIcon,
gamma: GammaIcon,
github: GithubIcon,
github_v2: GithubIcon,
gitlab: GitLabIcon,
gmail: GmailIcon,
gmail_v2: GmailIcon,
gong: GongIcon,
google_ads: GoogleAdsIcon,
google_appsheet: GoogleAppsheetIcon,
google_bigquery: GoogleBigQueryIcon,
google_books: GoogleBooksIcon,
google_calendar: GoogleCalendarIcon,
google_calendar_v2: GoogleCalendarIcon,
google_contacts: GoogleContactsIcon,
google_docs: GoogleDocsIcon,
google_drive: GoogleDriveIcon,
google_forms: GoogleFormsIcon,
google_groups: GoogleGroupsIcon,
google_maps: GoogleMapsIcon,
google_meet: GoogleMeetIcon,
google_pagespeed: GooglePagespeedIcon,
google_search: GoogleIcon,
google_sheets: GoogleSheetsIcon,
google_sheets_v2: GoogleSheetsIcon,
google_slides: GoogleSlidesIcon,
google_slides_v2: GoogleSlidesIcon,
google_tasks: GoogleTasksIcon,
google_translate: GoogleTranslateIcon,
google_vault: GoogleVaultIcon,
grafana: GrafanaIcon,
grain: GrainIcon,
granola: GranolaIcon,
greenhouse: GreenhouseIcon,
greptile: GreptileIcon,
hex: HexIcon,
hubspot: HubspotIcon,
huggingface: HuggingFaceIcon,
hunter: HunterIOIcon,
iam: IAMIcon,
icypeas: IcypeasIcon,
identity_center: IdentityCenterIcon,
imap: MailServerIcon,
incidentio: IncidentioIcon,
infisical: InfisicalIcon,
instantly: InstantlyIcon,
intercom: IntercomIcon,
intercom_v2: IntercomIcon,
jina: JinaAIIcon,
jira: JiraIcon,
jira_service_management: JiraServiceManagementIcon,
jupyter: JupyterIcon,
kalshi: KalshiIcon,
kalshi_v2: KalshiIcon,
ketch: KetchIcon,
knowledge: PackageSearchIcon,
langsmith: LangsmithIcon,
latex: LatexIcon,
launchdarkly: LaunchDarklyIcon,
leadmagic: LeadMagicIcon,
lemlist: LemlistIcon,
linear: LinearIcon,
linear_v2: LinearIcon,
linkedin: LinkedInIcon,
linkup: LinkupIcon,
linq: LinqIcon,
loops: LoopsIcon,
luma: LumaIcon,
mailchimp: MailchimpIcon,
mailgun: MailgunIcon,
mem0: Mem0Icon,
memory: BrainIcon,
microsoft_ad: AzureIcon,
microsoft_dataverse: MicrosoftDataverseIcon,
microsoft_excel: MicrosoftExcelIcon,
microsoft_excel_v2: MicrosoftExcelIcon,
microsoft_planner: MicrosoftPlannerIcon,
microsoft_teams: MicrosoftTeamsIcon,
millionverifier: MillionVerifierIcon,
mistral_parse: MistralIcon,
mistral_parse_v2: MistralIcon,
mistral_parse_v3: MistralIcon,
monday: MondayIcon,
mongodb: MongoDBIcon,
mysql: MySQLIcon,
neo4j: Neo4jIcon,
neverbounce: NeverBounceIcon,
new_relic: NewRelicIcon,
notion: NotionIcon,
notion_v2: NotionIcon,
obsidian: ObsidianIcon,
okta: OktaIcon,
onedrive: MicrosoftOneDriveIcon,
onepassword: OnePasswordIcon,
openai: OpenAIIcon,
outlook: OutlookIcon,
pagerduty: PagerDutyIcon,
parallel_ai: ParallelIcon,
peopledatalabs: PeopleDataLabsIcon,
perplexity: PerplexityIcon,
persona: PersonaIcon,
pinecone: PineconeIcon,
pipedrive: PipedriveIcon,
polymarket: PolymarketIcon,
postgresql: PostgresIcon,
posthog: PosthogIcon,
profound: ProfoundIcon,
prospeo: ProspeoIcon,
pulse: PulseIcon,
pulse_v2: PulseIcon,
qdrant: QdrantIcon,
quartr: QuartrIcon,
quiver: QuiverIcon,
railway: RailwayIcon,
rb2b: RB2BIcon,
rds: RDSIcon,
reddit: RedditIcon,
redis: RedisIcon,
reducto: ReductoIcon,
reducto_v2: ReductoIcon,
resend: ResendIcon,
revenuecat: RevenueCatIcon,
rippling: RipplingIcon,
rootly: RootlyIcon,
s3: S3Icon,
salesforce: SalesforceIcon,
sap_concur: SapConcurIcon,
sap_s4hana: SapS4HanaIcon,
secrets_manager: SecretsManagerIcon,
sendblue: SendblueIcon,
sendgrid: SendgridIcon,
sentry: SentryIcon,
serper: SerperIcon,
servicenow: ServiceNowIcon,
ses: SESIcon,
sftp: SftpIcon,
sharepoint: MicrosoftSharepointIcon,
sharepoint_v2: MicrosoftSharepointIcon,
shopify: ShopifyIcon,
sim_workspace_event: SimTriggerIcon,
similarweb: SimilarwebIcon,
sixtyfour: SixtyfourIcon,
slack: SlackIcon,
smtp: SmtpIcon,
sportmonks: SportmonksIcon,
sqs: SQSIcon,
square: SquareIcon,
ssh: SshIcon,
stagehand: StagehandIcon,
stripe: StripeIcon,
sts: STSIcon,
stt: STTIcon,
stt_v2: STTIcon,
supabase: SupabaseIcon,
tailscale: TailscaleIcon,
tavily: TavilyIcon,
telegram: TelegramIcon,
temporal: TemporalIcon,
textract: TextractIcon,
textract_v2: TextractIcon,
thrive: ThriveIcon,
tinybird: TinybirdIcon,
trello: TrelloIcon,
trigger_dev: TriggerDevIcon,
twilio_sms: TwilioIcon,
twilio_voice: TwilioIcon,
typeform: TypeformIcon,
upstash: UpstashIcon,
uptimerobot: UptimeRobotIcon,
vanta: VantaIcon,
vercel: VercelIcon,
video_generator: VideoIcon,
video_generator_v2: VideoIcon,
vision: EyeIcon,
vision_v2: EyeIcon,
wealthbox: WealthboxIcon,
webflow: WebflowIcon,
whatsapp: WhatsAppIcon,
wikipedia: WikipediaIcon,
wiza: WizaIcon,
wordpress: WordpressIcon,
workday: WorkdayIcon,
x: xIcon,
youtube: YouTubeIcon,
zendesk: ZendeskIcon,
zep: ZepIcon,
zerobounce: ZeroBounceIcon,
zoom: ZoomIcon,
zoominfo: ZoomInfoIcon,
}
+63
View File
@@ -0,0 +1,63 @@
'use client'
import { useState } from 'react'
import NextImage, { type ImageProps as NextImageProps } from 'next/image'
import { Lightbox } from '@/components/ui/lightbox'
import { cn } from '@/lib/utils'
interface ImageProps extends Omit<NextImageProps, 'className'> {
className?: string
enableLightbox?: boolean
}
export function Image({
className = 'w-full',
enableLightbox = true,
alt = '',
src,
...props
}: ImageProps) {
const [isLightboxOpen, setIsLightboxOpen] = useState(false)
const openLightbox = () => setIsLightboxOpen(true)
const image = (
<NextImage
className={cn(
'overflow-hidden rounded-xl border border-[var(--border)] object-cover',
enableLightbox && 'cursor-pointer transition-opacity group-hover:opacity-95',
className
)}
alt={alt}
src={src}
{...props}
/>
)
return (
<>
{enableLightbox ? (
<button
type='button'
onClick={openLightbox}
aria-label={`Open ${alt} in media viewer`}
className='group contents'
>
{image}
</button>
) : (
image
)}
{enableLightbox && (
<Lightbox
isOpen={isLightboxOpen}
onClose={() => setIsLightboxOpen(false)}
src={typeof src === 'string' ? src : String(src)}
alt={alt}
type='image'
/>
)}
</>
)
}
@@ -0,0 +1,59 @@
'use client'
import { ChipDropdown } from '@sim/emcn'
import { useParams, usePathname, useRouter } from 'next/navigation'
const languages = {
en: { name: 'English', flag: '🇺🇸' },
es: { name: 'Español', flag: '🇪🇸' },
fr: { name: 'Français', flag: '🇫🇷' },
de: { name: 'Deutsch', flag: '🇩🇪' },
ja: { name: '日本語', flag: '🇯🇵' },
zh: { name: '简体中文', flag: '🇨🇳' },
}
export function LanguageDropdown() {
const pathname = usePathname()
const params = useParams()
const { push } = useRouter()
const languageOptions = Object.entries(languages).map(([code, lang]) => ({
value: code,
label: lang.name,
iconElement: <span className='text-[13px]'>{lang.flag}</span>,
}))
const langFromParams = params?.lang as string
const currentLang =
langFromParams && Object.keys(languages).includes(langFromParams) ? langFromParams : 'en'
const handleLanguageChange = (locale: string) => {
if (locale === currentLang) return
const segments = pathname.split('/').filter(Boolean)
if (segments[0] && Object.keys(languages).includes(segments[0])) {
segments.shift()
}
let newPath = ''
if (locale === 'en') {
newPath = segments.length > 0 ? `/${segments.join('/')}` : '/introduction'
} else {
newPath = `/${locale}${segments.length > 0 ? `/${segments.join('/')}` : '/introduction'}`
}
push(newPath)
}
return (
<ChipDropdown
value={currentLang}
onChange={handleLanguageChange}
options={languageOptions}
align='end'
matchTriggerWidth={false}
contentClassName='min-w-[160px]'
/>
)
}
+96
View File
@@ -0,0 +1,96 @@
'use client'
import { useEffect, useEffectEvent, useLayoutEffect, useRef } from 'react'
import { getAssetUrl } from '@/lib/utils'
interface LightboxProps {
isOpen: boolean
onClose: () => void
src: string
alt: string
type: 'image' | 'video'
startTime?: number
}
export function Lightbox({ isOpen, onClose, src, alt, type, startTime }: LightboxProps) {
const overlayRef = useRef<HTMLDivElement>(null)
const mediaButtonRef = useRef<HTMLButtonElement>(null)
const videoRef = useRef<HTMLVideoElement>(null)
const closeLightbox = useEffectEvent(onClose)
useEffect(() => {
if (!isOpen) return
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
closeLightbox()
}
}
const handleClickOutside = (event: MouseEvent) => {
if (overlayRef.current && event.target === overlayRef.current) {
closeLightbox()
}
}
const previousOverflow = document.body.style.overflow
document.addEventListener('keydown', handleKeyDown)
document.addEventListener('click', handleClickOutside)
document.body.style.overflow = 'hidden'
mediaButtonRef.current?.focus()
return () => {
document.removeEventListener('keydown', handleKeyDown)
document.removeEventListener('click', handleClickOutside)
document.body.style.overflow = previousOverflow
}
}, [isOpen])
useLayoutEffect(() => {
if (isOpen && type === 'video' && videoRef.current && startTime != null && startTime > 0) {
videoRef.current.currentTime = startTime
}
}, [isOpen, startTime, type])
if (!isOpen) return null
return (
<div
ref={overlayRef}
className='fixed inset-0 z-50 flex items-center justify-center bg-black/80 p-12 backdrop-blur-sm'
role='dialog'
aria-modal='true'
aria-label='Media viewer'
>
<div className='relative max-h-full max-w-full overflow-hidden rounded-xl'>
<button
ref={mediaButtonRef}
type='button'
onClick={onClose}
aria-label='Close media viewer'
className='block cursor-pointer rounded-xl p-0 outline-none focus-visible:ring-2 focus-visible:ring-white/70'
>
{type === 'image' ? (
<img
src={src}
alt={alt}
className='max-h-[75vh] max-w-[75vw] rounded-xl object-contain'
loading='lazy'
/>
) : (
<video
ref={videoRef}
src={getAssetUrl(src)}
autoPlay
loop
muted
playsInline
className='max-h-[75vh] max-w-[75vw] rounded-xl outline-none focus:outline-none'
/>
)}
</button>
</div>
</div>
)
}
@@ -0,0 +1,169 @@
'use client'
import { useEffect, useRef, useState } from 'react'
import { ChevronDown } from 'lucide-react'
import { cn } from '@/lib/utils'
interface ResponseSectionProps {
children: React.ReactNode
}
export function ResponseSection({ children }: ResponseSectionProps) {
const containerRef = useRef<HTMLDivElement>(null)
const [statusCodes, setStatusCodes] = useState<string[]>([])
const [selectedCode, setSelectedCode] = useState<string>('')
const [isOpen, setIsOpen] = useState(false)
const dropdownRef = useRef<HTMLDivElement>(null)
function getAccordionItems() {
const root = containerRef.current?.querySelector('[data-orientation="vertical"]')
if (!root) return []
return Array.from(root.children).filter(
(el) => el.getAttribute('data-state') !== null
) as HTMLElement[]
}
function showStatusCode(code: string) {
const items = getAccordionItems()
for (const item of items) {
const triggerBtn = item.querySelector('h3 button') as HTMLButtonElement | null
const text = triggerBtn?.textContent?.trim() ?? ''
const itemCode = text.match(/^\d{3}/)?.[0]
if (itemCode === code) {
item.style.display = ''
if (item.getAttribute('data-state') === 'closed' && triggerBtn) {
triggerBtn.click()
}
} else {
item.style.display = 'none'
if (item.getAttribute('data-state') === 'open' && triggerBtn) {
triggerBtn.click()
}
}
}
}
/**
* Detect when the fumadocs accordion children mount via MutationObserver,
* then extract status codes and show the first one.
* Replaces the previous approach that used `children` as a dependency
* (which triggered on every render since children is a new object each time).
*/
useEffect(() => {
const container = containerRef.current
if (!container) return
const initialize = () => {
const items = getAccordionItems()
if (items.length === 0) return false
const codes: string[] = []
const seen = new Set<string>()
for (const item of items) {
const triggerBtn = item.querySelector('h3 button')
if (triggerBtn) {
const text = triggerBtn.textContent?.trim() ?? ''
const code = text.match(/^\d{3}/)?.[0]
if (code && !seen.has(code)) {
seen.add(code)
codes.push(code)
}
}
}
if (codes.length > 0) {
setStatusCodes(codes)
setSelectedCode(codes[0])
showStatusCode(codes[0])
return true
}
return false
}
if (initialize()) return
const observer = new MutationObserver(() => {
if (initialize()) {
observer.disconnect()
}
})
observer.observe(container, { childList: true, subtree: true })
return () => observer.disconnect()
}, []) // eslint-disable-line react-hooks/exhaustive-deps
function handleSelectCode(code: string) {
setSelectedCode(code)
setIsOpen(false)
showStatusCode(code)
}
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [])
return (
<div ref={containerRef} className='response-section-wrapper'>
{statusCodes.length > 0 && (
<div className='response-section-header'>
<h2 className='response-section-title'>Response</h2>
<div className='response-section-meta'>
<div ref={dropdownRef} className='response-section-dropdown-wrapper'>
<button
type='button'
className='response-section-dropdown-trigger'
onClick={() => setIsOpen(!isOpen)}
>
<span>{selectedCode}</span>
<ChevronDown
className={cn(
'response-section-chevron',
isOpen && 'response-section-chevron-open'
)}
/>
</button>
{isOpen && (
<div className='response-section-dropdown-menu'>
{statusCodes.map((code) => (
<button
key={code}
type='button'
className={cn(
'response-section-dropdown-item',
code === selectedCode && 'response-section-dropdown-item-selected'
)}
onClick={() => handleSelectCode(code)}
>
<span>{code}</span>
{code === selectedCode && (
<svg
className='response-section-check'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
>
<polyline points='20 6 9 17 4 12' />
</svg>
)}
</button>
))}
</div>
)}
</div>
<span className='response-section-content-type'>application/json</span>
</div>
</div>
)}
<div className='response-section-content'>{children}</div>
</div>
)
}
@@ -0,0 +1,42 @@
'use client'
import {
chipContentIconClass,
chipFilledFillTokens,
chipGeometryClass,
TRIGGER_BORDER_CLASS,
} from '@sim/emcn'
import { Search } from 'lucide-react'
import { cn } from '@/lib/utils'
export function SearchTrigger() {
const openSearchDialog = () => {
const event = new KeyboardEvent('keydown', {
key: 'k',
metaKey: true,
bubbles: true,
})
document.dispatchEvent(event)
}
return (
<button
type='button'
data-search-trigger
className={cn(
chipGeometryClass,
chipFilledFillTokens,
TRIGGER_BORDER_CLASS,
'flex w-[360px] cursor-pointer font-season text-[var(--text-muted)] transition-colors hover:bg-[var(--surface-active)]'
)}
onClick={openSearchDialog}
>
<Search className={chipContentIconClass} />
<span>Search&hellip;</span>
<kbd className='ml-auto flex items-center'>
<span className='text-[15px]'></span>
<span className='text-[12px]'>K</span>
</kbd>
</button>
)
}
+138
View File
@@ -0,0 +1,138 @@
'use client'
import { useId } from 'react'
import { cn } from '@/lib/utils'
interface SimLogoProps {
className?: string
}
/**
* Inline "sim" wordmark, no separate icon mark — same paths as the landing
* footer's `SimWordmark` (`apps/sim/app/(landing)/components/navbar/components/sim-wordmark`),
* ported here so the docs footer can match it exactly. Filled with
* `var(--text-body)` so it reads as one solid ink matching surrounding text.
*/
export function SimWordmark({ className }: SimLogoProps) {
return (
<svg
viewBox='0 0 441 212'
width={37}
height={18}
fill='none'
aria-hidden='true'
className={cn('-translate-y-[1.5px] h-[18px] w-auto', className)}
>
<g fill='var(--text-body)'>
<path d='M0 160.9H29.51C29.51 169.08 32.46 175.61 38.37 180.48C44.27 185.12 52.25 187.44 62.31 187.44C73.24 187.44 81.65 185.34 87.56 181.14C93.46 176.71 96.41 170.85 96.41 163.55C96.41 158.24 94.77 153.82 91.49 150.28C88.43 146.74 82.75 143.86 74.44 141.65L46.24 135.01C32.03 131.47 21.42 126.05 14.43 118.75C7.65 111.45 4.26 101.83 4.26 89.88C4.26 79.93 6.78 71.3 11.81 64C17.05 56.7 24.16 51.06 33.12 47.08C42.3 43.09 52.8 41.1 64.6 41.1C76.41 41.1 86.57 43.2 95.1 47.41C103.84 51.61 110.62 57.47 115.43 64.99C120.46 72.52 123.08 81.48 123.3 91.87H93.79C93.57 83.47 90.84 76.94 85.59 72.3C80.34 67.65 73.02 65.33 63.62 65.33C54 65.33 46.57 67.43 41.32 71.63C36.07 75.83 33.45 81.59 33.45 88.89C33.45 99.73 41.32 107.14 57.06 111.12L85.26 118.09C98.81 121.19 108.98 126.28 115.76 133.35C122.53 140.21 125.92 149.61 125.92 161.56C125.92 171.74 123.19 180.7 117.73 188.44C112.26 195.96 104.72 201.82 95.1 206.03C85.7 210.01 74.55 212 61.65 212C42.85 212 27.87 207.35 16.72 198.06C5.57 188.77 0 176.38 0 160.9Z' />
<path d='M232.8 212H202.13L202.13 49.76H229.54V77.39C232.8 68.34 239.11 60.66 247.81 54.7C256.73 48.52 267.5 45.43 280.12 45.43C294.26 45.43 306.01 49.29 315.36 57.02C324.72 64.75 330.81 75.01 333.64 87.82H328.09C330.27 75.01 336.25 64.75 346.04 57.02C355.83 49.29 367.9 45.43 382.26 45.43C400.54 45.43 414.89 50.84 425.34 61.66C435.78 72.47 441 87.26 441 106.03V212H410.98V113.65C410.98 100.84 407.71 91.02 401.19 84.17C394.88 77.11 386.29 73.58 375.41 73.58C367.79 73.58 361.05 75.34 355.17 78.88C349.52 82.19 345.06 87.04 341.8 93.45C338.53 99.85 336.9 107.36 336.9 115.97V212H306.55V113.32C306.55 100.51 303.4 90.8 297.09 84.17C290.78 77.33 282.19 73.91 271.31 73.91C263.69 73.91 256.95 75.67 251.08 79.21C245.42 82.52 240.96 87.38 237.7 93.78C234.43 99.96 232.8 107.36 232.8 115.97V212Z' />
<path d='M184.83 20.55C184.83 31.9 175.64 41.1 164.29 41.1C152.95 41.1 143.76 31.9 143.76 20.55C143.76 9.2 152.95 0 164.29 0C175.64 0 184.83 9.2 184.83 20.55Z' />
<path d='M179.43 212H149.16V49.76C153.76 51.91 158.88 53.12 164.29 53.12C169.7 53.12 174.83 51.91 179.43 49.76V212Z' />
</g>
</svg>
)
}
/**
* Icon-only Sim mark, no wordmark text. Same brandbook icon geometry as
* {@link SimLogoFull}'s icon, at its native square viewBox.
*/
export function SimLogoIcon({ className }: SimLogoProps) {
const gradientId = `sim-logo-icon-gradient-${useId()}`
return (
<svg
viewBox='0 0 222 222'
fill='none'
xmlns='http://www.w3.org/2000/svg'
className={cn('size-7', className)}
aria-label='Sim'
>
<defs>
<linearGradient
id={gradientId}
gradientUnits='userSpaceOnUse'
x1='129.434'
y1='129.266'
x2='185.629'
y2='185.33'
>
<stop offset='0' />
<stop offset='1' stopOpacity='0' />
</linearGradient>
</defs>
<path
clipRule='evenodd'
d='m107.822 93.7612c0 3.5869-1.419 7.0308-3.938 9.5668l-.361.364c-2.517 2.544-5.9375 3.966-9.4994 3.966h-80.5781c-7.42094 0-13.4455 6.06-13.4455 13.533v87.141c0 7.474 6.02456 13.534 13.4455 13.534h86.5167c7.4208 0 13.4378-6.06 13.4378-13.534v-81.587c0-3.326 1.31-6.517 3.647-8.871 2.33-2.347 5.499-3.667 8.802-3.667h81.928c7.421 0 13.437-6.059 13.437-13.533v-87.1407c0-7.47374-6.016-13.5333-13.437-13.5333h-86.517c-7.421 0-13.438 6.05956-13.438 13.5333zm26.256-75.2112h60.874c4.337 0 7.844 3.5393 7.844 7.9003v61.3071c0 4.3604-3.507 7.9003-7.844 7.9003h-60.874c-4.33 0-7.845-3.5399-7.845-7.9003v-61.3071c0-4.361 3.515-7.9003 7.845-7.9003z'
fill='#33C482'
fillRule='evenodd'
/>
<path
d='m207.878 129.57h-64.324c-7.798 0-14.12 6.367-14.12 14.221v63.993c0 7.854 6.322 14.221 14.12 14.221h64.324c7.799 0 14.121-6.367 14.121-14.221v-63.993c0-7.854-6.322-14.221-14.121-14.221z'
fill='#33C482'
/>
<path
d='m207.878 129.266h-64.324c-7.798 0-14.12 6.366-14.12 14.221v63.992c0 7.854 6.322 14.22 14.12 14.22h64.324c7.799 0 14.121-6.366 14.121-14.22v-63.992c0-7.855-6.322-14.221-14.121-14.221z'
fill={`url(#${gradientId})`}
fillOpacity='0.2'
/>
</svg>
)
}
/**
* Full Sim logo with icon and "Sim" text.
* Uses the same SVG source as the landing page navbar for exact visual alignment.
* The icon stays green (#33C482), text adapts to light/dark mode.
*/
export function SimLogoFull({ className }: SimLogoProps) {
const gradientId = `sim-logo-full-gradient-${useId()}`
return (
<svg
viewBox='0 0 71 22'
fill='none'
xmlns='http://www.w3.org/2000/svg'
className={cn('h-7 w-auto', className)}
aria-label='Sim'
>
<defs>
<linearGradient
id={gradientId}
gradientUnits='userSpaceOnUse'
x1='171.406'
y1='171.18'
x2='245.831'
y2='245.428'
>
<stop offset='0' />
<stop offset='1' stopOpacity='0' />
</linearGradient>
</defs>
{/* Green icon — scaled to match landing logo proportions */}
<g transform='scale(.07483)'>
<path
clipRule='evenodd'
d='m142.79 124.17c0 4.75-1.88 9.31-5.22 12.67l-.48.48c-3.33 3.37-7.86 5.25-12.58 5.25h-106.71c-9.83 0-17.81 8.03-17.81 17.92v115.41c0 9.9 7.98 17.92 17.81 17.92h114.58c9.83 0 17.8-8.03 17.8-17.92v-108.05c0-4.41 1.74-8.63 4.83-11.75 3.09-3.11 7.28-4.86 11.66-4.86h108.5c9.83 0 17.8-8.02 17.8-17.92v-115.41c0-9.9-7.97-17.92-17.8-17.92h-114.58c-9.83 0-17.8 8.03-17.8 17.92zm34.77-99.61h80.62c5.74 0 10.39 4.69 10.39 10.46v81.19c0 5.77-4.64 10.46-10.39 10.46h-80.62c-5.73 0-10.39-4.69-10.39-10.46v-81.19c0-5.78 4.66-10.46 10.39-10.46z'
fill='#33C482'
fillRule='evenodd'
/>
<path
d='m275.293 171.578h-85.187c-10.327 0-18.7 8.432-18.7 18.834v84.75c0 10.402 8.373 18.834 18.7 18.834h85.187c10.328 0 18.701-8.432 18.701-18.834v-84.75c0-10.402-8.373-18.834-18.701-18.834z'
fill='#33C482'
/>
<path
d='m275.293 171.18h-85.187c-10.327 0-18.7 8.432-18.7 18.834v84.749c0 10.402 8.373 18.833 18.7 18.833h85.187c10.328 0 18.701-8.431 18.701-18.833v-84.749c0-10.402-8.373-18.834-18.701-18.834z'
fill={`url(#${gradientId})`}
fillOpacity='0.2'
/>
</g>
{/* "Sim" text — adapts to light/dark mode */}
<g className='fill-[var(--text-primary)]'>
<path d='M31.57 15.85h2.59c0 .71.26 1.28.78 1.71.52.41 1.22.61 2.1.61.96 0 1.7-.18 2.21-.55.52-.39.78-.9.78-1.53 0-.46-.14-.85-.43-1.16-.27-.31-.77-.56-1.49-.75l-2.47-.58c-1.25-.31-2.17-.78-2.79-1.42-.59-.64-.89-1.48-.89-2.52 0-.87.22-1.62.66-2.26.46-.64 1.08-1.13 1.87-1.48.8-.35 1.72-.52 2.76-.52s1.93.18 2.67.55c.77.37 1.36.88 1.78 1.53.44.66.67 1.44.69 2.35h-2.59c-.02-.73-.26-1.3-.72-1.71-.46-.41-1.1-.61-1.93-.61-.84 0-1.49.18-1.95.55-.46.37-.69.87-.69 1.51 0 .95.69 1.59 2.07 1.94l2.47.61c1.19.27 2.08.71 2.67 1.33.59.6.89 1.42.89 2.46 0 .89-.24 1.67-.72 2.35-.48.66-1.14 1.17-1.98 1.53-.82.35-1.8.52-2.93.52-1.65 0-2.96-.41-3.94-1.22-.98-.81-1.47-1.89-1.47-3.24z' />
<path d='M44.51 19.96v-14.16c1.08.39 1.55.39 2.7 0v14.16zm1.32-15.09c-.48 0-.9-.17-1.26-.52-.34-.37-.52-.79-.52-1.27 0-.5.17-.93.52-1.27.36-.35.79-.52 1.26-.52.5 0 .92.17 1.26.52s.52.77.52 1.27c0 .48-.17.91-.52 1.27-.34.35-.77.52-1.26.52z' />
<path d='M51.98 19.96h-2.7v-14.16h2.41v2.39c.29-.79.84-1.46 1.61-1.98.79-.54 1.73-.81 2.85-.81 1.25 0 2.28.34 3.1 1.01.82.68 1.36 1.57 1.61 2.69h-.49c.19-1.12.72-2.02 1.58-2.69.86-.68 1.93-1.01 3.19-1.01 1.61 0 2.87.47 3.79 1.42.92.95 1.38 2.24 1.38 3.88v9.26h-2.64v-8.6c0-1.12-.29-1.98-.86-2.58-.56-.62-1.31-.93-2.27-.93-.67 0-1.26.15-1.78.46-.5.29-.89.71-1.18 1.27-.29.56-.43 1.22-.43 1.97v8.4h-2.67v-8.63c0-1.12-.28-1.97-.83-2.55-.56-.6-1.31-.9-2.27-.9-.67 0-1.26.15-1.78.46-.5.29-.89.71-1.18 1.27-.29.54-.43 1.19-.43 1.94z' />
</g>
</svg>
)
}
+66
View File
@@ -0,0 +1,66 @@
'use client'
import type { SVGProps } from 'react'
import { useTheme } from 'next-themes'
function SunIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
xmlns='http://www.w3.org/2000/svg'
width='16'
height='16'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='1.5'
strokeLinecap='round'
strokeLinejoin='round'
{...props}
>
<circle cx='12' cy='12' r='4' />
<path d='M12 2v2' />
<path d='M12 20v2' />
<path d='m4.93 4.93 1.41 1.41' />
<path d='m17.66 17.66 1.41 1.41' />
<path d='M2 12h2' />
<path d='M20 12h2' />
<path d='m6.34 17.66-1.41 1.41' />
<path d='m19.07 4.93-1.41 1.41' />
</svg>
)
}
function MoonIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
xmlns='http://www.w3.org/2000/svg'
width='16'
height='16'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='1.5'
strokeLinecap='round'
strokeLinejoin='round'
{...props}
>
<path d='M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z' />
</svg>
)
}
export function ThemeToggle() {
const { resolvedTheme, setTheme } = useTheme()
return (
<button
type='button'
onClick={() => setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')}
className='flex size-[30px] cursor-pointer items-center justify-center rounded-lg text-[var(--text-icon)] transition-colors hover:bg-[var(--surface-active)]'
aria-label='Toggle theme'
>
<SunIcon className='block size-[14px] dark:hidden' />
<MoonIcon className='hidden size-[14px] dark:block' />
</button>
)
}
@@ -0,0 +1,73 @@
'use client'
import { useEffect, useState } from 'react'
import { cn } from '@/lib/utils'
/** Parse a chapter timestamp ("M:SS" or "H:MM:SS") into seconds. */
function parseTime(time: string): number {
const parts = time.split(':').map(Number)
if (parts.some(Number.isNaN)) return 0
return parts.reduce((acc, n) => acc * 60 + n, 0)
}
interface Chapter {
/** Chapter label. */
title: string
/** Timestamp, e.g. "0:45". */
time?: string
}
interface VideoChaptersProps {
/** Panel heading. Defaults to "Chapters". */
title?: string
chapters: Chapter[]
className?: string
}
/**
* Right-rail list of the current video's chapters — flat and borderless to
* match the docs' "On this page" TOC (small muted label, hover-highlighted
* rows). Rows are skip-to controls; they activate once the lesson's video is
* recorded.
*/
export function VideoChapters({ title = 'Chapters', chapters, className }: VideoChaptersProps) {
// Chapters only seek when a VideoPlaceholder with a real video is on the page.
// Handshake so the rows stay inert (not falsely clickable) on video-less lessons.
const [hasVideo, setHasVideo] = useState(false)
useEffect(() => {
const onReady = () => setHasVideo(true)
window.addEventListener('academy:video-ready', onReady)
window.dispatchEvent(new Event('academy:video-query'))
return () => window.removeEventListener('academy:video-ready', onReady)
}, [])
return (
<aside className={cn('not-prose', className)}>
<p className='mb-2 px-2.5 font-medium text-[0.8125rem] text-[var(--text-muted)]'>{title}</p>
<ul className='m-0 flex list-none flex-col gap-0.5 p-0'>
{chapters.map((chapter) => (
<li key={chapter.title}>
<button
type='button'
disabled={!hasVideo || chapter.time == null}
onClick={() => {
if (chapter.time == null) return
window.dispatchEvent(
new CustomEvent('academy:seek', { detail: { time: parseTime(chapter.time) } })
)
}}
className='flex w-full cursor-pointer items-baseline gap-3 rounded-lg px-2.5 py-2 text-left text-[var(--text-secondary)] text-sm transition-colors hover:bg-[var(--surface-active)] disabled:cursor-default disabled:hover:bg-transparent'
>
<span className='min-w-0 flex-1 break-words'>{chapter.title}</span>
{chapter.time && (
<span className='shrink-0 text-[var(--text-muted)] text-xs tabular-nums'>
{chapter.time}
</span>
)}
</button>
</li>
))}
</ul>
</aside>
)
}
@@ -0,0 +1,206 @@
'use client'
import { useEffect, useRef, useState } from 'react'
import { cn, getAssetUrl } from '@/lib/utils'
interface VideoPlaceholderProps {
/** Large title shown on the hero. */
title?: string
/** Small italic eyebrow above the title, e.g. a module name. */
eyebrow?: string
/** Pill in the top-right corner. Defaults to "Coming soon" (shown only until a video is set). */
label?: string
/**
* Self-hosted video source. Accepts an absolute URL, a root-relative path
* (`/static/...`), or a bare asset name resolved through the Blob CDN. When
* set, the play button loads the video; otherwise the card is "coming soon".
*/
src?: string
className?: string
}
/** Resolve a video source: pass absolute/root-relative through, send bare names to the Blob CDN. */
function resolveVideoSrc(src: string): string {
if (/^https?:\/\//.test(src) || src.startsWith('/')) return src
return getAssetUrl(src)
}
/** The sim logotype, drawn with currentColor so the theme can tint it. */
function SimWordmark({ className }: { className?: string }) {
return (
<svg viewBox='0 0 816 392' fill='currentColor' aria-label='Sim' className={className}>
<path d='M 0 297.507 L 54.609 297.507 C 54.609 312.642 60.07 324.71 70.992 333.709 C 81.914 342.299 96.679 346.594 115.287 346.594 C 135.512 346.594 151.086 342.707 162.008 334.936 C 172.93 326.754 178.391 315.915 178.391 302.415 C 178.391 292.598 175.357 284.417 169.289 277.871 C 163.627 271.326 153.109 266.009 137.737 261.918 L 85.555 249.646 C 59.261 243.102 39.642 233.08 26.698 219.581 C 14.158 206.082 7.888 188.287 7.888 166.198 C 7.888 147.79 12.54 131.837 21.844 118.338 C 31.552 104.838 44.699 94.408 61.284 87.045 C 78.274 79.682 97.69 76 119.534 76 C 141.378 76 160.187 79.886 175.964 87.658 C 192.144 95.43 204.684 106.271 213.584 120.179 C 222.888 134.086 227.742 150.654 228.146 169.88 L 173.536 169.88 C 173.132 154.335 168.076 142.267 158.368 133.678 C 148.659 125.087 135.108 120.792 117.714 120.792 C 99.915 120.792 86.162 124.678 76.453 132.451 C 66.745 140.223 61.891 150.858 61.891 164.357 C 61.891 184.402 76.453 198.105 105.579 205.468 L 157.76 218.354 C 182.841 224.08 201.651 233.489 214.191 246.579 C 226.73 259.26 233 276.644 233 298.734 C 233 317.55 227.943 334.118 217.831 348.435 C 207.718 362.343 193.762 373.183 175.964 380.955 C 158.57 388.318 137.939 392 114.073 392 C 79.285 392 51.576 383.409 30.945 366.229 C 10.315 349.048 0 326.141 0 297.507 Z' />
<path d='M 430.759 392 L 374 392 L 374 92 L 424.721 92 L 424.721 143.095 C 430.76 126.357 442.433 112.167 458.535 101.145 C 475.039 89.715 494.966 84 518.314 84 C 544.48 84 566.217 91.144 583.527 105.431 C 600.837 119.719 612.108 138.701 617.342 162.378 L 607.076 162.378 C 611.102 138.701 622.172 119.719 640.287 105.431 C 658.401 91.144 680.743 84 707.311 84 C 741.126 84 767.694 94.001 787.017 114.004 C 806.339 134.006 816 161.357 816 196.056 L 816 392 L 760.448 392 L 760.448 210.139 C 760.448 186.462 754.41 168.297 742.333 155.643 C 730.66 142.579 714.758 136.048 694.631 136.048 C 680.542 136.048 668.062 139.314 657.194 145.845 C 646.728 151.968 638.475 160.949 632.437 172.787 C 626.398 184.625 623.38 198.505 623.38 214.425 L 623.38 392 L 567.223 392 L 567.223 209.527 C 567.223 185.85 561.387 167.888 549.713 155.643 C 538.039 142.988 522.138 136.66 502.01 136.66 C 487.921 136.66 475.442 139.926 464.574 146.457 C 454.108 152.58 445.855 161.562 439.817 173.4 C 433.778 184.83 430.759 198.505 430.759 214.425 L 430.759 392 Z' />
<path d='M 342 38 C 342 58.987 324.987 76 304 76 C 283.013 76 266 58.987 266 38 C 266 17.013 283.013 0 304 0 C 324.987 0 342 17.013 342 38 Z' />
<path d='M 332 392 L 276 392 L 276 92 C 284.5 95.988 293.99 98.218 304 98.218 C 314.01 98.218 323.5 95.988 332 92 L 332 392 Z' />
</svg>
)
}
/**
* A 16:9 lesson hero used across the Academy. Always shows the design-system
* video card (title, blueprint grid, theme-aware dark/light). When a `src` is
* provided the play button loads the self-hosted video inline; otherwise the
* card reads "Coming soon" and the play button is muted.
*/
export function VideoPlaceholder({
title,
eyebrow,
label = 'Coming soon',
src,
className,
}: VideoPlaceholderProps) {
const hasVideo = Boolean(src)
const [playing, setPlaying] = useState(false)
const videoRef = useRef<HTMLVideoElement>(null)
const pendingSeek = useRef<number | null>(null)
// Chapter rows (VideoChapters) dispatch `academy:seek` with a time in seconds.
// Start the video if it isn't playing yet, then jump there. We also announce
// that a video exists (and answer a chapters-side query) so the chapter rows
// only become interactive when there's actually something to seek.
useEffect(() => {
if (!src) return
const onSeek = (e: Event) => {
const time = (e as CustomEvent<{ time: number }>).detail?.time
if (typeof time !== 'number') return
const video = videoRef.current
if (video) {
video.currentTime = time
void video.play()
} else {
pendingSeek.current = time
setPlaying(true)
}
}
const announce = () => window.dispatchEvent(new Event('academy:video-ready'))
window.addEventListener('academy:seek', onSeek)
window.addEventListener('academy:video-query', announce)
announce()
return () => {
window.removeEventListener('academy:seek', onSeek)
window.removeEventListener('academy:video-query', announce)
}
}, [src])
if (playing && src) {
return (
<div
className={cn(
'not-prose my-6 aspect-video w-full overflow-hidden rounded-[20px] bg-black',
className
)}
>
{/* biome-ignore lint/a11y/useMediaCaption: lesson videos have no caption track yet */}
<video
ref={videoRef}
src={resolveVideoSrc(src)}
title={title ?? 'Lesson video'}
controls
autoPlay
playsInline
onLoadedMetadata={() => {
if (pendingSeek.current != null && videoRef.current) {
videoRef.current.currentTime = pendingSeek.current
void videoRef.current.play()
pendingSeek.current = null
}
}}
className='h-full w-full border-0'
/>
</div>
)
}
return (
<div
className={cn(
'not-prose group relative my-6 aspect-video w-full select-none overflow-hidden rounded-[20px] font-season transition-transform duration-200 [container-type:inline-size]',
'shadow-[inset_0_0_0_1px_#E6E6E6] [background:radial-gradient(130%_130%_at_50%_14%,#ffffff_0%,#f6f6f6_55%,#ececec_100%)]',
'dark:shadow-none dark:[background:radial-gradient(130%_130%_at_50%_18%,#1c1c1c_0%,#121212_45%,#0a0a0a_100%)]',
className
)}
>
{/* Blueprint grid — faint, fading to atmosphere at the edges */}
<div
aria-hidden
className='pointer-events-none absolute inset-0 [background-image:linear-gradient(rgba(18,18,18,0.05)_1px,transparent_1px),linear-gradient(90deg,rgba(18,18,18,0.05)_1px,transparent_1px)] [background-size:64px_64px] [mask-image:radial-gradient(120%_90%_at_50%_35%,#000_30%,transparent_100%)] dark:[background-image:linear-gradient(rgba(255,255,255,0.06)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,0.06)_1px,transparent_1px)]'
/>
{/* Corner plus-marks, 20px inset */}
{['top-5 left-5', 'top-5 right-5', 'bottom-5 left-5', 'right-5 bottom-5'].map((pos) => (
<span
key={pos}
aria-hidden
className={cn(
'absolute font-mono text-[20px] text-[rgba(18,18,18,0.22)] leading-none dark:text-[rgba(255,255,255,0.28)]',
pos
)}
>
+
</span>
))}
{/* Top-right status pill — only until a video is wired up */}
{!hasVideo && (
<span className='absolute top-6 right-6 z-10 inline-flex items-center gap-2 rounded-full border border-[var(--border-1)] bg-[var(--surface-2)] px-4 py-2 font-medium text-[12px] text-[var(--text-secondary)] uppercase tracking-[0.14em] md:top-8 md:right-8 dark:bg-[var(--surface-1)]'>
<span className='size-1.5 rounded-full bg-[var(--brand-accent)]' />
{label}
</span>
)}
{/* Heading: eyebrow + title, bottom-left (design: left:40 bottom:40) */}
<div className='absolute bottom-10 left-10 z-10 max-w-[80%]'>
{eyebrow && (
<span className='mb-[14px] block font-normal text-[#5F5F5F] text-[clamp(15px,2cqi,22px)] italic tracking-[-0.01em] dark:text-[#B4B4B4]'>
{eyebrow}
</span>
)}
{title && (
<span className='block font-semibold text-[#121212] text-[clamp(2.5rem,9.5cqi,5.5rem)] leading-[0.96] tracking-[-0.035em] dark:text-[#F8F8F8]'>
{title}
</span>
)}
</div>
{/* Wordmark, bottom-right (design: right:40 bottom:40, svg height 22) */}
<span className='absolute right-10 bottom-10 z-10 text-[#121212] dark:text-white/90'>
<SimWordmark className='block h-[22px] w-auto' />
</span>
{/* Centered play button — active when a video is wired, muted otherwise */}
<div className='absolute inset-0 z-10 grid place-items-center'>
{hasVideo ? (
<button
type='button'
onClick={() => setPlaying(true)}
aria-label={title ? `Play ${title}` : 'Play video'}
className='grid h-12 w-16 cursor-pointer place-items-center rounded-[14px] bg-[rgba(255,255,255,0.78)] shadow-[0_1px_3px_rgba(18,18,18,0.12),inset_0_0_0_1px_#E6E6E6] backdrop-blur-[4px] transition-transform duration-200 hover:scale-105 active:scale-95 dark:bg-[rgba(10,10,10,0.72)] dark:shadow-none'
>
<svg
width='18'
height='20'
viewBox='0 0 18 20'
aria-hidden
className='translate-x-[1px] text-[#121212] dark:text-white'
>
<path d='M0 0l18 10L0 20z' fill='currentColor' />
</svg>
</button>
) : (
<span className='grid h-12 w-16 place-items-center rounded-[14px] bg-[rgba(255,255,255,0.78)] opacity-60 shadow-[0_1px_3px_rgba(18,18,18,0.12),inset_0_0_0_1px_#E6E6E6] backdrop-blur-[4px] dark:bg-[rgba(10,10,10,0.72)] dark:shadow-none'>
<svg
width='18'
height='20'
viewBox='0 0 18 20'
aria-hidden
className='translate-x-[1px] text-[#121212] dark:text-white'
>
<path d='M0 0l18 10L0 20z' fill='currentColor' />
</svg>
</span>
)}
</div>
</div>
)
}
+83
View File
@@ -0,0 +1,83 @@
'use client'
import { useRef, useState } from 'react'
import { cn, getAssetUrl } from '@/lib/utils'
import { Lightbox } from './lightbox'
interface VideoProps {
src: string
className?: string
autoPlay?: boolean
loop?: boolean
muted?: boolean
playsInline?: boolean
enableLightbox?: boolean
width?: number
height?: number
}
export function Video({
src,
className = 'w-full rounded-xl border border-[var(--border)] overflow-hidden outline-none focus:outline-none',
autoPlay = true,
loop = true,
muted = true,
playsInline = true,
enableLightbox = true,
width,
height,
}: VideoProps) {
const videoRef = useRef<HTMLVideoElement>(null)
const startTimeRef = useRef(0)
const [isLightboxOpen, setIsLightboxOpen] = useState(false)
const openLightbox = () => {
startTimeRef.current = videoRef.current?.currentTime ?? 0
setIsLightboxOpen(true)
}
const video = (
<video
ref={videoRef}
autoPlay={autoPlay}
loop={loop}
muted={muted}
playsInline={playsInline}
width={width}
height={height}
className={cn(
className,
enableLightbox && 'cursor-pointer transition-opacity group-hover:opacity-[0.97]'
)}
src={getAssetUrl(src)}
/>
)
return (
<>
{enableLightbox ? (
<button
type='button'
onClick={openLightbox}
aria-label={`Open ${src} in media viewer`}
className='group contents'
>
{video}
</button>
) : (
video
)}
{enableLightbox && (
<Lightbox
isOpen={isLightboxOpen}
onClose={() => setIsLightboxOpen(false)}
src={src}
alt={`Video: ${src}`}
type='video'
startTime={startTimeRef.current}
/>
)}
</>
)
}
@@ -0,0 +1,35 @@
import { cn } from '@/lib/utils'
interface LearnItem {
title: string
body: string
}
interface WhatYouWillLearnProps {
items: LearnItem[]
className?: string
}
/**
* "What you will learn" — a flat callout matching the docs' flat/divider
* language. A quiet muted label (like the TOC heading) sits above the
* takeaways; dividers fall only between items, so the label reads as a marker
* rather than an underlined heading and never competes with the item titles.
*/
export function WhatYouWillLearn({ items, className }: WhatYouWillLearnProps) {
return (
<div className={cn('not-prose', className)}>
<p className='mb-3 font-medium text-[0.8125rem] text-[var(--text-muted)]'>
What you will learn
</p>
<div className='divide-y divide-[var(--border)]'>
{items.map((item) => (
<div key={item.title} className='py-3.5 first:pt-0 last:pb-0'>
<p className='mb-1 font-medium text-[var(--text-primary)] text-sm'>{item.title}</p>
<p className='m-0 text-[var(--text-secondary)] text-sm leading-relaxed'>{item.body}</p>
</div>
))}
</div>
</div>
)
}
@@ -0,0 +1,393 @@
import type { PreviewWorkflow } from './workflow-data'
/**
* Workflows shown in the academy videos, reproduced block-for-block so each
* page's written supplement shows the same machine the video builds. Rows and
* operation labels match the videos (which match the block registry).
*/
/** files/intro + files/object — the invoice intake machine. */
export const AV_INVOICE_INTAKE_WORKFLOW: PreviewWorkflow = {
id: 'av-invoice-intake',
name: 'Invoice intake',
blocks: [
{
id: 'gmailtrigger',
name: 'Gmail Email Trigger',
type: 'gmail',
bgColor: '#E0E0E0',
position: { x: 0, y: 0 },
hideTargetHandle: true,
rows: [{ title: 'Include Attachments', value: 'Enabled' }],
},
{
id: 'file',
name: 'File',
type: 'file',
bgColor: '#40916C',
position: { x: 340, y: 0 },
rows: [
{ title: 'Operation', value: 'Read' },
{ title: 'Files', value: '<gmailtrigger.attachments[0]>' },
],
},
{
id: 'agent',
name: 'Agent',
type: 'agent',
bgColor: '#33C482',
position: { x: 680, y: 0 },
rows: [
{ title: 'Messages', value: 'Extract the invoice fields' },
{ title: 'Model', value: 'claude-sonnet-4-6' },
{ title: 'Files', value: '<file.files[0]>' },
],
},
{
id: 'supabase',
name: 'Supabase',
type: 'supabase',
bgColor: '#1C1C1C',
position: { x: 1020, y: 0 },
rows: [
{ title: 'Operation', value: 'Create a Row' },
{ title: 'Table', value: 'invoices' },
{ title: 'Data', value: '<agent.content>' },
],
},
],
edges: [
{ id: 'trigger-file', source: 'gmailtrigger', target: 'file' },
{ id: 'file-agent', source: 'file', target: 'agent' },
{ id: 'agent-supabase', source: 'agent', target: 'supabase' },
],
}
/** agents/block — the Qualify agent the camera rides through. */
export const AV_QUALIFY_WORKFLOW: PreviewWorkflow = {
id: 'av-qualify',
name: 'Qualify a lead',
blocks: [
{
id: 'start',
name: 'Start',
type: 'start_trigger',
bgColor: '#2FB3FF',
position: { x: 0, y: 0 },
hideTargetHandle: true,
rows: [{ title: 'Input', value: 'Lead' }],
},
{
id: 'qualify',
name: 'Qualify',
type: 'agent',
bgColor: '#33C482',
position: { x: 340, y: 0 },
rows: [
{ title: 'Messages', value: 'Qualify this lead: <start.input>' },
{ title: 'Model', value: 'claude-sonnet-4-6' },
],
},
{
id: 'response',
name: 'Response',
type: 'response',
bgColor: '#2F55FF',
position: { x: 680, y: 0 },
rows: [{ title: 'Data', value: '<qualify.content>' }],
},
],
edges: [
{ id: 'start-qualify', source: 'start', target: 'qualify' },
{ id: 'qualify-response', source: 'qualify', target: 'response' },
],
}
/** agents/memory — the Support agent with Memory set to Conversation. */
export const AV_MEMORY_WORKFLOW: PreviewWorkflow = {
id: 'av-memory',
name: 'Support agent with memory',
blocks: [
{
id: 'start',
name: 'Start',
type: 'start_trigger',
bgColor: '#2FB3FF',
position: { x: 0, y: 0 },
hideTargetHandle: true,
rows: [{ title: 'Input', value: 'Customer message' }],
},
{
id: 'support',
name: 'Support',
type: 'agent',
bgColor: '#33C482',
position: { x: 340, y: 0 },
rows: [
{ title: 'Messages', value: '<start.input>' },
{ title: 'Model', value: 'claude-sonnet-4-6' },
{ title: 'Memory', value: 'Conversation · user-123' },
],
},
],
edges: [{ id: 'start-support', source: 'start', target: 'support' }],
}
/** tables/operations — the Table block with a filtered query. */
export const AV_TABLE_OPS_WORKFLOW: PreviewWorkflow = {
id: 'av-table-ops',
name: 'Query the tickets table',
blocks: [
{
id: 'start',
name: 'Start',
type: 'start_trigger',
bgColor: '#2FB3FF',
position: { x: 0, y: 0 },
hideTargetHandle: true,
rows: [{ title: 'Input', value: 'Run' }],
},
{
id: 'table',
name: 'Table 1',
type: 'table',
bgColor: '#10B981',
position: { x: 340, y: 0 },
rows: [
{ title: 'Operation', value: 'Query Rows' },
{ title: 'Table', value: 'tickets' },
{ title: 'Filter Conditions', value: 'priority equals high' },
],
},
],
edges: [{ id: 'start-table', source: 'start', target: 'table' }],
}
/** agents/tool-calling — the Qualify agent with research tools attached. */
export const AV_TOOLS_WORKFLOW: PreviewWorkflow = {
id: 'av-tools',
name: 'Qualify with tools',
blocks: [
{
id: 'start',
name: 'Start',
type: 'start_trigger',
bgColor: '#2FB3FF',
position: { x: 0, y: 0 },
hideTargetHandle: true,
rows: [{ title: 'Input', value: 'Lead' }],
},
{
id: 'qualify',
name: 'Qualify',
type: 'agent',
bgColor: '#33C482',
position: { x: 340, y: 0 },
rows: [
{ title: 'Messages', value: 'Qualify this lead: <start.input>' },
{ title: 'Model', value: 'claude-sonnet-4-6' },
],
tools: [
{ type: 'exa', name: 'Search', bgColor: '#1F40ED' },
{ type: 'github', name: 'GitHub', bgColor: '#181717' },
{ type: 'hubspot', name: 'CRM', bgColor: '#FF7A59' },
],
},
{
id: 'response',
name: 'Response',
type: 'response',
bgColor: '#2F55FF',
position: { x: 680, y: 0 },
rows: [{ title: 'Data', value: '<qualify.content>' }],
},
],
edges: [
{ id: 'start-qualify', source: 'start', target: 'qualify' },
{ id: 'qualify-response', source: 'qualify', target: 'response' },
],
}
/** agents/skills — the same agent with a skill attached. */
export const AV_SKILLS_WORKFLOW: PreviewWorkflow = {
id: 'av-skills',
name: 'Qualify with a skill',
blocks: [
{
id: 'start',
name: 'Start',
type: 'start_trigger',
bgColor: '#2FB3FF',
position: { x: 0, y: 0 },
hideTargetHandle: true,
rows: [{ title: 'Input', value: 'Lead' }],
},
{
id: 'qualify',
name: 'Qualify',
type: 'agent',
bgColor: '#33C482',
position: { x: 340, y: 0 },
rows: [
{ title: 'Messages', value: 'Qualify this lead: <start.input>' },
{ title: 'Model', value: 'claude-sonnet-4-6' },
{ title: 'Skills', value: 'answer-with-citations' },
],
tools: [{ type: 'exa', name: 'Search', bgColor: '#1F40ED' }],
},
],
edges: [{ id: 'start-qualify', source: 'start', target: 'qualify' }],
}
/** chat/intro — the support-desk workflow the chat operates. */
export const AV_SUPPORT_DESK_WORKFLOW: PreviewWorkflow = {
id: 'av-support-desk',
name: 'support-desk',
blocks: [
{
id: 'start',
name: 'Start',
type: 'start_trigger',
bgColor: '#2FB3FF',
position: { x: 0, y: 60 },
hideTargetHandle: true,
rows: [{ title: 'Input', value: 'Ticket' }],
},
{
id: 'triage',
name: 'Triage',
type: 'agent',
bgColor: '#33C482',
position: { x: 320, y: 60 },
rows: [
{ title: 'Messages', value: '<start.input>' },
{ title: 'Model', value: 'claude-sonnet-4-6' },
],
tools: [{ type: 'knowledge', name: 'Help Center', bgColor: '#00B0B0' }],
},
{
id: 'condition',
name: 'Urgent?',
type: 'condition',
bgColor: '#FF752F',
position: { x: 660, y: 60 },
rows: [],
branches: [
{ id: 'condition-if', label: 'If', value: '<triage.urgent>' },
{ id: 'condition-else', label: 'else' },
],
},
{
id: 'escalate',
name: 'Escalate',
type: 'slack',
bgColor: '#611F69',
position: { x: 1000, y: -40 },
rows: [{ title: 'Channel', value: '#support-urgent' }],
},
{
id: 'reply',
name: 'Reply',
type: 'agent',
bgColor: '#33C482',
position: { x: 1000, y: 160 },
rows: [{ title: 'Messages', value: 'Draft the reply' }],
},
{
id: 'log',
name: 'Log',
type: 'table',
bgColor: '#10B981',
position: { x: 1340, y: 60 },
rows: [
{ title: 'Operation', value: 'Insert Row' },
{ title: 'Table', value: 'tickets' },
],
},
],
edges: [
{ id: 'start-triage', source: 'start', target: 'triage' },
{ id: 'triage-condition', source: 'triage', target: 'condition' },
{
id: 'condition-escalate',
source: 'condition',
target: 'escalate',
sourceHandle: 'condition-if',
},
{ id: 'condition-reply', source: 'condition', target: 'reply', sourceHandle: 'condition-else' },
{ id: 'escalate-log', source: 'escalate', target: 'log' },
{ id: 'reply-log', source: 'reply', target: 'log' },
],
}
/** chat/building — the content-agent the chat builds: candidates drafted in
* parallel, media generated, an evaluator scoring, results kept. */
export const AV_CONTENT_AGENT_WORKFLOW: PreviewWorkflow = {
id: 'av-content-agent',
name: 'content-agent',
blocks: [
{
id: 'start',
name: 'Start',
type: 'start_trigger',
bgColor: '#2FB3FF',
position: { x: 0, y: 95 },
hideTargetHandle: true,
rows: [{ title: 'Input', value: 'Idea' }],
},
{
id: 'candidates',
name: 'Candidates',
type: 'parallel',
bgColor: '#1D1C1A',
position: { x: 320, y: 30 },
size: { width: 430, height: 170 },
rows: [],
},
{
id: 'writer',
name: 'Writer',
type: 'agent',
bgColor: '#33C482',
position: { x: 150, y: 62 },
parentId: 'candidates',
rows: [
{ title: 'Messages', value: '<start.input>' },
{ title: 'Model', value: 'claude-sonnet-4-6' },
],
},
{
id: 'evaluator',
name: 'Evaluator',
type: 'evaluator',
bgColor: '#8B5CF6',
position: { x: 840, y: 95 },
rows: [
{ title: 'Metrics', value: 'Voice · Hook' },
{ title: 'Content', value: '<writer.content>' },
],
},
{
id: 'scores',
name: 'Scores',
type: 'table',
bgColor: '#10B981',
position: { x: 1180, y: 95 },
rows: [
{ title: 'Operation', value: 'Insert Row' },
{ title: 'Table', value: 'scores' },
],
},
],
edges: [
{ id: 'start-candidates', source: 'start', target: 'candidates' },
{
id: 'candidates-writer',
source: 'candidates',
target: 'writer',
sourceHandle: 'parallel-start-source',
},
{ id: 'candidates-evaluator', source: 'candidates', target: 'evaluator' },
{ id: 'evaluator-scores', source: 'evaluator', target: 'scores' },
],
}
@@ -0,0 +1,372 @@
import type { PreviewWorkflow } from '@/components/workflow-preview/workflow-data'
/**
* Single-block preview workflows for the block reference heroes — one per block,
* authored to match exactly what the builder canvas shows. Source of truth for
* `<BlockPreview type="...">`.
*
* Each entry is a one-block {@link PreviewWorkflow} rendered through the shared
* {@link WorkflowBlockView} (via `DocsBlockNode`), so the hero stays pixel-faithful
* to the canvas. Authoring notes:
*
* - `rows` are the visible sub-block rows; use `'-'` for an empty/unset field (the
* canvas shows a dash), or a representative value where the field has a default.
* - `branches` render one output handle per entry (Condition's if/else-if/else,
* Router's routes). The View regenerates handle topology, so the label doubles as
* the branch id.
* - `hideTargetHandle: true` for triggers (entry points — no input). The View derives
* the default target/source handles and the bottom `Error` row from this gate, so
* there is no separate `showError`/`hideSourceHandle` flag.
* - `bgColor` is the resolved hex; the Agent uses Sim green `#33C482` (`var(--brand)`).
*/
export const BLOCK_DISPLAY_WORKFLOWS: Record<string, PreviewWorkflow> = {
agent: {
id: 'agent',
name: 'Agent',
blocks: [
{
id: 'agent',
name: 'Agent',
type: 'agent',
bgColor: '#33C482',
position: { x: 0, y: 0 },
rows: [
{ title: 'Messages', value: '-' },
{ title: 'Model', value: 'claude-sonnet-4-6' },
{ title: 'Files', value: '-' },
{ title: 'Tools', value: '-' },
{ title: 'Skills', value: '-' },
{ title: 'Memory', value: 'None' },
{ title: 'Temperature', value: '0.7' },
{ title: 'Response Format', value: '-' },
],
},
],
edges: [],
},
api: {
id: 'api',
name: 'API',
blocks: [
{
id: 'api',
name: 'API',
type: 'api',
bgColor: '#2F55FF',
position: { x: 0, y: 0 },
rows: [
{ title: 'URL', value: '-' },
{ title: 'Method', value: 'GET' },
{ title: 'Query Params', value: '-' },
{ title: 'Headers', value: '-' },
{ title: 'Body', value: '-' },
],
},
],
edges: [],
},
condition: {
id: 'condition',
name: 'Condition',
blocks: [
{
id: 'condition',
name: 'Condition',
type: 'condition',
bgColor: '#FF752F',
position: { x: 0, y: 0 },
rows: [],
branches: [
{ id: 'if', label: 'if' },
{ id: 'else if', label: 'else if' },
{ id: 'else', label: 'else' },
],
},
],
edges: [],
},
credential: {
id: 'credential',
name: 'Credential',
blocks: [
{
id: 'credential',
name: 'Credential',
type: 'credential',
bgColor: '#6366F1',
position: { x: 0, y: 0 },
rows: [
{ title: 'Operation', value: 'Select Credential' },
{ title: 'Credential', value: '-' },
],
},
],
edges: [],
},
evaluator: {
id: 'evaluator',
name: 'Evaluator',
blocks: [
{
id: 'evaluator',
name: 'Evaluator',
type: 'evaluator',
bgColor: '#4D5FFF',
position: { x: 0, y: 0 },
rows: [
{ title: 'Evaluation Metrics', value: '-' },
{ title: 'Content', value: '-' },
{ title: 'Model', value: 'claude-sonnet-4-6' },
],
},
],
edges: [],
},
function: {
id: 'function',
name: 'Function',
blocks: [
{
id: 'function',
name: 'Function',
type: 'function',
bgColor: '#FF402F',
position: { x: 0, y: 0 },
rows: [
{ title: 'Language', value: 'JavaScript' },
{ title: 'Code', value: '-' },
],
},
],
edges: [],
},
guardrails: {
id: 'guardrails',
name: 'Guardrails',
blocks: [
{
id: 'guardrails',
name: 'Guardrails',
type: 'guardrails',
bgColor: '#3D642D',
position: { x: 0, y: 0 },
rows: [
{ title: 'Content to Validate', value: '-' },
{ title: 'Validation Type', value: 'Valid JSON' },
],
},
],
edges: [],
},
response: {
id: 'response',
name: 'Response',
blocks: [
{
id: 'response',
name: 'Response',
type: 'response',
bgColor: '#2F55FF',
position: { x: 0, y: 0 },
rows: [
{ title: 'Response Data Mode', value: 'Builder' },
{ title: 'Response Structure', value: '-' },
{ title: 'Status Code', value: '-' },
{ title: 'Response Headers', value: '-' },
],
},
],
edges: [],
},
router: {
id: 'router',
name: 'Router',
blocks: [
{
id: 'router',
name: 'Router',
type: 'router',
bgColor: '#28C43F',
position: { x: 0, y: 0 },
rows: [
{ title: 'Context', value: '-' },
{ title: 'Model', value: 'claude-sonnet-4-6' },
],
branches: [{ id: 'route 1', label: 'route 1' }],
},
],
edges: [],
},
variables: {
id: 'variables',
name: 'Variables',
blocks: [
{
id: 'variables',
name: 'Variables',
type: 'variables',
bgColor: '#8B5CF6',
position: { x: 0, y: 0 },
rows: [{ title: 'Variable Assignments', value: '-' }],
},
],
edges: [],
},
wait: {
id: 'wait',
name: 'Wait',
blocks: [
{
id: 'wait',
name: 'Wait',
type: 'wait',
bgColor: '#F59E0B',
position: { x: 0, y: 0 },
rows: [
{ title: 'Wait Amount', value: '10' },
{ title: 'Unit', value: 'Seconds' },
{ title: 'Async', value: '-' },
],
},
],
edges: [],
},
webhook: {
id: 'webhook',
name: 'Webhook',
blocks: [
{
id: 'webhook',
name: 'Webhook',
type: 'webhook',
bgColor: '#10B981',
position: { x: 0, y: 0 },
rows: [
{ title: 'Webhook URL', value: '-' },
{ title: 'Payload', value: '-' },
{ title: 'Signing Secret', value: '-' },
{ title: 'Additional Headers', value: '-' },
],
},
],
edges: [],
},
workflow: {
id: 'workflow',
name: 'Workflow',
blocks: [
{
id: 'workflow',
name: 'Workflow',
type: 'workflow',
bgColor: '#6366F1',
position: { x: 0, y: 0 },
rows: [
{ title: 'Select Workflow', value: '-' },
{ title: 'Input Variable', value: '-' },
],
},
],
edges: [],
},
human_in_the_loop: {
id: 'human_in_the_loop',
name: 'Human in the Loop',
blocks: [
{
id: 'human_in_the_loop',
name: 'Human in the Loop',
type: 'human_in_the_loop',
bgColor: '#10B981',
position: { x: 0, y: 0 },
rows: [
{ title: 'Display Data', value: '-' },
{ title: 'Notification (Send URL)', value: '-' },
{ title: 'Resume Form', value: '-' },
],
},
],
edges: [],
},
schedule: {
id: 'schedule',
name: 'Schedule',
blocks: [
{
id: 'schedule',
name: 'Schedule',
type: 'schedule',
bgColor: '#6366F1',
position: { x: 0, y: 0 },
hideTargetHandle: true,
rows: [
{ title: 'Run frequency', value: 'Daily' },
{ title: 'Time', value: '-' },
{ title: 'Timezone', value: '-' },
],
},
],
edges: [],
},
rss: {
id: 'rss',
name: 'RSS Feed',
blocks: [
{
id: 'rss',
name: 'RSS Feed',
type: 'rss',
bgColor: '#F97316',
position: { x: 0, y: 0 },
hideTargetHandle: true,
rows: [{ title: 'Feed URL', value: '-' }],
},
],
edges: [],
},
webhook_trigger: {
id: 'webhook_trigger',
name: 'Webhook',
blocks: [
{
id: 'webhook_trigger',
name: 'Webhook',
type: 'webhook',
bgColor: '#10B981',
position: { x: 0, y: 0 },
hideTargetHandle: true,
rows: [
{ title: 'Webhook URL', value: '-' },
{ title: 'Require Authentication', value: 'On' },
{ title: 'Authentication Token', value: '-' },
{ title: 'Secret Header Name (Optional)', value: '-' },
{ title: 'Deduplication Field (Optional)', value: '-' },
{ title: 'Acknowledgement', value: 'Default' },
{ title: 'Verify Test Events', value: '-' },
{ title: 'Input Format', value: '-' },
],
},
],
edges: [],
},
table: {
id: 'table',
name: 'Table',
blocks: [
{
id: 'table',
name: 'Table',
type: 'table',
bgColor: '#10B981',
position: { x: 0, y: 0 },
hideTargetHandle: true,
rows: [
{ title: 'Table', value: '-' },
{ title: 'Event type', value: 'Row updated' },
{ title: 'Watch columns', value: '-' },
],
},
],
edges: [],
},
}
@@ -0,0 +1,113 @@
import type { ComponentType, SVGProps } from 'react'
import { Clock, Database, Layers, Repeat, Table } from 'lucide-react'
import {
ApiIcon,
ChartBarIcon,
CodeIcon,
ConditionalIcon,
ConnectIcon,
CredentialIcon,
HumanInTheLoopIcon,
ResponseIcon,
RssIcon,
ScheduleIcon,
ShieldCheckIcon,
VariableIcon,
WebhookIcon,
WorkflowIcon,
} from '@/components/icons'
import { blockTypeToIconMap } from '@/components/ui/icon-mapping'
/**
* The two Sim-specific block glyphs we need, ported verbatim from
* `apps/sim/components/icons.tsx` so the preview matches the real builder.
* Other block types fall back to lucide-react stand-ins for now.
*/
export function StartIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
{...props}
width='26'
height='16'
viewBox='0 0 26 16'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M7.8 13C9.23 13 10.45 12.49 11.47 11.47C12.49 10.45 13 9.23 13 7.8C13 6.37 12.49 5.15 11.47 4.13C10.45 3.11 9.23 2.6 7.8 2.6C6.37 2.6 5.15 3.11 4.13 4.13C3.11 5.15 2.6 6.37 2.6 7.8C2.6 9.23 3.11 10.45 4.13 11.47C5.15 12.49 6.37 13 7.8 13ZM7.8 15.6C5.63 15.6 3.79 14.84 2.28 13.33C0.76 11.81 0 9.97 0 7.8C0 5.63 0.76 3.79 2.28 2.28C3.79 0.76 5.63 0 7.8 0C9.75 0 11.45 0.62 12.89 1.85C14.33 3.09 15.2 4.64 15.5 6.5H24.7C25.07 6.5 25.38 6.62 25.63 6.87C25.88 7.12 26 7.43 26 7.8C26 8.17 25.87 8.48 25.63 8.73C25.38 8.98 25.07 9.1 24.7 9.1H15.5C15.2 10.96 14.33 12.51 12.89 13.75C11.44 14.98 9.75 15.6 7.8 15.6Z'
fill='currentColor'
/>
</svg>
)
}
export function AgentIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
{...props}
width='21'
height='24'
viewBox='0 0 21 24'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M15.67 9.25H4.67C2.64 9.25 1 10.89 1 12.92V18.42C1 20.44 2.64 22.08 4.67 22.08H15.67C17.69 22.08 19.33 20.44 19.33 18.42V12.92C19.33 10.89 17.69 9.25 15.67 9.25Z'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
/>
<path
d='M10.17 5.58C11.18 5.58 12 4.76 12 3.75C12 2.74 11.18 1.92 10.17 1.92C9.15 1.92 8.33 2.74 8.33 3.75C8.33 4.76 9.15 5.58 10.17 5.58Z'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
/>
<path
d='M10.17 5.59V9.25M7.42 16.59V14.75M12.92 14.75V16.59'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
/>
</svg>
)
}
/** Block type → glyph. Brand glyphs from the app for core blocks; lucide stand-ins for the rest. */
export const BLOCK_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {
starter: StartIcon,
start_trigger: StartIcon,
agent: AgentIcon,
api: ApiIcon,
condition: ConditionalIcon,
credential: CredentialIcon,
evaluator: ChartBarIcon,
function: CodeIcon,
guardrails: ShieldCheckIcon,
human_in_the_loop: HumanInTheLoopIcon,
response: ResponseIcon,
router: ConnectIcon,
variables: VariableIcon,
wait: Clock,
webhook: WebhookIcon,
workflow: WorkflowIcon,
schedule: ScheduleIcon,
rss: RssIcon,
loop: Repeat,
parallel: Layers,
knowledge_base: Database,
knowledge: Database,
table: Table,
}
/**
* Resolves a block (or tool) type to its glyph: the core-block map first, then
* the integration icon map so diagrams can render tool chips too. Returns
* `null` when no glyph is registered.
*/
export function resolveIcon(type: string): ComponentType<{ className?: string }> | null {
return BLOCK_ICONS[type] ?? blockTypeToIconMap[type] ?? null
}
@@ -0,0 +1,225 @@
'use client'
import {
ChipSelect,
ChipSwitch,
ChipTag,
chipFieldSurfaceClass,
chipFieldTextClass,
cn,
FieldDivider,
Label,
} from '@sim/emcn'
import { BookOpen, Pencil } from 'lucide-react'
import { resolveIcon } from '@/components/workflow-preview/block-icons'
import { formatReferences } from '@/components/workflow-preview/format-references'
type FieldKind = 'select' | 'input' | 'textarea' | 'code' | 'slider' | 'toggle'
interface InspectorField {
label: string
required?: boolean
kind?: FieldKind
/** Shown inside the control. For 'toggle', "on"/"off". For 'slider', the number. */
value?: string
/** Muted placeholder when there's no value. */
placeholder?: string
/** Slider fill, 0100. */
percent?: number
}
interface InspectorTool {
type: string
name: string
bgColor: string
}
interface BlockInspectorProps {
/** Block name in the header, e.g. "Agent 1". */
name: string
/** Block type, for the header icon. */
type?: string
color?: string
fields: InspectorField[]
tools?: InspectorTool[]
/** Render as a borderless panel filling its parent (the lightbox sidebar). */
embedded?: boolean
}
const NOOP = () => {}
/**
* Read-only facsimile of one configuration field, composed from emcn chip
* chrome: `select`→{@link ChipSelect}, `toggle`→{@link ChipSwitch}; text fields
* (`input`/`textarea`/`code`) render the value with `<...>`/`{{...}}` references
* highlighted via {@link formatReferences} in the canonical chip field surface.
* `slider` has no chip equivalent and stays a minimal app-token bar.
*/
function FieldControl({ field }: { field: InspectorField }) {
const kind = field.kind ?? 'input'
const value = field.value ?? ''
const placeholder = field.placeholder ?? '—'
if (kind === 'select') {
return (
<ChipSelect
fullWidth
value={value || undefined}
onChange={NOOP}
placeholder={placeholder}
options={value ? [{ value, label: value }] : []}
/>
)
}
if (kind === 'toggle') {
const on = field.value === 'on'
return (
<ChipSwitch
value={on ? 'on' : 'off'}
onChange={NOOP}
aria-label={field.label}
options={[
{ value: 'on', label: 'On' },
{ value: 'off', label: 'Off' },
]}
/>
)
}
if (kind === 'slider') {
const percent = field.percent ?? 50
return (
<div className='flex w-full items-center gap-3'>
<div className='relative h-[4px] flex-1 rounded-full bg-[var(--surface-5)]'>
<div
className='absolute inset-y-0 left-0 rounded-full bg-[var(--brand-secondary)]'
style={{ width: `${percent}%` }}
/>
<div
className='-translate-y-1/2 absolute top-1/2 size-[12px] rounded-full border border-[var(--border-1)] bg-white'
style={{ left: `calc(${percent}% - 6px)` }}
/>
</div>
<span className='text-[13px] text-[var(--text-primary)]'>{field.value}</span>
</div>
)
}
// input / textarea / code: read-only value with `<...>` block references and
// `{{...}}` environment variables highlighted, in the canonical chip chrome.
const content = value ? (
formatReferences(value)
) : (
<span className='text-[var(--text-muted)]'>{placeholder}</span>
)
if (kind === 'textarea' || kind === 'code') {
return (
<div
className={cn(
chipFieldSurfaceClass,
chipFieldTextClass,
'min-h-[60px] whitespace-pre-wrap break-words px-2 py-1.5',
kind === 'code' && 'font-mono'
)}
>
{content}
</div>
)
}
return (
<div className={cn(chipFieldSurfaceClass, 'flex h-[30px] items-center px-2')}>
<span className={cn(chipFieldTextClass, 'min-w-0 truncate')}>{content}</span>
</div>
)
}
function InspectorFieldRow({ field }: { field: InspectorField }) {
return (
<div className='flex flex-col gap-2.5'>
<Label className='pl-0.5'>
{field.label}
{field.required && <span className='ml-0.5'>*</span>}
</Label>
<FieldControl field={field} />
</div>
)
}
/**
* A read-only facsimile of the editor's right-hand block inspector: the block
* header, its configuration fields as static chip controls, and its
* connections. Hand-authored per usage, like {@link WorkflowPreview} examples.
*/
export function BlockInspector({
name,
type = 'agent',
color = '#33C482',
fields,
tools,
embedded = false,
}: BlockInspectorProps) {
const Icon = resolveIcon(type)
const hasTools = Boolean(tools && tools.length > 0)
return (
<div
className={cn(
'bg-[var(--surface-1)]',
embedded
? 'flex h-full w-full flex-col overflow-y-auto'
: 'not-prose my-6 w-full max-w-[380px] overflow-hidden rounded-xl border border-[var(--border)]'
)}
>
<div className='flex items-center justify-between border-[var(--border)] border-b bg-[var(--surface-4)] px-3 py-1.5'>
<div className='flex min-w-0 flex-1 items-center gap-2'>
<div
className='flex size-[18px] flex-shrink-0 items-center justify-center rounded-sm'
style={{ background: color }}
>
{Icon && <Icon className='size-[12px] text-white' />}
</div>
<span className='truncate font-medium text-[var(--text-primary)] text-sm'>{name}</span>
</div>
<div className='flex shrink-0 items-center gap-2 text-[var(--text-secondary)]'>
<Pencil className='size-[14px]' />
<BookOpen className='size-[14px]' />
</div>
</div>
<div className='flex flex-col px-3 py-3'>
{fields.map((field, i) => (
<div key={field.label}>
{i > 0 && <FieldDivider />}
<InspectorFieldRow field={field} />
</div>
))}
{hasTools && (
<div>
{fields.length > 0 && <FieldDivider />}
<div className='flex flex-col gap-2.5'>
<Label className='pl-0.5'>Tools</Label>
<div className='flex flex-wrap gap-[6px]'>
{tools?.map((tool) => {
const TIcon = resolveIcon(tool.type)
return (
<ChipTag key={tool.type} variant='gray'>
<span
className='flex size-[14px] flex-shrink-0 items-center justify-center rounded-[4px]'
style={{ background: tool.bgColor }}
>
{TIcon && <TIcon className='size-[9px] text-white' />}
</span>
{tool.name}
</ChipTag>
)
})}
</div>
</div>
</div>
)}
</div>
</div>
)
}
@@ -0,0 +1,68 @@
'use client'
import { useMemo } from 'react'
import { domAnimation, LazyMotion } from 'framer-motion'
import ReactFlow, { type NodeTypes, ReactFlowProvider } from 'reactflow'
import 'reactflow/dist/style.css'
import { BLOCK_DISPLAY_WORKFLOWS } from '@/components/workflow-preview/block-display-workflows'
import { DocsBlockNode } from '@/components/workflow-preview/docs-block-node'
import { toReactFlowElements } from '@/components/workflow-preview/workflow-data'
/** The hero mounts the same node type the canvas uses, so it can never drift. */
const NODE_TYPES: NodeTypes = { previewBlock: DocsBlockNode }
const PRO_OPTIONS = { hideAttribution: true }
/** `maxZoom` mirrors the previous hand-rolled hero's 1.3 scale. */
const FIT_VIEW_OPTIONS = { padding: 0.2, maxZoom: 1.3 } as const
interface BlockPreviewProps {
/** Block key from {@link BLOCK_DISPLAY_WORKFLOWS} (e.g. `agent`, `condition`, `webhook_trigger`). */
type: string
}
/**
* Renders a single block exactly as it appears on the builder canvas, drawn by the
* shared {@link WorkflowBlockView} (via `DocsBlockNode`) through the same ReactFlow
* machinery as the multi-block diagrams — never a parallel hand-rolled card. Static
* and non-interactive (no pan/zoom), centered in a bordered container. Use as the hero
* on a block reference page: `<BlockPreview type="agent" />`. Edit the source data in
* `block-display-workflows.ts`.
*/
export function BlockPreview({ type }: BlockPreviewProps) {
const workflow = BLOCK_DISPLAY_WORKFLOWS[type]
const elements = useMemo(() => (workflow ? toReactFlowElements(workflow) : null), [workflow])
if (!workflow || !elements) return null
return (
<div
className='not-prose my-6 overflow-hidden rounded-xl border border-[var(--border)] bg-[var(--bg)]'
style={{ height: 400 }}
>
<LazyMotion features={domAnimation}>
<ReactFlowProvider>
<ReactFlow
nodes={elements.nodes}
edges={elements.edges}
nodeTypes={NODE_TYPES}
proOptions={PRO_OPTIONS}
fitView
fitViewOptions={FIT_VIEW_OPTIONS}
minZoom={0.2}
maxZoom={1.3}
nodesDraggable={false}
nodesConnectable={false}
elementsSelectable={false}
zoomOnScroll={false}
zoomOnDoubleClick={false}
zoomOnPinch={false}
panOnDrag={false}
panOnScroll={false}
preventScrolling={false}
className='h-full w-full'
/>
</ReactFlowProvider>
</LazyMotion>
</div>
)
}
@@ -0,0 +1,145 @@
'use client'
import { type ComponentType, memo } from 'react'
import { SubBlockRowView, WorkflowBlockView } from '@sim/workflow-renderer'
import { m } from 'framer-motion'
import type { NodeProps } from 'reactflow'
import { resolveIcon } from '@/components/workflow-preview/block-icons'
import {
BLOCK_STAGGER,
EASE_OUT,
type PreviewTool,
} from '@/components/workflow-preview/workflow-data'
/** Renders the colored square with no glyph when a block type has no registered icon. */
const EMPTY_ICON: ComponentType<{ className?: string }> = () => null
const RING_STYLES = 'ring-[1.75px] ring-[var(--brand-secondary)]'
interface DocsBlockData {
name: string
blockType: string
bgColor: string
rows: Array<{ title: string; value: string }>
branches?: Array<{ id: string; label: string; value?: string }>
tools?: PreviewTool[]
hideTargetHandle?: boolean
index?: number
animate?: boolean
isHighlighted?: boolean
isDimmed?: boolean
}
/**
* Docs adapter for workflow block nodes: maps the static preview data to the
* shared {@link WorkflowBlockView}'s props. Carries no stores, hooks, or
* queries — it only reshapes data into View props and wraps the result in the
* dim/stagger motion used by the rest of the diagram (the parent
* `WorkflowPreview` provides the `LazyMotion` feature set). The block's ring is
* driven by `hasRing`/`ringStyles` inside the View.
*/
export const DocsBlockNode = memo(function DocsBlockNode({ id, data }: NodeProps<DocsBlockData>) {
const {
name,
blockType,
bgColor,
rows: dataRows,
branches,
tools,
hideTargetHandle = false,
index = 0,
animate = false,
isHighlighted = false,
isDimmed = false,
} = data
/** The View gates router handle topology on `type === 'router_v2'`. */
const type = blockType === 'router' ? 'router_v2' : blockType
const Icon = resolveIcon(blockType) ?? EMPTY_ICON
const delay = animate ? index * BLOCK_STAGGER : 0
const hasBranches = Boolean(branches && branches.length > 0)
const hasTools = Boolean(tools && tools.length > 0)
/** The View renders the default target/source/error handles (and the error row) for non-trigger blocks; mirror that gate. */
const shouldShowDefaultHandles = !hideTargetHandle
const hasContentBelowHeader =
dataRows.length > 0 || hasBranches || hasTools || shouldShowDefaultHandles
/**
* Strip the app's `condition-`/`router-` handle prefixes — the View
* regenerates them, so passing them through would double-prefix the handle id.
* Branch + router-context values render through the editor's `getDisplayValue`,
* which shows `-` for a blank value (e.g. an `else` branch); mirror that.
*/
const conditionRows =
type === 'condition'
? (branches ?? []).map((branch) => ({
id: branch.id.replace(/^condition-/, ''),
title: branch.label,
value: branch.value || '-',
}))
: []
const routerRows =
type === 'router_v2'
? (branches ?? []).map((branch) => ({
id: branch.id.replace(/^router-/, ''),
value: branch.value || '-',
}))
: []
/** The View renders the router's leading Context row from this prop, not `rows`. */
const routerContextValue =
type === 'router_v2' ? dataRows.find((row) => row.title === 'Context')?.value || '-' : undefined
/**
* Non-branch content only — the View renders condition/router/error rows from
* the conditionRows/routerRows it receives, so their order stays locked to its
* handle geometry in one place.
*/
const rows =
type === 'condition' || type === 'router_v2' ? null : (
<>
{dataRows.map((row) => (
<SubBlockRowView key={row.title} title={row.title} displayValue={row.value} />
))}
{hasTools && (
<SubBlockRowView
title='Tools'
displayValue={tools?.map((tool) => tool.name).join(', ')}
/>
)}
</>
)
return (
<m.div
className='relative transition-opacity duration-300'
style={{ opacity: isDimmed ? 0.35 : 1 }}
initial={animate ? { opacity: 0 } : false}
animate={{ opacity: isDimmed ? 0.35 : 1 }}
transition={{ duration: 0.45, delay, ease: EASE_OUT }}
>
<WorkflowBlockView
id={id}
type={type}
name={name}
isEnabled
isLocked={false}
hasRing={Boolean(isHighlighted)}
ringStyles={RING_STYLES}
Icon={Icon}
iconBgColor={bgColor}
horizontalHandles
shouldShowDefaultHandles={shouldShowDefaultHandles}
hasContentBelowHeader={hasContentBelowHeader}
conditionRows={conditionRows}
routerRows={routerRows}
routerContextValue={routerContextValue}
wouldCreateConnectionCycle={() => false}
onSelect={() => {}}
rows={rows}
/>
</m.div>
)
})
@@ -0,0 +1,42 @@
'use client'
import { memo } from 'react'
import { type SubflowNodeData, SubflowNodeView } from '@sim/workflow-renderer'
import type { NodeProps } from 'reactflow'
interface DocsContainerData {
name: string
blockType: string
size?: { width: number; height: number }
}
/**
* Docs adapter for loop/parallel container blocks: maps the static preview data
* to {@link SubflowNodeView}'s read-only `isPreview` shape. Carries no stores,
* hooks, or queries — it only reshapes data into View props.
*/
export const DocsContainerNode = memo(function DocsContainerNode({
id,
data,
}: NodeProps<DocsContainerData>) {
const subflowData: SubflowNodeData = {
kind: data.blockType === 'parallel' ? 'parallel' : 'loop',
name: data.name,
width: data.size?.width,
height: data.size?.height,
isPreview: true,
}
return (
<SubflowNodeView
id={id}
data={subflowData}
isEnabled
isLocked={false}
isFocused={false}
nestingLevel={0}
canEditWorkflow={false}
onSelect={() => {}}
/>
)
})
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,25 @@
import type { ReactNode } from 'react'
/** Block references `<block.field>` and environment variables `{{VAR}}`. */
const REFERENCE_PATTERN = /(<[^<>]+>|\{\{[^{}]+\}\})/g
/**
* Highlights `<...>` block references and `{{...}}` environment variables in
* brand-secondary, mirroring the editor's `formatDisplayText`. Read-only and
* static — no validation or tag interactivity, since docs has no workflow state.
*/
export function formatReferences(text: string): ReactNode[] {
if (!text) return []
return text.split(REFERENCE_PATTERN).map((part, index) => {
if (!part) return null
const isReference =
(part.startsWith('<') && part.endsWith('>')) || (part.startsWith('{{') && part.endsWith('}}'))
return isReference ? (
<span key={index} className='text-[var(--brand-secondary)]'>
{part}
</span>
) : (
<span key={index}>{part}</span>
)
})
}
@@ -0,0 +1,52 @@
export { BlockPreview } from '@/components/workflow-preview/block-preview'
export {
API_FETCH_WORKFLOW,
BUILD_AGENT_WORKFLOW,
CLASSIFY_REPLY_WORKFLOW,
CLASSIFY_WORKFLOW,
COMBINATION_WORKFLOW,
CONCURRENCY_WORKFLOW,
CONDITION_MODERATE_WORKFLOW,
CONDITION_ONBOARD_WORKFLOW,
CONDITION_ROUTE_WORKFLOW,
CREDENTIAL_ROUTE_WORKFLOW,
CREDENTIAL_SHARE_WORKFLOW,
ERROR_PATH_WORKFLOW,
EVALUATOR_GATE_WORKFLOW,
FILE_SUMMARY_WORKFLOW,
FUNCTION_RESHAPE_WORKFLOW,
FUNCTION_VALIDATE_WORKFLOW,
GUARDRAILS_HALLUCINATION_WORKFLOW,
GUARDRAILS_JSON_WORKFLOW,
GUARDRAILS_PII_WORKFLOW,
HITL_APPROVAL_WORKFLOW,
HITL_MULTISTAGE_WORKFLOW,
HITL_VALIDATE_WORKFLOW,
LEAD_SCORER_WORKFLOW,
LOOP_WORKFLOW,
PARALLEL_WORKFLOW,
RESPONSE_API_WORKFLOW,
RESPONSE_ERROR_WORKFLOW,
RESPONSE_WEBHOOK_WORKFLOW,
ROUTER_CLASSIFY_WORKFLOW,
ROUTER_LEAD_WORKFLOW,
ROUTER_TRIAGE_WORKFLOW,
ROUTING_WORKFLOW,
SUPPORT_KB_WORKFLOW,
TABLE_ENRICH_WORKFLOW,
TABLE_ROUNDTRIP_WORKFLOW,
VARIABLES_CONFIG_WORKFLOW,
VARIABLES_RETRY_WORKFLOW,
WAIT_FOLLOWUP_WORKFLOW,
WAIT_RATELIMIT_WORKFLOW,
WEBHOOK_NOTIFY_WORKFLOW,
WEBHOOK_TRIGGER_WORKFLOW,
WORKFLOW_CALL_WORKFLOW,
} from '@/components/workflow-preview/examples'
export { OutputBundle } from '@/components/workflow-preview/output-bundle'
export type {
PreviewBlock,
PreviewTool,
PreviewWorkflow,
} from '@/components/workflow-preview/workflow-data'
export { WorkflowPreview } from '@/components/workflow-preview/workflow-preview'
@@ -0,0 +1,161 @@
'use client'
import { Badge } from '@sim/emcn'
import { ChevronDown, Clipboard, Download, Search } from 'lucide-react'
import { resolveIcon } from '@/components/workflow-preview/block-icons'
type ValueType = 'string' | 'number' | 'boolean' | 'object' | 'array' | 'null'
/** Output value type → emcn Badge color variant. */
const TYPE_VARIANT = {
string: 'green',
number: 'blue',
boolean: 'orange',
array: 'purple',
object: 'gray',
null: 'gray',
} as const
interface OutputNode {
key: string
type?: ValueType
/** Primitive value shown beneath the key when expanded. */
value?: string
/** Nested fields for object/array nodes. */
children?: OutputNode[]
/** Collapse the node (chevron points right, nothing rendered beneath). */
expanded?: boolean
/** Emphasize this row as the one being read by the tag below. */
highlight?: boolean
}
interface LogRow {
name: string
type?: string
color?: string
duration?: string
selected?: boolean
}
interface OutputBundleProps {
/** The block's name (unique within the workflow), e.g. "classify". */
blockName: string
blockType?: string
blockColor?: string
/** Duration shown on the selected log row. */
duration?: string
/** Override the Logs column; defaults to Start + this block (selected). */
logs?: LogRow[]
values: OutputNode[]
}
function TypeBadge({ type }: { type: ValueType }) {
return (
<Badge variant={TYPE_VARIANT[type]} size='sm'>
{type}
</Badge>
)
}
function TreeNode({ node, depth = 0 }: { node: OutputNode; depth?: number }) {
const type = node.type ?? 'string'
const expanded = node.expanded ?? Boolean(node.children || node.value !== undefined)
return (
<div className='flex min-w-0 flex-col'>
<div className='flex min-h-[26px] items-center gap-2 rounded-[6px] px-1'>
<span
className='text-[13px]'
style={{ color: node.highlight ? 'var(--brand-secondary)' : 'var(--text-primary)' }}
>
{node.key}
</span>
<TypeBadge type={type} />
<ChevronDown
className='h-[7px] w-[9px] flex-shrink-0 text-[var(--text-muted)]'
style={expanded ? undefined : { transform: 'rotate(-90deg)' }}
/>
</div>
{expanded && (node.children || node.value !== undefined) && (
<div className='mt-0.5 ml-[5px] flex min-w-0 flex-col gap-0.5 border-[var(--divider)] border-l pl-[10px]'>
{node.children
? node.children.map((child) => (
<TreeNode key={child.key} node={child} depth={depth + 1} />
))
: node.value !== undefined && (
<div className='py-0.5 text-[13px] text-[var(--text-secondary)]'>{node.value}</div>
)}
</div>
)}
</div>
)
}
/**
* A miniature of the app's run inspector — the Logs list beside the Output
* panel's typed tree — teaching what a block's output is: named, typed values
* remembered under the block's name, read with a `<blockName.key>` tag.
*/
export function OutputBundle({
blockName,
blockType = 'agent',
blockColor = '#33C482',
duration = '1.2s',
logs,
values,
}: OutputBundleProps) {
const logRows: LogRow[] = logs ?? [
{ name: 'Start', type: 'start_trigger', color: '#2FB3FF', duration: '9ms' },
{ name: blockName, type: blockType, color: blockColor, duration, selected: true },
]
return (
<div className='not-prose my-6 flex w-full max-w-[640px] flex-col gap-3'>
<div className='flex overflow-hidden rounded-xl border border-[var(--border)] bg-[var(--surface-1)]'>
<div className='flex w-[210px] flex-shrink-0 flex-col border-[var(--border)] border-r px-2 py-2'>
<div className='px-2 pb-2 text-[12px] text-[var(--text-muted)]'>Logs</div>
{logRows.map((row) => {
const Icon = row.type ? resolveIcon(row.type) : null
return (
<div
key={row.name}
className='flex h-[30px] items-center gap-2 rounded-[6px] px-2'
style={row.selected ? { background: 'var(--surface-active)' } : undefined}
>
<div
className='flex size-[18px] flex-shrink-0 items-center justify-center rounded-[5px]'
style={{ background: row.color ?? 'var(--border-1)' }}
>
{Icon && <Icon className='size-[10px] text-white' />}
</div>
<span className='truncate text-[13px] text-[var(--text-primary)]'>{row.name}</span>
{row.duration && (
<span className='ml-auto text-[12px] text-[var(--text-muted)]'>
{row.duration}
</span>
)}
</div>
)
})}
</div>
<div className='flex min-w-0 flex-1 flex-col px-3 py-2'>
<div className='flex items-center gap-3 pb-2'>
<span className='text-[13px] text-[var(--text-primary)]'>Output</span>
<span className='text-[13px] text-[var(--text-muted)]'>Input</span>
<span className='ml-auto flex items-center gap-2 text-[var(--text-subtle)]'>
<Search className='size-[12px]' />
<Clipboard className='size-[12px]' />
<Download className='size-[12px]' />
<ChevronDown className='size-[12px]' />
</span>
</div>
<div className='flex min-w-0 flex-col gap-0.5'>
{values.map((node) => (
<TreeNode key={node.key} node={node} />
))}
</div>
</div>
</div>
</div>
)
}
@@ -0,0 +1,153 @@
import { type Edge, type Node, Position } from 'reactflow'
/**
* Tool entry displayed as a chip on a block (e.g. an Agent's attached tools).
*/
export interface PreviewTool {
name: string
type: string
bgColor: string
}
/**
* A single block in a preview workflow. Presentational shape — authored by hand
* for docs diagrams, not the app's full serialized block state.
*/
export interface PreviewBlock {
id: string
name: string
type: string
bgColor: string
rows: Array<{ title: string; value: string }>
/**
* Branch rows, each with its own right-edge source handle whose id is the
* branch id. Author ids in the app's own handle scheme so edges match the
* real workflow representation verbatim: `condition-<id>` on Condition
* blocks, `router-<routeId>` on Routers.
*/
branches?: Array<{ id: string; label: string; value?: string }>
tools?: PreviewTool[]
position: { x: number; y: number }
hideTargetHandle?: boolean
/** When set, the block renders as a Loop/Parallel container sized to hold its children. */
size?: { width: number; height: number }
/** Id of the container block this block sits inside. Its position is relative to the container. */
parentId?: string
}
/**
* A workflow rendered as a read-only, app-styled diagram in the docs.
*/
export interface PreviewWorkflow {
id: string
name: string
blocks: PreviewBlock[]
edges: Array<{ id: string; source: string; target: string; sourceHandle?: string }>
}
export const BLOCK_STAGGER = 0.12
export const EASE_OUT: [number, number, number, number] = [0.16, 1, 0.3, 1]
const EDGE_STYLE = { stroke: 'var(--workflow-edge)', strokeWidth: 2 } as const
const EDGE_STYLE_HIGHLIGHT = { stroke: 'var(--brand-secondary)', strokeWidth: 2.5 } as const
/** Edges leaving a block's error port render red, matching the editor. */
const EDGE_STYLE_ERROR = { stroke: 'var(--text-error)', strokeWidth: 2 } as const
/** Optional emphasis: light one block or one edge and dim everything else. */
export interface HighlightOptions {
highlightBlock?: string
highlightEdge?: string
/** Ring one block (selection) without dimming the rest. */
selectedBlock?: string
}
/**
* Converts a {@link PreviewWorkflow} to React Flow nodes and edges.
*
* @param workflow - The workflow definition
* @param animate - When true, node/edge data carries stagger metadata
* @param highlight - Optional block/edge to emphasize (dims the rest)
*/
export function toReactFlowElements(
workflow: PreviewWorkflow,
animate = false,
highlight: HighlightOptions = {}
): { nodes: Node[]; edges: Edge[] } {
const { highlightBlock, highlightEdge, selectedBlock } = highlight
const hasHighlight = Boolean(highlightBlock || highlightEdge)
const blockIndexMap = new Map(workflow.blocks.map((b, i) => [b.id, i]))
const blocksById = new Map(workflow.blocks.map((b) => [b.id, b]))
const nodes: Node[] = workflow.blocks.map((block, index) => {
const isContainer = Boolean(block.size)
// Nested blocks are authored relative to their container; render them at
// absolute coordinates (not React Flow parentNode children) so the edges
// between a container and its nested blocks render reliably and on top.
const parent = block.parentId ? blocksById.get(block.parentId) : undefined
const position = parent
? { x: parent.position.x + block.position.x, y: parent.position.y + block.position.y }
: block.position
return {
id: block.id,
type: isContainer ? 'previewContainer' : 'previewBlock',
position,
zIndex: isContainer ? 0 : 1,
...(block.size ? { style: { width: block.size.width, height: block.size.height } } : {}),
data: {
name: block.name,
blockType: block.type,
bgColor: block.bgColor,
rows: block.rows,
branches: block.branches,
tools: block.tools,
hideTargetHandle: block.hideTargetHandle,
size: block.size,
index,
animate,
isHighlighted: highlightBlock === block.id || selectedBlock === block.id,
isDimmed: hasHighlight && highlightBlock !== block.id,
},
draggable: true,
selectable: false,
connectable: false,
sourcePosition: Position.Right,
targetPosition: Position.Left,
}
})
const edges: Edge[] = workflow.edges.map((e) => {
const sourceIndex = blockIndexMap.get(e.source) ?? 0
const isEdgeHighlight = highlightEdge === e.id
const dimmed = hasHighlight && !isEdgeHighlight
const isErrorEdge = e.sourceHandle === 'error'
// Subflow containers expose a right-edge output handle (`loop-end-source` /
// `parallel-end-source`) and a left-edge input handle with no id; regular
// blocks use `source` / `target`. Resolve each end to the block's real handle
// so edges into and out of Loop/Parallel containers still connect.
const sourceBlock = blocksById.get(e.source)
const targetBlock = blocksById.get(e.target)
const sourceHandle =
e.sourceHandle ?? (sourceBlock?.size ? `${sourceBlock.type}-end-source` : 'source')
const targetHandle = targetBlock?.size ? undefined : 'target'
return {
id: e.id,
source: e.source,
target: e.target,
type: 'previewEdge',
animated: false,
style: {
...(isEdgeHighlight ? EDGE_STYLE_HIGHLIGHT : isErrorEdge ? EDGE_STYLE_ERROR : EDGE_STYLE),
opacity: dimmed ? 0.35 : 1,
},
sourceHandle,
targetHandle,
data: {
animate,
delay: animate ? sourceIndex * BLOCK_STAGGER + BLOCK_STAGGER : 0,
},
}
})
return { nodes, edges }
}
@@ -0,0 +1,367 @@
'use client'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { domAnimation, LazyMotion, m } from 'framer-motion'
import { Maximize2, X } from 'lucide-react'
import ReactFlow, {
applyEdgeChanges,
applyNodeChanges,
type Edge,
type EdgeProps,
type EdgeTypes,
getSmoothStepPath,
type Node,
type NodeTypes,
type OnEdgesChange,
type OnNodesChange,
ReactFlowProvider,
} from 'reactflow'
import 'reactflow/dist/style.css'
import { BLOCK_DISPLAY_WORKFLOWS } from '@/components/workflow-preview/block-display-workflows'
import { BlockInspector } from '@/components/workflow-preview/block-inspector'
import { DocsBlockNode } from '@/components/workflow-preview/docs-block-node'
import { DocsContainerNode } from '@/components/workflow-preview/docs-container-node'
import {
EASE_OUT,
type PreviewBlock,
type PreviewWorkflow,
toReactFlowElements,
} from '@/components/workflow-preview/workflow-data'
interface WorkflowPreviewProps {
workflow: PreviewWorkflow
/** Canvas height in px. Default 260. */
height?: number
animate?: boolean
/** Emphasize one block by id, dimming the rest. */
highlightBlock?: string
/** Emphasize one edge by id, dimming the rest. */
highlightEdge?: string
}
/** Smooth-step edge, matching the app's connection styling. */
function PreviewEdge({
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
style,
data,
}: EdgeProps) {
const [edgePath] = getSmoothStepPath({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
borderRadius: 8,
offset: 30,
})
if (data?.animate) {
return (
<m.path
id={id}
className='react-flow__edge-path'
d={edgePath}
style={{ ...style, fill: 'none' }}
initial={{ pathLength: 0, opacity: 0 }}
animate={{ pathLength: 1, opacity: 1 }}
transition={{
pathLength: { duration: 0.4, delay: data.delay ?? 0, ease: EASE_OUT },
opacity: { duration: 0.15, delay: data.delay ?? 0 },
}}
/>
)
}
return (
<path
id={id}
className='react-flow__edge-path'
d={edgePath}
style={{ ...style, fill: 'none' }}
/>
)
}
const NODE_TYPES: NodeTypes = {
previewBlock: DocsBlockNode,
previewContainer: DocsContainerNode,
}
const EDGE_TYPES: EdgeTypes = { previewEdge: PreviewEdge }
const PRO_OPTIONS = { hideAttribution: true }
const FIT_VIEW_OPTIONS = { padding: 0.25, maxZoom: 1 } as const
const LIGHTBOX_FIT_VIEW_OPTIONS = { padding: 0.3, maxZoom: 1.4 } as const
/** Field titles rendered as multiline text in the inspector. */
const TEXTAREA_TITLES = new Set(['Messages', 'Prompt', 'Code', 'Data', 'Body', 'Display'])
/** Field titles rendered as dropdowns in the inspector. */
const SELECT_TITLES = new Set([
'Model',
'Operation',
'Method',
'Unit',
'Event type',
'Validation',
'Account',
'Table',
'Knowledge Base',
'Language',
'Workflow',
'Format',
])
function inspectorFieldsFor(block: PreviewBlock) {
// Show the block type's full field list (from the reference data) with this
// block's example values overlaid, so the inspector reads like the editor's
// panel — every field — not just the summary rows shown on the canvas node.
// Apply the template only when the block authored rows that are actually a
// subset of it; otherwise the type string is ambiguous (a `table` action vs
// the table trigger, a `webhook` trigger vs the webhook action) and the wrong
// template would win, so the block's own authored rows are the source of
// truth. A block with no rows (e.g. a router defined only by its branches)
// keeps its empty set rather than inheriting the template's invented defaults.
const exampleByTitle = new Map(block.rows.map((row) => [row.title, row.value]))
const template = BLOCK_DISPLAY_WORKFLOWS[block.type]?.blocks[0]?.rows
const fullRows =
template &&
block.rows.length > 0 &&
block.rows.every((row) => template.some((field) => field.title === row.title))
? template
: block.rows
const rowFields = fullRows.map((row) => {
const value = exampleByTitle.get(row.title) ?? row.value
return {
label: row.title,
kind:
TEXTAREA_TITLES.has(row.title) || value.length > 40
? ('textarea' as const)
: SELECT_TITLES.has(row.title)
? ('select' as const)
: ('input' as const),
value,
}
})
const branchFields = (block.branches ?? []).map((branch) => ({
label: branch.label,
kind: 'code' as const,
// Match the canvas + the editor's getDisplayValue: a blank value reads as '-'.
value: branch.value || '-',
}))
return [...rowFields, ...branchFields]
}
function PreviewFlow({
workflow,
animate = false,
highlightBlock,
highlightEdge,
selectedBlock,
interactive = false,
onNodeClick,
onPaneClick,
}: WorkflowPreviewProps & {
selectedBlock?: string
interactive?: boolean
onNodeClick?: (blockId: string) => void
onPaneClick?: () => void
}) {
const { nodes: initialNodes, edges: initialEdges } = useMemo(
() => toReactFlowElements(workflow, animate, { highlightBlock, highlightEdge, selectedBlock }),
[workflow, animate, highlightBlock, highlightEdge, selectedBlock]
)
const [nodes, setNodes] = useState<Node[]>(initialNodes)
const [edges, setEdges] = useState<Edge[]>(initialEdges)
/**
* Apply data changes (highlight/selection) without discarding positions the
* viewer has dragged — only a different workflow should relayout the canvas.
*/
useEffect(() => {
setNodes((prev) => {
const positions = new Map(prev.map((node) => [node.id, node.position]))
return initialNodes.map((node) => {
const position = positions.get(node.id)
return position ? { ...node, position } : node
})
})
setEdges(initialEdges)
}, [initialNodes, initialEdges])
const onNodesChange: OnNodesChange = useCallback(
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
[]
)
const onEdgesChange: OnEdgesChange = useCallback(
(changes) => setEdges((eds) => applyEdgeChanges(changes, eds)),
[]
)
return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeClick={onNodeClick ? (_, node) => onNodeClick(node.id) : undefined}
onPaneClick={onPaneClick}
nodeTypes={NODE_TYPES}
edgeTypes={EDGE_TYPES}
defaultEdgeOptions={{ type: 'previewEdge' }}
elementsSelectable={false}
nodesDraggable
nodesConnectable={false}
zoomOnScroll={interactive}
zoomOnDoubleClick={interactive}
panOnScroll={false}
zoomOnPinch
panOnDrag
preventScrolling={interactive}
autoPanOnNodeDrag={false}
proOptions={PRO_OPTIONS}
minZoom={0.1}
fitView
fitViewOptions={interactive ? LIGHTBOX_FIT_VIEW_OPTIONS : FIT_VIEW_OPTIONS}
className='h-full w-full'
/>
)
}
/**
* Read-only, app-styled workflow diagram for docs pages. Renders a
* {@link PreviewWorkflow} with ReactFlow — draggable, non-editable, no app
* runtime. Clicking a block (or the expand control) opens a full-screen
* lightbox with zoom and pan, plus a read-only inspector panel showing the
* selected block's full configuration — canvas rows truncate, the inspector
* doesn't.
*
* @example
* <WorkflowPreview workflow={CLASSIFY_WORKFLOW} />
*/
export function WorkflowPreview({
workflow,
height = 340,
animate = false,
highlightBlock,
highlightEdge,
}: WorkflowPreviewProps) {
const [expanded, setExpanded] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
useEffect(() => {
if (!expanded) return
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') setExpanded(false)
}
document.addEventListener('keydown', onKey)
const previousOverflow = document.body.style.overflow
document.body.style.overflow = 'hidden'
document.body.classList.add('wp-lightbox-open')
return () => {
document.removeEventListener('keydown', onKey)
document.body.style.overflow = previousOverflow
document.body.classList.remove('wp-lightbox-open')
}
}, [expanded])
const selectedBlock = selectedId
? (workflow.blocks.find((b) => b.id === selectedId) ?? null)
: null
const openWith = (blockId: string | null) => {
setSelectedId(blockId)
setExpanded(true)
}
return (
<LazyMotion features={domAnimation}>
<div
className='not-prose group relative my-6 overflow-hidden rounded-xl border border-[var(--border)] bg-[var(--bg)]'
style={{ height }}
>
<ReactFlowProvider key={`${workflow.id}-${highlightBlock ?? ''}-${highlightEdge ?? ''}`}>
<PreviewFlow
workflow={workflow}
animate={animate}
highlightBlock={highlightBlock}
highlightEdge={highlightEdge}
onNodeClick={(id) => openWith(id)}
onPaneClick={() => openWith(null)}
/>
</ReactFlowProvider>
<button
type='button'
aria-label='Expand workflow preview'
onClick={() => openWith(null)}
className='absolute top-2 right-2 z-10 flex size-[28px] items-center justify-center rounded-[6px] border border-[var(--border-1)] bg-[var(--surface-4)] text-[var(--text-muted)] opacity-0 transition-opacity duration-150 hover:text-[var(--text-primary)] group-hover:opacity-100'
>
<Maximize2 className='size-[13px]' />
</button>
</div>
{expanded && (
<div
className='fixed inset-0 z-50 flex items-center justify-center bg-black/70 p-6 backdrop-blur-sm'
onClick={() => setExpanded(false)}
onKeyDown={() => {}}
role='presentation'
>
<div
className='relative flex h-[86vh] w-[92vw] overflow-hidden rounded-xl border border-[var(--border)] bg-[var(--bg)]'
onClick={(e) => e.stopPropagation()}
onKeyDown={() => {}}
role='presentation'
>
<div className='relative min-w-0 flex-1'>
<div className='pointer-events-none absolute top-0 right-0 left-0 z-10 flex items-center justify-between px-4 py-3'>
<span className='text-[13px] text-[var(--text-muted)]'>{workflow.name}</span>
<button
type='button'
aria-label='Close'
onClick={() => setExpanded(false)}
className='pointer-events-auto flex size-[28px] items-center justify-center rounded-[6px] border border-[var(--border-1)] bg-[var(--surface-4)] text-[var(--text-muted)] transition-colors hover:text-[var(--text-primary)]'
>
<X className='size-[14px]' />
</button>
</div>
<ReactFlowProvider key={`${workflow.id}-lightbox`}>
<PreviewFlow
workflow={workflow}
highlightBlock={highlightBlock}
highlightEdge={highlightEdge}
selectedBlock={selectedId ?? undefined}
interactive
onNodeClick={(id) => setSelectedId(id)}
onPaneClick={() => setSelectedId(null)}
/>
</ReactFlowProvider>
</div>
<div className='w-[340px] flex-shrink-0 border-[var(--border)] border-l'>
{selectedBlock ? (
<BlockInspector
embedded
name={selectedBlock.name}
type={selectedBlock.type}
color={selectedBlock.bgColor}
fields={inspectorFieldsFor(selectedBlock)}
tools={selectedBlock.tools}
/>
) : (
<div className='flex h-full items-center justify-center px-6 text-center text-[13px] text-[var(--text-muted)]'>
Select a block to see its full configuration
</div>
)}
</div>
</div>
</div>
)}
</LazyMotion>
)
}
@@ -0,0 +1,94 @@
---
title: Authentication
description: API key types, generation, and how to authenticate requests
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
To access the Sim API, you need an API key. Sim supports two types of API keys — **personal keys** and **workspace keys** — each with different billing and access behaviors.
## Key Types
| | **Personal Keys** | **Workspace Keys** |
| --- | --- | --- |
| **Billed to** | Your individual account | Workspace owner |
| **Scope** | Across workspaces you have access to | Shared across the workspace |
| **Managed by** | Each user individually | Workspace admins |
| **Permissions** | Must be enabled at workspace level | Require admin permissions |
<Callout type="info">
Workspace admins can disable personal API key usage for their workspace. If disabled, only workspace keys can be used.
</Callout>
## Generating API Keys
To generate a key, open the Sim dashboard and navigate to **Settings**, then go to **Sim Keys** and click **Create**.
<Callout type="warn">
API keys are only shown once when generated. Store your key securely — you will not be able to view it again.
</Callout>
## Using API Keys
Pass your API key in the `X-API-Key` header with every request:
<Tabs items={['curl', 'TypeScript', 'Python']}>
<Tab value="curl">
```bash
curl -X POST https://www.sim.ai/api/workflows/{workflowId}/execute \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{"inputs": {}}'
```
</Tab>
<Tab value="TypeScript">
```typescript
const response = await fetch(
'https://www.sim.ai/api/workflows/{workflowId}/execute',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': process.env.SIM_API_KEY!,
},
body: JSON.stringify({ inputs: {} }),
}
)
```
</Tab>
<Tab value="Python">
```python
import requests
response = requests.post(
"https://www.sim.ai/api/workflows/{workflowId}/execute",
headers={
"Content-Type": "application/json",
"X-API-Key": os.environ["SIM_API_KEY"],
},
json={"inputs": {}},
)
```
</Tab>
</Tabs>
## Where Keys Are Used
API keys authenticate access to:
- **Workflow execution** — run deployed workflows via the API
- **Logs API** — query workflow execution logs and metrics
- **MCP servers** — authenticate connections to deployed MCP servers
- **SDKs** — the [Python](/api-reference/python) and [TypeScript](/api-reference/typescript) SDKs use API keys for all operations
## Security
- Keys use the `sk-sim-` prefix and are encrypted at rest
- Keys can be revoked at any time from the dashboard
- Use environment variables to store keys — never hardcode them in source code
- For browser-based applications, use a backend proxy to avoid exposing keys to the client
<Callout type="warn">
Never expose your API key in client-side code. Use a server-side proxy to make authenticated requests on behalf of your frontend.
</Callout>
@@ -0,0 +1,212 @@
---
title: Getting Started
description: Base URL, first API call, response format, error handling, and pagination
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
import { Step, Steps } from 'fumadocs-ui/components/steps'
## Base URL
All API requests are made to:
```
https://www.sim.ai
```
## Quick Start
<Steps>
<Step>
### Get your API key
Go to the Sim dashboard and navigate to **Settings → Sim Keys**, then click **Create**. See [Authentication](/api-reference/authentication) for details on key types.
</Step>
<Step>
### Find your workflow ID
Open a workflow in the Sim editor. The workflow ID is in the URL:
```
https://www.sim.ai/workspace/{workspaceId}/w/{workflowId}
```
You can also use the [List Workflows](/api-reference/workflows/listWorkflows) endpoint to get all workflow IDs in a workspace.
</Step>
<Step>
### Deploy your workflow
A workflow must be deployed before it can be executed via the API. Click the **Deploy** button in the editor toolbar.
</Step>
<Step>
### Make your first request
<Tabs items={['curl', 'TypeScript', 'Python']}>
<Tab value="curl">
```bash
curl -X POST https://www.sim.ai/api/workflows/{workflowId}/execute \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{"inputs": {}}'
```
</Tab>
<Tab value="TypeScript">
```typescript
const response = await fetch(
`https://www.sim.ai/api/workflows/${workflowId}/execute`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': process.env.SIM_API_KEY!,
},
body: JSON.stringify({ inputs: {} }),
}
)
const data = await response.json()
console.log(data.output)
```
</Tab>
<Tab value="Python">
```python
import requests
import os
response = requests.post(
f"https://www.sim.ai/api/workflows/{workflow_id}/execute",
headers={
"Content-Type": "application/json",
"X-API-Key": os.environ["SIM_API_KEY"],
},
json={"inputs": {}},
)
data = response.json()
print(data["output"])
```
</Tab>
</Tabs>
</Step>
</Steps>
## Sync vs Async Execution
By default, workflow executions are **synchronous** — the API blocks until the workflow completes and returns the result directly.
For long-running workflows, use **asynchronous execution** by passing `async: true`:
```bash
curl -X POST https://www.sim.ai/api/workflows/{workflowId}/execute \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{"inputs": {}, "async": true}'
```
This returns immediately with a `jobId` and `statusUrl`:
```json
{
"success": true,
"jobId": "job_abc123",
"statusUrl": "https://www.sim.ai/api/jobs/job_abc123",
"message": "Workflow execution started",
"async": true
}
```
Poll the [Get Job Status](/api-reference/workflows/getJobStatus) endpoint until the status is `completed` or `failed`:
```bash
curl https://www.sim.ai/api/jobs/{jobId} \
-H "X-API-Key: YOUR_API_KEY"
```
<Callout type="info">
Job status transitions follow: `queued` → `processing` → `completed` or `failed`. The `output` field is only present when status is `completed`.
</Callout>
## Response Format
Successful responses include an `output` object with your workflow results and a `limits` object with your current rate limit and usage status:
```json
{
"success": true,
"output": {
"result": "Hello, world!"
},
"limits": {
"workflowExecutionRateLimit": {
"sync": {
"requestsPerMinute": 60,
"maxBurst": 10,
"remaining": 59,
"resetAt": "2025-01-01T00:01:00Z"
},
"async": {
"requestsPerMinute": 30,
"maxBurst": 5,
"remaining": 30,
"resetAt": "2025-01-01T00:01:00Z"
}
},
"usage": {
"currentPeriodCost": 1.25,
"limit": 50.00,
"plan": "pro",
"isExceeded": false
}
}
}
```
## Error Handling
The API uses standard HTTP status codes. Error responses include a human-readable `error` message:
```json
{
"error": "Workflow not found"
}
```
| Status | Meaning | What to do |
| --- | --- | --- |
| `400` | Invalid request parameters | Check the `details` array for specific field errors |
| `401` | Missing or invalid API key | Verify your `X-API-Key` header |
| `403` | Access denied | Check you have permission for this resource |
| `404` | Resource not found | Verify the ID exists and belongs to your workspace |
| `429` | Rate limit exceeded | Wait for the duration in the `Retry-After` header |
<Callout type="info">
Use the [Get Usage Limits](/api-reference/usage/getUsageLimits) endpoint to check your current rate limit status and billing usage at any time.
</Callout>
## Rate Limits
Rate limits depend on your subscription plan and apply separately to synchronous and asynchronous executions. Every execution response includes a `limits` object showing your current rate limit status.
When rate limited, the API returns a `429` response with a `Retry-After` header indicating how many seconds to wait before retrying.
## Pagination
List endpoints (workflows, logs, audit logs) use **cursor-based pagination**:
```bash
# First page
curl "https://www.sim.ai/api/v1/logs?limit=20" \
-H "X-API-Key: YOUR_API_KEY"
# Next page — use the nextCursor from the previous response
curl "https://www.sim.ai/api/v1/logs?limit=20&cursor=abc123" \
-H "X-API-Key: YOUR_API_KEY"
```
The response includes a `nextCursor` field. When `nextCursor` is absent or `null`, you have reached the last page.
@@ -0,0 +1,18 @@
{
"title": "API Reference",
"root": true,
"pages": [
"getting-started",
"authentication",
"---SDKs---",
"python",
"typescript",
"---Endpoints---",
"(generated)/workflows",
"(generated)/logs",
"(generated)/usage",
"(generated)/audit-logs",
"(generated)/tables",
"(generated)/files"
]
}
@@ -0,0 +1,766 @@
---
title: Python
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Card, Cards } from 'fumadocs-ui/components/card'
import { Step, Steps } from 'fumadocs-ui/components/steps'
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
Das offizielle Python SDK für Sim ermöglicht es Ihnen, Workflows programmatisch aus Ihren Python-Anwendungen heraus mit dem offiziellen Python SDK auszuführen.
<Callout type="info">
Das Python SDK unterstützt Python 3.8+ mit Unterstützung für asynchrone Ausführung, automatischer Ratenbegrenzung mit exponentiellem Backoff und Nutzungsverfolgung.
</Callout>
## Installation
Installieren Sie das SDK mit pip:
```bash
pip install simstudio-sdk
```
## Schnellstart
Hier ist ein einfaches Beispiel für den Einstieg:
```python
from simstudio import SimStudioClient
# Initialize the client
client = SimStudioClient(
api_key="your-api-key-here",
base_url="https://sim.ai" # optional, defaults to https://sim.ai
)
# Execute a workflow
try:
result = client.execute_workflow("workflow-id")
print("Workflow executed successfully:", result)
except Exception as error:
print("Workflow execution failed:", error)
```
## API-Referenz
### SimStudioClient
#### Konstruktor
```python
SimStudioClient(api_key: str, base_url: str = "https://sim.ai")
```
**Parameter:**
- `api_key` (str): Ihr Sim API-Schlüssel
- `base_url` (str, optional): Basis-URL für die Sim API
#### Methoden
##### execute_workflow()
Führt einen Workflow mit optionalen Eingabedaten aus.
```python
result = client.execute_workflow(
"workflow-id",
input_data={"message": "Hello, world!"},
timeout=30.0 # 30 seconds
)
```
**Parameter:**
- `workflow_id` (str): Die ID des auszuführenden Workflows
- `input_data` (dict, optional): Eingabedaten, die an den Workflow übergeben werden
- `timeout` (float, optional): Timeout in Sekunden (Standard: 30.0)
- `stream` (bool, optional): Streaming-Antworten aktivieren (Standard: False)
- `selected_outputs` (list[str], optional): Block-Ausgaben zum Streamen im Format `blockName.attribute` (z. B. `["agent1.content"]`)
- `async_execution` (bool, optional): Asynchron ausführen (Standard: False)
**Rückgabewert:** `WorkflowExecutionResult | AsyncExecutionResult`
Wenn `async_execution=True`, wird sofort mit einer Task-ID zum Polling zurückgegeben. Andernfalls wird auf die Fertigstellung gewartet.
##### get_workflow_status()
Ruft den Status eines Workflows ab (Deployment-Status usw.).
```python
status = client.get_workflow_status("workflow-id")
print("Is deployed:", status.is_deployed)
```
**Parameter:**
- `workflow_id` (str): Die ID des Workflows
**Rückgabe:** `WorkflowStatus`
##### validate_workflow()
Überprüft, ob ein Workflow zur Ausführung bereit ist.
```python
is_ready = client.validate_workflow("workflow-id")
if is_ready:
# Workflow is deployed and ready
pass
```
**Parameter:**
- `workflow_id` (str): Die ID des Workflows
**Rückgabe:** `bool`
##### get_job_status()
Ruft den Status einer asynchronen Job-Ausführung ab.
```python
status = client.get_job_status("task-id-from-async-execution")
print("Status:", status["status"]) # 'queued', 'processing', 'completed', 'failed'
if status["status"] == "completed":
print("Output:", status["output"])
```
**Parameter:**
- `task_id` (str): Die Task-ID, die von der asynchronen Ausführung zurückgegeben wurde
**Rückgabe:** `Dict[str, Any]`
**Antwortfelder:**
- `success` (bool): Ob die Anfrage erfolgreich war
- `taskId` (str): Die Task-ID
- `status` (str): Einer von `'queued'`, `'processing'`, `'completed'`, `'failed'`, `'cancelled'`
- `metadata` (dict): Enthält `startedAt`, `completedAt` und `duration`
- `output` (any, optional): Die Workflow-Ausgabe (wenn abgeschlossen)
- `error` (any, optional): Fehlerdetails (wenn fehlgeschlagen)
- `estimatedDuration` (int, optional): Geschätzte Dauer in Millisekunden (wenn in Bearbeitung/in Warteschlange)
##### execute_with_retry()
Führt einen Workflow mit automatischer Wiederholung bei Rate-Limit-Fehlern unter Verwendung von exponentiellem Backoff aus.
```python
result = client.execute_with_retry(
"workflow-id",
input_data={"message": "Hello"},
timeout=30.0,
max_retries=3, # Maximum number of retries
initial_delay=1.0, # Initial delay in seconds
max_delay=30.0, # Maximum delay in seconds
backoff_multiplier=2.0 # Exponential backoff multiplier
)
```
**Parameter:**
- `workflow_id` (str): Die ID des auszuführenden Workflows
- `input_data` (dict, optional): Eingabedaten, die an den Workflow übergeben werden
- `timeout` (float, optional): Timeout in Sekunden
- `stream` (bool, optional): Streaming-Antworten aktivieren
- `selected_outputs` (list, optional): Block-Ausgaben zum Streamen
- `async_execution` (bool, optional): Asynchron ausführen
- `max_retries` (int, optional): Maximale Anzahl von Wiederholungen (Standard: 3)
- `initial_delay` (float, optional): Anfangsverzögerung in Sekunden (Standard: 1.0)
- `max_delay` (float, optional): Maximale Verzögerung in Sekunden (Standard: 30.0)
- `backoff_multiplier` (float, optional): Backoff-Multiplikator (Standard: 2.0)
**Rückgabe:** `WorkflowExecutionResult | AsyncExecutionResult`
Die Wiederholungslogik verwendet exponentielles Backoff (1s → 2s → 4s → 8s...) mit ±25% Jitter, um Thundering Herd zu verhindern. Wenn die API einen `retry-after`-Header bereitstellt, wird dieser stattdessen verwendet.
##### get_rate_limit_info()
Ruft die aktuellen Rate-Limit-Informationen aus der letzten API-Antwort ab.
```python
rate_limit_info = client.get_rate_limit_info()
if rate_limit_info:
print("Limit:", rate_limit_info.limit)
print("Remaining:", rate_limit_info.remaining)
print("Reset:", datetime.fromtimestamp(rate_limit_info.reset))
```
**Rückgabewert:** `RateLimitInfo | None`
##### get_usage_limits()
Ruft aktuelle Nutzungslimits und Kontingentinformationen für Ihr Konto ab.
```python
limits = client.get_usage_limits()
print("Sync requests remaining:", limits.rate_limit["sync"]["remaining"])
print("Async requests remaining:", limits.rate_limit["async"]["remaining"])
print("Current period cost:", limits.usage["currentPeriodCost"])
print("Plan:", limits.usage["plan"])
```
**Rückgabewert:** `UsageLimits`
**Antwortstruktur:**
```python
{
"success": bool,
"rateLimit": {
"sync": {
"isLimited": bool,
"limit": int,
"remaining": int,
"resetAt": str
},
"async": {
"isLimited": bool,
"limit": int,
"remaining": int,
"resetAt": str
},
"authType": str # 'api' or 'manual'
},
"usage": {
"currentPeriodCost": float,
"limit": float,
"plan": str # e.g., 'free', 'pro'
}
}
```
##### set_api_key()
Aktualisiert den API-Schlüssel.
```python
client.set_api_key("new-api-key")
```
##### set_base_url()
Aktualisiert die Basis-URL.
```python
client.set_base_url("https://my-custom-domain.com")
```
##### close()
Schließt die zugrunde liegende HTTP-Sitzung.
```python
client.close()
```
## Datenklassen
### WorkflowExecutionResult
```python
@dataclass
class WorkflowExecutionResult:
success: bool
output: Optional[Any] = None
error: Optional[str] = None
logs: Optional[List[Any]] = None
metadata: Optional[Dict[str, Any]] = None
trace_spans: Optional[List[Any]] = None
total_duration: Optional[float] = None
```
### AsyncExecutionResult
```python
@dataclass
class AsyncExecutionResult:
success: bool
task_id: str
status: str # 'queued'
created_at: str
links: Dict[str, str] # e.g., {"status": "/api/jobs/{taskId}"}
```
### WorkflowStatus
```python
@dataclass
class WorkflowStatus:
is_deployed: bool
deployed_at: Optional[str] = None
needs_redeployment: bool = False
```
### RateLimitInfo
```python
@dataclass
class RateLimitInfo:
limit: int
remaining: int
reset: int
retry_after: Optional[int] = None
```
### UsageLimits
```python
@dataclass
class UsageLimits:
success: bool
rate_limit: Dict[str, Any]
usage: Dict[str, Any]
```
### SimStudioError
```python
class SimStudioError(Exception):
def __init__(self, message: str, code: Optional[str] = None, status: Optional[int] = None):
super().__init__(message)
self.code = code
self.status = status
```
**Häufige Fehlercodes:**
- `UNAUTHORIZED`: Ungültiger API-Schlüssel
- `TIMEOUT`: Zeitüberschreitung der Anfrage
- `RATE_LIMIT_EXCEEDED`: Ratenlimit überschritten
- `USAGE_LIMIT_EXCEEDED`: Nutzungslimit überschritten
- `EXECUTION_ERROR`: Workflow-Ausführung fehlgeschlagen
## Beispiele
### Grundlegende Workflow-Ausführung
<Steps>
<Step title="Client initialisieren">
Richten Sie den SimStudioClient mit Ihrem API-Schlüssel ein.
</Step>
<Step title="Workflow validieren">
Prüfen Sie, ob der Workflow bereitgestellt und zur Ausführung bereit ist.
</Step>
<Step title="Workflow ausführen">
Führen Sie den Workflow mit Ihren Eingabedaten aus.
</Step>
<Step title="Ergebnis verarbeiten">
Verarbeiten Sie das Ausführungsergebnis und behandeln Sie eventuelle Fehler.
</Step>
</Steps>
```python
import os
from simstudio import SimStudioClient
client = SimStudioClient(api_key=os.getenv("SIM_API_KEY"))
def run_workflow():
try:
# Check if workflow is ready
is_ready = client.validate_workflow("my-workflow-id")
if not is_ready:
raise Exception("Workflow is not deployed or ready")
# Execute the workflow
result = client.execute_workflow(
"my-workflow-id",
input_data={
"message": "Process this data",
"user_id": "12345"
}
)
if result.success:
print("Output:", result.output)
print("Duration:", result.metadata.get("duration") if result.metadata else None)
else:
print("Workflow failed:", result.error)
except Exception as error:
print("Error:", error)
run_workflow()
```
### Fehlerbehandlung
Behandeln Sie verschiedene Fehlertypen, die während der Workflow-Ausführung auftreten können:
```python
from simstudio import SimStudioClient, SimStudioError
import os
client = SimStudioClient(api_key=os.getenv("SIM_API_KEY"))
def execute_with_error_handling():
try:
result = client.execute_workflow("workflow-id")
return result
except SimStudioError as error:
if error.code == "UNAUTHORIZED":
print("Invalid API key")
elif error.code == "TIMEOUT":
print("Workflow execution timed out")
elif error.code == "USAGE_LIMIT_EXCEEDED":
print("Usage limit exceeded")
elif error.code == "INVALID_JSON":
print("Invalid JSON in request body")
else:
print(f"Workflow error: {error}")
raise
except Exception as error:
print(f"Unexpected error: {error}")
raise
```
### Verwendung des Context-Managers
Verwenden Sie den Client als Context-Manager, um die Ressourcenbereinigung automatisch zu handhaben:
```python
from simstudio import SimStudioClient
import os
# Using context manager to automatically close the session
with SimStudioClient(api_key=os.getenv("SIM_API_KEY")) as client:
result = client.execute_workflow("workflow-id")
print("Result:", result)
# Session is automatically closed here
```
### Batch-Workflow-Ausführung
Führen Sie mehrere Workflows effizient aus:
```python
from simstudio import SimStudioClient
import os
client = SimStudioClient(api_key=os.getenv("SIM_API_KEY"))
def execute_workflows_batch(workflow_data_pairs):
"""Execute multiple workflows with different input data."""
results = []
for workflow_id, input_data in workflow_data_pairs:
try:
# Validate workflow before execution
if not client.validate_workflow(workflow_id):
print(f"Skipping {workflow_id}: not deployed")
continue
result = client.execute_workflow(workflow_id, input_data)
results.append({
"workflow_id": workflow_id,
"success": result.success,
"output": result.output,
"error": result.error
})
except Exception as error:
results.append({
"workflow_id": workflow_id,
"success": False,
"error": str(error)
})
return results
# Example usage
workflows = [
("workflow-1", {"type": "analysis", "data": "sample1"}),
("workflow-2", {"type": "processing", "data": "sample2"}),
]
results = execute_workflows_batch(workflows)
for result in results:
print(f"Workflow {result['workflow_id']}: {'Success' if result['success'] else 'Failed'}")
```
### Asynchrone Workflow-Ausführung
Führen Sie Workflows asynchron für langwierige Aufgaben aus:
```python
import os
import time
from simstudio import SimStudioClient
client = SimStudioClient(api_key=os.getenv("SIM_API_KEY"))
def execute_async():
try:
# Start async execution
result = client.execute_workflow(
"workflow-id",
input_data={"data": "large dataset"},
async_execution=True # Execute asynchronously
)
# Check if result is an async execution
if hasattr(result, 'task_id'):
print(f"Task ID: {result.task_id}")
print(f"Status endpoint: {result.links['status']}")
# Poll for completion
status = client.get_job_status(result.task_id)
while status["status"] in ["queued", "processing"]:
print(f"Current status: {status['status']}")
time.sleep(2) # Wait 2 seconds
status = client.get_job_status(result.task_id)
if status["status"] == "completed":
print("Workflow completed!")
print(f"Output: {status['output']}")
print(f"Duration: {status['metadata']['duration']}")
else:
print(f"Workflow failed: {status['error']}")
except Exception as error:
print(f"Error: {error}")
execute_async()
```
### Ratenlimitierung und Wiederholung
Behandeln Sie Ratenbegrenzungen automatisch mit exponentiellem Backoff:
```python
import os
from simstudio import SimStudioClient, SimStudioError
client = SimStudioClient(api_key=os.getenv("SIM_API_KEY"))
def execute_with_retry_handling():
try:
# Automatically retries on rate limit
result = client.execute_with_retry(
"workflow-id",
input_data={"message": "Process this"},
max_retries=5,
initial_delay=1.0,
max_delay=60.0,
backoff_multiplier=2.0
)
print(f"Success: {result}")
except SimStudioError as error:
if error.code == "RATE_LIMIT_EXCEEDED":
print("Rate limit exceeded after all retries")
# Check rate limit info
rate_limit_info = client.get_rate_limit_info()
if rate_limit_info:
from datetime import datetime
reset_time = datetime.fromtimestamp(rate_limit_info.reset)
print(f"Rate limit resets at: {reset_time}")
execute_with_retry_handling()
```
### Nutzungsüberwachung
Überwachen Sie die Nutzung und Limits Ihres Kontos:
```python
import os
from simstudio import SimStudioClient
client = SimStudioClient(api_key=os.getenv("SIM_API_KEY"))
def check_usage():
try:
limits = client.get_usage_limits()
print("=== Rate Limits ===")
print("Sync requests:")
print(f" Limit: {limits.rate_limit['sync']['limit']}")
print(f" Remaining: {limits.rate_limit['sync']['remaining']}")
print(f" Resets at: {limits.rate_limit['sync']['resetAt']}")
print(f" Is limited: {limits.rate_limit['sync']['isLimited']}")
print("\nAsync requests:")
print(f" Limit: {limits.rate_limit['async']['limit']}")
print(f" Remaining: {limits.rate_limit['async']['remaining']}")
print(f" Resets at: {limits.rate_limit['async']['resetAt']}")
print(f" Is limited: {limits.rate_limit['async']['isLimited']}")
print("\n=== Usage ===")
print(f"Current period cost: ${limits.usage['currentPeriodCost']:.2f}")
print(f"Limit: ${limits.usage['limit']:.2f}")
print(f"Plan: {limits.usage['plan']}")
percent_used = (limits.usage['currentPeriodCost'] / limits.usage['limit']) * 100
print(f"Usage: {percent_used:.1f}%")
if percent_used > 80:
print("⚠️ Warning: You are approaching your usage limit!")
except Exception as error:
print(f"Error checking usage: {error}")
check_usage()
```
### Streaming-Workflow-Ausführung
Führen Sie Workflows mit Echtzeit-Streaming-Antworten aus:
```python
from simstudio import SimStudioClient
import os
client = SimStudioClient(api_key=os.getenv("SIM_API_KEY"))
def execute_with_streaming():
"""Execute workflow with streaming enabled."""
try:
# Enable streaming for specific block outputs
result = client.execute_workflow(
"workflow-id",
input_data={"message": "Count to five"},
stream=True,
selected_outputs=["agent1.content"] # Use blockName.attribute format
)
print("Workflow result:", result)
except Exception as error:
print("Error:", error)
execute_with_streaming()
```
Die Streaming-Antwort folgt dem Server-Sent-Events- (SSE-) Format:
```
data: {"blockId":"7b7735b9-19e5-4bd6-818b-46aae2596e9f","chunk":"One"}
data: {"blockId":"7b7735b9-19e5-4bd6-818b-46aae2596e9f","chunk":", two"}
data: {"event":"done","success":true,"output":{},"metadata":{"duration":610}}
data: [DONE]
```
**Flask-Streaming-Beispiel:**
```python
from flask import Flask, Response, stream_with_context
import requests
import json
import os
app = Flask(__name__)
@app.route('/stream-workflow')
def stream_workflow():
"""Stream workflow execution to the client."""
def generate():
response = requests.post(
'https://sim.ai/api/workflows/WORKFLOW_ID/execute',
headers={
'Content-Type': 'application/json',
'X-API-Key': os.getenv('SIM_API_KEY')
},
json={
'message': 'Generate a story',
'stream': True,
'selectedOutputs': ['agent1.content']
},
stream=True
)
for line in response.iter_lines():
if line:
decoded_line = line.decode('utf-8')
if decoded_line.startswith('data: '):
data = decoded_line[6:] # Remove 'data: ' prefix
if data == '[DONE]':
break
try:
parsed = json.loads(data)
if 'chunk' in parsed:
yield f"data: {json.dumps(parsed)}\n\n"
elif parsed.get('event') == 'done':
yield f"data: {json.dumps(parsed)}\n\n"
print("Execution complete:", parsed.get('metadata'))
except json.JSONDecodeError:
pass
return Response(
stream_with_context(generate()),
mimetype='text/event-stream'
)
if __name__ == '__main__':
app.run(debug=True)
```
### Umgebungs­konfiguration
Konfigurieren Sie den Client mit Umgebungsvariablen:
<Tabs items={['Development', 'Production']}>
<Tab value="Development">
```python
import os
from simstudio import SimStudioClient
# Development configuration
client = SimStudioClient(
api_key=os.getenv("SIM_API_KEY")
base_url=os.getenv("SIM_BASE_URL", "https://sim.ai")
)
```
</Tab>
<Tab value="Production">
```python
import os
from simstudio import SimStudioClient
# Production configuration with error handling
api_key = os.getenv("SIM_API_KEY")
if not api_key:
raise ValueError("SIM_API_KEY environment variable is required")
client = SimStudioClient(
api_key=api_key,
base_url=os.getenv("SIM_BASE_URL", "https://sim.ai")
)
```
</Tab>
</Tabs>
## Ihren API-Schlüssel erhalten
<Steps>
<Step title="Bei Sim anmelden">
Navigieren Sie zu [Sim](https://sim.ai) und melden Sie sich in Ihrem Konto an.
</Step>
<Step title="Workflow öffnen">
Navigieren Sie zu dem Workflow, den Sie programmatisch ausführen möchten.
</Step>
<Step title="Workflow bereitstellen">
Klicken Sie auf "Bereitstellen", um Ihren Workflow bereitzustellen, falls dies noch nicht geschehen ist.
</Step>
<Step title="API-Schlüssel erstellen oder auswählen">
Wählen oder erstellen Sie während des Bereitstellungsprozesses einen API-Schlüssel.
</Step>
<Step title="API-Schlüssel kopieren">
Kopieren Sie den API-Schlüssel, um ihn in Ihrer Python-Anwendung zu verwenden.
</Step>
</Steps>
## Voraussetzungen
- Python 3.8+
- requests >= 2.25.0
## Lizenz
Apache-2.0
File diff suppressed because it is too large Load Diff
+160
View File
@@ -0,0 +1,160 @@
---
title: Agent
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
import { Image } from '@/components/ui/image'
Der Agent-Block verbindet deinen Workflow mit Large Language Models (LLMs). Er verarbeitet natürlichsprachliche Eingaben, ruft externe Tools auf und generiert strukturierte oder unstrukturierte Ausgaben.
<div className="flex justify-center">
<Image
src="/static/blocks/agent.png"
alt="Agent-Block-Konfiguration"
width={500}
height={400}
className="my-6"
/>
</div>
## Konfigurationsoptionen
### System-Prompt
Der System-Prompt legt die Betriebsparameter und Verhaltenseinschränkungen des Agenten fest. Diese Konfiguration definiert die Rolle des Agenten, die Antwortmethodik und die Verarbeitungsgrenzen für alle eingehenden Anfragen.
```markdown
You are a helpful assistant that specializes in financial analysis.
Always provide clear explanations and cite sources when possible.
When responding to questions about investments, include risk disclaimers.
```
### Benutzer-Prompt
Der Benutzer-Prompt stellt die primären Eingabedaten für die Inferenzverarbeitung dar. Dieser Parameter akzeptiert natürlichsprachlichen Text oder strukturierte Daten, die der Agent analysieren und auf die er reagieren wird. Zu den Eingabequellen gehören:
- **Statische Konfiguration**: Direkte Texteingabe, die in der Block-Konfiguration angegeben ist
- **Dynamische Eingabe**: Daten, die von vorgelagerten Blöcken über Verbindungsschnittstellen übergeben werden
- **Laufzeitgenerierung**: Programmatisch generierte Inhalte während der Workflow-Ausführung
### Modellauswahl
Der Agent-Block unterstützt mehrere LLM-Anbieter über eine einheitliche Inferenzschnittstelle. Verfügbare Modelle umfassen:
- **OpenAI**: GPT-5.1, GPT-5, GPT-4o, o1, o3, o4-mini, gpt-4.1
- **Anthropic**: Claude 4.5 Sonnet, Claude Opus 4.1
- **Google**: Gemini 2.5 Pro, Gemini 2.0 Flash
- **Andere Anbieter**: Groq, Cerebras, xAI, Azure OpenAI, OpenRouter
- **Lokale Modelle**: Ollama oder VLLM-kompatible Modelle
### Temperatur
Steuert die Zufälligkeit und Kreativität der Antworten:
- **Niedrig (0-0,3)**: Deterministisch und fokussiert. Am besten für faktische Aufgaben und Genauigkeit.
- **Mittel (0,3-0,7)**: Ausgewogene Kreativität und Fokus. Gut für allgemeine Verwendung.
- **Hoch (0,7-2,0)**: Kreativ und abwechslungsreich. Ideal für Brainstorming und Content-Generierung.
### API-Schlüssel
Ihr API-Schlüssel für den ausgewählten LLM-Anbieter. Dieser wird sicher gespeichert und für die Authentifizierung verwendet.
### Tools
Erweitern Sie die Fähigkeiten des Agenten mit externen Integrationen. Wählen Sie aus über 60 vorgefertigten Tools oder definieren Sie benutzerdefinierte Funktionen.
**Verfügbare Kategorien:**
- **Kommunikation**: Gmail, Slack, Telegram, WhatsApp, Microsoft Teams
- **Datenquellen**: Notion, Google Sheets, Airtable, Supabase, Pinecone
- **Webdienste**: Firecrawl, Google Search, Exa AI, Browser-Automatisierung
- **Entwicklung**: GitHub, Jira, Linear
- **KI-Dienste**: OpenAI, Perplexity, Hugging Face, ElevenLabs
**Ausführungsmodi:**
- **Auto**: Modell entscheidet kontextbasiert, wann Tools verwendet werden
- **Erforderlich**: Tool muss bei jeder Anfrage aufgerufen werden
- **Keine**: Tool verfügbar, aber dem Modell nicht vorgeschlagen
### Antwortformat
Der Parameter für das Antwortformat erzwingt die Generierung strukturierter Ausgaben durch JSON-Schema-Validierung. Dies gewährleistet konsistente, maschinenlesbare Antworten, die vordefinierten Datenstrukturen entsprechen:
```json
{
"type": "object",
"properties": {
"sentiment": {
"type": "string",
"enum": ["positive", "neutral", "negative"]
},
"summary": {
"type": "string",
"description": "Brief summary of the content"
}
},
"required": ["sentiment", "summary"]
}
```
Diese Konfiguration beschränkt die Ausgabe des Modells auf die Einhaltung des angegebenen Schemas, verhindert Freitextantworten und stellt die Generierung strukturierter Daten sicher.
### Zugriff auf Ergebnisse
Nach Abschluss eines Agenten können Sie auf seine Ausgaben zugreifen:
- **response**: Der Antworttext oder die strukturierten Daten des Agenten
- **usage**: Token-Nutzungsstatistiken (Prompt, Completion, Gesamt)
- **toolExecutions**: Details zu allen Tools, die der Agent während der Ausführung verwendet hat
- **estimatedCost**: Geschätzte Kosten des API-Aufrufs (falls verfügbar)
## Erweiterte Funktionen
### Memory + Agent: Gesprächsverlauf
Verwenden Sie einen memory Block mit einer konsistenten memoryId (zum Beispiel, conversationHistory), um Nachrichten zwischen Durchläufen zu speichern und diesen Verlauf in den Prompt des Agenten einzubeziehen.
- Fügen Sie die Nachricht des Benutzers vor dem Agenten hinzu
- Lesen Sie den Gesprächsverlauf für den Kontext
- Hängen Sie die Antwort des Agenten nach dessen Ausführung an
Siehe den [`Memory`](/tools/memory) Blockverweis für Details.
## Ausgaben
- **`<agent.content>`**: Antworttext des Agenten
- **`<agent.tokens>`**: Token-Nutzungsstatistiken
- **`<agent.tool_calls>`**: Details zur Tool-Ausführung
- **`<agent.cost>`**: Geschätzte Kosten des API-Aufrufs
## Beispielanwendungsfälle
**Automatisierung des Kundenservice** - Bearbeitung von Anfragen mit Datenbank- und Tool-Zugriff
```
API (Ticket) → Agent (Postgres, KB, Linear) → Gmail (Reply) → Memory (Save)
```
**Multi-Modell-Inhaltsanalyse** - Analyse von Inhalten mit verschiedenen KI-Modellen
```
Function (Process) → Agent (GPT-4o Technical) → Agent (Claude Sentiment) → Function (Report)
```
**Tool-gestützter Rechercheassistent** - Recherche mit Websuche und Dokumentenzugriff
```
Input → Agent (Google Search, Notion) → Function (Compile Report)
```
## Bewährte Praktiken
- **Sei spezifisch in System-Prompts**: Definiere die Rolle, den Ton und die Einschränkungen des Agenten klar. Je spezifischer deine Anweisungen sind, desto besser kann der Agent seinen vorgesehenen Zweck erfüllen.
- **Wähle die richtige Temperatureinstellung**: Verwende niedrigere Temperatureinstellungen (0-0,3), wenn Genauigkeit wichtig ist, oder erhöhe die Temperatur (0,7-2,0) für kreativere oder vielfältigere Antworten
- **Nutze Tools effektiv**: Integriere Tools, die den Zweck des Agenten ergänzen und seine Fähigkeiten erweitern. Sei selektiv bei der Auswahl der Tools, um den Agenten nicht zu überfordern. Für Aufgaben mit wenig Überschneidung verwende einen anderen Agent-Block für die besten Ergebnisse.
## Best Practices
- **Seien Sie spezifisch in System-Prompts**: Definieren Sie die Rolle, den Ton und die Grenzen des Agenten klar. Je spezifischer Ihre Anweisungen sind, desto besser kann der Agent seinen beabsichtigten Zweck erfüllen.
- **Wählen Sie die richtige Temperatureinstellung**: Verwenden Sie niedrigere Temperatureinstellungen (00,3), wenn Genauigkeit wichtig ist, oder erhöhen Sie die Temperatur (0,72,0) für kreativere oder vielfältigere Antworten
- **Nutzen Sie Tools effektiv**: Integrieren Sie Tools, die den Zweck des Agenten ergänzen und seine Fähigkeiten erweitern. Seien Sie selektiv bei der Auswahl der Tools, um den Agenten nicht zu überfordern. Verwenden Sie für Aufgaben mit geringer Überschneidung einen weiteren Agent-Block für die besten Ergebnisse.
+145
View File
@@ -0,0 +1,145 @@
---
title: API
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
import { Image } from '@/components/ui/image'
Der API-Block verbindet Ihren Workflow mit externen Diensten durch HTTP-Anfragen. Unterstützt GET, POST, PUT, DELETE und PATCH Methoden für die Interaktion mit REST-APIs.
<div className="flex justify-center">
<Image
src="/static/blocks/api.png"
alt="API-Block"
width={500}
height={400}
className="my-6"
/>
</div>
## Konfigurationsoptionen
### URL
Die Endpunkt-URL für die API-Anfrage. Diese kann sein:
- Eine statische URL, die direkt im Block eingegeben wird
- Eine dynamische URL, die mit der Ausgabe eines anderen Blocks verbunden ist
- Eine URL mit Pfadparametern
### Methode
Wählen Sie die HTTP-Methode für Ihre Anfrage:
- **GET**: Daten vom Server abrufen
- **POST**: Daten an den Server senden, um eine Ressource zu erstellen
- **PUT**: Eine bestehende Ressource auf dem Server aktualisieren
- **DELETE**: Eine Ressource vom Server entfernen
- **PATCH**: Eine bestehende Ressource teilweise aktualisieren
### Abfrageparameter
Definieren Sie Schlüssel-Wert-Paare, die als Abfrageparameter an die URL angehängt werden. Zum Beispiel:
```
Key: apiKey
Value: your_api_key_here
Key: limit
Value: 10
```
Diese würden der URL als `?apiKey=your_api_key_here&limit=10` hinzugefügt.
### Header
Konfigurieren Sie HTTP-Header für Ihre Anfrage. Häufige Header sind:
```
Key: Content-Type
Value: application/json
Key: Authorization
Value: Bearer your_token_here
```
### Anfragekörper
Für Methoden, die einen Anfragekörper unterstützen (POST, PUT, PATCH), können Sie die zu sendenden Daten definieren. Der Körper kann sein:
- JSON-Daten, die direkt im Block eingegeben werden
- Daten, die mit der Ausgabe eines anderen Blocks verbunden sind
- Dynamisch während der Workflow-Ausführung generiert
### Zugriff auf Ergebnisse
Nach Abschluss einer API-Anfrage können Sie auf folgende Ausgaben zugreifen:
- **`<api.data>`**: Die Antwortdaten vom API
- **`<api.status>`**: HTTP-Statuscode (200, 404, 500, usw.)
- **`<api.headers>`**: Antwort-Header vom Server
- **`<api.error>`**: Fehlerdetails, falls die Anfrage fehlgeschlagen ist
## Erweiterte Funktionen
### Dynamische URL-Konstruktion
Erstellen Sie URLs dynamisch mit Variablen aus vorherigen Blöcken:
```javascript
// In a Function block before the API
const userId = <start.userId>;
const apiUrl = `https://api.example.com/users/${userId}/profile`;
```
### Anfrage-Wiederholungen
Der API-Block verarbeitet automatisch:
- Netzwerk-Timeouts mit exponentiellem Backoff
- Antworten bei Ratenbegrenzung (429-Statuscodes)
- Serverfehler (5xx-Statuscodes) mit Wiederholungslogik
- Verbindungsfehler mit Wiederverbindungsversuchen
### Antwortvalidierung
Validieren Sie API-Antworten vor der Verarbeitung:
```javascript
// In a Function block after the API
if (<api.status> === 200) {
const data = <api.data>;
// Process successful response
} else {
// Handle error response
console.error(`API Error: ${<api.status>}`);
}
```
## Ausgaben
- **`<api.data>`**: Antwortdaten vom API
- **`<api.status>`**: HTTP-Statuscode
- **`<api.headers>`**: Antwort-Header
- **`<api.error>`**: Fehlerdetails bei fehlgeschlagener Anfrage
## Anwendungsbeispiele
**Benutzerprofildaten abrufen** - Benutzerinformationen von externem Dienst abrufen
```
Function (Build ID) → API (GET /users/{id}) → Function (Format) → Response
```
**Zahlungsabwicklung** - Zahlung über die Stripe-API verarbeiten
```
Function (Validate) → API (Stripe) → Condition (Success) → Supabase (Update)
```
## Bewährte Praktiken
- **Umgebungsvariablen für sensible Daten verwenden**: Keine API-Schlüssel oder Anmeldedaten im Code festlegen
- **Fehler elegant behandeln**: Fehlerbehandlungslogik für fehlgeschlagene Anfragen einbinden
- **Antworten validieren**: Statuscode und Antwortformate vor der Datenverarbeitung prüfen
- **Ratenbegrenzungen respektieren**: Achten Sie auf API-Ratenbegrenzungen und implementieren Sie angemessene Drosselung
@@ -0,0 +1,149 @@
---
title: Bedingung
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
import { Image } from '@/components/ui/image'
Der Bedingungsblock verzweigt die Workflow-Ausführung basierend auf booleschen Ausdrücken. Bewerten Sie Bedingungen anhand vorheriger Block-Ausgaben und leiten Sie zu verschiedenen Pfaden weiter, ohne dass ein LLM erforderlich ist.
<div className="flex justify-center">
<Image
src="/static/blocks/condition.png"
alt="Bedingungsblock"
width={500}
height={400}
className="my-6"
/>
</div>
## Konfigurationsoptionen
### Bedingungen
Definieren Sie eine oder mehrere Bedingungen, die ausgewertet werden. Jede Bedingung umfasst:
- **Ausdruck**: Ein JavaScript/TypeScript-Ausdruck, der zu wahr oder falsch ausgewertet wird
- **Pfad**: Der Zielblock, zu dem weitergeleitet werden soll, wenn die Bedingung wahr ist
- **Beschreibung**: Optionale Erklärung, was die Bedingung prüft
Sie können mehrere Bedingungen erstellen, die der Reihe nach ausgewertet werden, wobei die erste übereinstimmende Bedingung den Ausführungspfad bestimmt.
### Format für Bedingungsausdrücke
Bedingungen verwenden JavaScript-Syntax und können auf Eingabewerte aus vorherigen Blöcken verweisen.
<Tabs items={['Schwellenwert', 'Textanalyse', 'Mehrere Bedingungen']}>
<Tab>
```javascript
// Check if a score is above a threshold
<agent.score> > 75
```
</Tab>
<Tab>
```javascript
// Check if a text contains specific keywords
<agent.text>.includes('urgent') || <agent.text>.includes('emergency')
```
</Tab>
<Tab>
```javascript
// Check multiple conditions
<agent.age> >= 18 && <agent.country> === 'US'
```
</Tab>
</Tabs>
### Zugriff auf Ergebnisse
Nach der Auswertung einer Bedingung können Sie auf folgende Ausgaben zugreifen:
- **condition.result**: Boolesches Ergebnis der Bedingungsauswertung
- **condition.matched_condition**: ID der übereinstimmenden Bedingung
- **condition.content**: Beschreibung des Auswertungsergebnisses
- **condition.path**: Details zum gewählten Routing-Ziel
## Erweiterte Funktionen
### Komplexe Ausdrücke
Verwenden Sie JavaScript-Operatoren und -Funktionen in Bedingungen:
```javascript
// String operations
<user.email>.endsWith('@company.com')
// Array operations
<api.tags>.includes('urgent')
// Mathematical operations
<agent.confidence> * 100 > 85
// Date comparisons
new Date(<api.created_at>) > new Date('2024-01-01')
```
### Auswertung mehrerer Bedingungen
Bedingungen werden der Reihe nach ausgewertet, bis eine übereinstimmt:
```javascript
// Condition 1: Check for high priority
<ticket.priority> === 'high'
// Condition 2: Check for urgent keywords
<ticket.subject>.toLowerCase().includes('urgent')
// Condition 3: Default fallback
true
```
### Fehlerbehandlung
Bedingungen behandeln automatisch:
- Undefinierte oder Null-Werte mit sicherer Auswertung
- Typabweichungen mit geeigneten Fallbacks
- Ungültige Ausdrücke mit Fehlerprotokollierung
- Fehlende Variablen mit Standardwerten
## Ausgaben
- **`<condition.result>`**: Boolesches Ergebnis der Auswertung
- **`<condition.matched_condition>`**: ID der übereinstimmenden Bedingung
- **`<condition.content>`**: Beschreibung des Auswertungsergebnisses
- **`<condition.path>`**: Details zum gewählten Routing-Ziel
## Beispielanwendungsfälle
**Kundenservice-Routing** - Tickets basierend auf Priorität weiterleiten
```
API (Ticket) → Condition (priority === 'high') → Agent (Escalation) or Agent (Standard)
```
**Inhaltsmoderation** - Inhalte basierend auf Analysen filtern
```
Agent (Analyze) → Condition (toxicity > 0.7) → Moderation or Publish
```
**Benutzer-Onboarding-Ablauf** - Onboarding basierend auf Benutzertyp personalisieren
```
Function (Process) → Condition (account_type === 'enterprise') → Advanced or Simple
```
## Bewährte Praktiken
- **Bedingungen korrekt anordnen**: Platzieren Sie spezifischere Bedingungen vor allgemeinen, um sicherzustellen, dass spezifische Logik Vorrang vor Fallbacks hat
- **Verwenden Sie den Else-Zweig bei Bedarf**: Wenn keine Bedingungen übereinstimmen und der Else-Zweig nicht verbunden ist, endet der Workflow-Zweig ordnungsgemäß. Verbinden Sie den Else-Zweig, wenn Sie einen Fallback-Pfad für nicht übereinstimmende Fälle benötigen
- **Halten Sie Ausdrücke einfach**: Verwenden Sie klare, unkomplizierte boolesche Ausdrücke für bessere Lesbarkeit und einfachere Fehlersuche
- **Dokumentieren Sie Ihre Bedingungen**: Fügen Sie Beschreibungen hinzu, um den Zweck jeder Bedingung für bessere Teamzusammenarbeit und Wartung zu erklären
- **Testen Sie Grenzfälle**: Überprüfen Sie, ob Bedingungen Grenzwerte korrekt behandeln, indem Sie mit Werten an den Grenzen Ihrer Bedingungsbereiche testen
@@ -0,0 +1,96 @@
---
title: Evaluator
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
import { Image } from '@/components/ui/image'
Der Evaluator-Block nutzt KI, um die Inhaltsqualität anhand benutzerdefinierter Metriken zu bewerten. Perfekt für Qualitätskontrolle, A/B-Tests und um sicherzustellen, dass KI-Ausgaben bestimmte Standards erfüllen.
<div className="flex justify-center">
<Image
src="/static/blocks/evaluator.png"
alt="Evaluator-Block-Konfiguration"
width={500}
height={400}
className="my-6"
/>
</div>
## Konfigurationsoptionen
### Bewertungsmetriken
Definieren Sie benutzerdefinierte Metriken, anhand derer Inhalte bewertet werden. Jede Metrik umfasst:
- **Name**: Eine kurze Bezeichnung für die Metrik
- **Beschreibung**: Eine detaillierte Erklärung, was die Metrik misst
- **Bereich**: Der numerische Bereich für die Bewertung (z.B. 1-5, 0-10)
Beispielmetriken:
```
Accuracy (1-5): How factually accurate is the content?
Clarity (1-5): How clear and understandable is the content?
Relevance (1-5): How relevant is the content to the original query?
```
### Inhalt
Der zu bewertende Inhalt. Dies kann sein:
- Direkt in der Blockkonfiguration bereitgestellt
- Verbunden mit der Ausgabe eines anderen Blocks (typischerweise ein Agent-Block)
- Dynamisch während der Workflow-Ausführung generiert
### Modellauswahl
Wählen Sie ein KI-Modell für die Durchführung der Bewertung:
- **OpenAI**: GPT-4o, o1, o3, o4-mini, gpt-4.1
- **Anthropic**: Claude 3.7 Sonnet
- **Google**: Gemini 2.5 Pro, Gemini 2.0 Flash
- **Andere Anbieter**: Groq, Cerebras, xAI, DeepSeek
- **Lokale Modelle**: Ollama oder VLLM-kompatible Modelle
Verwenden Sie Modelle mit starken Argumentationsfähigkeiten wie GPT-4o oder Claude 3.7 Sonnet für beste Ergebnisse.
### API-Schlüssel
Ihr API-Schlüssel für den ausgewählten LLM-Anbieter. Dieser wird sicher gespeichert und für die Authentifizierung verwendet.
## Beispielanwendungsfälle
**Bewertung der Inhaltsqualität** - Inhalte vor der Veröffentlichung bewerten
```
Agent (Generate) → Evaluator (Score) → Condition (Check threshold) → Publish or Revise
```
**A/B-Tests von Inhalten** - Vergleich mehrerer KI-generierter Antworten
```
Parallel (Variations) → Evaluator (Score Each) → Function (Select Best) → Response
```
**Qualitätskontrolle im Kundenservice** - Sicherstellen, dass Antworten Qualitätsstandards erfüllen
```
Agent (Support Response) → Evaluator (Score) → Function (Log) → Condition (Review if Low)
```
## Ausgaben
- **`<evaluator.content>`**: Zusammenfassung der Bewertung mit Punktzahlen
- **`<evaluator.model>`**: Für die Bewertung verwendetes Modell
- **`<evaluator.tokens>`**: Statistik zur Token-Nutzung
- **`<evaluator.cost>`**: Geschätzte Bewertungskosten
## Best Practices
- **Verwenden Sie spezifische Metrikbeschreibungen**: Definieren Sie klar, was jede Metrik misst, um genauere Bewertungen zu erhalten
- **Wählen Sie geeignete Bereiche**: Wählen Sie Bewertungsbereiche, die ausreichend Granularität bieten, ohne zu komplex zu sein
- **Verbinden Sie mit Agent-Blöcken**: Verwenden Sie Evaluator-Blöcke, um die Ausgaben von Agent-Blöcken zu bewerten und Feedback-Schleifen zu erstellen
- **Verwenden Sie konsistente Metriken**: Für vergleichende Analysen sollten Sie konsistente Metriken über ähnliche Bewertungen hinweg beibehalten
- **Kombinieren Sie mehrere Metriken**: Verwenden Sie verschiedene Metriken, um eine umfassende Bewertung zu erhalten
@@ -0,0 +1,76 @@
---
title: Funktion
---
import { Image } from '@/components/ui/image'
Der Funktionsblock führt benutzerdefinierten JavaScript- oder TypeScript-Code in Ihren Workflows aus. Transformieren Sie Daten, führen Sie Berechnungen durch oder implementieren Sie benutzerdefinierte Logik.
<div className="flex justify-center">
<Image
src="/static/blocks/function.png"
alt="Funktionsblock mit Code-Editor"
width={500}
height={400}
className="my-6"
/>
</div>
## Ausgaben
- **`<function.result>`**: Der von Ihrer Funktion zurückgegebene Wert
- **`<function.stdout>`**: Console.log()-Ausgabe Ihres Codes
## Beispielanwendungsfälle
**Datenverarbeitungspipeline** - Transformation von API-Antworten in strukturierte Daten
```
API (Fetch) → Function (Process & Validate) → Function (Calculate Metrics) → Response
```
**Implementierung von Geschäftslogik** - Berechnung von Treuepunkten und Stufen
```
Agent (Get History) → Function (Calculate Score) → Function (Determine Tier) → Condition (Route)
```
**Datenvalidierung und -bereinigung** - Validierung und Bereinigung von Benutzereingaben
```
Input → Function (Validate & Sanitize) → API (Save to Database)
```
### Beispiel: Treuepunkte-Rechner
```javascript title="loyalty-calculator.js"
// Process customer data and calculate loyalty score
const { purchaseHistory, accountAge, supportTickets } = <agent>;
// Calculate metrics
const totalSpent = purchaseHistory.reduce((sum, purchase) => sum + purchase.amount, 0);
const purchaseFrequency = purchaseHistory.length / (accountAge / 365);
const ticketRatio = supportTickets.resolved / supportTickets.total;
// Calculate loyalty score (0-100)
const spendScore = Math.min(totalSpent / 1000 * 30, 30);
const frequencyScore = Math.min(purchaseFrequency * 20, 40);
const supportScore = ticketRatio * 30;
const loyaltyScore = Math.round(spendScore + frequencyScore + supportScore);
return {
customer: <agent.name>,
loyaltyScore,
loyaltyTier: loyaltyScore >= 80 ? "Platinum" : loyaltyScore >= 60 ? "Gold" : "Silver",
metrics: { spendScore, frequencyScore, supportScore }
};
```
## Best Practices
- **Funktionen fokussiert halten**: Schreiben Sie Funktionen, die eine Sache gut erledigen, um die Wartbarkeit und Fehlersuche zu verbessern
- **Fehler elegant behandeln**: Verwenden Sie try/catch-Blöcke, um potenzielle Fehler zu behandeln und aussagekräftige Fehlermeldungen bereitzustellen
- **Grenzfälle testen**: Stellen Sie sicher, dass Ihr Code ungewöhnliche Eingaben, Null-Werte und Grenzbedingungen korrekt behandelt
- **Für Leistung optimieren**: Achten Sie bei großen Datensätzen auf die Berechnungskomplexität und den Speicherverbrauch
- **Console.log() zum Debuggen verwenden**: Nutzen Sie die Stdout-Ausgabe zum Debuggen und Überwachen der Funktionsausführung
@@ -0,0 +1,207 @@
---
title: Guardrails
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Image } from '@/components/ui/image'
import { Video } from '@/components/ui/video'
Der Guardrails-Block validiert und schützt Ihre KI-Workflows, indem er Inhalte anhand mehrerer Validierungstypen überprüft. Stellen Sie die Datenqualität sicher, verhindern Sie Halluzinationen, erkennen Sie personenbezogene Daten und erzwingen Sie Formatanforderungen, bevor Inhalte durch Ihren Workflow fließen.
<div className="flex justify-center">
<Image
src="/static/blocks/guardrails.png"
alt="Guardrails-Block"
width={500}
height={400}
className="my-6"
/>
</div>
## Validierungstypen
### JSON-Validierung
Überprüft, ob der Inhalt korrekt formatiertes JSON ist. Perfekt, um sicherzustellen, dass strukturierte LLM-Ausgaben sicher geparst werden können.
**Anwendungsfälle:**
- Validierung von JSON-Antworten aus Agent-Blöcken vor dem Parsen
- Sicherstellen, dass API-Payloads korrekt formatiert sind
- Überprüfung der Integrität strukturierter Daten
**Ausgabe:**
- `passed`: `true` bei gültigem JSON, sonst `false`
- `error`: Fehlermeldung bei fehlgeschlagener Validierung (z.B. "Ungültiges JSON: Unerwartetes Token...")
### Regex-Validierung
Prüft, ob der Inhalt einem bestimmten regulären Ausdrucksmuster entspricht.
**Anwendungsfälle:**
- Validierung von E-Mail-Adressen
- Überprüfung von Telefonnummernformaten
- Verifizierung von URLs oder benutzerdefinierten Kennungen
- Durchsetzung spezifischer Textmuster
**Konfiguration:**
- **Regex-Muster**: Der reguläre Ausdruck, gegen den geprüft wird (z.B. `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$` für E-Mails)
**Ausgabe:**
- `passed`: `true` wenn der Inhalt dem Muster entspricht, sonst `false`
- `error`: Fehlermeldung bei fehlgeschlagener Validierung
### Halluzinationserkennung
Verwendet Retrieval-Augmented Generation (RAG) mit LLM-Bewertung, um zu erkennen, wann KI-generierte Inhalte im Widerspruch zu Ihrer Wissensdatenbank stehen oder nicht darin begründet sind.
**Funktionsweise:**
1. Abfrage Ihrer Wissensdatenbank nach relevantem Kontext
2. Übermittlung sowohl der KI-Ausgabe als auch des abgerufenen Kontexts an ein LLM
3. LLM weist einen Konfidenzwert zu (Skala 0-10)
- **0** = Vollständige Halluzination (völlig unbegründet)
- **10** = Vollständig begründet (komplett durch die Wissensdatenbank gestützt)
4. Validierung besteht, wenn der Wert ≥ Schwellenwert ist (Standard: 3)
**Konfiguration:**
- **Wissensdatenbank**: Wählen Sie aus Ihren vorhandenen Wissensdatenbanken
- **Modell**: Wählen Sie LLM für die Bewertung (erfordert starkes Denkvermögen - GPT-4o, Claude 3.7 Sonnet empfohlen)
- **API-Schlüssel**: Authentifizierung für den ausgewählten LLM-Anbieter (automatisch ausgeblendet für gehostete/Ollama oder VLLM-kompatible Modelle)
- **Vertrauensschwelle**: Mindestpunktzahl zum Bestehen (0-10, Standard: 3)
- **Top K** (Erweitert): Anzahl der abzurufenden Wissensdatenbank-Chunks (Standard: 10)
**Ausgabe:**
- `passed`: `true` wenn Konfidenzwert ≥ Schwellenwert
- `score`: Konfidenzwert (0-10)
- `reasoning`: Erklärung des LLM für den Wert
- `error`: Fehlermeldung bei fehlgeschlagener Validierung
**Anwendungsfälle:**
- Validierung von Agent-Antworten anhand der Dokumentation
- Sicherstellung der faktischen Richtigkeit von Kundendienstantworten
- Überprüfung, ob generierte Inhalte mit dem Quellmaterial übereinstimmen
- Qualitätskontrolle für RAG-Anwendungen
### PII-Erkennung
Erkennt personenbezogene Daten mithilfe von Microsoft Presidio. Unterstützt über 40 Entitätstypen in mehreren Ländern und Sprachen.
<div className="flex justify-center">
<Image
src="/static/blocks/guardrails-2.png"
alt="PII-Erkennungskonfiguration"
width={700}
height={450}
className="my-6"
/>
</div>
**Funktionsweise:**
1. Übergabe des zu validierenden Inhalts (z.B. `<agent1.content>`)
2. Auswahl der zu erkennenden PII-Typen über den Modal-Selektor
3. Auswahl des Erkennungsmodus (Erkennen oder Maskieren)
4. Inhalt wird auf übereinstimmende PII-Entitäten gescannt
5. Gibt Erkennungsergebnisse und optional maskierten Text zurück
<div className="mx-auto w-3/5 overflow-hidden rounded-lg">
<Video src="guardrails.mp4" width={500} height={350} />
</div>
**Konfiguration:**
- **Zu erkennende PII-Typen**: Auswahl aus gruppierten Kategorien über Modal-Selektor
- **Allgemein**: Personenname, E-Mail, Telefon, Kreditkarte, IP-Adresse usw.
- **USA**: Sozialversicherungsnummer, Führerschein, Reisepass usw.
- **UK**: NHS-Nummer, Sozialversicherungsnummer
- **Spanien**: NIF, NIE, CIF
- **Italien**: Steuernummer, Führerschein, Umsatzsteuer-ID
- **Polen**: PESEL, NIP, REGON
- **Singapur**: NRIC/FIN, UEN
- **Australien**: ABN, ACN, TFN, Medicare
- **Indien**: Aadhaar, PAN, Reisepass, Wählernummer
- **Modus**:
- **Erkennen**: Nur PII identifizieren (Standard)
- **Maskieren**: Erkannte PII durch maskierte Werte ersetzen
- **Sprache**: Erkennungssprache (Standard: Englisch)
**Ausgabe:**
- `passed`: `false` wenn ausgewählte PII-Typen erkannt werden
- `detectedEntities`: Array erkannter PII mit Typ, Position und Konfidenz
- `maskedText`: Inhalt mit maskierter PII (nur wenn Modus = "Mask")
- `error`: Fehlermeldung bei fehlgeschlagener Validierung
**Anwendungsfälle:**
- Blockieren von Inhalten mit sensiblen persönlichen Informationen
- Maskieren von personenbezogenen Daten vor der Protokollierung oder Speicherung
- Einhaltung der DSGVO und anderer Datenschutzbestimmungen
- Bereinigung von Benutzereingaben vor der Verarbeitung
## Konfiguration
### Zu validierende Inhalte
Der zu validierende Eingabeinhalt. Dieser stammt typischerweise aus:
- Ausgaben von Agent-Blöcken: `<agent.content>`
- Ergebnisse von Funktionsblöcken: `<function.output>`
- API-Antworten: `<api.output>`
- Jede andere Blockausgabe
### Validierungstyp
Wählen Sie aus vier Validierungstypen:
- **Gültiges JSON**: Prüfen, ob der Inhalt korrekt formatiertes JSON ist
- **Regex-Übereinstimmung**: Überprüfen, ob der Inhalt einem Regex-Muster entspricht
- **Halluzinationsprüfung**: Validierung gegen Wissensdatenbank mit LLM-Bewertung
- **PII-Erkennung**: Erkennung und optional Maskierung personenbezogener Daten
## Ausgaben
Alle Validierungstypen geben zurück:
- **`<guardrails.passed>`**: Boolescher Wert, der angibt, ob die Validierung bestanden wurde
- **`<guardrails.validationType>`**: Der durchgeführte Validierungstyp
- **`<guardrails.input>`**: Die ursprüngliche Eingabe, die validiert wurde
- **`<guardrails.error>`**: Fehlermeldung, wenn die Validierung fehlgeschlagen ist (optional)
Zusätzliche Ausgaben nach Typ:
**Halluzinationsprüfung:**
- **`<guardrails.score>`**: Konfidenzwert (0-10)
- **`<guardrails.reasoning>`**: Erklärung des LLM
**PII-Erkennung:**
- **`<guardrails.detectedEntities>`**: Array erkannter PII-Entitäten
- **`<guardrails.maskedText>`**: Inhalt mit maskierten PII (wenn Modus = "Mask")
## Beispielanwendungsfälle
**JSON vor dem Parsen validieren** - Stellen Sie sicher, dass die Agent-Ausgabe gültiges JSON ist
```
Agent (Generate) → Guardrails (Validate) → Condition (Check passed) → Function (Parse)
```
**Halluzinationen verhindern** - Validieren Sie Kundendienstantworten anhand der Wissensdatenbank
```
Agent (Response) → Guardrails (Check KB) → Condition (Score ≥ 3) → Send or Flag
```
**PII in Benutzereingaben blockieren** - Bereinigen Sie von Benutzern übermittelte Inhalte
```
Input → Guardrails (Detect PII) → Condition (No PII) → Process or Reject
```
## Bewährte Praktiken
- **Verkettung mit Bedingungsblöcken**: Verwenden Sie `<guardrails.passed>`, um die Workflow-Logik basierend auf Validierungsergebnissen zu verzweigen
- **JSON-Validierung vor dem Parsen verwenden**: Validieren Sie immer die JSON-Struktur, bevor Sie versuchen, LLM-Ausgaben zu parsen
- **Geeignete PII-Typen auswählen**: Wählen Sie nur die für Ihren Anwendungsfall relevanten PII-Entitätstypen für bessere Leistung
- **Angemessene Konfidenzgrenzwerte festlegen**: Passen Sie für die Halluzinationserkennung den Grenzwert an Ihre Genauigkeitsanforderungen an (höher = strenger)
- **Starke Modelle für die Halluzinationserkennung verwenden**: GPT-4o oder Claude 3.7 Sonnet bieten genauere Konfidenzwerte
- **PII für die Protokollierung maskieren**: Verwenden Sie den Modus "Mask", wenn Sie Inhalte protokollieren oder speichern müssen, die PII enthalten könnten
- **Regex-Muster testen**: Validieren Sie Ihre Regex-Muster gründlich, bevor Sie sie in der Produktion einsetzen
- **Validierungsfehler überwachen**: Verfolgen Sie `<guardrails.error>`Nachrichten, um häufige Validierungsprobleme zu identifizieren
<Callout type="info">
Die Validierung von Guardrails erfolgt synchron in Ihrem Workflow. Für die Erkennung von Halluzinationen sollten Sie schnellere Modelle (wie GPT-4o-mini) wählen, wenn die Latenz kritisch ist.
</Callout>
@@ -0,0 +1,188 @@
---
title: Human in the Loop
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
import { Image } from '@/components/ui/image'
import { Video } from '@/components/ui/video'
Der Human in the Loop Block pausiert die Workflow-Ausführung und wartet auf menschliches Eingreifen, bevor er fortfährt. Verwenden Sie ihn, um Genehmigungspunkte hinzuzufügen, Feedback zu sammeln oder zusätzliche Eingaben an kritischen Entscheidungspunkten einzuholen.
<div className="flex justify-center">
<Image
src="/static/blocks/hitl-1.png"
alt="Human in the Loop Block Konfiguration"
width={500}
height={400}
className="my-6"
/>
</div>
Wenn die Ausführung diesen Block erreicht, pausiert der Workflow auf unbestimmte Zeit, bis ein Mensch über das Genehmigungsportal, die API oder den Webhook eine Eingabe macht.
<div className="flex justify-center">
<Image
src="/static/blocks/hitl-2.png"
alt="Human in the Loop Genehmigungsportal"
width={700}
height={500}
className="my-6"
/>
</div>
## Konfigurationsoptionen
### Pausierte Ausgabe
Definiert, welche Daten dem Genehmigenden angezeigt werden. Dies ist der Kontext, der im Genehmigungsportal angezeigt wird, um eine fundierte Entscheidung zu ermöglichen.
Verwenden Sie den visuellen Builder oder den JSON-Editor, um die Daten zu strukturieren. Referenzieren Sie Workflow-Variablen mit der `<blockName.output>` Syntax.
```json
{
"customerName": "<agent1.content.name>",
"proposedAction": "<router1.selectedPath>",
"confidenceScore": "<evaluator1.score>",
"generatedEmail": "<agent2.content>"
}
```
### Benachrichtigung
Konfiguriert, wie Genehmigende benachrichtigt werden, wenn eine Genehmigung erforderlich ist. Unterstützte Kanäle sind:
- **Slack** - Nachrichten an Kanäle oder DMs
- **Gmail** - E-Mail mit Genehmigungslink
- **Microsoft Teams** - Team-Kanal-Benachrichtigungen
- **SMS** - Textwarnungen über Twilio
- **Webhooks** - Benutzerdefinierte Benachrichtigungssysteme
Fügen Sie die Genehmigungs-URL (`<blockId.url>`) in Ihre Benachrichtigungsnachrichten ein, damit Genehmigende auf das Portal zugreifen können.
### Fortsetzungseingabe
Definiert die Felder, die Genehmigende bei der Antwort ausfüllen. Diese Daten werden nach der Fortsetzung des Workflows für nachfolgende Blöcke verfügbar.
```json
{
"approved": {
"type": "boolean",
"description": "Approve or reject this request"
},
"comments": {
"type": "string",
"description": "Optional feedback or explanation"
}
}
```
Greifen Sie in nachgelagerten Blöcken auf Wiederaufnahmedaten mit `<blockId.resumeInput.fieldName>` zu.
## Genehmigungsmethoden
<Tabs items={['Genehmigungsportal', 'API', 'Webhook']}>
<Tab>
### Genehmigungsportal
Jeder Block generiert eine eindeutige Portal-URL (`<blockId.url>`) mit einer visuellen Oberfläche, die alle pausierten Ausgabedaten und Formularfelder für die Fortsetzungseingabe anzeigt. Mobilgerätekompatibel und sicher.
Teilen Sie diese URL in Benachrichtigungen, damit Genehmiger die Anfragen prüfen und beantworten können.
</Tab>
<Tab>
### REST API
Workflows programmatisch fortsetzen:
```bash
POST /api/workflows/{workflowId}/executions/{executionId}/resume/{blockId}
{
"approved": true,
"comments": "Looks good to proceed"
}
```
Erstellen Sie benutzerdefinierte Genehmigungs-UIs oder integrieren Sie bestehende Systeme.
</Tab>
<Tab>
### Webhook
Fügen Sie ein Webhook-Tool im Benachrichtigungsbereich hinzu, um Genehmigungsanfragen an externe Systeme zu senden. Integration mit Ticketing-Systemen wie Jira oder ServiceNow.
</Tab>
</Tabs>
## Häufige Anwendungsfälle
**Inhaltsgenehmigung** - Überprüfung von KI-generierten Inhalten vor der Veröffentlichung
```
Agent → Human in the Loop → API (Publish)
```
**Mehrstufige Genehmigungen** - Verkettung mehrerer Genehmigungsschritte für wichtige Entscheidungen
```
Agent → Human in the Loop (Manager) → Human in the Loop (Director) → Execute
```
**Datenvalidierung** - Überprüfung extrahierter Daten vor der Verarbeitung
```
Agent (Extract) → Human in the Loop (Validate) → Function (Process)
```
**Qualitätskontrolle** - Überprüfung von KI-Ausgaben vor dem Versand an Kunden
```
Agent (Generate) → Human in the Loop (QA) → Gmail (Send)
```
## Block-Ausgaben
**`url`** - Eindeutige URL für das Genehmigungsportal
**`resumeInput.*`** - Alle in der Fortsetzungseingabe definierten Felder werden verfügbar, nachdem der Workflow fortgesetzt wird
Zugriff über `<blockId.resumeInput.fieldName>`.
## Beispiel
**Pausierte Ausgabe:**
```json
{
"title": "<agent1.content.title>",
"body": "<agent1.content.body>",
"qualityScore": "<evaluator1.score>"
}
```
**Fortsetzungseingabe:**
```json
{
"approved": { "type": "boolean" },
"feedback": { "type": "string" }
}
```
**Nachgelagerte Verwendung:**
```javascript
// Condition block
<approval1.resumeInput.approved> === true
```
Das Beispiel unten zeigt ein Genehmigungsportal, wie es von einem Genehmiger gesehen wird, nachdem der Workflow angehalten wurde. Genehmiger können die Daten überprüfen und Eingaben als Teil der Workflow-Wiederaufnahme bereitstellen. Auf das Genehmigungsportal kann direkt über die eindeutige URL, `<blockId.url>`, zugegriffen werden.
<div className="mx-auto w-full overflow-hidden rounded-lg my-6">
<Video src="hitl-resume.mp4" width={700} height={450} />
</div>
## Verwandte Blöcke
- **[Bedingung](/blocks/condition)** - Verzweigung basierend auf Genehmigungsentscheidungen
- **[Variablen](/blocks/variables)** - Speichern von Genehmigungsverlauf und Metadaten
- **[Antwort](/blocks/response)** - Rückgabe von Workflow-Ergebnissen an API-Aufrufer
+143
View File
@@ -0,0 +1,143 @@
---
title: Übersicht
description: Die Bausteine deiner KI-Workflows
---
import { Card, Cards } from 'fumadocs-ui/components/card'
import { Step, Steps } from 'fumadocs-ui/components/steps'
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
import { Video } from '@/components/ui/video'
Blöcke sind die Bausteine, die du miteinander verbindest, um KI-Workflows zu erstellen. Betrachte sie als spezialisierte Module, die jeweils eine bestimmte Aufgabe übernehmen vom Chatten mit KI-Modellen über API-Aufrufe bis hin zur Datenverarbeitung.
<div className="w-full max-w-2xl mx-auto overflow-hidden rounded-lg">
<Video src="connections.mp4" width={700} height={450} />
</div>
## Grundlegende Blocktypen
Sim bietet wesentliche Blocktypen, die die Kernfunktionen von KI-Workflows abdecken:
### Verarbeitungsblöcke
- **[Agent](/blocks/agent)** - Chatte mit KI-Modellen (OpenAI, Anthropic, Google, lokale Modelle)
- **[Function](/blocks/function)** - Führe benutzerdefinierten JavaScript/TypeScript-Code aus
- **[API](/blocks/api)** - Verbinde dich mit externen Diensten über HTTP-Anfragen
### Logikblöcke
- **[Condition](/blocks/condition)** - Verzweige Workflow-Pfade basierend auf booleschen Ausdrücken
- **[Router](/blocks/router)** - Nutze KI, um Anfragen intelligent auf verschiedene Pfade zu leiten
- **[Evaluator](/blocks/evaluator)** - Bewerte und beurteile die Inhaltsqualität mit KI
### Ablaufsteuerungsblöcke
- **[Variablen](/blocks/variables)** - Workflow-bezogene Variablen setzen und verwalten
- **[Warten](/blocks/wait)** - Workflow-Ausführung für eine bestimmte Zeitverzögerung pausieren
- **[Mensch in der Schleife](/blocks/human-in-the-loop)** - Pausieren für menschliche Genehmigung und Feedback vor dem Fortfahren
### Ausgabeblöcke
- **[Antwort](/blocks/response)** - Formatieren und Zurückgeben der endgültigen Ergebnisse aus Ihrem Workflow
## Wie Blöcke funktionieren
Jeder Block hat drei Hauptkomponenten:
**Eingaben**: Daten, die in den Block von anderen Blöcken oder Benutzereingaben kommen
**Konfiguration**: Einstellungen, die steuern, wie der Block sich verhält
**Ausgaben**: Daten, die der Block für andere Blöcke zur Verwendung erzeugt
<Steps>
<Step>
<strong>Eingabe empfangen</strong>: Block erhält Daten von verbundenen Blöcken oder Benutzereingaben
</Step>
<Step>
<strong>Verarbeiten</strong>: Block verarbeitet die Eingabe gemäß seiner Konfiguration
</Step>
<Step>
<strong>Ergebnisse ausgeben</strong>: Block erzeugt Ausgabedaten für die nächsten Blöcke im Workflow
</Step>
</Steps>
## Blöcke verbinden
Sie erstellen Workflows, indem Sie Blöcke miteinander verbinden. Die Ausgabe eines Blocks wird zur Eingabe eines anderen:
- **Ziehen zum Verbinden**: Ziehen Sie von einem Ausgabeport zu einem Eingabeport
- **Mehrfachverbindungen**: Eine Ausgabe kann mit mehreren Eingaben verbunden werden
- **Verzweigende Pfade**: Einige Blöcke können basierend auf Bedingungen zu verschiedenen Pfaden weiterleiten
<div className="w-full max-w-2xl mx-auto overflow-hidden rounded-lg">
<Video src="connections.mp4" width={700} height={450} />
</div>
## Häufige Muster
### Sequentielle Verarbeitung
Verbinden Sie Blöcke in einer Kette, wobei jeder Block die Ausgabe des vorherigen verarbeitet:
```
User Input → Agent → Function → Response
```
### Bedingte Verzweigung
Verwenden Sie Bedingung- oder Router-Blöcke, um verschiedene Pfade zu erstellen:
```
User Input → Router → Agent A (for questions)
→ Agent B (for commands)
```
### Qualitätskontrolle
Verwenden Sie Evaluator-Blöcke, um Ausgaben zu bewerten und zu filtern:
```
Agent → Evaluator → Condition → Response (if good)
→ Agent (retry if bad)
```
## Block-Konfiguration
Jeder Blocktyp hat spezifische Konfigurationsoptionen:
**Alle Blöcke**:
- Eingabe-/Ausgabeverbindungen
- Fehlerbehandlungsverhalten
- Einstellungen für Ausführungs-Timeout
**KI-Blöcke** (Agent, Router, Evaluator):
- Modellauswahl (OpenAI, Anthropic, Google, lokal)
- API-Schlüssel und Authentifizierung
- Temperatur und andere Modellparameter
- Systemaufforderungen und Anweisungen
**Logik-Blöcke** (Bedingung, Funktion):
- Benutzerdefinierte Ausdrücke oder Code
- Variablenreferenzen
- Einstellungen für Ausführungsumgebung
**Integrations-Blöcke** (API, Response):
- Endpunktkonfiguration
- Header und Authentifizierung
- Anfrage-/Antwortformatierung
<Cards>
<Card title="Agent-Block" href="/blocks/agent">
Verbindung zu KI-Modellen herstellen und intelligente Antworten erstellen
</Card>
<Card title="Funktionsblock" href="/blocks/function">
Benutzerdefinierten Code ausführen, um Daten zu verarbeiten und zu transformieren
</Card>
<Card title="API-Block" href="/blocks/api">
Integration mit externen Diensten und APIs
</Card>
<Card title="Bedingungsblock" href="/blocks/condition">
Verzweigende Logik basierend auf Datenbewertung erstellen
</Card>
<Card title="Mensch-in-der-Schleife-Block" href="/blocks/human-in-the-loop">
Pausieren für menschliche Genehmigung und Feedback vor dem Fortfahren
</Card>
<Card title="Variablenblock" href="/blocks/variables">
Workflow-bezogene Variablen setzen und verwalten
</Card>
<Card title="Warteblock" href="/blocks/wait">
Workflow-Ausführung für bestimmte Zeitverzögerungen pausieren
</Card>
</Cards>
+306
View File
@@ -0,0 +1,306 @@
---
title: Loop
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
import { Image } from '@/components/ui/image'
Der Schleifenblock ist ein Container, der Blöcke wiederholt ausführt. Iteriere über Sammlungen, wiederhole Operationen eine festgelegte Anzahl von Malen oder fahre fort, solange eine Bedingung erfüllt ist.
<Callout type="info">
Schleifenblöcke sind Container-Knoten, die andere Blöcke in sich enthalten. Die enthaltenen Blöcke werden mehrfach ausgeführt, basierend auf deiner Konfiguration.
</Callout>
## Konfigurationsoptionen
### Schleifentyp
Wähle zwischen vier Arten von Schleifen:
<Tabs items={['For-Schleife', 'ForEach-Schleife', 'While-Schleife', 'Do-While-Schleife']}>
<Tab>
**For-Schleife (Iterationen)** - Eine numerische Schleife, die eine festgelegte Anzahl von Malen ausgeführt wird:
<div className="flex justify-center">
<Image
src="/static/blocks/loop-1.png"
alt="For-Schleife mit Iterationen"
width={500}
height={400}
className="my-6"
/>
</div>
Verwende diese, wenn du eine Operation eine bestimmte Anzahl von Malen wiederholen musst.
```
Example: Run 5 times
- Iteration 1
- Iteration 2
- Iteration 3
- Iteration 4
- Iteration 5
```
</Tab>
<Tab>
**ForEach-Schleife (Sammlung)** - Eine sammlungsbasierte Schleife, die über jedes Element in einem Array oder Objekt iteriert:
<div className="flex justify-center">
<Image
src="/static/blocks/loop-2.png"
alt="ForEach-Schleife mit Sammlung"
width={500}
height={400}
className="my-6"
/>
</div>
Verwende diese, wenn du eine Sammlung von Elementen verarbeiten musst.
```
Example: Process ["apple", "banana", "orange"]
- Iteration 1: Process "apple"
- Iteration 2: Process "banana"
- Iteration 3: Process "orange"
```
</Tab>
<Tab>
**While-Schleife (Bedingungsbasiert)** - Wird ausgeführt, solange eine Bedingung als wahr ausgewertet wird:
<div className="flex justify-center">
<Image
src="/static/blocks/loop-3.png"
alt="While-Schleife mit Bedingung"
width={500}
height={400}
className="my-6"
/>
</div>
Verwende diese, wenn du eine Schleife benötigst, die läuft, bis eine bestimmte Bedingung erfüllt ist. Die Bedingung wird **vor** jeder Iteration überprüft.
```
Example: While {"<variable.i>"} < 10
- Check condition → Execute if true
- Inside loop: Increment {"<variable.i>"}
- Inside loop: Variables assigns i = {"<variable.i>"} + 1
- Check condition → Execute if true
- Check condition → Exit if false
```
</Tab>
<Tab>
**Do-While-Schleife (Bedingungsbasiert)** - Wird mindestens einmal ausgeführt und dann fortgesetzt, solange eine Bedingung wahr ist:
<div className="flex justify-center">
<Image
src="/static/blocks/loop-4.png"
alt="Do-While-Schleife mit Bedingung"
width={500}
height={400}
className="my-6"
/>
</div>
Verwende diese, wenn du eine Operation mindestens einmal ausführen musst und dann die Schleife fortsetzen willst, bis eine Bedingung erfüllt ist. Die Bedingung wird **nach** jeder Iteration überprüft.
```
Example: Do-while {"<variable.i>"} < 10
- Execute blocks
- Inside loop: Increment {"<variable.i>"}
- Inside loop: Variables assigns i = {"<variable.i>"} + 1
- Check condition → Continue if true
- Check condition → Exit if false
```
</Tab>
</Tabs>
## Wie man Schleifen verwendet
### Eine Schleife erstellen
1. Ziehe einen Schleifenblock aus der Werkzeugleiste auf deine Leinwand
2. Konfiguriere den Schleifentyp und die Parameter
3. Ziehe andere Blöcke in den Schleifencontainer
4. Verbinde die Blöcke nach Bedarf
### Auf Ergebnisse zugreifen
Nach Abschluss einer Schleife kannst du auf aggregierte Ergebnisse zugreifen:
- **loop.results**: Array mit Ergebnissen aller Schleifendurchläufe
## Beispielanwendungsfälle
**Verarbeitung von API-Ergebnissen** - ForEach-Schleife verarbeitet Kundendatensätze aus einer API
```javascript
// Beispiel: ForEach-Schleife für API-Ergebnisse
const customers = await api.getCustomers();
loop.forEach(customers, (customer) => {
// Verarbeite jeden Kunden
if (customer.status === 'active') {
sendEmail(customer.email, 'Sonderangebot');
}
});
```
**Iterative Inhaltsgenerierung** - For-Schleife generiert mehrere Inhaltsvariationen
```javascript
// Beispiel: For-Schleife für Inhaltsgenerierung
const variations = [];
loop.for(5, (i) => {
// Generiere 5 verschiedene Variationen
const content = ai.generateContent({
prompt: `Variation ${i+1} für Produktbeschreibung`,
temperature: 0.7 + (i * 0.1)
});
variations.push(content);
});
```
**Zähler mit While-Schleife** - While-Schleife verarbeitet Elemente mit Zähler
```javascript
// Beispiel: While-Schleife mit Zähler
let counter = 0;
let processedItems = 0;
loop.while(() => counter < items.length, () => {
if (items[counter].isValid) {
processItem(items[counter]);
processedItems++;
}
counter++;
});
console.log(`${processedItems} gültige Elemente verarbeitet`);
```
## Erweiterte Funktionen
### Einschränkungen
<Callout type="info">
Container-Blöcke (Schleifen und Parallele) unterstützen Verschachtelung. Du kannst Schleifen in Schleifen, Parallele in Schleifen und jede Kombination von Container-Blöcken platzieren, um komplexe mehrdimensionale Workflows zu erstellen.
</Callout>
<Callout type="info">
Schleifen werden sequentiell ausgeführt, nicht parallel. Wenn du eine gleichzeitige Ausführung benötigst, verwende stattdessen den Parallel-Block.
</Callout>
## Eingaben und Ausgaben
<Tabs items={['Configuration', 'Variables', 'Results']}>
<Tab>
<ul className="list-disc space-y-2 pl-6">
<li>
<strong>Schleifentyp</strong>: Wähle zwischen 'for', 'forEach', 'while' oder 'doWhile'
</li>
<li>
<strong>Iterationen</strong>: Anzahl der Ausführungen (für for-Schleifen)
</li>
<li>
<strong>Sammlung</strong>: Array oder Objekt zum Durchlaufen (für forEach-Schleifen)
</li>
<li>
<strong>Bedingung</strong>: Boolescher Ausdruck zur Auswertung (für while/do-while-Schleifen)
</li>
</ul>
</Tab>
<Tab>
<ul className="list-disc space-y-2 pl-6">
<li>
<strong>loop.currentItem</strong>: Aktuell verarbeitetes Element
</li>
<li>
<strong>loop.index</strong>: Aktuelle Iterationsnummer (0-basiert)
</li>
<li>
<strong>loop.items</strong>: Vollständige Sammlung (für forEach-Schleifen)
</li>
</ul>
</Tab>
<Tab>
<ul className="list-disc space-y-2 pl-6">
<li>
<strong>loop.results</strong>: Array aller Iterationsergebnisse
</li>
<li>
<strong>Struktur</strong>: Ergebnisse behalten die Iterationsreihenfolge bei
</li>
<li>
<strong>Zugriff</strong>: Verfügbar in Blöcken nach der Schleife
</li>
</ul>
</Tab>
</Tabs>
## Bewährte Praktiken
- **Setzen Sie vernünftige Grenzen**: Halten Sie die Anzahl der Iterationen in einem vernünftigen Rahmen, um lange Ausführungszeiten zu vermeiden
- **Verwenden Sie ForEach für Sammlungen**: Verwenden Sie beim Verarbeiten von Arrays oder Objekten ForEach anstelle von For-Schleifen
- **Behandeln Sie Fehler elegant**: Erwägen Sie, Fehlerbehandlung innerhalb von Schleifen hinzuzufügen, um robuste Workflows zu gewährleisten
## Eingaben und Ausgaben
<Tabs items={['Configuration', 'Variables', 'Results']}>
<Tab>
<ul className="list-disc space-y-2 pl-6">
<li>
<strong>Schleifentyp</strong>: Wählen Sie zwischen 'for', 'forEach', 'while' oder 'doWhile'
</li>
<li>
<strong>Iterationen</strong>: Anzahl der Ausführungen (für for-Schleifen)
</li>
<li>
<strong>Sammlung</strong>: Array oder Objekt zum Durchlaufen (für forEach-Schleifen)
</li>
<li>
<strong>Bedingung</strong>: Boolescher Ausdruck zur Auswertung (für while/do-while-Schleifen)
</li>
</ul>
</Tab>
<Tab>
Verfügbar **innerhalb** der Schleife:
<ul className="list-disc space-y-2 pl-6">
<li>
<strong>{"<loop.index>"}</strong>: Aktuelle Iterationsnummer (0-basiert)
</li>
<li>
<strong>{"<loop.currentItem>"}</strong>: Aktuell verarbeitetes Element (nur forEach)
</li>
<li>
<strong>{"<loop.items>"}</strong>: Vollständige Sammlung (nur forEach)
</li>
</ul>
</Tab>
<Tab>
<ul className="list-disc space-y-2 pl-6">
<li>
<strong>{"<blockname.results>"}</strong>: Array aller Iterationsergebnisse (Zugriff über Blocknamen)
</li>
<li>
<strong>Struktur</strong>: Ergebnisse behalten die Iterationsreihenfolge bei
</li>
<li>
<strong>Zugriff</strong>: Verfügbar in Blöcken nach Abschluss der Schleife
</li>
</ul>
</Tab>
</Tabs>
## Best Practices
- **Setzen Sie vernünftige Grenzen**: Halten Sie die Iterationsanzahl angemessen, um lange Ausführungszeiten zu vermeiden
- **Verwenden Sie ForEach für Sammlungen**: Verwenden Sie beim Verarbeiten von Arrays oder Objekten ForEach anstelle von For-Schleifen
- **Behandeln Sie Fehler elegant**: Erwägen Sie, Fehlerbehandlung innerhalb von Schleifen hinzuzufügen, um robuste Workflows zu gewährleisten
@@ -0,0 +1,261 @@
---
title: Parallel
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
import { Image } from '@/components/ui/image'
Der Parallel-Block ist ein Container, der mehrere Instanzen gleichzeitig ausführt, um Workflows schneller zu verarbeiten. Verarbeiten Sie Elemente simultan statt sequentiell.
<Callout type="info">
Parallel-Blöcke sind Container-Knoten, die ihre Inhalte mehrfach gleichzeitig ausführen, im Gegensatz zu Schleifen, die sequentiell ausgeführt werden.
</Callout>
## Konfigurationsoptionen
### Parallel-Typ
Wählen Sie zwischen zwei Arten der parallelen Ausführung:
<Tabs items={['Count-based', 'Collection-based']}>
<Tab>
**Anzahlbasierte Parallelisierung** - Führen Sie eine feste Anzahl paralleler Instanzen aus:
<div className="flex justify-center">
<Image
src="/static/blocks/parallel-1.png"
alt="Anzahlbasierte parallele Ausführung"
width={500}
height={400}
className="my-6"
/>
</div>
Verwenden Sie diese Option, wenn Sie dieselbe Operation mehrmals gleichzeitig ausführen müssen.
```javascript
// Beispiel: 3 parallele Instanzen ausführen
const results = await blocks.parallel({
type: 'count',
count: 3,
async process(index) {
// Jede Instanz erhält einen eindeutigen Index (0, 1, 2)
return `Ergebnis von Instanz ${index}`;
}
});
```
</Tab>
<Tab>
**Sammlungsbasierte Parallelisierung** - Verteilen Sie eine Sammlung auf parallele Instanzen:
<div className="flex justify-center">
<Image
src="/static/blocks/parallel-2.png"
alt="Sammlungsbasierte parallele Ausführung"
width={500}
height={400}
className="my-6"
/>
</div>
Jede Instanz verarbeitet gleichzeitig ein Element aus der Sammlung.
```javascript
// Beispiel: Eine Liste von URLs parallel verarbeiten
const urls = [
'https://example.com/api/1',
'https://example.com/api/2',
'https://example.com/api/3'
];
const results = await blocks.parallel({
type: 'collection',
items: urls,
async process(url, index) {
// Jede Instanz verarbeitet eine URL
const response = await fetch(url);
return response.json();
}
});
```
</Tab>
</Tabs>
## Verwendung von Parallel-Blöcken
### Erstellen eines Parallel-Blocks
1. Ziehen Sie einen Parallel-Block aus der Symbolleiste auf Ihre Leinwand
2. Konfigurieren Sie den Parallel-Typ und die Parameter
3. Ziehen Sie einen einzelnen Block in den Parallel-Container
4. Verbinden Sie den Block nach Bedarf
### Zugriff auf Ergebnisse
Nach Abschluss eines Parallel-Blocks können Sie auf aggregierte Ergebnisse zugreifen:
- **`results`**: Array von Ergebnissen aus allen parallelen Instanzen
## Beispielanwendungsfälle
**Batch-API-Verarbeitung** - Verarbeiten Sie mehrere API-Aufrufe gleichzeitig
<div className="mb-4 rounded-md border p-4">
<h4 className="font-medium">Szenario: Mehrere API-Endpunkte abfragen</h4>
<ol className="list-decimal pl-5 text-sm">
<li>Sammlungsbasierte Parallelisierung über eine Liste von API-Endpunkten</li>
<li>Jede Instanz führt einen API-Aufruf durch und verarbeitet die Antwort</li>
<li>Ergebnisse werden in einem Array gesammelt und können weiterverarbeitet werden</li>
</ol>
</div>
**Multi-Modell-KI-Verarbeitung** - Erhalten Sie Antworten von mehreren KI-Modellen gleichzeitig
<div className="mb-4 rounded-md border p-4">
<h4 className="font-medium">Szenario: Antworten von mehreren KI-Modellen erhalten</h4>
<ol className="list-decimal pl-5 text-sm">
<li>Sammlungsbasierte Parallelverarbeitung über eine Liste von Modell-IDs (z.B. ["gpt-4o", "claude-3.7-sonnet", "gemini-2.5-pro"])</li>
<li>Innerhalb des parallelen Blocks: Das Modell des Agenten wird auf das aktuelle Element aus der Sammlung gesetzt</li>
<li>Nach dem parallelen Block: Vergleichen und Auswählen der besten Antwort</li>
</ol>
</div>
## Erweiterte Funktionen
### Ergebnisaggregation
Ergebnisse aus allen parallelen Instanzen werden automatisch gesammelt:
### Anwendungsbeispiele
### Instanzisolierung
Jede parallele Instanz läuft unabhängig:
- Separate Variablenbereiche
- Kein gemeinsamer Zustand zwischen Instanzen
- Fehler in einer Instanz beeinflussen andere nicht
### Einschränkungen
<Callout type="info">
Container-Blöcke (Schleifen und Parallele) unterstützen Verschachtelung. Sie können Parallele in Parallele, Schleifen in Parallele und jede Kombination von Container-Blöcken platzieren, um komplexe mehrdimensionale Workflows zu erstellen.
</Callout>
<Callout type="info">
Während die parallele Ausführung schneller ist, beachten Sie bitte:
- API-Ratenbegrenzungen bei gleichzeitigen Anfragen
- Speicherverbrauch bei großen Datensätzen
- Maximum von 20 gleichzeitigen Instanzen, um Ressourcenerschöpfung zu vermeiden
</Callout>
## Parallel vs. Schleife
Wann Sie welche Methode verwenden sollten:
| Funktion | Parallel | Schleife |
|---------|----------|------|
| Ausführung | Gleichzeitig | Sequentiell |
| Geschwindigkeit | Schneller für unabhängige Operationen | Langsamer, aber geordnet |
| Reihenfolge | Keine garantierte Reihenfolge | Behält Reihenfolge bei |
| Anwendungsfall | Unabhängige Operationen | Abhängige Operationen |
| Ressourcennutzung | Höher | Niedriger |
## Eingaben und Ausgaben
<Tabs items={['Configuration', 'Variables', 'Results']}>
<Tab>
<ul className="list-disc space-y-2 pl-6">
<li>
<strong>Parallel-Typ</strong>: Wählen Sie zwischen 'count' oder 'collection'
</li>
<li>
<strong>Anzahl</strong>: Anzahl der auszuführenden Instanzen (anzahlbasiert)
</li>
<li>
<strong>Sammlung</strong>: Array oder Objekt zur Verteilung (sammlungsbasiert)
</li>
</ul>
</Tab>
<Tab>
<ul className="list-disc space-y-2 pl-6">
<li>
<strong>parallel.currentItem</strong>: Element für diese Instanz
</li>
<li>
<strong>parallel.index</strong>: Instanznummer (0-basiert)
</li>
<li>
<strong>parallel.items</strong>: Vollständige Sammlung (sammlungsbasiert)
</li>
</ul>
</Tab>
<Tab>
<ul className="list-disc space-y-2 pl-6">
<li>
<strong>parallel.results</strong>: Array aller Instanzergebnisse
</li>
<li>
<strong>Zugriff</strong>: Verfügbar in Blöcken nach der Parallelausführung
</li>
</ul>
</Tab>
</Tabs>
## Best Practices
- **Nur unabhängige Operationen**: Stellen Sie sicher, dass Operationen nicht voneinander abhängen
- **Ratenbegrenzungen berücksichtigen**: Fügen Sie Verzögerungen oder Drosselungen für API-intensive Workflows hinzu
- **Fehlerbehandlung**: Jede Instanz sollte ihre eigenen Fehler angemessen behandeln
## Eingaben und Ausgaben
<Tabs items={['Konfiguration', 'Variablen', 'Ergebnisse']}>
<Tab>
<ul className="list-disc space-y-2 pl-6">
<li>
<strong>Parallel-Typ</strong>: Wählen Sie zwischen „count" oder „collection"
</li>
<li>
<strong>Anzahl</strong>: Anzahl der auszuführenden Instanzen (anzahlbasiert)
</li>
<li>
<strong>Collection</strong>: Array oder Objekt zur Verteilung (sammlungsbasiert)
</li>
</ul>
</Tab>
<Tab>
Verfügbar **innerhalb** der Parallelverarbeitung:
<ul className="list-disc space-y-2 pl-6">
<li>
<strong>{"<parallel.index>"}</strong>: Instanznummer (0-basiert)
</li>
<li>
<strong>{"<parallel.currentItem>"}</strong>: Element für diese Instanz (nur sammlungsbasiert)
</li>
<li>
<strong>{"<parallel.items>"}</strong>: Vollständige Sammlung (nur sammlungsbasiert)
</li>
</ul>
</Tab>
<Tab>
<ul className="list-disc space-y-2 pl-6">
<li>
<strong>{"<blockname.results>"}</strong>: Array aller Instanzergebnisse (Zugriff über Blockname)
</li>
<li>
<strong>Zugriff</strong>: Verfügbar in Blöcken nach Abschluss der Parallelverarbeitung
</li>
</ul>
</Tab>
</Tabs>
## Best Practices
- **Nur unabhängige Operationen**: Stellen Sie sicher, dass Operationen nicht voneinander abhängen
- **Rate Limits beachten**: Fügen Sie Verzögerungen oder Drosselung für API-intensive Workflows hinzu
- **Fehlerbehandlung**: Jede Instanz sollte ihre eigenen Fehler ordnungsgemäß behandeln
@@ -0,0 +1,120 @@
---
title: Antwort
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
import { Image } from '@/components/ui/image'
Der Response-Block formatiert und sendet strukturierte HTTP-Antworten zurück an API-Aufrufer. Verwenden Sie ihn, um Workflow-Ergebnisse mit korrekten Statuscodes und Headern zurückzugeben.
<div className="flex justify-center">
<Image
src="/static/blocks/response.png"
alt="Konfiguration des Antwort-Blocks"
width={500}
height={400}
className="my-6"
/>
</div>
<Callout type="info">
Response-Blöcke sind terminale Blöcke - sie beenden die Workflow-Ausführung und können nicht mit anderen Blöcken verbunden werden.
</Callout>
## Konfigurationsoptionen
### Antwortdaten
Die Antwortdaten sind der Hauptinhalt, der an den API-Aufrufer zurückgesendet wird. Diese sollten als JSON formatiert sein und können Folgendes enthalten:
- Statische Werte
- Dynamische Verweise auf Workflow-Variablen mit der `<variable.name>` Syntax
- Verschachtelte Objekte und Arrays
- Jede gültige JSON-Struktur
### Statuscode
Legen Sie den HTTP-Statuscode für die Antwort fest (standardmäßig 200):
**Erfolg (2xx):**
- **200**: OK - Standard-Erfolgsantwort
- **201**: Erstellt - Ressource erfolgreich erstellt
- **204**: Kein Inhalt - Erfolg ohne Antworttext
**Client-Fehler (4xx):**
- **400**: Ungültige Anfrage - Ungültige Anfrageparameter
- **401**: Nicht autorisiert - Authentifizierung erforderlich
- **404**: Nicht gefunden - Ressource existiert nicht
- **422**: Nicht verarbeitbare Entität - Validierungsfehler
**Server-Fehler (5xx):**
- **500**: Interner Serverfehler - Serverseitiger Fehler
- **502**: Bad Gateway - Fehler eines externen Dienstes
- **503**: Dienst nicht verfügbar - Dienst vorübergehend nicht erreichbar
### Antwort-Header
Konfigurieren Sie zusätzliche HTTP-Header, die in die Antwort aufgenommen werden sollen.
Header werden als Schlüssel-Wert-Paare konfiguriert:
| Schlüssel | Wert |
|-----|-------|
| Content-Type | application/json |
| Cache-Control | no-cache |
| X-API-Version | 1.0 |
## Beispielanwendungsfälle
**API-Endpunkt-Antwort** - Strukturierte Daten von einer Such-API zurückgeben
```
Agent (Search) → Function (Format & Paginate) → Response (200, JSON)
```
**Webhook-Bestätigung** - Bestätigung des Webhook-Empfangs und der Verarbeitung
```
Webhook Trigger → Function (Process) → Response (200, Confirmation)
```
**Fehlerantwort-Behandlung** - Angemessene Fehlerantworten zurückgeben
```
Condition (Error Detected) → Router → Response (400/500, Error Details)
```
## Ausgaben
Antwortblöcke sind endgültig - sie beenden die Workflow-Ausführung und senden die HTTP-Antwort an den API-Aufrufer. Es stehen keine Ausgaben für nachgelagerte Blöcke zur Verfügung.
## Variablenreferenzen
Verwenden Sie die `<variable.name>` Syntax, um Workflow-Variablen dynamisch in Ihre Antwort einzufügen:
```json
{
"user": {
"id": "<variable.userId>",
"name": "<variable.userName>",
"email": "<variable.userEmail>"
},
"query": "<variable.searchQuery>",
"results": "<variable.searchResults>",
"totalFound": "<variable.resultCount>",
"processingTime": "<variable.executionTime>ms"
}
```
<Callout type="warning">
Variablennamen sind Groß- und Kleinschreibung sensitiv und müssen exakt mit den in Ihrem Workflow verfügbaren Variablen übereinstimmen.
</Callout>
## Best Practices
- **Verwenden Sie aussagekräftige Statuscodes**: Wählen Sie passende HTTP-Statuscodes, die das Ergebnis des Workflows genau widerspiegeln
- **Strukturieren Sie Ihre Antworten einheitlich**: Behalten Sie eine konsistente JSON-Struktur über alle Ihre API-Endpunkte bei, um eine bessere Entwicklererfahrung zu gewährleisten
- **Fügen Sie relevante Metadaten hinzu**: Fügen Sie Zeitstempel und Versionsinformationen hinzu, um bei der Fehlerbehebung und Überwachung zu helfen
- **Behandeln Sie Fehler elegant**: Verwenden Sie bedingte Logik in Ihrem Workflow, um angemessene Fehlerantworten mit aussagekräftigen Meldungen zu setzen
- **Validieren Sie Variablenreferenzen**: Stellen Sie sicher, dass alle referenzierten Variablen existieren und die erwarteten Datentypen enthalten, bevor der Antwortblock ausgeführt wird
+117
View File
@@ -0,0 +1,117 @@
---
title: Router
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
import { Image } from '@/components/ui/image'
Der Router-Block verwendet KI, um Workflows basierend auf Inhaltsanalysen intelligent zu leiten. Im Gegensatz zu Bedingungsblöcken, die einfache Regeln verwenden, verstehen Router Kontext und Absicht.
<div className="flex justify-center">
<Image
src="/static/blocks/router.png"
alt="Router-Block mit mehreren Pfaden"
width={500}
height={400}
className="my-6"
/>
</div>
## Router vs. Bedingung
**Verwende Router, wenn:**
- KI-gestützte Inhaltsanalyse benötigt wird
- Mit unstrukturierten oder variierenden Inhalten gearbeitet wird
- Absichtsbasierte Weiterleitung erforderlich ist (z.B. "Support-Tickets an Abteilungen weiterleiten")
**Verwende Bedingung, wenn:**
- Einfache regelbasierte Entscheidungen ausreichen
- Mit strukturierten Daten oder numerischen Vergleichen gearbeitet wird
- Schnelle, deterministische Weiterleitung benötigt wird
## Konfigurationsoptionen
### Inhalt/Prompt
Der Inhalt oder Prompt, den der Router analysieren wird, um Weiterleitungsentscheidungen zu treffen. Dies kann sein:
- Eine direkte Benutzeranfrage oder -eingabe
- Ausgabe eines vorherigen Blocks
- Eine systemgenerierte Nachricht
### Zielblöcke
Die möglichen Zielblöcke, aus denen der Router auswählen kann. Der Router erkennt automatisch verbundene Blöcke, aber du kannst auch:
- Die Beschreibungen von Zielblöcken anpassen, um die Weiterleitungsgenauigkeit zu verbessern
- Weiterleitungskriterien für jeden Zielblock festlegen
- Bestimmte Blöcke von der Berücksichtigung als Weiterleitungsziele ausschließen
### Modellauswahl
Wähle ein KI-Modell für die Weiterleitungsentscheidung:
- **OpenAI**: GPT-4o, o1, o3, o4-mini, gpt-4.1
- **Anthropic**: Claude 3.7 Sonnet
- **Google**: Gemini 2.5 Pro, Gemini 2.0 Flash
- **Andere Anbieter**: Groq, Cerebras, xAI, DeepSeek
- **Lokale Modelle**: Ollama oder VLLM-kompatible Modelle
Verwende Modelle mit starken Argumentationsfähigkeiten wie GPT-4o oder Claude 3.7 Sonnet für beste Ergebnisse.
### API-Schlüssel
Ihr API-Schlüssel für den ausgewählten LLM-Anbieter. Dieser wird sicher gespeichert und für die Authentifizierung verwendet.
## Ausgaben
- **`<router.prompt>`**: Zusammenfassung des Routing-Prompts
- **`<router.selected_path>`**: Ausgewählter Zielblock
- **`<router.tokens>`**: Token-Nutzungsstatistiken
- **`<router.cost>`**: Geschätzte Routing-Kosten
- **`<router.model>`**: Für die Entscheidungsfindung verwendetes Modell
## Beispielanwendungsfälle
**Kundensupport-Triage** - Tickets an spezialisierte Abteilungen weiterleiten
```
Input (Ticket) → Router → Agent (Engineering) or Agent (Finance)
```
**Inhaltsklassifizierung** - Nutzergenerierte Inhalte klassifizieren und weiterleiten
```
Input (Feedback) → Router → Workflow (Product) or Workflow (Technical)
```
**Lead-Qualifizierung** - Leads basierend auf Qualifizierungskriterien weiterleiten
```
Input (Lead) → Router → Agent (Enterprise Sales) or Workflow (Self-serve)
```
## Best Practices
- **Klare Zielbeschreibungen bereitstellen**: Helfen Sie dem Router zu verstehen, wann jedes Ziel ausgewählt werden soll, mit spezifischen, detaillierten Beschreibungen
- **Spezifische Routing-Kriterien verwenden**: Definieren Sie klare Bedingungen und Beispiele für jeden Pfad, um die Genauigkeit zu verbessern
- **Fallback-Pfade implementieren**: Verbinden Sie ein Standardziel für Fälle, in denen kein spezifischer Pfad geeignet ist
- **Mit verschiedenen Eingaben testen**: Stellen Sie sicher, dass der Router verschiedene Eingabetypen, Grenzfälle und unerwartete Inhalte verarbeiten kann
- **Routing-Leistung überwachen**: Überprüfen Sie Routing-Entscheidungen regelmäßig und verfeinern Sie Kriterien basierend auf tatsächlichen Nutzungsmustern
- **Geeignete Modelle auswählen**: Verwenden Sie Modelle mit starken Argumentationsfähigkeiten für komplexe Routing-Entscheidungen
Wenn der Router keine geeignete Route für den gegebenen Kontext ermitteln kann, leitet er stattdessen zum **Fehlerpfad** weiter, anstatt willkürlich eine Route auszuwählen. Dies geschieht, wenn:
- Der Kontext keiner der definierten Routenbeschreibungen eindeutig entspricht
- Die KI feststellt, dass keine der verfügbaren Routen geeignet ist
## Best Practices
- **Klare Routenbeschreibungen verfassen**: Jede Routenbeschreibung sollte klar erklären, wann diese Route ausgewählt werden sollte. Seien Sie spezifisch bezüglich der Kriterien.
- **Routen gegenseitig ausschließend gestalten**: Stellen Sie nach Möglichkeit sicher, dass sich Routenbeschreibungen nicht überschneiden, um mehrdeutige Routing-Entscheidungen zu vermeiden.
- **Einen Fehlerpfad verbinden**: Behandeln Sie Fälle, in denen keine Route passt, indem Sie einen Fehlerbehandler für ein elegantes Fallback-Verhalten verbinden.
- **Aussagekräftige Routentitel verwenden**: Routentitel erscheinen im Workflow-Canvas, machen Sie sie daher für bessere Lesbarkeit aussagekräftig.
- **Mit verschiedenen Eingaben testen**: Stellen Sie sicher, dass der Router verschiedene Eingabetypen, Grenzfälle und unerwartete Inhalte verarbeitet.
- **Routing-Performance überwachen**: Überprüfen Sie Routing-Entscheidungen regelmäßig und verfeinern Sie Routenbeschreibungen basierend auf tatsächlichen Nutzungsmustern.
- **Geeignete Modelle wählen**: Verwenden Sie Modelle mit starken Reasoning-Fähigkeiten für komplexe Routing-Entscheidungen.
@@ -0,0 +1,86 @@
---
title: Variablen
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Step, Steps } from 'fumadocs-ui/components/steps'
import { Image } from '@/components/ui/image'
Der Variablen-Block aktualisiert Workflow-Variablen während der Ausführung. Variablen müssen zuerst im Variablen-Bereich deines Workflows initialisiert werden, dann kannst du diesen Block verwenden, um ihre Werte während der Ausführung deines Workflows zu aktualisieren.
<div className="flex justify-center">
<Image
src="/static/blocks/variables.png"
alt="Variablen-Block"
width={500}
height={400}
className="my-6"
/>
</div>
<Callout>
Greife überall in deinem Workflow auf Variablen zu, indem du die `<variable.variableName>` Syntax verwendest.
</Callout>
## Wie man Variablen verwendet
### 1. Initialisierung in Workflow-Variablen
Erstellen Sie zunächst Ihre Variablen im Variablenbereich des Workflows (zugänglich über die Workflow-Einstellungen):
```
customerEmail = ""
retryCount = 0
currentStatus = "pending"
```
### 2. Aktualisierung mit dem Variablen-Block
Verwenden Sie den Variablen-Block, um diese Werte während der Ausführung zu aktualisieren:
```
customerEmail = <api.email>
retryCount = <variable.retryCount> + 1
currentStatus = "processing"
```
### 3. Überall zugreifen
Referenzieren Sie Variablen in jedem Block:
```
Agent prompt: "Send email to <variable.customerEmail>"
Condition: <variable.retryCount> < 5
API body: {"status": "<variable.currentStatus>"}
```
## Beispielanwendungsfälle
**Schleifenzähler und Status** - Fortschritt durch Iterationen verfolgen
```
Loop → Agent (Process) → Variables (itemsProcessed + 1) → Variables (Store lastResult)
```
**Wiederholungslogik** - API-Wiederholungsversuche verfolgen
```
API (Try) → Variables (retryCount + 1) → Condition (retryCount < 3)
```
**Dynamische Konfiguration** - Benutzerkontext für Workflow speichern
```
API (Fetch Profile) → Variables (userId, userTier) → Agent (Personalize)
```
## Ausgaben
- **`<variables.assignments>`**: JSON-Objekt mit allen Variablenzuweisungen aus diesem Block
## Bewährte Praktiken
- **In Workflow-Einstellungen initialisieren**: Erstellen Sie Variablen immer im Variablenbereich des Workflows, bevor Sie sie verwenden
- **Dynamisch aktualisieren**: Verwenden Sie Variablen-Blöcke, um Werte basierend auf Block-Ausgaben oder Berechnungen zu aktualisieren
- **In Schleifen verwenden**: Perfekt für die Verfolgung des Status über Iterationen hinweg
- **Beschreibend benennen**: Verwenden Sie klare Namen wie `currentIndex`, `totalProcessed` oder `lastError`
+67
View File
@@ -0,0 +1,67 @@
---
title: Warten
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Step, Steps } from 'fumadocs-ui/components/steps'
import { Image } from '@/components/ui/image'
Der Warten-Block pausiert deinen Workflow für eine bestimmte Zeit, bevor er mit dem nächsten Block fortfährt. Verwende ihn, um Verzögerungen zwischen Aktionen einzufügen, API-Ratenbegrenzungen einzuhalten oder Operationen zeitlich zu verteilen.
<div className="flex justify-center">
<Image
src="/static/blocks/wait.png"
alt="Warte-Block"
width={500}
height={400}
className="my-6"
/>
</div>
## Konfiguration
### Wartezeit
Geben Sie die Dauer für die Ausführungspause ein:
- **Eingabe**: Positive Zahl
- **Maximum**: 600 Sekunden (10 Minuten) oder 10 Minuten
### Einheit
Wählen Sie die Zeiteinheit:
- **Sekunden**: Für kurze, präzise Verzögerungen
- **Minuten**: Für längere Pausen
<Callout type="info">
Warteblöcke können durch Stoppen des Workflows abgebrochen werden. Die maximale Wartezeit beträgt 10 Minuten.
</Callout>
## Ausgaben
- **`<wait.waitDuration>`**: Die Wartezeit in Millisekunden
- **`<wait.status>`**: Status der Wartezeit ('waiting', 'completed' oder 'cancelled')
## Beispielanwendungsfälle
**API-Ratenbegrenzung** - Bleiben Sie zwischen Anfragen innerhalb der API-Ratenlimits
```
API (Request 1) → Wait (2s) → API (Request 2)
```
**Zeitgesteuerte Benachrichtigungen** - Senden Sie Folgenachrichten nach einer Verzögerung
```
Function (Send Email) → Wait (5min) → Function (Follow-up)
```
**Verarbeitungsverzögerungen** - Warten Sie, bis das externe System die Verarbeitung abgeschlossen hat
```
API (Trigger Job) → Wait (30s) → API (Check Status)
```
## Bewährte Praktiken
- **Halten Sie Wartezeiten angemessen**: Verwenden Sie Wait für Verzögerungen bis zu 10 Minuten. Für längere Verzögerungen sollten Sie geplante Workflows in Betracht ziehen
- **Überwachen Sie die Ausführungszeit**: Denken Sie daran, dass Wartezeiten die Gesamtdauer des Workflows verlängern
@@ -0,0 +1,89 @@
---
title: Webhook
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Image } from '@/components/ui/image'
Der Webhook-Block sendet HTTP-POST-Anfragen an externe Webhook-Endpunkte mit automatischen Webhook-Headern und optionaler HMAC-Signierung.
<div className="flex justify-center">
<Image
src="/static/blocks/webhook.png"
alt="Webhook-Block"
width={500}
height={400}
className="my-6"
/>
</div>
## Konfiguration
### Webhook-URL
Der Ziel-Endpunkt für Ihre Webhook-Anfrage. Unterstützt sowohl statische URLs als auch dynamische Werte aus anderen Blöcken.
### Payload
JSON-Daten, die im Anfrage-Body gesendet werden. Verwenden Sie den KI-Zauberstab, um Payloads zu generieren oder auf Workflow-Variablen zu verweisen:
```json
{
"event": "workflow.completed",
"data": {
"result": "<agent.content>",
"timestamp": "<function.result>"
}
}
```
### Signierungsgeheimnis
Optionales Geheimnis für die HMAC-SHA256-Payload-Signierung. Wenn angegeben, wird ein `X-Webhook-Signature`Header hinzugefügt:
```
X-Webhook-Signature: t=1704067200000,v1=5d41402abc4b2a76b9719d911017c592...
```
Um Signaturen zu verifizieren, berechnen Sie `HMAC-SHA256(secret, "${timestamp}.${body}")` und vergleichen Sie mit dem `v1`Wert.
### Zusätzliche Header
Benutzerdefinierte Schlüssel-Wert-Header, die in die Anfrage aufgenommen werden. Diese überschreiben alle automatischen Header mit demselben Namen.
## Automatische Header
Jede Anfrage enthält automatisch diese Header:
| Header | Beschreibung |
|--------|-------------|
| `Content-Type` | `application/json` |
| `X-Webhook-Timestamp` | Unix-Zeitstempel in Millisekunden |
| `X-Delivery-ID` | Eindeutige UUID für diese Zustellung |
| `Idempotency-Key` | Identisch mit `X-Delivery-ID` zur Deduplizierung |
## Ausgaben
| Ausgabe | Typ | Beschreibung |
|--------|------|-------------|
| `data` | json | Antwort-Body vom Endpunkt |
| `status` | number | HTTP-Statuscode |
| `headers` | object | Antwort-Header |
## Beispiel-Anwendungsfälle
**Externe Dienste benachrichtigen** - Workflow-Ergebnisse an Slack, Discord oder benutzerdefinierte Endpunkte senden
```
Agent → Function (format) → Webhook (notify)
```
**Externe Workflows auslösen** - Prozesse in anderen Systemen starten, wenn Bedingungen erfüllt sind
```
Condition (check) → Webhook (trigger) → Response
```
<Callout>
Der Webhook-Block verwendet immer POST. Für andere HTTP-Methoden oder mehr Kontrolle verwenden Sie den [API-Block](/blocks/api).
</Callout>
@@ -0,0 +1,63 @@
---
title: Workflow-Block
description: Führe einen anderen Workflow innerhalb des aktuellen Ablaufs aus
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Image } from '@/components/ui/image'
## Was er macht
<div className='flex justify-center my-6'>
<Image
src='/static/blocks/workflow.png'
alt='Workflow-Block-Konfiguration'
width={500}
height={400}
className='rounded-xl border border-border shadow-sm'
/>
</div>
Füge einen Workflow-Block hinzu, wenn du einen untergeordneten Workflow als Teil eines größeren Ablaufs aufrufen möchtest. Der Block führt die neueste bereitgestellte Version dieses Workflows aus, wartet auf dessen Abschluss und setzt dann mit dem übergeordneten Workflow fort.
## Konfiguration
1. **Wähle einen Workflow** aus dem Dropdown-Menü (Selbstreferenzen sind blockiert, um Schleifen zu verhindern).
2. **Eingaben zuordnen**: Wenn der untergeordnete Workflow einen Eingabeformular-Trigger hat, siehst du jedes Feld und kannst übergeordnete Variablen verbinden. Die zugeordneten Werte sind das, was der untergeordnete Workflow erhält.
<div className='flex justify-center my-6'>
<Image
src='/static/blocks/workflow-2.png'
alt='Workflow-Block mit Beispiel für Eingabezuordnung'
width={700}
height={400}
className='rounded-xl border border-border shadow-sm'
/>
</div>
3. **Ausgaben**: Nachdem der untergeordnete Workflow abgeschlossen ist, stellt der Block folgendes bereit:
- `result` die endgültige Antwort des untergeordneten Workflows
- `success` ob er ohne Fehler ausgeführt wurde
- `error` Nachricht, wenn die Ausführung fehlschlägt
## Bereitstellungsstatus-Badge
Der Workflow-Block zeigt ein Bereitstellungsstatus-Badge an, das dir hilft zu verfolgen, ob der untergeordnete Workflow ausführungsbereit ist:
- **Bereitgestellt** Der untergeordnete Workflow wurde bereitgestellt und ist einsatzbereit. Der Block führt die aktuell bereitgestellte Version aus.
- **Nicht bereitgestellt** Der untergeordnete Workflow wurde noch nie bereitgestellt. Du musst ihn bereitstellen, bevor der Workflow-Block ihn ausführen kann.
- **Erneut bereitstellen** Seit der letzten Bereitstellung wurden Änderungen im untergeordneten Workflow erkannt. Klicke auf das Badge, um den untergeordneten Workflow mit den neuesten Änderungen erneut bereitzustellen.
<Callout type="warn">
Der Workflow-Block führt immer die zuletzt bereitgestellte Version des untergeordneten Workflows aus, nicht die Editor-Version. Stelle sicher, dass du nach Änderungen eine erneute Bereitstellung durchführst, damit der Block die neueste Logik verwendet.
</Callout>
## Hinweise zur Ausführung
- Untergeordnete Workflows laufen im gleichen Workspace-Kontext, sodass Umgebungsvariablen und Tools übernommen werden.
- Der Block verwendet Bereitstellungsversionierung: Jede API-, Zeitplan-, Webhook-, manuelle oder Chat-Ausführung ruft den bereitgestellten Snapshot auf. Stelle den untergeordneten Workflow nach Änderungen erneut bereit.
- Wenn der untergeordnete Workflow fehlschlägt, löst der Block einen Fehler aus, es sei denn, du behandelst ihn nachgelagert.
<Callout>
Halte untergeordnete Workflows fokussiert. Kleine, wiederverwendbare Abläufe machen es einfacher, sie zu kombinieren, ohne tiefe Verschachtelungen zu erzeugen.
</Callout>
@@ -0,0 +1,47 @@
---
title: Grundlagen
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Step, Steps } from 'fumadocs-ui/components/steps'
import { Video } from '@/components/ui/video'
## Wie Verbindungen funktionieren
Verbindungen sind die Pfade, die den Datenfluss zwischen Blöcken in Ihrem Workflow ermöglichen. In Sim definieren Verbindungen, wie Informationen von einem Block zum anderen übertragen werden und ermöglichen so den Datenfluss durch Ihren gesamten Workflow.
<Callout type="info">
Jede Verbindung stellt eine gerichtete Beziehung dar, bei der Daten vom Ausgang eines Quellblocks
zum Eingang eines Zielblocks fließen.
</Callout>
### Verbindungen erstellen
<Steps>
<Step>
<strong>Quellblock auswählen</strong>: Klicken Sie auf den Ausgangsport des Blocks, von dem aus Sie verbinden möchten
</Step>
<Step>
<strong>Verbindung ziehen</strong>: Ziehen Sie zum Eingangsport des Zielblocks
</Step>
<Step>
<strong>Verbindung bestätigen</strong>: Loslassen, um die Verbindung zu erstellen
</Step>
</Steps>
<div className="mx-auto w-full overflow-hidden rounded-lg my-6">
<Video src="connections-build.mp4" width={700} height={450} />
</div>
### Verbindungsablauf
Der Datenfluss durch Verbindungen folgt diesen Prinzipien:
1. **Gerichteter Fluss**: Daten fließen immer von Ausgängen zu Eingängen
2. **Ausführungsreihenfolge**: Blöcke werden basierend auf ihren Verbindungen der Reihe nach ausgeführt
3. **Datentransformation**: Daten können beim Übergang zwischen Blöcken transformiert werden
4. **Bedingte Pfade**: Einige Blöcke (wie Router und Bedingung) können den Fluss auf verschiedene Pfade leiten
<Callout type="warning">
Das Löschen einer Verbindung stoppt sofort den Datenfluss zwischen den Blöcken. Stellen Sie sicher, dass dies beabsichtigt ist, bevor Sie Verbindungen entfernen.
</Callout>
@@ -0,0 +1,192 @@
---
title: Datenstruktur
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
Wenn Sie Blöcke verbinden, ist das Verständnis der Datenstruktur verschiedener Block-Ausgaben wichtig, da die Ausgabedatenstruktur des Quellblocks bestimmt, welche Werte im Zielblock verfügbar sind. Jeder Blocktyp erzeugt eine spezifische Ausgabestruktur, auf die Sie in nachgelagerten Blöcken verweisen können.
<Callout type="info">
Das Verständnis dieser Datenstrukturen ist wesentlich für die effektive Nutzung von Verbindungs-Tags und
den Zugriff auf die richtigen Daten in Ihren Workflows.
</Callout>
## Block-Ausgabestrukturen
Verschiedene Blocktypen erzeugen unterschiedliche Ausgabestrukturen. Hier ist, was Sie von jedem Blocktyp erwarten können:
<Tabs items={['Agent Output', 'API Output', 'Function Output', 'Evaluator Output', 'Condition Output', 'Router Output']}>
<Tab>
```json
{
"content": "The generated text response",
"model": "gpt-4o",
"tokens": {
"prompt": 120,
"completion": 85,
"total": 205
},
"toolCalls": [...],
"cost": [...],
"usage": [...]
}
```
### Ausgabefelder des Agent-Blocks
- **content**: Die vom Agenten generierte Haupttextantwort
- **model**: Das verwendete KI-Modell (z.B. "gpt-4o", "claude-3-opus")
- **tokens**: Token-Nutzungsstatistiken
- **prompt**: Anzahl der Token in der Eingabeaufforderung
- **completion**: Anzahl der Token in der Vervollständigung
- **total**: Insgesamt verwendete Token
- **toolCalls**: Array von Werkzeugaufrufen des Agenten (falls vorhanden)
- **cost**: Array von Kostenobjekten für jeden Werkzeugaufruf (falls vorhanden)
- **usage**: Token-Nutzungsstatistiken für die gesamte Antwort
</Tab>
<Tab>
```json
{
"data": "Response data",
"status": 200,
"headers": {
"content-type": "application/json",
"cache-control": "no-cache"
}
}
```
### Ausgabefelder des API-Blocks
- **data**: Die Antwortdaten von der API (kann jeden Typ haben)
- **status**: HTTP-Statuscode der Antwort
- **headers**: Von der API zurückgegebene HTTP-Header
</Tab>
<Tab>
```json
{
"result": "Function return value",
"stdout": "Console output",
}
```
### Ausgabefelder des Funktionsblocks
- **result**: Der Rückgabewert der Funktion (kann jeden Typ haben)
- **stdout**: Während der Funktionsausführung erfasste Konsolenausgabe
</Tab>
<Tab>
```json
{
"content": "Evaluation summary",
"model": "gpt-5",
"tokens": {
"prompt": 120,
"completion": 85,
"total": 205
},
"metric1": 8.5,
"metric2": 7.2,
"metric3": 9.0
}
```
### Ausgabefelder des Evaluator-Blocks
- **content**: Zusammenfassung der Auswertung
- **model**: Das für die Auswertung verwendete KI-Modell
- **tokens**: Statistiken zur Token-Nutzung
- **[metricName]**: Bewertung für jede im Evaluator definierte Metrik (dynamische Felder)
</Tab>
<Tab>
```json
{
"conditionResult": true,
"selectedPath": {
"blockId": "2acd9007-27e8-4510-a487-73d3b825e7c1",
"blockType": "agent",
"blockTitle": "Follow-up Agent"
},
"selectedOption": "condition-1"
}
```
### Ausgabefelder des Condition-Blocks
- **conditionResult**: Boolesches Ergebnis der Bedingungsauswertung
- **selectedPath**: Informationen über den ausgewählten Pfad
- **blockId**: ID des nächsten Blocks im ausgewählten Pfad
- **blockType**: Typ des nächsten Blocks
- **blockTitle**: Titel des nächsten Blocks
- **selectedOption**: ID der ausgewählten Bedingung
</Tab>
<Tab>
```json
{
"content": "Routing decision",
"model": "gpt-4o",
"tokens": {
"prompt": 120,
"completion": 85,
"total": 205
},
"selectedPath": {
"blockId": "2acd9007-27e8-4510-a487-73d3b825e7c1",
"blockType": "agent",
"blockTitle": "Customer Service Agent"
}
}
```
### Ausgabefelder des Router-Blocks
- **content**: Der Routing-Entscheidungstext
- **model**: Das für das Routing verwendete KI-Modell
- **tokens**: Statistiken zur Token-Nutzung
- **selectedPath**: Informationen über den ausgewählten Pfad
- **blockId**: ID des ausgewählten Zielblocks
- **blockType**: Typ des ausgewählten Blocks
- **blockTitle**: Titel des ausgewählten Blocks
</Tab>
</Tabs>
## Benutzerdefinierte Ausgabestrukturen
Einige Blöcke können basierend auf ihrer Konfiguration benutzerdefinierte Ausgabestrukturen erzeugen:
1. **Agent-Blöcke mit Antwortformat**: Bei Verwendung eines Antwortformats in einem Agent-Block entspricht die Ausgabestruktur dem definierten Schema anstelle der Standardstruktur.
2. **Function-Blöcke**: Das Feld `result` kann jede Datenstruktur enthalten, die von Ihrem Funktionscode zurückgegeben wird.
3. **API-Blöcke**: Das Feld `data` enthält die Rückgabe der API, die jede gültige JSON-Struktur sein kann.
<Callout type="warning">
Überprüfen Sie während der Entwicklung immer die tatsächliche Ausgabestruktur Ihrer Blöcke, um sicherzustellen, dass Sie in Ihren Verbindungen auf die richtigen Felder verweisen.
</Callout>
## Verschachtelte Datenstrukturen
Viele Block-Ausgaben enthalten verschachtelte Datenstrukturen. Du kannst auf diese mit Punktnotation in Verbindungs-Tags zugreifen:
```
<blockName.path.to.nested.data>
```
Zum Beispiel:
- `<agent1.tokens.total>` - Greife auf die Gesamtzahl der Tokens aus einem Agent-Block zu
- `<api1.data.results[0].id>` - Greife auf die ID des ersten Ergebnisses einer API-Antwort zu
- `<function1.result.calculations.total>` - Greife auf ein verschachteltes Feld im Ergebnis eines Funktionsblocks zu
@@ -0,0 +1,42 @@
---
title: Übersicht
description: Verbinde deine Blöcke miteinander.
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Card, Cards } from 'fumadocs-ui/components/card'
import { ConnectIcon } from '@/components/icons'
import { Video } from '@/components/ui/video'
Verbindungen sind die Pfade, die den Datenfluss zwischen Blöcken in deinem Workflow ermöglichen. Sie definieren, wie Informationen von einem Block zum anderen weitergegeben werden und ermöglichen dir, komplexe, mehrstufige Prozesse zu erstellen.
<Callout type="info">
Richtig konfigurierte Verbindungen sind entscheidend für die Erstellung effektiver Workflows. Sie bestimmen, wie
Daten durch dein System fließen und wie Blöcke miteinander interagieren.
</Callout>
<div className="mx-auto w-full overflow-hidden rounded-lg">
<Video src="connections.mp4" />
</div>
## Verbindungstypen
Sim unterstützt verschiedene Arten von Verbindungen, die verschiedene Workflow-Muster ermöglichen:
<Cards>
<Card title="Grundlagen der Verbindungen" href="/connections/basics">
Lerne, wie Verbindungen funktionieren und wie du sie in deinen Workflows erstellst
</Card>
<Card title="Verbindungs-Tags" href="/connections/tags">
Verstehe, wie du Verbindungs-Tags verwendest, um auf Daten zwischen Blöcken zu verweisen
</Card>
<Card title="Datenstruktur" href="/connections/data-structure">
Erkunde die Ausgabedatenstrukturen verschiedener Blocktypen
</Card>
<Card title="Datenzugriff" href="/connections/accessing-data">
Lerne Techniken für den Zugriff und die Manipulation verbundener Daten
</Card>
<Card title="Best Practices" href="/connections/best-practices">
Folge empfohlenen Mustern für effektives Verbindungsmanagement
</Card>
</Cards>
@@ -0,0 +1,109 @@
---
title: Tags
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Video } from '@/components/ui/video'
Verbindungs-Tags sind visuelle Darstellungen der verfügbaren Daten aus verbundenen Blöcken und bieten eine einfache Möglichkeit, auf Daten zwischen Blöcken und Ausgaben aus vorherigen Blöcken in Ihrem Workflow zu verweisen.
<div className="mx-auto w-full overflow-hidden rounded-lg">
<Video src="connections.mp4" />
</div>
### Was sind Verbindungs-Tags?
Verbindungs-Tags sind interaktive Elemente, die erscheinen, wenn Blöcke verbunden werden. Sie repräsentieren die Daten, die von einem Block zum anderen fließen können und ermöglichen es Ihnen:
- Verfügbare Daten aus Quellblöcken zu visualisieren
- Auf bestimmte Datenfelder in Zielblöcken zu verweisen
- Dynamische Datenflüsse zwischen Blöcken zu erstellen
<Callout type="info">
Verbindungs-Tags machen es einfach zu sehen, welche Daten aus vorherigen Blöcken verfügbar sind und diese in Ihrem
aktuellen Block zu verwenden, ohne sich komplexe Datenstrukturen merken zu müssen.
</Callout>
## Verwendung von Verbindungs-Tags
Es gibt zwei Hauptmethoden, um Verbindungs-Tags in Ihren Workflows zu verwenden:
<div className="my-6 grid grid-cols-1 gap-4 md:grid-cols-2">
<div className="rounded-lg border border-gray-200 p-4 dark:border-gray-800">
<h3 className="mb-2 text-lg font-medium">Drag and Drop</h3>
<div className="text-sm text-gray-600 dark:text-gray-400">
Klicken Sie auf einen Verbindungs-Tag und ziehen Sie ihn in Eingabefelder von Zielblöcken. Ein Dropdown-Menü wird
angezeigt, das verfügbare Werte zeigt.
</div>
<ol className="mt-2 list-decimal pl-5 text-sm text-gray-600 dark:text-gray-400">
<li>Fahren Sie mit dem Mauszeiger über einen Verbindungs-Tag, um verfügbare Daten zu sehen</li>
<li>Klicken und ziehen Sie den Tag in ein Eingabefeld</li>
<li>Wählen Sie das spezifische Datenfeld aus dem Dropdown-Menü</li>
<li>Die Referenz wird automatisch eingefügt</li>
</ol>
</div>
<div className="rounded-lg border border-gray-200 p-4 dark:border-gray-800">
<h3 className="mb-2 text-lg font-medium">Spitze-Klammer-Syntax</h3>
<div className="text-sm text-gray-600 dark:text-gray-400">
Geben Sie <code>&lt;&gt;</code> in Eingabefeldern ein, um ein Dropdown-Menü mit verfügbaren Verbindungswerten
aus vorherigen Blöcken zu sehen.
</div>
<ol className="mt-2 list-decimal pl-5 text-sm text-gray-600 dark:text-gray-400">
<li>Klicken Sie in ein beliebiges Eingabefeld, in dem Sie verbundene Daten verwenden möchten</li>
<li>
Geben Sie <code>&lt;&gt;</code> ein, um das Verbindungs-Dropdown-Menü aufzurufen
</li>
<li>Durchsuchen und wählen Sie die Daten aus, auf die Sie verweisen möchten</li>
<li>Tippen Sie weiter oder wählen Sie aus dem Dropdown-Menü, um die Referenz zu vervollständigen</li>
</ol>
</div>
</div>
## Tag-Syntax
Verbindungs-Tags verwenden eine einfache Syntax, um auf Daten zu verweisen:
```
<blockName.path.to.data>
```
Wobei:
- `blockName` ist der Name des Quellblocks
- `path.to.data` ist der Pfad zum spezifischen Datenfeld
Zum Beispiel:
- `<agent1.content>` - Verweist auf das Inhaltsfeld eines Blocks mit der ID "agent1"
- `<api2.data.users[0].name>` - Verweist auf den Namen des ersten Benutzers im Benutzer-Array aus dem Datenfeld eines Blocks mit der ID "api2"
## Dynamische Tag-Referenzen
Verbindungs-Tags werden zur Laufzeit ausgewertet, was bedeutet:
1. Sie verweisen immer auf die aktuellsten Daten
2. Sie können in Ausdrücken verwendet und mit statischem Text kombiniert werden
3. Sie können in andere Datenstrukturen eingebettet werden
### Beispiele
```javascript
// Reference in text
"The user's name is <userBlock.name>"
// Reference in JSON
{
"userName": "<userBlock.name>",
"orderTotal": <apiBlock.data.total>
}
// Reference in code
const greeting = "Hello, <userBlock.name>!";
const total = <apiBlock.data.total> * 1.1; // Add 10% tax
```
<Callout type="warning">
Wenn Sie Verbindungs-Tags in numerischen Kontexten verwenden, stellen Sie sicher, dass die referenzierten Daten tatsächlich eine Zahl sind,
um Typkonvertierungsprobleme zu vermeiden.
</Callout>
+248
View File
@@ -0,0 +1,248 @@
---
title: Copilot
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Card, Cards } from 'fumadocs-ui/components/card'
import { Image } from '@/components/ui/image'
import { MessageCircle, Package, Zap, Infinity as InfinityIcon, Brain, BrainCircuit } from 'lucide-react'
Copilot ist dein Assistent im Editor, der dir hilft, Workflows mit Sim Copilot zu erstellen und zu bearbeiten sowie diese zu verstehen und zu verbessern. Er kann:
- **Erklären**: Beantwortet Fragen zu Sim und deinem aktuellen Workflow
- **Anleiten**: Schlägt Bearbeitungen und Best Practices vor
- **Bearbeiten**: Nimmt Änderungen an Blöcken, Verbindungen und Einstellungen vor, wenn du zustimmst
<Callout type="info">
Copilot ist ein von Sim verwalteter Dienst. Für selbst gehostete Installationen generiere einen Copilot API-Schlüssel in der gehosteten App (sim.ai → Einstellungen → Copilot)
1. Gehe zu [sim.ai](https://sim.ai) → Einstellungen → Copilot und generiere einen Copilot API-Schlüssel
2. Setze `COPILOT_API_KEY` in deiner selbst gehosteten Umgebung auf diesen Wert
</Callout>
## Kontextmenü (@)
Verwende das `@` Symbol, um auf verschiedene Ressourcen zu verweisen und Copilot mehr Kontext über deinen Arbeitsbereich zu geben:
<Image
src="/static/copilot/copilot-menu.png"
alt="Copilot-Kontextmenü mit verfügbaren Referenzoptionen"
width={600}
height={400}
/>
Das `@` Menü bietet Zugriff auf:
- **Chats**: Verweise auf vorherige Copilot-Gespräche
- **Alle Workflows**: Verweise auf beliebige Workflows in deinem Arbeitsbereich
- **Workflow-Blöcke**: Verweise auf bestimmte Blöcke aus Workflows
- **Blöcke**: Verweise auf Blocktypen und Vorlagen
- **Wissen**: Verweise auf deine hochgeladenen Dokumente und Wissensdatenbank
- **Dokumentation**: Verweise auf Sim-Dokumentation
- **Vorlagen**: Verweise auf Workflow-Vorlagen
- **Logs**: Verweise auf Ausführungsprotokolle und Ergebnisse
Diese kontextbezogenen Informationen helfen Copilot, genauere und relevantere Unterstützung für deinen spezifischen Anwendungsfall zu bieten.
## Modi
<Cards>
<Card
title={
<span className="inline-flex items-center gap-2">
<MessageCircle className="h-4 w-4 text-muted-foreground" />
Fragen
</span>
}
>
<div className="m-0 text-sm">
Frage-Antwort-Modus für Erklärungen, Anleitungen und Vorschläge ohne Änderungen an deinem Workflow vorzunehmen.
</div>
</Card>
<Card
title={
<span className="inline-flex items-center gap-2">
<Package className="h-4 w-4 text-muted-foreground" />
Agent
</span>
}
>
<div className="m-0 text-sm">
Erstellen-und-Bearbeiten-Modus. Copilot schlägt spezifische Änderungen vor (Blöcke hinzufügen, Variablen verbinden, Einstellungen anpassen) und wendet sie an, wenn du zustimmst.
</div>
</Card>
</Cards>
<div className="flex justify-center">
<Image
src="/static/copilot/copilot-mode.png"
alt="Copilot-Modusauswahl-Oberfläche"
width={600}
height={400}
className="my-6"
/>
</div>
## Tiefenebenen
<Cards>
<Card
title={
<span className="inline-flex items-center gap-2">
<Zap className="h-4 w-4 text-muted-foreground" />
Schnell
</span>
}
>
<div className="m-0 text-sm">Am schnellsten und günstigsten. Ideal für kleine Änderungen, einfache Arbeitsabläufe und geringfügige Anpassungen.</div>
</Card>
<Card
title={
<span className="inline-flex items-center gap-2">
<InfinityIcon className="h-4 w-4 text-muted-foreground" />
Auto
</span>
}
>
<div className="m-0 text-sm">Ausgewogene Geschwindigkeit und Denkleistung. Empfohlene Standardeinstellung für die meisten Aufgaben.</div>
</Card>
<Card
title={
<span className="inline-flex items-center gap-2">
<Brain className="h-4 w-4 text-muted-foreground" />
Erweitert
</span>
}
>
<div className="m-0 text-sm">Mehr Denkleistung für umfangreichere Arbeitsabläufe und komplexe Änderungen bei gleichzeitiger Leistungsfähigkeit.</div>
</Card>
<Card
title={
<span className="inline-flex items-center gap-2">
<BrainCircuit className="h-4 w-4 text-muted-foreground" />
Behemoth
</span>
}
>
<div className="m-0 text-sm">Maximale Denkleistung für tiefgreifende Planung, Fehlerbehebung und komplexe architektonische Änderungen.</div>
</Card>
</Cards>
### Modusauswahl-Oberfläche
Du kannst einfach zwischen verschiedenen Denkmodi über die Modusauswahl in der Copilot-Oberfläche wechseln:
<Image
src="/static/copilot/copilot-models.png"
alt="Copilot-Modusauswahl zeigt den erweiterten Modus mit MAX-Umschalter"
width={600}
height={300}
/>
Die Oberfläche ermöglicht dir:
- **Denkebene auswählen**: Wähle zwischen Schnell, Auto, Erweitert oder Behemoth
- **MAX-Modus aktivieren**: Umschalten für maximale Denkfähigkeiten, wenn du die gründlichste Analyse benötigst
- **Modusbeschreibungen anzeigen**: Verstehe, wofür jeder Modus optimiert ist
Wähle deinen Modus basierend auf der Komplexität deiner Aufgabe - verwende Schnell für einfache Fragen und Behemoth für komplexe architektonische Änderungen.
## Abrechnung und Kostenberechnung
### Wie Kosten berechnet werden
Die Copilot-Nutzung wird pro Token vom zugrundeliegenden LLM abgerechnet:
- **Eingabe-Tokens**: werden zum Basispreis des Anbieters berechnet (**zum Selbstkostenpreis**)
- **Ausgabe-Tokens**: werden mit dem **1,5-fachen** des Basis-Ausgabepreises des Anbieters berechnet
```javascript
copilotCost = (inputTokens × inputPrice + outputTokens × (outputPrice × 1.5)) / 1,000,000
```
| Komponente | Angewendeter Tarif |
|------------|------------------------|
| Eingabe | inputPrice |
| Ausgabe | outputPrice × 1,5 |
<Callout type="warning">
Die angezeigten Preise spiegeln die Tarife vom 4. September 2025 wider. Überprüfen Sie die Anbieter-Dokumentation für aktuelle Preise.
</Callout>
<Callout type="info">
Modellpreise werden pro Million Tokens angegeben. Die Berechnung teilt durch 1.000.000, um die tatsächlichen Kosten zu ermitteln. Siehe <a href="/execution/costs">die Seite zur Kostenberechnung</a> für Hintergründe und Beispiele.
</Callout>
Fahre mit der Maus über eine deiner Nachrichten und klicke auf **Bearbeiten**, um sie zu ändern und erneut zu senden. Dies ist nützlich, um deine Eingaben zu verfeinern.
### Nachrichtenwarteschlange
Wenn du eine Nachricht sendest, während Copilot noch antwortet, wird sie in die Warteschlange gestellt. Du kannst:
- Warteschlangennachrichten im erweiterbaren Warteschlangenpanel anzeigen
- Eine Nachricht aus der Warteschlange sofort senden (bricht die aktuelle Antwort ab)
- Nachrichten aus der Warteschlange entfernen
## Dateianhänge
Klicke auf das Anhang-Symbol, um Dateien mit deiner Nachricht hochzuladen. Unterstützte Dateitypen umfassen:
- Bilder (Vorschau-Thumbnails werden angezeigt)
- PDFs
- Textdateien, JSON, XML
- Andere Dokumentformate
Dateien werden als anklickbare Thumbnails angezeigt, die in einem neuen Tab geöffnet werden.
## Checkpoints & Änderungen
Wenn Copilot Änderungen an deinem Workflow vornimmt, speichert es Checkpoints, damit du bei Bedarf zurückkehren kannst.
### Checkpoints anzeigen
Fahre mit der Maus über eine Copilot-Nachricht und klicke auf das Checkpoints-Symbol, um gespeicherte Workflow-Zustände für diese Nachricht anzuzeigen.
### Änderungen rückgängig machen
Klicke bei jedem Checkpoint auf **Rückgängig machen**, um deinen Workflow auf diesen Zustand zurückzusetzen. Ein Bestätigungsdialog warnt dich, dass diese Aktion nicht rückgängig gemacht werden kann.
### Änderungen akzeptieren
Wenn Copilot Änderungen vorschlägt, kannst du:
- **Akzeptieren**: Die vorgeschlagenen Änderungen anwenden (`Mod+Shift+Enter`)
- **Ablehnen**: Die Änderungen verwerfen und deinen aktuellen Workflow beibehalten
## Denkblöcke
Bei komplexen Anfragen kann Copilot seinen Denkprozess in erweiterbaren Denkblöcken anzeigen:
- Blöcke werden automatisch erweitert, während Copilot denkt
- Klicken zum manuellen Erweitern/Reduzieren
- Zeigt die Dauer des Denkprozesses an
- Hilft dir zu verstehen, wie Copilot zu seiner Lösung gekommen ist
## Optionsauswahl
Wenn Copilot mehrere Optionen präsentiert, kannst du auswählen mit:
| Steuerung | Aktion |
|---------|--------|
| **1-9** | Option nach Nummer auswählen |
| **Pfeiltaste auf/ab** | Zwischen Optionen navigieren |
| **Eingabetaste** | Hervorgehobene Option auswählen |
Ausgewählte Optionen sind hervorgehoben; nicht ausgewählte Optionen erscheinen durchgestrichen.
## Tastenkombinationen
| Tastenkombination | Aktion |
|----------|--------|
| `@` | Kontextmenü öffnen |
| `/` | Slash-Befehle öffnen |
| `Arrow Up/Down` | Menüelemente navigieren |
| `Enter` | Menüelement auswählen |
| `Esc` | Menüs schließen |
| `Mod+Shift+Enter` | Copilot-Änderungen akzeptieren |
## Nutzungslimits
Die Copilot-Nutzung wird pro Token des zugrunde liegenden LLM abgerechnet. Wenn Sie Ihr Nutzungslimit erreichen, fordert Copilot Sie auf, Ihr Limit zu erhöhen. Sie können die Nutzung in Schritten (50 $, 100 $) von Ihrer aktuellen Basis aus hinzufügen.
<Callout type="info">
Siehe die [Seite zur Kostenberechnung](/execution/costs) für Abrechnungsdetails.
</Callout>
@@ -0,0 +1,123 @@
---
title: Enterprise
description: Enterprise-Funktionen für Organisationen mit erweiterten
Sicherheits- und Compliance-Anforderungen
---
import { Callout } from 'fumadocs-ui/components/callout'
Sim Enterprise bietet erweiterte Funktionen für Organisationen mit erhöhten Sicherheits-, Compliance- und Verwaltungsanforderungen.
---
## Bring Your Own Key (BYOK)
Verwenden Sie Ihre eigenen API-Schlüssel für KI-Modellanbieter anstelle der gehosteten Schlüssel von Sim.
### Unterstützte Anbieter
| Anbieter | Verwendung |
|----------|-------|
| OpenAI | Knowledge Base-Embeddings, Agent-Block |
| Anthropic | Agent-Block |
| Google | Agent-Block |
| Mistral | Knowledge Base OCR, Agent-Block |
| Fireworks | Agent-Block |
| Firecrawl | Web-Scraping, Crawling, Suche und Extraktion |
| Exa | KI-gestützte Suche und Recherche |
| Serper | Google-Such-API |
| Linkup | Websuche und Inhaltsabruf |
| Parallel AI | Websuche, Extraktion und tiefgehende Recherche |
| Perplexity | KI-gestützter Chat und Websuche |
| Jina AI | Web-Lesen und Suche |
| Google Cloud | Translate, Maps, PageSpeed und Books APIs |
| Brandfetch | Marken-Assets, Logos, Farben und Unternehmensinformationen |
### Einrichtung
1. Navigieren Sie zu **Einstellungen** → **BYOK** in Ihrem Workspace
2. Klicken Sie auf **Schlüssel hinzufügen** für Ihren Anbieter
3. Geben Sie Ihren API-Schlüssel ein und speichern Sie
<Callout type="warn">
BYOK-Schlüssel werden verschlüsselt gespeichert. Nur Organisationsadministratoren und -inhaber können Schlüssel verwalten.
</Callout>
Wenn konfiguriert, verwenden Workflows Ihren Schlüssel anstelle der gehosteten Schlüssel von Sim. Bei Entfernung wechseln Workflows automatisch zu den gehosteten Schlüsseln zurück.
---
## Single Sign-On (SSO)
Enterprise-Authentifizierung mit SAML 2.0- und OIDC-Unterstützung für zentralisiertes Identitätsmanagement.
### Unterstützte Anbieter
- Okta
- Azure AD / Entra ID
- Google Workspace
- OneLogin
- Jeder SAML 2.0- oder OIDC-Anbieter
### Einrichtung
1. Navigieren Sie zu **Einstellungen** → **SSO** in Ihrem Workspace
2. Wählen Sie Ihren Identitätsanbieter
3. Konfigurieren Sie die Verbindung mithilfe der Metadaten Ihres IdP
4. Aktivieren Sie SSO für Ihre Organisation
<Callout type="info">
Sobald SSO aktiviert ist, authentifizieren sich Teammitglieder über Ihren Identitätsanbieter anstelle von E-Mail/Passwort.
</Callout>
---
## Self-Hosted
Für selbst gehostete Bereitstellungen können Enterprise-Funktionen über Umgebungsvariablen aktiviert werden:
| Variable | Beschreibung |
|----------|-------------|
| `SSO_ENABLED`, `NEXT_PUBLIC_SSO_ENABLED` | Single Sign-On mit SAML/OIDC |
| `DISABLE_INVITATIONS`, `NEXT_PUBLIC_DISABLE_INVITATIONS` | Workspace-/Organisations-Einladungen global deaktivieren |
<Callout type="warn">
BYOK ist nur im gehosteten Sim verfügbar. Selbst gehostete Deployments konfigurieren AI-Provider-Schlüssel direkt über Umgebungsvariablen.
</Callout>
Wenn die Abrechnung deaktiviert ist, verwenden Sie die Admin-API zur Verwaltung von Organisationen:
```bash
# Create an organization
curl -X POST https://your-instance/api/v1/admin/organizations \
-H "x-admin-key: YOUR_ADMIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "My Organization", "ownerId": "user-id-here"}'
# Add a member
curl -X POST https://your-instance/api/v1/admin/organizations/{orgId}/members \
-H "x-admin-key: YOUR_ADMIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"userId": "user-id-here", "role": "admin"}'
```
### Workspace-Mitglieder
Wenn Einladungen deaktiviert sind, verwenden Sie die Admin-API zur direkten Verwaltung von Workspace-Mitgliedschaften:
```bash
# Add a user to a workspace
curl -X POST https://your-instance/api/v1/admin/workspaces/{workspaceId}/members \
-H "x-admin-key: YOUR_ADMIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"userId": "user-id-here", "permissions": "write"}'
# Remove a user from a workspace
curl -X DELETE "https://your-instance/api/v1/admin/workspaces/{workspaceId}/members?userId=user-id-here" \
-H "x-admin-key: YOUR_ADMIN_API_KEY"
```
### Hinweise
- Die Aktivierung von `ACCESS_CONTROL_ENABLED` aktiviert automatisch Organisationen, da die Zugriffskontrolle eine Organisationsmitgliedschaft erfordert.
- Wenn `DISABLE_INVITATIONS` gesetzt ist, können Benutzer keine Einladungen versenden. Verwenden Sie stattdessen die Admin-API zur Verwaltung von Workspace- und Organisationsmitgliedschaften.
+609
View File
@@ -0,0 +1,609 @@
---
title: Externe API
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
import { Video } from '@/components/ui/video'
Sim bietet eine umfassende externe API zum Abfragen von Workflow-Ausführungsprotokollen und zum Einrichten von Webhooks für Echtzeit-Benachrichtigungen, wenn Workflows abgeschlossen werden.
## Authentifizierung
Alle API-Anfragen erfordern einen API-Schlüssel, der im Header `x-api-key` übergeben wird:
```bash
curl -H "x-api-key: YOUR_API_KEY" \
https://sim.ai/api/v1/logs?workspaceId=YOUR_WORKSPACE_ID
```
Sie können API-Schlüssel in Ihren Benutzereinstellungen im Sim-Dashboard generieren.
## Logs-API
Alle API-Antworten enthalten Informationen über Ihre Workflow-Ausführungslimits und -nutzung:
```json
"limits": {
"workflowExecutionRateLimit": {
"sync": {
"requestsPerMinute": 60, // Sustained rate limit per minute
"maxBurst": 120, // Maximum burst capacity
"remaining": 118, // Current tokens available (up to maxBurst)
"resetAt": "..." // When tokens next refill
},
"async": {
"requestsPerMinute": 200, // Sustained rate limit per minute
"maxBurst": 400, // Maximum burst capacity
"remaining": 398, // Current tokens available
"resetAt": "..." // When tokens next refill
}
},
"usage": {
"currentPeriodCost": 1.234, // Current billing period usage in USD
"limit": 10, // Usage limit in USD
"plan": "pro", // Current subscription plan
"isExceeded": false // Whether limit is exceeded
}
}
```
**Hinweis:** Ratenbegrenzungen verwenden einen Token-Bucket-Algorithmus. `remaining` kann `requestsPerMinute` bis zu `maxBurst` überschreiten, wenn du dein volles Kontingent in letzter Zeit nicht genutzt hast, was Burst-Traffic ermöglicht. Die Ratenbegrenzungen im Antworttext gelten für Workflow-Ausführungen. Die Ratenbegrenzungen für den Aufruf dieses API-Endpunkts befinden sich in den Antwort-Headern (`X-RateLimit-*`).
### Logs abfragen
Fragen Sie Workflow-Ausführungsprotokolle mit umfangreichen Filteroptionen ab.
<Tabs items={['Request', 'Response']}>
<Tab value="Request">
```http
GET /api/v1/logs
```
**Erforderliche Parameter:**
- `workspaceId` - Ihre Workspace-ID
**Optionale Filter:**
- `workflowIds` - Kommagetrennte Workflow-IDs
- `folderIds` - Kommagetrennte Ordner-IDs
- `triggers` - Kommagetrennte Auslösertypen: `api`, `webhook`, `schedule`, `manual`, `chat`
- `level` - Nach Level filtern: `info`, `error`
- `startDate` - ISO-Zeitstempel für den Beginn des Datumsbereichs
- `endDate` - ISO-Zeitstempel für das Ende des Datumsbereichs
- `executionId` - Exakte Übereinstimmung der Ausführungs-ID
- `minDurationMs` - Minimale Ausführungsdauer in Millisekunden
- `maxDurationMs` - Maximale Ausführungsdauer in Millisekunden
- `minCost` - Minimale Ausführungskosten
- `maxCost` - Maximale Ausführungskosten
- `model` - Nach verwendetem KI-Modell filtern
**Paginierung:**
- `limit` - Ergebnisse pro Seite (Standard: 100)
- `cursor` - Cursor für die nächste Seite
- `order` - Sortierreihenfolge: `desc`, `asc` (Standard: desc)
**Detailebene:**
- `details` - Detailebene der Antwort: `basic`, `full` (Standard: basic)
- `includeTraceSpans` - Trace-Spans einschließen (Standard: false)
- `includeFinalOutput` - Endgültige Ausgabe einschließen (Standard: false)
</Tab>
<Tab value="Response">
```json
{
"data": [
{
"id": "log_abc123",
"workflowId": "wf_xyz789",
"executionId": "exec_def456",
"level": "info",
"trigger": "api",
"startedAt": "2025-01-01T12:34:56.789Z",
"endedAt": "2025-01-01T12:34:57.123Z",
"totalDurationMs": 334,
"cost": {
"total": 0.00234
},
"files": null
}
],
"nextCursor": "eyJzIjoiMjAyNS0wMS0wMVQxMjozNDo1Ni43ODlaIiwiaWQiOiJsb2dfYWJjMTIzIn0",
"limits": {
"workflowExecutionRateLimit": {
"sync": {
"requestsPerMinute": 60,
"maxBurst": 120,
"remaining": 118,
"resetAt": "2025-01-01T12:35:56.789Z"
},
"async": {
"requestsPerMinute": 200,
"maxBurst": 400,
"remaining": 398,
"resetAt": "2025-01-01T12:35:56.789Z"
}
},
"usage": {
"currentPeriodCost": 1.234,
"limit": 10,
"plan": "pro",
"isExceeded": false
}
}
}
```
</Tab>
</Tabs>
### Log-Details abrufen
Rufen Sie detaillierte Informationen zu einem bestimmten Logeintrag ab.
<Tabs items={['Request', 'Response']}>
<Tab value="Request">
```http
GET /api/v1/logs/{id}
```
</Tab>
<Tab value="Response">
```json
{
"data": {
"id": "log_abc123",
"workflowId": "wf_xyz789",
"executionId": "exec_def456",
"level": "info",
"trigger": "api",
"startedAt": "2025-01-01T12:34:56.789Z",
"endedAt": "2025-01-01T12:34:57.123Z",
"totalDurationMs": 334,
"workflow": {
"id": "wf_xyz789",
"name": "My Workflow",
"description": "Process customer data"
},
"executionData": {
"traceSpans": [...],
"finalOutput": {...}
},
"cost": {
"total": 0.00234,
"tokens": {
"prompt": 123,
"completion": 456,
"total": 579
},
"models": {
"gpt-4o": {
"input": 0.001,
"output": 0.00134,
"total": 0.00234,
"tokens": {
"prompt": 123,
"completion": 456,
"total": 579
}
}
}
},
"limits": {
"workflowExecutionRateLimit": {
"sync": {
"requestsPerMinute": 60,
"maxBurst": 120,
"remaining": 118,
"resetAt": "2025-01-01T12:35:56.789Z"
},
"async": {
"requestsPerMinute": 200,
"maxBurst": 400,
"remaining": 398,
"resetAt": "2025-01-01T12:35:56.789Z"
}
},
"usage": {
"currentPeriodCost": 1.234,
"limit": 10,
"plan": "pro",
"isExceeded": false
}
}
}
}
```
</Tab>
</Tabs>
### Ausführungsdetails abrufen
Rufen Sie Ausführungsdetails einschließlich des Workflow-Zustandsschnappschusses ab.
<Tabs items={['Request', 'Response']}>
<Tab value="Request">
```http
GET /api/v1/logs/executions/{executionId}
```
</Tab>
<Tab value="Response">
```json
{
"executionId": "exec_def456",
"workflowId": "wf_xyz789",
"workflowState": {
"blocks": {...},
"edges": [...],
"loops": {...},
"parallels": {...}
},
"executionMetadata": {
"trigger": "api",
"startedAt": "2025-01-01T12:34:56.789Z",
"endedAt": "2025-01-01T12:34:57.123Z",
"totalDurationMs": 334,
"cost": {...}
}
}
```
</Tab>
</Tabs>
## Benachrichtigungen
Erhalten Sie Echtzeit-Benachrichtigungen, wenn Workflow-Ausführungen abgeschlossen sind, per Webhook, E-Mail oder Slack. Benachrichtigungen werden auf Workspace-Ebene von der Protokollseite aus konfiguriert.
### Konfiguration
Konfigurieren Sie Benachrichtigungen von der Protokollseite aus, indem Sie auf die Menütaste klicken und "Benachrichtigungen konfigurieren" auswählen.
**Benachrichtigungskanäle:**
- **Webhook**: Senden Sie HTTP POST-Anfragen an Ihren Endpunkt
- **E-Mail**: Erhalten Sie E-Mail-Benachrichtigungen mit Ausführungsdetails
- **Slack**: Posten Sie Nachrichten in einen Slack-Kanal
**Workflow-Auswahl:**
- Wählen Sie bestimmte Workflows zur Überwachung aus
- Oder wählen Sie "Alle Workflows", um aktuelle und zukünftige Workflows einzubeziehen
**Filteroptionen:**
- `levelFilter`: Zu empfangende Protokollebenen (`info`, `error`)
- `triggerFilter`: Zu empfangende Auslösertypen (`api`, `webhook`, `schedule`, `manual`, `chat`)
**Optionale Daten:**
- `includeFinalOutput`: Schließt die endgültige Ausgabe des Workflows ein
- `includeTraceSpans`: Schließt detaillierte Ausführungs-Trace-Spans ein
- `includeRateLimits`: Schließt Informationen zum Ratenlimit ein (Sync/Async-Limits und verbleibende)
- `includeUsageData`: Schließt Abrechnungszeitraum-Nutzung und -Limits ein
### Alarmregeln
Anstatt Benachrichtigungen für jede Ausführung zu erhalten, konfigurieren Sie Alarmregeln, um nur bei erkannten Problemen benachrichtigt zu werden:
**Aufeinanderfolgende Fehler**
- Alarm nach X aufeinanderfolgenden fehlgeschlagenen Ausführungen (z.B. 3 Fehler in Folge)
- Wird zurückgesetzt, wenn eine Ausführung erfolgreich ist
**Fehlerrate**
- Alarm, wenn die Fehlerrate X% in den letzten Y Stunden überschreitet
- Erfordert mindestens 5 Ausführungen im Zeitfenster
- Wird erst nach Ablauf des vollständigen Zeitfensters ausgelöst
**Latenz-Schwellenwert**
- Alarm, wenn eine Ausführung länger als X Sekunden dauert
- Nützlich zum Erkennen langsamer oder hängender Workflows
**Latenz-Spitze**
- Alarm, wenn die Ausführung X% langsamer als der Durchschnitt ist
- Vergleicht mit der durchschnittlichen Dauer über das konfigurierte Zeitfenster
- Erfordert mindestens 5 Ausführungen, um eine Baseline zu etablieren
**Kostenschwelle**
- Alarmierung, wenn eine einzelne Ausführung mehr als $X kostet
- Nützlich, um teure LLM-Aufrufe zu erkennen
**Keine Aktivität**
- Alarmierung, wenn innerhalb von X Stunden keine Ausführungen stattfinden
- Nützlich zur Überwachung geplanter Workflows, die regelmäßig ausgeführt werden sollten
**Fehlerzählung**
- Alarmierung, wenn die Fehleranzahl X innerhalb eines Zeitfensters überschreitet
- Erfasst die Gesamtfehler, nicht aufeinanderfolgende
Alle Alarmtypen beinhalten eine Abklingzeit von 1 Stunde, um Benachrichtigungsspam zu vermeiden.
### Webhook-Konfiguration
Für Webhooks stehen zusätzliche Optionen zur Verfügung:
- `url`: Ihre Webhook-Endpunkt-URL
- `secret`: Optionales Geheimnis für HMAC-Signaturverifizierung
### Payload-Struktur
Wenn eine Workflow-Ausführung abgeschlossen ist, sendet Sim die folgende Payload (über Webhook POST, E-Mail oder Slack):
```json
{
"id": "evt_123",
"type": "workflow.execution.completed",
"timestamp": 1735925767890,
"data": {
"workflowId": "wf_xyz789",
"executionId": "exec_def456",
"status": "success",
"level": "info",
"trigger": "api",
"startedAt": "2025-01-01T12:34:56.789Z",
"endedAt": "2025-01-01T12:34:57.123Z",
"totalDurationMs": 334,
"cost": {
"total": 0.00234,
"tokens": {
"prompt": 123,
"completion": 456,
"total": 579
},
"models": {
"gpt-4o": {
"input": 0.001,
"output": 0.00134,
"total": 0.00234,
"tokens": {
"prompt": 123,
"completion": 456,
"total": 579
}
}
}
},
"files": null,
"finalOutput": {...}, // Only if includeFinalOutput=true
"traceSpans": [...], // Only if includeTraceSpans=true
"rateLimits": {...}, // Only if includeRateLimits=true
"usage": {...} // Only if includeUsageData=true
},
"links": {
"log": "/v1/logs/log_abc123",
"execution": "/v1/logs/executions/exec_def456"
}
}
```
### Webhook-Header
Jede Webhook-Anfrage enthält diese Header (nur Webhook-Kanal):
- `sim-event`: Ereignistyp (immer `workflow.execution.completed`)
- `sim-timestamp`: Unix-Zeitstempel in Millisekunden
- `sim-delivery-id`: Eindeutige Zustell-ID für Idempotenz
- `sim-signature`: HMAC-SHA256-Signatur zur Verifizierung (falls Geheimnis konfiguriert)
- `Idempotency-Key`: Gleich wie Zustell-ID zur Erkennung von Duplikaten
### Signaturverifizierung
Wenn Sie ein Webhook-Geheimnis konfigurieren, überprüfen Sie die Signatur, um sicherzustellen, dass der Webhook von Sim stammt:
<Tabs items={['Node.js', 'Python']}>
<Tab value="Node.js">
```javascript
import crypto from 'crypto';
function verifyWebhookSignature(body, signature, secret) {
const [timestampPart, signaturePart] = signature.split(',');
const timestamp = timestampPart.replace('t=', '');
const expectedSignature = signaturePart.replace('v1=', '');
const signatureBase = `${timestamp}.${body}`;
const hmac = crypto.createHmac('sha256', secret);
hmac.update(signatureBase);
const computedSignature = hmac.digest('hex');
return computedSignature === expectedSignature;
}
// In your webhook handler
app.post('/webhook', (req, res) => {
const signature = req.headers['sim-signature'];
const body = JSON.stringify(req.body);
if (!verifyWebhookSignature(body, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
// Process the webhook...
});
```
</Tab>
<Tab value="Python">
```python
import hmac
import hashlib
import json
def verify_webhook_signature(body: str, signature: str, secret: str) -> bool:
timestamp_part, signature_part = signature.split(',')
timestamp = timestamp_part.replace('t=', '')
expected_signature = signature_part.replace('v1=', '')
signature_base = f"{timestamp}.{body}"
computed_signature = hmac.new(
secret.encode(),
signature_base.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(computed_signature, expected_signature)
# In your webhook handler
@app.route('/webhook', methods=['POST'])
def webhook():
signature = request.headers.get('sim-signature')
body = json.dumps(request.json)
if not verify_webhook_signature(body, signature, os.environ['WEBHOOK_SECRET']):
return 'Invalid signature', 401
# Process the webhook...
```
</Tab>
</Tabs>
### Wiederholungsrichtlinie
Fehlgeschlagene Webhook-Zustellungen werden mit exponentiellem Backoff und Jitter wiederholt:
- Maximale Versuche: 5
- Wiederholungsverzögerungen: 5 Sekunden, 15 Sekunden, 1 Minute, 3 Minuten, 10 Minuten
- Jitter: Bis zu 10% zusätzliche Verzögerung, um Überlastung zu vermeiden
- Nur HTTP 5xx und 429 Antworten lösen Wiederholungen aus
- Zustellungen haben ein Timeout nach 30 Sekunden
<Callout type="info">
Webhook-Zustellungen werden asynchron verarbeitet und beeinträchtigen nicht die Leistung der Workflow-Ausführung.
</Callout>
## Best Practices
1. **Polling-Strategie**: Verwende bei der Abfrage von Logs eine cursor-basierte Paginierung mit `order=asc` und `startDate`, um neue Logs effizient abzurufen.
2. **Webhook-Sicherheit**: Konfiguriere immer ein Webhook-Secret und überprüfe Signaturen, um sicherzustellen, dass Anfragen von Sim stammen.
3. **Idempotenz**: Verwende den `Idempotency-Key`Header, um doppelte Webhook-Zustellungen zu erkennen und zu behandeln.
4. **Datenschutz**: Standardmäßig werden `finalOutput` und `traceSpans` aus den Antworten ausgeschlossen. Aktiviere diese nur, wenn du die Daten benötigst und die Datenschutzauswirkungen verstehst.
5. **Rate-Limiting**: Implementiere exponentielles Backoff, wenn du 429-Antworten erhältst. Überprüfe den `Retry-After`Header für die empfohlene Wartezeit.
## Rate-Limiting
Die API verwendet einen **Token-Bucket-Algorithmus** für die Ratenbegrenzung, der eine faire Nutzung ermöglicht und gleichzeitig Burst-Traffic zulässt:
| Plan | Anfragen/Minute | Burst-Kapazität |
|------|-----------------|----------------|
| Free | 10 | 20 |
| Pro | 30 | 60 |
| Team | 60 | 120 |
| Enterprise | 120 | 240 |
**Wie es funktioniert:**
- Tokens werden mit der Rate `requestsPerMinute` aufgefüllt
- Du kannst im Leerlauf bis zu `maxBurst` Tokens ansammeln
- Jede Anfrage verbraucht 1 Token
- Die Burst-Kapazität ermöglicht die Bewältigung von Verkehrsspitzen
Informationen zur Ratenbegrenzung sind in den Antwort-Headern enthalten:
- `X-RateLimit-Limit`: Anfragen pro Minute (Auffüllrate)
- `X-RateLimit-Remaining`: Aktuell verfügbare Tokens
- `X-RateLimit-Reset`: ISO-Zeitstempel, wann Tokens als nächstes aufgefüllt werden
## Beispiel: Abfragen nach neuen Logs
```javascript
let cursor = null;
const workspaceId = 'YOUR_WORKSPACE_ID';
const startDate = new Date().toISOString();
async function pollLogs() {
const params = new URLSearchParams({
workspaceId,
startDate,
order: 'asc',
limit: '100'
});
if (cursor) {
params.append('cursor', cursor);
}
const response = await fetch(
`https://sim.ai/api/v1/logs?${params}`,
{
headers: {
'x-api-key': 'YOUR_API_KEY'
}
}
);
if (response.ok) {
const data = await response.json();
// Process new logs
for (const log of data.data) {
console.log(`New execution: ${log.executionId}`);
}
// Update cursor for next poll
if (data.nextCursor) {
cursor = data.nextCursor;
}
}
}
// Poll every 30 seconds
setInterval(pollLogs, 30000);
```
## Beispiel: Verarbeitung von Webhooks
```javascript
import express from 'express';
import crypto from 'crypto';
const app = express();
app.use(express.json());
app.post('/sim-webhook', (req, res) => {
// Verify signature
const signature = req.headers['sim-signature'];
const body = JSON.stringify(req.body);
if (!verifyWebhookSignature(body, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
// Check timestamp to prevent replay attacks
const timestamp = parseInt(req.headers['sim-timestamp']);
const fiveMinutesAgo = Date.now() - (5 * 60 * 1000);
if (timestamp < fiveMinutesAgo) {
return res.status(401).send('Timestamp too old');
}
// Process the webhook
const event = req.body;
switch (event.type) {
case 'workflow.execution.completed':
const { workflowId, executionId, status, cost } = event.data;
if (status === 'error') {
console.error(`Workflow ${workflowId} failed: ${executionId}`);
// Handle error...
} else {
console.log(`Workflow ${workflowId} completed: ${executionId}`);
console.log(`Cost: $${cost.total}`);
// Process successful execution...
}
break;
}
// Return 200 to acknowledge receipt
res.status(200).send('OK');
});
app.listen(3000, () => {
console.log('Webhook server listening on port 3000');
});
```
@@ -0,0 +1,107 @@
---
title: Grundlagen
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Card, Cards } from 'fumadocs-ui/components/card'
import { Image } from '@/components/ui/image'
Das Verständnis der Workflow-Ausführung in Sim ist entscheidend für die Erstellung effizienter und zuverlässiger Automatisierungen. Die Ausführungs-Engine verwaltet automatisch Abhängigkeiten, Parallelität und Datenfluss, um sicherzustellen, dass Ihre Workflows reibungslos und vorhersehbar ablaufen.
## Wie Workflows ausgeführt werden
Die Ausführungs-Engine von Sim verarbeitet Workflows intelligent, indem sie Abhängigkeiten analysiert und Blöcke in der effizientesten Reihenfolge ausführt.
### Parallele Ausführung als Standard
Mehrere Blöcke werden gleichzeitig ausgeführt, wenn sie nicht voneinander abhängig sind. Diese parallele Ausführung verbessert die Leistung erheblich, ohne dass eine manuelle Konfiguration erforderlich ist.
<Image
src="/static/execution/concurrency.png"
alt="Mehrere Blöcke, die nach dem Start-Block parallel ausgeführt werden"
width={800}
height={500}
/>
In diesem Beispiel werden sowohl der Kundensupport- als auch der Deep-Researcher-Agentenblock gleichzeitig nach dem Start-Block ausgeführt, was die Effizienz maximiert.
### Automatische Ausgabekombination
Wenn Blöcke mehrere Abhängigkeiten haben, wartet die Ausführungs-Engine automatisch auf den Abschluss aller Abhängigkeiten und stellt dann ihre kombinierten Ausgaben dem nächsten Block zur Verfügung. Keine manuelle Kombination erforderlich.
<Image
src="/static/execution/combination.png"
alt="Funktionsblock, der automatisch Ausgaben von mehreren vorherigen Blöcken empfängt"
width={800}
height={500}
/>
Der Funktionsblock erhält Ausgaben von beiden Agentenblöcken, sobald diese abgeschlossen sind, sodass Sie die kombinierten Ergebnisse verarbeiten können.
### Intelligentes Routing
Workflows können sich in mehrere Richtungen verzweigen, indem sie Routing-Blöcke verwenden. Die Ausführungs-Engine unterstützt sowohl deterministisches Routing (mit Bedingungsblöcken) als auch KI-gesteuertes Routing (mit Router-Blöcken).
<Image
src="/static/execution/routing.png"
alt="Workflow, der sowohl bedingte als auch router-basierte Verzweigungen zeigt"
width={800}
height={500}
/>
Dieser Workflow zeigt, wie die Ausführung unterschiedlichen Pfaden basierend auf Bedingungen oder KI-Entscheidungen folgen kann, wobei jeder Pfad unabhängig ausgeführt wird.
## Blocktypen
Sim bietet verschiedene Arten von Blöcken, die spezifische Zwecke in Ihren Workflows erfüllen:
<Cards>
<Card title="Auslöser" href="/triggers">
**Starter-Blöcke** initiieren Workflows und **Webhook-Blöcke** reagieren auf externe Ereignisse. Jeder Workflow benötigt einen Auslöser, um die Ausführung zu beginnen.
</Card>
<Card title="Verarbeitungsblöcke" href="/blocks">
**Agent-Blöcke** interagieren mit KI-Modellen, **Funktionsblöcke** führen benutzerdefinierten Code aus und **API-Blöcke** verbinden sich mit externen Diensten. Diese Blöcke transformieren und verarbeiten Ihre Daten.
</Card>
<Card title="Kontrollfluss" href="/blocks">
**Router-Blöcke** nutzen KI, um Pfade zu wählen, **Bedingungsblöcke** verzweigen basierend auf Logik und **Schleifen-/Parallelblöcke** handhaben Iterationen und Nebenläufigkeit.
</Card>
<Card title="Ausgabe & Antwort" href="/blocks">
**Antwortblöcke** formatieren endgültige Ausgaben für APIs und Chat-Schnittstellen und liefern strukturierte Ergebnisse aus Ihren Workflows.
</Card>
</Cards>
Alle Blöcke werden automatisch basierend auf ihren Abhängigkeiten ausgeführt - Sie müssen die Ausführungsreihenfolge oder das Timing nicht manuell verwalten.
## Ausführungsüberwachung
Wenn Workflows ausgeführt werden, bietet Sim Echtzeit-Einblick in den Ausführungsprozess:
- **Live-Block-Status**: Sehen Sie, welche Blöcke gerade ausgeführt werden, abgeschlossen sind oder fehlgeschlagen sind
- **Ausführungsprotokolle**: Detaillierte Protokolle erscheinen in Echtzeit und zeigen Eingaben, Ausgaben und eventuelle Fehler
- **Leistungskennzahlen**: Verfolgen Sie die Ausführungszeit und Kosten für jeden Block
- **Pfadvisualisierung**: Verstehen Sie, welche Ausführungspfade durch Ihren Workflow genommen wurden
<Callout type="info">
Alle Ausführungsdetails werden erfasst und sind auch nach Abschluss der Workflows zur Überprüfung verfügbar, was bei der Fehlerbehebung und Optimierung hilft.
</Callout>
## Wichtige Ausführungsprinzipien
Das Verständnis dieser Grundprinzipien wird Ihnen helfen, bessere Workflows zu erstellen:
1. **Abhängigkeitsbasierte Ausführung**: Blöcke werden nur ausgeführt, wenn alle ihre Abhängigkeiten abgeschlossen sind
2. **Automatische Parallelisierung**: Unabhängige Blöcke laufen ohne Konfiguration gleichzeitig
3. **Intelligenter Datenfluss**: Ausgaben fließen automatisch zu verbundenen Blöcken
4. **Fehlerbehandlung**: Fehlgeschlagene Blöcke stoppen ihren Ausführungspfad, beeinflussen aber keine unabhängigen Pfade
5. **Zustandspersistenz**: Alle Blockausgaben und Ausführungsdetails werden für die Fehlerbehebung gespeichert
## Nächste Schritte
Nachdem Sie die Grundlagen der Ausführung verstanden haben, erkunden Sie:
- **[Blocktypen](/blocks)** - Erfahren Sie mehr über spezifische Block-Funktionen
- **[Protokollierung](/execution/logging)** - Überwachen Sie Workflow-Ausführungen und beheben Sie Probleme
- **[Kostenberechnung](/execution/costs)** - Verstehen und optimieren Sie Workflow-Kosten
- **[Trigger](/triggers)** - Richten Sie verschiedene Möglichkeiten ein, Ihre Workflows auszuführen
@@ -0,0 +1,384 @@
---
title: Kostenberechnung
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
import { Image } from '@/components/ui/image'
Sim berechnet automatisch die Kosten für alle Workflow-Ausführungen und bietet transparente Preise basierend auf der Nutzung von KI-Modellen und Ausführungsgebühren. Das Verständnis dieser Kosten hilft Ihnen, Workflows zu optimieren und Ihr Budget effektiv zu verwalten.
## Wie Kosten berechnet werden
Jede Workflow-Ausführung umfasst zwei Kostenkomponenten:
**Basis-Ausführungsgebühr**: 0,001 $ pro Ausführung
**KI-Modellnutzung**: Variable Kosten basierend auf dem Token-Verbrauch
```javascript
modelCost = (inputTokens × inputPrice + outputTokens × outputPrice) / 1,000,000
totalCost = baseExecutionCharge + modelCost
```
<Callout type="info">
KI-Modellpreise werden pro Million Token berechnet. Die Berechnung teilt durch 1.000.000, um die tatsächlichen Kosten zu ermitteln. Workflows ohne KI-Blöcke verursachen nur die Basis-Ausführungsgebühr.
</Callout>
## Modellaufschlüsselung in Logs
Für Workflows mit KI-Blöcken können Sie detaillierte Kosteninformationen in den Logs einsehen:
<div className="flex justify-center">
<Image
src="/static/logs/logs-cost.png"
alt="Modellaufschlüsselung"
width={600}
height={400}
className="my-6"
/>
</div>
Die Modellaufschlüsselung zeigt:
- **Token-Nutzung**: Eingabe- und Ausgabe-Token-Anzahl für jedes Modell
- **Kostenaufschlüsselung**: Einzelkosten pro Modell und Operation
- **Modellverteilung**: Welche Modelle verwendet wurden und wie oft
- **Gesamtkosten**: Gesamtkosten für die gesamte Workflow-Ausführung
## Preisoptionen
<Tabs items={['Hosted Models', 'Bring Your Own API Key']}>
<Tab>
**Hosted Models** - Sim bietet API-Schlüssel mit einem 1,4-fachen Preismultiplikator für Agent-Blöcke:
**OpenAI**
| Modell | Basispreis (Eingabe/Ausgabe) | Hosted-Preis (Eingabe/Ausgabe) |
|-------|---------------------------|----------------------------|
| GPT-5.1 | $1.25 / $10.00 | $1.75 / $14.00 |
| GPT-5 | $1.25 / $10.00 | $1.75 / $14.00 |
| GPT-5 Mini | $0.25 / $2.00 | $0.35 / $2.80 |
| GPT-5 Nano | $0.05 / $0.40 | $0.07 / $0.56 |
| GPT-4o | $2.50 / $10.00 | $3.50 / $14.00 |
| GPT-4.1 | $2.00 / $8.00 | $2.80 / $11.20 |
| GPT-4.1 Mini | $0.40 / $1.60 | $0.56 / $2.24 |
| GPT-4.1 Nano | $0.10 / $0.40 | $0.14 / $0.56 |
| o1 | $15.00 / $60.00 | $21.00 / $84.00 |
| o3 | $2.00 / $8.00 | $2.80 / $11.20 |
| o4 Mini | $1.10 / $4.40 | $1.54 / $6.16 |
**Anthropic**
| Modell | Basispreis (Eingabe/Ausgabe) | Hosted-Preis (Eingabe/Ausgabe) |
|-------|---------------------------|----------------------------|
| Claude Opus 4.5 | $5.00 / $25.00 | $7.00 / $35.00 |
| Claude Opus 4.1 | $15.00 / $75.00 | $21.00 / $105.00 |
| Claude Sonnet 4.5 | $3.00 / $15.00 | $4.20 / $21.00 |
| Claude Sonnet 4.0 | $3.00 / $15.00 | $4.20 / $21.00 |
| Claude Haiku 4.5 | $1.00 / $5.00 | $1.40 / $7.00 |
**Google**
| Modell | Basispreis (Eingabe/Ausgabe) | Hosted-Preis (Eingabe/Ausgabe) |
|-------|---------------------------|----------------------------|
| Gemini 3 Pro Preview | $2.00 / $12.00 | $2.80 / $16.80 |
| Gemini 2.5 Pro | $1.25 / $10.00 | $1.75 / $14.00 |
| Gemini 2.5 Flash | $0.30 / $2.50 | $0.42 / $3.50 |
*Der 1,4-fache Multiplikator deckt Infrastruktur- und API-Verwaltungskosten ab.*
</Tab>
<Tab>
**Eigene API-Schlüssel** - Nutzen Sie jedes Modell zum Basispreis:
| Anbieter | Beispielmodelle | Input / Output |
|----------|----------------|----------------|
| Deepseek | V3, R1 | $0,75 / $1,00 |
| xAI | Grok 4 Latest, Grok 3 | $3,00 / $15,00 |
| Groq | Llama 4 Scout, Llama 3.3 70B | $0,11 / $0,34 |
| Cerebras | Llama 4 Scout, Llama 3.3 70B | $0,11 / $0,34 |
| Ollama | Lokale Modelle | Kostenlos |
| VLLM | Lokale Modelle | Kostenlos |
*Bezahlen Sie Anbieter direkt ohne Aufschlag*
</Tab>
</Tabs>
<Callout type="warning">
Die angezeigten Preise entsprechen den Tarifen vom 10. September 2025. Überprüfen Sie die Dokumentation der Anbieter für aktuelle Preise.
</Callout>
## Gehostete Tool-Preise
Wenn Workflows Tool-Blöcke mit den gehosteten API-Schlüsseln von Sim verwenden, werden die Kosten pro Operation berechnet. Verwenden Sie Ihre eigenen Schlüssel über BYOK, um direkt an die Anbieter zu zahlen.
<Tabs items={['Firecrawl', 'Exa', 'Serper', 'Perplexity', 'Linkup', 'Parallel AI', 'Jina AI', 'Google Cloud', 'Brandfetch']}>
<Tab>
**Firecrawl** - Web-Scraping, Crawling, Suche und Extraktion
| Operation | Cost |
|-----------|------|
| Scrape | $0.001 per credit used |
| Crawl | $0.001 per credit used |
| Search | $0.001 per credit used |
| Extract | $0.001 per credit used |
| Map | $0.001 per credit used |
</Tab>
<Tab>
**Exa** - KI-gestützte Suche und Recherche
| Operation | Cost |
|-----------|------|
| Search | Dynamic (returned by API) |
| Get Contents | Dynamic (returned by API) |
| Find Similar Links | Dynamic (returned by API) |
| Answer | Dynamic (returned by API) |
</Tab>
<Tab>
**Serper** - Google-Such-API
| Operation | Cost |
|-----------|------|
| Search (≤10 results) | $0.001 |
| Search (>10 results) | $0.002 |
</Tab>
<Tab>
**Perplexity** - KI-gestützter Chat und Websuche
| Operation | Cost |
|-----------|------|
| Search | $0.005 per request |
| Chat | Token-based (varies by model) |
</Tab>
<Tab>
**Linkup** - Websuche und Inhaltsabruf
| Operation | Cost |
|-----------|------|
| Standard search | ~$0.006 |
| Deep search | ~$0.055 |
</Tab>
<Tab>
**Parallel AI** - Websuche, Extraktion und tiefgehende Recherche
| Operation | Cost |
|-----------|------|
| Search (≤10 results) | $0.005 |
| Search (>10 results) | $0.005 + $0.001 per additional result |
| Extract | $0.001 per URL |
| Deep Research | $0.005$2.40 (varies by processor tier) |
</Tab>
<Tab>
**Jina AI** - Web-Lesen und Suche
| Operation | Cost |
|-----------|------|
| Read URL | $0.20 per 1M tokens |
| Search | $0.20 per 1M tokens (minimum 10K tokens) |
</Tab>
<Tab>
**Google Cloud** - Translate, Maps, PageSpeed und Books APIs
| Operation | Cost |
|-----------|------|
| Translate / Detect | $0.00002 per character |
| Maps (Geocode, Directions, Distance Matrix, Elevation, Timezone, Reverse Geocode, Geolocate, Validate Address) | $0.005 per request |
| Maps (Snap to Roads) | $0.01 per request |
| Maps (Place Details) | $0.017 per request |
| Maps (Places Search) | $0.032 per request |
| PageSpeed | Free |
| Books (Search, Details) | Free |
</Tab>
<Tab>
**Brandfetch** - Marken-Assets, Logos, Farben und Unternehmensinformationen
| Operation | Cost |
|-----------|------|
| Search | Free |
| Get Brand | $0.04 per request |
</Tab>
</Tabs>
## Bring Your Own Key (BYOK)
Sie können Ihre eigenen API-Schlüssel für unterstützte Anbieter (OpenAI, Anthropic, Google, Mistral, Fireworks, Firecrawl, Exa, Serper, Linkup, Parallel AI, Perplexity, Jina AI, Google Cloud, Brandfetch) unter **Einstellungen → BYOK** verwenden, um Basispreise zu zahlen. Schlüssel werden verschlüsselt und gelten arbeitsbereichsweit.
## Strategien zur Kostenoptimierung
- **Modellauswahl**: Wählen Sie Modelle basierend auf der Aufgabenkomplexität. Einfache Aufgaben können GPT-4.1-nano verwenden, während komplexes Reasoning o1 oder Claude Opus erfordern könnte.
- **Prompt Engineering**: Gut strukturierte, prägnante Prompts reduzieren den Token-Verbrauch ohne Qualitätsverlust.
- **Lokale Modelle**: Verwenden Sie Ollama oder VLLM für unkritische Aufgaben, um API-Kosten vollständig zu eliminieren.
- **Caching und Wiederverwendung**: Speichern Sie häufig verwendete Ergebnisse in Variablen oder Dateien, um wiederholte AI-Modellaufrufe zu vermeiden.
- **Batch-Verarbeitung**: Verarbeiten Sie mehrere Elemente in einer einzigen AI-Anfrage, anstatt einzelne Aufrufe zu tätigen.
## Nutzungsüberwachung
Überwachen Sie Ihre Nutzung und Abrechnung unter Einstellungen → Abonnement:
- **Aktuelle Nutzung**: Echtzeit-Nutzung und Kosten für den aktuellen Zeitraum
- **Nutzungslimits**: Plan-Limits mit visuellen Fortschrittsindikatoren
- **Abrechnungsdetails**: Prognostizierte Gebühren und Mindestverpflichtungen
- **Plan-Verwaltung**: Upgrade-Optionen und Abrechnungsverlauf
### Programmatisches Nutzungs-Tracking
Sie können Ihre aktuelle Nutzung und Limits programmatisch über die API abfragen:
**Endpoint:**
```text
GET /api/users/me/usage-limits
```
**Authentifizierung:**
- Fügen Sie Ihren API-Schlüssel im `X-API-Key` Header hinzu
**Beispielanfrage:**
```bash
curl -X GET -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" https://sim.ai/api/users/me/usage-limits
```
**Beispielantwort:**
```json
{
"success": true,
"rateLimit": {
"sync": {
"isLimited": false,
"requestsPerMinute": 25,
"maxBurst": 50,
"remaining": 50,
"resetAt": "2025-09-08T22:51:55.999Z"
},
"async": {
"isLimited": false,
"requestsPerMinute": 200,
"maxBurst": 400,
"remaining": 400,
"resetAt": "2025-09-08T22:51:56.155Z"
},
"authType": "api"
},
"usage": {
"currentPeriodCost": 12.34,
"limit": 100,
"plan": "pro"
}
}
```
**Rate-Limit-Felder:**
- `requestsPerMinute`: Dauerhaftes Rate-Limit (Tokens werden mit dieser Rate aufgefüllt)
- `maxBurst`: Maximale Tokens, die Sie akkumulieren können (Burst-Kapazität)
- `remaining`: Aktuell verfügbare Tokens (kann bis zu `maxBurst` betragen)
**Antwortfelder:**
- `currentPeriodCost` spiegelt die Nutzung im aktuellen Abrechnungszeitraum wider
- `limit` wird aus individuellen Limits (Free/Pro) oder gepoolten Organisationslimits (Team/Enterprise) abgeleitet
- `plan` ist der Plan mit der höchsten Priorität, der Ihrem Benutzer zugeordnet ist
## Plan-Limits
Verschiedene Abonnement-Pläne haben unterschiedliche Nutzungslimits:
| Plan | Monatliches Nutzungslimit | Ratenlimits (pro Minute) |
|------|-------------------|-------------------------|
| **Free** | 20 $ | 5 sync, 10 async |
| **Pro** | 100 $ | 10 sync, 50 async |
| **Team** | 500 $ (gemeinsam) | 50 sync, 100 async |
| **Enterprise** | Individuell | Individuell |
## Abrechnungsmodell
Sim verwendet ein **Basis-Abonnement + Mehrverbrauch**-Abrechnungsmodell:
### So funktioniert es
**Pro-Plan (20 $/Monat):**
- Monatsabonnement beinhaltet 20 $ Nutzung
- Nutzung unter 20 $ → Keine zusätzlichen Gebühren
- Nutzung über 20 $ → Mehrverbrauch am Monatsende zahlen
- Beispiel: 35 $ Nutzung = 20 $ (Abonnement) + 15 $ (Mehrverbrauch)
**Team-Plan (40 $/Platz/Monat):**
- Gemeinsame Nutzung über alle Teammitglieder
- Mehrverbrauch wird aus der gesamten Team-Nutzung berechnet
- Organisationsinhaber erhält eine Rechnung
**Enterprise-Pläne:**
- Fester Monatspreis, kein Mehrverbrauch
- Individuelle Nutzungslimits gemäß Vereinbarung
### Schwellenwert-Abrechnung
Wenn der nicht abgerechnete Mehrverbrauch 50 $ erreicht, rechnet Sim automatisch den gesamten nicht abgerechneten Betrag ab.
**Beispiel:**
- Tag 10: 70 $ Mehrverbrauch → 70 $ sofort abrechnen
- Tag 15: Zusätzliche 35 $ Nutzung (105 $ gesamt) → Bereits abgerechnet, keine Aktion
- Tag 20: Weitere 50 $ Nutzung (155 $ gesamt, 85 $ nicht abgerechnet) → 85 $ sofort abrechnen
Dies verteilt große Mehrverbrauchsgebühren über den Monat, anstatt einer großen Rechnung am Periodenende.
## Best Practices für Kostenmanagement
1. **Regelmäßig überwachen**: Überprüfen Sie Ihr Nutzungs-Dashboard häufig, um Überraschungen zu vermeiden
2. **Budgets festlegen**: Nutzen Sie Plan-Limits als Leitplanken für Ihre Ausgaben
3. **Workflows optimieren**: Überprüfen Sie kostenintensive Ausführungen und optimieren Sie Prompts oder Modellauswahl
4. **Passende Modelle verwenden**: Passen Sie die Modellkomplexität an die Aufgabenanforderungen an
5. **Ähnliche Aufgaben bündeln**: Kombinieren Sie mehrere Anfragen, wenn möglich, um Overhead zu reduzieren
## Nächste Schritte
- Überprüfen Sie Ihre aktuelle Nutzung unter [Einstellungen → Abonnement](https://sim.ai/settings/subscription)
- Erfahren Sie mehr über [Protokollierung](/execution/logging), um Ausführungsdetails zu verfolgen
- Entdecken Sie die [externe API](/execution/api) für programmatische Kostenüberwachung
- Sehen Sie sich [Workflow-Optimierungstechniken](/blocks) an, um Kosten zu reduzieren
**Pro-Tarif (20 $/Monat):**
- Monatliches Abonnement beinhaltet 20 $ Nutzung
- Nutzung unter 20 $ → Keine zusätzlichen Gebühren
- Nutzung über 20 $ → Mehrverbrauch wird am Monatsende abgerechnet
- Beispiel: 35 $ Nutzung = 20 $ (Abonnement) + 15 $ (Mehrverbrauch)
**Team-Tarif (40 $/Platz/Monat):**
- Gemeinsame Nutzung über alle Teammitglieder hinweg
- Mehrverbrauch wird aus der gesamten Teamnutzung berechnet
- Der Organisationsinhaber erhält eine Rechnung
**Enterprise-Tarife:**
- Fester Monatspreis, keine Mehrverbräuche
- Individuelle Nutzungslimits gemäß Vereinbarung
### Schwellenwertabrechnung
Wenn der nicht abgerechnete Mehrverbrauch 50 $ erreicht, rechnet Sim automatisch den gesamten nicht abgerechneten Betrag ab.
**Beispiel:**
- Tag 10: 70 $ Mehrverbrauch → 70 $ sofort abrechnen
- Tag 15: Weitere 35 $ Nutzung (105 $ gesamt) → Bereits abgerechnet, keine Aktion
- Tag 20: Weitere 50 $ Nutzung (155 $ gesamt, 85 $ nicht abgerechnet) → 85 $ sofort abrechnen
Dies verteilt hohe Mehrverbrauchsgebühren über den Monat hinweg, anstatt einer großen Rechnung am Periodenende.
## Best Practices für das Kostenmanagement
1. **Regelmäßig überwachen**: Überprüfen Sie Ihr Nutzungs-Dashboard häufig, um Überraschungen zu vermeiden
2. **Budgets festlegen**: Nutzen Sie Tariflimits als Leitplanken für Ihre Ausgaben
3. **Workflows optimieren**: Überprüfen Sie kostenintensive Ausführungen und optimieren Sie Prompts oder Modellauswahl
4. **Passende Modelle verwenden**: Stimmen Sie die Modellkomplexität auf die Aufgabenanforderungen ab
5. **Ähnliche Aufgaben bündeln**: Kombinieren Sie mehrere Anfragen, wenn möglich, um den Overhead zu reduzieren
## Nächste Schritte
- Überprüfen Sie Ihre aktuelle Nutzung unter [Einstellungen → Abonnement](https://sim.ai/settings/subscription)
- Erfahren Sie mehr über [Protokollierung](/execution/logging), um Ausführungsdetails zu verfolgen
- Erkunden Sie die [externe API](/execution/api) für programmatische Kostenüberwachung
- Informieren Sie sich über [Workflow-Optimierungstechniken](/blocks), um Kosten zu reduzieren
@@ -0,0 +1,172 @@
---
title: Dateien übergeben
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
Sim macht es einfach, mit Dateien in Ihren Workflows zu arbeiten. Blöcke können Dateien empfangen, verarbeiten und nahtlos an andere Blöcke weitergeben.
## Dateiobjekte
Wenn Blöcke Dateien ausgeben (wie Gmail-Anhänge, generierte Bilder oder geparste Dokumente), geben sie ein standardisiertes Dateiobjekt zurück:
```json
{
"name": "report.pdf",
"url": "https://...",
"base64": "JVBERi0xLjQK...",
"type": "application/pdf",
"size": 245678
}
```
Sie können auf alle diese Eigenschaften zugreifen, wenn Sie auf Dateien aus vorherigen Blöcken verweisen.
## Der Datei-Block
Der **Datei-Block** ist der universelle Einstiegspunkt für Dateien in Ihren Workflows. Er akzeptiert Dateien aus jeder Quelle und gibt standardisierte Dateiobjekte aus, die mit allen Integrationen funktionieren.
**Eingaben:**
- **Hochgeladene Dateien** - Dateien direkt per Drag & Drop oder Auswahl hinzufügen
- **Externe URLs** - Jede öffentlich zugängliche Datei-URL
- **Dateien von anderen Blöcken** - Dateien von Gmail-Anhängen, Slack-Downloads usw. übergeben
**Ausgaben:**
- Eine Liste von `UserFile`-Objekten mit konsistenter Struktur (`name`, `url`, `base64`, `type`, `size`)
- `combinedContent` - Extrahierter Textinhalt aus allen Dateien (für Dokumente)
**Beispielverwendung:**
```
// Get all files from the File block
<file.files>
// Get the first file
<file.files[0]>
// Get combined text content from parsed documents
<file.combinedContent>
```
Der Datei-Block führt automatisch folgende Aktionen aus:
- Erkennt Dateitypen aus URLs und Erweiterungen
- Extrahiert Text aus PDFs, CSVs und Dokumenten
- Generiert Base64-Kodierung für Binärdateien
- Erstellt vorsignierte URLs für sicheren Zugriff
Verwenden Sie den Datei-Block, wenn Sie Dateien aus verschiedenen Quellen normalisieren müssen, bevor Sie sie an andere Blöcke wie Vision, STT oder E-Mail-Integrationen übergeben.
## Dateien zwischen Blöcken übergeben
Verweisen Sie auf Dateien aus vorherigen Blöcken über das Tag-Dropdown. Klicken Sie in ein beliebiges Dateieingabefeld und geben Sie `<` ein, um verfügbare Ausgaben anzuzeigen.
**Häufige Muster:**
```
// Single file from a block
<gmail.attachments[0]>
// Pass the whole file object
<file_parser.files[0]>
// Access specific properties
<gmail.attachments[0].name>
<gmail.attachments[0].base64>
```
Die meisten Blöcke akzeptieren das vollständige Dateiobjekt und extrahieren automatisch, was sie benötigen. Sie müssen `base64` oder `url` in den meisten Fällen nicht manuell extrahieren.
## Workflows mit Dateien auslösen
Wenn Sie einen Workflow über die API aufrufen, der Dateieingaben erwartet, fügen Sie Dateien in Ihre Anfrage ein:
<Tabs items={['Base64', 'URL']}>
<Tab value="Base64">
```bash
curl -X POST "https://sim.ai/api/workflows/YOUR_WORKFLOW_ID/execute" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"document": {
"name": "report.pdf",
"base64": "JVBERi0xLjQK...",
"type": "application/pdf"
}
}'
```
</Tab>
<Tab value="URL">
```bash
curl -X POST "https://sim.ai/api/workflows/YOUR_WORKFLOW_ID/execute" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"document": {
"name": "report.pdf",
"url": "https://example.com/report.pdf",
"type": "application/pdf"
}
}'
```
</Tab>
</Tabs>
Der Start-Block des Workflows sollte ein Eingabefeld haben, das für den Empfang des Dateiparameters konfiguriert ist.
## Dateien in API-Antworten empfangen
Wenn ein Workflow Dateien ausgibt, sind diese in der Antwort enthalten:
```json
{
"success": true,
"output": {
"generatedFile": {
"name": "output.png",
"url": "https://...",
"base64": "iVBORw0KGgo...",
"type": "image/png",
"size": 34567
}
}
}
```
Verwenden Sie `url` für direkte Downloads oder `base64` für Inline-Verarbeitung.
## Blöcke, die mit Dateien arbeiten
**Dateieingaben:**
- **File** - Dokumente, Bilder und Textdateien parsen
- **Vision** - Bilder mit KI-Modellen analysieren
- **Mistral Parser** - Text aus PDFs extrahieren
**Dateiausgaben:**
- **Gmail** - E-Mail-Anhänge
- **Slack** - Heruntergeladene Dateien
- **TTS** - Generierte Audiodateien
- **Video Generator** - Generierte Videos
- **Image Generator** - Generierte Bilder
**Dateispeicherung:**
- **Supabase** - Upload/Download aus dem Speicher
- **S3** - AWS S3-Operationen
- **Google Drive** - Drive-Dateioperationen
- **Dropbox** - Dropbox-Dateioperationen
<Callout type="info">
Dateien sind automatisch für nachgelagerte Blöcke verfügbar. Die Ausführungs-Engine übernimmt die gesamte Dateiübertragung und Formatkonvertierung.
</Callout>
## Best Practices
1. **Dateiobjekte direkt verwenden** - Übergeben Sie das vollständige Dateiobjekt, anstatt einzelne Eigenschaften zu extrahieren. Blöcke übernehmen die Konvertierung automatisch.
2. **Dateitypen prüfen** - Stellen Sie sicher, dass der Dateityp mit dem übereinstimmt, was der empfangende Block erwartet. Der Vision-Block benötigt Bilder, der File-Block verarbeitet Dokumente.
3. **Dateigröße beachten** Große Dateien erhöhen die Ausführungszeit. Bei sehr großen Dateien sollten Sie Storage-Blöcke (S3, Supabase) für die Zwischenspeicherung verwenden.
@@ -0,0 +1,115 @@
---
title: Übersicht
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Card, Cards } from 'fumadocs-ui/components/card'
import { Image } from '@/components/ui/image'
Die Ausführungs-Engine von Sim bringt Ihre Workflows zum Leben, indem sie Blöcke in der richtigen Reihenfolge verarbeitet, den Datenfluss verwaltet und Fehler elegant behandelt, sodass Sie genau verstehen können, wie Workflows in Sim ausgeführt werden.
<Callout type="info">
Jede Workflow-Ausführung folgt einem deterministischen Pfad, der auf Ihren Blockverbindungen und Ihrer Logik basiert, um vorhersehbare und zuverlässige Ergebnisse zu gewährleisten.
</Callout>
## Dokumentationsübersicht
<Cards>
<Card title="Grundlagen der Ausführung" href="/execution/basics">
Erfahren Sie mehr über den grundlegenden Ausführungsablauf, Blocktypen und wie Daten durch Ihren
Workflow fließen
</Card>
<Card title="Protokollierung" href="/execution/logging">
Überwachen Sie Workflow-Ausführungen mit umfassender Protokollierung und Echtzeit-Sichtbarkeit
</Card>
<Card title="Kostenberechnung" href="/execution/costs">
Verstehen Sie, wie die Kosten für Workflow-Ausführungen berechnet und optimiert werden
</Card>
<Card title="Externe API" href="/execution/api">
Greifen Sie programmgesteuert über REST-API auf Ausführungsprotokolle zu und richten Sie Webhooks ein
</Card>
</Cards>
## Schlüsselkonzepte
### Topologische Ausführung
Blöcke werden in Abhängigkeitsreihenfolge ausgeführt, ähnlich wie eine Tabellenkalkulation Zellen neu berechnet. Die Ausführungs-Engine bestimmt automatisch, welche Blöcke basierend auf abgeschlossenen Abhängigkeiten ausgeführt werden können.
### Pfadverfolgung
Die Engine verfolgt aktiv Ausführungspfade durch Ihren Workflow. Router- und Bedingungsblöcke aktualisieren diese Pfade dynamisch und stellen sicher, dass nur relevante Blöcke ausgeführt werden.
### Schichtbasierte Verarbeitung
Anstatt Blöcke einzeln auszuführen, identifiziert die Engine Schichten von Blöcken, die parallel ausgeführt werden können, und optimiert so die Leistung für komplexe Workflows.
### Ausführungskontext
Jeder Workflow behält während der Ausführung einen umfangreichen Kontext bei, der Folgendes enthält:
- Block-Ausgaben und -Zustände
- Aktive Ausführungspfade
- Verfolgung von Schleifen- und Paralleliterationen
- Umgebungsvariablen
- Routing-Entscheidungen
## Deployment-Snapshots
Alle öffentlichen Einstiegspunkte API, Chat, Zeitplan, Webhook und manuelle Ausführungen führen den aktiven Deployment-Snapshot des Workflows aus. Veröffentliche ein neues Deployment, wann immer du die Arbeitsfläche änderst, damit jeder Auslöser die aktualisierte Version verwendet.
<div className='flex justify-center my-6'>
<Image
src='/static/execution/deployment-versions.png'
alt='Tabelle mit Deployment-Versionen'
width={500}
height={280}
className='rounded-xl border border-border shadow-sm'
/>
</div>
Das Deploy-Modal behält eine vollständige Versionshistorie bei untersuche jeden Snapshot, vergleiche ihn mit deinem Entwurf und führe Upgrades oder Rollbacks mit einem Klick durch, wenn du eine frühere Version wiederherstellen musst.
## Programmatische Ausführung
Führe Workflows aus deinen Anwendungen heraus mit unseren offiziellen SDKs aus:
```bash
# TypeScript/JavaScript
npm install simstudio-ts-sdk
# Python
pip install simstudio-sdk
```
```typescript
// TypeScript Example
import { SimStudioClient } from 'simstudio-ts-sdk';
const client = new SimStudioClient({
apiKey: 'your-api-key'
});
const result = await client.executeWorkflow('workflow-id', {
input: { message: 'Hello' }
});
```
## Best Practices
### Design für Zuverlässigkeit
- Behandle Fehler elegant mit geeigneten Fallback-Pfaden
- Verwende Umgebungsvariablen für sensible Daten
- Füge Logging zu Funktionsblöcken für Debugging hinzu
### Optimiere Performance
- Minimiere externe API-Aufrufe wo möglich
- Nutze parallele Ausführung für unabhängige Operationen
- Cache Ergebnisse mit Memory-Blöcken wenn angemessen
### Überwache Ausführungen
- Überprüfe Logs regelmäßig, um Leistungsmuster zu verstehen
- Verfolge Kosten für KI-Modellnutzung
- Verwende Workflow-Snapshots zur Fehlerbehebung
## Was kommt als nächstes?
Beginne mit [Ausführungsgrundlagen](/execution/basics), um zu verstehen, wie Workflows laufen, und erkunde dann [Logging](/execution/logging), um deine Ausführungen zu überwachen, sowie [Kostenberechnung](/execution/costs), um deine Ausgaben zu optimieren.
@@ -0,0 +1,150 @@
---
title: Protokollierung
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
import { Image } from '@/components/ui/image'
Sim bietet umfassende Protokollierung für alle Workflow-Ausführungen und gibt Ihnen vollständige Transparenz darüber, wie Ihre Workflows laufen, welche Daten durch sie fließen und wo möglicherweise Probleme auftreten.
## Protokollierungssystem
Sim bietet zwei komplementäre Protokollierungsschnittstellen, die verschiedenen Workflows und Anwendungsfällen entsprechen:
### Echtzeit-Konsole
Während der manuellen oder Chat-Workflow-Ausführung erscheinen Protokolle in Echtzeit im Konsolen-Panel auf der rechten Seite des Workflow-Editors:
<div className="flex justify-center">
<Image
src="/static/logs/console.png"
alt="Echtzeit-Konsolen-Panel"
width={400}
height={300}
className="my-6"
/>
</div>
Die Konsole zeigt:
- Fortschritt der Blockausführung mit Hervorhebung des aktiven Blocks
- Echtzeit-Ausgaben nach Abschluss der Blöcke
- Ausführungszeit für jeden Block
- Erfolgs-/Fehlerstatusanzeigen
### Protokollseite
Alle Workflow-Ausführungen ob manuell ausgelöst, über API, Chat, Zeitplan oder Webhook werden auf der dedizierten Protokollseite protokolliert:
<div className="flex justify-center">
<Image
src="/static/logs/logs.png"
alt="Protokollseite"
width={600}
height={400}
className="my-6"
/>
</div>
Die Protokollseite bietet:
- Umfassende Filterung nach Zeitraum, Status, Auslösertyp, Ordner und Workflow
- Suchfunktion über alle Protokolle
- Live-Modus für Echtzeit-Updates
- 7-tägige Protokollaufbewahrung (erweiterbar für längere Aufbewahrung)
## Protokolldetails-Seitenleiste
Durch Klicken auf einen Protokolleintrag öffnet sich eine detaillierte Seitenleistenansicht:
<div className="flex justify-center">
<Image
src="/static/logs/logs-sidebar.png"
alt="Protokoll-Seitenleiste mit Details"
width={600}
height={400}
className="my-6"
/>
</div>
### Block-Eingabe/Ausgabe
Sehen Sie den vollständigen Datenfluss für jeden Block mit Tabs zum Umschalten zwischen:
<Tabs items={['Output', 'Input']}>
<Tab>
**Output-Tab** zeigt das Ausführungsergebnis des Blocks:
- Strukturierte Daten mit JSON-Formatierung
- Markdown-Rendering für KI-generierte Inhalte
- Kopierschaltfläche für einfache Datenextraktion
</Tab>
<Tab>
**Input-Tab** zeigt, was an den Block übergeben wurde:
- Aufgelöste Variablenwerte
- Referenzierte Ausgaben anderer Blöcke
- Verwendete Umgebungsvariablen
- API-Schlüssel werden aus Sicherheitsgründen automatisch unkenntlich gemacht
</Tab>
</Tabs>
### Ausführungszeitlinie
Für Workflow-übergreifende Protokolle, sehen Sie detaillierte Ausführungsmetriken:
- Start- und Endzeitstempel
- Gesamtdauer des Workflows
- Ausführungszeiten einzelner Blöcke
- Identifikation von Leistungsengpässen
## Workflow-Snapshots
Für jede protokollierte Ausführung klicken Sie auf "Snapshot anzeigen", um den exakten Workflow-Zustand zum Ausführungszeitpunkt zu sehen:
<div className="flex justify-center">
<Image
src="/static/logs/logs-frozen-canvas.png"
alt="Workflow-Snapshot"
width={600}
height={400}
className="my-6"
/>
</div>
Der Snapshot bietet:
- Eingefrorene Arbeitsfläche, die die Workflow-Struktur zeigt
- Block-Zustände und Verbindungen, wie sie während der Ausführung waren
- Klicken Sie auf einen beliebigen Block, um dessen Ein- und Ausgaben zu sehen
- Nützlich zum Debuggen von Workflows, die seitdem geändert wurden
<Callout type="info">
Workflow-Snapshots sind nur für Ausführungen verfügbar, die nach der Einführung des erweiterten Protokollierungssystems durchgeführt wurden. Ältere migrierte Protokolle zeigen die Meldung "Protokollierter Zustand nicht gefunden".
</Callout>
## Protokollaufbewahrung
- **Kostenloser Plan**: 7 Tage Protokollaufbewahrung
- **Pro-Plan**: 30 Tage Protokollaufbewahrung
- **Team-Plan**: 90 Tage Protokollaufbewahrung
- **Enterprise-Plan**: Individuelle Aufbewahrungszeiträume verfügbar
## Best Practices
### Für die Entwicklung
- Verwenden Sie die Echtzeit-Konsole für sofortiges Feedback während des Testens
- Überprüfen Sie Block-Ein- und Ausgaben, um den Datenfluss zu verifizieren
- Nutzen Sie Workflow-Snapshots, um funktionierende mit fehlerhaften Versionen zu vergleichen
### Für die Produktion
- Überwachen Sie die Protokollseite regelmäßig auf Fehler oder Leistungsprobleme
- Richten Sie Filter ein, um sich auf bestimmte Workflows oder Zeiträume zu konzentrieren
- Verwenden Sie den Live-Modus während kritischer Bereitstellungen, um Ausführungen in Echtzeit zu beobachten
### Für das Debugging
- Überprüfen Sie immer die Ausführungszeitlinie, um langsame Blöcke zu identifizieren
- Vergleichen Sie Eingaben zwischen funktionierenden und fehlerhaften Ausführungen
- Verwenden Sie Workflow-Snapshots, um den genauen Zustand zu sehen, wenn Probleme aufgetreten sind
## Nächste Schritte
- Erfahren Sie mehr über die [Kostenberechnung](/execution/costs), um die Preisgestaltung von Workflows zu verstehen
- Erkunden Sie die [externe API](/execution/api) für programmatischen Zugriff auf Protokolle
- Richten Sie [Benachrichtigungen](/execution/api#notifications) für Echtzeit-Warnungen per Webhook, E-Mail oder Slack ein
@@ -0,0 +1,196 @@
---
title: Erste Schritte
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Card, Cards } from 'fumadocs-ui/components/card'
import { File, Files, Folder } from 'fumadocs-ui/components/files'
import { Step, Steps } from 'fumadocs-ui/components/steps'
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
import {
AgentIcon,
ApiIcon,
ChartBarIcon,
CodeIcon,
ConditionalIcon,
ConnectIcon,
ExaAIIcon,
FirecrawlIcon,
GmailIcon,
NotionIcon,
PerplexityIcon,
SlackIcon,
} from '@/components/icons'
import { Video } from '@/components/ui/video'
import { Image } from '@/components/ui/image'
Erstelle deinen ersten KI-Workflow in 10 Minuten. In diesem Tutorial wirst du einen Personenrecherche-Agenten erstellen, der fortschrittliche LLM-gestützte Suchwerkzeuge nutzt, um Informationen über Personen zu extrahieren und zu strukturieren.
<Callout type="info">
Dieses Tutorial behandelt die wesentlichen Konzepte zum Erstellen von Workflows in Sim. Geschätzte Bearbeitungszeit: 10 Minuten.
</Callout>
## Was du erstellen wirst
Einen Personenrecherche-Agenten, der:
1. Benutzereingaben über eine Chat-Schnittstelle akzeptiert
2. Das Web mit KI-gestützten Tools durchsucht (Exa und Linkup)
3. Informationen über Personen extrahiert und strukturiert
4. Formatierte JSON-Daten mit Standort, Beruf und Ausbildung zurückgibt
<Image
src="/static/getting-started/started-1.png"
alt="Beispiel für erste Schritte"
width={800}
height={500}
/>
## Schritt-für-Schritt-Anleitung
<Steps>
<Step title="Workflow erstellen und einen KI-Agenten hinzufügen">
Klicke im Dashboard auf **Neuer Workflow** und benenne ihn "Getting Started".
Jeder neue Workflow enthält standardmäßig einen **Start-Block** dies ist der Eingangspunkt, der Benutzereingaben empfängt. Da wir diesen Workflow über Chat auslösen werden, ist keine Konfiguration für den Start-Block erforderlich.
Ziehe einen **Agenten-Block** aus dem linken Bereich auf die Arbeitsfläche und konfiguriere ihn:
- **Modell**: Wähle "OpenAI GPT-4o"
- **System-Prompt**: "Du bist ein Personenrecherche-Agent. Wenn dir ein Name einer Person gegeben wird, nutze deine verfügbaren Suchwerkzeuge, um umfassende Informationen über sie zu finden, einschließlich ihres Standorts, Berufs, Bildungshintergrunds und anderer relevanter Details."
- **Benutzer-Prompt**: Ziehe die Verbindung vom Ausgabefeld des Start-Blocks in dieses Feld, um `<start.input>` mit dem Benutzer-Prompt zu verbinden
<div className="mx-auto w-full overflow-hidden rounded-lg">
<Video src="getting-started/started-2.mp4" width={700} height={450} />
</div>
</Step>
<Step title="Suchwerkzeuge zum Agenten hinzufügen">
Erweitere deinen Agenten mit Websuche-Funktionen. Klicke auf den Agenten-Block, um ihn auszuwählen.
Im Bereich **Tools**:
- Klicke auf **Tool hinzufügen**
- Wähle **Exa** und **Linkup** aus den verfügbaren Tools
- Gib deine API-Schlüssel für beide Tools ein, um Websuche und Datenzugriff zu ermöglichen
<div className="mx-auto w-3/5 overflow-hidden rounded-lg">
<Video src="getting-started/started-3.mp4" width={700} height={450} />
</div>
</Step>
<Step title="Workflow testen">
Teste deinen Workflow mit dem **Chat-Panel** auf der rechten Seite des Bildschirms.
Im Chat-Panel:
- Klicke auf das Dropdown-Menü und wähle `agent1.content`, um die Ausgabe des Agenten anzuzeigen
- Gib eine Testnachricht ein: "John ist ein Softwareentwickler aus San Francisco, der Informatik an der Stanford University studiert hat."
- Klicke auf **Senden**, um den Workflow auszuführen
Der Agent wird die Person analysieren und strukturierte Informationen zurückgeben.
<div className="mx-auto w-full overflow-hidden rounded-lg">
<Video src="getting-started/started-4.mp4" width={700} height={450} />
</div>
</Step>
<Step title="Strukturierte Ausgabe konfigurieren">
Konfiguriere deinen Agenten, um strukturierte JSON-Daten zurückzugeben. Klicke auf den Agenten-Block, um ihn auszuwählen.
Im Bereich **Antwortformat**:
- Klicke auf das **Zauberstab-Symbol** (✨) neben dem Schema-Feld
- Gib den Prompt ein: "Erstelle ein Schema namens person, das Standort, Beruf und Ausbildung enthält"
- Die KI wird automatisch das JSON-Schema generieren
<div className="mx-auto w-full overflow-hidden rounded-lg">
<Video src="getting-started/started-5.mp4" width={700} height={450} />
</div>
</Step>
<Step title="Mit strukturierter Ausgabe testen">
Kehre zum **Chat-Panel** zurück, um das strukturierte Antwortformat zu testen.
Mit dem konfigurierten Antwortformat sind jetzt neue Ausgabeoptionen verfügbar:
- Klicke auf das Dropdown-Menü und wähle die Option für strukturierte Ausgabe (das Schema, das du gerade erstellt hast)
- Gib eine Testnachricht ein: "Sarah ist eine Marketing-Managerin aus New York mit einem MBA von der Harvard Business School."
- Klicke auf **Senden**, um den Workflow auszuführen
Der Agent wird nun strukturierte JSON-Ausgabe zurückgeben, wobei die Informationen der Person in die Felder Standort, Beruf und Ausbildung organisiert sind.
<div className="mx-auto w-full overflow-hidden rounded-lg">
<Video src="getting-started/started-6.mp4" width={700} height={450} />
</div>
</Step>
</Steps>
## Was du erstellt hast
Du hast erfolgreich einen KI-Workflow erstellt, der:
- ✅ Benutzereingaben über eine Chat-Schnittstelle akzeptiert
- ✅ Unstrukturierten Text mit KI verarbeitet
- ✅ Externe Suchwerkzeuge integriert (Exa und Linkup)
- ✅ Strukturierte JSON-Daten mit KI-generierten Schemas zurückgibt
- ✅ Echtzeit-Tests und Iteration demonstriert
- ✅ Die Leistungsfähigkeit der visuellen, codefreien Entwicklung zeigt
## Wichtige Konzepte, die du gelernt hast
### Verwendete Block-Typen
<Files>
<File
name="Start Block"
icon={<ConnectIcon className="h-4 w-4" />}
annotation="Einstiegspunkt für Benutzereingaben (automatisch enthalten)"
/>
<File
name="Agent Block"
icon={<AgentIcon className="h-4 w-4" />}
annotation="KI-Modell für Textverarbeitung und -analyse"
/>
</Files>
### Grundlegende Workflow-Konzepte
**Datenfluss**
Verbinde Blöcke durch Ziehen von Verbindungen, um Daten zwischen Workflow-Schritten zu übertragen
**Chat-Schnittstelle**
Teste Workflows in Echtzeit mit dem Chat-Panel und wähle verschiedene Ausgabeoptionen
**Tool-Integration**
Erweitere die Fähigkeiten des Agenten durch Integration externer Dienste wie Exa und Linkup
**Variablenreferenzen**
Greife auf Block-Ausgaben mit der `<blockName.output>` Syntax zu
**Strukturierte Ausgabe**
Definiere JSON-Schemas, um konsistente, formatierte Antworten von der KI zu gewährleisten
**KI-generierte Schemas**
Verwende den Zauberstab (✨), um Schemas aus natürlichsprachigen Eingabeaufforderungen zu generieren
**Iterative Entwicklung**
Erstelle, teste und verfeinere Workflows schnell mit sofortigem Feedback
## Nächste Schritte
<Cards>
<Card title="Workflow-Blöcke erkunden" href="/blocks">
Entdecke API-, Funktions-, Bedingungs- und andere Workflow-Blöcke
</Card>
<Card title="Integrationen durchsuchen" href="/tools">
Verbinde über 80 Dienste einschließlich Gmail, Slack, Notion und mehr
</Card>
<Card title="Benutzerdefinierte Logik hinzufügen" href="/blocks/function">
Schreibe benutzerdefinierte Funktionen für fortgeschrittene Datenverarbeitung
</Card>
<Card title="Deinen Workflow bereitstellen" href="/execution">
Mache deinen Workflow über REST API oder Webhooks zugänglich
</Card>
</Cards>
## Ressourcen
**Brauchst du detaillierte Erklärungen?** Besuche die [Blocks-Dokumentation](/blocks) für umfassende Anleitungen zu jeder Komponente.
**Suchst du nach Integrationen?** Erkunde die [Tools-Dokumentation](/tools), um alle 80+ verfügbaren Integrationen zu sehen.
**Bereit für den Livebetrieb?** Erfahre mehr über [Ausführung und Bereitstellung](/execution), um deine Workflows produktionsreif zu machen.
+57
View File
@@ -0,0 +1,57 @@
---
title: Dokumentation
---
import { Card, Cards } from 'fumadocs-ui/components/card'
# Sim Dokumentation
Willkommen bei Sim, einem visuellen Workflow-Builder für KI-Anwendungen. Erstellen Sie leistungsstarke KI-Agenten, Automatisierungs-Workflows und Datenverarbeitungs-Pipelines, indem Sie Blöcke auf einer Leinwand verbinden.
## Schnellstart
<Cards>
<Card title="Einführung" href="/introduction">
Erfahren Sie, was Sie mit Sim erstellen können
</Card>
<Card title="Erste Schritte" href="/getting-started">
Erstellen Sie Ihren ersten Workflow in 10 Minuten
</Card>
<Card title="Workflow-Blöcke" href="/blocks">
Lernen Sie die Bausteine kennen
</Card>
<Card title="Tools & Integrationen" href="/tools">
Entdecken Sie über 80 integrierte Schnittstellen
</Card>
</Cards>
## Kernkonzepte
<Cards>
<Card title="Verbindungen" href="/connections">
Verstehen Sie, wie Daten zwischen Blöcken fließen
</Card>
<Card title="Variablen" href="/variables">
Arbeiten Sie mit Workflow- und Umgebungsvariablen
</Card>
<Card title="Ausführung" href="/execution">
Überwachen Sie Workflow-Ausführungen und verwalten Sie Kosten
</Card>
<Card title="Trigger" href="/triggers">
Starten Sie Workflows über API, Webhooks oder Zeitpläne
</Card>
</Cards>
## Erweiterte Funktionen
<Cards>
<Card title="Team-Management" href="/permissions/roles-and-permissions">
Workspace-Rollen und Berechtigungen einrichten
</Card>
<Card title="MCP-Integration" href="/mcp">
Externe Dienste mit dem Model Context Protocol verbinden
</Card>
<Card title="SDKs" href="/api-reference">
Sim in Ihre Anwendungen integrieren
</Card>
</Cards>
@@ -0,0 +1,117 @@
---
title: Einführung
---
import { Card, Cards } from 'fumadocs-ui/components/card'
import { Callout } from 'fumadocs-ui/components/callout'
import { Image } from '@/components/ui/image'
import { Video } from '@/components/ui/video'
Sim ist ein Open-Source-Tool zur visuellen Workflow-Erstellung für die Entwicklung und Bereitstellung von KI-Agenten-Workflows. Entwerfen Sie intelligente Automatisierungssysteme mit einer No-Code-Oberfläche verbinden Sie KI-Modelle, Datenbanken, APIs und Business-Tools über eine intuitive Drag-and-Drop-Oberfläche. Ob Sie Chatbots entwickeln, Geschäftsprozesse automatisieren oder komplexe Datenpipelines orchestrieren Sim bietet die Werkzeuge, um Ihre KI-Workflows zum Leben zu erwecken.
<div className="flex justify-center">
<Image
src="/static/introduction.png"
alt="Sim visuelle Workflow-Leinwand"
width={700}
height={450}
className="my-6"
/>
</div>
## Was Sie erstellen können
**KI-Assistenten & Chatbots**
Entwickeln Sie intelligente Konversationsagenten, die sich in Ihre Tools und Daten integrieren lassen. Ermöglichen Sie Funktionen wie Websuche, Kalenderverwaltung, E-Mail-Automatisierung und nahtlose Interaktion mit Geschäftssystemen.
**Automatisierung von Geschäftsprozessen**
Beseitigen Sie manuelle Aufgaben in Ihrer gesamten Organisation. Automatisieren Sie Dateneingaben, erstellen Sie Berichte, beantworten Sie Kundenanfragen und optimieren Sie Workflows zur Inhaltserstellung.
**Datenverarbeitung & -analyse**
Verwandeln Sie Rohdaten in umsetzbare Erkenntnisse. Extrahieren Sie Informationen aus Dokumenten, führen Sie Datensatzanalysen durch, erstellen Sie automatisierte Berichte und synchronisieren Sie Daten über verschiedene Plattformen hinweg.
**API-Integrations-Workflows**
Orchestieren Sie komplexe Interaktionen zwischen mehreren Diensten. Erstellen Sie einheitliche API-Endpunkte, implementieren Sie anspruchsvolle Geschäftslogik und bauen Sie ereignisgesteuerte Automatisierungssysteme.
<div className="mx-auto w-full overflow-hidden rounded-lg my-6">
<Video src="introduction/chat-workflow.mp4" width={700} height={450} />
</div>
## Wie es funktioniert
**Visueller Workflow-Editor**
Entwerfen Sie Workflows mit einer intuitiven Drag-and-Drop-Oberfläche. Verbinden Sie KI-Modelle, Datenbanken, APIs und Dienste von Drittanbietern über eine visuelle No-Code-Schnittstelle, die komplexe Automatisierungslogik leicht verständlich und wartbar macht.
**Modulares Blocksystem**
Bauen Sie mit spezialisierten Komponenten: Verarbeitungsblöcke (KI-Agenten, API-Aufrufe, benutzerdefinierte Funktionen), Logikblöcke (bedingte Verzweigungen, Schleifen, Router) und Ausgabeblöcke (Antworten, Evaluatoren). Jeder Block übernimmt eine bestimmte Aufgabe in Ihrem Workflow.
**Flexible Ausführungsauslöser**
Starten Sie Workflows über mehrere Kanäle, einschließlich Chat-Schnittstellen, REST-APIs, Webhooks, geplante Cron-Jobs oder externe Ereignisse von Plattformen wie Slack und GitHub.
**Echtzeit-Zusammenarbeit**
Ermöglichen Sie Ihrem Team, gemeinsam zu arbeiten. Mehrere Benutzer können Workflows gleichzeitig bearbeiten, mit Live-Updates und detaillierten Berechtigungskontrollen.
<div className="mx-auto w-full overflow-hidden rounded-lg my-6">
<Video src="introduction/build-workflow.mp4" width={700} height={450} />
</div>
## Integrationen
Sim bietet native Integrationen mit über 80 Diensten in verschiedenen Kategorien:
- **KI-Modelle**: OpenAI, Anthropic, Google Gemini, Groq, Cerebras, lokale Modelle über Ollama oder VLLM
- **Kommunikation**: Gmail, Slack, Microsoft Teams, Telegram, WhatsApp
- **Produktivität**: Notion, Google Workspace, Airtable, Monday.com
- **Entwicklung**: GitHub, Jira, Linear, automatisierte Browser-Tests
- **Suche & Daten**: Google Search, Perplexity, Firecrawl, Exa AI
- **Datenbanken**: PostgreSQL, MySQL, Supabase, Pinecone, Qdrant
Für benutzerdefinierte Integrationen nutzen Sie unsere [MCP (Model Context Protocol)-Unterstützung](/mcp), um beliebige externe Dienste oder Tools anzubinden.
<div className="mx-auto w-full overflow-hidden rounded-lg my-6">
<Video src="introduction/integrations-sidebar.mp4" width={700} height={450} />
</div>
## Copilot
**Fragen stellen & Anleitung erhalten**
Der Copilot beantwortet Fragen zu Sim, erklärt Ihre Workflows und gibt Verbesserungsvorschläge. Verwenden Sie das `@` Symbol, um auf Workflows, Blöcke, Dokumentation, Wissen und Protokolle für kontextbezogene Unterstützung zu verweisen.
**Workflows erstellen & bearbeiten**
Wechseln Sie in den Agent-Modus, damit der Copilot Änderungen direkt auf Ihrer Arbeitsfläche vorschlagen und anwenden kann. Fügen Sie Blöcke hinzu, konfigurieren Sie Einstellungen, verbinden Sie Variablen und strukturieren Sie Workflows mit natürlichsprachlichen Befehlen um.
**Adaptive Reasoning-Stufen**
Wählen Sie zwischen den Modi Schnell, Auto, Erweitert oder Behemoth, je nach Komplexität der Aufgabe. Beginnen Sie mit Schnell für einfache Fragen und steigern Sie sich bis zu Behemoth für komplexe architektonische Änderungen und tiefgehendes Debugging.
Erfahren Sie mehr über [Copilot-Funktionen](/copilot) und wie Sie die Produktivität mit KI-Unterstützung maximieren können.
<div className="mx-auto w-full overflow-hidden rounded-lg my-6">
<Video src="introduction/copilot-workflow.mp4" width={700} height={450} />
</div>
## Bereitstellungsoptionen
**Cloud-Hosting**
Starten Sie sofort bei [sim.ai](https://sim.ai) mit vollständig verwalteter Infrastruktur, automatischer Skalierung und integrierter Beobachtbarkeit. Konzentrieren Sie sich auf den Aufbau von Workflows, während wir den Betrieb übernehmen.
**Self-Hosting**
Stellen Sie die Lösung auf Ihrer eigenen Infrastruktur mit Docker Compose oder Kubernetes bereit. Behalten Sie die vollständige Kontrolle über Ihre Daten mit Unterstützung für lokale KI-Modelle durch Ollama-Integration.
## Nächste Schritte
Bereit, Ihren ersten KI-Workflow zu erstellen?
<Cards>
<Card title="Erste Schritte" href="/getting-started">
Erstellen Sie Ihren ersten Workflow in 10 Minuten
</Card>
<Card title="Workflow-Blöcke" href="/blocks">
Erfahren Sie mehr über die Bausteine
</Card>
<Card title="Tools & Integrationen" href="/tools">
Entdecken Sie über 80 integrierte Integrationen
</Card>
<Card title="Team-Berechtigungen" href="/permissions/roles-and-permissions">
Richten Sie Workspace-Rollen und Berechtigungen ein
</Card>
</Cards>
@@ -0,0 +1,60 @@
---
title: Tastaturkürzel
description: Meistern Sie die Workflow-Arbeitsfläche mit Tastaturkürzeln und Maussteuerung
---
import { Callout } from 'fumadocs-ui/components/callout'
Beschleunigen Sie die Erstellung Ihrer Workflows mit diesen Tastaturkürzeln und Maussteuerungen. Alle Tastenkombinationen funktionieren, wenn die Arbeitsfläche fokussiert ist (nicht beim Tippen in einem Eingabefeld).
<Callout type="info">
**Mod** bezieht sich auf `Cmd` unter macOS und `Ctrl` unter Windows/Linux.
</Callout>
## Arbeitsflächen-Steuerung
### Maussteuerung
| Aktion | Steuerung |
|--------|---------|
| Arbeitsfläche verschieben | Linksziehen auf leerer Fläche |
| Arbeitsfläche verschieben | Scrollen oder Trackpad |
| Mehrere Blöcke auswählen | Rechtsziehen zum Aufziehen eines Auswahlrahmens |
| Block ziehen | Linksziehen auf Block-Kopfzeile |
| Zur Auswahl hinzufügen | `Mod` + Klick auf Blöcke |
### Workflow-Aktionen
| Tastenkombination | Aktion |
|----------|--------|
| `Mod` + `Enter` | Workflow ausführen (oder abbrechen, falls aktiv) |
| `Mod` + `Z` | Rückgängig |
| `Mod` + `Shift` + `Z` | Wiederholen |
| `Mod` + `C` | Ausgewählte Blöcke kopieren |
| `Mod` + `V` | Blöcke einfügen |
| `Delete` oder `Backspace` | Ausgewählte Blöcke oder Verbindungen löschen |
| `Shift` + `L` | Arbeitsfläche automatisch anordnen |
## Panel-Navigation
Diese Tastenkombinationen wechseln zwischen den Panel-Tabs auf der rechten Seite der Arbeitsfläche.
| Tastenkombination | Aktion |
|----------|--------|
| `Mod` + `F` | Toolbar-Suche fokussieren |
## Globale Navigation
| Tastenkombination | Aktion |
|----------|--------|
| `Mod` + `K` | Suche öffnen |
| `Mod` + `Shift` + `A` | Neuen Agenten-Workflow hinzufügen |
| `Mod` + `Y` | Zu Vorlagen gehen |
| `Mod` + `L` | Zu Logs gehen |
## Dienstprogramm
| Tastenkombination | Aktion |
|----------|--------|
| `Mod` + `D` | Terminal-Konsole leeren |
| `Mod` + `E` | Benachrichtigungen löschen |
@@ -0,0 +1,121 @@
---
title: Übersicht
description: Laden Sie Ihre Dokumente hoch, verarbeiten und durchsuchen Sie sie
mit intelligenter Vektorsuche und Chunking
---
import { Video } from '@/components/ui/video'
import { Image } from '@/components/ui/image'
Die Wissensdatenbank ermöglicht es Ihnen, Ihre Dokumente hochzuladen, zu verarbeiten und mit intelligenter Vektorsuche und Chunking zu durchsuchen. Dokumente verschiedener Typen werden automatisch verarbeitet, eingebettet und durchsuchbar gemacht. Ihre Dokumente werden intelligent in Chunks aufgeteilt, und Sie können sie mit natürlichsprachlichen Abfragen anzeigen, bearbeiten und durchsuchen.
## Upload und Verarbeitung
Laden Sie einfach Ihre Dokumente hoch, um zu beginnen. Sim verarbeitet sie automatisch im Hintergrund, extrahiert Text, erstellt Embeddings und teilt sie in durchsuchbare Chunks auf.
<div className="mx-auto w-full overflow-hidden rounded-lg">
<Video src="knowledgebase-1.mp4" width={700} height={450} />
</div>
Das System übernimmt den gesamten Verarbeitungsprozess für Sie:
1. **Textextraktion**: Inhalte werden aus Ihren Dokumenten mit spezialisierten Parsern für jeden Dateityp extrahiert
2. **Intelligentes Chunking**: Dokumente werden in sinnvolle Chunks mit konfigurierbarer Größe und Überlappung aufgeteilt
3. **Embedding-Generierung**: Vektoreinbettungen werden für semantische Suchfunktionen erstellt
4. **Verarbeitungsstatus**: Verfolgen Sie den Fortschritt während Ihre Dokumente verarbeitet werden
## Unterstützte Dateitypen
Sim unterstützt PDF, Word (DOC/DOCX), Klartext (TXT), Markdown (MD), HTML, Excel (XLS/XLSX), PowerPoint (PPT/PPTX) und CSV-Dateien. Dateien können bis zu 100MB groß sein, wobei die optimale Leistung bei Dateien unter 50MB liegt. Sie können mehrere Dokumente gleichzeitig hochladen, und PDF-Dateien werden mit OCR-Verarbeitung für gescannte Dokumente unterstützt.
## Anzeigen und Bearbeiten von Chunks
Sobald Ihre Dokumente verarbeitet sind, können Sie die einzelnen Chunks anzeigen und bearbeiten. Dies gibt Ihnen volle Kontrolle darüber, wie Ihre Inhalte organisiert und durchsucht werden.
<Image src="/static/knowledgebase/knowledgebase.png" alt="Dokumentchunk-Ansicht mit verarbeiteten Inhalten" width={800} height={500} />
### Chunk-Konfiguration
Beim Erstellen einer Wissensdatenbank können Sie konfigurieren, wie Dokumente in Chunks aufgeteilt werden:
| Einstellung | Einheit | Standard | Bereich | Beschreibung |
|---------|------|---------|-------|-------------|
| **Maximale Chunk-Größe** | Tokens | 1.024 | 100-4.000 | Maximale Größe jedes Chunks (1 Token ≈ 4 Zeichen) |
| **Minimale Chunk-Größe** | Zeichen | 1 | 1-2.000 | Minimale Chunk-Größe, um winzige Fragmente zu vermeiden |
| **Überlappung** | Zeichen | 200 | 0-500 | Kontextüberlappung zwischen aufeinanderfolgenden Chunks |
- **Hierarchische Aufteilung**: Berücksichtigt die Dokumentstruktur (Abschnitte, Absätze, Sätze)
### Bearbeitungsmöglichkeiten
- **Chunk-Inhalt bearbeiten**: Textinhalt einzelner Chunks ändern
- **Chunk-Grenzen anpassen**: Chunks nach Bedarf zusammenführen oder aufteilen
- **Metadaten hinzufügen**: Chunks mit zusätzlichem Kontext anreichern
- **Massenoperationen**: Mehrere Chunks effizient verwalten
## Erweiterte PDF-Verarbeitung
Für PDF-Dokumente bietet Sim erweiterte Verarbeitungsfunktionen:
### OCR-Unterstützung
Wenn mit Azure oder [Mistral OCR](https://docs.mistral.ai/ocr/) konfiguriert:
- **Verarbeitung gescannter Dokumente**: Text aus bildbasierten PDFs extrahieren
- **Verarbeitung gemischter Inhalte**: PDFs mit Text und Bildern verarbeiten
- **Hohe Genauigkeit**: Fortschrittliche KI-Modelle gewährleisten präzise Textextraktion
## Verwendung des Knowledge-Blocks in Workflows
Sobald Ihre Dokumente verarbeitet sind, können Sie sie in Ihren KI-Workflows über den Knowledge-Block verwenden. Dies ermöglicht Retrieval-Augmented Generation (RAG), wodurch Ihre KI-Agenten auf Ihre Dokumentinhalte zugreifen und darüber nachdenken können, um genauere, kontextbezogene Antworten zu liefern.
<Image src="/static/knowledgebase/knowledgebase-2.png" alt="Verwendung des Knowledge-Blocks in Workflows" width={800} height={500} />
### Knowledge-Block-Funktionen
- **Semantische Suche**: Relevante Inhalte mithilfe natürlichsprachlicher Abfragen finden
- **Kontextintegration**: Relevante Chunks automatisch in Agenten-Prompts einbinden
- **Dynamisches Abrufen**: Suche erfolgt in Echtzeit während der Workflow-Ausführung
- **Relevanz-Bewertung**: Ergebnisse nach semantischer Ähnlichkeit sortiert
### Integrationsoptionen
- **System-Prompts**: Stellen Sie Ihren KI-Agenten Kontext bereit
- **Dynamischer Kontext**: Suchen und fügen Sie relevante Informationen während Konversationen hinzu
- **Multi-Dokument-Suche**: Durchsuchen Sie Ihre gesamte Wissensdatenbank
- **Gefilterte Suche**: Kombinieren Sie mit Tags für präzises Abrufen von Inhalten
## Vektor-Suchtechnologie
Sim verwendet Vektorsuche, die von [pgvector](https://github.com/pgvector/pgvector) unterstützt wird, um die Bedeutung und den Kontext Ihrer Inhalte zu verstehen:
### Semantisches Verständnis
- **Kontextuelle Suche**: Findet relevante Inhalte, auch wenn exakte Schlüsselwörter nicht übereinstimmen
- **Konzeptbasiertes Abrufen**: Versteht Beziehungen zwischen Ideen
- **Mehrsprachige Unterstützung**: Funktioniert über verschiedene Sprachen hinweg
- **Synonymerkennung**: Findet verwandte Begriffe und Konzepte
### Suchfunktionen
- **Natürlichsprachige Abfragen**: Stellen Sie Fragen in einfachem Deutsch
- **Ähnlichkeitssuche**: Finden Sie konzeptionell ähnliche Inhalte
- **Hybride Suche**: Kombiniert Vektor- und traditionelle Schlüsselwortsuche
- **Konfigurierbare Ergebnisse**: Steuern Sie die Anzahl und Relevanzschwelle der Ergebnisse
## Dokumentenverwaltung
### Organisationsfunktionen
- **Massen-Upload**: Laden Sie mehrere Dateien gleichzeitig über die asynchrone API hoch
- **Verarbeitungsstatus**: Echtzeit-Updates zur Dokumentenverarbeitung
- **Suchen und filtern**: Finden Sie Dokumente schnell in großen Sammlungen
- **Metadaten-Tracking**: Automatische Erfassung von Dateiinformationen und Verarbeitungsdetails
### Sicherheit und Datenschutz
- **Sichere Speicherung**: Dokumente werden mit Sicherheit auf Unternehmensniveau gespeichert
- **Zugriffskontrolle**: Workspace-basierte Berechtigungen
- **Verarbeitungsisolierung**: Jeder Workspace hat isolierte Dokumentenverarbeitung
- **Datenaufbewahrung**: Konfigurieren Sie Richtlinien zur Dokumentenaufbewahrung
## Erste Schritte
1. **Navigieren Sie zu Ihrer Wissensdatenbank**: Zugriff über Ihre Workspace-Seitenleiste
2. **Dokumente hochladen**: Ziehen und ablegen oder Dateien zum Hochladen auswählen
3. **Verarbeitung überwachen**: Beobachten Sie, wie Dokumente verarbeitet und in Abschnitte unterteilt werden
4. **Abschnitte erkunden**: Zeigen Sie die verarbeiteten Inhalte an und bearbeiten Sie sie
5. **Zu Workflows hinzufügen**: Verwenden Sie den Knowledge-Block, um mit Ihren KI-Agenten zu integrieren
Die Wissensdatenbank verwandelt Ihre statischen Dokumente in eine intelligente, durchsuchbare Ressource, die Ihre KI-Workflows für fundiertere und kontextbezogene Antworten nutzen können.
@@ -0,0 +1,108 @@
---
title: Tags und Filterung
---
import { Video } from '@/components/ui/video'
Tags bieten eine leistungsstarke Möglichkeit, Ihre Dokumente zu organisieren und präzise Filterungen für Ihre Vektorsuchen zu erstellen. Durch die Kombination von tag-basierter Filterung mit semantischer Suche können Sie genau die Inhalte aus Ihrer Wissensdatenbank abrufen, die Sie benötigen.
## Tags zu Dokumenten hinzufügen
Sie können jedem Dokument in Ihrer Wissensdatenbank benutzerdefinierte Tags hinzufügen, um Ihre Inhalte zu organisieren und zu kategorisieren und so leichter auffindbar zu machen.
<div className="mx-auto w-full overflow-hidden rounded-lg">
<Video src="knowledgebase-tag.mp4" width={700} height={450} />
</div>
### Tag-Verwaltung
- **Benutzerdefinierte Tags**: Erstellen Sie Ihr eigenes Tag-System, das zu Ihrem Arbeitsablauf passt
- **Mehrere Tags pro Dokument**: Wenden Sie so viele Tags wie nötig auf jedes Dokument an, es stehen 7 Tag-Slots pro Wissensdatenbank zur Verfügung, die von allen Dokumenten in der Wissensdatenbank gemeinsam genutzt werden
- **Tag-Organisation**: Gruppieren Sie verwandte Dokumente mit einheitlichen Tags
### Best Practices für Tags
- **Einheitliche Benennung**: Verwenden Sie standardisierte Tag-Namen für alle Ihre Dokumente
- **Beschreibende Tags**: Verwenden Sie klare, aussagekräftige Tag-Namen
- **Regelmäßige Bereinigung**: Entfernen Sie ungenutzte oder veraltete Tags regelmäßig
## Verwendung von Tags in Wissensblöcken
Tags werden besonders leistungsstark, wenn sie mit dem Wissensblock in Ihren Workflows kombiniert werden. Sie können Ihre Suchen auf bestimmte getaggte Inhalte filtern und so sicherstellen, dass Ihre KI-Agenten die relevantesten Informationen erhalten.
<div className="mx-auto w-full overflow-hidden rounded-lg">
<Video src="knowledgebase-tag2.mp4" width={700} height={450} />
</div>
## Suchmodi
Der Wissensblock unterstützt drei verschiedene Suchmodi, abhängig davon, was Sie angeben:
### 1. Nur-Tag-Suche
Wenn Sie **nur Tags angeben** (keine Suchanfrage):
- **Direkter Abruf**: Ruft alle Dokumente ab, die die angegebenen Tags haben
- **Keine Vektorsuche**: Ergebnisse basieren ausschließlich auf Tag-Übereinstimmung
- **Schnelle Leistung**: Schneller Abruf ohne semantische Verarbeitung
- **Exakte Übereinstimmung**: Nur Dokumente mit allen angegebenen Tags werden zurückgegeben
**Anwendungsfall**: Wenn du alle Dokumente aus einer bestimmten Kategorie oder einem Projekt benötigst
### 2. Nur Vektorsuche
Wenn du **nur eine Suchanfrage angibst** (keine Tags):
- **Semantische Suche**: Findet Inhalte basierend auf Bedeutung und Kontext
- **Vollständige Wissensdatenbank**: Durchsucht alle Dokumente
- **Relevanz-Ranking**: Ergebnisse nach semantischer Ähnlichkeit geordnet
- **Natürliche Sprache**: Verwende Fragen oder Phrasen, um relevante Inhalte zu finden
**Anwendungsfall**: Wenn du die relevantesten Inhalte unabhängig von der Organisation benötigst
### 3. Kombinierte Tag-Filterung + Vektorsuche
Wenn du **sowohl Tags als auch eine Suchanfrage angibst**:
1. **Zuerst**: Filtere Dokumente auf solche mit den angegebenen Tags
2. **Dann**: Führe eine Vektorsuche innerhalb dieser gefilterten Teilmenge durch
3. **Ergebnis**: Semantisch relevante Inhalte nur aus deinen getaggten Dokumenten
**Anwendungsfall**: Wenn du relevante Inhalte aus einer bestimmten Kategorie oder einem Projekt benötigst
### Suchkonfiguration
#### Tag-Filterung
- **Mehrere Tags**: Verwende mehrere Tags für ODER-Logik (Dokument muss einen oder mehrere der Tags haben)
- **Tag-Kombinationen**: Mische verschiedene Tag-Typen für präzise Filterung
- **Groß-/Kleinschreibung**: Tag-Abgleich ist unabhängig von Groß-/Kleinschreibung
- **Teilabgleich**: Exakte Übereinstimmung des Tag-Namens erforderlich
#### Vektorsuche-Parameter
- **Abfragekomplexität**: Fragen in natürlicher Sprache funktionieren am besten
- **Ergebnislimits**: Konfiguriere, wie viele Chunks abgerufen werden sollen
- **Relevanzschwelle**: Lege minimale Ähnlichkeitswerte fest
- **Kontextfenster**: Passe die Chunk-Größe an deinen Anwendungsfall an
## Integration mit Workflows
### Konfiguration des Wissensblocks
1. **Wissensdatenbank auswählen**: Wähle aus, welche Wissensdatenbank durchsucht werden soll
2. **Tags hinzufügen**: Gib Filterungs-Tags an (optional)
3. **Anfrage eingeben**: Füge deine Suchanfrage hinzu (optional)
4. **Ergebnisse konfigurieren**: Lege die Anzahl der abzurufenden Chunks fest
5. **Suche testen**: Sieh dir die Ergebnisse an, bevor du sie im Workflow verwendest
### Dynamische Tag-Nutzung
- **Variable Tags**: Verwenden Sie Workflow-Variablen als Tag-Werte
- **Bedingte Filterung**: Wenden Sie verschiedene Tags basierend auf Workflow-Logik an
- **Kontextbezogene Suche**: Passen Sie Tags basierend auf dem Gesprächskontext an
- **Mehrstufige Filterung**: Verfeinern Sie Suchen durch Workflow-Schritte
### Leistungsoptimierung
- **Effiziente Filterung**: Tag-Filterung erfolgt vor der Vektorsuche für bessere Leistung
- **Caching**: Häufig verwendete Tag-Kombinationen werden für Geschwindigkeit zwischengespeichert
- **Parallele Verarbeitung**: Mehrere Tag-Suchen können gleichzeitig ausgeführt werden
- **Ressourcenmanagement**: Automatische Optimierung der Suchressourcen
## Erste Schritte mit Tags
1. **Planen Sie Ihre Tag-Struktur**: Entscheiden Sie sich für einheitliche Namenskonventionen
2. **Beginnen Sie mit dem Taggen**: Fügen Sie Ihren vorhandenen Dokumenten relevante Tags hinzu
3. **Testen Sie Kombinationen**: Experimentieren Sie mit Tag- und Suchanfragekombinationen
4. **Integration in Workflows**: Verwenden Sie den Knowledge-Block mit Ihrer Tagging-Strategie
5. **Verfeinern Sie im Laufe der Zeit**: Passen Sie Ihren Tagging-Ansatz basierend auf Suchergebnissen an
Tags verwandeln Ihre Wissensdatenbank von einem einfachen Dokumentenspeicher in ein präzise organisiertes, durchsuchbares Intelligenzsystem, das Ihre KI-Workflows mit chirurgischer Präzision navigieren können.
@@ -0,0 +1,108 @@
---
title: Workflows als MCP bereitstellen
description: Stellen Sie Ihre Workflows als MCP-Tools für externe KI-Assistenten
und Anwendungen bereit
---
import { Video } from '@/components/ui/video'
import { Callout } from 'fumadocs-ui/components/callout'
Stellen Sie Ihre Workflows als MCP-Tools bereit, um sie für externe KI-Assistenten wie Claude Desktop, Cursor und andere MCP-kompatible Clients zugänglich zu machen. Dies verwandelt Ihre Workflows in aufrufbare Tools, die von überall aus aufgerufen werden können.
## MCP-Server erstellen und verwalten
MCP-Server gruppieren Ihre Workflow-Tools zusammen. Erstellen und verwalten Sie sie in den Workspace-Einstellungen:
<div className="mx-auto w-full overflow-hidden rounded-lg">
<Video src="mcp/mcp-server.mp4" width={700} height={450} />
</div>
1. Navigieren Sie zu **Einstellungen → MCP-Server**
2. Klicken Sie auf **Server erstellen**
3. Geben Sie einen Namen und eine optionale Beschreibung ein
4. Kopieren Sie die Server-URL zur Verwendung in Ihren MCP-Clients
5. Zeigen Sie alle zum Server hinzugefügten Tools an und verwalten Sie diese
## Einen Workflow als Tool hinzufügen
Sobald Ihr Workflow bereitgestellt ist, können Sie ihn als MCP-Tool verfügbar machen:
<div className="mx-auto w-full overflow-hidden rounded-lg">
<Video src="mcp/mcp-deploy-tool.mp4" width={700} height={450} />
</div>
1. Öffnen Sie Ihren bereitgestellten Workflow
2. Klicken Sie auf **Bereitstellen** und wechseln Sie zum Tab **MCP**
3. Konfigurieren Sie den Tool-Namen und die Beschreibung
4. Fügen Sie Beschreibungen für jeden Parameter hinzu (hilft der KI, Eingaben zu verstehen)
5. Wählen Sie aus, zu welchen MCP-Servern es hinzugefügt werden soll
<Callout type="info">
Der Workflow muss bereitgestellt sein, bevor er als MCP-Tool hinzugefügt werden kann.
</Callout>
## Tool-Konfiguration
### Tool-Name
Verwenden Sie Kleinbuchstaben, Zahlen und Unterstriche. Der Name sollte beschreibend sein und den MCP-Namenskonventionen folgen (z. B. `search_documents`, `send_email`).
### Beschreibung
Schreiben Sie eine klare Beschreibung dessen, was das Tool tut. Dies hilft KI-Assistenten zu verstehen, wann das Tool verwendet werden soll.
### Parameter
Die Eingabeformatfelder deines Workflows werden zu Tool-Parametern. Füge jedem Parameter Beschreibungen hinzu, um KI-Assistenten zu helfen, korrekte Werte bereitzustellen.
## MCP-Clients verbinden
Verwende die Server-URL aus den Einstellungen, um externe Anwendungen zu verbinden:
### Claude Desktop
Füge dies zu deiner Claude Desktop-Konfiguration hinzu (`~/Library/Application Support/Claude/claude_desktop_config.json`):
```json
{
"mcpServers": {
"my-sim-workflows": {
"command": "npx",
"args": ["-y", "mcp-remote", "YOUR_SERVER_URL"]
}
}
}
```
### Cursor
Füge die Server-URL in den MCP-Einstellungen von Cursor mit demselben mcp-remote-Muster hinzu.
<Callout type="warn">
Füge deinen API-Key-Header (`X-API-Key`) für authentifizierten Zugriff hinzu, wenn du mcp-remote oder andere HTTP-basierte MCP-Transporte verwendest.
</Callout>
## Server-Verwaltung
In der Server-Detailansicht unter **Einstellungen → MCP-Server** können Sie:
- **Tools anzeigen**: Alle Workflows sehen, die einem Server hinzugefügt wurden
- **URL kopieren**: Die Server-URL für MCP-Clients abrufen
- **Workflows hinzufügen**: Weitere bereitgestellte Workflows als Tools hinzufügen
- **Tools entfernen**: Workflows vom Server entfernen
- **Server löschen**: Den gesamten Server und alle seine Tools entfernen
## So funktioniert es
Wenn ein MCP-Client dein Tool aufruft:
1. Die Anfrage wird an deiner MCP-Server-URL empfangen
2. Sim validiert die Anfrage und ordnet Parameter den Workflow-Eingaben zu
3. Der bereitgestellte Workflow wird mit den angegebenen Eingaben ausgeführt
4. Die Ergebnisse werden an den MCP-Client zurückgegeben
Workflows werden mit derselben Bereitstellungsversion wie API-Aufrufe ausgeführt, was konsistentes Verhalten gewährleistet.
## Berechtigungsanforderungen
| Aktion | Erforderliche Berechtigung |
|--------|-------------------|
| MCP-Server erstellen | **Admin** |
| Workflows zu Servern hinzufügen | **Write** oder **Admin** |
| MCP-Server anzeigen | **Read**, **Write** oder **Admin** |
| MCP-Server löschen | **Admin** |
+144
View File
@@ -0,0 +1,144 @@
---
title: MCP-Tools verwenden
description: Externe Tools und Dienste über das Model Context Protocol verbinden
---
import { Image } from '@/components/ui/image'
import { Video } from '@/components/ui/video'
import { Callout } from 'fumadocs-ui/components/callout'
Das Model Context Protocol ([MCP](https://modelcontextprotocol.com/)) ermöglicht es Ihnen, externe Tools und Dienste über ein standardisiertes Protokoll zu verbinden, wodurch Sie APIs und Dienste direkt in Ihre Workflows integrieren können. Mit MCP können Sie die Fähigkeiten von Sim erweitern, indem Sie benutzerdefinierte Integrationen hinzufügen, die nahtlos mit Ihren Agenten und Workflows zusammenarbeiten.
## Was ist MCP?
MCP ist ein offener Standard, der es KI-Assistenten ermöglicht, sich sicher mit externen Datenquellen und Tools zu verbinden. Es bietet eine standardisierte Methode, um:
- Verbindungen zu Datenbanken, APIs und Dateisystemen herzustellen
- Auf Echtzeitdaten von externen Diensten zuzugreifen
- Benutzerdefinierte Tools und Skripte auszuführen
- Sicheren, kontrollierten Zugriff auf externe Ressourcen zu gewährleisten
## Konfiguration von MCP-Servern
MCP-Server stellen Sammlungen von Tools bereit, die Ihre Agenten nutzen können. Konfigurieren Sie diese in den Workspace-Einstellungen:
<div className="mx-auto w-full overflow-hidden rounded-lg">
<Video src="mcp/settings-mcp-tools.mp4" width={700} height={450} />
</div>
1. Navigieren Sie zu Ihren Workspace-Einstellungen
2. Gehen Sie zum Abschnitt **MCP-Server**
3. Klicken Sie auf **MCP-Server hinzufügen**
4. Geben Sie die Server-Konfigurationsdetails ein
5. Speichern Sie die Konfiguration
<Callout type="info">
Sie können MCP-Server auch direkt über die Symbolleiste in einem Agent-Block für eine schnelle Einrichtung konfigurieren.
</Callout>
### Tools aktualisieren
Klicken Sie bei einem Server auf **Aktualisieren**, um die neuesten Tool-Schemas abzurufen und alle Agent-Blöcke, die diese Tools verwenden, automatisch mit den neuen Parameterdefinitionen zu aktualisieren.
## MCP-Tools in Agents verwenden
Sobald MCP-Server konfiguriert sind, werden ihre Tools in Ihren Agent-Blöcken verfügbar:
<div className="flex justify-center">
<Image
src="/static/blocks/mcp-2.png"
alt="Using MCP Tool in Agent Block"
width={700}
height={450}
className="my-6"
/>
</div>
1. Öffnen Sie einen **Agent**-Block
2. Im Bereich **Tools** sehen Sie die verfügbaren MCP-Tools
3. Wählen Sie die Tools aus, die der Agent verwenden soll
4. Der Agent kann nun während der Ausführung auf diese Tools zugreifen
## Eigenständiger MCP-Tool-Block
Für eine präzisere Steuerung können Sie den dedizierten MCP-Tool-Block verwenden, um bestimmte MCP-Tools auszuführen:
<div className="flex justify-center">
<Image
src="/static/blocks/mcp-3.png"
alt="Standalone MCP Tool Block"
width={700}
height={450}
className="my-6"
/>
</div>
Der MCP-Tool-Block ermöglicht Ihnen:
- Jedes konfigurierte MCP-Tool direkt auszuführen
- Spezifische Parameter an das Tool zu übergeben
- Die Ausgabe des Tools in nachfolgenden Workflow-Schritten zu verwenden
- Mehrere MCP-Tools miteinander zu verketten
### Wann MCP-Tool vs. Agent verwenden
**Verwenden Sie Agent mit MCP-Tools, wenn:**
- Sie möchten, dass die KI entscheidet, welche Tools verwendet werden
- Sie komplexes Reasoning darüber benötigen, wann und wie Tools verwendet werden
- Sie eine natürlichsprachliche Interaktion mit den Tools wünschen
**Verwenden Sie den MCP-Tool-Block, wenn:**
- Sie eine deterministische Tool-Ausführung benötigen
- Sie ein bestimmtes Tool mit bekannten Parametern ausführen möchten
- Sie strukturierte Workflows mit vorhersehbaren Schritten erstellen
## Berechtigungsanforderungen
Die MCP-Funktionalität erfordert spezifische Workspace-Berechtigungen:
| Aktion | Erforderliche Berechtigung |
|--------|-------------------|
| MCP-Server in den Einstellungen konfigurieren | **Admin** |
| MCP-Tools in Agenten verwenden | **Write** oder **Admin** |
| Verfügbare MCP-Tools anzeigen | **Read**, **Write** oder **Admin** |
| MCP-Tool-Blöcke ausführen | **Write** oder **Admin** |
## Häufige Anwendungsfälle
### Datenbankintegration
Verbinden Sie sich mit Datenbanken, um Daten in Ihren Workflows abzufragen, einzufügen oder zu aktualisieren.
### API-Integrationen
Greifen Sie auf externe APIs und Webdienste zu, die keine integrierten Sim-Integrationen haben.
### Dateisystemzugriff
Lesen, schreiben und bearbeiten Sie Dateien auf lokalen oder entfernten Dateisystemen.
### Individuelle Geschäftslogik
Führen Sie benutzerdefinierte Skripte oder Tools aus, die spezifisch für die Anforderungen Ihrer Organisation sind.
### Echtzeit-Datenzugriff
Rufen Sie Live-Daten von externen Systemen während der Workflow-Ausführung ab.
## Sicherheitsüberlegungen
- MCP-Server laufen mit den Berechtigungen des Benutzers, der sie konfiguriert hat
- Überprüfen Sie immer die MCP-Server-Quellen vor der Installation
- Verwenden Sie Umgebungsvariablen für sensible Konfigurationsdaten
- Überprüfen Sie die MCP-Server-Funktionen, bevor Sie Agenten Zugriff gewähren
## Fehlerbehebung
### MCP-Server wird nicht angezeigt
- Überprüfen Sie, ob die Serverkonfiguration korrekt ist
- Prüfen Sie, ob Sie über die erforderlichen Berechtigungen verfügen
- Stellen Sie sicher, dass der MCP-Server läuft und erreichbar ist
### Tool-Ausführungsfehler
- Überprüfen Sie, ob die Tool-Parameter korrekt formatiert sind
- Prüfen Sie die MCP-Server-Logs auf Fehlermeldungen
- Stellen Sie sicher, dass die erforderliche Authentifizierung konfiguriert ist
### Berechtigungsfehler
- Bestätigen Sie Ihre Workspace-Berechtigungsstufe
- Prüfen Sie, ob der MCP-Server zusätzliche Authentifizierung erfordert
- Überprüfen Sie, ob der Server ordnungsgemäß für Ihren Workspace konfiguriert ist

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