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`) |
|
||||
| `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
|
||||
|
||||
- **Framework**: [Next.js](https://nextjs.org/) (App Router)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { createElement, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { ExternalLink, Users } from 'lucide-react'
|
||||
import { Button, Combobox } from '@/components/emcn/components'
|
||||
@@ -203,7 +203,7 @@ export function CredentialSelector({
|
||||
if (!baseProviderConfig) {
|
||||
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) => {
|
||||
|
||||
@@ -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 { Button, Combobox } from '@/components/emcn/components'
|
||||
import {
|
||||
@@ -22,7 +22,7 @@ const getProviderIcon = (providerName: OAuthProvider) => {
|
||||
if (!baseProviderConfig) {
|
||||
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) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { createElement, useEffect, useRef, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { Check, ChevronDown, ExternalLink, Search } from 'lucide-react'
|
||||
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 h-9 w-9 flex-shrink-0 items-center justify-center overflow-hidden rounded-[6px] bg-[var(--surface-5)]'>
|
||||
{typeof service.icon === 'function'
|
||||
? service.icon({ className: 'h-4 w-4' })
|
||||
: service.icon}
|
||||
{createElement(service.icon, { className: 'h-4 w-4' })}
|
||||
</div>
|
||||
<div className='flex flex-col justify-center gap-[1px]'>
|
||||
<span className='font-medium text-[14px]'>{service.name}</span>
|
||||
|
||||
@@ -2417,4 +2417,177 @@ describe('EdgeManager', () => {
|
||||
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) {
|
||||
if (!this.isControlEdge(outgoingEdge.sourceHandle)) {
|
||||
if (!this.isBackwardsEdge(outgoingEdge.sourceHandle)) {
|
||||
this.deactivateEdgeAndDescendants(
|
||||
targetId,
|
||||
outgoingEdge.target,
|
||||
|
||||
@@ -325,18 +325,6 @@ const nextConfig: NextConfig = {
|
||||
|
||||
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
|
||||
|
||||
@@ -134,6 +134,24 @@ function handleSecurityFiltering(request: NextRequest): NextResponse | null {
|
||||
export async function proxy(request: NextRequest) {
|
||||
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 hasActiveSession = isAuthDisabled || !!sessionCookie
|
||||
|
||||
@@ -195,6 +213,7 @@ export async function proxy(request: NextRequest) {
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
'/ingest/:path*', // PostHog proxy for session recording
|
||||
'/', // Root path for self-hosted redirect logic
|
||||
'/terms', // Whitelabel terms redirect
|
||||
'/privacy', // Whitelabel privacy redirect
|
||||
|
||||
Reference in New Issue
Block a user