mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-29 16:58:11 -05:00
Compare commits
5 Commits
v0.5.75
...
fix/execut
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
691cff9790 | ||
|
|
85130f47f4 | ||
|
|
ae17c90bdf | ||
|
|
1256a15266 | ||
|
|
0b2b7ed9c8 |
25
README.md
25
README.md
@@ -172,31 +172,6 @@ Key environment variables for self-hosted deployments. See [`.env.example`](apps
|
|||||||
| `API_ENCRYPTION_KEY` | Yes | Encrypts API keys (`openssl rand -hex 32`) |
|
| `API_ENCRYPTION_KEY` | Yes | Encrypts API keys (`openssl rand -hex 32`) |
|
||||||
| `COPILOT_API_KEY` | No | API key from sim.ai for Copilot features |
|
| `COPILOT_API_KEY` | No | API key from sim.ai for Copilot features |
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Ollama models not showing in dropdown (Docker)
|
|
||||||
|
|
||||||
If you're running Ollama on your host machine and Sim in Docker, change `OLLAMA_URL` from `localhost` to `host.docker.internal`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
OLLAMA_URL=http://host.docker.internal:11434 docker compose -f docker-compose.prod.yml up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
See [Using an External Ollama Instance](#using-an-external-ollama-instance) for details.
|
|
||||||
|
|
||||||
### Database connection issues
|
|
||||||
|
|
||||||
Ensure PostgreSQL has the pgvector extension installed. When using Docker, wait for the database to be healthy before running migrations.
|
|
||||||
|
|
||||||
### Port conflicts
|
|
||||||
|
|
||||||
If ports 3000, 3002, or 5432 are in use, configure alternatives:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Custom ports
|
|
||||||
NEXT_PUBLIC_APP_URL=http://localhost:3100 POSTGRES_PORT=5433 docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
## Tech Stack
|
## Tech Stack
|
||||||
|
|
||||||
- **Framework**: [Next.js](https://nextjs.org/) (App Router)
|
- **Framework**: [Next.js](https://nextjs.org/) (App Router)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
import { createElement, useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { ExternalLink, Users } from 'lucide-react'
|
import { ExternalLink, Users } from 'lucide-react'
|
||||||
import { Button, Combobox } from '@/components/emcn/components'
|
import { Button, Combobox } from '@/components/emcn/components'
|
||||||
@@ -203,7 +203,7 @@ export function CredentialSelector({
|
|||||||
if (!baseProviderConfig) {
|
if (!baseProviderConfig) {
|
||||||
return <ExternalLink className='h-3 w-3' />
|
return <ExternalLink className='h-3 w-3' />
|
||||||
}
|
}
|
||||||
return baseProviderConfig.icon({ className: 'h-3 w-3' })
|
return createElement(baseProviderConfig.icon, { className: 'h-3 w-3' })
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const getProviderName = useCallback((providerName: OAuthProvider) => {
|
const getProviderName = useCallback((providerName: OAuthProvider) => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
import { createElement, useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import { ExternalLink } from 'lucide-react'
|
import { ExternalLink } from 'lucide-react'
|
||||||
import { Button, Combobox } from '@/components/emcn/components'
|
import { Button, Combobox } from '@/components/emcn/components'
|
||||||
import {
|
import {
|
||||||
@@ -22,7 +22,7 @@ const getProviderIcon = (providerName: OAuthProvider) => {
|
|||||||
if (!baseProviderConfig) {
|
if (!baseProviderConfig) {
|
||||||
return <ExternalLink className='h-3 w-3' />
|
return <ExternalLink className='h-3 w-3' />
|
||||||
}
|
}
|
||||||
return baseProviderConfig.icon({ className: 'h-3 w-3' })
|
return createElement(baseProviderConfig.icon, { className: 'h-3 w-3' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const getProviderName = (providerName: OAuthProvider) => {
|
const getProviderName = (providerName: OAuthProvider) => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { createElement, useEffect, useRef, useState } from 'react'
|
||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { Check, ChevronDown, ExternalLink, Search } from 'lucide-react'
|
import { Check, ChevronDown, ExternalLink, Search } from 'lucide-react'
|
||||||
import { useRouter, useSearchParams } from 'next/navigation'
|
import { useRouter, useSearchParams } from 'next/navigation'
|
||||||
@@ -339,9 +339,7 @@ export function Integrations({ onOpenChange, registerCloseHandler }: Integration
|
|||||||
>
|
>
|
||||||
<div className='flex items-center gap-[12px]'>
|
<div className='flex items-center gap-[12px]'>
|
||||||
<div className='flex h-9 w-9 flex-shrink-0 items-center justify-center overflow-hidden rounded-[6px] bg-[var(--surface-5)]'>
|
<div className='flex h-9 w-9 flex-shrink-0 items-center justify-center overflow-hidden rounded-[6px] bg-[var(--surface-5)]'>
|
||||||
{typeof service.icon === 'function'
|
{createElement(service.icon, { className: 'h-4 w-4' })}
|
||||||
? service.icon({ className: 'h-4 w-4' })
|
|
||||||
: service.icon}
|
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-col justify-center gap-[1px]'>
|
<div className='flex flex-col justify-center gap-[1px]'>
|
||||||
<span className='font-medium text-[14px]'>{service.name}</span>
|
<span className='font-medium text-[14px]'>{service.name}</span>
|
||||||
|
|||||||
@@ -2417,4 +2417,177 @@ describe('EdgeManager', () => {
|
|||||||
expect(successReady).toContain(targetId)
|
expect(successReady).toContain(targetId)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Condition with loop downstream - deactivation propagation', () => {
|
||||||
|
it('should deactivate nodes after loop when condition branch containing loop is deactivated', () => {
|
||||||
|
// Scenario: condition → (if) → sentinel_start → loopBody → sentinel_end → (loop_exit) → after_loop
|
||||||
|
// → (else) → other_branch
|
||||||
|
// When condition takes "else" path, the entire if-branch including nodes after the loop should be deactivated
|
||||||
|
const conditionId = 'condition'
|
||||||
|
const sentinelStartId = 'sentinel-start'
|
||||||
|
const loopBodyId = 'loop-body'
|
||||||
|
const sentinelEndId = 'sentinel-end'
|
||||||
|
const afterLoopId = 'after-loop'
|
||||||
|
const otherBranchId = 'other-branch'
|
||||||
|
|
||||||
|
const conditionNode = createMockNode(conditionId, [
|
||||||
|
{ target: sentinelStartId, sourceHandle: 'condition-if' },
|
||||||
|
{ target: otherBranchId, sourceHandle: 'condition-else' },
|
||||||
|
])
|
||||||
|
|
||||||
|
const sentinelStartNode = createMockNode(
|
||||||
|
sentinelStartId,
|
||||||
|
[{ target: loopBodyId }],
|
||||||
|
[conditionId]
|
||||||
|
)
|
||||||
|
|
||||||
|
const loopBodyNode = createMockNode(
|
||||||
|
loopBodyId,
|
||||||
|
[{ target: sentinelEndId }],
|
||||||
|
[sentinelStartId]
|
||||||
|
)
|
||||||
|
|
||||||
|
const sentinelEndNode = createMockNode(
|
||||||
|
sentinelEndId,
|
||||||
|
[
|
||||||
|
{ target: sentinelStartId, sourceHandle: 'loop_continue' },
|
||||||
|
{ target: afterLoopId, sourceHandle: 'loop_exit' },
|
||||||
|
],
|
||||||
|
[loopBodyId]
|
||||||
|
)
|
||||||
|
|
||||||
|
const afterLoopNode = createMockNode(afterLoopId, [], [sentinelEndId])
|
||||||
|
const otherBranchNode = createMockNode(otherBranchId, [], [conditionId])
|
||||||
|
|
||||||
|
const nodes = new Map<string, DAGNode>([
|
||||||
|
[conditionId, conditionNode],
|
||||||
|
[sentinelStartId, sentinelStartNode],
|
||||||
|
[loopBodyId, loopBodyNode],
|
||||||
|
[sentinelEndId, sentinelEndNode],
|
||||||
|
[afterLoopId, afterLoopNode],
|
||||||
|
[otherBranchId, otherBranchNode],
|
||||||
|
])
|
||||||
|
|
||||||
|
const dag = createMockDAG(nodes)
|
||||||
|
const edgeManager = new EdgeManager(dag)
|
||||||
|
|
||||||
|
// Condition selects "else" branch, deactivating the "if" branch (which contains the loop)
|
||||||
|
const readyNodes = edgeManager.processOutgoingEdges(conditionNode, { selectedOption: 'else' })
|
||||||
|
|
||||||
|
// Only otherBranch should be ready
|
||||||
|
expect(readyNodes).toContain(otherBranchId)
|
||||||
|
expect(readyNodes).not.toContain(sentinelStartId)
|
||||||
|
|
||||||
|
// afterLoop should NOT be ready - its incoming edge from sentinel_end should be deactivated
|
||||||
|
expect(readyNodes).not.toContain(afterLoopId)
|
||||||
|
|
||||||
|
// Verify that countActiveIncomingEdges returns 0 for afterLoop
|
||||||
|
// (meaning the loop_exit edge was properly deactivated)
|
||||||
|
// Note: isNodeReady returns true when all edges are deactivated (no pending deps),
|
||||||
|
// but the node won't be in readyNodes since it wasn't reached via an active path
|
||||||
|
expect(edgeManager.isNodeReady(afterLoopNode)).toBe(true) // All edges deactivated = no blocking deps
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should deactivate nodes after parallel when condition branch containing parallel is deactivated', () => {
|
||||||
|
// Similar scenario with parallel instead of loop
|
||||||
|
const conditionId = 'condition'
|
||||||
|
const parallelStartId = 'parallel-start'
|
||||||
|
const parallelBodyId = 'parallel-body'
|
||||||
|
const parallelEndId = 'parallel-end'
|
||||||
|
const afterParallelId = 'after-parallel'
|
||||||
|
const otherBranchId = 'other-branch'
|
||||||
|
|
||||||
|
const conditionNode = createMockNode(conditionId, [
|
||||||
|
{ target: parallelStartId, sourceHandle: 'condition-if' },
|
||||||
|
{ target: otherBranchId, sourceHandle: 'condition-else' },
|
||||||
|
])
|
||||||
|
|
||||||
|
const parallelStartNode = createMockNode(
|
||||||
|
parallelStartId,
|
||||||
|
[{ target: parallelBodyId }],
|
||||||
|
[conditionId]
|
||||||
|
)
|
||||||
|
|
||||||
|
const parallelBodyNode = createMockNode(
|
||||||
|
parallelBodyId,
|
||||||
|
[{ target: parallelEndId }],
|
||||||
|
[parallelStartId]
|
||||||
|
)
|
||||||
|
|
||||||
|
const parallelEndNode = createMockNode(
|
||||||
|
parallelEndId,
|
||||||
|
[{ target: afterParallelId, sourceHandle: 'parallel_exit' }],
|
||||||
|
[parallelBodyId]
|
||||||
|
)
|
||||||
|
|
||||||
|
const afterParallelNode = createMockNode(afterParallelId, [], [parallelEndId])
|
||||||
|
const otherBranchNode = createMockNode(otherBranchId, [], [conditionId])
|
||||||
|
|
||||||
|
const nodes = new Map<string, DAGNode>([
|
||||||
|
[conditionId, conditionNode],
|
||||||
|
[parallelStartId, parallelStartNode],
|
||||||
|
[parallelBodyId, parallelBodyNode],
|
||||||
|
[parallelEndId, parallelEndNode],
|
||||||
|
[afterParallelId, afterParallelNode],
|
||||||
|
[otherBranchId, otherBranchNode],
|
||||||
|
])
|
||||||
|
|
||||||
|
const dag = createMockDAG(nodes)
|
||||||
|
const edgeManager = new EdgeManager(dag)
|
||||||
|
|
||||||
|
// Condition selects "else" branch
|
||||||
|
const readyNodes = edgeManager.processOutgoingEdges(conditionNode, { selectedOption: 'else' })
|
||||||
|
|
||||||
|
expect(readyNodes).toContain(otherBranchId)
|
||||||
|
expect(readyNodes).not.toContain(parallelStartId)
|
||||||
|
expect(readyNodes).not.toContain(afterParallelId)
|
||||||
|
// isNodeReady returns true when all edges are deactivated (no pending deps)
|
||||||
|
expect(edgeManager.isNodeReady(afterParallelNode)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should still correctly handle normal loop exit (not deactivate when loop runs)', () => {
|
||||||
|
// When a loop actually executes and exits normally, after_loop should become ready
|
||||||
|
const sentinelStartId = 'sentinel-start'
|
||||||
|
const loopBodyId = 'loop-body'
|
||||||
|
const sentinelEndId = 'sentinel-end'
|
||||||
|
const afterLoopId = 'after-loop'
|
||||||
|
|
||||||
|
const sentinelStartNode = createMockNode(sentinelStartId, [{ target: loopBodyId }])
|
||||||
|
|
||||||
|
const loopBodyNode = createMockNode(
|
||||||
|
loopBodyId,
|
||||||
|
[{ target: sentinelEndId }],
|
||||||
|
[sentinelStartId]
|
||||||
|
)
|
||||||
|
|
||||||
|
const sentinelEndNode = createMockNode(
|
||||||
|
sentinelEndId,
|
||||||
|
[
|
||||||
|
{ target: sentinelStartId, sourceHandle: 'loop_continue' },
|
||||||
|
{ target: afterLoopId, sourceHandle: 'loop_exit' },
|
||||||
|
],
|
||||||
|
[loopBodyId]
|
||||||
|
)
|
||||||
|
|
||||||
|
const afterLoopNode = createMockNode(afterLoopId, [], [sentinelEndId])
|
||||||
|
|
||||||
|
const nodes = new Map<string, DAGNode>([
|
||||||
|
[sentinelStartId, sentinelStartNode],
|
||||||
|
[loopBodyId, loopBodyNode],
|
||||||
|
[sentinelEndId, sentinelEndNode],
|
||||||
|
[afterLoopId, afterLoopNode],
|
||||||
|
])
|
||||||
|
|
||||||
|
const dag = createMockDAG(nodes)
|
||||||
|
const edgeManager = new EdgeManager(dag)
|
||||||
|
|
||||||
|
// Simulate sentinel_end completing with loop_exit (loop is done)
|
||||||
|
const readyNodes = edgeManager.processOutgoingEdges(sentinelEndNode, {
|
||||||
|
selectedRoute: 'loop_exit',
|
||||||
|
})
|
||||||
|
|
||||||
|
// afterLoop should be ready
|
||||||
|
expect(readyNodes).toContain(afterLoopId)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -243,7 +243,7 @@ export class EdgeManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const [, outgoingEdge] of targetNode.outgoingEdges) {
|
for (const [, outgoingEdge] of targetNode.outgoingEdges) {
|
||||||
if (!this.isControlEdge(outgoingEdge.sourceHandle)) {
|
if (!this.isBackwardsEdge(outgoingEdge.sourceHandle)) {
|
||||||
this.deactivateEdgeAndDescendants(
|
this.deactivateEdgeAndDescendants(
|
||||||
targetId,
|
targetId,
|
||||||
outgoingEdge.target,
|
outgoingEdge.target,
|
||||||
|
|||||||
@@ -325,18 +325,6 @@ const nextConfig: NextConfig = {
|
|||||||
|
|
||||||
return redirects
|
return redirects
|
||||||
},
|
},
|
||||||
async rewrites() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
source: '/ingest/static/:path*',
|
|
||||||
destination: 'https://us-assets.i.posthog.com/static/:path*',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
source: '/ingest/:path*',
|
|
||||||
destination: 'https://us.i.posthog.com/:path*',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default nextConfig
|
export default nextConfig
|
||||||
|
|||||||
@@ -134,6 +134,24 @@ function handleSecurityFiltering(request: NextRequest): NextResponse | null {
|
|||||||
export async function proxy(request: NextRequest) {
|
export async function proxy(request: NextRequest) {
|
||||||
const url = request.nextUrl
|
const url = request.nextUrl
|
||||||
|
|
||||||
|
if (url.pathname.startsWith('/ingest/')) {
|
||||||
|
const hostname = url.pathname.startsWith('/ingest/static/')
|
||||||
|
? 'us-assets.i.posthog.com'
|
||||||
|
: 'us.i.posthog.com'
|
||||||
|
|
||||||
|
const targetPath = url.pathname.replace(/^\/ingest/, '')
|
||||||
|
const targetUrl = `https://${hostname}${targetPath}${url.search}`
|
||||||
|
|
||||||
|
return NextResponse.rewrite(new URL(targetUrl), {
|
||||||
|
request: {
|
||||||
|
headers: new Headers({
|
||||||
|
...Object.fromEntries(request.headers),
|
||||||
|
host: hostname,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const sessionCookie = getSessionCookie(request)
|
const sessionCookie = getSessionCookie(request)
|
||||||
const hasActiveSession = isAuthDisabled || !!sessionCookie
|
const hasActiveSession = isAuthDisabled || !!sessionCookie
|
||||||
|
|
||||||
@@ -195,6 +213,7 @@ export async function proxy(request: NextRequest) {
|
|||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
matcher: [
|
matcher: [
|
||||||
|
'/ingest/:path*', // PostHog proxy for session recording
|
||||||
'/', // Root path for self-hosted redirect logic
|
'/', // Root path for self-hosted redirect logic
|
||||||
'/terms', // Whitelabel terms redirect
|
'/terms', // Whitelabel terms redirect
|
||||||
'/privacy', // Whitelabel privacy redirect
|
'/privacy', // Whitelabel privacy redirect
|
||||||
|
|||||||
Reference in New Issue
Block a user