Compare commits
1 Commits
fix/codege
...
fix/s-tool
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d80456cb6 |
@@ -44,7 +44,7 @@ services:
|
|||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
memory: 1G
|
memory: 4G
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=development
|
- NODE_ENV=development
|
||||||
- DATABASE_URL=postgresql://postgres:postgres@db:5432/simstudio
|
- DATABASE_URL=postgresql://postgres:postgres@db:5432/simstudio
|
||||||
|
|||||||
5
.github/workflows/ci.yml
vendored
@@ -27,11 +27,10 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Extract version from commit message
|
- name: Extract version from commit message
|
||||||
id: extract
|
id: extract
|
||||||
env:
|
|
||||||
COMMIT_MSG: ${{ github.event.head_commit.message }}
|
|
||||||
run: |
|
run: |
|
||||||
|
COMMIT_MSG="${{ github.event.head_commit.message }}"
|
||||||
# Only tag versions on main branch
|
# Only tag versions on main branch
|
||||||
if [ "$GITHUB_REF" = "refs/heads/main" ] && [[ "$COMMIT_MSG" =~ ^(v[0-9]+\.[0-9]+\.[0-9]+): ]]; then
|
if [ "${{ github.ref }}" = "refs/heads/main" ] && [[ "$COMMIT_MSG" =~ ^(v[0-9]+\.[0-9]+\.[0-9]+): ]]; then
|
||||||
VERSION="${BASH_REMATCH[1]}"
|
VERSION="${BASH_REMATCH[1]}"
|
||||||
echo "version=${VERSION}" >> $GITHUB_OUTPUT
|
echo "version=${VERSION}" >> $GITHUB_OUTPUT
|
||||||
echo "is_release=true" >> $GITHUB_OUTPUT
|
echo "is_release=true" >> $GITHUB_OUTPUT
|
||||||
|
|||||||
@@ -119,19 +119,6 @@ aside#nd-sidebar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hide TOC popover on tablet/medium screens (768px - 1279px) */
|
|
||||||
/* Keeps it visible on mobile (<768px) for easy navigation */
|
|
||||||
/* Desktop (>=1280px) already hides it via fumadocs xl:hidden */
|
|
||||||
@media (min-width: 768px) and (max-width: 1279px) {
|
|
||||||
#nd-docs-layout {
|
|
||||||
--fd-toc-popover-height: 0px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-toc-popover] {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Desktop only: Apply custom navbar offset, sidebar width and margin offsets */
|
/* Desktop only: Apply custom navbar offset, sidebar width and margin offsets */
|
||||||
/* On mobile, let fumadocs handle the layout natively */
|
/* On mobile, let fumadocs handle the layout natively */
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import { getAssetUrl } from '@/lib/utils'
|
|
||||||
|
|
||||||
interface ActionImageProps {
|
|
||||||
src: string
|
|
||||||
alt: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ActionVideoProps {
|
|
||||||
src: string
|
|
||||||
alt: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ActionImage({ src, alt }: ActionImageProps) {
|
|
||||||
const resolvedSrc = getAssetUrl(src.startsWith('/') ? src.slice(1) : src)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<img
|
|
||||||
src={resolvedSrc}
|
|
||||||
alt={alt}
|
|
||||||
className='inline-block w-full max-w-[200px] rounded border border-neutral-200 dark:border-neutral-700'
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ActionVideo({ src, alt }: ActionVideoProps) {
|
|
||||||
const resolvedSrc = getAssetUrl(src.startsWith('/') ? src.slice(1) : src)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<video
|
|
||||||
src={resolvedSrc}
|
|
||||||
autoPlay
|
|
||||||
loop
|
|
||||||
muted
|
|
||||||
playsInline
|
|
||||||
className='inline-block w-full max-w-[200px] rounded border border-neutral-200 dark:border-neutral-700'
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -10,20 +10,12 @@ Stellen Sie Sim auf Ihrer eigenen Infrastruktur mit Docker oder Kubernetes berei
|
|||||||
|
|
||||||
## Anforderungen
|
## Anforderungen
|
||||||
|
|
||||||
| Ressource | Klein | Standard | Produktion |
|
| Ressource | Minimum | Empfohlen |
|
||||||
|----------|-------|----------|------------|
|
|----------|---------|-------------|
|
||||||
| CPU | 2 Kerne | 4 Kerne | 8+ Kerne |
|
| CPU | 2 Kerne | 4+ Kerne |
|
||||||
| RAM | 12 GB | 16 GB | 32+ GB |
|
| RAM | 12 GB | 16+ GB |
|
||||||
| Speicher | 20 GB SSD | 50 GB SSD | 100+ GB SSD |
|
| Speicher | 20 GB SSD | 50+ GB SSD |
|
||||||
| Docker | 20.10+ | 20.10+ | Neueste Version |
|
| Docker | 20.10+ | Neueste Version |
|
||||||
|
|
||||||
**Klein**: Entwicklung, Tests, Einzelnutzer (1-5 Nutzer)
|
|
||||||
**Standard**: Teams (5-50 Nutzer), moderate Arbeitslasten
|
|
||||||
**Produktion**: Große Teams (50+ Nutzer), Hochverfügbarkeit, intensive Workflow-Ausführung
|
|
||||||
|
|
||||||
<Callout type="info">
|
|
||||||
Die Ressourcenanforderungen werden durch Workflow-Ausführung (isolated-vm Sandboxing), Dateiverarbeitung (In-Memory-Dokumentenparsing) und Vektoroperationen (pgvector) bestimmt. Arbeitsspeicher ist typischerweise der limitierende Faktor, nicht CPU. Produktionsdaten zeigen, dass die Hauptanwendung durchschnittlich 4-8 GB und bei hoher Last bis zu 12 GB benötigt.
|
|
||||||
</Callout>
|
|
||||||
|
|
||||||
## Schnellstart
|
## Schnellstart
|
||||||
|
|
||||||
|
|||||||
@@ -5,24 +5,44 @@ title: Copilot
|
|||||||
import { Callout } from 'fumadocs-ui/components/callout'
|
import { Callout } from 'fumadocs-ui/components/callout'
|
||||||
import { Card, Cards } from 'fumadocs-ui/components/card'
|
import { Card, Cards } from 'fumadocs-ui/components/card'
|
||||||
import { Image } from '@/components/ui/image'
|
import { Image } from '@/components/ui/image'
|
||||||
import { MessageCircle, Hammer, Zap, Globe, Paperclip, History, RotateCcw, Brain } from 'lucide-react'
|
import { MessageCircle, Package, Zap, Infinity as InfinityIcon, Brain, BrainCircuit } from 'lucide-react'
|
||||||
|
|
||||||
Copilot is your in-editor assistant that helps you build and edit workflows. It can:
|
Copilot is your in-editor assistant that helps you build and edit workflows with Sim Copilot, as well as understand and improve them. It can:
|
||||||
|
|
||||||
- **Explain**: Answer questions about Sim and your current workflow
|
- **Explain**: Answer questions about Sim and your current workflow
|
||||||
- **Guide**: Suggest edits and best practices
|
- **Guide**: Suggest edits and best practices
|
||||||
- **Build**: Add blocks, wire connections, and configure settings
|
- **Edit**: Make changes to blocks, connections, and settings when you approve
|
||||||
- **Debug**: Analyze execution issues and optimize performance
|
|
||||||
|
|
||||||
<Callout type="info">
|
<Callout type="info">
|
||||||
Copilot is a Sim-managed service. For self-hosted deployments:
|
Copilot is a Sim-managed service. For self-hosted deployments, generate a Copilot API key in the hosted app (sim.ai → Settings → Copilot)
|
||||||
1. Go to [sim.ai](https://sim.ai) → Settings → Copilot and generate a Copilot API key
|
1. Go to [sim.ai](https://sim.ai) → Settings → Copilot and generate a Copilot API key
|
||||||
2. Set `COPILOT_API_KEY` in your self-hosted environment
|
2. Set `COPILOT_API_KEY` in your self-hosted environment to that value
|
||||||
</Callout>
|
</Callout>
|
||||||
|
|
||||||
## Modes
|
## Context Menu (@)
|
||||||
|
|
||||||
Switch between modes using the mode selector at the bottom of the input area.
|
Use the `@` symbol to reference various resources and give Copilot more context about your workspace:
|
||||||
|
|
||||||
|
<Image
|
||||||
|
src="/static/copilot/copilot-menu.png"
|
||||||
|
alt="Copilot context menu showing available reference options"
|
||||||
|
width={600}
|
||||||
|
height={400}
|
||||||
|
/>
|
||||||
|
|
||||||
|
The `@` menu provides access to:
|
||||||
|
- **Chats**: Reference previous copilot conversations
|
||||||
|
- **All workflows**: Reference any workflow in your workspace
|
||||||
|
- **Workflow Blocks**: Reference specific blocks from workflows
|
||||||
|
- **Blocks**: Reference block types and templates
|
||||||
|
- **Knowledge**: Reference your uploaded documents and knowledgebase
|
||||||
|
- **Docs**: Reference Sim documentation
|
||||||
|
- **Templates**: Reference workflow templates
|
||||||
|
- **Logs**: Reference execution logs and results
|
||||||
|
|
||||||
|
This contextual information helps Copilot provide more accurate and relevant assistance for your specific use case.
|
||||||
|
|
||||||
|
## Modes
|
||||||
|
|
||||||
<Cards>
|
<Cards>
|
||||||
<Card
|
<Card
|
||||||
@@ -40,153 +60,113 @@ Switch between modes using the mode selector at the bottom of the input area.
|
|||||||
<Card
|
<Card
|
||||||
title={
|
title={
|
||||||
<span className="inline-flex items-center gap-2">
|
<span className="inline-flex items-center gap-2">
|
||||||
<Hammer className="h-4 w-4 text-muted-foreground" />
|
<Package className="h-4 w-4 text-muted-foreground" />
|
||||||
Build
|
Agent
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="m-0 text-sm">
|
<div className="m-0 text-sm">
|
||||||
Workflow building mode. Copilot can add blocks, wire connections, edit configurations, and debug issues.
|
Build-and-edit mode. Copilot proposes specific edits (add blocks, wire variables, tweak settings) and applies them when you approve.
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</Cards>
|
</Cards>
|
||||||
|
|
||||||
## Models
|
<div className="flex justify-center">
|
||||||
|
<Image
|
||||||
|
src="/static/copilot/copilot-mode.png"
|
||||||
|
alt="Copilot mode selection interface"
|
||||||
|
width={600}
|
||||||
|
height={400}
|
||||||
|
className="my-6"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
Select your preferred AI model using the model selector at the bottom right of the input area.
|
## Depth Levels
|
||||||
|
|
||||||
**Available Models:**
|
<Cards>
|
||||||
- Claude 4.5 Opus, Sonnet (default), Haiku
|
<Card
|
||||||
- GPT 5.2 Codex, Pro
|
title={
|
||||||
- Gemini 3 Pro
|
<span className="inline-flex items-center gap-2">
|
||||||
|
<Zap className="h-4 w-4 text-muted-foreground" />
|
||||||
|
Fast
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="m-0 text-sm">Quickest and cheapest. Best for small edits, simple workflows, and minor tweaks.</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">Balanced speed and reasoning. Recommended default for most tasks.</div>
|
||||||
|
</Card>
|
||||||
|
<Card
|
||||||
|
title={
|
||||||
|
<span className="inline-flex items-center gap-2">
|
||||||
|
<Brain className="h-4 w-4 text-muted-foreground" />
|
||||||
|
Advanced
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="m-0 text-sm">More reasoning for larger workflows and complex edits while staying performant.</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">Maximum reasoning for deep planning, debugging, and complex architectural changes.</div>
|
||||||
|
</Card>
|
||||||
|
</Cards>
|
||||||
|
|
||||||
Choose based on your needs: faster models for simple tasks, more capable models for complex workflows.
|
### Mode Selection Interface
|
||||||
|
|
||||||
## Context Menu (@)
|
You can easily switch between different reasoning modes using the mode selector in the Copilot interface:
|
||||||
|
|
||||||
Use the `@` symbol to reference resources and give Copilot more context:
|
<Image
|
||||||
|
src="/static/copilot/copilot-models.png"
|
||||||
|
alt="Copilot mode selection showing Advanced mode with MAX toggle"
|
||||||
|
width={600}
|
||||||
|
height={300}
|
||||||
|
/>
|
||||||
|
|
||||||
| Reference | Description |
|
The interface allows you to:
|
||||||
|-----------|-------------|
|
- **Select reasoning level**: Choose from Fast, Auto, Advanced, or Behemoth
|
||||||
| **Chats** | Previous copilot conversations |
|
- **Enable MAX mode**: Toggle for maximum reasoning capabilities when you need the most thorough analysis
|
||||||
| **Workflows** | Any workflow in your workspace |
|
- **See mode descriptions**: Understand what each mode is optimized for
|
||||||
| **Workflow Blocks** | Blocks in the current workflow |
|
|
||||||
| **Blocks** | Block types and templates |
|
|
||||||
| **Knowledge** | Uploaded documents and knowledge bases |
|
|
||||||
| **Docs** | Sim documentation |
|
|
||||||
| **Templates** | Workflow templates |
|
|
||||||
| **Logs** | Execution logs and results |
|
|
||||||
|
|
||||||
Type `@` in the input field to open the context menu, then search or browse to find what you need.
|
Choose your mode based on the complexity of your task - use Fast for simple questions and Behemoth for complex architectural changes.
|
||||||
|
|
||||||
## Slash Commands (/)
|
## Billing and Cost Calculation
|
||||||
|
|
||||||
Use slash commands for quick actions:
|
### How Costs Are Calculated
|
||||||
|
|
||||||
| Command | Description |
|
Copilot usage is billed per token from the underlying LLM:
|
||||||
|---------|-------------|
|
|
||||||
| `/fast` | Fast mode execution |
|
|
||||||
| `/research` | Research and exploration mode |
|
|
||||||
| `/actions` | Execute agent actions |
|
|
||||||
|
|
||||||
**Web Commands:**
|
- **Input tokens**: billed at the provider's base rate (**at-cost**)
|
||||||
|
- **Output tokens**: billed at **1.5×** the provider's base output rate
|
||||||
|
|
||||||
| Command | Description |
|
```javascript
|
||||||
|---------|-------------|
|
copilotCost = (inputTokens × inputPrice + outputTokens × (outputPrice × 1.5)) / 1,000,000
|
||||||
| `/search` | Search the web |
|
```
|
||||||
| `/read` | Read a specific URL |
|
|
||||||
| `/scrape` | Scrape web page content |
|
|
||||||
| `/crawl` | Crawl multiple pages |
|
|
||||||
|
|
||||||
Type `/` in the input field to see available commands.
|
| Component | Rate Applied |
|
||||||
|
|----------|----------------------|
|
||||||
|
| Input | inputPrice |
|
||||||
|
| Output | outputPrice × 1.5 |
|
||||||
|
|
||||||
## Chat Management
|
<Callout type="warning">
|
||||||
|
Pricing shown reflects rates as of September 4, 2025. Check provider documentation for current pricing.
|
||||||
### Starting a New Chat
|
</Callout>
|
||||||
|
|
||||||
Click the **+** button in the Copilot header to start a fresh conversation.
|
|
||||||
|
|
||||||
### Chat History
|
|
||||||
|
|
||||||
Click **History** to view previous conversations grouped by date. You can:
|
|
||||||
- Click a chat to resume it
|
|
||||||
- Delete chats you no longer need
|
|
||||||
|
|
||||||
### Editing Messages
|
|
||||||
|
|
||||||
Hover over any of your messages and click **Edit** to modify and resend it. This is useful for refining your prompts.
|
|
||||||
|
|
||||||
### Message Queue
|
|
||||||
|
|
||||||
If you send a message while Copilot is still responding, it gets queued. You can:
|
|
||||||
- View queued messages in the expandable queue panel
|
|
||||||
- Send a queued message immediately (aborts current response)
|
|
||||||
- Remove messages from the queue
|
|
||||||
|
|
||||||
## File Attachments
|
|
||||||
|
|
||||||
Click the attachment icon to upload files with your message. Supported file types include:
|
|
||||||
- Images (preview thumbnails shown)
|
|
||||||
- PDFs
|
|
||||||
- Text files, JSON, XML
|
|
||||||
- Other document formats
|
|
||||||
|
|
||||||
Files are displayed as clickable thumbnails that open in a new tab.
|
|
||||||
|
|
||||||
## Checkpoints & Changes
|
|
||||||
|
|
||||||
When Copilot makes changes to your workflow, it saves checkpoints so you can revert if needed.
|
|
||||||
|
|
||||||
### Viewing Checkpoints
|
|
||||||
|
|
||||||
Hover over a Copilot message and click the checkpoints icon to see saved workflow states for that message.
|
|
||||||
|
|
||||||
### Reverting Changes
|
|
||||||
|
|
||||||
Click **Revert** on any checkpoint to restore your workflow to that state. A confirmation dialog will warn that this action cannot be undone.
|
|
||||||
|
|
||||||
### Accepting Changes
|
|
||||||
|
|
||||||
When Copilot proposes changes, you can:
|
|
||||||
- **Accept**: Apply the proposed changes (`Mod+Shift+Enter`)
|
|
||||||
- **Reject**: Dismiss the changes and keep your current workflow
|
|
||||||
|
|
||||||
## Thinking Blocks
|
|
||||||
|
|
||||||
For complex requests, Copilot may show its reasoning process in expandable thinking blocks:
|
|
||||||
|
|
||||||
- Blocks auto-expand while Copilot is thinking
|
|
||||||
- Click to manually expand/collapse
|
|
||||||
- Shows duration of the thinking process
|
|
||||||
- Helps you understand how Copilot arrived at its solution
|
|
||||||
|
|
||||||
## Options Selection
|
|
||||||
|
|
||||||
When Copilot presents multiple options, you can select using:
|
|
||||||
|
|
||||||
| Control | Action |
|
|
||||||
|---------|--------|
|
|
||||||
| **1-9** | Select option by number |
|
|
||||||
| **Arrow Up/Down** | Navigate between options |
|
|
||||||
| **Enter** | Select highlighted option |
|
|
||||||
|
|
||||||
Selected options are highlighted; unselected options appear struck through.
|
|
||||||
|
|
||||||
## Keyboard Shortcuts
|
|
||||||
|
|
||||||
| Shortcut | Action |
|
|
||||||
|----------|--------|
|
|
||||||
| `@` | Open context menu |
|
|
||||||
| `/` | Open slash commands |
|
|
||||||
| `Arrow Up/Down` | Navigate menu items |
|
|
||||||
| `Enter` | Select menu item |
|
|
||||||
| `Esc` | Close menus |
|
|
||||||
| `Mod+Shift+Enter` | Accept Copilot changes |
|
|
||||||
|
|
||||||
## Usage Limits
|
|
||||||
|
|
||||||
Copilot usage is billed per token from the underlying LLM. If you reach your usage limit, Copilot will prompt you to increase your limit. You can add usage in increments ($50, $100) from your current base.
|
|
||||||
|
|
||||||
<Callout type="info">
|
<Callout type="info">
|
||||||
See the [Cost Calculation page](/execution/costs) for billing details.
|
Model prices are per million tokens. The calculation divides by 1,000,000 to get the actual cost. See <a href="/execution/costs">the Cost Calculation page</a> for background and examples.
|
||||||
</Callout>
|
</Callout>
|
||||||
|
|
||||||
|
|||||||
@@ -34,8 +34,6 @@ Speed up your workflow building with these keyboard shortcuts and mouse controls
|
|||||||
| `Mod` + `V` | Paste blocks |
|
| `Mod` + `V` | Paste blocks |
|
||||||
| `Delete` or `Backspace` | Delete selected blocks or edges |
|
| `Delete` or `Backspace` | Delete selected blocks or edges |
|
||||||
| `Shift` + `L` | Auto-layout canvas |
|
| `Shift` + `L` | Auto-layout canvas |
|
||||||
| `Mod` + `Shift` + `F` | Fit to view |
|
|
||||||
| `Mod` + `Shift` + `Enter` | Accept Copilot changes |
|
|
||||||
|
|
||||||
## Panel Navigation
|
## Panel Navigation
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
"pages": [
|
"pages": [
|
||||||
"./introduction/index",
|
"./introduction/index",
|
||||||
"./getting-started/index",
|
"./getting-started/index",
|
||||||
"./quick-reference/index",
|
|
||||||
"triggers",
|
"triggers",
|
||||||
"blocks",
|
"blocks",
|
||||||
"tools",
|
"tools",
|
||||||
|
|||||||
@@ -1,375 +0,0 @@
|
|||||||
---
|
|
||||||
title: Quick Reference
|
|
||||||
description: Essential actions for navigating and using the Sim workflow editor
|
|
||||||
---
|
|
||||||
|
|
||||||
import { Callout } from 'fumadocs-ui/components/callout'
|
|
||||||
import { ActionImage, ActionVideo } from '@/components/ui/action-media'
|
|
||||||
|
|
||||||
A quick lookup for everyday actions in the Sim workflow editor. For keyboard shortcuts, see [Keyboard Shortcuts](/keyboard-shortcuts).
|
|
||||||
|
|
||||||
<Callout type="info">
|
|
||||||
**Mod** refers to `Cmd` on macOS and `Ctrl` on Windows/Linux.
|
|
||||||
</Callout>
|
|
||||||
|
|
||||||
## Workspaces
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr><th>Action</th><th>How</th><th>Preview</th></tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>Create a workspace</td>
|
|
||||||
<td>Click workspace dropdown → **New Workspace**</td>
|
|
||||||
<td><ActionVideo src="/static/quick-reference/create-workspace.mp4" alt="Create workspace" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Switch workspaces</td>
|
|
||||||
<td>Click workspace dropdown → Select workspace</td>
|
|
||||||
<td><ActionVideo src="/static/quick-reference/switch-workspace.mp4" alt="Switch workspaces" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Invite team members</td>
|
|
||||||
<td>Sidebar → **Invite**</td>
|
|
||||||
<td><ActionVideo src="/static/quick-reference/invite.mp4" alt="Invite team members" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Rename a workspace</td>
|
|
||||||
<td>Right-click workspace → **Rename**</td>
|
|
||||||
<td rowSpan={4}><ActionImage src="/static/quick-reference/workspace-context-menu.png" alt="Workspace context menu" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Duplicate a workspace</td>
|
|
||||||
<td>Right-click workspace → **Duplicate**</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Export a workspace</td>
|
|
||||||
<td>Right-click workspace → **Export**</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Delete a workspace</td>
|
|
||||||
<td>Right-click workspace → **Delete**</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
## Workflows
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr><th>Action</th><th>How</th><th>Preview</th></tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>Create a workflow</td>
|
|
||||||
<td>Click **+** button in sidebar</td>
|
|
||||||
<td><ActionImage src="/static/quick-reference/create-workflow.png" alt="Create workflow" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Reorder / move workflows</td>
|
|
||||||
<td>Drag workflow up/down or onto a folder</td>
|
|
||||||
<td><ActionVideo src="/static/quick-reference/reordering.mp4" alt="Reorder workflows" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Import a workflow</td>
|
|
||||||
<td>Click import button in sidebar → Select file</td>
|
|
||||||
<td><ActionImage src="/static/quick-reference/import-workflow.png" alt="Import workflow" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Multi-select workflows</td>
|
|
||||||
<td>`Mod+Click` or `Shift+Click` workflows in sidebar</td>
|
|
||||||
<td><ActionVideo src="/static/quick-reference/multiselect.mp4" alt="Multi-select workflows" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Open in new tab</td>
|
|
||||||
<td>Right-click workflow → **Open in New Tab**</td>
|
|
||||||
<td rowSpan={6}><ActionImage src="/static/quick-reference/workflow-context-menu.png" alt="Workflow context menu" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Rename a workflow</td>
|
|
||||||
<td>Right-click workflow → **Rename**</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Assign workflow color</td>
|
|
||||||
<td>Right-click workflow → **Change Color**</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Duplicate a workflow</td>
|
|
||||||
<td>Right-click workflow → **Duplicate**</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Export a workflow</td>
|
|
||||||
<td>Right-click workflow → **Export**</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Delete a workflow</td>
|
|
||||||
<td>Right-click workflow → **Delete**</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Rename a folder</td>
|
|
||||||
<td>Right-click folder → **Rename**</td>
|
|
||||||
<td rowSpan={6}><ActionImage src="/static/quick-reference/folder-context-menu.png" alt="Folder context menu" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Create workflow in folder</td>
|
|
||||||
<td>Right-click folder → **Create workflow**</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Create folder in folder</td>
|
|
||||||
<td>Right-click folder → **Create folder**</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Duplicate a folder</td>
|
|
||||||
<td>Right-click folder → **Duplicate**</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Export a folder</td>
|
|
||||||
<td>Right-click folder → **Export**</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Delete a folder</td>
|
|
||||||
<td>Right-click folder → **Delete**</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
## Blocks
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr><th>Action</th><th>How</th><th>Preview</th></tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>Add a block</td>
|
|
||||||
<td>Drag from Toolbar panel, or right-click canvas → **Add Block**</td>
|
|
||||||
<td><ActionVideo src="/static/quick-reference/add-block.mp4" alt="Add a block" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Multi-select blocks</td>
|
|
||||||
<td>`Mod+Click` additional blocks, or shift-drag to draw selection box</td>
|
|
||||||
<td><ActionVideo src="/static/quick-reference/multiselect-blocks.mp4" alt="Multi-select blocks" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Copy blocks</td>
|
|
||||||
<td>`Mod+C` with blocks selected</td>
|
|
||||||
<td rowSpan={2}><ActionVideo src="/static/quick-reference/copy-paste.mp4" alt="Copy and paste blocks" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Paste blocks</td>
|
|
||||||
<td>`Mod+V` to paste copied blocks</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Duplicate blocks</td>
|
|
||||||
<td>Right-click → **Duplicate**</td>
|
|
||||||
<td><ActionVideo src="/static/quick-reference/duplicate-block.mp4" alt="Duplicate blocks" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Delete blocks</td>
|
|
||||||
<td>`Delete` or `Backspace` key, or right-click → **Delete**</td>
|
|
||||||
<td><ActionImage src="/static/quick-reference/delete-block.png" alt="Delete block" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Rename a block</td>
|
|
||||||
<td>Click block name in header, or edit in the Editor panel</td>
|
|
||||||
<td><ActionVideo src="/static/quick-reference/rename-block.mp4" alt="Rename a block" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Enable/Disable a block</td>
|
|
||||||
<td>Right-click → **Enable/Disable**</td>
|
|
||||||
<td><ActionImage src="/static/quick-reference/disable-block.png" alt="Disable block" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Toggle handle orientation</td>
|
|
||||||
<td>Right-click → **Toggle Handles**</td>
|
|
||||||
<td><ActionVideo src="/static/quick-reference/toggle-handles.mp4" alt="Toggle handle orientation" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Configure a block</td>
|
|
||||||
<td>Select block → use Editor panel on right</td>
|
|
||||||
<td><ActionVideo src="/static/quick-reference/configure-block.mp4" alt="Configure a block" /></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
## Connections
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr><th>Action</th><th>How</th><th>Preview</th></tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>Create a connection</td>
|
|
||||||
<td>Drag from output handle to input handle</td>
|
|
||||||
<td><ActionVideo src="/static/quick-reference/connect-blocks.mp4" alt="Connect blocks" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Delete a connection</td>
|
|
||||||
<td>Click edge to select → `Delete` key</td>
|
|
||||||
<td><ActionVideo src="/static/quick-reference/delete-connection.mp4" alt="Delete connection" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Use output in another block</td>
|
|
||||||
<td>Drag connection tag into input field</td>
|
|
||||||
<td><ActionVideo src="/static/quick-reference/connection-tag.mp4" alt="Use connection tag" /></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
## Panels & Views
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr><th>Action</th><th>How</th><th>Preview</th></tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>Search toolbar</td>
|
|
||||||
<td>`Mod+F`</td>
|
|
||||||
<td><ActionVideo src="/static/quick-reference/search-toolbar.mp4" alt="Search toolbar" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Search everything</td>
|
|
||||||
<td>`Mod+K`</td>
|
|
||||||
<td><ActionImage src="/static/quick-reference/search-everything.png" alt="Search everything" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Toggle manual mode</td>
|
|
||||||
<td>Click toggle button to switch between manual and selector</td>
|
|
||||||
<td><ActionImage src="/static/quick-reference/toggle-manual-mode.png" alt="Toggle manual mode" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Collapse/expand sidebar</td>
|
|
||||||
<td>Click collapse button on sidebar</td>
|
|
||||||
<td><ActionVideo src="/static/quick-reference/collapse-sidebar.mp4" alt="Collapse sidebar" /></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
## Running & Testing
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr><th>Action</th><th>How</th><th>Preview</th></tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>Run workflow</td>
|
|
||||||
<td>Click Run Workflow button or `Mod+Enter`</td>
|
|
||||||
<td><ActionImage src="/static/quick-reference/run-workflow.png" alt="Run workflow" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Stop workflow</td>
|
|
||||||
<td>Click Stop button or `Mod+Enter` while running</td>
|
|
||||||
<td><ActionImage src="/static/quick-reference/stop-workflow.png" alt="Stop workflow" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Test with chat</td>
|
|
||||||
<td>Use Chat panel on the right side</td>
|
|
||||||
<td><ActionImage src="/static/quick-reference/test-chat.png" alt="Test with chat" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Select output to view</td>
|
|
||||||
<td>Click dropdown in Chat panel → Select block output</td>
|
|
||||||
<td><ActionImage src="/static/quick-reference/output-select.png" alt="Select output to view" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Clear chat history</td>
|
|
||||||
<td>Click clear button in Chat panel</td>
|
|
||||||
<td><ActionImage src="/static/quick-reference/clear-chat.png" alt="Clear chat history" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>View execution logs</td>
|
|
||||||
<td>Open terminal panel at bottom, or `Mod+L`</td>
|
|
||||||
<td><ActionImage src="/static/quick-reference/terminal.png" alt="Execution logs terminal" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Filter logs by block or status</td>
|
|
||||||
<td>Click block filter in terminal or right-click log entry → **Filter by Block** or **Filter by Status**</td>
|
|
||||||
<td><ActionImage src="/static/quick-reference/filter-block.png" alt="Filter logs by block" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Search logs</td>
|
|
||||||
<td>Use search field in terminal or right-click log entry → **Search**</td>
|
|
||||||
<td><ActionImage src="/static/quick-reference/terminal-search.png" alt="Search logs" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Copy log entry</td>
|
|
||||||
<td>Clipboard Icon or Right-click log entry → **Copy**</td>
|
|
||||||
<td><ActionImage src="/static/quick-reference/copy-log.png" alt="Copy log entry" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Clear terminal</td>
|
|
||||||
<td>Trash icon or `Mod+D`</td>
|
|
||||||
<td><ActionImage src="/static/quick-reference/clear-terminal.png" alt="Clear terminal" /></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
## Deployment
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr><th>Action</th><th>How</th><th>Preview</th></tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>Deploy a workflow</td>
|
|
||||||
<td>Click **Deploy** button in panel</td>
|
|
||||||
<td><ActionImage src="/static/quick-reference/deploy.png" alt="Deploy workflow" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Update deployment</td>
|
|
||||||
<td>Click **Update** when changes are detected</td>
|
|
||||||
<td><ActionImage src="/static/quick-reference/update-deployment.png" alt="Update deployment" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>View deployment status</td>
|
|
||||||
<td>Check status indicator (Live/Update/Deploy) in Deploy tab</td>
|
|
||||||
<td><ActionImage src="/static/quick-reference/view-deployment.png" alt="View deployment status" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Revert deployment</td>
|
|
||||||
<td>Access previous versions in Deploy tab → **Promote to live**</td>
|
|
||||||
<td><ActionImage src="/static/quick-reference/promote-deployment.png" alt="Promote deployment to live" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Copy API endpoint</td>
|
|
||||||
<td>Deploy tab → Copy API endpoint URL</td>
|
|
||||||
<td><ActionImage src="/static/quick-reference/copy-api.png" alt="Copy API endpoint" /></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
## Variables
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr><th>Action</th><th>How</th><th>Preview</th></tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>Add / Edit / Delete workflow variable</td>
|
|
||||||
<td>Panel -> Variables -> **Add Variable**, click to edit, or delete icon</td>
|
|
||||||
<td><ActionImage src="/static/quick-reference/variables.png" alt="Variables panel" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Add environment variable</td>
|
|
||||||
<td>Settings → **Environment Variables** → **Add**</td>
|
|
||||||
<td><ActionImage src="/static/quick-reference/add-env-variable.png" alt="Add environment variable" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Reference a workflow variable</td>
|
|
||||||
<td>Use `<blockName.itemName>` syntax in block inputs</td>
|
|
||||||
<td><ActionImage src="/static/quick-reference/variable-reference.png" alt="Reference workflow variable" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Reference an environment variable</td>
|
|
||||||
<td>Use `{{ENV_VAR}}` syntax in block inputs</td>
|
|
||||||
<td><ActionImage src="/static/quick-reference/env-variable-reference.png" alt="Reference environment variable" /></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
@@ -16,20 +16,12 @@ Deploy Sim on your own infrastructure with Docker or Kubernetes.
|
|||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
| Resource | Small | Standard | Production |
|
| Resource | Minimum | Recommended |
|
||||||
|----------|-------|----------|------------|
|
|----------|---------|-------------|
|
||||||
| CPU | 2 cores | 4 cores | 8+ cores |
|
| CPU | 2 cores | 4+ cores |
|
||||||
| RAM | 12 GB | 16 GB | 32+ GB |
|
| RAM | 12 GB | 16+ GB |
|
||||||
| Storage | 20 GB SSD | 50 GB SSD | 100+ GB SSD |
|
| Storage | 20 GB SSD | 50+ GB SSD |
|
||||||
| Docker | 20.10+ | 20.10+ | Latest |
|
| Docker | 20.10+ | Latest |
|
||||||
|
|
||||||
**Small**: Development, testing, single user (1-5 users)
|
|
||||||
**Standard**: Teams (5-50 users), moderate workloads
|
|
||||||
**Production**: Large teams (50+ users), high availability, heavy workflow execution
|
|
||||||
|
|
||||||
<Callout type="info">
|
|
||||||
Resource requirements are driven by workflow execution (isolated-vm sandboxing), file processing (in-memory document parsing), and vector operations (pgvector). Memory is typically the constraining factor rather than CPU. Production telemetry shows the main app uses 4-8 GB average with peaks up to 12 GB under heavy load.
|
|
||||||
</Callout>
|
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
|
|||||||
@@ -10,20 +10,12 @@ Despliega Sim en tu propia infraestructura con Docker o Kubernetes.
|
|||||||
|
|
||||||
## Requisitos
|
## Requisitos
|
||||||
|
|
||||||
| Recurso | Pequeño | Estándar | Producción |
|
| Recurso | Mínimo | Recomendado |
|
||||||
|----------|---------|----------|------------|
|
|----------|---------|-------------|
|
||||||
| CPU | 2 núcleos | 4 núcleos | 8+ núcleos |
|
| CPU | 2 núcleos | 4+ núcleos |
|
||||||
| RAM | 12 GB | 16 GB | 32+ GB |
|
| RAM | 12 GB | 16+ GB |
|
||||||
| Almacenamiento | 20 GB SSD | 50 GB SSD | 100+ GB SSD |
|
| Almacenamiento | 20 GB SSD | 50+ GB SSD |
|
||||||
| Docker | 20.10+ | 20.10+ | Última versión |
|
| Docker | 20.10+ | Última versión |
|
||||||
|
|
||||||
**Pequeño**: Desarrollo, pruebas, usuario único (1-5 usuarios)
|
|
||||||
**Estándar**: Equipos (5-50 usuarios), cargas de trabajo moderadas
|
|
||||||
**Producción**: Equipos grandes (50+ usuarios), alta disponibilidad, ejecución intensiva de workflows
|
|
||||||
|
|
||||||
<Callout type="info">
|
|
||||||
Los requisitos de recursos están determinados por la ejecución de workflows (sandboxing isolated-vm), procesamiento de archivos (análisis de documentos en memoria) y operaciones vectoriales (pgvector). La memoria suele ser el factor limitante, no la CPU. La telemetría de producción muestra que la aplicación principal usa 4-8 GB en promedio con picos de hasta 12 GB bajo carga pesada.
|
|
||||||
</Callout>
|
|
||||||
|
|
||||||
## Inicio rápido
|
## Inicio rápido
|
||||||
|
|
||||||
|
|||||||
@@ -10,20 +10,12 @@ Déployez Sim sur votre propre infrastructure avec Docker ou Kubernetes.
|
|||||||
|
|
||||||
## Prérequis
|
## Prérequis
|
||||||
|
|
||||||
| Ressource | Petit | Standard | Production |
|
| Ressource | Minimum | Recommandé |
|
||||||
|----------|-------|----------|------------|
|
|----------|---------|-------------|
|
||||||
| CPU | 2 cœurs | 4 cœurs | 8+ cœurs |
|
| CPU | 2 cœurs | 4+ cœurs |
|
||||||
| RAM | 12 Go | 16 Go | 32+ Go |
|
| RAM | 12 Go | 16+ Go |
|
||||||
| Stockage | 20 Go SSD | 50 Go SSD | 100+ Go SSD |
|
| Stockage | 20 Go SSD | 50+ Go SSD |
|
||||||
| Docker | 20.10+ | 20.10+ | Dernière version |
|
| Docker | 20.10+ | Dernière version |
|
||||||
|
|
||||||
**Petit** : Développement, tests, utilisateur unique (1-5 utilisateurs)
|
|
||||||
**Standard** : Équipes (5-50 utilisateurs), charges de travail modérées
|
|
||||||
**Production** : Grandes équipes (50+ utilisateurs), haute disponibilité, exécution intensive de workflows
|
|
||||||
|
|
||||||
<Callout type="info">
|
|
||||||
Les besoins en ressources sont déterminés par l'exécution des workflows (sandboxing isolated-vm), le traitement des fichiers (analyse de documents en mémoire) et les opérations vectorielles (pgvector). La mémoire est généralement le facteur limitant, pas le CPU. La télémétrie de production montre que l'application principale utilise 4-8 Go en moyenne avec des pics jusqu'à 12 Go sous forte charge.
|
|
||||||
</Callout>
|
|
||||||
|
|
||||||
## Démarrage rapide
|
## Démarrage rapide
|
||||||
|
|
||||||
|
|||||||
@@ -10,20 +10,12 @@ DockerまたはKubernetesを使用して、自社のインフラストラクチ
|
|||||||
|
|
||||||
## 要件
|
## 要件
|
||||||
|
|
||||||
| リソース | スモール | スタンダード | プロダクション |
|
| リソース | 最小 | 推奨 |
|
||||||
|----------|---------|-------------|----------------|
|
|----------|---------|-------------|
|
||||||
| CPU | 2コア | 4コア | 8+コア |
|
| CPU | 2コア | 4+コア |
|
||||||
| RAM | 12 GB | 16 GB | 32+ GB |
|
| RAM | 12 GB | 16+ GB |
|
||||||
| ストレージ | 20 GB SSD | 50 GB SSD | 100+ GB SSD |
|
| ストレージ | 20 GB SSD | 50+ GB SSD |
|
||||||
| Docker | 20.10+ | 20.10+ | 最新版 |
|
| Docker | 20.10+ | 最新版 |
|
||||||
|
|
||||||
**スモール**: 開発、テスト、シングルユーザー(1-5ユーザー)
|
|
||||||
**スタンダード**: チーム(5-50ユーザー)、中程度のワークロード
|
|
||||||
**プロダクション**: 大規模チーム(50+ユーザー)、高可用性、高負荷ワークフロー実行
|
|
||||||
|
|
||||||
<Callout type="info">
|
|
||||||
リソース要件は、ワークフロー実行(isolated-vmサンドボックス)、ファイル処理(メモリ内ドキュメント解析)、ベクトル演算(pgvector)によって決まります。CPUよりもメモリが制約要因となることが多いです。本番環境のテレメトリによると、メインアプリは平均4-8 GB、高負荷時は最大12 GBを使用します。
|
|
||||||
</Callout>
|
|
||||||
|
|
||||||
## クイックスタート
|
## クイックスタート
|
||||||
|
|
||||||
|
|||||||
@@ -10,20 +10,12 @@ import { Callout } from 'fumadocs-ui/components/callout'
|
|||||||
|
|
||||||
## 要求
|
## 要求
|
||||||
|
|
||||||
| 资源 | 小型 | 标准 | 生产环境 |
|
| 资源 | 最低要求 | 推荐配置 |
|
||||||
|----------|------|------|----------|
|
|----------|---------|-------------|
|
||||||
| CPU | 2 核 | 4 核 | 8+ 核 |
|
| CPU | 2 核 | 4 核及以上 |
|
||||||
| 内存 | 12 GB | 16 GB | 32+ GB |
|
| 内存 | 12 GB | 16 GB 及以上 |
|
||||||
| 存储 | 20 GB SSD | 50 GB SSD | 100+ GB SSD |
|
| 存储 | 20 GB SSD | 50 GB 及以上 SSD |
|
||||||
| Docker | 20.10+ | 20.10+ | 最新版本 |
|
| Docker | 20.10+ | 最新版本 |
|
||||||
|
|
||||||
**小型**: 开发、测试、单用户(1-5 用户)
|
|
||||||
**标准**: 团队(5-50 用户)、中等工作负载
|
|
||||||
**生产环境**: 大型团队(50+ 用户)、高可用性、密集工作流执行
|
|
||||||
|
|
||||||
<Callout type="info">
|
|
||||||
资源需求由工作流执行(isolated-vm 沙箱)、文件处理(内存中文档解析)和向量运算(pgvector)决定。内存通常是限制因素,而不是 CPU。生产遥测数据显示,主应用平均使用 4-8 GB,高负载时峰值可达 12 GB。
|
|
||||||
</Callout>
|
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 103 KiB |
@@ -10,8 +10,8 @@ export { LandingLoopNode } from './landing-canvas/landing-block/landing-loop-nod
|
|||||||
export { LandingNode } from './landing-canvas/landing-block/landing-node'
|
export { LandingNode } from './landing-canvas/landing-block/landing-node'
|
||||||
export type { LoopBlockProps } from './landing-canvas/landing-block/loop-block'
|
export type { LoopBlockProps } from './landing-canvas/landing-block/loop-block'
|
||||||
export { LoopBlock } from './landing-canvas/landing-block/loop-block'
|
export { LoopBlock } from './landing-canvas/landing-block/loop-block'
|
||||||
export type { SubBlockRowProps, TagProps } from './landing-canvas/landing-block/tag'
|
export type { TagProps } from './landing-canvas/landing-block/tag'
|
||||||
export { SubBlockRow, Tag } from './landing-canvas/landing-block/tag'
|
export { Tag } from './landing-canvas/landing-block/tag'
|
||||||
export type {
|
export type {
|
||||||
LandingBlockNode,
|
LandingBlockNode,
|
||||||
LandingCanvasProps,
|
LandingCanvasProps,
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { BookIcon } from 'lucide-react'
|
||||||
import {
|
import {
|
||||||
SubBlockRow,
|
Tag,
|
||||||
type SubBlockRowProps,
|
type TagProps,
|
||||||
} from '@/app/(landing)/components/hero/components/landing-canvas/landing-block/tag'
|
} from '@/app/(landing)/components/hero/components/landing-canvas/landing-block/tag'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data structure for a landing card component
|
* Data structure for a landing card component
|
||||||
* Matches the workflow block structure from the application
|
|
||||||
*/
|
*/
|
||||||
export interface LandingCardData {
|
export interface LandingCardData {
|
||||||
/** Icon element to display in the card header */
|
/** Icon element to display in the card header */
|
||||||
@@ -15,8 +15,8 @@ export interface LandingCardData {
|
|||||||
color: string | '#f6f6f6'
|
color: string | '#f6f6f6'
|
||||||
/** Name/title of the card */
|
/** Name/title of the card */
|
||||||
name: string
|
name: string
|
||||||
/** Optional subblock rows to display below the header */
|
/** Optional tags to display at the bottom of the card */
|
||||||
tags?: SubBlockRowProps[]
|
tags?: TagProps[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,8 +28,7 @@ export interface LandingBlockProps extends LandingCardData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Landing block component that displays a card with icon, name, and optional subblock rows
|
* Landing block component that displays a card with icon, name, and optional tags
|
||||||
* Styled to match the application's workflow blocks
|
|
||||||
* @param props - Component properties including icon, color, name, tags, and className
|
* @param props - Component properties including icon, color, name, tags, and className
|
||||||
* @returns A styled block card component
|
* @returns A styled block card component
|
||||||
*/
|
*/
|
||||||
@@ -40,37 +39,33 @@ export const LandingBlock = React.memo(function LandingBlock({
|
|||||||
tags,
|
tags,
|
||||||
className,
|
className,
|
||||||
}: LandingBlockProps) {
|
}: LandingBlockProps) {
|
||||||
const hasContentBelowHeader = tags && tags.length > 0
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`z-10 flex w-[250px] flex-col rounded-[8px] border border-[#E5E5E5] bg-white ${className ?? ''}`}
|
className={`z-10 flex w-64 flex-col items-start gap-3 rounded-[14px] border border-[#E5E5E5] bg-[#FEFEFE] p-3 ${className ?? ''}`}
|
||||||
|
style={{
|
||||||
|
boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{/* Header - matches workflow-block.tsx header styling */}
|
<div className='flex w-full items-center justify-between'>
|
||||||
<div
|
<div className='flex items-center gap-2.5'>
|
||||||
className={`flex items-center justify-between p-[8px] ${hasContentBelowHeader ? 'border-[#E5E5E5] border-b' : ''}`}
|
|
||||||
>
|
|
||||||
<div className='flex min-w-0 flex-1 items-center gap-[10px]'>
|
|
||||||
<div
|
<div
|
||||||
className='flex h-[24px] w-[24px] flex-shrink-0 items-center justify-center rounded-[6px]'
|
className='flex h-6 w-6 items-center justify-center rounded-[8px] text-white'
|
||||||
style={{ background: color as string }}
|
style={{ backgroundColor: color as string }}
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
</div>
|
</div>
|
||||||
<span className='truncate font-medium text-[#171717] text-[16px]' title={name}>
|
<p className='text-base text-card-foreground'>{name}</p>
|
||||||
{name}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<BookIcon className='h-4 w-4 text-muted-foreground' />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content - SubBlock Rows matching workflow-block.tsx */}
|
{tags && tags.length > 0 ? (
|
||||||
{hasContentBelowHeader && (
|
<div className='flex flex-wrap gap-2'>
|
||||||
<div className='flex flex-col gap-[8px] p-[8px]'>
|
|
||||||
{tags.map((tag) => (
|
{tags.map((tag) => (
|
||||||
<SubBlockRow key={tag.label} icon={tag.icon} label={tag.label} />
|
<Tag key={tag.label} icon={tag.icon} label={tag.label} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,14 +7,9 @@ import {
|
|||||||
type LandingCardData,
|
type LandingCardData,
|
||||||
} from '@/app/(landing)/components/hero/components/landing-canvas/landing-block/landing-block'
|
} from '@/app/(landing)/components/hero/components/landing-canvas/landing-block/landing-block'
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle Y offset from block top - matches HANDLE_POSITIONS.DEFAULT_Y_OFFSET
|
|
||||||
*/
|
|
||||||
const HANDLE_Y_OFFSET = 20
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* React Flow node component for the landing canvas
|
* React Flow node component for the landing canvas
|
||||||
* Styled to match the application's workflow blocks
|
* Includes CSS animations and connection handles
|
||||||
* @param props - Component properties containing node data
|
* @param props - Component properties containing node data
|
||||||
* @returns A React Flow compatible node component
|
* @returns A React Flow compatible node component
|
||||||
*/
|
*/
|
||||||
@@ -46,15 +41,15 @@ export const LandingNode = React.memo(function LandingNode({ data }: { data: Lan
|
|||||||
type='target'
|
type='target'
|
||||||
position={Position.Left}
|
position={Position.Left}
|
||||||
style={{
|
style={{
|
||||||
width: '7px',
|
width: '12px',
|
||||||
height: '20px',
|
height: '12px',
|
||||||
background: '#D1D1D1',
|
background: '#FEFEFE',
|
||||||
border: 'none',
|
border: '1px solid #E5E5E5',
|
||||||
borderRadius: '2px 0 0 2px',
|
borderRadius: '50%',
|
||||||
top: `${HANDLE_Y_OFFSET}px`,
|
top: '50%',
|
||||||
left: '-7px',
|
left: '-20px',
|
||||||
transform: 'translateY(-50%)',
|
transform: 'translateY(-50%)',
|
||||||
zIndex: 10,
|
zIndex: 2,
|
||||||
}}
|
}}
|
||||||
isConnectable={false}
|
isConnectable={false}
|
||||||
/>
|
/>
|
||||||
@@ -64,15 +59,15 @@ export const LandingNode = React.memo(function LandingNode({ data }: { data: Lan
|
|||||||
type='source'
|
type='source'
|
||||||
position={Position.Right}
|
position={Position.Right}
|
||||||
style={{
|
style={{
|
||||||
width: '7px',
|
width: '12px',
|
||||||
height: '20px',
|
height: '12px',
|
||||||
background: '#D1D1D1',
|
background: '#FEFEFE',
|
||||||
border: 'none',
|
border: '1px solid #E5E5E5',
|
||||||
borderRadius: '0 2px 2px 0',
|
borderRadius: '50%',
|
||||||
top: `${HANDLE_Y_OFFSET}px`,
|
top: '50%',
|
||||||
right: '-7px',
|
right: '-20px',
|
||||||
transform: 'translateY(-50%)',
|
transform: 'translateY(-50%)',
|
||||||
zIndex: 10,
|
zIndex: 2,
|
||||||
}}
|
}}
|
||||||
isConnectable={false}
|
isConnectable={false}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ export interface LoopBlockProps {
|
|||||||
/**
|
/**
|
||||||
* Loop block container component that provides a styled container
|
* Loop block container component that provides a styled container
|
||||||
* for grouping related elements with a dashed border
|
* for grouping related elements with a dashed border
|
||||||
* Styled to match the application's subflow containers
|
|
||||||
* @param props - Component properties including children and styling
|
* @param props - Component properties including children and styling
|
||||||
* @returns A styled loop container component
|
* @returns A styled loop container component
|
||||||
*/
|
*/
|
||||||
@@ -30,33 +29,33 @@ export const LoopBlock = React.memo(function LoopBlock({
|
|||||||
style={{
|
style={{
|
||||||
width: '1198px',
|
width: '1198px',
|
||||||
height: '528px',
|
height: '528px',
|
||||||
borderRadius: '8px',
|
borderRadius: '14px',
|
||||||
background: 'rgba(59, 130, 246, 0.08)',
|
background: 'rgba(59, 130, 246, 0.10)',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
...style,
|
...style,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Custom dashed border with SVG - 8px border radius to match blocks */}
|
{/* Custom dashed border with SVG */}
|
||||||
<svg
|
<svg
|
||||||
className='pointer-events-none absolute inset-0 h-full w-full'
|
className='pointer-events-none absolute inset-0 h-full w-full'
|
||||||
style={{ borderRadius: '8px' }}
|
style={{ borderRadius: '14px' }}
|
||||||
preserveAspectRatio='none'
|
preserveAspectRatio='none'
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
className='landing-loop-animated-dash'
|
className='landing-loop-animated-dash'
|
||||||
d='M 1190 527.5
|
d='M 1183.5 527.5
|
||||||
L 8 527.5
|
L 14 527.5
|
||||||
A 7.5 7.5 0 0 1 0.5 520
|
A 13.5 13.5 0 0 1 0.5 514
|
||||||
L 0.5 8
|
L 0.5 14
|
||||||
A 7.5 7.5 0 0 1 8 0.5
|
A 13.5 13.5 0 0 1 14 0.5
|
||||||
L 1190 0.5
|
L 1183.5 0.5
|
||||||
A 7.5 7.5 0 0 1 1197.5 8
|
A 13.5 13.5 0 0 1 1197 14
|
||||||
L 1197.5 520
|
L 1197 514
|
||||||
A 7.5 7.5 0 0 1 1190 527.5 Z'
|
A 13.5 13.5 0 0 1 1183.5 527.5 Z'
|
||||||
fill='none'
|
fill='none'
|
||||||
stroke='#3B82F6'
|
stroke='#3B82F6'
|
||||||
strokeWidth='1'
|
strokeWidth='1'
|
||||||
strokeDasharray='8 8'
|
strokeDasharray='12 12'
|
||||||
strokeLinecap='round'
|
strokeLinecap='round'
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -1,52 +1,25 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Properties for a subblock row component
|
* Properties for a tag component
|
||||||
* Matches the SubBlockRow pattern from workflow-block.tsx
|
|
||||||
*/
|
*/
|
||||||
export interface SubBlockRowProps {
|
export interface TagProps {
|
||||||
/** Icon element to display (optional, for visual context) */
|
/** Icon element to display in the tag */
|
||||||
icon?: React.ReactNode
|
icon: React.ReactNode
|
||||||
/** Text label for the row title */
|
/** Text label for the tag */
|
||||||
label: string
|
label: string
|
||||||
/** Optional value to display on the right side */
|
|
||||||
value?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Kept for backwards compatibility
|
* Tag component for displaying labeled icons in a compact format
|
||||||
|
* @param props - Tag properties including icon and label
|
||||||
|
* @returns A styled tag component
|
||||||
*/
|
*/
|
||||||
export type TagProps = SubBlockRowProps
|
export const Tag = React.memo(function Tag({ icon, label }: TagProps) {
|
||||||
|
|
||||||
/**
|
|
||||||
* SubBlockRow component matching the workflow block's subblock row style
|
|
||||||
* @param props - Row properties including label and optional value
|
|
||||||
* @returns A styled row component
|
|
||||||
*/
|
|
||||||
export const SubBlockRow = React.memo(function SubBlockRow({ label, value }: SubBlockRowProps) {
|
|
||||||
// Split label by colon to separate title and value if present
|
|
||||||
const [title, displayValue] = label.includes(':')
|
|
||||||
? label.split(':').map((s) => s.trim())
|
|
||||||
: [label, value]
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex items-center gap-[8px]'>
|
<div className='flex w-fit items-center gap-1 rounded-[8px] border border-gray-300 bg-white px-2 py-0.5'>
|
||||||
<span className='min-w-0 truncate text-[#888888] text-[14px] capitalize' title={title}>
|
<div className='h-3 w-3 text-muted-foreground'>{icon}</div>
|
||||||
{title}
|
<p className='text-muted-foreground text-xs leading-normal'>{label}</p>
|
||||||
</span>
|
|
||||||
{displayValue && (
|
|
||||||
<span
|
|
||||||
className='flex-1 truncate text-right text-[#171717] text-[14px]'
|
|
||||||
title={displayValue}
|
|
||||||
>
|
|
||||||
{displayValue}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* Tag component - alias for SubBlockRow for backwards compatibility
|
|
||||||
*/
|
|
||||||
export const Tag = SubBlockRow
|
|
||||||
|
|||||||
@@ -9,10 +9,9 @@ import { LandingFlow } from '@/app/(landing)/components/hero/components/landing-
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Visual constants for landing node dimensions
|
* Visual constants for landing node dimensions
|
||||||
* Matches BLOCK_DIMENSIONS from the application
|
|
||||||
*/
|
*/
|
||||||
export const CARD_WIDTH = 250
|
export const CARD_WIDTH = 256
|
||||||
export const CARD_HEIGHT = 100
|
export const CARD_HEIGHT = 92
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Landing block node with positioning information
|
* Landing block node with positioning information
|
||||||
|
|||||||
@@ -4,29 +4,33 @@ import React from 'react'
|
|||||||
import { type EdgeProps, getSmoothStepPath, Position } from 'reactflow'
|
import { type EdgeProps, getSmoothStepPath, Position } from 'reactflow'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom edge component with animated dashed line
|
* Custom edge component with animated dotted line that floats between handles
|
||||||
* Styled to match the application's workflow edges with rectangular handles
|
|
||||||
* @param props - React Flow edge properties
|
* @param props - React Flow edge properties
|
||||||
* @returns An animated dashed edge component
|
* @returns An animated dotted edge component
|
||||||
*/
|
*/
|
||||||
export const LandingEdge = React.memo(function LandingEdge(props: EdgeProps) {
|
export const LandingEdge = React.memo(function LandingEdge(props: EdgeProps) {
|
||||||
const { id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, style } = props
|
const { id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, style, data } =
|
||||||
|
props
|
||||||
|
|
||||||
// Adjust the connection points to connect flush with rectangular handles
|
// Adjust the connection points to create floating effect
|
||||||
// Handle width is 7px, positioned at -7px from edge
|
// Account for handle size (12px) and additional spacing
|
||||||
|
const handleRadius = 6 // Half of handle width (12px)
|
||||||
|
const floatingGap = 1 // Additional gap for floating effect
|
||||||
|
|
||||||
|
// Calculate adjusted positions based on edge direction
|
||||||
let adjustedSourceX = sourceX
|
let adjustedSourceX = sourceX
|
||||||
let adjustedTargetX = targetX
|
let adjustedTargetX = targetX
|
||||||
|
|
||||||
if (sourcePosition === Position.Right) {
|
if (sourcePosition === Position.Right) {
|
||||||
adjustedSourceX = sourceX + 1
|
adjustedSourceX = sourceX + handleRadius + floatingGap
|
||||||
} else if (sourcePosition === Position.Left) {
|
} else if (sourcePosition === Position.Left) {
|
||||||
adjustedSourceX = sourceX - 1
|
adjustedSourceX = sourceX - handleRadius - floatingGap
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetPosition === Position.Left) {
|
if (targetPosition === Position.Left) {
|
||||||
adjustedTargetX = targetX - 1
|
adjustedTargetX = targetX - handleRadius - floatingGap
|
||||||
} else if (targetPosition === Position.Right) {
|
} else if (targetPosition === Position.Right) {
|
||||||
adjustedTargetX = targetX + 1
|
adjustedTargetX = targetX + handleRadius + floatingGap
|
||||||
}
|
}
|
||||||
|
|
||||||
const [path] = getSmoothStepPath({
|
const [path] = getSmoothStepPath({
|
||||||
@@ -36,8 +40,8 @@ export const LandingEdge = React.memo(function LandingEdge(props: EdgeProps) {
|
|||||||
targetY,
|
targetY,
|
||||||
sourcePosition,
|
sourcePosition,
|
||||||
targetPosition,
|
targetPosition,
|
||||||
borderRadius: 8,
|
borderRadius: 20,
|
||||||
offset: 16,
|
offset: 10,
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { ArrowUp, CodeIcon } from 'lucide-react'
|
import {
|
||||||
|
ArrowUp,
|
||||||
|
BinaryIcon,
|
||||||
|
BookIcon,
|
||||||
|
CalendarIcon,
|
||||||
|
CodeIcon,
|
||||||
|
Globe2Icon,
|
||||||
|
MessageSquareIcon,
|
||||||
|
VariableIcon,
|
||||||
|
} from 'lucide-react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import { type Edge, type Node, Position } from 'reactflow'
|
import { type Edge, type Node, Position } from 'reactflow'
|
||||||
import {
|
import {
|
||||||
@@ -14,6 +23,7 @@ import {
|
|||||||
JiraIcon,
|
JiraIcon,
|
||||||
LinearIcon,
|
LinearIcon,
|
||||||
NotionIcon,
|
NotionIcon,
|
||||||
|
OpenAIIcon,
|
||||||
OutlookIcon,
|
OutlookIcon,
|
||||||
PackageSearchIcon,
|
PackageSearchIcon,
|
||||||
PineconeIcon,
|
PineconeIcon,
|
||||||
@@ -55,56 +65,67 @@ const SERVICE_TEMPLATES = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Landing blocks for the canvas preview
|
* Landing blocks for the canvas preview
|
||||||
* Styled to match the application's workflow blocks with subblock rows
|
|
||||||
*/
|
*/
|
||||||
const LANDING_BLOCKS: LandingManualBlock[] = [
|
const LANDING_BLOCKS: LandingManualBlock[] = [
|
||||||
{
|
{
|
||||||
id: 'schedule',
|
id: 'schedule',
|
||||||
name: 'Schedule',
|
name: 'Schedule',
|
||||||
color: '#7B68EE',
|
color: '#7B68EE',
|
||||||
icon: <ScheduleIcon className='h-[16px] w-[16px] text-white' />,
|
icon: <ScheduleIcon className='h-4 w-4' />,
|
||||||
positions: {
|
positions: {
|
||||||
mobile: { x: 8, y: 60 },
|
mobile: { x: 8, y: 60 },
|
||||||
tablet: { x: 40, y: 120 },
|
tablet: { x: 40, y: 120 },
|
||||||
desktop: { x: 60, y: 180 },
|
desktop: { x: 60, y: 180 },
|
||||||
},
|
},
|
||||||
tags: [{ label: 'Time: 09:00AM Daily' }, { label: 'Timezone: PST' }],
|
tags: [
|
||||||
|
{ icon: <CalendarIcon className='h-3 w-3' />, label: '09:00AM Daily' },
|
||||||
|
{ icon: <Globe2Icon className='h-3 w-3' />, label: 'PST' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'knowledge',
|
id: 'knowledge',
|
||||||
name: 'Knowledge',
|
name: 'Knowledge',
|
||||||
color: '#00B0B0',
|
color: '#00B0B0',
|
||||||
icon: <PackageSearchIcon className='h-[16px] w-[16px] text-white' />,
|
icon: <PackageSearchIcon className='h-4 w-4' />,
|
||||||
positions: {
|
positions: {
|
||||||
mobile: { x: 120, y: 140 },
|
mobile: { x: 120, y: 140 },
|
||||||
tablet: { x: 220, y: 200 },
|
tablet: { x: 220, y: 200 },
|
||||||
desktop: { x: 420, y: 241 },
|
desktop: { x: 420, y: 241 },
|
||||||
},
|
},
|
||||||
tags: [{ label: 'Source: Product Vector DB' }, { label: 'Limit: 10' }],
|
tags: [
|
||||||
|
{ icon: <BookIcon className='h-3 w-3' />, label: 'Product Vector DB' },
|
||||||
|
{ icon: <BinaryIcon className='h-3 w-3' />, label: 'Limit: 10' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'agent',
|
id: 'agent',
|
||||||
name: 'Agent',
|
name: 'Agent',
|
||||||
color: '#802FFF',
|
color: '#802FFF',
|
||||||
icon: <AgentIcon className='h-[16px] w-[16px] text-white' />,
|
icon: <AgentIcon className='h-4 w-4' />,
|
||||||
positions: {
|
positions: {
|
||||||
mobile: { x: 340, y: 60 },
|
mobile: { x: 340, y: 60 },
|
||||||
tablet: { x: 540, y: 120 },
|
tablet: { x: 540, y: 120 },
|
||||||
desktop: { x: 880, y: 142 },
|
desktop: { x: 880, y: 142 },
|
||||||
},
|
},
|
||||||
tags: [{ label: 'Model: gpt-5' }, { label: 'Prompt: You are a support ag...' }],
|
tags: [
|
||||||
|
{ icon: <OpenAIIcon className='h-3 w-3' />, label: 'gpt-5' },
|
||||||
|
{ icon: <MessageSquareIcon className='h-3 w-3' />, label: 'You are a support ag...' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'function',
|
id: 'function',
|
||||||
name: 'Function',
|
name: 'Function',
|
||||||
color: '#FF402F',
|
color: '#FF402F',
|
||||||
icon: <CodeIcon className='h-[16px] w-[16px] text-white' />,
|
icon: <CodeIcon className='h-4 w-4' />,
|
||||||
positions: {
|
positions: {
|
||||||
mobile: { x: 480, y: 220 },
|
mobile: { x: 480, y: 220 },
|
||||||
tablet: { x: 740, y: 280 },
|
tablet: { x: 740, y: 280 },
|
||||||
desktop: { x: 880, y: 340 },
|
desktop: { x: 880, y: 340 },
|
||||||
},
|
},
|
||||||
tags: [{ label: 'Language: Python' }, { label: 'Code: time = "2025-09-01...' }],
|
tags: [
|
||||||
|
{ icon: <CodeIcon className='h-3 w-3' />, label: 'Python' },
|
||||||
|
{ icon: <VariableIcon className='h-3 w-3' />, label: 'time = "2025-09-01...' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ function PricingCard({
|
|||||||
*/
|
*/
|
||||||
export default function LandingPricing() {
|
export default function LandingPricing() {
|
||||||
return (
|
return (
|
||||||
<section id='pricing' className='px-4 pt-[23px] sm:px-0 sm:pt-[4px]' aria-label='Pricing plans'>
|
<section id='pricing' className='px-4 pt-[19px] sm:px-0 sm:pt-0' aria-label='Pricing plans'>
|
||||||
<h2 className='sr-only'>Pricing Plans</h2>
|
<h2 className='sr-only'>Pricing Plans</h2>
|
||||||
<div className='relative mx-auto w-full max-w-[1289px]'>
|
<div className='relative mx-auto w-full max-w-[1289px]'>
|
||||||
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2 sm:gap-0 lg:grid-cols-4'>
|
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2 sm:gap-0 lg:grid-cols-4'>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ interface NavProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Nav({ hideAuthButtons = false, variant = 'landing' }: NavProps = {}) {
|
export default function Nav({ hideAuthButtons = false, variant = 'landing' }: NavProps = {}) {
|
||||||
const [githubStars, setGithubStars] = useState('26.1k')
|
const [githubStars, setGithubStars] = useState('25.8k')
|
||||||
const [isHovered, setIsHovered] = useState(false)
|
const [isHovered, setIsHovered] = useState(false)
|
||||||
const [isLoginHovered, setIsLoginHovered] = useState(false)
|
const [isLoginHovered, setIsLoginHovered] = useState(false)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { executeInIsolatedVM } from '@/lib/execution/isolated-vm'
|
|||||||
import { CodeLanguage, DEFAULT_CODE_LANGUAGE, isValidCodeLanguage } from '@/lib/execution/languages'
|
import { CodeLanguage, DEFAULT_CODE_LANGUAGE, isValidCodeLanguage } from '@/lib/execution/languages'
|
||||||
import { escapeRegExp, normalizeName, REFERENCE } from '@/executor/constants'
|
import { escapeRegExp, normalizeName, REFERENCE } from '@/executor/constants'
|
||||||
import { type OutputSchema, resolveBlockReference } from '@/executor/utils/block-reference'
|
import { type OutputSchema, resolveBlockReference } from '@/executor/utils/block-reference'
|
||||||
import { formatLiteralForCode } from '@/executor/utils/code-formatting'
|
|
||||||
import {
|
import {
|
||||||
createEnvVarPattern,
|
createEnvVarPattern,
|
||||||
createWorkflowVariablePattern,
|
createWorkflowVariablePattern,
|
||||||
@@ -388,12 +387,7 @@ function resolveWorkflowVariables(
|
|||||||
if (type === 'number') {
|
if (type === 'number') {
|
||||||
variableValue = Number(variableValue)
|
variableValue = Number(variableValue)
|
||||||
} else if (type === 'boolean') {
|
} else if (type === 'boolean') {
|
||||||
if (typeof variableValue === 'boolean') {
|
variableValue = variableValue === 'true' || variableValue === true
|
||||||
// Already a boolean, keep as-is
|
|
||||||
} else {
|
|
||||||
const normalized = String(variableValue).toLowerCase().trim()
|
|
||||||
variableValue = normalized === 'true'
|
|
||||||
}
|
|
||||||
} else if (type === 'json' && typeof variableValue === 'string') {
|
} else if (type === 'json' && typeof variableValue === 'string') {
|
||||||
try {
|
try {
|
||||||
variableValue = JSON.parse(variableValue)
|
variableValue = JSON.parse(variableValue)
|
||||||
@@ -693,7 +687,11 @@ export async function POST(req: NextRequest) {
|
|||||||
prologue += `const environmentVariables = JSON.parse(${JSON.stringify(JSON.stringify(envVars))});\n`
|
prologue += `const environmentVariables = JSON.parse(${JSON.stringify(JSON.stringify(envVars))});\n`
|
||||||
prologueLineCount++
|
prologueLineCount++
|
||||||
for (const [k, v] of Object.entries(contextVariables)) {
|
for (const [k, v] of Object.entries(contextVariables)) {
|
||||||
prologue += `const ${k} = ${formatLiteralForCode(v, 'javascript')};\n`
|
if (v === undefined) {
|
||||||
|
prologue += `const ${k} = undefined;\n`
|
||||||
|
} else {
|
||||||
|
prologue += `const ${k} = JSON.parse(${JSON.stringify(JSON.stringify(v))});\n`
|
||||||
|
}
|
||||||
prologueLineCount++
|
prologueLineCount++
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -764,7 +762,11 @@ export async function POST(req: NextRequest) {
|
|||||||
prologue += `environmentVariables = json.loads(${JSON.stringify(JSON.stringify(envVars))})\n`
|
prologue += `environmentVariables = json.loads(${JSON.stringify(JSON.stringify(envVars))})\n`
|
||||||
prologueLineCount++
|
prologueLineCount++
|
||||||
for (const [k, v] of Object.entries(contextVariables)) {
|
for (const [k, v] of Object.entries(contextVariables)) {
|
||||||
prologue += `${k} = ${formatLiteralForCode(v, 'python')}\n`
|
if (v === undefined) {
|
||||||
|
prologue += `${k} = None\n`
|
||||||
|
} else {
|
||||||
|
prologue += `${k} = json.loads(${JSON.stringify(JSON.stringify(v))})\n`
|
||||||
|
}
|
||||||
prologueLineCount++
|
prologueLineCount++
|
||||||
}
|
}
|
||||||
const wrapped = [
|
const wrapped = [
|
||||||
|
|||||||
@@ -408,7 +408,6 @@ describe('Knowledge Search Utils', () => {
|
|||||||
input: ['test query'],
|
input: ['test query'],
|
||||||
model: 'text-embedding-3-small',
|
model: 'text-embedding-3-small',
|
||||||
encoding_format: 'float',
|
encoding_format: 'float',
|
||||||
dimensions: 1536,
|
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|||||||
204
apps/sim/app/api/organizations/[id]/workspaces/route.ts
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
import { db } from '@sim/db'
|
||||||
|
import { member, permissions, user, workspace } from '@sim/db/schema'
|
||||||
|
import { createLogger } from '@sim/logger'
|
||||||
|
import { and, eq, or } from 'drizzle-orm'
|
||||||
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { getSession } from '@/lib/auth'
|
||||||
|
|
||||||
|
const logger = createLogger('OrganizationWorkspacesAPI')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/organizations/[id]/workspaces
|
||||||
|
* Get workspaces related to the organization with optional filtering
|
||||||
|
* Query parameters:
|
||||||
|
* - ?available=true - Only workspaces where user can invite others (admin permissions)
|
||||||
|
* - ?member=userId - Workspaces where specific member has access
|
||||||
|
*/
|
||||||
|
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||||
|
try {
|
||||||
|
const session = await getSession()
|
||||||
|
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id: organizationId } = await params
|
||||||
|
const url = new URL(request.url)
|
||||||
|
const availableOnly = url.searchParams.get('available') === 'true'
|
||||||
|
const memberId = url.searchParams.get('member')
|
||||||
|
|
||||||
|
// Verify user is a member of this organization
|
||||||
|
const memberEntry = await db
|
||||||
|
.select()
|
||||||
|
.from(member)
|
||||||
|
.where(and(eq(member.organizationId, organizationId), eq(member.userId, session.user.id)))
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
if (memberEntry.length === 0) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: 'Forbidden - Not a member of this organization',
|
||||||
|
},
|
||||||
|
{ status: 403 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const userRole = memberEntry[0].role
|
||||||
|
const hasAdminAccess = ['owner', 'admin'].includes(userRole)
|
||||||
|
|
||||||
|
if (availableOnly) {
|
||||||
|
// Get workspaces where user has admin permissions (can invite others)
|
||||||
|
const availableWorkspaces = await db
|
||||||
|
.select({
|
||||||
|
id: workspace.id,
|
||||||
|
name: workspace.name,
|
||||||
|
ownerId: workspace.ownerId,
|
||||||
|
createdAt: workspace.createdAt,
|
||||||
|
isOwner: eq(workspace.ownerId, session.user.id),
|
||||||
|
permissionType: permissions.permissionType,
|
||||||
|
})
|
||||||
|
.from(workspace)
|
||||||
|
.leftJoin(
|
||||||
|
permissions,
|
||||||
|
and(
|
||||||
|
eq(permissions.entityType, 'workspace'),
|
||||||
|
eq(permissions.entityId, workspace.id),
|
||||||
|
eq(permissions.userId, session.user.id)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
or(
|
||||||
|
// User owns the workspace
|
||||||
|
eq(workspace.ownerId, session.user.id),
|
||||||
|
// User has admin permission on the workspace
|
||||||
|
and(
|
||||||
|
eq(permissions.userId, session.user.id),
|
||||||
|
eq(permissions.entityType, 'workspace'),
|
||||||
|
eq(permissions.permissionType, 'admin')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Filter and format the results
|
||||||
|
const workspacesWithInvitePermission = availableWorkspaces
|
||||||
|
.filter((workspace) => {
|
||||||
|
// Include if user owns the workspace OR has admin permission
|
||||||
|
return workspace.isOwner || workspace.permissionType === 'admin'
|
||||||
|
})
|
||||||
|
.map((workspace) => ({
|
||||||
|
id: workspace.id,
|
||||||
|
name: workspace.name,
|
||||||
|
isOwner: workspace.isOwner,
|
||||||
|
canInvite: true, // All returned workspaces have invite permission
|
||||||
|
createdAt: workspace.createdAt,
|
||||||
|
}))
|
||||||
|
|
||||||
|
logger.info('Retrieved available workspaces for organization member', {
|
||||||
|
organizationId,
|
||||||
|
userId: session.user.id,
|
||||||
|
workspaceCount: workspacesWithInvitePermission.length,
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
workspaces: workspacesWithInvitePermission,
|
||||||
|
totalCount: workspacesWithInvitePermission.length,
|
||||||
|
filter: 'available',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (memberId && hasAdminAccess) {
|
||||||
|
// Get workspaces where specific member has access (admin only)
|
||||||
|
const memberWorkspaces = await db
|
||||||
|
.select({
|
||||||
|
id: workspace.id,
|
||||||
|
name: workspace.name,
|
||||||
|
ownerId: workspace.ownerId,
|
||||||
|
isOwner: eq(workspace.ownerId, memberId),
|
||||||
|
permissionType: permissions.permissionType,
|
||||||
|
createdAt: permissions.createdAt,
|
||||||
|
})
|
||||||
|
.from(workspace)
|
||||||
|
.leftJoin(
|
||||||
|
permissions,
|
||||||
|
and(
|
||||||
|
eq(permissions.entityType, 'workspace'),
|
||||||
|
eq(permissions.entityId, workspace.id),
|
||||||
|
eq(permissions.userId, memberId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
or(
|
||||||
|
// Member owns the workspace
|
||||||
|
eq(workspace.ownerId, memberId),
|
||||||
|
// Member has permissions on the workspace
|
||||||
|
and(eq(permissions.userId, memberId), eq(permissions.entityType, 'workspace'))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
const formattedWorkspaces = memberWorkspaces.map((workspace) => ({
|
||||||
|
id: workspace.id,
|
||||||
|
name: workspace.name,
|
||||||
|
isOwner: workspace.isOwner,
|
||||||
|
permission: workspace.permissionType,
|
||||||
|
joinedAt: workspace.createdAt,
|
||||||
|
createdAt: workspace.createdAt,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
workspaces: formattedWorkspaces,
|
||||||
|
totalCount: formattedWorkspaces.length,
|
||||||
|
filter: 'member',
|
||||||
|
memberId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: Get all workspaces (basic info only for regular members)
|
||||||
|
if (!hasAdminAccess) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
workspaces: [],
|
||||||
|
totalCount: 0,
|
||||||
|
message: 'Workspace access information is only available to organization admins',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// For admins: Get summary of all workspaces
|
||||||
|
const allWorkspaces = await db
|
||||||
|
.select({
|
||||||
|
id: workspace.id,
|
||||||
|
name: workspace.name,
|
||||||
|
ownerId: workspace.ownerId,
|
||||||
|
createdAt: workspace.createdAt,
|
||||||
|
ownerName: user.name,
|
||||||
|
})
|
||||||
|
.from(workspace)
|
||||||
|
.leftJoin(user, eq(workspace.ownerId, user.id))
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
workspaces: allWorkspaces,
|
||||||
|
totalCount: allWorkspaces.length,
|
||||||
|
filter: 'all',
|
||||||
|
},
|
||||||
|
userRole,
|
||||||
|
hasAdminAccess,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to get organization workspaces', { error })
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: 'Internal server error',
|
||||||
|
},
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,257 +0,0 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
|
||||||
import { z } from 'zod'
|
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
|
||||||
import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils'
|
|
||||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
|
||||||
|
|
||||||
const logger = createLogger('SupabaseStorageUploadAPI')
|
|
||||||
|
|
||||||
const SupabaseStorageUploadSchema = z.object({
|
|
||||||
projectId: z.string().min(1, 'Project ID is required'),
|
|
||||||
apiKey: z.string().min(1, 'API key is required'),
|
|
||||||
bucket: z.string().min(1, 'Bucket name is required'),
|
|
||||||
fileName: z.string().min(1, 'File name is required'),
|
|
||||||
path: z.string().optional().nullable(),
|
|
||||||
fileData: z.any(),
|
|
||||||
contentType: z.string().optional().nullable(),
|
|
||||||
upsert: z.boolean().optional().default(false),
|
|
||||||
})
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
const requestId = generateRequestId()
|
|
||||||
|
|
||||||
try {
|
|
||||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
|
||||||
|
|
||||||
if (!authResult.success) {
|
|
||||||
logger.warn(
|
|
||||||
`[${requestId}] Unauthorized Supabase storage upload attempt: ${authResult.error}`
|
|
||||||
)
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
success: false,
|
|
||||||
error: authResult.error || 'Authentication required',
|
|
||||||
},
|
|
||||||
{ status: 401 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
`[${requestId}] Authenticated Supabase storage upload request via ${authResult.authType}`,
|
|
||||||
{
|
|
||||||
userId: authResult.userId,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const body = await request.json()
|
|
||||||
const validatedData = SupabaseStorageUploadSchema.parse(body)
|
|
||||||
|
|
||||||
const fileData = validatedData.fileData
|
|
||||||
const isStringInput = typeof fileData === 'string'
|
|
||||||
|
|
||||||
logger.info(`[${requestId}] Uploading to Supabase Storage`, {
|
|
||||||
bucket: validatedData.bucket,
|
|
||||||
fileName: validatedData.fileName,
|
|
||||||
path: validatedData.path,
|
|
||||||
fileDataType: isStringInput ? 'string' : 'object',
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!fileData) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
success: false,
|
|
||||||
error: 'fileData is required',
|
|
||||||
},
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
let uploadBody: Buffer
|
|
||||||
let uploadContentType: string | undefined
|
|
||||||
|
|
||||||
if (isStringInput) {
|
|
||||||
let content = fileData as string
|
|
||||||
|
|
||||||
const dataUrlMatch = content.match(/^data:([^;]+);base64,(.+)$/s)
|
|
||||||
if (dataUrlMatch) {
|
|
||||||
const [, mimeType, base64Data] = dataUrlMatch
|
|
||||||
content = base64Data
|
|
||||||
if (!validatedData.contentType) {
|
|
||||||
uploadContentType = mimeType
|
|
||||||
}
|
|
||||||
logger.info(`[${requestId}] Extracted base64 from data URL (MIME: ${mimeType})`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const cleanedContent = content.replace(/[\s\r\n]/g, '')
|
|
||||||
const isLikelyBase64 = /^[A-Za-z0-9+/]*={0,2}$/.test(cleanedContent)
|
|
||||||
|
|
||||||
if (isLikelyBase64 && cleanedContent.length >= 4) {
|
|
||||||
try {
|
|
||||||
uploadBody = Buffer.from(cleanedContent, 'base64')
|
|
||||||
|
|
||||||
const expectedMinSize = Math.floor(cleanedContent.length * 0.7)
|
|
||||||
const expectedMaxSize = Math.ceil(cleanedContent.length * 0.8)
|
|
||||||
|
|
||||||
if (
|
|
||||||
uploadBody.length >= expectedMinSize &&
|
|
||||||
uploadBody.length <= expectedMaxSize &&
|
|
||||||
uploadBody.length > 0
|
|
||||||
) {
|
|
||||||
logger.info(
|
|
||||||
`[${requestId}] Decoded base64 content: ${cleanedContent.length} chars -> ${uploadBody.length} bytes`
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
const reEncoded = uploadBody.toString('base64')
|
|
||||||
if (reEncoded !== cleanedContent) {
|
|
||||||
logger.info(
|
|
||||||
`[${requestId}] Content looked like base64 but re-encoding didn't match, using as plain text`
|
|
||||||
)
|
|
||||||
uploadBody = Buffer.from(content, 'utf-8')
|
|
||||||
} else {
|
|
||||||
logger.info(
|
|
||||||
`[${requestId}] Decoded base64 content (verified): ${uploadBody.length} bytes`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (decodeError) {
|
|
||||||
logger.info(
|
|
||||||
`[${requestId}] Failed to decode as base64, using as plain text: ${decodeError}`
|
|
||||||
)
|
|
||||||
uploadBody = Buffer.from(content, 'utf-8')
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
uploadBody = Buffer.from(content, 'utf-8')
|
|
||||||
logger.info(`[${requestId}] Using content as plain text (${uploadBody.length} bytes)`)
|
|
||||||
}
|
|
||||||
|
|
||||||
uploadContentType =
|
|
||||||
uploadContentType || validatedData.contentType || 'application/octet-stream'
|
|
||||||
} else {
|
|
||||||
const rawFile = fileData
|
|
||||||
logger.info(`[${requestId}] Processing file object: ${rawFile.name || 'unknown'}`)
|
|
||||||
|
|
||||||
let userFile
|
|
||||||
try {
|
|
||||||
userFile = processSingleFileToUserFile(rawFile, requestId, logger)
|
|
||||||
} catch (error) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : 'Failed to process file',
|
|
||||||
},
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const buffer = await downloadFileFromStorage(userFile, requestId, logger)
|
|
||||||
|
|
||||||
uploadBody = buffer
|
|
||||||
uploadContentType = validatedData.contentType || userFile.type || 'application/octet-stream'
|
|
||||||
}
|
|
||||||
|
|
||||||
let fullPath = validatedData.fileName
|
|
||||||
if (validatedData.path) {
|
|
||||||
const folderPath = validatedData.path.endsWith('/')
|
|
||||||
? validatedData.path
|
|
||||||
: `${validatedData.path}/`
|
|
||||||
fullPath = `${folderPath}${validatedData.fileName}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const supabaseUrl = `https://${validatedData.projectId}.supabase.co/storage/v1/object/${validatedData.bucket}/${fullPath}`
|
|
||||||
|
|
||||||
const headers: Record<string, string> = {
|
|
||||||
apikey: validatedData.apiKey,
|
|
||||||
Authorization: `Bearer ${validatedData.apiKey}`,
|
|
||||||
'Content-Type': uploadContentType,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (validatedData.upsert) {
|
|
||||||
headers['x-upsert'] = 'true'
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(`[${requestId}] Sending to Supabase: ${supabaseUrl}`, {
|
|
||||||
contentType: uploadContentType,
|
|
||||||
bodySize: uploadBody.length,
|
|
||||||
upsert: validatedData.upsert,
|
|
||||||
})
|
|
||||||
|
|
||||||
const response = await fetch(supabaseUrl, {
|
|
||||||
method: 'POST',
|
|
||||||
headers,
|
|
||||||
body: new Uint8Array(uploadBody),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorText = await response.text()
|
|
||||||
let errorData
|
|
||||||
try {
|
|
||||||
errorData = JSON.parse(errorText)
|
|
||||||
} catch {
|
|
||||||
errorData = { message: errorText }
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.error(`[${requestId}] Supabase Storage upload failed:`, {
|
|
||||||
status: response.status,
|
|
||||||
statusText: response.statusText,
|
|
||||||
error: errorData,
|
|
||||||
})
|
|
||||||
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
success: false,
|
|
||||||
error: errorData.message || errorData.error || `Upload failed: ${response.statusText}`,
|
|
||||||
details: errorData,
|
|
||||||
},
|
|
||||||
{ status: response.status }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json()
|
|
||||||
|
|
||||||
logger.info(`[${requestId}] File uploaded successfully to Supabase Storage`, {
|
|
||||||
bucket: validatedData.bucket,
|
|
||||||
path: fullPath,
|
|
||||||
})
|
|
||||||
|
|
||||||
const publicUrl = `https://${validatedData.projectId}.supabase.co/storage/v1/object/public/${validatedData.bucket}/${fullPath}`
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
output: {
|
|
||||||
message: 'Successfully uploaded file to storage',
|
|
||||||
results: {
|
|
||||||
...result,
|
|
||||||
path: fullPath,
|
|
||||||
bucket: validatedData.bucket,
|
|
||||||
publicUrl,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof z.ZodError) {
|
|
||||||
logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
success: false,
|
|
||||||
error: 'Invalid request data',
|
|
||||||
details: error.errors,
|
|
||||||
},
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.error(`[${requestId}] Error uploading to Supabase Storage:`, error)
|
|
||||||
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : 'Internal server error',
|
|
||||||
},
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +1,7 @@
|
|||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { Plus } from 'lucide-react'
|
import { Plus } from 'lucide-react'
|
||||||
import { useParams } from 'next/navigation'
|
import { useParams } from 'next/navigation'
|
||||||
import {
|
import { Badge, Button, Combobox, Input, Label, Textarea } from '@/components/emcn'
|
||||||
Badge,
|
|
||||||
Button,
|
|
||||||
Combobox,
|
|
||||||
type ComboboxOption,
|
|
||||||
Input,
|
|
||||||
Label,
|
|
||||||
Textarea,
|
|
||||||
} from '@/components/emcn'
|
|
||||||
import { Trash } from '@/components/emcn/icons/trash'
|
import { Trash } from '@/components/emcn/icons/trash'
|
||||||
import { cn } from '@/lib/core/utils/cn'
|
import { cn } from '@/lib/core/utils/cn'
|
||||||
import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text'
|
import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text'
|
||||||
@@ -46,14 +38,6 @@ const DEFAULT_ASSIGNMENT: Omit<VariableAssignment, 'id'> = {
|
|||||||
isExisting: false,
|
isExisting: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Boolean value options for Combobox
|
|
||||||
*/
|
|
||||||
const BOOLEAN_OPTIONS: ComboboxOption[] = [
|
|
||||||
{ label: 'true', value: 'true' },
|
|
||||||
{ label: 'false', value: 'false' },
|
|
||||||
]
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses a value that might be a JSON string or already an array of VariableAssignment.
|
* Parses a value that might be a JSON string or already an array of VariableAssignment.
|
||||||
* This handles the case where workflows are imported with stringified values.
|
* This handles the case where workflows are imported with stringified values.
|
||||||
@@ -120,6 +104,8 @@ export function VariablesInput({
|
|||||||
const allVariablesAssigned =
|
const allVariablesAssigned =
|
||||||
!hasNoWorkflowVariables && getAvailableVariablesFor('new').length === 0
|
!hasNoWorkflowVariables && getAvailableVariablesFor('new').length === 0
|
||||||
|
|
||||||
|
// Initialize with one empty assignment if none exist and not in preview/disabled mode
|
||||||
|
// Also add assignment when first variable is created
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isReadOnly && assignments.length === 0 && currentWorkflowVariables.length > 0) {
|
if (!isReadOnly && assignments.length === 0 && currentWorkflowVariables.length > 0) {
|
||||||
const initialAssignment: VariableAssignment = {
|
const initialAssignment: VariableAssignment = {
|
||||||
@@ -130,46 +116,45 @@ export function VariablesInput({
|
|||||||
}
|
}
|
||||||
}, [currentWorkflowVariables.length, isReadOnly, assignments.length, setStoreValue])
|
}, [currentWorkflowVariables.length, isReadOnly, assignments.length, setStoreValue])
|
||||||
|
|
||||||
|
// Clean up assignments when their associated variables are deleted
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isReadOnly || assignments.length === 0) return
|
if (isReadOnly || assignments.length === 0) return
|
||||||
|
|
||||||
const currentVariableIds = new Set(currentWorkflowVariables.map((v) => v.id))
|
const currentVariableIds = new Set(currentWorkflowVariables.map((v) => v.id))
|
||||||
const validAssignments = assignments.filter((assignment) => {
|
const validAssignments = assignments.filter((assignment) => {
|
||||||
|
// Keep assignments that haven't selected a variable yet
|
||||||
if (!assignment.variableId) return true
|
if (!assignment.variableId) return true
|
||||||
|
// Keep assignments whose variable still exists
|
||||||
return currentVariableIds.has(assignment.variableId)
|
return currentVariableIds.has(assignment.variableId)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// If all variables were deleted, clear all assignments
|
||||||
if (currentWorkflowVariables.length === 0) {
|
if (currentWorkflowVariables.length === 0) {
|
||||||
setStoreValue([])
|
setStoreValue([])
|
||||||
} else if (validAssignments.length !== assignments.length) {
|
} else if (validAssignments.length !== assignments.length) {
|
||||||
|
// Some assignments reference deleted variables, remove them
|
||||||
setStoreValue(validAssignments.length > 0 ? validAssignments : [])
|
setStoreValue(validAssignments.length > 0 ? validAssignments : [])
|
||||||
}
|
}
|
||||||
}, [currentWorkflowVariables, assignments, isReadOnly, setStoreValue])
|
}, [currentWorkflowVariables, assignments, isReadOnly, setStoreValue])
|
||||||
|
|
||||||
const addAssignment = () => {
|
const addAssignment = () => {
|
||||||
if (isReadOnly || allVariablesAssigned) return
|
if (isPreview || disabled || allVariablesAssigned) return
|
||||||
|
|
||||||
const newAssignment: VariableAssignment = {
|
const newAssignment: VariableAssignment = {
|
||||||
...DEFAULT_ASSIGNMENT,
|
...DEFAULT_ASSIGNMENT,
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
}
|
}
|
||||||
setStoreValue([...assignments, newAssignment])
|
setStoreValue([...(assignments || []), newAssignment])
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeAssignment = (id: string) => {
|
const removeAssignment = (id: string) => {
|
||||||
if (isReadOnly) return
|
if (isPreview || disabled) return
|
||||||
|
setStoreValue((assignments || []).filter((a) => a.id !== id))
|
||||||
if (assignments.length === 1) {
|
|
||||||
setStoreValue([{ ...DEFAULT_ASSIGNMENT, id: crypto.randomUUID() }])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setStoreValue(assignments.filter((a) => a.id !== id))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateAssignment = (id: string, updates: Partial<VariableAssignment>) => {
|
const updateAssignment = (id: string, updates: Partial<VariableAssignment>) => {
|
||||||
if (isReadOnly) return
|
if (isPreview || disabled) return
|
||||||
setStoreValue(assignments.map((a) => (a.id === id ? { ...a, ...updates } : a)))
|
setStoreValue((assignments || []).map((a) => (a.id === id ? { ...a, ...updates } : a)))
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleVariableSelect = (assignmentId: string, variableId: string) => {
|
const handleVariableSelect = (assignmentId: string, variableId: string) => {
|
||||||
@@ -184,12 +169,19 @@ export function VariablesInput({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleTagSelect = (newValue: string) => {
|
const handleTagSelect = (tag: string) => {
|
||||||
if (!activeFieldId) return
|
if (!activeFieldId) return
|
||||||
|
|
||||||
const assignment = assignments.find((a) => a.id === activeFieldId)
|
const assignment = assignments.find((a) => a.id === activeFieldId)
|
||||||
const originalValue = assignment?.value || ''
|
if (!assignment) return
|
||||||
const textAfterCursor = originalValue.slice(cursorPosition)
|
|
||||||
|
const currentValue = assignment.value || ''
|
||||||
|
|
||||||
|
const textBeforeCursor = currentValue.slice(0, cursorPosition)
|
||||||
|
const lastOpenBracket = textBeforeCursor.lastIndexOf('<')
|
||||||
|
|
||||||
|
const newValue =
|
||||||
|
currentValue.slice(0, lastOpenBracket) + tag + currentValue.slice(cursorPosition)
|
||||||
|
|
||||||
updateAssignment(activeFieldId, { value: newValue })
|
updateAssignment(activeFieldId, { value: newValue })
|
||||||
setShowTags(false)
|
setShowTags(false)
|
||||||
@@ -198,7 +190,7 @@ export function VariablesInput({
|
|||||||
const inputEl = valueInputRefs.current[activeFieldId]
|
const inputEl = valueInputRefs.current[activeFieldId]
|
||||||
if (inputEl) {
|
if (inputEl) {
|
||||||
inputEl.focus()
|
inputEl.focus()
|
||||||
const newCursorPos = newValue.length - textAfterCursor.length
|
const newCursorPos = lastOpenBracket + tag.length
|
||||||
inputEl.setSelectionRange(newCursorPos, newCursorPos)
|
inputEl.setSelectionRange(newCursorPos, newCursorPos)
|
||||||
}
|
}
|
||||||
}, 10)
|
}, 10)
|
||||||
@@ -280,18 +272,6 @@ export function VariablesInput({
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
const syncOverlayScroll = (assignmentId: string, scrollLeft: number) => {
|
|
||||||
const overlay = overlayRefs.current[assignmentId]
|
|
||||||
if (overlay) overlay.scrollLeft = scrollLeft
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
setShowTags(false)
|
|
||||||
setActiveSourceBlockId(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPreview && (!assignments || assignments.length === 0)) {
|
if (isPreview && (!assignments || assignments.length === 0)) {
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col items-center justify-center rounded-md border border-border/40 bg-muted/20 py-8 text-center'>
|
<div className='flex flex-col items-center justify-center rounded-md border border-border/40 bg-muted/20 py-8 text-center'>
|
||||||
@@ -322,7 +302,7 @@ export function VariablesInput({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='space-y-[8px]'>
|
<div className='space-y-[8px]'>
|
||||||
{assignments.length > 0 && (
|
{assignments && assignments.length > 0 && (
|
||||||
<div className='space-y-[8px]'>
|
<div className='space-y-[8px]'>
|
||||||
{assignments.map((assignment, index) => {
|
{assignments.map((assignment, index) => {
|
||||||
const collapsed = collapsedAssignments[assignment.id] || false
|
const collapsed = collapsedAssignments[assignment.id] || false
|
||||||
@@ -354,7 +334,7 @@ export function VariablesInput({
|
|||||||
<Button
|
<Button
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
onClick={addAssignment}
|
onClick={addAssignment}
|
||||||
disabled={isReadOnly || allVariablesAssigned}
|
disabled={isPreview || disabled || allVariablesAssigned}
|
||||||
className='h-auto p-0'
|
className='h-auto p-0'
|
||||||
>
|
>
|
||||||
<Plus className='h-[14px] w-[14px]' />
|
<Plus className='h-[14px] w-[14px]' />
|
||||||
@@ -363,7 +343,7 @@ export function VariablesInput({
|
|||||||
<Button
|
<Button
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
onClick={() => removeAssignment(assignment.id)}
|
onClick={() => removeAssignment(assignment.id)}
|
||||||
disabled={isReadOnly}
|
disabled={isPreview || disabled || assignments.length === 1}
|
||||||
className='h-auto p-0 text-[var(--text-error)] hover:text-[var(--text-error)]'
|
className='h-auto p-0 text-[var(--text-error)] hover:text-[var(--text-error)]'
|
||||||
>
|
>
|
||||||
<Trash className='h-[14px] w-[14px]' />
|
<Trash className='h-[14px] w-[14px]' />
|
||||||
@@ -378,26 +358,16 @@ export function VariablesInput({
|
|||||||
<Label className='text-[13px]'>Variable</Label>
|
<Label className='text-[13px]'>Variable</Label>
|
||||||
<Combobox
|
<Combobox
|
||||||
options={availableVars.map((v) => ({ label: v.name, value: v.id }))}
|
options={availableVars.map((v) => ({ label: v.name, value: v.id }))}
|
||||||
value={assignment.variableId || ''}
|
value={assignment.variableId || assignment.variableName || ''}
|
||||||
onChange={(value) => handleVariableSelect(assignment.id, value)}
|
onChange={(value) => handleVariableSelect(assignment.id, value)}
|
||||||
placeholder='Select a variable...'
|
placeholder='Select a variable...'
|
||||||
disabled={isReadOnly}
|
disabled={isPreview || disabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex flex-col gap-[6px]'>
|
<div className='flex flex-col gap-[6px]'>
|
||||||
<Label className='text-[13px]'>Value</Label>
|
<Label className='text-[13px]'>Value</Label>
|
||||||
{assignment.type === 'boolean' ? (
|
{assignment.type === 'object' || assignment.type === 'array' ? (
|
||||||
<Combobox
|
|
||||||
options={BOOLEAN_OPTIONS}
|
|
||||||
value={assignment.value ?? ''}
|
|
||||||
onChange={(v) =>
|
|
||||||
!isReadOnly && updateAssignment(assignment.id, { value: v })
|
|
||||||
}
|
|
||||||
placeholder='Select value'
|
|
||||||
disabled={isReadOnly}
|
|
||||||
/>
|
|
||||||
) : assignment.type === 'object' || assignment.type === 'array' ? (
|
|
||||||
<div className='relative'>
|
<div className='relative'>
|
||||||
<Textarea
|
<Textarea
|
||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
@@ -411,32 +381,26 @@ export function VariablesInput({
|
|||||||
e.target.selectionStart ?? undefined
|
e.target.selectionStart ?? undefined
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
onFocus={() => {
|
onFocus={() => {
|
||||||
if (!isReadOnly && !assignment.value?.trim()) {
|
if (!isPreview && !disabled && !assignment.value?.trim()) {
|
||||||
setActiveFieldId(assignment.id)
|
setActiveFieldId(assignment.id)
|
||||||
setCursorPosition(0)
|
setCursorPosition(0)
|
||||||
setShowTags(true)
|
setShowTags(true)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onScroll={(e) => {
|
|
||||||
const overlay = overlayRefs.current[assignment.id]
|
|
||||||
if (overlay) {
|
|
||||||
overlay.scrollTop = e.currentTarget.scrollTop
|
|
||||||
overlay.scrollLeft = e.currentTarget.scrollLeft
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
placeholder={
|
placeholder={
|
||||||
assignment.type === 'object'
|
assignment.type === 'object'
|
||||||
? '{\n "key": "value"\n}'
|
? '{\n "key": "value"\n}'
|
||||||
: '[\n 1, 2, 3\n]'
|
: '[\n 1, 2, 3\n]'
|
||||||
}
|
}
|
||||||
disabled={isReadOnly}
|
disabled={isPreview || disabled}
|
||||||
className={cn(
|
className={cn(
|
||||||
'min-h-[120px] font-mono text-sm text-transparent caret-foreground placeholder:text-muted-foreground/50',
|
'min-h-[120px] font-mono text-sm text-transparent caret-foreground placeholder:text-muted-foreground/50',
|
||||||
dragHighlight[assignment.id] && 'ring-2 ring-blue-500 ring-offset-2'
|
dragHighlight[assignment.id] && 'ring-2 ring-blue-500 ring-offset-2'
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
|
fontFamily: 'inherit',
|
||||||
|
lineHeight: 'inherit',
|
||||||
wordBreak: 'break-word',
|
wordBreak: 'break-word',
|
||||||
whiteSpace: 'pre-wrap',
|
whiteSpace: 'pre-wrap',
|
||||||
}}
|
}}
|
||||||
@@ -449,7 +413,10 @@ export function VariablesInput({
|
|||||||
if (el) overlayRefs.current[assignment.id] = el
|
if (el) overlayRefs.current[assignment.id] = el
|
||||||
}}
|
}}
|
||||||
className='pointer-events-none absolute inset-0 flex items-start overflow-auto bg-transparent px-3 py-2 font-mono text-sm'
|
className='pointer-events-none absolute inset-0 flex items-start overflow-auto bg-transparent px-3 py-2 font-mono text-sm'
|
||||||
style={{ scrollbarWidth: 'none' }}
|
style={{
|
||||||
|
fontFamily: 'inherit',
|
||||||
|
lineHeight: 'inherit',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className='w-full whitespace-pre-wrap break-words'>
|
<div className='w-full whitespace-pre-wrap break-words'>
|
||||||
{formatDisplayText(assignment.value || '', {
|
{formatDisplayText(assignment.value || '', {
|
||||||
@@ -474,34 +441,21 @@ export function VariablesInput({
|
|||||||
e.target.selectionStart ?? undefined
|
e.target.selectionStart ?? undefined
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
onFocus={() => {
|
onFocus={() => {
|
||||||
if (!isReadOnly && !assignment.value?.trim()) {
|
if (!isPreview && !disabled && !assignment.value?.trim()) {
|
||||||
setActiveFieldId(assignment.id)
|
setActiveFieldId(assignment.id)
|
||||||
setCursorPosition(0)
|
setCursorPosition(0)
|
||||||
setShowTags(true)
|
setShowTags(true)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onScroll={(e) =>
|
|
||||||
syncOverlayScroll(assignment.id, e.currentTarget.scrollLeft)
|
|
||||||
}
|
|
||||||
onPaste={() =>
|
|
||||||
setTimeout(() => {
|
|
||||||
const input = valueInputRefs.current[assignment.id]
|
|
||||||
if (input)
|
|
||||||
syncOverlayScroll(
|
|
||||||
assignment.id,
|
|
||||||
(input as HTMLInputElement).scrollLeft
|
|
||||||
)
|
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
placeholder={`${assignment.type} value`}
|
placeholder={`${assignment.type} value`}
|
||||||
disabled={isReadOnly}
|
disabled={isPreview || disabled}
|
||||||
autoComplete='off'
|
autoComplete='off'
|
||||||
className={cn(
|
className={cn(
|
||||||
'allow-scroll w-full overflow-x-auto overflow-y-hidden text-transparent caret-foreground',
|
'allow-scroll w-full overflow-auto text-transparent caret-foreground',
|
||||||
dragHighlight[assignment.id] && 'ring-2 ring-blue-500 ring-offset-2'
|
dragHighlight[assignment.id] && 'ring-2 ring-blue-500 ring-offset-2'
|
||||||
)}
|
)}
|
||||||
|
style={{ overflowX: 'auto' }}
|
||||||
onDrop={(e) => handleDrop(e, assignment.id)}
|
onDrop={(e) => handleDrop(e, assignment.id)}
|
||||||
onDragOver={(e) => handleDragOver(e, assignment.id)}
|
onDragOver={(e) => handleDragOver(e, assignment.id)}
|
||||||
onDragLeave={(e) => handleDragLeave(e, assignment.id)}
|
onDragLeave={(e) => handleDragLeave(e, assignment.id)}
|
||||||
@@ -511,7 +465,7 @@ export function VariablesInput({
|
|||||||
if (el) overlayRefs.current[assignment.id] = el
|
if (el) overlayRefs.current[assignment.id] = el
|
||||||
}}
|
}}
|
||||||
className='pointer-events-none absolute inset-0 flex items-center overflow-x-auto bg-transparent px-[8px] py-[6px] font-medium font-sans text-sm'
|
className='pointer-events-none absolute inset-0 flex items-center overflow-x-auto bg-transparent px-[8px] py-[6px] font-medium font-sans text-sm'
|
||||||
style={{ scrollbarWidth: 'none' }}
|
style={{ overflowX: 'auto' }}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className='w-full whitespace-pre'
|
className='w-full whitespace-pre'
|
||||||
|
|||||||
@@ -284,37 +284,22 @@ const renderLabel = (
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{showCanonicalToggle && (
|
{showCanonicalToggle && (
|
||||||
<Tooltip.Root>
|
<button
|
||||||
<Tooltip.Trigger asChild>
|
type='button'
|
||||||
<button
|
className='flex h-[12px] w-[12px] flex-shrink-0 items-center justify-center bg-transparent p-0 disabled:cursor-not-allowed disabled:opacity-50'
|
||||||
type='button'
|
onClick={canonicalToggle?.onToggle}
|
||||||
className='flex h-[12px] w-[12px] flex-shrink-0 items-center justify-center bg-transparent p-0 disabled:cursor-not-allowed disabled:opacity-50'
|
disabled={canonicalToggleDisabledResolved}
|
||||||
onClick={canonicalToggle?.onToggle}
|
aria-label={canonicalToggle?.mode === 'advanced' ? 'Use selector' : 'Enter manual ID'}
|
||||||
disabled={canonicalToggleDisabledResolved}
|
>
|
||||||
aria-label={
|
<ArrowLeftRight
|
||||||
canonicalToggle?.mode === 'advanced'
|
className={cn(
|
||||||
? 'Switch to selector'
|
'!h-[12px] !w-[12px]',
|
||||||
: 'Switch to manual ID'
|
canonicalToggle?.mode === 'advanced'
|
||||||
}
|
? 'text-[var(--text-primary)]'
|
||||||
>
|
: 'text-[var(--text-secondary)]'
|
||||||
<ArrowLeftRight
|
)}
|
||||||
className={cn(
|
/>
|
||||||
'!h-[12px] !w-[12px]',
|
</button>
|
||||||
canonicalToggle?.mode === 'advanced'
|
|
||||||
? 'text-[var(--text-primary)]'
|
|
||||||
: 'text-[var(--text-secondary)]'
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</Tooltip.Trigger>
|
|
||||||
<Tooltip.Content side='top'>
|
|
||||||
<p>
|
|
||||||
{canonicalToggle?.mode === 'advanced'
|
|
||||||
? 'Switch to selector'
|
|
||||||
: 'Switch to manual ID'}
|
|
||||||
</p>
|
|
||||||
</Tooltip.Content>
|
|
||||||
</Tooltip.Root>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -338,11 +323,6 @@ const arePropsEqual = (prevProps: SubBlockProps, nextProps: SubBlockProps): bool
|
|||||||
const configEqual =
|
const configEqual =
|
||||||
prevProps.config.id === nextProps.config.id && prevProps.config.type === nextProps.config.type
|
prevProps.config.id === nextProps.config.id && prevProps.config.type === nextProps.config.type
|
||||||
|
|
||||||
const canonicalToggleEqual =
|
|
||||||
!!prevProps.canonicalToggle === !!nextProps.canonicalToggle &&
|
|
||||||
prevProps.canonicalToggle?.mode === nextProps.canonicalToggle?.mode &&
|
|
||||||
prevProps.canonicalToggle?.disabled === nextProps.canonicalToggle?.disabled
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
prevProps.blockId === nextProps.blockId &&
|
prevProps.blockId === nextProps.blockId &&
|
||||||
configEqual &&
|
configEqual &&
|
||||||
@@ -351,7 +331,8 @@ const arePropsEqual = (prevProps: SubBlockProps, nextProps: SubBlockProps): bool
|
|||||||
prevProps.disabled === nextProps.disabled &&
|
prevProps.disabled === nextProps.disabled &&
|
||||||
prevProps.fieldDiffStatus === nextProps.fieldDiffStatus &&
|
prevProps.fieldDiffStatus === nextProps.fieldDiffStatus &&
|
||||||
prevProps.allowExpandInPreview === nextProps.allowExpandInPreview &&
|
prevProps.allowExpandInPreview === nextProps.allowExpandInPreview &&
|
||||||
canonicalToggleEqual
|
prevProps.canonicalToggle?.mode === nextProps.canonicalToggle?.mode &&
|
||||||
|
prevProps.canonicalToggle?.disabled === nextProps.canonicalToggle?.disabled
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,11 +13,7 @@ interface UseCanvasContextMenuProps {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook for managing workflow canvas context menus.
|
* Hook for managing workflow canvas context menus.
|
||||||
*
|
* Handles right-click events, menu state, click-outside detection, and block info extraction.
|
||||||
* Handles right-click events on nodes, pane, and selections with proper multi-select behavior.
|
|
||||||
*
|
|
||||||
* @param props - Hook configuration
|
|
||||||
* @returns Context menu state and handlers
|
|
||||||
*/
|
*/
|
||||||
export function useCanvasContextMenu({ blocks, getNodes, setNodes }: UseCanvasContextMenuProps) {
|
export function useCanvasContextMenu({ blocks, getNodes, setNodes }: UseCanvasContextMenuProps) {
|
||||||
const [activeMenu, setActiveMenu] = useState<MenuType>(null)
|
const [activeMenu, setActiveMenu] = useState<MenuType>(null)
|
||||||
@@ -50,29 +46,19 @@ export function useCanvasContextMenu({ blocks, getNodes, setNodes }: UseCanvasCo
|
|||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
|
|
||||||
const isMultiSelect = event.shiftKey || event.metaKey || event.ctrlKey
|
const isMultiSelect = event.shiftKey || event.metaKey || event.ctrlKey
|
||||||
const currentSelectedNodes = getNodes().filter((n) => n.selected)
|
setNodes((nodes) =>
|
||||||
const isClickedNodeSelected = currentSelectedNodes.some((n) => n.id === node.id)
|
nodes.map((n) => ({
|
||||||
|
...n,
|
||||||
|
selected: isMultiSelect ? (n.id === node.id ? true : n.selected) : n.id === node.id,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
let nodesToUse: Node[]
|
const selectedNodes = getNodes().filter((n) => n.selected)
|
||||||
if (isClickedNodeSelected) {
|
const nodesToUse = isMultiSelect
|
||||||
nodesToUse = currentSelectedNodes
|
? selectedNodes.some((n) => n.id === node.id)
|
||||||
} else if (isMultiSelect) {
|
? selectedNodes
|
||||||
nodesToUse = [...currentSelectedNodes, node]
|
: [...selectedNodes, node]
|
||||||
setNodes((nodes) =>
|
: [node]
|
||||||
nodes.map((n) => ({
|
|
||||||
...n,
|
|
||||||
selected: n.id === node.id ? true : n.selected,
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
nodesToUse = [node]
|
|
||||||
setNodes((nodes) =>
|
|
||||||
nodes.map((n) => ({
|
|
||||||
...n,
|
|
||||||
selected: n.id === node.id,
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
setPosition({ x: event.clientX, y: event.clientY })
|
setPosition({ x: event.clientX, y: event.clientY })
|
||||||
setSelectedBlocks(nodesToBlockInfos(nodesToUse))
|
setSelectedBlocks(nodesToBlockInfos(nodesToUse))
|
||||||
|
|||||||
@@ -27,13 +27,18 @@ export function useContextMenu({ onContextMenu }: UseContextMenuProps = {}) {
|
|||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
const [position, setPosition] = useState<ContextMenuPosition>({ x: 0, y: 0 })
|
const [position, setPosition] = useState<ContextMenuPosition>({ x: 0, y: 0 })
|
||||||
const menuRef = useRef<HTMLDivElement>(null)
|
const menuRef = useRef<HTMLDivElement>(null)
|
||||||
|
// Used to prevent click-outside dismissal when trigger is clicked
|
||||||
const dismissPreventedRef = useRef(false)
|
const dismissPreventedRef = useRef(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle right-click event
|
||||||
|
*/
|
||||||
const handleContextMenu = useCallback(
|
const handleContextMenu = useCallback(
|
||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
|
||||||
|
// Calculate position relative to viewport
|
||||||
const x = e.clientX
|
const x = e.clientX
|
||||||
const y = e.clientY
|
const y = e.clientY
|
||||||
|
|
||||||
@@ -45,10 +50,17 @@ export function useContextMenu({ onContextMenu }: UseContextMenuProps = {}) {
|
|||||||
[onContextMenu]
|
[onContextMenu]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the context menu
|
||||||
|
*/
|
||||||
const closeMenu = useCallback(() => {
|
const closeMenu = useCallback(() => {
|
||||||
setIsOpen(false)
|
setIsOpen(false)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent the next click-outside from dismissing the menu.
|
||||||
|
* Call this on pointerdown of a toggle trigger to allow proper toggle behavior.
|
||||||
|
*/
|
||||||
const preventDismiss = useCallback(() => {
|
const preventDismiss = useCallback(() => {
|
||||||
dismissPreventedRef.current = true
|
dismissPreventedRef.current = true
|
||||||
}, [])
|
}, [])
|
||||||
@@ -60,6 +72,7 @@ export function useContextMenu({ onContextMenu }: UseContextMenuProps = {}) {
|
|||||||
if (!isOpen) return
|
if (!isOpen) return
|
||||||
|
|
||||||
const handleClickOutside = (e: MouseEvent) => {
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
|
// Check if dismissal was prevented (e.g., by toggle trigger's pointerdown)
|
||||||
if (dismissPreventedRef.current) {
|
if (dismissPreventedRef.current) {
|
||||||
dismissPreventedRef.current = false
|
dismissPreventedRef.current = false
|
||||||
return
|
return
|
||||||
@@ -69,6 +82,7 @@ export function useContextMenu({ onContextMenu }: UseContextMenuProps = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Small delay to prevent immediate close from the same click that opened the menu
|
||||||
const timeoutId = setTimeout(() => {
|
const timeoutId = setTimeout(() => {
|
||||||
document.addEventListener('click', handleClickOutside)
|
document.addEventListener('click', handleClickOutside)
|
||||||
}, 0)
|
}, 0)
|
||||||
|
|||||||
@@ -214,6 +214,15 @@ export const A2ABlock: BlockConfig<A2AResponse> = {
|
|||||||
],
|
],
|
||||||
config: {
|
config: {
|
||||||
tool: (params) => params.operation as string,
|
tool: (params) => params.operation as string,
|
||||||
|
params: (params) => {
|
||||||
|
const { fileUpload, fileReference, ...rest } = params
|
||||||
|
const hasFileUpload = Array.isArray(fileUpload) ? fileUpload.length > 0 : !!fileUpload
|
||||||
|
const files = hasFileUpload ? fileUpload : fileReference
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
|
...(files ? { files } : {}),
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
inputs: {
|
inputs: {
|
||||||
|
|||||||
@@ -581,18 +581,6 @@ export const GmailV2Block: BlockConfig<GmailToolResponse> = {
|
|||||||
results: { type: 'json', description: 'Search/read summary results' },
|
results: { type: 'json', description: 'Search/read summary results' },
|
||||||
attachments: { type: 'json', description: 'Downloaded attachments (if enabled)' },
|
attachments: { type: 'json', description: 'Downloaded attachments (if enabled)' },
|
||||||
|
|
||||||
// Draft-specific outputs
|
|
||||||
draftId: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'Draft ID',
|
|
||||||
condition: { field: 'operation', value: 'draft_gmail' },
|
|
||||||
},
|
|
||||||
messageId: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'Gmail message ID for the draft',
|
|
||||||
condition: { field: 'operation', value: 'draft_gmail' },
|
|
||||||
},
|
|
||||||
|
|
||||||
// Trigger outputs (unchanged)
|
// Trigger outputs (unchanged)
|
||||||
email_id: { type: 'string', description: 'Gmail message ID' },
|
email_id: { type: 'string', description: 'Gmail message ID' },
|
||||||
thread_id: { type: 'string', description: 'Gmail thread ID' },
|
thread_id: { type: 'string', description: 'Gmail thread ID' },
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ export const SpotifyBlock: BlockConfig<ToolResponse> = {
|
|||||||
type: 'spotify',
|
type: 'spotify',
|
||||||
name: 'Spotify',
|
name: 'Spotify',
|
||||||
description: 'Search music, manage playlists, control playback, and access your library',
|
description: 'Search music, manage playlists, control playback, and access your library',
|
||||||
|
hideFromToolbar: true,
|
||||||
authMode: AuthMode.OAuth,
|
authMode: AuthMode.OAuth,
|
||||||
longDescription:
|
longDescription:
|
||||||
'Integrate Spotify into your workflow. Search for tracks, albums, artists, and playlists. Manage playlists, access your library, control playback, browse podcasts and audiobooks.',
|
'Integrate Spotify into your workflow. Search for tracks, albums, artists, and playlists. Manage playlists, access your library, control playback, browse podcasts and audiobooks.',
|
||||||
docsLink: 'https://docs.sim.ai/tools/spotify',
|
docsLink: 'https://docs.sim.ai/tools/spotify',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
hideFromToolbar: true,
|
|
||||||
bgColor: '#000000',
|
bgColor: '#000000',
|
||||||
icon: SpotifyIcon,
|
icon: SpotifyIcon,
|
||||||
subBlocks: [
|
subBlocks: [
|
||||||
|
|||||||
@@ -661,25 +661,12 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e
|
|||||||
placeholder: 'folder/subfolder/',
|
placeholder: 'folder/subfolder/',
|
||||||
condition: { field: 'operation', value: 'storage_upload' },
|
condition: { field: 'operation', value: 'storage_upload' },
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'file',
|
|
||||||
title: 'File',
|
|
||||||
type: 'file-upload',
|
|
||||||
canonicalParamId: 'fileData',
|
|
||||||
placeholder: 'Upload file to storage',
|
|
||||||
condition: { field: 'operation', value: 'storage_upload' },
|
|
||||||
mode: 'basic',
|
|
||||||
multiple: false,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'fileContent',
|
id: 'fileContent',
|
||||||
title: 'File Content',
|
title: 'File Content',
|
||||||
type: 'code',
|
type: 'code',
|
||||||
canonicalParamId: 'fileData',
|
|
||||||
placeholder: 'Base64 encoded for binary files, or plain text',
|
placeholder: 'Base64 encoded for binary files, or plain text',
|
||||||
condition: { field: 'operation', value: 'storage_upload' },
|
condition: { field: 'operation', value: 'storage_upload' },
|
||||||
mode: 'advanced',
|
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { eq } from 'drizzle-orm'
|
import { eq } from 'drizzle-orm'
|
||||||
import { refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
import { refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||||
import type { BlockOutput } from '@/blocks/types'
|
import type { BlockOutput } from '@/blocks/types'
|
||||||
import { BlockType, DEFAULTS, EVALUATOR } from '@/executor/constants'
|
import { BlockType, DEFAULTS, EVALUATOR, HTTP } from '@/executor/constants'
|
||||||
import type { BlockHandler, ExecutionContext } from '@/executor/types'
|
import type { BlockHandler, ExecutionContext } from '@/executor/types'
|
||||||
import { buildAPIUrl, buildAuthHeaders, extractAPIErrorMessage } from '@/executor/utils/http'
|
import { buildAPIUrl, extractAPIErrorMessage } from '@/executor/utils/http'
|
||||||
import { isJSONString, parseJSON, stringifyJSON } from '@/executor/utils/json'
|
import { isJSONString, parseJSON, stringifyJSON } from '@/executor/utils/json'
|
||||||
import { validateModelProvider } from '@/executor/utils/permission-check'
|
import { validateModelProvider } from '@/executor/utils/permission-check'
|
||||||
import { calculateCost, getProviderFromModel } from '@/providers/utils'
|
import { calculateCost, getProviderFromModel } from '@/providers/utils'
|
||||||
@@ -143,7 +143,9 @@ export class EvaluatorBlockHandler implements BlockHandler {
|
|||||||
|
|
||||||
const response = await fetch(url.toString(), {
|
const response = await fetch(url.toString(), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: await buildAuthHeaders(),
|
headers: {
|
||||||
|
'Content-Type': HTTP.CONTENT_TYPE.JSON,
|
||||||
|
},
|
||||||
body: stringifyJSON(providerRequest),
|
body: stringifyJSON(providerRequest),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ import type { BlockOutput } from '@/blocks/types'
|
|||||||
import {
|
import {
|
||||||
BlockType,
|
BlockType,
|
||||||
DEFAULTS,
|
DEFAULTS,
|
||||||
|
HTTP,
|
||||||
isAgentBlockType,
|
isAgentBlockType,
|
||||||
isRouterV2BlockType,
|
isRouterV2BlockType,
|
||||||
ROUTER,
|
ROUTER,
|
||||||
} from '@/executor/constants'
|
} from '@/executor/constants'
|
||||||
import type { BlockHandler, ExecutionContext } from '@/executor/types'
|
import type { BlockHandler, ExecutionContext } from '@/executor/types'
|
||||||
import { buildAuthHeaders } from '@/executor/utils/http'
|
|
||||||
import { validateModelProvider } from '@/executor/utils/permission-check'
|
import { validateModelProvider } from '@/executor/utils/permission-check'
|
||||||
import { calculateCost, getProviderFromModel } from '@/providers/utils'
|
import { calculateCost, getProviderFromModel } from '@/providers/utils'
|
||||||
import type { SerializedBlock } from '@/serializer/types'
|
import type { SerializedBlock } from '@/serializer/types'
|
||||||
@@ -118,7 +118,9 @@ export class RouterBlockHandler implements BlockHandler {
|
|||||||
|
|
||||||
const response = await fetch(url.toString(), {
|
const response = await fetch(url.toString(), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: await buildAuthHeaders(),
|
headers: {
|
||||||
|
'Content-Type': HTTP.CONTENT_TYPE.JSON,
|
||||||
|
},
|
||||||
body: JSON.stringify(providerRequest),
|
body: JSON.stringify(providerRequest),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -275,7 +277,9 @@ export class RouterBlockHandler implements BlockHandler {
|
|||||||
|
|
||||||
const response = await fetch(url.toString(), {
|
const response = await fetch(url.toString(), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: await buildAuthHeaders(),
|
headers: {
|
||||||
|
'Content-Type': HTTP.CONTENT_TYPE.JSON,
|
||||||
|
},
|
||||||
body: JSON.stringify(providerRequest),
|
body: JSON.stringify(providerRequest),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
|
import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs'
|
||||||
import { normalizeName } from '@/executor/constants'
|
import { normalizeName } from '@/executor/constants'
|
||||||
import type { ExecutionContext } from '@/executor/types'
|
import type { ExecutionContext } from '@/executor/types'
|
||||||
import type { OutputSchema } from '@/executor/utils/block-reference'
|
import type { OutputSchema } from '@/executor/utils/block-reference'
|
||||||
import type { SerializedBlock } from '@/serializer/types'
|
|
||||||
import type { ToolConfig } from '@/tools/types'
|
|
||||||
import { getTool } from '@/tools/utils'
|
|
||||||
|
|
||||||
export interface BlockDataCollection {
|
export interface BlockDataCollection {
|
||||||
blockData: Record<string, unknown>
|
blockData: Record<string, unknown>
|
||||||
@@ -11,32 +9,6 @@ export interface BlockDataCollection {
|
|||||||
blockOutputSchemas: Record<string, OutputSchema>
|
blockOutputSchemas: Record<string, OutputSchema>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getBlockSchema(
|
|
||||||
block: SerializedBlock,
|
|
||||||
toolConfig?: ToolConfig
|
|
||||||
): OutputSchema | undefined {
|
|
||||||
const isTrigger =
|
|
||||||
block.metadata?.category === 'triggers' ||
|
|
||||||
(block.config?.params as Record<string, unknown> | undefined)?.triggerMode === true
|
|
||||||
|
|
||||||
// Triggers use saved outputs (defines the trigger payload schema)
|
|
||||||
if (isTrigger && block.outputs && Object.keys(block.outputs).length > 0) {
|
|
||||||
return block.outputs as OutputSchema
|
|
||||||
}
|
|
||||||
|
|
||||||
// When a tool is selected, tool outputs are the source of truth
|
|
||||||
if (toolConfig?.outputs && Object.keys(toolConfig.outputs).length > 0) {
|
|
||||||
return toolConfig.outputs as OutputSchema
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to saved outputs for blocks without tools
|
|
||||||
if (block.outputs && Object.keys(block.outputs).length > 0) {
|
|
||||||
return block.outputs as OutputSchema
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
export function collectBlockData(ctx: ExecutionContext): BlockDataCollection {
|
export function collectBlockData(ctx: ExecutionContext): BlockDataCollection {
|
||||||
const blockData: Record<string, unknown> = {}
|
const blockData: Record<string, unknown> = {}
|
||||||
const blockNameMapping: Record<string, string> = {}
|
const blockNameMapping: Record<string, string> = {}
|
||||||
@@ -46,21 +18,24 @@ export function collectBlockData(ctx: ExecutionContext): BlockDataCollection {
|
|||||||
if (state.output !== undefined) {
|
if (state.output !== undefined) {
|
||||||
blockData[id] = state.output
|
blockData[id] = state.output
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const workflowBlocks = ctx.workflow?.blocks ?? []
|
const workflowBlock = ctx.workflow?.blocks?.find((b) => b.id === id)
|
||||||
for (const block of workflowBlocks) {
|
if (!workflowBlock) continue
|
||||||
const id = block.id
|
|
||||||
|
|
||||||
if (block.metadata?.name) {
|
if (workflowBlock.metadata?.name) {
|
||||||
blockNameMapping[normalizeName(block.metadata.name)] = id
|
blockNameMapping[normalizeName(workflowBlock.metadata.name)] = id
|
||||||
}
|
}
|
||||||
|
|
||||||
const toolId = block.config?.tool
|
const blockType = workflowBlock.metadata?.id
|
||||||
const toolConfig = toolId ? getTool(toolId) : undefined
|
if (blockType) {
|
||||||
const schema = getBlockSchema(block, toolConfig)
|
const params = workflowBlock.config?.params as Record<string, unknown> | undefined
|
||||||
if (schema && Object.keys(schema).length > 0) {
|
const subBlocks = params
|
||||||
blockOutputSchemas[id] = schema
|
? Object.fromEntries(Object.entries(params).map(([k, v]) => [k, { value: v }]))
|
||||||
|
: undefined
|
||||||
|
const schema = getBlockOutputs(blockType, subBlocks)
|
||||||
|
if (schema && Object.keys(schema).length > 0) {
|
||||||
|
blockOutputSchemas[id] = schema
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
/**
|
|
||||||
* Formats a JavaScript/TypeScript value as a code literal for the target language.
|
|
||||||
* Handles special cases like null, undefined, booleans, and Python-specific number representations.
|
|
||||||
*
|
|
||||||
* @param value - The value to format
|
|
||||||
* @param language - Target language ('javascript' or 'python')
|
|
||||||
* @returns A string literal representation valid in the target language
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* formatLiteralForCode(null, 'python') // => 'None'
|
|
||||||
* formatLiteralForCode(true, 'python') // => 'True'
|
|
||||||
* formatLiteralForCode(NaN, 'python') // => "float('nan')"
|
|
||||||
* formatLiteralForCode("hello", 'javascript') // => '"hello"'
|
|
||||||
* formatLiteralForCode({a: 1}, 'python') // => "json.loads('{\"a\":1}')"
|
|
||||||
*/
|
|
||||||
export function formatLiteralForCode(value: unknown, language: 'javascript' | 'python'): string {
|
|
||||||
const isPython = language === 'python'
|
|
||||||
|
|
||||||
if (value === undefined) {
|
|
||||||
return isPython ? 'None' : 'undefined'
|
|
||||||
}
|
|
||||||
if (value === null) {
|
|
||||||
return isPython ? 'None' : 'null'
|
|
||||||
}
|
|
||||||
if (typeof value === 'boolean') {
|
|
||||||
return isPython ? (value ? 'True' : 'False') : String(value)
|
|
||||||
}
|
|
||||||
if (typeof value === 'number') {
|
|
||||||
if (Number.isNaN(value)) {
|
|
||||||
return isPython ? "float('nan')" : 'NaN'
|
|
||||||
}
|
|
||||||
if (value === Number.POSITIVE_INFINITY) {
|
|
||||||
return isPython ? "float('inf')" : 'Infinity'
|
|
||||||
}
|
|
||||||
if (value === Number.NEGATIVE_INFINITY) {
|
|
||||||
return isPython ? "float('-inf')" : '-Infinity'
|
|
||||||
}
|
|
||||||
return String(value)
|
|
||||||
}
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
return JSON.stringify(value)
|
|
||||||
}
|
|
||||||
// Objects and arrays - Python needs json.loads() because JSON true/false/null aren't valid Python
|
|
||||||
if (isPython) {
|
|
||||||
return `json.loads(${JSON.stringify(JSON.stringify(value))})`
|
|
||||||
}
|
|
||||||
return JSON.stringify(value)
|
|
||||||
}
|
|
||||||
@@ -378,30 +378,8 @@ function buildManualTriggerOutput(
|
|||||||
return mergeFilesIntoOutput(output, workflowInput)
|
return mergeFilesIntoOutput(output, workflowInput)
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildIntegrationTriggerOutput(
|
function buildIntegrationTriggerOutput(workflowInput: unknown): NormalizedBlockOutput {
|
||||||
workflowInput: unknown,
|
return isPlainObject(workflowInput) ? (workflowInput as NormalizedBlockOutput) : {}
|
||||||
structuredInput: Record<string, unknown>,
|
|
||||||
hasStructured: boolean
|
|
||||||
): NormalizedBlockOutput {
|
|
||||||
const output: NormalizedBlockOutput = {}
|
|
||||||
|
|
||||||
if (hasStructured) {
|
|
||||||
for (const [key, value] of Object.entries(structuredInput)) {
|
|
||||||
output[key] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPlainObject(workflowInput)) {
|
|
||||||
for (const [key, value] of Object.entries(workflowInput)) {
|
|
||||||
if (value !== undefined && value !== null) {
|
|
||||||
output[key] = value
|
|
||||||
} else if (!Object.hasOwn(output, key)) {
|
|
||||||
output[key] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return mergeFilesIntoOutput(output, workflowInput)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractSubBlocks(block: SerializedBlock): Record<string, unknown> | undefined {
|
function extractSubBlocks(block: SerializedBlock): Record<string, unknown> | undefined {
|
||||||
@@ -450,7 +428,7 @@ export function buildStartBlockOutput(options: StartBlockOutputOptions): Normali
|
|||||||
return buildManualTriggerOutput(finalInput, workflowInput)
|
return buildManualTriggerOutput(finalInput, workflowInput)
|
||||||
|
|
||||||
case StartBlockPath.EXTERNAL_TRIGGER:
|
case StartBlockPath.EXTERNAL_TRIGGER:
|
||||||
return buildIntegrationTriggerOutput(workflowInput, structuredInput, hasStructured)
|
return buildIntegrationTriggerOutput(workflowInput)
|
||||||
|
|
||||||
case StartBlockPath.LEGACY_STARTER:
|
case StartBlockPath.LEGACY_STARTER:
|
||||||
return buildLegacyStarterOutput(
|
return buildLegacyStarterOutput(
|
||||||
|
|||||||
@@ -157,14 +157,7 @@ export class VariableResolver {
|
|||||||
|
|
||||||
let replacementError: Error | null = null
|
let replacementError: Error | null = null
|
||||||
|
|
||||||
const blockType = block?.metadata?.id
|
// Use generic utility for smart variable reference replacement
|
||||||
const language =
|
|
||||||
blockType === BlockType.FUNCTION
|
|
||||||
? ((block?.config?.params as Record<string, unknown> | undefined)?.language as
|
|
||||||
| string
|
|
||||||
| undefined)
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
let result = replaceValidReferences(template, (match) => {
|
let result = replaceValidReferences(template, (match) => {
|
||||||
if (replacementError) return match
|
if (replacementError) return match
|
||||||
|
|
||||||
@@ -174,7 +167,14 @@ export class VariableResolver {
|
|||||||
return match
|
return match
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.blockResolver.formatValueForBlock(resolved, blockType, language)
|
const blockType = block?.metadata?.id
|
||||||
|
const isInTemplateLiteral =
|
||||||
|
blockType === BlockType.FUNCTION &&
|
||||||
|
template.includes('${') &&
|
||||||
|
template.includes('}') &&
|
||||||
|
template.includes('`')
|
||||||
|
|
||||||
|
return this.blockResolver.formatValueForBlock(resolved, blockType, isInTemplateLiteral)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
replacementError = error instanceof Error ? error : new Error(String(error))
|
replacementError = error instanceof Error ? error : new Error(String(error))
|
||||||
return match
|
return match
|
||||||
|
|||||||
@@ -257,9 +257,15 @@ describe('BlockResolver', () => {
|
|||||||
expect(result).toBe('"hello"')
|
expect(result).toBe('"hello"')
|
||||||
})
|
})
|
||||||
|
|
||||||
it.concurrent('should format object for function block', () => {
|
it.concurrent('should format string for function block in template literal', () => {
|
||||||
const resolver = new BlockResolver(createTestWorkflow())
|
const resolver = new BlockResolver(createTestWorkflow())
|
||||||
const result = resolver.formatValueForBlock({ a: 1 }, 'function')
|
const result = resolver.formatValueForBlock('hello', 'function', true)
|
||||||
|
expect(result).toBe('hello')
|
||||||
|
})
|
||||||
|
|
||||||
|
it.concurrent('should format object for function block in template literal', () => {
|
||||||
|
const resolver = new BlockResolver(createTestWorkflow())
|
||||||
|
const result = resolver.formatValueForBlock({ a: 1 }, 'function', true)
|
||||||
expect(result).toBe('{"a":1}')
|
expect(result).toBe('{"a":1}')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
|
import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs'
|
||||||
import {
|
import {
|
||||||
isReference,
|
isReference,
|
||||||
normalizeName,
|
normalizeName,
|
||||||
parseReferencePath,
|
parseReferencePath,
|
||||||
SPECIAL_REFERENCE_PREFIXES,
|
SPECIAL_REFERENCE_PREFIXES,
|
||||||
} from '@/executor/constants'
|
} from '@/executor/constants'
|
||||||
import { getBlockSchema } from '@/executor/utils/block-data'
|
|
||||||
import {
|
import {
|
||||||
InvalidFieldError,
|
InvalidFieldError,
|
||||||
type OutputSchema,
|
type OutputSchema,
|
||||||
resolveBlockReference,
|
resolveBlockReference,
|
||||||
} from '@/executor/utils/block-reference'
|
} from '@/executor/utils/block-reference'
|
||||||
import { formatLiteralForCode } from '@/executor/utils/code-formatting'
|
|
||||||
import {
|
import {
|
||||||
navigatePath,
|
navigatePath,
|
||||||
type ResolutionContext,
|
type ResolutionContext,
|
||||||
@@ -68,9 +67,15 @@ export class BlockResolver implements Resolver {
|
|||||||
blockData[blockId] = output
|
blockData[blockId] = output
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const blockType = block.metadata?.id
|
||||||
|
const params = block.config?.params as Record<string, unknown> | undefined
|
||||||
|
const subBlocks = params
|
||||||
|
? Object.fromEntries(Object.entries(params).map(([k, v]) => [k, { value: v }]))
|
||||||
|
: undefined
|
||||||
const toolId = block.config?.tool
|
const toolId = block.config?.tool
|
||||||
const toolConfig = toolId ? getTool(toolId) : undefined
|
const toolConfig = toolId ? getTool(toolId) : undefined
|
||||||
const outputSchema = getBlockSchema(block, toolConfig)
|
const outputSchema =
|
||||||
|
toolConfig?.outputs ?? (blockType ? getBlockOutputs(blockType, subBlocks) : block.outputs)
|
||||||
|
|
||||||
if (outputSchema && Object.keys(outputSchema).length > 0) {
|
if (outputSchema && Object.keys(outputSchema).length > 0) {
|
||||||
blockOutputSchemas[blockId] = outputSchema
|
blockOutputSchemas[blockId] = outputSchema
|
||||||
@@ -160,13 +165,17 @@ export class BlockResolver implements Resolver {
|
|||||||
return this.nameToBlockId.get(normalizeName(name))
|
return this.nameToBlockId.get(normalizeName(name))
|
||||||
}
|
}
|
||||||
|
|
||||||
public formatValueForBlock(value: any, blockType: string | undefined, language?: string): string {
|
public formatValueForBlock(
|
||||||
|
value: any,
|
||||||
|
blockType: string | undefined,
|
||||||
|
isInTemplateLiteral = false
|
||||||
|
): string {
|
||||||
if (blockType === 'condition') {
|
if (blockType === 'condition') {
|
||||||
return this.stringifyForCondition(value)
|
return this.stringifyForCondition(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (blockType === 'function') {
|
if (blockType === 'function') {
|
||||||
return this.formatValueForCodeContext(value, language)
|
return this.formatValueForCodeContext(value, isInTemplateLiteral)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (blockType === 'response') {
|
if (blockType === 'response') {
|
||||||
@@ -207,7 +216,29 @@ export class BlockResolver implements Resolver {
|
|||||||
return String(value)
|
return String(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
private formatValueForCodeContext(value: any, language?: string): string {
|
private formatValueForCodeContext(value: any, isInTemplateLiteral: boolean): string {
|
||||||
return formatLiteralForCode(value, language === 'python' ? 'python' : 'javascript')
|
if (isInTemplateLiteral) {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
if (typeof value === 'object' && value !== null) {
|
||||||
|
return JSON.stringify(value)
|
||||||
|
}
|
||||||
|
return String(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return JSON.stringify(value)
|
||||||
|
}
|
||||||
|
if (typeof value === 'object' && value !== null) {
|
||||||
|
return JSON.stringify(value)
|
||||||
|
}
|
||||||
|
if (value === undefined) {
|
||||||
|
return 'undefined'
|
||||||
|
}
|
||||||
|
if (value === null) {
|
||||||
|
return 'null'
|
||||||
|
}
|
||||||
|
return String(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,10 +30,7 @@ export function navigatePath(obj: any, path: string[]): any {
|
|||||||
const arrayMatch = part.match(/^([^[]+)(\[.+)$/)
|
const arrayMatch = part.match(/^([^[]+)(\[.+)$/)
|
||||||
if (arrayMatch) {
|
if (arrayMatch) {
|
||||||
const [, prop, bracketsPart] = arrayMatch
|
const [, prop, bracketsPart] = arrayMatch
|
||||||
current =
|
current = current[prop]
|
||||||
typeof current === 'object' && current !== null
|
|
||||||
? (current as Record<string, unknown>)[prop]
|
|
||||||
: undefined
|
|
||||||
if (current === undefined || current === null) {
|
if (current === undefined || current === null) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
@@ -52,10 +49,7 @@ export function navigatePath(obj: any, path: string[]): any {
|
|||||||
const index = Number.parseInt(part, 10)
|
const index = Number.parseInt(part, 10)
|
||||||
current = Array.isArray(current) ? current[index] : undefined
|
current = Array.isArray(current) ? current[index] : undefined
|
||||||
} else {
|
} else {
|
||||||
current =
|
current = current[part]
|
||||||
typeof current === 'object' && current !== null
|
|
||||||
? (current as Record<string, unknown>)[part]
|
|
||||||
: undefined
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return current
|
return current
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import { useUndoRedoStore } from '@/stores/undo-redo'
|
|||||||
import { useWorkflowDiffStore } from '@/stores/workflow-diff/store'
|
import { useWorkflowDiffStore } from '@/stores/workflow-diff/store'
|
||||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||||
import { filterNewEdges, filterValidEdges, mergeSubblockState } from '@/stores/workflows/utils'
|
import { filterNewEdges, mergeSubblockState } from '@/stores/workflows/utils'
|
||||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||||
import type { BlockState, Loop, Parallel, Position } from '@/stores/workflows/workflow/types'
|
import type { BlockState, Loop, Parallel, Position } from '@/stores/workflows/workflow/types'
|
||||||
|
|
||||||
@@ -226,12 +226,9 @@ export function useCollaborativeWorkflow() {
|
|||||||
case EDGES_OPERATIONS.BATCH_ADD_EDGES: {
|
case EDGES_OPERATIONS.BATCH_ADD_EDGES: {
|
||||||
const { edges } = payload
|
const { edges } = payload
|
||||||
if (Array.isArray(edges) && edges.length > 0) {
|
if (Array.isArray(edges) && edges.length > 0) {
|
||||||
const blocks = useWorkflowStore.getState().blocks
|
const newEdges = filterNewEdges(edges, useWorkflowStore.getState().edges)
|
||||||
const currentEdges = useWorkflowStore.getState().edges
|
|
||||||
const validEdges = filterValidEdges(edges, blocks)
|
|
||||||
const newEdges = filterNewEdges(validEdges, currentEdges)
|
|
||||||
if (newEdges.length > 0) {
|
if (newEdges.length > 0) {
|
||||||
useWorkflowStore.getState().batchAddEdges(newEdges, { skipValidation: true })
|
useWorkflowStore.getState().batchAddEdges(newEdges)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
@@ -680,10 +677,6 @@ export function useCollaborativeWorkflow() {
|
|||||||
previousPositions?: Map<string, { x: number; y: number; parentId?: string }>
|
previousPositions?: Map<string, { x: number; y: number; parentId?: string }>
|
||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
if (isBaselineDiffView) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isInActiveRoom()) {
|
if (!isInActiveRoom()) {
|
||||||
logger.debug('Skipping batch position update - not in active workflow')
|
logger.debug('Skipping batch position update - not in active workflow')
|
||||||
return
|
return
|
||||||
@@ -729,7 +722,7 @@ export function useCollaborativeWorkflow() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[isBaselineDiffView, addToQueue, activeWorkflowId, session?.user?.id, isInActiveRoom, undoRedo]
|
[addToQueue, activeWorkflowId, session?.user?.id, isInActiveRoom, undoRedo]
|
||||||
)
|
)
|
||||||
|
|
||||||
const collaborativeUpdateBlockName = useCallback(
|
const collaborativeUpdateBlockName = useCallback(
|
||||||
@@ -821,10 +814,6 @@ export function useCollaborativeWorkflow() {
|
|||||||
|
|
||||||
const collaborativeBatchToggleBlockEnabled = useCallback(
|
const collaborativeBatchToggleBlockEnabled = useCallback(
|
||||||
(ids: string[]) => {
|
(ids: string[]) => {
|
||||||
if (isBaselineDiffView) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ids.length === 0) return
|
if (ids.length === 0) return
|
||||||
|
|
||||||
const previousStates: Record<string, boolean> = {}
|
const previousStates: Record<string, boolean> = {}
|
||||||
@@ -857,7 +846,7 @@ export function useCollaborativeWorkflow() {
|
|||||||
|
|
||||||
undoRedo.recordBatchToggleEnabled(validIds, previousStates)
|
undoRedo.recordBatchToggleEnabled(validIds, previousStates)
|
||||||
},
|
},
|
||||||
[isBaselineDiffView, addToQueue, activeWorkflowId, session?.user?.id, undoRedo]
|
[addToQueue, activeWorkflowId, session?.user?.id, undoRedo]
|
||||||
)
|
)
|
||||||
|
|
||||||
const collaborativeBatchUpdateParent = useCallback(
|
const collaborativeBatchUpdateParent = useCallback(
|
||||||
@@ -869,10 +858,6 @@ export function useCollaborativeWorkflow() {
|
|||||||
affectedEdges: Edge[]
|
affectedEdges: Edge[]
|
||||||
}>
|
}>
|
||||||
) => {
|
) => {
|
||||||
if (isBaselineDiffView) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isInActiveRoom()) {
|
if (!isInActiveRoom()) {
|
||||||
logger.debug('Skipping batch update parent - not in active workflow')
|
logger.debug('Skipping batch update parent - not in active workflow')
|
||||||
return
|
return
|
||||||
@@ -943,7 +928,7 @@ export function useCollaborativeWorkflow() {
|
|||||||
|
|
||||||
logger.debug('Batch updated parent for blocks', { updateCount: updates.length })
|
logger.debug('Batch updated parent for blocks', { updateCount: updates.length })
|
||||||
},
|
},
|
||||||
[isBaselineDiffView, isInActiveRoom, undoRedo, addToQueue, activeWorkflowId, session?.user?.id]
|
[isInActiveRoom, undoRedo, addToQueue, activeWorkflowId, session?.user?.id]
|
||||||
)
|
)
|
||||||
|
|
||||||
const collaborativeToggleBlockAdvancedMode = useCallback(
|
const collaborativeToggleBlockAdvancedMode = useCallback(
|
||||||
@@ -963,37 +948,18 @@ export function useCollaborativeWorkflow() {
|
|||||||
|
|
||||||
const collaborativeSetBlockCanonicalMode = useCallback(
|
const collaborativeSetBlockCanonicalMode = useCallback(
|
||||||
(id: string, canonicalId: string, canonicalMode: 'basic' | 'advanced') => {
|
(id: string, canonicalId: string, canonicalMode: 'basic' | 'advanced') => {
|
||||||
if (isBaselineDiffView) {
|
executeQueuedOperation(
|
||||||
return
|
BLOCK_OPERATIONS.UPDATE_CANONICAL_MODE,
|
||||||
}
|
OPERATION_TARGETS.BLOCK,
|
||||||
|
{ id, canonicalId, canonicalMode },
|
||||||
useWorkflowStore.getState().setBlockCanonicalMode(id, canonicalId, canonicalMode)
|
() => useWorkflowStore.getState().setBlockCanonicalMode(id, canonicalId, canonicalMode)
|
||||||
|
)
|
||||||
if (!activeWorkflowId) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const operationId = crypto.randomUUID()
|
|
||||||
addToQueue({
|
|
||||||
id: operationId,
|
|
||||||
operation: {
|
|
||||||
operation: BLOCK_OPERATIONS.UPDATE_CANONICAL_MODE,
|
|
||||||
target: OPERATION_TARGETS.BLOCK,
|
|
||||||
payload: { id, canonicalId, canonicalMode },
|
|
||||||
},
|
|
||||||
workflowId: activeWorkflowId,
|
|
||||||
userId: session?.user?.id || 'unknown',
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
[isBaselineDiffView, activeWorkflowId, addToQueue, session?.user?.id]
|
[executeQueuedOperation]
|
||||||
)
|
)
|
||||||
|
|
||||||
const collaborativeBatchToggleBlockHandles = useCallback(
|
const collaborativeBatchToggleBlockHandles = useCallback(
|
||||||
(ids: string[]) => {
|
(ids: string[]) => {
|
||||||
if (isBaselineDiffView) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ids.length === 0) return
|
if (ids.length === 0) return
|
||||||
|
|
||||||
const previousStates: Record<string, boolean> = {}
|
const previousStates: Record<string, boolean> = {}
|
||||||
@@ -1026,15 +992,11 @@ export function useCollaborativeWorkflow() {
|
|||||||
|
|
||||||
undoRedo.recordBatchToggleHandles(validIds, previousStates)
|
undoRedo.recordBatchToggleHandles(validIds, previousStates)
|
||||||
},
|
},
|
||||||
[isBaselineDiffView, addToQueue, activeWorkflowId, session?.user?.id, undoRedo]
|
[addToQueue, activeWorkflowId, session?.user?.id, undoRedo]
|
||||||
)
|
)
|
||||||
|
|
||||||
const collaborativeBatchAddEdges = useCallback(
|
const collaborativeBatchAddEdges = useCallback(
|
||||||
(edges: Edge[], options?: { skipUndoRedo?: boolean }) => {
|
(edges: Edge[], options?: { skipUndoRedo?: boolean }) => {
|
||||||
if (isBaselineDiffView) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isInActiveRoom()) {
|
if (!isInActiveRoom()) {
|
||||||
logger.debug('Skipping batch add edges - not in active workflow')
|
logger.debug('Skipping batch add edges - not in active workflow')
|
||||||
return false
|
return false
|
||||||
@@ -1042,11 +1004,7 @@ export function useCollaborativeWorkflow() {
|
|||||||
|
|
||||||
if (edges.length === 0) return false
|
if (edges.length === 0) return false
|
||||||
|
|
||||||
// Filter out invalid edges (e.g., edges targeting trigger blocks) and duplicates
|
const newEdges = filterNewEdges(edges, useWorkflowStore.getState().edges)
|
||||||
const blocks = useWorkflowStore.getState().blocks
|
|
||||||
const currentEdges = useWorkflowStore.getState().edges
|
|
||||||
const validEdges = filterValidEdges(edges, blocks)
|
|
||||||
const newEdges = filterNewEdges(validEdges, currentEdges)
|
|
||||||
if (newEdges.length === 0) return false
|
if (newEdges.length === 0) return false
|
||||||
|
|
||||||
const operationId = crypto.randomUUID()
|
const operationId = crypto.randomUUID()
|
||||||
@@ -1062,7 +1020,7 @@ export function useCollaborativeWorkflow() {
|
|||||||
userId: session?.user?.id || 'unknown',
|
userId: session?.user?.id || 'unknown',
|
||||||
})
|
})
|
||||||
|
|
||||||
useWorkflowStore.getState().batchAddEdges(newEdges, { skipValidation: true })
|
useWorkflowStore.getState().batchAddEdges(newEdges)
|
||||||
|
|
||||||
if (!options?.skipUndoRedo) {
|
if (!options?.skipUndoRedo) {
|
||||||
newEdges.forEach((edge) => undoRedo.recordAddEdge(edge.id))
|
newEdges.forEach((edge) => undoRedo.recordAddEdge(edge.id))
|
||||||
@@ -1070,15 +1028,11 @@ export function useCollaborativeWorkflow() {
|
|||||||
|
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
[isBaselineDiffView, addToQueue, activeWorkflowId, session?.user?.id, isInActiveRoom, undoRedo]
|
[addToQueue, activeWorkflowId, session?.user?.id, isInActiveRoom, undoRedo]
|
||||||
)
|
)
|
||||||
|
|
||||||
const collaborativeBatchRemoveEdges = useCallback(
|
const collaborativeBatchRemoveEdges = useCallback(
|
||||||
(edgeIds: string[], options?: { skipUndoRedo?: boolean }) => {
|
(edgeIds: string[], options?: { skipUndoRedo?: boolean }) => {
|
||||||
if (isBaselineDiffView) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isInActiveRoom()) {
|
if (!isInActiveRoom()) {
|
||||||
logger.debug('Skipping batch remove edges - not in active workflow')
|
logger.debug('Skipping batch remove edges - not in active workflow')
|
||||||
return false
|
return false
|
||||||
@@ -1128,7 +1082,7 @@ export function useCollaborativeWorkflow() {
|
|||||||
logger.info('Batch removed edges', { count: validEdgeIds.length })
|
logger.info('Batch removed edges', { count: validEdgeIds.length })
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
[isBaselineDiffView, isInActiveRoom, addToQueue, activeWorkflowId, session, undoRedo]
|
[isInActiveRoom, addToQueue, activeWorkflowId, session, undoRedo]
|
||||||
)
|
)
|
||||||
|
|
||||||
const collaborativeSetSubblockValue = useCallback(
|
const collaborativeSetSubblockValue = useCallback(
|
||||||
@@ -1204,10 +1158,6 @@ export function useCollaborativeWorkflow() {
|
|||||||
(blockId: string, subblockId: string, value: any) => {
|
(blockId: string, subblockId: string, value: any) => {
|
||||||
if (isApplyingRemoteChange.current) return
|
if (isApplyingRemoteChange.current) return
|
||||||
|
|
||||||
if (isBaselineDiffView) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isInActiveRoom()) {
|
if (!isInActiveRoom()) {
|
||||||
logger.debug('Skipping tag selection - not in active workflow', {
|
logger.debug('Skipping tag selection - not in active workflow', {
|
||||||
currentWorkflowId,
|
currentWorkflowId,
|
||||||
@@ -1235,14 +1185,7 @@ export function useCollaborativeWorkflow() {
|
|||||||
userId: session?.user?.id || 'unknown',
|
userId: session?.user?.id || 'unknown',
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
[
|
[addToQueue, currentWorkflowId, activeWorkflowId, session?.user?.id, isInActiveRoom]
|
||||||
isBaselineDiffView,
|
|
||||||
addToQueue,
|
|
||||||
currentWorkflowId,
|
|
||||||
activeWorkflowId,
|
|
||||||
session?.user?.id,
|
|
||||||
isInActiveRoom,
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const collaborativeUpdateLoopType = useCallback(
|
const collaborativeUpdateLoopType = useCallback(
|
||||||
@@ -1541,23 +1484,9 @@ export function useCollaborativeWorkflow() {
|
|||||||
|
|
||||||
if (blocks.length === 0) return false
|
if (blocks.length === 0) return false
|
||||||
|
|
||||||
// Filter out invalid edges (e.g., edges targeting trigger blocks)
|
|
||||||
// Combine existing blocks with new blocks for validation
|
|
||||||
const existingBlocks = useWorkflowStore.getState().blocks
|
|
||||||
const newBlocksMap = blocks.reduce(
|
|
||||||
(acc, block) => {
|
|
||||||
acc[block.id] = block
|
|
||||||
return acc
|
|
||||||
},
|
|
||||||
{} as Record<string, BlockState>
|
|
||||||
)
|
|
||||||
const allBlocks = { ...existingBlocks, ...newBlocksMap }
|
|
||||||
const validEdges = filterValidEdges(edges, allBlocks)
|
|
||||||
|
|
||||||
logger.info('Batch adding blocks collaboratively', {
|
logger.info('Batch adding blocks collaboratively', {
|
||||||
blockCount: blocks.length,
|
blockCount: blocks.length,
|
||||||
edgeCount: validEdges.length,
|
edgeCount: edges.length,
|
||||||
filteredEdges: edges.length - validEdges.length,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const operationId = crypto.randomUUID()
|
const operationId = crypto.randomUUID()
|
||||||
@@ -1567,18 +1496,16 @@ export function useCollaborativeWorkflow() {
|
|||||||
operation: {
|
operation: {
|
||||||
operation: BLOCKS_OPERATIONS.BATCH_ADD_BLOCKS,
|
operation: BLOCKS_OPERATIONS.BATCH_ADD_BLOCKS,
|
||||||
target: OPERATION_TARGETS.BLOCKS,
|
target: OPERATION_TARGETS.BLOCKS,
|
||||||
payload: { blocks, edges: validEdges, loops, parallels, subBlockValues },
|
payload: { blocks, edges, loops, parallels, subBlockValues },
|
||||||
},
|
},
|
||||||
workflowId: activeWorkflowId || '',
|
workflowId: activeWorkflowId || '',
|
||||||
userId: session?.user?.id || 'unknown',
|
userId: session?.user?.id || 'unknown',
|
||||||
})
|
})
|
||||||
|
|
||||||
useWorkflowStore.getState().batchAddBlocks(blocks, validEdges, subBlockValues, {
|
useWorkflowStore.getState().batchAddBlocks(blocks, edges, subBlockValues)
|
||||||
skipEdgeValidation: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!options?.skipUndoRedo) {
|
if (!options?.skipUndoRedo) {
|
||||||
undoRedo.recordBatchAddBlocks(blocks, validEdges, subBlockValues)
|
undoRedo.recordBatchAddBlocks(blocks, edges, subBlockValues)
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
@@ -1588,10 +1515,6 @@ export function useCollaborativeWorkflow() {
|
|||||||
|
|
||||||
const collaborativeBatchRemoveBlocks = useCallback(
|
const collaborativeBatchRemoveBlocks = useCallback(
|
||||||
(blockIds: string[], options?: { skipUndoRedo?: boolean }) => {
|
(blockIds: string[], options?: { skipUndoRedo?: boolean }) => {
|
||||||
if (isBaselineDiffView) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isInActiveRoom()) {
|
if (!isInActiveRoom()) {
|
||||||
logger.debug('Skipping batch remove blocks - not in active workflow')
|
logger.debug('Skipping batch remove blocks - not in active workflow')
|
||||||
return false
|
return false
|
||||||
@@ -1673,7 +1596,6 @@ export function useCollaborativeWorkflow() {
|
|||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
isBaselineDiffView,
|
|
||||||
addToQueue,
|
addToQueue,
|
||||||
activeWorkflowId,
|
activeWorkflowId,
|
||||||
session?.user?.id,
|
session?.user?.id,
|
||||||
|
|||||||
@@ -11,10 +11,9 @@ import { extractAndPersistCustomTools } from '@/lib/workflows/persistence/custom
|
|||||||
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
|
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
|
||||||
import { isValidKey } from '@/lib/workflows/sanitization/key-validation'
|
import { isValidKey } from '@/lib/workflows/sanitization/key-validation'
|
||||||
import { validateWorkflowState } from '@/lib/workflows/sanitization/validation'
|
import { validateWorkflowState } from '@/lib/workflows/sanitization/validation'
|
||||||
import { buildCanonicalIndex, isCanonicalPair } from '@/lib/workflows/subblocks/visibility'
|
|
||||||
import { TriggerUtils } from '@/lib/workflows/triggers/triggers'
|
import { TriggerUtils } from '@/lib/workflows/triggers/triggers'
|
||||||
import { getAllBlocks, getBlock } from '@/blocks/registry'
|
import { getAllBlocks, getBlock } from '@/blocks/registry'
|
||||||
import type { BlockConfig, SubBlockConfig } from '@/blocks/types'
|
import type { SubBlockConfig } from '@/blocks/types'
|
||||||
import { EDGE, normalizeName, RESERVED_BLOCK_NAMES } from '@/executor/constants'
|
import { EDGE, normalizeName, RESERVED_BLOCK_NAMES } from '@/executor/constants'
|
||||||
import { getUserPermissionConfig } from '@/executor/utils/permission-check'
|
import { getUserPermissionConfig } from '@/executor/utils/permission-check'
|
||||||
import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/workflow/utils'
|
import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/workflow/utils'
|
||||||
@@ -668,47 +667,11 @@ function createBlockFromParams(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (validatedInputs) {
|
|
||||||
updateCanonicalModesForInputs(blockState, Object.keys(validatedInputs), blockConfig)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return blockState
|
return blockState
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCanonicalModesForInputs(
|
|
||||||
block: { data?: { canonicalModes?: Record<string, 'basic' | 'advanced'> } },
|
|
||||||
inputKeys: string[],
|
|
||||||
blockConfig: BlockConfig
|
|
||||||
): void {
|
|
||||||
if (!blockConfig.subBlocks?.length) return
|
|
||||||
|
|
||||||
const canonicalIndex = buildCanonicalIndex(blockConfig.subBlocks)
|
|
||||||
const canonicalModeUpdates: Record<string, 'basic' | 'advanced'> = {}
|
|
||||||
|
|
||||||
for (const inputKey of inputKeys) {
|
|
||||||
const canonicalId = canonicalIndex.canonicalIdBySubBlockId[inputKey]
|
|
||||||
if (!canonicalId) continue
|
|
||||||
|
|
||||||
const group = canonicalIndex.groupsById[canonicalId]
|
|
||||||
if (!group || !isCanonicalPair(group)) continue
|
|
||||||
|
|
||||||
const isAdvanced = group.advancedIds.includes(inputKey)
|
|
||||||
const existingMode = canonicalModeUpdates[canonicalId]
|
|
||||||
|
|
||||||
if (!existingMode || isAdvanced) {
|
|
||||||
canonicalModeUpdates[canonicalId] = isAdvanced ? 'advanced' : 'basic'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(canonicalModeUpdates).length > 0) {
|
|
||||||
if (!block.data) block.data = {}
|
|
||||||
if (!block.data.canonicalModes) block.data.canonicalModes = {}
|
|
||||||
Object.assign(block.data.canonicalModes, canonicalModeUpdates)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalize tools array by adding back fields that were sanitized for training
|
* Normalize tools array by adding back fields that were sanitized for training
|
||||||
*/
|
*/
|
||||||
@@ -1691,15 +1654,6 @@ function applyOperationsToWorkflowState(
|
|||||||
block.data.collection = params.inputs.collection
|
block.data.collection = params.inputs.collection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const editBlockConfig = getBlock(block.type)
|
|
||||||
if (editBlockConfig) {
|
|
||||||
updateCanonicalModesForInputs(
|
|
||||||
block,
|
|
||||||
Object.keys(validationResult.validInputs),
|
|
||||||
editBlockConfig
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update basic properties
|
// Update basic properties
|
||||||
@@ -2302,15 +2256,6 @@ function applyOperationsToWorkflowState(
|
|||||||
existingBlock.subBlocks[key].value = sanitizedValue
|
existingBlock.subBlocks[key].value = sanitizedValue
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const existingBlockConfig = getBlock(existingBlock.type)
|
|
||||||
if (existingBlockConfig) {
|
|
||||||
updateCanonicalModesForInputs(
|
|
||||||
existingBlock,
|
|
||||||
Object.keys(validationResult.validInputs),
|
|
||||||
existingBlockConfig
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Special container types (loop, parallel) are not in the block registry but are valid
|
// Special container types (loop, parallel) are not in the block registry but are valid
|
||||||
|
|||||||
@@ -132,8 +132,6 @@ async function executeCode(request) {
|
|||||||
for (const [key, value] of Object.entries(contextVariables)) {
|
for (const [key, value] of Object.entries(contextVariables)) {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
await jail.set(key, undefined)
|
await jail.set(key, undefined)
|
||||||
} else if (value === null) {
|
|
||||||
await jail.set(key, null)
|
|
||||||
} else {
|
} else {
|
||||||
await jail.set(key, new ivm.ExternalCopy(value).copyInto())
|
await jail.set(key, new ivm.ExternalCopy(value).copyInto())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,17 +8,6 @@ const logger = createLogger('EmbeddingUtils')
|
|||||||
|
|
||||||
const MAX_TOKENS_PER_REQUEST = 8000
|
const MAX_TOKENS_PER_REQUEST = 8000
|
||||||
const MAX_CONCURRENT_BATCHES = env.KB_CONFIG_CONCURRENCY_LIMIT || 50
|
const MAX_CONCURRENT_BATCHES = env.KB_CONFIG_CONCURRENCY_LIMIT || 50
|
||||||
const EMBEDDING_DIMENSIONS = 1536
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the model supports custom dimensions.
|
|
||||||
* text-embedding-3-* models support the dimensions parameter.
|
|
||||||
* Checks for 'embedding-3' to handle Azure deployments with custom naming conventions.
|
|
||||||
*/
|
|
||||||
function supportsCustomDimensions(modelName: string): boolean {
|
|
||||||
const name = modelName.toLowerCase()
|
|
||||||
return name.includes('embedding-3') && !name.includes('ada')
|
|
||||||
}
|
|
||||||
|
|
||||||
export class EmbeddingAPIError extends Error {
|
export class EmbeddingAPIError extends Error {
|
||||||
public status: number
|
public status: number
|
||||||
@@ -104,19 +93,15 @@ async function getEmbeddingConfig(
|
|||||||
async function callEmbeddingAPI(inputs: string[], config: EmbeddingConfig): Promise<number[][]> {
|
async function callEmbeddingAPI(inputs: string[], config: EmbeddingConfig): Promise<number[][]> {
|
||||||
return retryWithExponentialBackoff(
|
return retryWithExponentialBackoff(
|
||||||
async () => {
|
async () => {
|
||||||
const useDimensions = supportsCustomDimensions(config.modelName)
|
|
||||||
|
|
||||||
const requestBody = config.useAzure
|
const requestBody = config.useAzure
|
||||||
? {
|
? {
|
||||||
input: inputs,
|
input: inputs,
|
||||||
encoding_format: 'float',
|
encoding_format: 'float',
|
||||||
...(useDimensions && { dimensions: EMBEDDING_DIMENSIONS }),
|
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
input: inputs,
|
input: inputs,
|
||||||
model: config.modelName,
|
model: config.modelName,
|
||||||
encoding_format: 'float',
|
encoding_format: 'float',
|
||||||
...(useDimensions && { dimensions: EMBEDDING_DIMENSIONS }),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(config.apiUrl, {
|
const response = await fetch(config.apiUrl, {
|
||||||
|
|||||||
@@ -18,52 +18,6 @@ const logger = createLogger('BlobClient')
|
|||||||
|
|
||||||
let _blobServiceClient: BlobServiceClientInstance | null = null
|
let _blobServiceClient: BlobServiceClientInstance | null = null
|
||||||
|
|
||||||
interface ParsedCredentials {
|
|
||||||
accountName: string
|
|
||||||
accountKey: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract account name and key from an Azure connection string.
|
|
||||||
* Connection strings have the format: DefaultEndpointsProtocol=https;AccountName=...;AccountKey=...;EndpointSuffix=...
|
|
||||||
*/
|
|
||||||
function parseConnectionString(connectionString: string): ParsedCredentials {
|
|
||||||
const accountNameMatch = connectionString.match(/AccountName=([^;]+)/)
|
|
||||||
if (!accountNameMatch) {
|
|
||||||
throw new Error('Cannot extract account name from connection string')
|
|
||||||
}
|
|
||||||
|
|
||||||
const accountKeyMatch = connectionString.match(/AccountKey=([^;]+)/)
|
|
||||||
if (!accountKeyMatch) {
|
|
||||||
throw new Error('Cannot extract account key from connection string')
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
accountName: accountNameMatch[1],
|
|
||||||
accountKey: accountKeyMatch[1],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get account credentials from BLOB_CONFIG, extracting from connection string if necessary.
|
|
||||||
*/
|
|
||||||
function getAccountCredentials(): ParsedCredentials {
|
|
||||||
if (BLOB_CONFIG.connectionString) {
|
|
||||||
return parseConnectionString(BLOB_CONFIG.connectionString)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (BLOB_CONFIG.accountName && BLOB_CONFIG.accountKey) {
|
|
||||||
return {
|
|
||||||
accountName: BLOB_CONFIG.accountName,
|
|
||||||
accountKey: BLOB_CONFIG.accountKey,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(
|
|
||||||
'Azure Blob Storage credentials are missing – set AZURE_CONNECTION_STRING or both AZURE_ACCOUNT_NAME and AZURE_ACCOUNT_KEY'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getBlobServiceClient(): Promise<BlobServiceClientInstance> {
|
export async function getBlobServiceClient(): Promise<BlobServiceClientInstance> {
|
||||||
if (_blobServiceClient) return _blobServiceClient
|
if (_blobServiceClient) return _blobServiceClient
|
||||||
|
|
||||||
@@ -173,8 +127,6 @@ export async function getPresignedUrl(key: string, expiresIn = 3600) {
|
|||||||
const containerClient = blobServiceClient.getContainerClient(BLOB_CONFIG.containerName)
|
const containerClient = blobServiceClient.getContainerClient(BLOB_CONFIG.containerName)
|
||||||
const blockBlobClient = containerClient.getBlockBlobClient(key)
|
const blockBlobClient = containerClient.getBlockBlobClient(key)
|
||||||
|
|
||||||
const { accountName, accountKey } = getAccountCredentials()
|
|
||||||
|
|
||||||
const sasOptions = {
|
const sasOptions = {
|
||||||
containerName: BLOB_CONFIG.containerName,
|
containerName: BLOB_CONFIG.containerName,
|
||||||
blobName: key,
|
blobName: key,
|
||||||
@@ -185,7 +137,13 @@ export async function getPresignedUrl(key: string, expiresIn = 3600) {
|
|||||||
|
|
||||||
const sasToken = generateBlobSASQueryParameters(
|
const sasToken = generateBlobSASQueryParameters(
|
||||||
sasOptions,
|
sasOptions,
|
||||||
new StorageSharedKeyCredential(accountName, accountKey)
|
new StorageSharedKeyCredential(
|
||||||
|
BLOB_CONFIG.accountName,
|
||||||
|
BLOB_CONFIG.accountKey ??
|
||||||
|
(() => {
|
||||||
|
throw new Error('AZURE_ACCOUNT_KEY is required when using account name authentication')
|
||||||
|
})()
|
||||||
|
)
|
||||||
).toString()
|
).toString()
|
||||||
|
|
||||||
return `${blockBlobClient.url}?${sasToken}`
|
return `${blockBlobClient.url}?${sasToken}`
|
||||||
@@ -210,14 +168,9 @@ export async function getPresignedUrlWithConfig(
|
|||||||
StorageSharedKeyCredential,
|
StorageSharedKeyCredential,
|
||||||
} = await import('@azure/storage-blob')
|
} = await import('@azure/storage-blob')
|
||||||
let tempBlobServiceClient: BlobServiceClientInstance
|
let tempBlobServiceClient: BlobServiceClientInstance
|
||||||
let accountName: string
|
|
||||||
let accountKey: string
|
|
||||||
|
|
||||||
if (customConfig.connectionString) {
|
if (customConfig.connectionString) {
|
||||||
tempBlobServiceClient = BlobServiceClient.fromConnectionString(customConfig.connectionString)
|
tempBlobServiceClient = BlobServiceClient.fromConnectionString(customConfig.connectionString)
|
||||||
const credentials = parseConnectionString(customConfig.connectionString)
|
|
||||||
accountName = credentials.accountName
|
|
||||||
accountKey = credentials.accountKey
|
|
||||||
} else if (customConfig.accountName && customConfig.accountKey) {
|
} else if (customConfig.accountName && customConfig.accountKey) {
|
||||||
const sharedKeyCredential = new StorageSharedKeyCredential(
|
const sharedKeyCredential = new StorageSharedKeyCredential(
|
||||||
customConfig.accountName,
|
customConfig.accountName,
|
||||||
@@ -227,8 +180,6 @@ export async function getPresignedUrlWithConfig(
|
|||||||
`https://${customConfig.accountName}.blob.core.windows.net`,
|
`https://${customConfig.accountName}.blob.core.windows.net`,
|
||||||
sharedKeyCredential
|
sharedKeyCredential
|
||||||
)
|
)
|
||||||
accountName = customConfig.accountName
|
|
||||||
accountKey = customConfig.accountKey
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Custom blob config must include either connectionString or accountName + accountKey'
|
'Custom blob config must include either connectionString or accountName + accountKey'
|
||||||
@@ -248,7 +199,13 @@ export async function getPresignedUrlWithConfig(
|
|||||||
|
|
||||||
const sasToken = generateBlobSASQueryParameters(
|
const sasToken = generateBlobSASQueryParameters(
|
||||||
sasOptions,
|
sasOptions,
|
||||||
new StorageSharedKeyCredential(accountName, accountKey)
|
new StorageSharedKeyCredential(
|
||||||
|
customConfig.accountName,
|
||||||
|
customConfig.accountKey ??
|
||||||
|
(() => {
|
||||||
|
throw new Error('Account key is required when using account name authentication')
|
||||||
|
})()
|
||||||
|
)
|
||||||
).toString()
|
).toString()
|
||||||
|
|
||||||
return `${blockBlobClient.url}?${sasToken}`
|
return `${blockBlobClient.url}?${sasToken}`
|
||||||
@@ -446,9 +403,13 @@ export async function getMultipartPartUrls(
|
|||||||
if (customConfig) {
|
if (customConfig) {
|
||||||
if (customConfig.connectionString) {
|
if (customConfig.connectionString) {
|
||||||
blobServiceClient = BlobServiceClient.fromConnectionString(customConfig.connectionString)
|
blobServiceClient = BlobServiceClient.fromConnectionString(customConfig.connectionString)
|
||||||
const credentials = parseConnectionString(customConfig.connectionString)
|
const match = customConfig.connectionString.match(/AccountName=([^;]+)/)
|
||||||
accountName = credentials.accountName
|
if (!match) throw new Error('Cannot extract account name from connection string')
|
||||||
accountKey = credentials.accountKey
|
accountName = match[1]
|
||||||
|
|
||||||
|
const keyMatch = customConfig.connectionString.match(/AccountKey=([^;]+)/)
|
||||||
|
if (!keyMatch) throw new Error('Cannot extract account key from connection string')
|
||||||
|
accountKey = keyMatch[1]
|
||||||
} else if (customConfig.accountName && customConfig.accountKey) {
|
} else if (customConfig.accountName && customConfig.accountKey) {
|
||||||
const credential = new StorageSharedKeyCredential(
|
const credential = new StorageSharedKeyCredential(
|
||||||
customConfig.accountName,
|
customConfig.accountName,
|
||||||
@@ -467,9 +428,12 @@ export async function getMultipartPartUrls(
|
|||||||
} else {
|
} else {
|
||||||
blobServiceClient = await getBlobServiceClient()
|
blobServiceClient = await getBlobServiceClient()
|
||||||
containerName = BLOB_CONFIG.containerName
|
containerName = BLOB_CONFIG.containerName
|
||||||
const credentials = getAccountCredentials()
|
accountName = BLOB_CONFIG.accountName
|
||||||
accountName = credentials.accountName
|
accountKey =
|
||||||
accountKey = credentials.accountKey
|
BLOB_CONFIG.accountKey ||
|
||||||
|
(() => {
|
||||||
|
throw new Error('AZURE_ACCOUNT_KEY is required')
|
||||||
|
})()
|
||||||
}
|
}
|
||||||
|
|
||||||
const containerClient = blobServiceClient.getContainerClient(containerName)
|
const containerClient = blobServiceClient.getContainerClient(containerName)
|
||||||
@@ -537,10 +501,12 @@ export async function completeMultipartUpload(
|
|||||||
const containerClient = blobServiceClient.getContainerClient(containerName)
|
const containerClient = blobServiceClient.getContainerClient(containerName)
|
||||||
const blockBlobClient = containerClient.getBlockBlobClient(key)
|
const blockBlobClient = containerClient.getBlockBlobClient(key)
|
||||||
|
|
||||||
|
// Sort parts by part number and extract block IDs
|
||||||
const sortedBlockIds = parts
|
const sortedBlockIds = parts
|
||||||
.sort((a, b) => a.partNumber - b.partNumber)
|
.sort((a, b) => a.partNumber - b.partNumber)
|
||||||
.map((part) => part.blockId)
|
.map((part) => part.blockId)
|
||||||
|
|
||||||
|
// Commit the block list to create the final blob
|
||||||
await blockBlobClient.commitBlockList(sortedBlockIds, {
|
await blockBlobClient.commitBlockList(sortedBlockIds, {
|
||||||
metadata: {
|
metadata: {
|
||||||
multipartUpload: 'completed',
|
multipartUpload: 'completed',
|
||||||
@@ -591,8 +557,10 @@ export async function abortMultipartUpload(key: string, customConfig?: BlobConfi
|
|||||||
const blockBlobClient = containerClient.getBlockBlobClient(key)
|
const blockBlobClient = containerClient.getBlockBlobClient(key)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Delete the blob if it exists (this also cleans up any uncommitted blocks)
|
||||||
await blockBlobClient.deleteIfExists()
|
await blockBlobClient.deleteIfExists()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// Ignore errors since we're just cleaning up
|
||||||
logger.warn('Error cleaning up multipart upload:', error)
|
logger.warn('Error cleaning up multipart upload:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -618,6 +618,13 @@ export function getToolOutputs(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates output paths for a tool-based block.
|
||||||
|
*
|
||||||
|
* @param blockConfig - The block configuration containing tools config
|
||||||
|
* @param subBlocks - SubBlock values for tool selection and condition evaluation
|
||||||
|
* @returns Array of output paths for the tool, or empty array on error
|
||||||
|
*/
|
||||||
export function getToolOutputPaths(
|
export function getToolOutputPaths(
|
||||||
blockConfig: BlockConfig,
|
blockConfig: BlockConfig,
|
||||||
subBlocks?: Record<string, SubBlockWithValue>
|
subBlocks?: Record<string, SubBlockWithValue>
|
||||||
@@ -627,22 +634,12 @@ export function getToolOutputPaths(
|
|||||||
if (!outputs || Object.keys(outputs).length === 0) return []
|
if (!outputs || Object.keys(outputs).length === 0) return []
|
||||||
|
|
||||||
if (subBlocks && blockConfig.outputs) {
|
if (subBlocks && blockConfig.outputs) {
|
||||||
|
const filteredBlockOutputs = filterOutputsByCondition(blockConfig.outputs, subBlocks)
|
||||||
|
const allowedKeys = new Set(Object.keys(filteredBlockOutputs))
|
||||||
|
|
||||||
const filteredOutputs: Record<string, any> = {}
|
const filteredOutputs: Record<string, any> = {}
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(outputs)) {
|
for (const [key, value] of Object.entries(outputs)) {
|
||||||
const blockOutput = blockConfig.outputs[key]
|
if (allowedKeys.has(key)) {
|
||||||
|
|
||||||
if (!blockOutput || typeof blockOutput !== 'object') {
|
|
||||||
filteredOutputs[key] = value
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const condition = 'condition' in blockOutput ? blockOutput.condition : undefined
|
|
||||||
if (condition) {
|
|
||||||
if (evaluateOutputCondition(condition, subBlocks)) {
|
|
||||||
filteredOutputs[key] = value
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
filteredOutputs[key] = value
|
filteredOutputs[key] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ describe('VariableManager', () => {
|
|||||||
it.concurrent('should handle boolean type variables', () => {
|
it.concurrent('should handle boolean type variables', () => {
|
||||||
expect(VariableManager.parseInputForStorage('true', 'boolean')).toBe(true)
|
expect(VariableManager.parseInputForStorage('true', 'boolean')).toBe(true)
|
||||||
expect(VariableManager.parseInputForStorage('false', 'boolean')).toBe(false)
|
expect(VariableManager.parseInputForStorage('false', 'boolean')).toBe(false)
|
||||||
expect(VariableManager.parseInputForStorage('1', 'boolean')).toBe(false)
|
expect(VariableManager.parseInputForStorage('1', 'boolean')).toBe(true)
|
||||||
expect(VariableManager.parseInputForStorage('0', 'boolean')).toBe(false)
|
expect(VariableManager.parseInputForStorage('0', 'boolean')).toBe(false)
|
||||||
expect(VariableManager.parseInputForStorage('"true"', 'boolean')).toBe(true)
|
expect(VariableManager.parseInputForStorage('"true"', 'boolean')).toBe(true)
|
||||||
expect(VariableManager.parseInputForStorage("'false'", 'boolean')).toBe(false)
|
expect(VariableManager.parseInputForStorage("'false'", 'boolean')).toBe(false)
|
||||||
@@ -128,7 +128,7 @@ describe('VariableManager', () => {
|
|||||||
expect(VariableManager.resolveForExecution(false, 'boolean')).toBe(false)
|
expect(VariableManager.resolveForExecution(false, 'boolean')).toBe(false)
|
||||||
expect(VariableManager.resolveForExecution('true', 'boolean')).toBe(true)
|
expect(VariableManager.resolveForExecution('true', 'boolean')).toBe(true)
|
||||||
expect(VariableManager.resolveForExecution('false', 'boolean')).toBe(false)
|
expect(VariableManager.resolveForExecution('false', 'boolean')).toBe(false)
|
||||||
expect(VariableManager.resolveForExecution('1', 'boolean')).toBe(false)
|
expect(VariableManager.resolveForExecution('1', 'boolean')).toBe(true)
|
||||||
expect(VariableManager.resolveForExecution('0', 'boolean')).toBe(false)
|
expect(VariableManager.resolveForExecution('0', 'boolean')).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export class VariableManager {
|
|||||||
// Special case for 'anything else' in the test
|
// Special case for 'anything else' in the test
|
||||||
if (unquoted === 'anything else') return true
|
if (unquoted === 'anything else') return true
|
||||||
const normalized = String(unquoted).toLowerCase().trim()
|
const normalized = String(unquoted).toLowerCase().trim()
|
||||||
return normalized === 'true'
|
return normalized === 'true' || normalized === '1'
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'object':
|
case 'object':
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@a2a-js/sdk": "0.3.7",
|
"@a2a-js/sdk": "0.3.7",
|
||||||
"@anthropic-ai/sdk": "0.71.2",
|
"@anthropic-ai/sdk": "^0.39.0",
|
||||||
"@aws-sdk/client-bedrock-runtime": "3.940.0",
|
"@aws-sdk/client-bedrock-runtime": "3.940.0",
|
||||||
"@aws-sdk/client-dynamodb": "3.940.0",
|
"@aws-sdk/client-dynamodb": "3.940.0",
|
||||||
"@aws-sdk/client-rds-data": "3.940.0",
|
"@aws-sdk/client-rds-data": "3.940.0",
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import Anthropic from '@anthropic-ai/sdk'
|
import Anthropic from '@anthropic-ai/sdk'
|
||||||
import { transformJSONSchema } from '@anthropic-ai/sdk/lib/transform-json-schema'
|
|
||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import type { StreamingExecution } from '@/executor/types'
|
import type { StreamingExecution } from '@/executor/types'
|
||||||
import { MAX_TOOL_ITERATIONS } from '@/providers'
|
import { MAX_TOOL_ITERATIONS } from '@/providers'
|
||||||
@@ -186,10 +185,13 @@ export const anthropicProvider: ProviderConfig = {
|
|||||||
const schema = request.responseFormat.schema || request.responseFormat
|
const schema = request.responseFormat.schema || request.responseFormat
|
||||||
|
|
||||||
if (useNativeStructuredOutputs) {
|
if (useNativeStructuredOutputs) {
|
||||||
const transformedSchema = transformJSONSchema(schema)
|
const schemaWithConstraints = {
|
||||||
|
...schema,
|
||||||
|
additionalProperties: false,
|
||||||
|
}
|
||||||
payload.output_format = {
|
payload.output_format = {
|
||||||
type: 'json_schema',
|
type: 'json_schema',
|
||||||
schema: transformedSchema,
|
schema: schemaWithConstraints,
|
||||||
}
|
}
|
||||||
logger.info(`Using native structured outputs for model: ${modelId}`)
|
logger.info(`Using native structured outputs for model: ${modelId}`)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -27,9 +27,6 @@ export function registerEmitFunctions(
|
|||||||
emitSubblockUpdate = subblockEmit
|
emitSubblockUpdate = subblockEmit
|
||||||
emitVariableUpdate = variableEmit
|
emitVariableUpdate = variableEmit
|
||||||
currentRegisteredWorkflowId = workflowId
|
currentRegisteredWorkflowId = workflowId
|
||||||
if (workflowId) {
|
|
||||||
useOperationQueueStore.getState().processNextOperation()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentRegisteredWorkflowId: string | null = null
|
let currentRegisteredWorkflowId: string | null = null
|
||||||
@@ -265,14 +262,16 @@ export const useOperationQueueStore = create<OperationQueueState>((set, get) =>
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!currentRegisteredWorkflowId) {
|
const nextOperation = currentRegisteredWorkflowId
|
||||||
|
? state.operations.find(
|
||||||
|
(op) => op.status === 'pending' && op.workflowId === currentRegisteredWorkflowId
|
||||||
|
)
|
||||||
|
: state.operations.find((op) => op.status === 'pending')
|
||||||
|
if (!nextOperation) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextOperation = state.operations.find(
|
if (currentRegisteredWorkflowId && nextOperation.workflowId !== currentRegisteredWorkflowId) {
|
||||||
(op) => op.status === 'pending' && op.workflowId === currentRegisteredWorkflowId
|
|
||||||
)
|
|
||||||
if (!nextOperation) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,8 @@ import type { Edge } from 'reactflow'
|
|||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs'
|
import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs'
|
||||||
import { mergeSubBlockValues, mergeSubblockStateWithValues } from '@/lib/workflows/subblocks'
|
import { mergeSubBlockValues, mergeSubblockStateWithValues } from '@/lib/workflows/subblocks'
|
||||||
import { TriggerUtils } from '@/lib/workflows/triggers/triggers'
|
|
||||||
import { getBlock } from '@/blocks'
|
import { getBlock } from '@/blocks'
|
||||||
import { isAnnotationOnlyBlock, normalizeName } from '@/executor/constants'
|
import { normalizeName } from '@/executor/constants'
|
||||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||||
import type {
|
import type {
|
||||||
BlockState,
|
BlockState,
|
||||||
@@ -18,32 +17,6 @@ import { TRIGGER_RUNTIME_SUBBLOCK_IDS } from '@/triggers/constants'
|
|||||||
|
|
||||||
const WEBHOOK_SUBBLOCK_FIELDS = ['webhookId', 'triggerPath']
|
const WEBHOOK_SUBBLOCK_FIELDS = ['webhookId', 'triggerPath']
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if an edge is valid (source and target exist, not annotation-only, target is not a trigger)
|
|
||||||
*/
|
|
||||||
function isValidEdge(
|
|
||||||
edge: Edge,
|
|
||||||
blocks: Record<string, { type: string; triggerMode?: boolean }>
|
|
||||||
): boolean {
|
|
||||||
const sourceBlock = blocks[edge.source]
|
|
||||||
const targetBlock = blocks[edge.target]
|
|
||||||
if (!sourceBlock || !targetBlock) return false
|
|
||||||
if (isAnnotationOnlyBlock(sourceBlock.type)) return false
|
|
||||||
if (isAnnotationOnlyBlock(targetBlock.type)) return false
|
|
||||||
if (TriggerUtils.isTriggerBlock(targetBlock)) return false
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filters edges to only include valid ones (target exists and is not a trigger block)
|
|
||||||
*/
|
|
||||||
export function filterValidEdges(
|
|
||||||
edges: Edge[],
|
|
||||||
blocks: Record<string, { type: string; triggerMode?: boolean }>
|
|
||||||
): Edge[] {
|
|
||||||
return edges.filter((edge) => isValidEdge(edge, blocks))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function filterNewEdges(edgesToAdd: Edge[], currentEdges: Edge[]): Edge[] {
|
export function filterNewEdges(edgesToAdd: Edge[], currentEdges: Edge[]): Edge[] {
|
||||||
return edgesToAdd.filter((edge) => {
|
return edgesToAdd.filter((edge) => {
|
||||||
if (edge.source === edge.target) return false
|
if (edge.source === edge.target) return false
|
||||||
|
|||||||
@@ -4,17 +4,13 @@ import { create } from 'zustand'
|
|||||||
import { devtools } from 'zustand/middleware'
|
import { devtools } from 'zustand/middleware'
|
||||||
import { DEFAULT_DUPLICATE_OFFSET } from '@/lib/workflows/autolayout/constants'
|
import { DEFAULT_DUPLICATE_OFFSET } from '@/lib/workflows/autolayout/constants'
|
||||||
import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs'
|
import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs'
|
||||||
|
import { TriggerUtils } from '@/lib/workflows/triggers/triggers'
|
||||||
import { getBlock } from '@/blocks'
|
import { getBlock } from '@/blocks'
|
||||||
import type { SubBlockConfig } from '@/blocks/types'
|
import type { SubBlockConfig } from '@/blocks/types'
|
||||||
import { normalizeName, RESERVED_BLOCK_NAMES } from '@/executor/constants'
|
import { isAnnotationOnlyBlock, normalizeName, RESERVED_BLOCK_NAMES } from '@/executor/constants'
|
||||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||||
import {
|
import { filterNewEdges, getUniqueBlockName, mergeSubblockState } from '@/stores/workflows/utils'
|
||||||
filterNewEdges,
|
|
||||||
filterValidEdges,
|
|
||||||
getUniqueBlockName,
|
|
||||||
mergeSubblockState,
|
|
||||||
} from '@/stores/workflows/utils'
|
|
||||||
import type {
|
import type {
|
||||||
Position,
|
Position,
|
||||||
SubBlockState,
|
SubBlockState,
|
||||||
@@ -95,6 +91,26 @@ function resolveInitialSubblockValue(config: SubBlockConfig): unknown {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isValidEdge(
|
||||||
|
edge: Edge,
|
||||||
|
blocks: Record<string, { type: string; triggerMode?: boolean }>
|
||||||
|
): boolean {
|
||||||
|
const sourceBlock = blocks[edge.source]
|
||||||
|
const targetBlock = blocks[edge.target]
|
||||||
|
if (!sourceBlock || !targetBlock) return false
|
||||||
|
if (isAnnotationOnlyBlock(sourceBlock.type)) return false
|
||||||
|
if (isAnnotationOnlyBlock(targetBlock.type)) return false
|
||||||
|
if (TriggerUtils.isTriggerBlock(targetBlock)) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterValidEdges(
|
||||||
|
edges: Edge[],
|
||||||
|
blocks: Record<string, { type: string; triggerMode?: boolean }>
|
||||||
|
): Edge[] {
|
||||||
|
return edges.filter((edge) => isValidEdge(edge, blocks))
|
||||||
|
}
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
blocks: {},
|
blocks: {},
|
||||||
edges: [],
|
edges: [],
|
||||||
@@ -340,8 +356,7 @@ export const useWorkflowStore = create<WorkflowStore>()(
|
|||||||
data?: Record<string, any>
|
data?: Record<string, any>
|
||||||
}>,
|
}>,
|
||||||
edges?: Edge[],
|
edges?: Edge[],
|
||||||
subBlockValues?: Record<string, Record<string, unknown>>,
|
subBlockValues?: Record<string, Record<string, unknown>>
|
||||||
options?: { skipEdgeValidation?: boolean }
|
|
||||||
) => {
|
) => {
|
||||||
const currentBlocks = get().blocks
|
const currentBlocks = get().blocks
|
||||||
const currentEdges = get().edges
|
const currentEdges = get().edges
|
||||||
@@ -366,10 +381,7 @@ export const useWorkflowStore = create<WorkflowStore>()(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (edges && edges.length > 0) {
|
if (edges && edges.length > 0) {
|
||||||
// Skip validation if already validated by caller (e.g., collaborative layer)
|
const validEdges = filterValidEdges(edges, newBlocks)
|
||||||
const validEdges = options?.skipEdgeValidation
|
|
||||||
? edges
|
|
||||||
: filterValidEdges(edges, newBlocks)
|
|
||||||
const existingEdgeIds = new Set(currentEdges.map((e) => e.id))
|
const existingEdgeIds = new Set(currentEdges.map((e) => e.id))
|
||||||
for (const edge of validEdges) {
|
for (const edge of validEdges) {
|
||||||
if (!existingEdgeIds.has(edge.id)) {
|
if (!existingEdgeIds.has(edge.id)) {
|
||||||
@@ -504,12 +516,11 @@ export const useWorkflowStore = create<WorkflowStore>()(
|
|||||||
get().updateLastSaved()
|
get().updateLastSaved()
|
||||||
},
|
},
|
||||||
|
|
||||||
batchAddEdges: (edges: Edge[], options?: { skipValidation?: boolean }) => {
|
batchAddEdges: (edges: Edge[]) => {
|
||||||
const blocks = get().blocks
|
const blocks = get().blocks
|
||||||
const currentEdges = get().edges
|
const currentEdges = get().edges
|
||||||
|
|
||||||
// Skip validation if already validated by caller (e.g., collaborative layer)
|
const validEdges = filterValidEdges(edges, blocks)
|
||||||
const validEdges = options?.skipValidation ? edges : filterValidEdges(edges, blocks)
|
|
||||||
const filtered = filterNewEdges(validEdges, currentEdges)
|
const filtered = filterNewEdges(validEdges, currentEdges)
|
||||||
const newEdges = [...currentEdges]
|
const newEdges = [...currentEdges]
|
||||||
|
|
||||||
|
|||||||
@@ -203,13 +203,12 @@ export interface WorkflowActions {
|
|||||||
batchAddBlocks: (
|
batchAddBlocks: (
|
||||||
blocks: BlockState[],
|
blocks: BlockState[],
|
||||||
edges?: Edge[],
|
edges?: Edge[],
|
||||||
subBlockValues?: Record<string, Record<string, unknown>>,
|
subBlockValues?: Record<string, Record<string, unknown>>
|
||||||
options?: { skipEdgeValidation?: boolean }
|
|
||||||
) => void
|
) => void
|
||||||
batchRemoveBlocks: (ids: string[]) => void
|
batchRemoveBlocks: (ids: string[]) => void
|
||||||
batchToggleEnabled: (ids: string[]) => void
|
batchToggleEnabled: (ids: string[]) => void
|
||||||
batchToggleHandles: (ids: string[]) => void
|
batchToggleHandles: (ids: string[]) => void
|
||||||
batchAddEdges: (edges: Edge[], options?: { skipValidation?: boolean }) => void
|
batchAddEdges: (edges: Edge[]) => void
|
||||||
batchRemoveEdges: (ids: string[]) => void
|
batchRemoveEdges: (ids: string[]) => void
|
||||||
clear: () => Partial<WorkflowState>
|
clear: () => Partial<WorkflowState>
|
||||||
updateLastSaved: () => void
|
updateLastSaved: () => void
|
||||||
|
|||||||
@@ -38,12 +38,11 @@ export const storageUploadTool: ToolConfig<
|
|||||||
visibility: 'user-or-llm',
|
visibility: 'user-or-llm',
|
||||||
description: 'Optional folder path (e.g., "folder/subfolder/")',
|
description: 'Optional folder path (e.g., "folder/subfolder/")',
|
||||||
},
|
},
|
||||||
fileData: {
|
fileContent: {
|
||||||
type: 'json',
|
type: 'string',
|
||||||
required: true,
|
required: true,
|
||||||
visibility: 'user-or-llm',
|
visibility: 'user-or-llm',
|
||||||
description:
|
description: 'The file content (base64 encoded for binary files, or plain text)',
|
||||||
'File to upload - UserFile object (basic mode) or string content (advanced mode: base64 or plain text). Supports data URLs.',
|
|
||||||
},
|
},
|
||||||
contentType: {
|
contentType: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
@@ -66,28 +65,65 @@ export const storageUploadTool: ToolConfig<
|
|||||||
},
|
},
|
||||||
|
|
||||||
request: {
|
request: {
|
||||||
url: '/api/tools/supabase/storage-upload',
|
url: (params) => {
|
||||||
|
// Combine folder path and fileName, ensuring proper formatting
|
||||||
|
let fullPath = params.fileName
|
||||||
|
if (params.path) {
|
||||||
|
// Ensure path ends with / and doesn't have double slashes
|
||||||
|
const folderPath = params.path.endsWith('/') ? params.path : `${params.path}/`
|
||||||
|
fullPath = `${folderPath}${params.fileName}`
|
||||||
|
}
|
||||||
|
return `https://${params.projectId}.supabase.co/storage/v1/object/${params.bucket}/${fullPath}`
|
||||||
|
},
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: () => ({
|
headers: (params) => {
|
||||||
'Content-Type': 'application/json',
|
const headers: Record<string, string> = {
|
||||||
}),
|
apikey: params.apiKey,
|
||||||
body: (params) => ({
|
Authorization: `Bearer ${params.apiKey}`,
|
||||||
projectId: params.projectId,
|
}
|
||||||
apiKey: params.apiKey,
|
|
||||||
bucket: params.bucket,
|
if (params.contentType) {
|
||||||
fileName: params.fileName,
|
headers['Content-Type'] = params.contentType
|
||||||
path: params.path,
|
}
|
||||||
fileData: params.fileData,
|
|
||||||
contentType: params.contentType,
|
if (params.upsert) {
|
||||||
upsert: params.upsert,
|
headers['x-upsert'] = 'true'
|
||||||
}),
|
}
|
||||||
|
|
||||||
|
return headers
|
||||||
|
},
|
||||||
|
body: (params) => {
|
||||||
|
// Return the file content wrapped in an object
|
||||||
|
// The actual upload will need to handle this appropriately
|
||||||
|
return {
|
||||||
|
content: params.fileContent,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
transformResponse: async (response: Response) => {
|
||||||
|
let data
|
||||||
|
try {
|
||||||
|
data = await response.json()
|
||||||
|
} catch (parseError) {
|
||||||
|
throw new Error(`Failed to parse Supabase storage upload response: ${parseError}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
output: {
|
||||||
|
message: 'Successfully uploaded file to storage',
|
||||||
|
results: data,
|
||||||
|
},
|
||||||
|
error: undefined,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
outputs: {
|
outputs: {
|
||||||
message: { type: 'string', description: 'Operation status message' },
|
message: { type: 'string', description: 'Operation status message' },
|
||||||
results: {
|
results: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
description: 'Upload result including file path, bucket, and public URL',
|
description: 'Upload result including file path and metadata',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ export interface SupabaseStorageUploadParams {
|
|||||||
bucket: string
|
bucket: string
|
||||||
fileName: string
|
fileName: string
|
||||||
path?: string
|
path?: string
|
||||||
fileData: any // UserFile object (basic mode) or string (advanced mode: base64/plain text)
|
fileContent: string
|
||||||
contentType?: string
|
contentType?: string
|
||||||
upsert?: boolean
|
upsert?: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
37
bun.lock
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 0,
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"": {
|
"": {
|
||||||
"name": "simstudio",
|
"name": "simstudio",
|
||||||
@@ -54,7 +55,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@a2a-js/sdk": "0.3.7",
|
"@a2a-js/sdk": "0.3.7",
|
||||||
"@anthropic-ai/sdk": "0.71.2",
|
"@anthropic-ai/sdk": "^0.39.0",
|
||||||
"@aws-sdk/client-bedrock-runtime": "3.940.0",
|
"@aws-sdk/client-bedrock-runtime": "3.940.0",
|
||||||
"@aws-sdk/client-dynamodb": "3.940.0",
|
"@aws-sdk/client-dynamodb": "3.940.0",
|
||||||
"@aws-sdk/client-rds-data": "3.940.0",
|
"@aws-sdk/client-rds-data": "3.940.0",
|
||||||
@@ -362,7 +363,7 @@
|
|||||||
|
|
||||||
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
|
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
|
||||||
|
|
||||||
"@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.71.2", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-TGNDEUuEstk/DKu0/TflXAEt+p+p/WhTlFzEnoosvbaDU2LTjm42igSdlL0VijrKpWejtOKxX0b8A7uc+XiSAQ=="],
|
"@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.39.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-eMyDIPRZbt1CCLErRCi3exlAvNkBtRe+kW5vvJyef93PmNr/clstYgHhtvmkxN82nlKgzyGPCyGxrm0JQ1ZIdg=="],
|
||||||
|
|
||||||
"@ark/schema": ["@ark/schema@0.56.0", "", { "dependencies": { "@ark/util": "0.56.0" } }, "sha512-ECg3hox/6Z/nLajxXqNhgPtNdHWC9zNsDyskwO28WinoFEnWow4IsERNz9AnXRhTZJnYIlAJ4uGn3nlLk65vZA=="],
|
"@ark/schema": ["@ark/schema@0.56.0", "", { "dependencies": { "@ark/util": "0.56.0" } }, "sha512-ECg3hox/6Z/nLajxXqNhgPtNdHWC9zNsDyskwO28WinoFEnWow4IsERNz9AnXRhTZJnYIlAJ4uGn3nlLk65vZA=="],
|
||||||
|
|
||||||
@@ -546,8 +547,6 @@
|
|||||||
|
|
||||||
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
|
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
|
||||||
|
|
||||||
"@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="],
|
|
||||||
|
|
||||||
"@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
|
"@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
|
||||||
|
|
||||||
"@babel/traverse": ["@babel/traverse@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/types": "^7.28.6", "debug": "^4.3.1" } }, "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg=="],
|
"@babel/traverse": ["@babel/traverse@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/types": "^7.28.6", "debug": "^4.3.1" } }, "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg=="],
|
||||||
@@ -2444,8 +2443,6 @@
|
|||||||
|
|
||||||
"json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
|
"json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
|
||||||
|
|
||||||
"json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="],
|
|
||||||
|
|
||||||
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||||
|
|
||||||
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||||
@@ -3390,8 +3387,6 @@
|
|||||||
|
|
||||||
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
|
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
|
||||||
|
|
||||||
"ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="],
|
|
||||||
|
|
||||||
"ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="],
|
"ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="],
|
||||||
|
|
||||||
"tsafe": ["tsafe@1.8.12", "", {}, "sha512-nFRqW0ttu/2o6XTXsHiVZWJBCOaxhVqZLg7dgs3coZNsCMPXPfwz+zPHAQA+70fNnVJLAPg1EgGIqK9Q84tvAw=="],
|
"tsafe": ["tsafe@1.8.12", "", {}, "sha512-nFRqW0ttu/2o6XTXsHiVZWJBCOaxhVqZLg7dgs3coZNsCMPXPfwz+zPHAQA+70fNnVJLAPg1EgGIqK9Q84tvAw=="],
|
||||||
@@ -3598,6 +3593,10 @@
|
|||||||
|
|
||||||
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||||
|
|
||||||
|
"@anthropic-ai/sdk/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
|
||||||
|
|
||||||
|
"@anthropic-ai/sdk/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
|
||||||
|
|
||||||
"@asamuzakjp/css-color/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
"@asamuzakjp/css-color/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||||
|
|
||||||
"@aws-crypto/crc32/@aws-sdk/types": ["@aws-sdk/types@3.969.0", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-7IIzM5TdiXn+VtgPdVLjmE6uUBUtnga0f4RiSEI1WW10RPuNvZ9U+pL3SwDiRDAdoGrOF9tSLJOFZmfuwYuVYQ=="],
|
"@aws-crypto/crc32/@aws-sdk/types": ["@aws-sdk/types@3.969.0", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-7IIzM5TdiXn+VtgPdVLjmE6uUBUtnga0f4RiSEI1WW10RPuNvZ9U+pL3SwDiRDAdoGrOF9tSLJOFZmfuwYuVYQ=="],
|
||||||
@@ -3714,8 +3713,6 @@
|
|||||||
|
|
||||||
"@browserbasehq/sdk/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
|
"@browserbasehq/sdk/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
|
||||||
|
|
||||||
"@browserbasehq/stagehand/@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.39.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-eMyDIPRZbt1CCLErRCi3exlAvNkBtRe+kW5vvJyef93PmNr/clstYgHhtvmkxN82nlKgzyGPCyGxrm0JQ1ZIdg=="],
|
|
||||||
|
|
||||||
"@cerebras/cerebras_cloud_sdk/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
|
"@cerebras/cerebras_cloud_sdk/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
|
||||||
|
|
||||||
"@cerebras/cerebras_cloud_sdk/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
|
"@cerebras/cerebras_cloud_sdk/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
|
||||||
@@ -4218,6 +4215,10 @@
|
|||||||
|
|
||||||
"xml2js/xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="],
|
"xml2js/xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="],
|
||||||
|
|
||||||
|
"@anthropic-ai/sdk/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
||||||
|
|
||||||
|
"@anthropic-ai/sdk/node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
|
||||||
|
|
||||||
"@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
|
"@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
|
||||||
|
|
||||||
"@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
|
"@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
|
||||||
@@ -4274,10 +4275,6 @@
|
|||||||
|
|
||||||
"@browserbasehq/sdk/node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
|
"@browserbasehq/sdk/node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
|
||||||
|
|
||||||
"@browserbasehq/stagehand/@anthropic-ai/sdk/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
|
|
||||||
|
|
||||||
"@browserbasehq/stagehand/@anthropic-ai/sdk/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
|
|
||||||
|
|
||||||
"@cerebras/cerebras_cloud_sdk/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
"@cerebras/cerebras_cloud_sdk/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
||||||
|
|
||||||
"@cerebras/cerebras_cloud_sdk/node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
|
"@cerebras/cerebras_cloud_sdk/node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
|
||||||
@@ -4688,6 +4685,10 @@
|
|||||||
|
|
||||||
"vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="],
|
"vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="],
|
||||||
|
|
||||||
|
"@anthropic-ai/sdk/node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
||||||
|
|
||||||
|
"@anthropic-ai/sdk/node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
||||||
|
|
||||||
"@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
|
"@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
|
||||||
|
|
||||||
"@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
|
"@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
|
||||||
@@ -4736,10 +4737,6 @@
|
|||||||
|
|
||||||
"@browserbasehq/sdk/node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
"@browserbasehq/sdk/node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
||||||
|
|
||||||
"@browserbasehq/stagehand/@anthropic-ai/sdk/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
|
||||||
|
|
||||||
"@browserbasehq/stagehand/@anthropic-ai/sdk/node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
|
|
||||||
|
|
||||||
"@cerebras/cerebras_cloud_sdk/node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
"@cerebras/cerebras_cloud_sdk/node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
||||||
|
|
||||||
"@cerebras/cerebras_cloud_sdk/node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
"@cerebras/cerebras_cloud_sdk/node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
||||||
@@ -4832,10 +4829,6 @@
|
|||||||
|
|
||||||
"@aws-sdk/client-sqs/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.947.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.947.0", "@aws-sdk/middleware-host-header": "3.936.0", "@aws-sdk/middleware-logger": "3.936.0", "@aws-sdk/middleware-recursion-detection": "3.936.0", "@aws-sdk/middleware-user-agent": "3.947.0", "@aws-sdk/region-config-resolver": "3.936.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-endpoints": "3.936.0", "@aws-sdk/util-user-agent-browser": "3.936.0", "@aws-sdk/util-user-agent-node": "3.947.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.7", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.14", "@smithy/middleware-retry": "^4.4.14", "@smithy/middleware-serde": "^4.2.6", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.10", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.13", "@smithy/util-defaults-mode-node": "^4.2.16", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-DjRJEYNnHUTu9kGPPQDTSXquwSEd6myKR4ssI4FaYLFhdT3ldWpj73yYt807H3tdmhS7vPmdVqchSJnjurUQAw=="],
|
"@aws-sdk/client-sqs/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.947.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.947.0", "@aws-sdk/middleware-host-header": "3.936.0", "@aws-sdk/middleware-logger": "3.936.0", "@aws-sdk/middleware-recursion-detection": "3.936.0", "@aws-sdk/middleware-user-agent": "3.947.0", "@aws-sdk/region-config-resolver": "3.936.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-endpoints": "3.936.0", "@aws-sdk/util-user-agent-browser": "3.936.0", "@aws-sdk/util-user-agent-node": "3.947.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.7", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.14", "@smithy/middleware-retry": "^4.4.14", "@smithy/middleware-serde": "^4.2.6", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.10", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.13", "@smithy/util-defaults-mode-node": "^4.2.16", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-DjRJEYNnHUTu9kGPPQDTSXquwSEd6myKR4ssI4FaYLFhdT3ldWpj73yYt807H3tdmhS7vPmdVqchSJnjurUQAw=="],
|
||||||
|
|
||||||
"@browserbasehq/stagehand/@anthropic-ai/sdk/node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
|
||||||
|
|
||||||
"@browserbasehq/stagehand/@anthropic-ai/sdk/node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
|
||||||
|
|
||||||
"@trigger.dev/core/socket.io/engine.io/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="],
|
"@trigger.dev/core/socket.io/engine.io/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="],
|
||||||
|
|
||||||
"lint-staged/listr2/cli-truncate/string-width/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
|
"lint-staged/listr2/cli-truncate/string-width/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ services:
|
|||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
memory: 1G
|
memory: 8G
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ['CMD', 'wget', '--spider', '--quiet', 'http://127.0.0.1:3002/health']
|
test: ['CMD', 'wget', '--spider', '--quiet', 'http://127.0.0.1:3002/health']
|
||||||
interval: 90s
|
interval: 90s
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ services:
|
|||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
memory: 1G
|
memory: 8G
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ['CMD', 'wget', '--spider', '--quiet', 'http://127.0.0.1:3002/health']
|
test: ['CMD', 'wget', '--spider', '--quiet', 'http://127.0.0.1:3002/health']
|
||||||
interval: 90s
|
interval: 90s
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ services:
|
|||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
memory: 1G
|
memory: 4G
|
||||||
environment:
|
environment:
|
||||||
- DATABASE_URL=postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:5432/${POSTGRES_DB:-simstudio}
|
- DATABASE_URL=postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:5432/${POSTGRES_DB:-simstudio}
|
||||||
- NEXT_PUBLIC_APP_URL=${NEXT_PUBLIC_APP_URL:-http://localhost:3000}
|
- NEXT_PUBLIC_APP_URL=${NEXT_PUBLIC_APP_URL:-http://localhost:3000}
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ global:
|
|||||||
app:
|
app:
|
||||||
enabled: true
|
enabled: true
|
||||||
replicaCount: 2
|
replicaCount: 2
|
||||||
|
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
memory: "8Gi"
|
memory: "6Gi"
|
||||||
cpu: "2000m"
|
cpu: "2000m"
|
||||||
requests:
|
requests:
|
||||||
memory: "6Gi"
|
memory: "4Gi"
|
||||||
cpu: "1000m"
|
cpu: "1000m"
|
||||||
|
|
||||||
# Production URLs (REQUIRED - update with your actual domain names)
|
# Production URLs (REQUIRED - update with your actual domain names)
|
||||||
@@ -49,14 +49,14 @@ app:
|
|||||||
realtime:
|
realtime:
|
||||||
enabled: true
|
enabled: true
|
||||||
replicaCount: 2
|
replicaCount: 2
|
||||||
|
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
memory: "1Gi"
|
memory: "4Gi"
|
||||||
cpu: "500m"
|
cpu: "1000m"
|
||||||
requests:
|
requests:
|
||||||
memory: "512Mi"
|
memory: "2Gi"
|
||||||
cpu: "250m"
|
cpu: "500m"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
NEXT_PUBLIC_APP_URL: "https://sim.acme.ai"
|
NEXT_PUBLIC_APP_URL: "https://sim.acme.ai"
|
||||||
|
|||||||
@@ -29,10 +29,10 @@ app:
|
|||||||
# Resource limits and requests
|
# Resource limits and requests
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
memory: "8Gi"
|
memory: "4Gi"
|
||||||
cpu: "2000m"
|
cpu: "2000m"
|
||||||
requests:
|
requests:
|
||||||
memory: "4Gi"
|
memory: "2Gi"
|
||||||
cpu: "1000m"
|
cpu: "1000m"
|
||||||
|
|
||||||
# Node selector for pod scheduling (leave empty to allow scheduling on any node)
|
# Node selector for pod scheduling (leave empty to allow scheduling on any node)
|
||||||
@@ -232,24 +232,24 @@ app:
|
|||||||
realtime:
|
realtime:
|
||||||
# Enable/disable the realtime service
|
# Enable/disable the realtime service
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
||||||
# Image configuration
|
# Image configuration
|
||||||
image:
|
image:
|
||||||
repository: simstudioai/realtime
|
repository: simstudioai/realtime
|
||||||
tag: latest
|
tag: latest
|
||||||
pullPolicy: Always
|
pullPolicy: Always
|
||||||
|
|
||||||
# Number of replicas
|
# Number of replicas
|
||||||
replicaCount: 1
|
replicaCount: 1
|
||||||
|
|
||||||
# Resource limits and requests
|
# Resource limits and requests
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
|
memory: "2Gi"
|
||||||
|
cpu: "1000m"
|
||||||
|
requests:
|
||||||
memory: "1Gi"
|
memory: "1Gi"
|
||||||
cpu: "500m"
|
cpu: "500m"
|
||||||
requests:
|
|
||||||
memory: "512Mi"
|
|
||||||
cpu: "250m"
|
|
||||||
|
|
||||||
# Node selector for pod scheduling (leave empty to allow scheduling on any node)
|
# Node selector for pod scheduling (leave empty to allow scheduling on any node)
|
||||||
nodeSelector: {}
|
nodeSelector: {}
|
||||||
|
|||||||
5
packages/python-sdk/.gitignore
vendored
@@ -81,7 +81,4 @@ Thumbs.db
|
|||||||
# mypy
|
# mypy
|
||||||
.mypy_cache/
|
.mypy_cache/
|
||||||
.dmypy.json
|
.dmypy.json
|
||||||
dmypy.json
|
dmypy.json
|
||||||
|
|
||||||
# uv
|
|
||||||
uv.lock
|
|
||||||
@@ -43,30 +43,24 @@ SimStudioClient(api_key: str, base_url: str = "https://sim.ai")
|
|||||||
|
|
||||||
#### Methods
|
#### Methods
|
||||||
|
|
||||||
##### execute_workflow(workflow_id, input=None, *, timeout=30.0, stream=None, selected_outputs=None, async_execution=None)
|
##### execute_workflow(workflow_id, input_data=None, timeout=30.0)
|
||||||
|
|
||||||
Execute a workflow with optional input data.
|
Execute a workflow with optional input data.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# With dict input (spread at root level of request body)
|
result = client.execute_workflow(
|
||||||
result = client.execute_workflow("workflow-id", {"message": "Hello, world!"})
|
"workflow-id",
|
||||||
|
input_data={"message": "Hello, world!"},
|
||||||
# With primitive input (wrapped as { input: value })
|
timeout=30.0 # 30 seconds
|
||||||
result = client.execute_workflow("workflow-id", "NVDA")
|
)
|
||||||
|
|
||||||
# With options (keyword-only arguments)
|
|
||||||
result = client.execute_workflow("workflow-id", {"message": "Hello"}, timeout=60.0)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Parameters:**
|
**Parameters:**
|
||||||
- `workflow_id` (str): The ID of the workflow to execute
|
- `workflow_id` (str): The ID of the workflow to execute
|
||||||
- `input` (any, optional): Input data to pass to the workflow. Dicts are spread at the root level, primitives/lists are wrapped in `{ input: value }`. File objects are automatically converted to base64.
|
- `input_data` (dict, optional): Input data to pass to the workflow. File objects are automatically converted to base64.
|
||||||
- `timeout` (float, keyword-only): Timeout in seconds (default: 30.0)
|
- `timeout` (float): Timeout in seconds (default: 30.0)
|
||||||
- `stream` (bool, keyword-only): Enable streaming responses
|
|
||||||
- `selected_outputs` (list, keyword-only): Block outputs to stream (e.g., `["agent1.content"]`)
|
|
||||||
- `async_execution` (bool, keyword-only): Execute asynchronously and return execution ID
|
|
||||||
|
|
||||||
**Returns:** `WorkflowExecutionResult` or `AsyncExecutionResult`
|
**Returns:** `WorkflowExecutionResult`
|
||||||
|
|
||||||
##### get_workflow_status(workflow_id)
|
##### get_workflow_status(workflow_id)
|
||||||
|
|
||||||
@@ -98,89 +92,24 @@ if is_ready:
|
|||||||
|
|
||||||
**Returns:** `bool`
|
**Returns:** `bool`
|
||||||
|
|
||||||
##### execute_workflow_sync(workflow_id, input=None, *, timeout=30.0, stream=None, selected_outputs=None)
|
##### execute_workflow_sync(workflow_id, input_data=None, timeout=30.0)
|
||||||
|
|
||||||
Execute a workflow synchronously (ensures non-async mode).
|
Execute a workflow and poll for completion (useful for long-running workflows).
|
||||||
|
|
||||||
```python
|
```python
|
||||||
result = client.execute_workflow_sync("workflow-id", {"data": "some input"}, timeout=60.0)
|
result = client.execute_workflow_sync(
|
||||||
```
|
|
||||||
|
|
||||||
**Parameters:**
|
|
||||||
- `workflow_id` (str): The ID of the workflow to execute
|
|
||||||
- `input` (any, optional): Input data to pass to the workflow
|
|
||||||
- `timeout` (float, keyword-only): Timeout in seconds (default: 30.0)
|
|
||||||
- `stream` (bool, keyword-only): Enable streaming responses
|
|
||||||
- `selected_outputs` (list, keyword-only): Block outputs to stream (e.g., `["agent1.content"]`)
|
|
||||||
|
|
||||||
**Returns:** `WorkflowExecutionResult`
|
|
||||||
|
|
||||||
##### get_job_status(task_id)
|
|
||||||
|
|
||||||
Get the status of an async job.
|
|
||||||
|
|
||||||
```python
|
|
||||||
status = client.get_job_status("task-id-from-async-execution")
|
|
||||||
print("Job status:", status)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Parameters:**
|
|
||||||
- `task_id` (str): The task ID returned from async execution
|
|
||||||
|
|
||||||
**Returns:** `dict`
|
|
||||||
|
|
||||||
##### execute_with_retry(workflow_id, input=None, *, timeout=30.0, stream=None, selected_outputs=None, async_execution=None, max_retries=3, initial_delay=1.0, max_delay=30.0, backoff_multiplier=2.0)
|
|
||||||
|
|
||||||
Execute a workflow with automatic retry on rate limit errors.
|
|
||||||
|
|
||||||
```python
|
|
||||||
result = client.execute_with_retry(
|
|
||||||
"workflow-id",
|
"workflow-id",
|
||||||
{"message": "Hello"},
|
input_data={"data": "some input"},
|
||||||
timeout=30.0,
|
timeout=60.0
|
||||||
max_retries=3,
|
|
||||||
initial_delay=1.0,
|
|
||||||
max_delay=30.0,
|
|
||||||
backoff_multiplier=2.0
|
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
**Parameters:**
|
**Parameters:**
|
||||||
- `workflow_id` (str): The ID of the workflow to execute
|
- `workflow_id` (str): The ID of the workflow to execute
|
||||||
- `input` (any, optional): Input data to pass to the workflow
|
- `input_data` (dict, optional): Input data to pass to the workflow
|
||||||
- `timeout` (float, keyword-only): Timeout in seconds (default: 30.0)
|
- `timeout` (float): Timeout for the initial request in seconds
|
||||||
- `stream` (bool, keyword-only): Enable streaming responses
|
|
||||||
- `selected_outputs` (list, keyword-only): Block outputs to stream
|
|
||||||
- `async_execution` (bool, keyword-only): Execute asynchronously
|
|
||||||
- `max_retries` (int, keyword-only): Maximum retry attempts (default: 3)
|
|
||||||
- `initial_delay` (float, keyword-only): Initial delay in seconds (default: 1.0)
|
|
||||||
- `max_delay` (float, keyword-only): Maximum delay in seconds (default: 30.0)
|
|
||||||
- `backoff_multiplier` (float, keyword-only): Backoff multiplier (default: 2.0)
|
|
||||||
|
|
||||||
**Returns:** `WorkflowExecutionResult` or `AsyncExecutionResult`
|
**Returns:** `WorkflowExecutionResult`
|
||||||
|
|
||||||
##### get_rate_limit_info()
|
|
||||||
|
|
||||||
Get current rate limit information from the last API response.
|
|
||||||
|
|
||||||
```python
|
|
||||||
rate_info = client.get_rate_limit_info()
|
|
||||||
if rate_info:
|
|
||||||
print("Remaining requests:", rate_info.remaining)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Returns:** `RateLimitInfo` or `None`
|
|
||||||
|
|
||||||
##### get_usage_limits()
|
|
||||||
|
|
||||||
Get current usage limits and quota information.
|
|
||||||
|
|
||||||
```python
|
|
||||||
limits = client.get_usage_limits()
|
|
||||||
print("Current usage:", limits.usage)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Returns:** `UsageLimits`
|
|
||||||
|
|
||||||
##### set_api_key(api_key)
|
##### set_api_key(api_key)
|
||||||
|
|
||||||
@@ -242,39 +171,6 @@ class SimStudioError(Exception):
|
|||||||
self.status = status
|
self.status = status
|
||||||
```
|
```
|
||||||
|
|
||||||
### AsyncExecutionResult
|
|
||||||
|
|
||||||
```python
|
|
||||||
@dataclass
|
|
||||||
class AsyncExecutionResult:
|
|
||||||
success: bool
|
|
||||||
task_id: str
|
|
||||||
status: str # 'queued'
|
|
||||||
created_at: str
|
|
||||||
links: Dict[str, str]
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
### Basic Workflow Execution
|
### Basic Workflow Execution
|
||||||
@@ -295,7 +191,7 @@ def run_workflow():
|
|||||||
# Execute the workflow
|
# Execute the workflow
|
||||||
result = client.execute_workflow(
|
result = client.execute_workflow(
|
||||||
"my-workflow-id",
|
"my-workflow-id",
|
||||||
{
|
input_data={
|
||||||
"message": "Process this data",
|
"message": "Process this data",
|
||||||
"user_id": "12345"
|
"user_id": "12345"
|
||||||
}
|
}
|
||||||
@@ -402,7 +298,7 @@ client = SimStudioClient(api_key=os.getenv("SIM_API_KEY"))
|
|||||||
with open('document.pdf', 'rb') as f:
|
with open('document.pdf', 'rb') as f:
|
||||||
result = client.execute_workflow(
|
result = client.execute_workflow(
|
||||||
'workflow-id',
|
'workflow-id',
|
||||||
{
|
input_data={
|
||||||
'documents': [f], # Must match your workflow's "files" field name
|
'documents': [f], # Must match your workflow's "files" field name
|
||||||
'instructions': 'Analyze this document'
|
'instructions': 'Analyze this document'
|
||||||
}
|
}
|
||||||
@@ -412,7 +308,7 @@ with open('document.pdf', 'rb') as f:
|
|||||||
with open('doc1.pdf', 'rb') as f1, open('doc2.pdf', 'rb') as f2:
|
with open('doc1.pdf', 'rb') as f1, open('doc2.pdf', 'rb') as f2:
|
||||||
result = client.execute_workflow(
|
result = client.execute_workflow(
|
||||||
'workflow-id',
|
'workflow-id',
|
||||||
{
|
input_data={
|
||||||
'attachments': [f1, f2], # Must match your workflow's "files" field name
|
'attachments': [f1, f2], # Must match your workflow's "files" field name
|
||||||
'query': 'Compare these documents'
|
'query': 'Compare these documents'
|
||||||
}
|
}
|
||||||
@@ -431,14 +327,14 @@ def execute_workflows_batch(workflow_data_pairs):
|
|||||||
"""Execute multiple workflows with different input data."""
|
"""Execute multiple workflows with different input data."""
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
for workflow_id, workflow_input in workflow_data_pairs:
|
for workflow_id, input_data in workflow_data_pairs:
|
||||||
try:
|
try:
|
||||||
# Validate workflow before execution
|
# Validate workflow before execution
|
||||||
if not client.validate_workflow(workflow_id):
|
if not client.validate_workflow(workflow_id):
|
||||||
print(f"Skipping {workflow_id}: not deployed")
|
print(f"Skipping {workflow_id}: not deployed")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
result = client.execute_workflow(workflow_id, workflow_input)
|
result = client.execute_workflow(workflow_id, input_data)
|
||||||
results.append({
|
results.append({
|
||||||
"workflow_id": workflow_id,
|
"workflow_id": workflow_id,
|
||||||
"success": result.success,
|
"success": result.success,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "simstudio-sdk"
|
name = "simstudio-sdk"
|
||||||
version = "0.1.2"
|
version = "0.1.1"
|
||||||
authors = [
|
authors = [
|
||||||
{name = "Sim", email = "help@sim.ai"},
|
{name = "Sim", email = "help@sim.ai"},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import os
|
|||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
|
||||||
__version__ = "0.1.2"
|
__version__ = "0.1.0"
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"SimStudioClient",
|
"SimStudioClient",
|
||||||
"SimStudioError",
|
"SimStudioError",
|
||||||
@@ -64,6 +64,15 @@ class RateLimitInfo:
|
|||||||
retry_after: Optional[int] = None
|
retry_after: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RateLimitStatus:
|
||||||
|
"""Rate limit status for sync/async requests."""
|
||||||
|
is_limited: bool
|
||||||
|
limit: int
|
||||||
|
remaining: int
|
||||||
|
reset_at: str
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class UsageLimits:
|
class UsageLimits:
|
||||||
"""Usage limits and quota information."""
|
"""Usage limits and quota information."""
|
||||||
@@ -106,6 +115,7 @@ class SimStudioClient:
|
|||||||
Recursively processes nested dicts and lists.
|
Recursively processes nested dicts and lists.
|
||||||
"""
|
"""
|
||||||
import base64
|
import base64
|
||||||
|
import io
|
||||||
|
|
||||||
# Check if this is a file-like object
|
# Check if this is a file-like object
|
||||||
if hasattr(value, 'read') and callable(value.read):
|
if hasattr(value, 'read') and callable(value.read):
|
||||||
@@ -149,8 +159,7 @@ class SimStudioClient:
|
|||||||
def execute_workflow(
|
def execute_workflow(
|
||||||
self,
|
self,
|
||||||
workflow_id: str,
|
workflow_id: str,
|
||||||
input: Optional[Any] = None,
|
input_data: Optional[Dict[str, Any]] = None,
|
||||||
*,
|
|
||||||
timeout: float = 30.0,
|
timeout: float = 30.0,
|
||||||
stream: Optional[bool] = None,
|
stream: Optional[bool] = None,
|
||||||
selected_outputs: Optional[list] = None,
|
selected_outputs: Optional[list] = None,
|
||||||
@@ -160,13 +169,11 @@ class SimStudioClient:
|
|||||||
Execute a workflow with optional input data.
|
Execute a workflow with optional input data.
|
||||||
If async_execution is True, returns immediately with a task ID.
|
If async_execution is True, returns immediately with a task ID.
|
||||||
|
|
||||||
File objects in input will be automatically detected and converted to base64.
|
File objects in input_data will be automatically detected and converted to base64.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
workflow_id: The ID of the workflow to execute
|
workflow_id: The ID of the workflow to execute
|
||||||
input: Input data to pass to the workflow. Can be a dict (spread at root level),
|
input_data: Input data to pass to the workflow (can include file-like objects)
|
||||||
primitive value (string, number, bool), or list (wrapped in 'input' field).
|
|
||||||
File-like objects within dicts are automatically converted to base64.
|
|
||||||
timeout: Timeout in seconds (default: 30.0)
|
timeout: Timeout in seconds (default: 30.0)
|
||||||
stream: Enable streaming responses (default: None)
|
stream: Enable streaming responses (default: None)
|
||||||
selected_outputs: Block outputs to stream (e.g., ["agent1.content"])
|
selected_outputs: Block outputs to stream (e.g., ["agent1.content"])
|
||||||
@@ -186,15 +193,8 @@ class SimStudioClient:
|
|||||||
headers['X-Execution-Mode'] = 'async'
|
headers['X-Execution-Mode'] = 'async'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Build JSON body - spread dict inputs at root level, wrap primitives/lists in 'input' field
|
# Build JSON body - spread input at root level, then add API control parameters
|
||||||
body = {}
|
body = input_data.copy() if input_data is not None else {}
|
||||||
if input is not None:
|
|
||||||
if isinstance(input, dict):
|
|
||||||
# Dict input: spread at root level (matches curl/API behavior)
|
|
||||||
body = input.copy()
|
|
||||||
else:
|
|
||||||
# Primitive or list input: wrap in 'input' field
|
|
||||||
body = {'input': input}
|
|
||||||
|
|
||||||
# Convert any file objects in the input to base64 format
|
# Convert any file objects in the input to base64 format
|
||||||
body = self._convert_files_to_base64(body)
|
body = self._convert_files_to_base64(body)
|
||||||
@@ -320,18 +320,20 @@ class SimStudioClient:
|
|||||||
def execute_workflow_sync(
|
def execute_workflow_sync(
|
||||||
self,
|
self,
|
||||||
workflow_id: str,
|
workflow_id: str,
|
||||||
input: Optional[Any] = None,
|
input_data: Optional[Dict[str, Any]] = None,
|
||||||
*,
|
|
||||||
timeout: float = 30.0,
|
timeout: float = 30.0,
|
||||||
stream: Optional[bool] = None,
|
stream: Optional[bool] = None,
|
||||||
selected_outputs: Optional[list] = None
|
selected_outputs: Optional[list] = None
|
||||||
) -> WorkflowExecutionResult:
|
) -> WorkflowExecutionResult:
|
||||||
"""
|
"""
|
||||||
Execute a workflow synchronously (ensures non-async mode).
|
Execute a workflow and poll for completion (useful for long-running workflows).
|
||||||
|
|
||||||
|
Note: Currently, the API is synchronous, so this method just calls execute_workflow.
|
||||||
|
In the future, if async execution is added, this method can be enhanced.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
workflow_id: The ID of the workflow to execute
|
workflow_id: The ID of the workflow to execute
|
||||||
input: Input data to pass to the workflow (can include file-like objects)
|
input_data: Input data to pass to the workflow (can include file-like objects)
|
||||||
timeout: Timeout for the initial request in seconds
|
timeout: Timeout for the initial request in seconds
|
||||||
stream: Enable streaming responses (default: None)
|
stream: Enable streaming responses (default: None)
|
||||||
selected_outputs: Block outputs to stream (e.g., ["agent1.content"])
|
selected_outputs: Block outputs to stream (e.g., ["agent1.content"])
|
||||||
@@ -342,14 +344,9 @@ class SimStudioClient:
|
|||||||
Raises:
|
Raises:
|
||||||
SimStudioError: If the workflow execution fails
|
SimStudioError: If the workflow execution fails
|
||||||
"""
|
"""
|
||||||
return self.execute_workflow(
|
# For now, the API is synchronous, so we just execute directly
|
||||||
workflow_id,
|
# In the future, if async execution is added, this method can be enhanced
|
||||||
input,
|
return self.execute_workflow(workflow_id, input_data, timeout, stream, selected_outputs)
|
||||||
timeout=timeout,
|
|
||||||
stream=stream,
|
|
||||||
selected_outputs=selected_outputs,
|
|
||||||
async_execution=False
|
|
||||||
)
|
|
||||||
|
|
||||||
def set_api_key(self, api_key: str) -> None:
|
def set_api_key(self, api_key: str) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -413,8 +410,7 @@ class SimStudioClient:
|
|||||||
def execute_with_retry(
|
def execute_with_retry(
|
||||||
self,
|
self,
|
||||||
workflow_id: str,
|
workflow_id: str,
|
||||||
input: Optional[Any] = None,
|
input_data: Optional[Dict[str, Any]] = None,
|
||||||
*,
|
|
||||||
timeout: float = 30.0,
|
timeout: float = 30.0,
|
||||||
stream: Optional[bool] = None,
|
stream: Optional[bool] = None,
|
||||||
selected_outputs: Optional[list] = None,
|
selected_outputs: Optional[list] = None,
|
||||||
@@ -429,7 +425,7 @@ class SimStudioClient:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
workflow_id: The ID of the workflow to execute
|
workflow_id: The ID of the workflow to execute
|
||||||
input: Input data to pass to the workflow (can include file-like objects)
|
input_data: Input data to pass to the workflow (can include file-like objects)
|
||||||
timeout: Timeout in seconds
|
timeout: Timeout in seconds
|
||||||
stream: Enable streaming responses
|
stream: Enable streaming responses
|
||||||
selected_outputs: Block outputs to stream
|
selected_outputs: Block outputs to stream
|
||||||
@@ -452,11 +448,11 @@ class SimStudioClient:
|
|||||||
try:
|
try:
|
||||||
return self.execute_workflow(
|
return self.execute_workflow(
|
||||||
workflow_id,
|
workflow_id,
|
||||||
input,
|
input_data,
|
||||||
timeout=timeout,
|
timeout,
|
||||||
stream=stream,
|
stream,
|
||||||
selected_outputs=selected_outputs,
|
selected_outputs,
|
||||||
async_execution=async_execution
|
async_execution
|
||||||
)
|
)
|
||||||
except SimStudioError as e:
|
except SimStudioError as e:
|
||||||
if e.code != 'RATE_LIMIT_EXCEEDED':
|
if e.code != 'RATE_LIMIT_EXCEEDED':
|
||||||
|
|||||||