diff --git a/.github/workflows/publish-cli.yml b/.github/workflows/publish-cli.yml index 81b1b6893df..41bce6a7b68 100644 --- a/.github/workflows/publish-cli.yml +++ b/.github/workflows/publish-cli.yml @@ -4,7 +4,7 @@ on: push: branches: [main] paths: - - 'packages/simstudio/**' + - 'packages/cli/**' jobs: publish-npm: @@ -25,16 +25,16 @@ jobs: registry-url: 'https://registry.npmjs.org/' - name: Install dependencies - working-directory: packages/simstudio + working-directory: packages/cli run: bun install - name: Build package - working-directory: packages/simstudio + working-directory: packages/cli run: bun run build - name: Get package version id: package_version - working-directory: packages/simstudio + working-directory: packages/cli run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT - name: Check if version already exists @@ -48,7 +48,7 @@ jobs: - name: Publish to npm if: steps.version_check.outputs.exists == 'false' - working-directory: packages/simstudio + working-directory: packages/cli run: npm publish --access=public env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/publish-python-sdk.yml b/.github/workflows/publish-python-sdk.yml new file mode 100644 index 00000000000..6892405de1f --- /dev/null +++ b/.github/workflows/publish-python-sdk.yml @@ -0,0 +1,89 @@ +name: Publish Python SDK + +on: + push: + branches: [main] + paths: + - 'packages/python-sdk/**' + +jobs: + publish-pypi: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + cache: 'pip' + + - name: Install build dependencies + run: | + python -m pip install --upgrade pip + pip install build twine pytest requests tomli + + - name: Run tests + working-directory: packages/python-sdk + run: | + PYTHONPATH=. pytest tests/ -v + + - name: Get package version + id: package_version + working-directory: packages/python-sdk + run: echo "version=$(python -c "import tomli; print(tomli.load(open('pyproject.toml', 'rb'))['project']['version'])")" >> $GITHUB_OUTPUT + + - name: Check if version already exists + id: version_check + run: | + if pip index versions simstudio-sdk | grep -q "${{ steps.package_version.outputs.version }}"; then + echo "exists=true" >> $GITHUB_OUTPUT + else + echo "exists=false" >> $GITHUB_OUTPUT + fi + + - name: Build package + if: steps.version_check.outputs.exists == 'false' + working-directory: packages/python-sdk + run: python -m build + + - name: Check package + if: steps.version_check.outputs.exists == 'false' + working-directory: packages/python-sdk + run: twine check dist/* + + - name: Publish to PyPI + if: steps.version_check.outputs.exists == 'false' + working-directory: packages/python-sdk + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} + run: twine upload dist/* + + - name: Log skipped publish + if: steps.version_check.outputs.exists == 'true' + run: echo "Skipped publishing because version ${{ steps.package_version.outputs.version }} already exists on PyPI" + + - name: Create GitHub Release + if: steps.version_check.outputs.exists == 'false' + uses: softprops/action-gh-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: python-sdk-v${{ steps.package_version.outputs.version }} + name: Python SDK v${{ steps.package_version.outputs.version }} + body: | + ## Python SDK v${{ steps.package_version.outputs.version }} + + Published simstudio-sdk==${{ steps.package_version.outputs.version }} to PyPI. + + ### Installation + ```bash + pip install simstudio-sdk==${{ steps.package_version.outputs.version }} + ``` + + ### Documentation + See the [README](https://github.com/simstudio/sim/tree/main/packages/python-sdk) for usage instructions. + draft: false + prerelease: false \ No newline at end of file diff --git a/.github/workflows/publish-ts-sdk.yml b/.github/workflows/publish-ts-sdk.yml new file mode 100644 index 00000000000..360f5aa20a6 --- /dev/null +++ b/.github/workflows/publish-ts-sdk.yml @@ -0,0 +1,85 @@ +name: Publish TypeScript SDK + +on: + push: + branches: [main] + paths: + - 'packages/ts-sdk/**' + +jobs: + publish-npm: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Setup Node.js for npm publishing + uses: actions/setup-node@v4 + with: + node-version: '18' + registry-url: 'https://registry.npmjs.org/' + + - name: Install dependencies + working-directory: packages/ts-sdk + run: bun install + + - name: Run tests + working-directory: packages/ts-sdk + run: bun run test + + - name: Build package + working-directory: packages/ts-sdk + run: bun run build + + - name: Get package version + id: package_version + working-directory: packages/ts-sdk + run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT + + - name: Check if version already exists + id: version_check + run: | + if npm view simstudio-ts-sdk@${{ steps.package_version.outputs.version }} version &> /dev/null; then + echo "exists=true" >> $GITHUB_OUTPUT + else + echo "exists=false" >> $GITHUB_OUTPUT + fi + + - name: Publish to npm + if: steps.version_check.outputs.exists == 'false' + working-directory: packages/ts-sdk + run: npm publish --access=public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Log skipped publish + if: steps.version_check.outputs.exists == 'true' + run: echo "Skipped publishing because version ${{ steps.package_version.outputs.version }} already exists on npm" + + - name: Create GitHub Release + if: steps.version_check.outputs.exists == 'false' + uses: softprops/action-gh-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: typescript-sdk-v${{ steps.package_version.outputs.version }} + name: TypeScript SDK v${{ steps.package_version.outputs.version }} + body: | + ## TypeScript SDK v${{ steps.package_version.outputs.version }} + + Published simstudio-ts-sdk@${{ steps.package_version.outputs.version }} to npm. + + ### Installation + ```bash + npm install simstudio-ts-sdk@${{ steps.package_version.outputs.version }} + ``` + + ### Documentation + See the [README](https://github.com/simstudio/sim/tree/main/packages/ts-sdk) for usage instructions. + draft: false + prerelease: false \ No newline at end of file diff --git a/.gitignore b/.gitignore index 33e7b36c952..08dedb8678c 100644 --- a/.gitignore +++ b/.gitignore @@ -29,7 +29,6 @@ sim-standalone.tar.gz # misc .DS_Store *.pem -uploads/ # env files .env @@ -63,4 +62,7 @@ docker-compose.collector.yml start-collector.sh # Turborepo -.turbo \ No newline at end of file +.turbo + +# VSCode +.vscode \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit index f54fc9cd5cf..36946c38ebf 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1 @@ -bunx lint-staged \ No newline at end of file +bun lint \ No newline at end of file diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index feac645b6ed..876fb6ad018 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -263,3 +263,10 @@ export const SlackIcon = (props: SVGProps) => ( ) + +export const ResponseIcon = (props: SVGProps) => ( + + + + +) diff --git a/apps/docs/components/ui/block-types.tsx b/apps/docs/components/ui/block-types.tsx index 1b2394666e7..96937dc728a 100644 --- a/apps/docs/components/ui/block-types.tsx +++ b/apps/docs/components/ui/block-types.tsx @@ -1,5 +1,13 @@ import { cn } from '@/lib/utils' -import { AgentIcon, ApiIcon, ChartBarIcon, CodeIcon, ConditionalIcon, ConnectIcon } from '../icons' +import { + AgentIcon, + ApiIcon, + ChartBarIcon, + CodeIcon, + ConditionalIcon, + ConnectIcon, + ResponseIcon, +} from '../icons' // Custom Feature component specifically for BlockTypes to handle the 6-item layout const BlockFeature = ({ @@ -127,6 +135,13 @@ export function BlockTypes() { icon: , href: '/blocks/evaluator', }, + { + title: 'Response', + description: + 'Send a response back to the caller with customizable data, status, and headers.', + icon: , + href: '/blocks/response', + }, ] const totalItems = features.length diff --git a/apps/docs/content/docs/blocks/meta.json b/apps/docs/content/docs/blocks/meta.json index 770522e1dd7..98a69a80e8a 100644 --- a/apps/docs/content/docs/blocks/meta.json +++ b/apps/docs/content/docs/blocks/meta.json @@ -1,4 +1,4 @@ { "title": "Blocks", - "pages": ["agent", "api", "condition", "function", "evaluator", "router"] + "pages": ["agent", "api", "condition", "function", "evaluator", "router", "response", "workflow"] } diff --git a/apps/docs/content/docs/blocks/response.mdx b/apps/docs/content/docs/blocks/response.mdx new file mode 100644 index 00000000000..2570acd872b --- /dev/null +++ b/apps/docs/content/docs/blocks/response.mdx @@ -0,0 +1,188 @@ +--- +title: Response +description: Send a structured response back to API calls +--- + +import { Callout } from 'fumadocs-ui/components/callout' +import { Step, Steps } from 'fumadocs-ui/components/steps' +import { Tab, Tabs } from 'fumadocs-ui/components/tabs' +import { ThemeImage } from '@/components/ui/theme-image' + +The Response block is the final component in API-enabled workflows that transforms your workflow's variables into a structured HTTP response. This block serves as the endpoint that returns data, status codes, and headers back to API callers. + + + + + Response blocks are terminal blocks - they mark the end of a workflow execution and cannot have further connections. + + +## Overview + +The Response block serves as the final output mechanism for API workflows, enabling you to: + + + + Return structured data: Transform workflow variables into JSON responses + + + Set HTTP status codes: Control the response status (200, 400, 500, etc.) + + + Configure headers: Add custom HTTP headers to the response + + + Reference variables: Use workflow variables dynamically in the response + + + +## Configuration Options + +### Response Data + +The response data is the main content that will be sent back to the API caller. This should be formatted as JSON and can include: + +- Static values +- Dynamic references to workflow variables using the `` syntax +- Nested objects and arrays +- Any valid JSON structure + +### Status Code + +Set the HTTP status code for the response. Common status codes include: + + + +
    +
  • 200: OK - Standard success response
  • +
  • 201: Created - Resource successfully created
  • +
  • 204: No Content - Success with no response body
  • +
+
+ +
    +
  • 400: Bad Request - Invalid request parameters
  • +
  • 401: Unauthorized - Authentication required
  • +
  • 404: Not Found - Resource doesn't exist
  • +
  • 422: Unprocessable Entity - Validation errors
  • +
+
+ +
    +
  • 500: Internal Server Error - Server-side error
  • +
  • 502: Bad Gateway - External service error
  • +
  • 503: Service Unavailable - Service temporarily down
  • +
+
+
+ +

+ Default status code is 200 if not specified. +

+ +### Response Headers + +Configure additional HTTP headers to include in the response. + +Headers are configured as key-value pairs: + +| Key | Value | +|-----|-------| +| Content-Type | application/json | +| Cache-Control | no-cache | +| X-API-Version | 1.0 | + +## Inputs and Outputs + + + +
    +
  • + data (JSON, optional): The JSON data to send in the response body +
  • +
  • + status (number, optional): HTTP status code (default: 200) +
  • +
  • + headers (JSON, optional): Additional response headers +
  • +
+
+ +
    +
  • + response: Complete response object containing: +
      +
    • data: The response body data
    • +
    • status: HTTP status code
    • +
    • headers: Response headers
    • +
    +
  • +
+
+
+ +## Variable References + +Use the `` syntax to dynamically insert workflow variables into your response: + +```json +{ + "user": { + "id": "", + "name": "", + "email": "" + }, + "query": "", + "results": "", + "totalFound": "", + "processingTime": "ms" +} +``` + + + Variable names are case-sensitive and must match exactly with the variables available in your workflow. + + +## Example Usage + +Here's an example of how a Response block might be configured for a user search API: + +```yaml +data: | + { + "success": true, + "data": { + "users": "", + "pagination": { + "page": "", + "limit": "", + "total": "" + } + }, + "query": { + "searchTerm": "", + "filters": "" + }, + "timestamp": "" + } +status: 200 +headers: + - key: X-Total-Count + value: + - key: Cache-Control + value: public, max-age=300 +``` + +## Best Practices + +- **Use meaningful status codes**: Choose appropriate HTTP status codes that accurately reflect the outcome of the workflow +- **Structure your responses consistently**: Maintain a consistent JSON structure across all your API endpoints for better developer experience +- **Include relevant metadata**: Add timestamps and version information to help with debugging and monitoring +- **Handle errors gracefully**: Use conditional logic in your workflow to set appropriate error responses with descriptive messages +- **Validate variable references**: Ensure all referenced variables exist and contain the expected data types before the Response block executes \ No newline at end of file diff --git a/apps/docs/content/docs/blocks/workflow.mdx b/apps/docs/content/docs/blocks/workflow.mdx new file mode 100644 index 00000000000..f45e0ce4173 --- /dev/null +++ b/apps/docs/content/docs/blocks/workflow.mdx @@ -0,0 +1,231 @@ +--- +title: Workflow +description: Execute other workflows as reusable components within your current workflow +--- + +import { Callout } from 'fumadocs-ui/components/callout' +import { Step, Steps } from 'fumadocs-ui/components/steps' +import { Tab, Tabs } from 'fumadocs-ui/components/tabs' +import { ThemeImage } from '@/components/ui/theme-image' + +The Workflow block allows you to execute other workflows as reusable components within your current workflow. This powerful feature enables modular design, code reuse, and the creation of complex nested workflows that can be composed from smaller, focused workflows. + + + + + Workflow blocks enable modular design by allowing you to compose complex workflows from smaller, reusable components. + + +## Overview + +The Workflow block serves as a bridge between workflows, enabling you to: + + + + Reuse existing workflows: Execute previously created workflows as components within new workflows + + + Create modular designs: Break down complex processes into smaller, manageable workflows + + + Maintain separation of concerns: Keep different business logic isolated in separate workflows + + + Enable team collaboration: Share and reuse workflows across different projects and team members + + + +## How It Works + +The Workflow block: + +1. Takes a reference to another workflow in your workspace +2. Passes input data from the current workflow to the child workflow +3. Executes the child workflow in an isolated context +4. Returns the results back to the parent workflow for further processing + +## Configuration Options + +### Workflow Selection + +Choose which workflow to execute from a dropdown list of available workflows in your workspace. The list includes: + +- All workflows you have access to in the current workspace +- Workflows shared with you by other team members +- Both enabled and disabled workflows (though only enabled workflows can be executed) + +### Input Data + +Define the data to pass to the child workflow: + +- **Single Variable Input**: Select a variable or block output to pass to the child workflow +- **Variable References**: Use `` to reference workflow variables +- **Block References**: Use `` to reference outputs from previous blocks +- **Automatic Mapping**: The selected data is automatically available as `start.response.input` in the child workflow +- **Optional**: The input field is optional - child workflows can run without input data +- **Type Preservation**: Variable types (strings, numbers, objects, etc.) are preserved when passed to the child workflow + +### Examples of Input References + +- `` - Pass a workflow variable +- `` - Pass the result from a previous block +- `` - Pass the original workflow input +- `` - Pass a specific field from an API response + +### Execution Context + +The child workflow executes with: + +- Its own isolated execution context +- Access to the same workspace resources (API keys, environment variables) +- Proper workspace membership and permission checks +- Independent logging and monitoring + +## Safety and Limitations + +To prevent infinite recursion and ensure system stability, the Workflow block includes several safety mechanisms: + + + **Cycle Detection**: The system automatically detects and prevents circular dependencies between workflows to avoid infinite loops. + + +- **Maximum Depth Limit**: Nested workflows are limited to a maximum depth of 10 levels +- **Cycle Detection**: Automatic detection and prevention of circular workflow dependencies +- **Timeout Protection**: Child workflows inherit timeout settings to prevent indefinite execution +- **Resource Limits**: Memory and execution time limits apply to prevent resource exhaustion + +## Inputs and Outputs + + + +
    +
  • + Workflow ID: The identifier of the workflow to execute +
  • +
  • + Input Variable: Variable or block reference to pass to the child workflow (e.g., `` or ``) +
  • +
+
+ +
    +
  • + Response: The complete output from the child workflow execution +
  • +
  • + Child Workflow Name: The name of the executed child workflow +
  • +
  • + Success Status: Boolean indicating whether the child workflow completed successfully +
  • +
  • + Error Information: Details about any errors that occurred during execution +
  • +
  • + Execution Metadata: Information about execution time, resource usage, and performance +
  • +
+
+
+ +## Example Usage + +Here's an example of how a Workflow block might be used to create a modular customer onboarding process: + +### Parent Workflow: Customer Onboarding +```yaml +# Main customer onboarding workflow +blocks: + - type: workflow + name: "Validate Customer Data" + workflowId: "customer-validation-workflow" + input: "" + + - type: workflow + name: "Setup Customer Account" + workflowId: "account-setup-workflow" + input: "" + + - type: workflow + name: "Send Welcome Email" + workflowId: "welcome-email-workflow" + input: "" +``` + +### Child Workflow: Customer Validation +```yaml +# Reusable customer validation workflow +# Access the input data using: start.response.input +blocks: + - type: function + name: "Validate Email" + code: | + const customerData = start.response.input; + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(customerData.email); + + - type: api + name: "Check Credit Score" + url: "https://api.creditcheck.com/score" + method: "POST" + body: "" +``` + +### Variable Reference Examples + +```yaml +# Using workflow variables +input: "" + +# Using block outputs +input: "" + +# Using nested object properties +input: "" + +# Using array elements (if supported by the resolver) +input: "" +``` + +## Access Control and Permissions + +The Workflow block respects workspace permissions and access controls: + +- **Workspace Membership**: Only workflows within the same workspace can be executed +- **Permission Inheritance**: Child workflows inherit the execution permissions of the parent workflow +- **API Key Access**: Child workflows have access to the same API keys and environment variables as the parent +- **User Context**: The execution maintains the original user context for audit and logging purposes + +## Best Practices + +- **Keep workflows focused**: Design child workflows to handle specific, well-defined tasks +- **Minimize nesting depth**: Avoid deeply nested workflow hierarchies for better maintainability +- **Handle errors gracefully**: Implement proper error handling for child workflow failures +- **Document dependencies**: Clearly document which workflows depend on others +- **Version control**: Consider versioning strategies for workflows that are used as components +- **Test independently**: Ensure child workflows can be tested and validated independently +- **Monitor performance**: Be aware that nested workflows can impact overall execution time + +## Common Patterns + +### Microservice Architecture +Break down complex business processes into smaller, focused workflows that can be developed and maintained independently. + +### Reusable Components +Create library workflows for common operations like data validation, email sending, or API integrations that can be reused across multiple projects. + +### Conditional Execution +Use workflow blocks within conditional logic to execute different business processes based on runtime conditions. + +### Parallel Processing +Combine workflow blocks with parallel execution to run multiple child workflows simultaneously for improved performance. + + + When designing modular workflows, think of each workflow as a function with clear inputs, outputs, and a single responsibility. + \ No newline at end of file diff --git a/apps/docs/content/docs/meta.json b/apps/docs/content/docs/meta.json index 953c887daab..6b37a43525f 100644 --- a/apps/docs/content/docs/meta.json +++ b/apps/docs/content/docs/meta.json @@ -12,7 +12,10 @@ "---Execution---", "execution", "---Advanced---", - "./variables/index" + "./variables/index", + "---SDKs---", + "./sdks/python", + "./sdks/typescript" ], "defaultOpen": true } diff --git a/apps/docs/content/docs/sdks/python.mdx b/apps/docs/content/docs/sdks/python.mdx new file mode 100644 index 00000000000..277080da7b2 --- /dev/null +++ b/apps/docs/content/docs/sdks/python.mdx @@ -0,0 +1,409 @@ +--- +title: Python SDK +description: The official Python SDK for Sim Studio +--- + +import { Callout } from 'fumadocs-ui/components/callout' +import { Card, Cards } from 'fumadocs-ui/components/card' +import { Step, Steps } from 'fumadocs-ui/components/steps' +import { Tab, Tabs } from 'fumadocs-ui/components/tabs' + +The official Python SDK for Sim Studio allows you to execute workflows programmatically from your Python applications. + + + The Python SDK supports Python 3.8+ and provides synchronous workflow execution. All workflow executions are currently synchronous. + + +## Installation + +Install the SDK using pip: + +```bash +pip install simstudio-sdk +``` + +## Quick Start + +Here's a simple example to get you started: + +```python +from simstudio import SimStudioClient + +# Initialize the client +client = SimStudioClient( + api_key="your-api-key-here", + base_url="https://simstudio.ai" # optional, defaults to https://simstudio.ai +) + +# Execute a workflow +try: + result = client.execute_workflow("workflow-id") + print("Workflow executed successfully:", result) +except Exception as error: + print("Workflow execution failed:", error) +``` + +## API Reference + +### SimStudioClient + +#### Constructor + +```python +SimStudioClient(api_key: str, base_url: str = "https://simstudio.ai") +``` + +**Parameters:** +- `api_key` (str): Your Sim Studio API key +- `base_url` (str, optional): Base URL for the Sim Studio API + +#### Methods + +##### execute_workflow() + +Execute a workflow with optional input data. + +```python +result = client.execute_workflow( + "workflow-id", + input_data={"message": "Hello, world!"}, + timeout=30.0 # 30 seconds +) +``` + +**Parameters:** +- `workflow_id` (str): The ID of the workflow to execute +- `input_data` (dict, optional): Input data to pass to the workflow +- `timeout` (float, optional): Timeout in seconds (default: 30.0) + +**Returns:** `WorkflowExecutionResult` + +##### get_workflow_status() + +Get the status of a workflow (deployment status, etc.). + +```python +status = client.get_workflow_status("workflow-id") +print("Is deployed:", status.is_deployed) +``` + +**Parameters:** +- `workflow_id` (str): The ID of the workflow + +**Returns:** `WorkflowStatus` + +##### validate_workflow() + +Validate that a workflow is ready for execution. + +```python +is_ready = client.validate_workflow("workflow-id") +if is_ready: + # Workflow is deployed and ready + pass +``` + +**Parameters:** +- `workflow_id` (str): The ID of the workflow + +**Returns:** `bool` + +##### execute_workflow_sync() + + + Currently, this method is identical to `execute_workflow()` since all executions are synchronous. This method is provided for future compatibility when asynchronous execution is added. + + +Execute a workflow (currently synchronous, same as `execute_workflow()`). + +```python +result = client.execute_workflow_sync( + "workflow-id", + input_data={"data": "some input"}, + timeout=60.0 +) +``` + +**Parameters:** +- `workflow_id` (str): The ID of the workflow to execute +- `input_data` (dict, optional): Input data to pass to the workflow +- `timeout` (float): Timeout for the initial request in seconds + +**Returns:** `WorkflowExecutionResult` + +##### set_api_key() + +Update the API key. + +```python +client.set_api_key("new-api-key") +``` + +##### set_base_url() + +Update the base URL. + +```python +client.set_base_url("https://my-custom-domain.com") +``` + +##### close() + +Close the underlying HTTP session. + +```python +client.close() +``` + +## Data Classes + +### WorkflowExecutionResult + +```python +@dataclass +class WorkflowExecutionResult: + success: bool + output: Optional[Any] = None + error: Optional[str] = None + logs: Optional[List[Any]] = None + metadata: Optional[Dict[str, Any]] = None + trace_spans: Optional[List[Any]] = None + total_duration: Optional[float] = None +``` + +### WorkflowStatus + +```python +@dataclass +class WorkflowStatus: + is_deployed: bool + deployed_at: Optional[str] = None + is_published: bool = False + needs_redeployment: bool = False +``` + +### SimStudioError + +```python +class SimStudioError(Exception): + def __init__(self, message: str, code: Optional[str] = None, status: Optional[int] = None): + super().__init__(message) + self.code = code + self.status = status +``` + +## Examples + +### Basic Workflow Execution + + + + Set up the SimStudioClient with your API key. + + + Check if the workflow is deployed and ready for execution. + + + Run the workflow with your input data. + + + Process the execution result and handle any errors. + + + +```python +import os +from simstudio import SimStudioClient + +client = SimStudioClient(api_key=os.getenv("SIMSTUDIO_API_KEY")) + +def run_workflow(): + try: + # Check if workflow is ready + is_ready = client.validate_workflow("my-workflow-id") + if not is_ready: + raise Exception("Workflow is not deployed or ready") + + # Execute the workflow + result = client.execute_workflow( + "my-workflow-id", + input_data={ + "message": "Process this data", + "user_id": "12345" + } + ) + + if result.success: + print("Output:", result.output) + print("Duration:", result.metadata.get("duration") if result.metadata else None) + else: + print("Workflow failed:", result.error) + + except Exception as error: + print("Error:", error) + +run_workflow() +``` + +### Error Handling + +Handle different types of errors that may occur during workflow execution: + +```python +from simstudio import SimStudioClient, SimStudioError +import os + +client = SimStudioClient(api_key=os.getenv("SIMSTUDIO_API_KEY")) + +def execute_with_error_handling(): + try: + result = client.execute_workflow("workflow-id") + return result + except SimStudioError as error: + if error.code == "UNAUTHORIZED": + print("Invalid API key") + elif error.code == "TIMEOUT": + print("Workflow execution timed out") + elif error.code == "USAGE_LIMIT_EXCEEDED": + print("Usage limit exceeded") + elif error.code == "INVALID_JSON": + print("Invalid JSON in request body") + else: + print(f"Workflow error: {error}") + raise + except Exception as error: + print(f"Unexpected error: {error}") + raise +``` + +### Context Manager Usage + +Use the client as a context manager to automatically handle resource cleanup: + +```python +from simstudio import SimStudioClient +import os + +# Using context manager to automatically close the session +with SimStudioClient(api_key=os.getenv("SIMSTUDIO_API_KEY")) as client: + result = client.execute_workflow("workflow-id") + print("Result:", result) +# Session is automatically closed here +``` + +### Batch Workflow Execution + +Execute multiple workflows efficiently: + +```python +from simstudio import SimStudioClient +import os + +client = SimStudioClient(api_key=os.getenv("SIMSTUDIO_API_KEY")) + +def execute_workflows_batch(workflow_data_pairs): + """Execute multiple workflows with different input data.""" + results = [] + + for workflow_id, input_data in workflow_data_pairs: + try: + # Validate workflow before execution + if not client.validate_workflow(workflow_id): + print(f"Skipping {workflow_id}: not deployed") + continue + + result = client.execute_workflow(workflow_id, input_data) + results.append({ + "workflow_id": workflow_id, + "success": result.success, + "output": result.output, + "error": result.error + }) + + except Exception as error: + results.append({ + "workflow_id": workflow_id, + "success": False, + "error": str(error) + }) + + return results + +# Example usage +workflows = [ + ("workflow-1", {"type": "analysis", "data": "sample1"}), + ("workflow-2", {"type": "processing", "data": "sample2"}), +] + +results = execute_workflows_batch(workflows) +for result in results: + print(f"Workflow {result['workflow_id']}: {'Success' if result['success'] else 'Failed'}") +``` + +### Environment Configuration + +Configure the client using environment variables: + + + + ```python + import os + from simstudio import SimStudioClient + + # Development configuration + client = SimStudioClient( + api_key=os.getenv("SIMSTUDIO_API_KEY"), + base_url=os.getenv("SIMSTUDIO_BASE_URL", "https://simstudio.ai") + ) + ``` + + + ```python + import os + from simstudio import SimStudioClient + + # Production configuration with error handling + api_key = os.getenv("SIMSTUDIO_API_KEY") + if not api_key: + raise ValueError("SIMSTUDIO_API_KEY environment variable is required") + + client = SimStudioClient( + api_key=api_key, + base_url=os.getenv("SIMSTUDIO_BASE_URL", "https://simstudio.ai") + ) + ``` + + + +## Getting Your API Key + + + + Navigate to [Sim Studio](https://simstudio.ai) and log in to your account. + + + Navigate to the workflow you want to execute programmatically. + + + Click on "Deploy" to deploy your workflow if it hasn't been deployed yet. + + + During the deployment process, select or create an API key. + + + Copy the API key to use in your Python application. + + + + + Keep your API key secure and never commit it to version control. Use environment variables or secure configuration management. + + +## Requirements + +- Python 3.8+ +- requests >= 2.25.0 + +## License + +Apache-2.0 \ No newline at end of file diff --git a/apps/docs/content/docs/sdks/typescript.mdx b/apps/docs/content/docs/sdks/typescript.mdx new file mode 100644 index 00000000000..6fb4bf4f7c8 --- /dev/null +++ b/apps/docs/content/docs/sdks/typescript.mdx @@ -0,0 +1,598 @@ +--- +title: TypeScript/JavaScript SDK +description: The official TypeScript/JavaScript SDK for Sim Studio +--- + +import { Callout } from 'fumadocs-ui/components/callout' +import { Card, Cards } from 'fumadocs-ui/components/card' +import { Step, Steps } from 'fumadocs-ui/components/steps' +import { Tab, Tabs } from 'fumadocs-ui/components/tabs' + +The official TypeScript/JavaScript SDK for Sim Studio allows you to execute workflows programmatically from your Node.js applications, web applications, and other JavaScript environments. + + + The TypeScript SDK provides full type safety and supports both Node.js and browser environments. All workflow executions are currently synchronous. + + +## Installation + +Install the SDK using your preferred package manager: + + + + ```bash + npm install simstudio-ts-sdk + ``` + + + ```bash + yarn add simstudio-ts-sdk + ``` + + + ```bash + bun add simstudio-ts-sdk + ``` + + + +## Quick Start + +Here's a simple example to get you started: + +```typescript +import { SimStudioClient } from 'simstudio-ts-sdk'; + +// Initialize the client +const client = new SimStudioClient({ + apiKey: 'your-api-key-here', + baseUrl: 'https://simstudio.ai' // optional, defaults to https://simstudio.ai +}); + +// Execute a workflow +try { + const result = await client.executeWorkflow('workflow-id'); + console.log('Workflow executed successfully:', result); +} catch (error) { + console.error('Workflow execution failed:', error); +} +``` + +## API Reference + +### SimStudioClient + +#### Constructor + +```typescript +new SimStudioClient(config: SimStudioConfig) +``` + +**Configuration:** +- `config.apiKey` (string): Your Sim Studio API key +- `config.baseUrl` (string, optional): Base URL for the Sim Studio API (defaults to `https://simstudio.ai`) + +#### Methods + +##### executeWorkflow() + +Execute a workflow with optional input data. + +```typescript +const result = await client.executeWorkflow('workflow-id', { + input: { message: 'Hello, world!' }, + timeout: 30000 // 30 seconds +}); +``` + +**Parameters:** +- `workflowId` (string): The ID of the workflow to execute +- `options` (ExecutionOptions, optional): + - `input` (any): Input data to pass to the workflow + - `timeout` (number): Timeout in milliseconds (default: 30000) + +**Returns:** `Promise` + +##### getWorkflowStatus() + +Get the status of a workflow (deployment status, etc.). + +```typescript +const status = await client.getWorkflowStatus('workflow-id'); +console.log('Is deployed:', status.isDeployed); +``` + +**Parameters:** +- `workflowId` (string): The ID of the workflow + +**Returns:** `Promise` + +##### validateWorkflow() + +Validate that a workflow is ready for execution. + +```typescript +const isReady = await client.validateWorkflow('workflow-id'); +if (isReady) { + // Workflow is deployed and ready +} +``` + +**Parameters:** +- `workflowId` (string): The ID of the workflow + +**Returns:** `Promise` + +##### executeWorkflowSync() + + + Currently, this method is identical to `executeWorkflow()` since all executions are synchronous. This method is provided for future compatibility when asynchronous execution is added. + + +Execute a workflow (currently synchronous, same as `executeWorkflow()`). + +```typescript +const result = await client.executeWorkflowSync('workflow-id', { + input: { data: 'some input' }, + timeout: 60000 +}); +``` + +**Parameters:** +- `workflowId` (string): The ID of the workflow to execute +- `options` (ExecutionOptions, optional): + - `input` (any): Input data to pass to the workflow + - `timeout` (number): Timeout for the initial request in milliseconds + +**Returns:** `Promise` + +##### setApiKey() + +Update the API key. + +```typescript +client.setApiKey('new-api-key'); +``` + +##### setBaseUrl() + +Update the base URL. + +```typescript +client.setBaseUrl('https://my-custom-domain.com'); +``` + +## Types + +### WorkflowExecutionResult + +```typescript +interface WorkflowExecutionResult { + success: boolean; + output?: any; + error?: string; + logs?: any[]; + metadata?: { + duration?: number; + executionId?: string; + [key: string]: any; + }; + traceSpans?: any[]; + totalDuration?: number; +} +``` + +### WorkflowStatus + +```typescript +interface WorkflowStatus { + isDeployed: boolean; + deployedAt?: string; + isPublished: boolean; + needsRedeployment: boolean; +} +``` + +### SimStudioError + +```typescript +class SimStudioError extends Error { + code?: string; + status?: number; +} +``` + +## Examples + +### Basic Workflow Execution + + + + Set up the SimStudioClient with your API key. + + + Check if the workflow is deployed and ready for execution. + + + Run the workflow with your input data. + + + Process the execution result and handle any errors. + + + +```typescript +import { SimStudioClient } from 'simstudio-ts-sdk'; + +const client = new SimStudioClient({ + apiKey: process.env.SIMSTUDIO_API_KEY! +}); + +async function runWorkflow() { + try { + // Check if workflow is ready + const isReady = await client.validateWorkflow('my-workflow-id'); + if (!isReady) { + throw new Error('Workflow is not deployed or ready'); + } + + // Execute the workflow + const result = await client.executeWorkflow('my-workflow-id', { + input: { + message: 'Process this data', + userId: '12345' + } + }); + + if (result.success) { + console.log('Output:', result.output); + console.log('Duration:', result.metadata?.duration); + } else { + console.error('Workflow failed:', result.error); + } + } catch (error) { + console.error('Error:', error); + } +} + +runWorkflow(); +``` + +### Error Handling + +Handle different types of errors that may occur during workflow execution: + +```typescript +import { SimStudioClient, SimStudioError } from 'simstudio-ts-sdk'; + +const client = new SimStudioClient({ + apiKey: process.env.SIMSTUDIO_API_KEY! +}); + +async function executeWithErrorHandling() { + try { + const result = await client.executeWorkflow('workflow-id'); + return result; + } catch (error) { + if (error instanceof SimStudioError) { + switch (error.code) { + case 'UNAUTHORIZED': + console.error('Invalid API key'); + break; + case 'TIMEOUT': + console.error('Workflow execution timed out'); + break; + case 'USAGE_LIMIT_EXCEEDED': + console.error('Usage limit exceeded'); + break; + case 'INVALID_JSON': + console.error('Invalid JSON in request body'); + break; + default: + console.error('Workflow error:', error.message); + } + } else { + console.error('Unexpected error:', error); + } + throw error; + } +} +``` + +### Environment Configuration + +Configure the client using environment variables: + + + + ```typescript + import { SimStudioClient } from 'simstudio-ts-sdk'; + + // Development configuration + const apiKey = process.env.SIMSTUDIO_API_KEY; + if (!apiKey) { + throw new Error('SIMSTUDIO_API_KEY environment variable is required'); + } + + const client = new SimStudioClient({ + apiKey, + baseUrl: process.env.SIMSTUDIO_BASE_URL // optional + }); + ``` + + + ```typescript + import { SimStudioClient } from 'simstudio-ts-sdk'; + + // Production configuration with validation + const apiKey = process.env.SIMSTUDIO_API_KEY; + if (!apiKey) { + throw new Error('SIMSTUDIO_API_KEY environment variable is required'); + } + + const client = new SimStudioClient({ + apiKey, + baseUrl: process.env.SIMSTUDIO_BASE_URL || 'https://simstudio.ai' + }); + ``` + + + +### Node.js Express Integration + +Integrate with an Express.js server: + +```typescript +import express from 'express'; +import { SimStudioClient } from 'simstudio-ts-sdk'; + +const app = express(); +const client = new SimStudioClient({ + apiKey: process.env.SIMSTUDIO_API_KEY! +}); + +app.use(express.json()); + +app.post('/execute-workflow', async (req, res) => { + try { + const { workflowId, input } = req.body; + + const result = await client.executeWorkflow(workflowId, { + input, + timeout: 60000 + }); + + res.json({ + success: true, + data: result + }); + } catch (error) { + console.error('Workflow execution error:', error); + res.status(500).json({ + success: false, + error: error instanceof Error ? error.message : 'Unknown error' + }); + } +}); + +app.listen(3000, () => { + console.log('Server running on port 3000'); +}); +``` + +### Next.js API Route + +Use with Next.js API routes: + +```typescript +// pages/api/workflow.ts or app/api/workflow/route.ts +import { NextApiRequest, NextApiResponse } from 'next'; +import { SimStudioClient } from 'simstudio-ts-sdk'; + +const client = new SimStudioClient({ + apiKey: process.env.SIMSTUDIO_API_KEY! +}); + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + try { + const { workflowId, input } = req.body; + + const result = await client.executeWorkflow(workflowId, { + input, + timeout: 30000 + }); + + res.status(200).json(result); + } catch (error) { + console.error('Error executing workflow:', error); + res.status(500).json({ + error: 'Failed to execute workflow' + }); + } +} +``` + +### Browser Usage + +Use in the browser (with proper CORS configuration): + +```typescript +import { SimStudioClient } from 'simstudio-ts-sdk'; + +// Note: In production, use a proxy server to avoid exposing API keys +const client = new SimStudioClient({ + apiKey: 'your-public-api-key', // Use with caution in browser + baseUrl: 'https://simstudio.ai' +}); + +async function executeClientSideWorkflow() { + try { + const result = await client.executeWorkflow('workflow-id', { + input: { + userInput: 'Hello from browser' + } + }); + + console.log('Workflow result:', result); + + // Update UI with result + document.getElementById('result')!.textContent = + JSON.stringify(result.output, null, 2); + } catch (error) { + console.error('Error:', error); + } +} + +// Attach to button click +document.getElementById('executeBtn')?.addEventListener('click', executeClientSideWorkflow); +``` + + + When using the SDK in the browser, be careful not to expose sensitive API keys. Consider using a backend proxy or public API keys with limited permissions. + + +### React Hook Example + +Create a custom React hook for workflow execution: + +```typescript +import { useState, useCallback } from 'react'; +import { SimStudioClient, WorkflowExecutionResult } from 'simstudio-ts-sdk'; + +const client = new SimStudioClient({ + apiKey: process.env.NEXT_PUBLIC_SIMSTUDIO_API_KEY! +}); + +interface UseWorkflowResult { + result: WorkflowExecutionResult | null; + loading: boolean; + error: Error | null; + executeWorkflow: (workflowId: string, input?: any) => Promise; +} + +export function useWorkflow(): UseWorkflowResult { + const [result, setResult] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const executeWorkflow = useCallback(async (workflowId: string, input?: any) => { + setLoading(true); + setError(null); + setResult(null); + + try { + const workflowResult = await client.executeWorkflow(workflowId, { + input, + timeout: 30000 + }); + setResult(workflowResult); + } catch (err) { + setError(err instanceof Error ? err : new Error('Unknown error')); + } finally { + setLoading(false); + } + }, []); + + return { + result, + loading, + error, + executeWorkflow + }; +} + +// Usage in component +function WorkflowComponent() { + const { result, loading, error, executeWorkflow } = useWorkflow(); + + const handleExecute = () => { + executeWorkflow('my-workflow-id', { + message: 'Hello from React!' + }); + }; + + return ( +
+ + + {error &&
Error: {error.message}
} + {result && ( +
+

Result:

+
{JSON.stringify(result, null, 2)}
+
+ )} +
+ ); +} +``` + +## Getting Your API Key + + + + Navigate to [Sim Studio](https://simstudio.ai) and log in to your account. + + + Navigate to the workflow you want to execute programmatically. + + + Click on "Deploy" to deploy your workflow if it hasn't been deployed yet. + + + During the deployment process, select or create an API key. + + + Copy the API key to use in your TypeScript/JavaScript application. + + + + + Keep your API key secure and never commit it to version control. Use environment variables or secure configuration management. + + +## Requirements + +- Node.js 16+ +- TypeScript 5.0+ (for TypeScript projects) + +## TypeScript Support + +The SDK is written in TypeScript and provides full type safety: + +```typescript +import { + SimStudioClient, + WorkflowExecutionResult, + WorkflowStatus, + SimStudioError +} from 'simstudio-ts-sdk'; + +// Type-safe client initialization +const client: SimStudioClient = new SimStudioClient({ + apiKey: process.env.SIMSTUDIO_API_KEY! +}); + +// Type-safe workflow execution +const result: WorkflowExecutionResult = await client.executeWorkflow('workflow-id', { + input: { + message: 'Hello, TypeScript!' + } +}); + +// Type-safe status checking +const status: WorkflowStatus = await client.getWorkflowStatus('workflow-id'); +``` + +## License + +Apache-2.0 \ No newline at end of file diff --git a/apps/docs/content/docs/tools/google_calendar.mdx b/apps/docs/content/docs/tools/google_calendar.mdx index e539292d929..1bac14004d0 100644 --- a/apps/docs/content/docs/tools/google_calendar.mdx +++ b/apps/docs/content/docs/tools/google_calendar.mdx @@ -90,7 +90,7 @@ In Sim Studio, the Google Calendar integration enables your agents to programmat ## Usage Instructions -Integrate Google Calendar functionality to create, read, update, and list calendar events within your workflow. Automate scheduling, check availability, and manage events using OAuth authentication. +Integrate Google Calendar functionality to create, read, update, and list calendar events within your workflow. Automate scheduling, check availability, and manage events using OAuth authentication. Email invitations are sent asynchronously and delivery depends on recipients @@ -180,6 +180,38 @@ Create events from natural language text | --------- | ---- | | `content` | string | +### `google_calendar_invite` + +Invite attendees to an existing Google Calendar event + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `accessToken` | string | Yes | Access token for Google Calendar API | +| `calendarId` | string | No | Calendar ID \(defaults to primary\) | +| `eventId` | string | Yes | Event ID to invite attendees to | +| `attendees` | array | Yes | Array of attendee email addresses to invite | +| `sendUpdates` | string | No | How to send updates to attendees: all, externalOnly, or none | +| `replaceExisting` | boolean | No | Whether to replace existing attendees or add to them \(defaults to false\) | + +#### Output + +| Parameter | Type | +| --------- | ---- | +| `metadata` | string | +| `htmlLink` | string | +| `status` | string | +| `summary` | string | +| `description` | string | +| `location` | string | +| `start` | string | +| `end` | string | +| `attendees` | string | +| `creator` | string | +| `organizer` | string | +| `content` | string | + ## Block Configuration diff --git a/apps/docs/content/docs/tools/huggingface.mdx b/apps/docs/content/docs/tools/huggingface.mdx new file mode 100644 index 00000000000..837884ed42c --- /dev/null +++ b/apps/docs/content/docs/tools/huggingface.mdx @@ -0,0 +1,127 @@ +--- +title: Hugging Face +description: Use Hugging Face Inference API +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + + + + + + + `} +/> + +{/* MANUAL-CONTENT-START:intro */} +[HuggingFace](https://huggingface.co/) is a leading AI platform that provides access to thousands of pre-trained machine learning models and powerful inference capabilities. With its extensive model hub and robust API, HuggingFace offers comprehensive tools for both research and production AI applications. +With HuggingFace, you can: + +Access pre-trained models: Utilize models for text generation, translation, image processing, and more +Generate AI completions: Create content using state-of-the-art language models through the Inference API +Natural language processing: Process and analyze text with specialized NLP models +Deploy at scale: Host and serve models for production applications +Customize models: Fine-tune existing models for specific use cases + +In Sim Studio, the HuggingFace integration enables your agents to programmatically generate completions using the HuggingFace Inference API. This allows for powerful automation scenarios such as content generation, text analysis, code completion, and creative writing. Your agents can generate completions with natural language prompts, access specialized models for different tasks, and integrate AI-generated content into workflows. This integration bridges the gap between your AI workflows and machine learning capabilities, enabling seamless AI-powered automation with one of the world's most comprehensive ML platforms. +{/* MANUAL-CONTENT-END */} + + +## Usage Instructions + +Generate completions using Hugging Face Inference API with access to various open-source models. Leverage cutting-edge AI models for chat completions, content generation, and AI-powered conversations with customizable parameters. + + + +## Tools + +### `huggingface_chat` + +Generate completions using Hugging Face Inference API + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Hugging Face API token | +| `provider` | string | Yes | The provider to use for the API request \(e.g., novita, cerebras, etc.\) | +| `model` | string | Yes | Model to use for chat completions \(e.g., deepseek/deepseek-v3-0324\) | +| `content` | string | Yes | The user message content to send to the model | +| `systemPrompt` | string | No | System prompt to guide the model behavior | +| `maxTokens` | number | No | Maximum number of tokens to generate | +| `temperature` | number | No | Sampling temperature \(0-2\). Higher values make output more random | +| `stream` | boolean | No | Whether to stream the response | + +#### Output + +| Parameter | Type | +| --------- | ---- | +| `content` | string | +| `model` | string | +| `usage` | string | +| `completion_tokens` | string | +| `total_tokens` | string | + + + +## Block Configuration + +### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `systemPrompt` | string | No | System Prompt - Enter system prompt to guide the model behavior... | + + + +### Outputs + +| Output | Type | Description | +| ------ | ---- | ----------- | +| `response` | object | Output from response | +| ↳ `content` | string | content of the response | +| ↳ `model` | string | model of the response | +| ↳ `usage` | json | usage of the response | + + +## Notes + +- Category: `tools` +- Type: `huggingface` diff --git a/apps/docs/content/docs/tools/knowledge.mdx b/apps/docs/content/docs/tools/knowledge.mdx index 5da46bc00b6..3424c624213 100644 --- a/apps/docs/content/docs/tools/knowledge.mdx +++ b/apps/docs/content/docs/tools/knowledge.mdx @@ -1,6 +1,6 @@ --- title: Knowledge -description: Search knowledge +description: Use vector search --- import { BlockInfoCard } from "@/components/ui/block-info-card" @@ -49,7 +49,7 @@ In Sim Studio, the Knowledge Base block enables your agents to perform intellige ## Usage Instructions -Perform semantic vector search across your knowledge base to find the most relevant content. Uses advanced AI embeddings to understand meaning and context, returning the most similar documents to your search query. +Perform semantic vector search across one or more knowledge bases or upload new chunks to documents. Uses advanced AI embeddings to understand meaning and context for search operations. @@ -57,13 +57,13 @@ Perform semantic vector search across your knowledge base to find the most relev ### `knowledge_search` -Search for similar content in a knowledge base using vector similarity +Search for similar content in one or more knowledge bases using vector similarity #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `knowledgeBaseId` | string | Yes | ID of the knowledge base to search in | +| `knowledgeBaseIds` | string | Yes | ID of the knowledge base to search in, or comma-separated IDs for multiple knowledge bases | | `query` | string | Yes | Search query text | | `topK` | number | No | Number of most similar results to return \(1-100\) | @@ -73,10 +73,32 @@ Search for similar content in a knowledge base using vector similarity | --------- | ---- | | `results` | string | | `query` | string | -| `knowledgeBaseId` | string | -| `topK` | string | | `totalResults` | string | -| `message` | string | + +### `knowledge_upload_chunk` + +Upload a new chunk to a document in a knowledge base + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `knowledgeBaseId` | string | Yes | ID of the knowledge base containing the document | +| `documentId` | string | Yes | ID of the document to upload the chunk to | +| `content` | string | Yes | Content of the chunk to upload | + +#### Output + +| Parameter | Type | +| --------- | ---- | +| `data` | string | +| `chunkIndex` | string | +| `content` | string | +| `contentLength` | string | +| `tokenCount` | string | +| `enabled` | string | +| `createdAt` | string | +| `updatedAt` | string | @@ -86,7 +108,7 @@ Search for similar content in a knowledge base using vector similarity | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `knowledgeBaseId` | string | Yes | Knowledge Base - Select knowledge base | +| `operation` | string | Yes | Operation | @@ -97,10 +119,7 @@ Search for similar content in a knowledge base using vector similarity | `response` | object | Output from response | | ↳ `results` | json | results of the response | | ↳ `query` | string | query of the response | -| ↳ `knowledgeBaseId` | string | knowledgeBaseId of the response | -| ↳ `topK` | number | topK of the response | | ↳ `totalResults` | number | totalResults of the response | -| ↳ `message` | string | message of the response | ## Notes diff --git a/apps/docs/content/docs/tools/meta.json b/apps/docs/content/docs/tools/meta.json index 380fb990b8d..80332879640 100644 --- a/apps/docs/content/docs/tools/meta.json +++ b/apps/docs/content/docs/tools/meta.json @@ -19,6 +19,7 @@ "google_search", "google_sheets", "guesty", + "huggingface", "image_generator", "jina", "jira", diff --git a/apps/docs/public/static/dark/response-dark.png b/apps/docs/public/static/dark/response-dark.png new file mode 100644 index 00000000000..e52919887de Binary files /dev/null and b/apps/docs/public/static/dark/response-dark.png differ diff --git a/apps/docs/public/static/dark/workflow-dark.png b/apps/docs/public/static/dark/workflow-dark.png new file mode 100644 index 00000000000..6a03a49990a Binary files /dev/null and b/apps/docs/public/static/dark/workflow-dark.png differ diff --git a/apps/docs/public/static/light/response-light.png b/apps/docs/public/static/light/response-light.png new file mode 100644 index 00000000000..4503b967f18 Binary files /dev/null and b/apps/docs/public/static/light/response-light.png differ diff --git a/apps/docs/public/static/light/workflow-light.png b/apps/docs/public/static/light/workflow-light.png new file mode 100644 index 00000000000..b27376fefeb Binary files /dev/null and b/apps/docs/public/static/light/workflow-light.png differ diff --git a/apps/sim/.env.example b/apps/sim/.env.example index fc42b3b54bc..738e9760366 100644 --- a/apps/sim/.env.example +++ b/apps/sim/.env.example @@ -5,7 +5,10 @@ DATABASE_URL="postgresql://postgres:password@localhost:5432/postgres" BETTER_AUTH_SECRET=your_secret_key # Use `openssl rand -hex 32` to generate, or visit https://www.better-auth.com/docs/installation BETTER_AUTH_URL=http://localhost:3000 -## Security (Required) +# NextJS (Required) +NEXT_PUBLIC_APP_URL=http://localhost:3000 + +# Security (Required) ENCRYPTION_KEY=your_encryption_key # Use `openssl rand -hex 32` to generate # Email Provider (Optional) diff --git a/apps/sim/app/(auth)/components/social-login-buttons.tsx b/apps/sim/app/(auth)/components/social-login-buttons.tsx index 29fb289076f..09157d7de1e 100644 --- a/apps/sim/app/(auth)/components/social-login-buttons.tsx +++ b/apps/sim/app/(auth)/components/social-login-buttons.tsx @@ -17,7 +17,7 @@ interface SocialLoginButtonsProps { export function SocialLoginButtons({ githubAvailable, googleAvailable, - callbackURL = '/w', + callbackURL = '/workspace', isProduction, }: SocialLoginButtonsProps) { const [isGithubLoading, setIsGithubLoading] = useState(false) diff --git a/apps/sim/app/(auth)/layout.tsx b/apps/sim/app/(auth)/layout.tsx index 6a81c5ba228..d448a4eae24 100644 --- a/apps/sim/app/(auth)/layout.tsx +++ b/apps/sim/app/(auth)/layout.tsx @@ -3,7 +3,7 @@ import Image from 'next/image' import Link from 'next/link' import { GridPattern } from '../(landing)/components/grid-pattern' -import { NotificationList } from '../w/[id]/components/notifications/notifications' +import { NotificationList } from '../workspace/[workspaceId]/w/[workflowId]/components/notifications/notifications' export default function AuthLayout({ children }: { children: React.ReactNode }) { return ( diff --git a/apps/sim/app/(auth)/login/login-form.test.tsx b/apps/sim/app/(auth)/login/login-form.test.tsx index 481d546333b..a40edfd88fc 100644 --- a/apps/sim/app/(auth)/login/login-form.test.tsx +++ b/apps/sim/app/(auth)/login/login-form.test.tsx @@ -2,7 +2,7 @@ * @vitest-environment jsdom */ -import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' import { useRouter, useSearchParams } from 'next/navigation' import { beforeEach, describe, expect, it, vi } from 'vitest' import { client } from '@/lib/auth-client' @@ -104,7 +104,10 @@ describe('LoginPage', () => { it('should show loading state during form submission', async () => { const mockSignIn = vi.mocked(client.signIn.email) mockSignIn.mockImplementation( - () => new Promise((resolve) => resolve({ data: { user: { id: '1' } }, error: null })) + () => + new Promise((resolve) => + setTimeout(() => resolve({ data: { user: { id: '1' } }, error: null }), 100) + ) ) render() @@ -113,12 +116,16 @@ describe('LoginPage', () => { const passwordInput = screen.getByPlaceholderText(/enter your password/i) const submitButton = screen.getByRole('button', { name: /sign in/i }) - fireEvent.change(emailInput, { target: { value: 'test@example.com' } }) - fireEvent.change(passwordInput, { target: { value: 'password123' } }) - fireEvent.click(submitButton) + await act(async () => { + fireEvent.change(emailInput, { target: { value: 'test@example.com' } }) + fireEvent.change(passwordInput, { target: { value: 'password123' } }) + fireEvent.click(submitButton) + }) - expect(screen.getByText('Signing in...')).toBeInTheDocument() - expect(submitButton).toBeDisabled() + await waitFor(() => { + expect(screen.getByText('Signing in...')).toBeInTheDocument() + expect(submitButton).toBeDisabled() + }) }) }) @@ -142,7 +149,7 @@ describe('LoginPage', () => { { email: 'test@example.com', password: 'password123', - callbackURL: '/w', + callbackURL: '/workspace', }, expect.objectContaining({ onError: expect.any(Function), diff --git a/apps/sim/app/(auth)/login/login-form.tsx b/apps/sim/app/(auth)/login/login-form.tsx index 548d20b0508..88b9e5af624 100644 --- a/apps/sim/app/(auth)/login/login-form.tsx +++ b/apps/sim/app/(auth)/login/login-form.tsx @@ -5,7 +5,13 @@ import { Eye, EyeOff } from 'lucide-react' import Link from 'next/link' import { useRouter, useSearchParams } from 'next/navigation' import { Button } from '@/components/ui/button' -import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { client } from '@/lib/auth-client' @@ -119,7 +125,7 @@ export default function LoginPage({ const [showValidationError, setShowValidationError] = useState(false) // Initialize state for URL parameters - const [callbackUrl, setCallbackUrl] = useState('/w') + const [callbackUrl, setCallbackUrl] = useState('/workspace') const [isInviteFlow, setIsInviteFlow] = useState(false) // Forgot password states @@ -149,7 +155,7 @@ export default function LoginPage({ setCallbackUrl(callback) } else { logger.warn('Invalid callback URL detected and blocked:', { url: callback }) - // Keep the default safe value ('/w') + // Keep the default safe value ('/workspace') } } @@ -216,7 +222,7 @@ export default function LoginPage({ try { // Final validation before submission - const safeCallbackUrl = validateCallbackUrl(callbackUrl) ? callbackUrl : '/w' + const safeCallbackUrl = validateCallbackUrl(callbackUrl) ? callbackUrl : '/workspace' const result = await client.signIn.email( { @@ -494,11 +500,11 @@ export default function LoginPage({ Reset Password + + Enter your email address and we'll send you a link to reset your password. +
-
- Enter your email address and we'll send you a link to reset your password. -
+ > + {isStreaming ? ( + <> + + + + ) : ( + <> + + + )} -
+ - - - - + + ) diff --git a/apps/sim/app/chat/[subdomain]/components/input/voice-input.tsx b/apps/sim/app/chat/[subdomain]/components/input/voice-input.tsx index 2eda962ca90..3092457ec07 100644 --- a/apps/sim/app/chat/[subdomain]/components/input/voice-input.tsx +++ b/apps/sim/app/chat/[subdomain]/components/input/voice-input.tsx @@ -43,6 +43,7 @@ interface VoiceInputProps { isListening?: boolean disabled?: boolean large?: boolean + minimal?: boolean } export function VoiceInput({ @@ -50,6 +51,7 @@ export function VoiceInput({ isListening = false, disabled = false, large = false, + minimal = false, }: VoiceInputProps) { const [isSupported, setIsSupported] = useState(false) @@ -68,6 +70,24 @@ export function VoiceInput({ return null } + if (minimal) { + return ( + + + + ) + } + if (large) { return (
@@ -93,21 +113,22 @@ export function VoiceInput({ return (
- {/* Voice Button */} + {/* Voice Button - Now matches send button styling */} - + +
) diff --git a/apps/sim/app/chat/[subdomain]/components/voice-interface/components/particles.tsx b/apps/sim/app/chat/[subdomain]/components/voice-interface/components/particles.tsx index 806f8be3efb..ab698f9be27 100644 --- a/apps/sim/app/chat/[subdomain]/components/voice-interface/components/particles.tsx +++ b/apps/sim/app/chat/[subdomain]/components/voice-interface/components/particles.tsx @@ -402,37 +402,37 @@ export function ParticlesVisualization({ avgLevel: number ) => { if (isMuted) { - // Muted: dim gray-blue - uniforms.u_red.value = 0.4 - uniforms.u_green.value = 0.4 - uniforms.u_blue.value = 0.6 + // Muted: dim purple-gray + uniforms.u_red.value = 0.25 + uniforms.u_green.value = 0.1 + uniforms.u_blue.value = 0.5 } else if (isProcessingInterruption) { - // Interruption: bright orange/yellow - uniforms.u_red.value = 1.0 - uniforms.u_green.value = 0.7 - uniforms.u_blue.value = 0.2 - } else if (isPlayingAudio) { - // AI speaking: bright blue-purple + // Interruption: bright purple uniforms.u_red.value = 0.6 - uniforms.u_green.value = 0.4 - uniforms.u_blue.value = 1.0 + uniforms.u_green.value = 0.2 + uniforms.u_blue.value = 0.9 + } else if (isPlayingAudio) { + // AI speaking: brand purple (#701FFC) + uniforms.u_red.value = 0.44 + uniforms.u_green.value = 0.12 + uniforms.u_blue.value = 0.99 } else if (isListening && avgLevel > 10) { - // User speaking: bright green-blue with intensity-based variation + // User speaking: lighter purple with intensity-based variation const intensity = Math.min(avgLevel / 50, 1) - uniforms.u_red.value = 0.2 + intensity * 0.3 - uniforms.u_green.value = 0.8 + intensity * 0.2 - uniforms.u_blue.value = 0.6 + intensity * 0.4 + uniforms.u_red.value = 0.35 + intensity * 0.15 + uniforms.u_green.value = 0.1 + intensity * 0.1 + uniforms.u_blue.value = 0.8 + intensity * 0.2 } else if (isStreaming) { - // AI thinking: pulsing purple + // AI thinking: pulsing brand purple const pulse = (Math.sin(elapsedTime * 2) + 1) / 2 - uniforms.u_red.value = 0.7 + pulse * 0.3 - uniforms.u_green.value = 0.3 - uniforms.u_blue.value = 0.9 + pulse * 0.1 + uniforms.u_red.value = 0.35 + pulse * 0.15 + uniforms.u_green.value = 0.08 + pulse * 0.08 + uniforms.u_blue.value = 0.95 + pulse * 0.05 } else { - // Default idle: soft blue-purple - uniforms.u_red.value = 0.8 - uniforms.u_green.value = 0.6 - uniforms.u_blue.value = 1.0 + // Default idle: soft brand purple + uniforms.u_red.value = 0.4 + uniforms.u_green.value = 0.15 + uniforms.u_blue.value = 0.9 } } diff --git a/apps/sim/app/invite/[id]/invite.tsx b/apps/sim/app/invite/[id]/invite.tsx index 3cac00e554f..86b748b6866 100644 --- a/apps/sim/app/invite/[id]/invite.tsx +++ b/apps/sim/app/invite/[id]/invite.tsx @@ -131,7 +131,7 @@ export default function Invite() { // Redirect to workspace after a brief delay setTimeout(() => { - router.push('/w') + router.push('/workspace') }, 2000) } else { // For organization invites, use the client API @@ -153,7 +153,7 @@ export default function Invite() { // Redirect to workspace after a brief delay setTimeout(() => { - router.push('/w') + router.push('/workspace') }, 2000) } } catch (err: any) { diff --git a/apps/sim/app/invite/invite-error/invite-error.tsx b/apps/sim/app/invite/invite-error/invite-error.tsx index 480a59d2ae8..064a70b9332 100644 --- a/apps/sim/app/invite/invite-error/invite-error.tsx +++ b/apps/sim/app/invite/invite-error/invite-error.tsx @@ -54,7 +54,7 @@ export default function InviteError() {

{displayMessage}

- + diff --git a/apps/sim/app/w/[id]/components/panel/components/chat/chat.tsx b/apps/sim/app/w/[id]/components/panel/components/chat/chat.tsx deleted file mode 100644 index 5c719500c60..00000000000 --- a/apps/sim/app/w/[id]/components/panel/components/chat/chat.tsx +++ /dev/null @@ -1,382 +0,0 @@ -'use client' - -import { type KeyboardEvent, useEffect, useMemo, useRef } from 'react' -import { ArrowUp } from 'lucide-react' -import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' -import { ScrollArea } from '@/components/ui/scroll-area' -import { buildTraceSpans } from '@/lib/logs/trace-spans' -import type { BlockLog } from '@/executor/types' -import { calculateCost } from '@/providers/utils' -import { useExecutionStore } from '@/stores/execution/store' -import { useChatStore } from '@/stores/panel/chat/store' -import { useConsoleStore } from '@/stores/panel/console/store' -import { useWorkflowRegistry } from '@/stores/workflows/registry/store' -import { useWorkflowExecution } from '../../../../hooks/use-workflow-execution' -import { ChatMessage } from './components/chat-message/chat-message' -import { OutputSelect } from './components/output-select/output-select' - -interface ChatProps { - panelWidth: number - chatMessage: string - setChatMessage: (message: string) => void -} - -export function Chat({ panelWidth, chatMessage, setChatMessage }: ChatProps) { - const { activeWorkflowId } = useWorkflowRegistry() - const { - messages, - addMessage, - selectedWorkflowOutputs, - setSelectedWorkflowOutput, - appendMessageContent, - finalizeMessageStream, - getConversationId, - } = useChatStore() - const { entries } = useConsoleStore() - const messagesEndRef = useRef(null) - - // Use the execution store state to track if a workflow is executing - const { isExecuting } = useExecutionStore() - - // Get workflow execution functionality - const { handleRunWorkflow } = useWorkflowExecution() - - // Get output entries from console for the dropdown - const outputEntries = useMemo(() => { - if (!activeWorkflowId) return [] - return entries.filter((entry) => entry.workflowId === activeWorkflowId && entry.output) - }, [entries, activeWorkflowId]) - - // Get filtered messages for current workflow - const workflowMessages = useMemo(() => { - if (!activeWorkflowId) return [] - return messages - .filter((msg) => msg.workflowId === activeWorkflowId) - .sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()) - }, [messages, activeWorkflowId]) - - // Get selected workflow outputs - const selectedOutputs = useMemo(() => { - if (!activeWorkflowId) return [] - const selected = selectedWorkflowOutputs[activeWorkflowId] - - if (!selected || selected.length === 0) { - const defaultSelection = outputEntries.length > 0 ? [outputEntries[0].id] : [] - return defaultSelection - } - - // Ensure we have no duplicates in the selection - const dedupedSelection = [...new Set(selected)] - - // If deduplication removed items, update the store - if (dedupedSelection.length !== selected.length) { - setSelectedWorkflowOutput(activeWorkflowId, dedupedSelection) - return dedupedSelection - } - - return selected - }, [selectedWorkflowOutputs, activeWorkflowId, outputEntries, setSelectedWorkflowOutput]) - - // Auto-scroll to bottom when new messages are added - useEffect(() => { - if (messagesEndRef.current) { - messagesEndRef.current.scrollIntoView({ behavior: 'smooth' }) - } - }, [workflowMessages]) - - // Handle send message - const handleSendMessage = async () => { - if (!chatMessage.trim() || !activeWorkflowId || isExecuting) return - - // Store the message being sent for reference - const sentMessage = chatMessage.trim() - - // Get the conversationId for this workflow before adding the message - const conversationId = getConversationId(activeWorkflowId) - - // Add user message - addMessage({ - content: sentMessage, - workflowId: activeWorkflowId, - type: 'user', - }) - - // Clear input - setChatMessage('') - - // Execute the workflow to generate a response, passing the chat message and conversationId as input - const result = await handleRunWorkflow({ - input: sentMessage, - conversationId: conversationId, - }) - - // Check if we got a streaming response - if (result && 'stream' in result && result.stream instanceof ReadableStream) { - // Generate a unique ID for the message - const messageId = crypto.randomUUID() - - // Create a content buffer to collect initial content - let initialContent = '' - let fullContent = '' // Store the complete content for updating logs later - let hasAddedMessage = false - const executionResult = (result as any).execution // Store the execution result with type assertion - - try { - // Process the stream - const reader = result.stream.getReader() - const decoder = new TextDecoder() - - console.log('Starting to read from stream') - - while (true) { - try { - const { done, value } = await reader.read() - if (done) { - console.log('Stream complete') - break - } - - // Decode and append chunk - const chunk = decoder.decode(value, { stream: true }) // Use stream option - - if (chunk) { - initialContent += chunk - fullContent += chunk - - // Only add the message to UI once we have some actual content to show - if (!hasAddedMessage && initialContent.trim().length > 0) { - // Add message with initial content - cast to any to bypass type checking for id - addMessage({ - content: initialContent, - workflowId: activeWorkflowId, - type: 'workflow', - isStreaming: true, - id: messageId, - } as any) - hasAddedMessage = true - } else if (hasAddedMessage) { - // Append to existing message - appendMessageContent(messageId, chunk) - } - } - } catch (streamError) { - console.error('Error reading from stream:', streamError) - // Break the loop on error - break - } - } - - // If we never added a message (no content received), add it now - if (!hasAddedMessage && initialContent.trim().length > 0) { - addMessage({ - content: initialContent, - workflowId: activeWorkflowId, - type: 'workflow', - id: messageId, - } as any) - } - - // Update logs with the full streaming content if available - if (executionResult && fullContent.trim().length > 0) { - try { - // Format the final content properly to match what's shown for manual executions - // Include all the markdown and formatting from the streamed response - const formattedContent = fullContent - - // Calculate cost based on token usage if available - let costData: any - - if (executionResult.output?.response?.tokens) { - const tokens = executionResult.output.response.tokens - const model = executionResult.output?.response?.model || 'gpt-4o' - const cost = calculateCost( - model, - tokens.prompt || 0, - tokens.completion || 0, - false // Don't use cached input for chat responses - ) - costData = { ...cost, model } as any - } - - // Build trace spans and total duration before persisting - const { traceSpans, totalDuration } = buildTraceSpans(executionResult as any) - - // Create a completed execution ID - const completedExecutionId = - executionResult.metadata?.executionId || crypto.randomUUID() - - // Import the workflow execution hook for direct access to the workflow service - const workflowExecutionApi = await fetch(`/api/workflows/${activeWorkflowId}/log`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - executionId: completedExecutionId, - result: { - ...executionResult, - output: { - ...executionResult.output, - response: { - ...executionResult.output?.response, - content: formattedContent, - model: executionResult.output?.response?.model, - tokens: executionResult.output?.response?.tokens, - toolCalls: executionResult.output?.response?.toolCalls, - providerTiming: executionResult.output?.response?.providerTiming, - cost: costData || executionResult.output?.response?.cost, - }, - }, - cost: costData, - // Update the message to include the formatted content - logs: (executionResult.logs || []).map((log: BlockLog) => { - // Check if this is the streaming block by comparing with the selected output IDs - // Selected output IDs typically include the block ID we are streaming from - const isStreamingBlock = selectedOutputs.some( - (outputId) => - outputId === log.blockId || outputId.startsWith(`${log.blockId}_`) - ) - - if (isStreamingBlock && log.blockType === 'agent' && log.output?.response) { - return { - ...log, - output: { - ...log.output, - response: { - ...log.output.response, - content: formattedContent, - providerTiming: log.output.response.providerTiming, - cost: costData || log.output.response.cost, - }, - }, - } - } - return log - }), - metadata: { - ...executionResult.metadata, - source: 'chat', - completedAt: new Date().toISOString(), - isStreamingComplete: true, - cost: costData || executionResult.metadata?.cost, - providerTiming: executionResult.output?.response?.providerTiming, - }, - traceSpans: traceSpans, - totalDuration: totalDuration, - }, - }), - }) - - if (!workflowExecutionApi.ok) { - console.error('Failed to log complete streaming execution') - } - } catch (logError) { - console.error('Error logging complete streaming execution:', logError) - } - } - } catch (error) { - console.error('Error processing stream:', error) - - // If there's an error and we haven't added a message yet, add an error message - if (!hasAddedMessage) { - addMessage({ - content: 'Error: Failed to process the streaming response.', - workflowId: activeWorkflowId, - type: 'workflow', - id: messageId, - } as any) - } else { - // Otherwise append the error to the existing message - appendMessageContent(messageId, '\n\nError: Failed to process the streaming response.') - } - } finally { - console.log('Finalizing stream') - if (hasAddedMessage) { - finalizeMessageStream(messageId) - } - } - } - } - - // Handle key press - const handleKeyPress = (e: KeyboardEvent) => { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault() - handleSendMessage() - } - } - - // Handle output selection - const handleOutputSelection = (values: string[]) => { - // Ensure no duplicates in selection - const dedupedValues = [...new Set(values)] - - if (activeWorkflowId) { - // If array is empty, explicitly set to empty array to ensure complete reset - if (dedupedValues.length === 0) { - setSelectedWorkflowOutput(activeWorkflowId, []) - } else { - setSelectedWorkflowOutput(activeWorkflowId, dedupedValues) - } - } - } - - return ( -
- {/* Output Source Dropdown */} -
- -
- - {/* Main layout with fixed heights to ensure input stays visible */} -
- {/* Chat messages section - Scrollable area */} -
- -
- {workflowMessages.length === 0 ? ( -
- No messages yet -
- ) : ( - workflowMessages.map((message) => ( - - )) - )} -
-
- -
- - {/* Input section - Fixed height */} -
-
- setChatMessage(e.target.value)} - onKeyDown={handleKeyPress} - placeholder='Type a message...' - className='h-10 flex-1 focus-visible:ring-0 focus-visible:ring-offset-0' - disabled={!activeWorkflowId || isExecuting} - /> - -
-
-
-
- ) -} diff --git a/apps/sim/app/w/[id]/components/toolbar/components/toolbar-loop-block/toolbar-loop-block.tsx b/apps/sim/app/w/[id]/components/toolbar/components/toolbar-loop-block/toolbar-loop-block.tsx deleted file mode 100644 index d07ca5e5713..00000000000 --- a/apps/sim/app/w/[id]/components/toolbar/components/toolbar-loop-block/toolbar-loop-block.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { useCallback } from 'react' -import { LoopTool } from '../../../loop-node/loop-config' - -// Custom component for the Loop Tool -export default function LoopToolbarItem() { - const handleDragStart = (e: React.DragEvent) => { - // Only send the essential data for the loop node - const simplifiedData = { - type: 'loop', - } - e.dataTransfer.setData('application/json', JSON.stringify(simplifiedData)) - e.dataTransfer.effectAllowed = 'move' - } - - // Handle click to add loop block - const handleClick = useCallback((e: React.MouseEvent) => { - // Dispatch a custom event to be caught by the workflow component - const event = new CustomEvent('add-block-from-toolbar', { - detail: { - type: 'loop', - clientX: e.clientX, - clientY: e.clientY, - }, - }) - window.dispatchEvent(event) - }, []) - - return ( -
-
- -
-
-

{LoopTool.name}

-

{LoopTool.description}

-
-
- ) -} diff --git a/apps/sim/app/w/[id]/components/toolbar/components/toolbar-parallel-block/toolbar-parallel-block.tsx b/apps/sim/app/w/[id]/components/toolbar/components/toolbar-parallel-block/toolbar-parallel-block.tsx deleted file mode 100644 index 9f277ba67ba..00000000000 --- a/apps/sim/app/w/[id]/components/toolbar/components/toolbar-parallel-block/toolbar-parallel-block.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { useCallback } from 'react' -import { ParallelTool } from '../../../parallel-node/parallel-config' - -// Custom component for the Parallel Tool -export default function ParallelToolbarItem() { - const handleDragStart = (e: React.DragEvent) => { - // Only send the essential data for the parallel node - const simplifiedData = { - type: 'parallel', - } - e.dataTransfer.setData('application/json', JSON.stringify(simplifiedData)) - e.dataTransfer.effectAllowed = 'move' - } - - // Handle click to add parallel block - const handleClick = useCallback((e: React.MouseEvent) => { - // Dispatch a custom event to be caught by the workflow component - const event = new CustomEvent('add-block-from-toolbar', { - detail: { - type: 'parallel', - clientX: e.clientX, - clientY: e.clientY, - }, - bubbles: true, - }) - window.dispatchEvent(event) - }, []) - - return ( -
-
- -
-
-

{ParallelTool.name}

-

{ParallelTool.description}

-
-
- ) -} diff --git a/apps/sim/app/w/[id]/components/toolbar/toolbar.tsx b/apps/sim/app/w/[id]/components/toolbar/toolbar.tsx deleted file mode 100644 index a5b6d6a1746..00000000000 --- a/apps/sim/app/w/[id]/components/toolbar/toolbar.tsx +++ /dev/null @@ -1,119 +0,0 @@ -'use client' - -import { useMemo, useState } from 'react' -import { PanelLeftClose, PanelRight, Search } from 'lucide-react' -import { Input } from '@/components/ui/input' -import { ScrollArea } from '@/components/ui/scroll-area' -import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' -import { getAllBlocks, getBlocksByCategory } from '@/blocks' -import type { BlockCategory } from '@/blocks/types' -import { useSidebarStore } from '@/stores/sidebar/store' -import { ToolbarBlock } from './components/toolbar-block/toolbar-block' -import LoopToolbarItem from './components/toolbar-loop-block/toolbar-loop-block' -import ParallelToolbarItem from './components/toolbar-parallel-block/toolbar-parallel-block' -import { ToolbarTabs } from './components/toolbar-tabs/toolbar-tabs' - -export function Toolbar() { - const [activeTab, setActiveTab] = useState('blocks') - const [searchQuery, setSearchQuery] = useState('') - const { mode, isExpanded } = useSidebarStore() - // In hover mode, act as if sidebar is always collapsed for layout purposes - const isSidebarCollapsed = - mode === 'expanded' ? !isExpanded : mode === 'collapsed' || mode === 'hover' - - // State to track if toolbar is open - independent of sidebar state - const [isToolbarOpen, setIsToolbarOpen] = useState(true) - - const blocks = useMemo(() => { - const filteredBlocks = !searchQuery.trim() ? getBlocksByCategory(activeTab) : getAllBlocks() - - return filteredBlocks.filter((block) => { - if (block.type === 'starter' || block.hideFromToolbar) return false - - return ( - !searchQuery.trim() || - block.name.toLowerCase().includes(searchQuery.toLowerCase()) || - block.description.toLowerCase().includes(searchQuery.toLowerCase()) - ) - }) - }, [searchQuery, activeTab]) - - // Show toolbar button when it's closed, regardless of sidebar state - if (!isToolbarOpen) { - return ( - - - - - Open Toolbar - - ) - } - - return ( -
-
-
-
- - setSearchQuery(e.target.value)} - autoComplete='off' - autoCorrect='off' - autoCapitalize='off' - spellCheck='false' - /> -
-
- - {!searchQuery && ( -
- -
- )} - - -
-
- {blocks.map((block) => ( - - ))} - {activeTab === 'blocks' && !searchQuery && ( - <> - - - - )} -
-
-
- -
- - - - - Close Toolbar - -
-
-
- ) -} diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/starter/input-format.tsx b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/starter/input-format.tsx deleted file mode 100644 index 6bac8da852d..00000000000 --- a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/starter/input-format.tsx +++ /dev/null @@ -1,231 +0,0 @@ -import { ChevronDown, Plus, Trash } from 'lucide-react' -import { Badge } from '@/components/ui/badge' -import { Button } from '@/components/ui/button' -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu' -import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' -import { cn } from '@/lib/utils' -import { useSubBlockValue } from '../../hooks/use-sub-block-value' - -interface InputField { - id: string - name: string - type: 'string' | 'number' | 'boolean' | 'object' | 'array' - collapsed?: boolean -} - -interface InputFormatProps { - blockId: string - subBlockId: string - isPreview?: boolean - previewValue?: InputField[] | null -} - -// Default values -const DEFAULT_FIELD: InputField = { - id: crypto.randomUUID(), - name: '', - type: 'string', - collapsed: true, -} - -export function InputFormat({ - blockId, - subBlockId, - isPreview = false, - previewValue, -}: InputFormatProps) { - const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId) - - // Use preview value when in preview mode, otherwise use store value - const value = isPreview ? previewValue : storeValue - const fields: InputField[] = value || [DEFAULT_FIELD] - - // Field operations - const addField = () => { - if (isPreview) return - - const newField: InputField = { - ...DEFAULT_FIELD, - id: crypto.randomUUID(), - } - setStoreValue([...fields, newField]) - } - - const removeField = (id: string) => { - if (isPreview || fields.length === 1) return - setStoreValue(fields.filter((field: InputField) => field.id !== id)) - } - - // Update handlers - const updateField = (id: string, field: keyof InputField, value: any) => { - if (isPreview) return - setStoreValue(fields.map((f: InputField) => (f.id === id ? { ...f, [field]: value } : f))) - } - - const toggleCollapse = (id: string) => { - if (isPreview) return - setStoreValue( - fields.map((f: InputField) => (f.id === id ? { ...f, collapsed: !f.collapsed } : f)) - ) - } - - // Field header - const renderFieldHeader = (field: InputField, index: number) => { - const isUnconfigured = !field.name || field.name.trim() === '' - - return ( -
toggleCollapse(field.id)} - > -
- - {field.name ? field.name : `Field ${index + 1}`} - - {field.name && ( - - {field.type} - - )} -
-
e.stopPropagation()}> - - - -
-
- ) - } - - // Check if any fields have been configured - const hasConfiguredFields = fields.some((field) => field.name && field.name.trim() !== '') - - // Main render - return ( -
- {fields.map((field, index) => { - const isUnconfigured = !field.name || field.name.trim() === '' - - return ( -
- {renderFieldHeader(field, index)} - - {!field.collapsed && ( -
-
- - updateField(field.id, 'name', e.target.value)} - placeholder='firstName' - disabled={isPreview} - className='h-9 placeholder:text-muted-foreground/50' - /> -
- -
- - - - - - - updateField(field.id, 'type', 'string')} - className='cursor-pointer' - > - Aa - String - - updateField(field.id, 'type', 'number')} - className='cursor-pointer' - > - 123 - Number - - updateField(field.id, 'type', 'boolean')} - className='cursor-pointer' - > - 0/1 - Boolean - - updateField(field.id, 'type', 'object')} - className='cursor-pointer' - > - {'{}'} - Object - - updateField(field.id, 'type', 'array')} - className='cursor-pointer' - > - [] - Array - - - -
-
- )} -
- ) - })} - - {!hasConfiguredFields && ( -
- Define fields above to enable structured API input -
- )} -
- ) -} diff --git a/apps/sim/app/w/components/providers/providers.tsx b/apps/sim/app/w/components/providers/providers.tsx deleted file mode 100644 index 86bb2e6c9b5..00000000000 --- a/apps/sim/app/w/components/providers/providers.tsx +++ /dev/null @@ -1,14 +0,0 @@ -'use client' - -import { TooltipProvider } from '@/components/ui/tooltip' -import { ThemeProvider } from './theme-provider' - -export default function Providers({ children }: { children: React.ReactNode }) { - return ( - - - {children} - - - ) -} diff --git a/apps/sim/app/w/components/sidebar/components/invite-modal/invite-modal.tsx b/apps/sim/app/w/components/sidebar/components/invite-modal/invite-modal.tsx deleted file mode 100644 index 9110bbef9dd..00000000000 --- a/apps/sim/app/w/components/sidebar/components/invite-modal/invite-modal.tsx +++ /dev/null @@ -1,359 +0,0 @@ -'use client' - -import { type KeyboardEvent, useState } from 'react' -import { Loader2, X } from 'lucide-react' -import { Button } from '@/components/ui/button' -import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' -import { Input } from '@/components/ui/input' -import { cn } from '@/lib/utils' -import { useWorkflowRegistry } from '@/stores/workflows/registry/store' - -interface InviteModalProps { - open: boolean - onOpenChange: (open: boolean) => void - onInviteMember?: (email: string) => void -} - -interface EmailTagProps { - email: string - onRemove: () => void - disabled?: boolean - isInvalid?: boolean -} - -const EmailTag = ({ email, onRemove, disabled, isInvalid }: EmailTagProps) => ( -
- {email} - {!disabled && ( - - )} -
-) - -const isValidEmail = (email: string): boolean => { - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ - return emailRegex.test(email) -} - -export function InviteModal({ open, onOpenChange }: InviteModalProps) { - const [inputValue, setInputValue] = useState('') - const [emails, setEmails] = useState([]) - const [invalidEmails, setInvalidEmails] = useState([]) - const [isSubmitting, setIsSubmitting] = useState(false) - const [showSent, setShowSent] = useState(false) - const [errorMessage, setErrorMessage] = useState(null) - const [successMessage, setSuccessMessage] = useState(null) - const { activeWorkspaceId } = useWorkflowRegistry() - - const addEmail = (email: string) => { - // Normalize by trimming and converting to lowercase - const normalizedEmail = email.trim().toLowerCase() - - if (!normalizedEmail) return false - - // Check for duplicates - if (emails.includes(normalizedEmail) || invalidEmails.includes(normalizedEmail)) { - return false - } - - // Validate email format - if (!isValidEmail(normalizedEmail)) { - setInvalidEmails([...invalidEmails, normalizedEmail]) - setInputValue('') - return false - } - - // Add to emails array - setEmails([...emails, normalizedEmail]) - setInputValue('') - return true - } - - const removeEmail = (index: number) => { - const newEmails = [...emails] - newEmails.splice(index, 1) - setEmails(newEmails) - } - - const removeInvalidEmail = (index: number) => { - const newInvalidEmails = [...invalidEmails] - newInvalidEmails.splice(index, 1) - setInvalidEmails(newInvalidEmails) - } - - const handleKeyDown = (e: KeyboardEvent) => { - // Add email on Enter, comma, or space - if (['Enter', ',', ' '].includes(e.key) && inputValue.trim()) { - e.preventDefault() - addEmail(inputValue) - } - - // Remove the last email on Backspace if input is empty - if (e.key === 'Backspace' && !inputValue) { - if (invalidEmails.length > 0) { - removeInvalidEmail(invalidEmails.length - 1) - } else if (emails.length > 0) { - removeEmail(emails.length - 1) - } - } - } - - const handlePaste = (e: React.ClipboardEvent) => { - e.preventDefault() - const pastedText = e.clipboardData.getData('text') - const pastedEmails = pastedText - .split(/[\s,;]+/) // Split by space, comma, or semicolon - .filter(Boolean) // Remove empty strings - - const validEmails = pastedEmails.filter((email) => { - return addEmail(email) - }) - - // If we didn't add any emails, keep the current input value - if (validEmails.length === 0 && pastedEmails.length === 1) { - setInputValue(inputValue + pastedEmails[0]) - } - } - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - - // Add current input as an email if it's valid - if (inputValue.trim()) { - addEmail(inputValue) - } - - // Clear any previous error or success messages - setErrorMessage(null) - setSuccessMessage(null) - - // Don't proceed if no emails or no workspace - if (emails.length === 0 || !activeWorkspaceId) { - return - } - - setIsSubmitting(true) - - try { - // Track failed invitations - const failedInvites: string[] = [] - - // Send invitations in parallel - const results = await Promise.all( - emails.map(async (email) => { - try { - const response = await fetch('/api/workspaces/invitations', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - workspaceId: activeWorkspaceId, - email: email, - role: 'member', // Default role for invited members - }), - }) - - const data = await response.json() - - if (!response.ok) { - // Don't add to invalid emails if it's already in the valid emails array - if (!invalidEmails.includes(email)) { - failedInvites.push(email) - } - - // Display the error message from the API if it exists - if (data.error) { - setErrorMessage(data.error) - } - - return false - } - - return true - } catch (_err) { - // Don't add to invalid emails if it's already in the valid emails array - if (!invalidEmails.includes(email)) { - failedInvites.push(email) - } - return false - } - }) - ) - - const successCount = results.filter(Boolean).length - - if (successCount > 0) { - // Clear everything on success, but keep track of failed emails - setInputValue('') - - // Only keep emails that failed in the emails array - if (failedInvites.length > 0) { - setEmails(failedInvites) - } else { - setEmails([]) - // Set success message when all invitations are successful - setSuccessMessage( - successCount === 1 - ? 'Invitation sent successfully!' - : `${successCount} invitations sent successfully!` - ) - } - - setInvalidEmails([]) - setShowSent(true) - - // Revert button text after 2 seconds - setTimeout(() => { - setShowSent(false) - }, 4000) - } - } catch (err: any) { - console.error('Error inviting members:', err) - setErrorMessage('An unexpected error occurred. Please try again.') - } finally { - setIsSubmitting(false) - } - } - - const resetState = () => { - setInputValue('') - setEmails([]) - setInvalidEmails([]) - setShowSent(false) - setErrorMessage(null) - setSuccessMessage(null) - } - - return ( - { - if (!newOpen) { - resetState() - } - onOpenChange(newOpen) - }} - > - - -
- Invite Members to Workspace - -
-
- -
-
-
-
- -
- {invalidEmails.map((email, index) => ( - removeInvalidEmail(index)} - disabled={isSubmitting} - isInvalid={true} - /> - ))} - {emails.map((email, index) => ( - removeEmail(index)} - disabled={isSubmitting} - /> - ))} - setInputValue(e.target.value)} - onKeyDown={handleKeyDown} - onPaste={handlePaste} - onBlur={() => inputValue.trim() && addEmail(inputValue)} - placeholder={ - emails.length > 0 || invalidEmails.length > 0 - ? 'Add another email' - : 'Enter email addresses (comma or Enter to separate)' - } - className={cn( - 'h-7 min-w-[180px] flex-1 border-none py-1 focus-visible:ring-0 focus-visible:ring-offset-0', - emails.length > 0 || invalidEmails.length > 0 ? 'pl-1' : 'pl-0' - )} - autoFocus - disabled={isSubmitting} - /> -
-

- {errorMessage || - successMessage || - 'Press Enter, comma, or space after each email.'} -

-
- -
- -
-
-
-
-
-
- ) -} diff --git a/apps/sim/app/w/components/sidebar/components/invite-modal/invites-sent/invites-sent.tsx b/apps/sim/app/w/components/sidebar/components/invite-modal/invites-sent/invites-sent.tsx deleted file mode 100644 index 8c7fb94b215..00000000000 --- a/apps/sim/app/w/components/sidebar/components/invite-modal/invites-sent/invites-sent.tsx +++ /dev/null @@ -1,120 +0,0 @@ -'use client' - -import { useEffect, useState } from 'react' -import { Skeleton } from '@/components/ui/skeleton' -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@/components/ui/table' -import { cn } from '@/lib/utils' -import { useWorkflowRegistry } from '@/stores/workflows/registry/store' - -type Invitation = { - id: string - email: string - status: 'pending' | 'accepted' | 'rejected' | 'expired' - createdAt: string -} - -export function InvitesSent() { - const [invitations, setInvitations] = useState([]) - const [isLoading, setIsLoading] = useState(true) - const [error, setError] = useState(null) - const { activeWorkspaceId } = useWorkflowRegistry() - - useEffect(() => { - async function fetchInvitations() { - if (!activeWorkspaceId) return - - setIsLoading(true) - setError(null) - - try { - const response = await fetch('/api/workspaces/invitations') - - if (!response.ok) { - throw new Error('Failed to fetch invitations') - } - - const data = await response.json() - setInvitations(data.invitations || []) - } catch (err) { - console.error('Error fetching invitations:', err) - setError('Failed to load invitations') - } finally { - setIsLoading(false) - } - } - - fetchInvitations() - }, [activeWorkspaceId]) - - const TableSkeleton = () => ( -
- {Array(5) - .fill(0) - .map((_, i) => ( -
- - -
- ))} -
- ) - - if (error) { - return
{error}
- } - - return ( -
-

Sent Invitations

- - {isLoading ? ( - - ) : invitations.length === 0 ? ( -
No invitations sent yet
- ) : ( -
- - - - - Email - - - Status - - - - - {invitations.map((invitation) => ( - - {invitation.email} - - - {invitation.status.charAt(0).toUpperCase() + invitation.status.slice(1)} - - - - ))} - -
-
- )} -
- ) -} diff --git a/apps/sim/app/w/components/sidebar/components/workspace-header/workspace-header.tsx b/apps/sim/app/w/components/sidebar/components/workspace-header/workspace-header.tsx deleted file mode 100644 index 838b8d58f36..00000000000 --- a/apps/sim/app/w/components/sidebar/components/workspace-header/workspace-header.tsx +++ /dev/null @@ -1,681 +0,0 @@ -'use client' - -import { useEffect, useState } from 'react' -import { ChevronDown, Pencil, Plus, Trash2, X } from 'lucide-react' -import Link from 'next/link' -import { useRouter } from 'next/navigation' -import { AgentIcon } from '@/components/icons' -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from '@/components/ui/alert-dialog' -import { Button } from '@/components/ui/button' -import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu' -import { Input } from '@/components/ui/input' -import { Skeleton } from '@/components/ui/skeleton' -import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' -import { useSession } from '@/lib/auth-client' -import { cn } from '@/lib/utils' -import { useSidebarStore } from '@/stores/sidebar/store' -import { useWorkflowRegistry } from '@/stores/workflows/registry/store' - -interface Workspace { - id: string - name: string - ownerId: string - role?: string -} - -interface WorkspaceHeaderProps { - onCreateWorkflow: () => void - isCollapsed?: boolean - onDropdownOpenChange?: (isOpen: boolean) => void -} - -// New WorkspaceModal component -interface WorkspaceModalProps { - open: boolean - onOpenChange: (open: boolean) => void - onCreateWorkspace: (name: string) => void -} - -function WorkspaceModal({ open, onOpenChange, onCreateWorkspace }: WorkspaceModalProps) { - const [workspaceName, setWorkspaceName] = useState('') - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault() - if (workspaceName.trim()) { - onCreateWorkspace(workspaceName.trim()) - setWorkspaceName('') - onOpenChange(false) - } - } - - return ( - - - -
- Create New Workspace - -
-
- -
-
-
-
- - setWorkspaceName(e.target.value)} - placeholder='Enter workspace name' - className='w-full' - autoFocus - /> -
-
- -
-
-
-
-
-
- ) -} - -// New WorkspaceEditModal component -interface WorkspaceEditModalProps { - open: boolean - onOpenChange: (open: boolean) => void - onUpdateWorkspace: (id: string, name: string) => void - workspace: Workspace | null -} - -function WorkspaceEditModal({ - open, - onOpenChange, - onUpdateWorkspace, - workspace, -}: WorkspaceEditModalProps) { - const [workspaceName, setWorkspaceName] = useState('') - - useEffect(() => { - if (workspace && open) { - setWorkspaceName(workspace.name) - } - }, [workspace, open]) - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault() - if (workspace && workspaceName.trim()) { - onUpdateWorkspace(workspace.id, workspaceName.trim()) - setWorkspaceName('') - onOpenChange(false) - } - } - - return ( - - - -
- Edit Workspace - -
-
- -
-
-
-
- - setWorkspaceName(e.target.value)} - placeholder='Enter workspace name' - className='w-full' - autoFocus - /> -
-
- -
-
-
-
-
-
- ) -} - -export function WorkspaceHeader({ - onCreateWorkflow, - isCollapsed, - onDropdownOpenChange, -}: WorkspaceHeaderProps) { - // Get sidebar store state to check current mode - const { mode, workspaceDropdownOpen, setAnyModalOpen } = useSidebarStore() - - // Keep local isOpen state in sync with the store (for internal component use) - const [isOpen, setIsOpen] = useState(workspaceDropdownOpen) - const { data: sessionData, isPending } = useSession() - const [plan, setPlan] = useState('Free Plan') - // Use client-side loading instead of isPending to avoid hydration mismatch - const [isClientLoading, setIsClientLoading] = useState(true) - const [workspaces, setWorkspaces] = useState([]) - const [activeWorkspace, setActiveWorkspace] = useState(null) - const [isWorkspacesLoading, setIsWorkspacesLoading] = useState(true) - const [isWorkspaceModalOpen, setIsWorkspaceModalOpen] = useState(false) - const [editingWorkspace, setEditingWorkspace] = useState(null) - const [isEditModalOpen, setIsEditModalOpen] = useState(false) - const [isDeleting, setIsDeleting] = useState(false) - const router = useRouter() - - // Get workflowRegistry state and actions - const { activeWorkspaceId, setActiveWorkspace: setActiveWorkspaceId } = useWorkflowRegistry() - - const userName = sessionData?.user?.name || sessionData?.user?.email || 'User' - - // Set isClientLoading to false after hydration - useEffect(() => { - setIsClientLoading(false) - }, []) - - useEffect(() => { - // Fetch subscription status if user is logged in - if (sessionData?.user?.id) { - fetch('/api/user/subscription') - .then((res) => res.json()) - .then((data) => { - setPlan(data.isPro ? 'Pro Plan' : 'Free Plan') - }) - .catch((err) => { - console.error('Error fetching subscription status:', err) - }) - - // Fetch user's workspaces - setIsWorkspacesLoading(true) - fetch('/api/workspaces') - .then((res) => res.json()) - .then((data) => { - if (data.workspaces && Array.isArray(data.workspaces)) { - const fetchedWorkspaces = data.workspaces as Workspace[] - setWorkspaces(fetchedWorkspaces) - - // Find workspace that matches the active ID from registry or use first workspace - const matchingWorkspace = fetchedWorkspaces.find( - (workspace) => workspace.id === activeWorkspaceId - ) - const workspaceToActivate = matchingWorkspace || fetchedWorkspaces[0] - - // If we found a workspace, set it as active and update registry if needed - if (workspaceToActivate) { - setActiveWorkspace(workspaceToActivate) - - // If active workspace in UI doesn't match registry, update registry - if (workspaceToActivate.id !== activeWorkspaceId) { - setActiveWorkspaceId(workspaceToActivate.id) - } - } - } - setIsWorkspacesLoading(false) - }) - .catch((err) => { - console.error('Error fetching workspaces:', err) - setIsWorkspacesLoading(false) - }) - } - }, [sessionData?.user?.id, activeWorkspaceId, setActiveWorkspaceId]) - - const switchWorkspace = (workspace: Workspace) => { - // If already on this workspace, do nothing - if (activeWorkspace?.id === workspace.id) { - setIsOpen(false) - return - } - - setActiveWorkspace(workspace) - setIsOpen(false) - - // Update the workflow registry store with the new active workspace - setActiveWorkspaceId(workspace.id) - - // Update URL to include workspace ID - router.push(`/w/${workspace.id}`) - } - - const handleCreateWorkspace = (name: string) => { - setIsWorkspacesLoading(true) - - fetch('/api/workspaces', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ name }), - }) - .then((res) => res.json()) - .then((data) => { - if (data.workspace) { - const newWorkspace = data.workspace as Workspace - setWorkspaces((prev) => [...prev, newWorkspace]) - setActiveWorkspace(newWorkspace) - - // Update the workflow registry store with the new active workspace - setActiveWorkspaceId(newWorkspace.id) - - // Update URL to include new workspace ID - router.push(`/w/${newWorkspace.id}`) - } - setIsWorkspacesLoading(false) - }) - .catch((err) => { - console.error('Error creating workspace:', err) - setIsWorkspacesLoading(false) - }) - } - - const handleUpdateWorkspace = async (id: string, name: string) => { - // Check if user has permission to update the workspace - const workspace = workspaces.find((w) => w.id === id) - if (!workspace || workspace.role !== 'owner') { - console.error('Permission denied: Only workspace owners can update workspaces') - return - } - - setIsWorkspacesLoading(true) - - try { - const response = await fetch(`/api/workspaces/${id}`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ name }), - }) - - if (!response.ok) { - throw new Error('Failed to update workspace') - } - - const { workspace } = await response.json() - - // Update workspaces list - setWorkspaces((prevWorkspaces) => - prevWorkspaces.map((w) => (w.id === workspace.id ? { ...w, name: workspace.name } : w)) - ) - - // If active workspace was updated, update it too - if (activeWorkspace?.id === workspace.id) { - setActiveWorkspace({ ...activeWorkspace, name: workspace.name } as Workspace) - } - } catch (err) { - console.error('Error updating workspace:', err) - } finally { - setIsWorkspacesLoading(false) - } - } - - const handleDeleteWorkspace = async (id: string) => { - // Check if user has permission to delete the workspace - const workspace = workspaces.find((w) => w.id === id) - if (!workspace || workspace.role !== 'owner') { - console.error('Permission denied: Only workspace owners can delete workspaces') - return - } - - setIsDeleting(true) - - try { - const response = await fetch(`/api/workspaces/${id}`, { - method: 'DELETE', - }) - - if (!response.ok) { - throw new Error('Failed to delete workspace') - } - - // Remove from workspace list - const updatedWorkspaces = workspaces.filter((w) => w.id !== id) - setWorkspaces(updatedWorkspaces) - - // If deleted workspace was active, switch to another workspace - if (activeWorkspace?.id === id && updatedWorkspaces.length > 0) { - // Use the specialized method for handling workspace deletion - const newWorkspaceId = updatedWorkspaces[0].id - useWorkflowRegistry.getState().handleWorkspaceDeletion(newWorkspaceId) - setActiveWorkspace(updatedWorkspaces[0]) - } - - setIsOpen(false) - } catch (err) { - console.error('Error deleting workspace:', err) - } finally { - setIsDeleting(false) - } - } - - const openEditModal = (workspace: Workspace, e: React.MouseEvent) => { - e.stopPropagation() - // Check if user has permission to edit the workspace - if (workspace.role !== 'owner') { - console.error('Permission denied: Only workspace owners can edit workspaces') - return - } - setEditingWorkspace(workspace) - setIsEditModalOpen(true) - } - - // Determine URL for workspace links - const workspaceUrl = activeWorkspace ? `/w/${activeWorkspace.id}` : '/w' - - // Notify parent component when dropdown opens/closes - const handleDropdownOpenChange = (open: boolean) => { - setIsOpen(open) - // Inform the parent component about the dropdown state change - if (onDropdownOpenChange) { - onDropdownOpenChange(open) - } - } - - // Special handling for click interactions in hover mode - const handleTriggerClick = (e: React.MouseEvent) => { - // When in hover mode, explicitly prevent bubbling for the trigger - if (mode === 'hover') { - e.stopPropagation() - e.preventDefault() - // Toggle dropdown state - handleDropdownOpenChange(!isOpen) - } - } - - // Handle modal open/close state - useEffect(() => { - // Update the modal state in the store - setAnyModalOpen(isWorkspaceModalOpen || isEditModalOpen || isDeleting) - }, [isWorkspaceModalOpen, isEditModalOpen, isDeleting, setAnyModalOpen]) - - return ( -
- {/* Workspace Modal */} - - - {/* Edit Workspace Modal */} - - - -
{ - // In hover mode, prevent clicks on the container from collapsing the sidebar - if (mode === 'hover') { - e.stopPropagation() - } - }} - > - {/* Hover background with consistent padding - only when not collapsed */} - {!isCollapsed &&
} - - {/* Content with consistent padding */} - {isCollapsed ? ( -
- - - -
- ) : ( -
- -
-
- { - if (isOpen) e.preventDefault() - }} - > - - - {isClientLoading || isWorkspacesLoading ? ( - - ) : ( -
- - {activeWorkspace?.name || `${userName}'s Workspace`} - - -
- )} -
-
-
- - {/* Plus button positioned absolutely */} - {!isCollapsed && ( -
- - -
- {isClientLoading ? ( - - ) : ( - - )} -
-
- New Workflow -
-
- )} -
- )} -
- -
-
-
-
- -
-
- {isClientLoading || isWorkspacesLoading ? ( - <> - - - - ) : ( - <> - - {activeWorkspace?.name || `${userName}'s Workspace`} - - {plan} - - )} -
-
-
-
- - - - {/* Workspaces list */} -
-
Workspaces
- {isWorkspacesLoading ? ( -
- -
- ) : ( -
- {workspaces.map((workspace) => ( - switchWorkspace(workspace)} - > - {workspace.name} - {workspace.role === 'owner' && ( -
- - - - - - - - - Delete Workspace - - Are you sure you want to delete "{workspace.name}"? This action - cannot be undone. - - - - e.stopPropagation()}> - Cancel - - { - e.stopPropagation() - handleDeleteWorkspace(workspace.id) - }} - className='bg-destructive text-destructive-foreground hover:bg-destructive/90' - > - Delete - - - - -
- )} -
- ))} -
- )} - - {/* Create new workspace button */} - setIsWorkspaceModalOpen(true)} - > - + New workspace - -
-
- -
- ) -} diff --git a/apps/sim/app/w/error.tsx b/apps/sim/app/w/error.tsx deleted file mode 100644 index adac0456b81..00000000000 --- a/apps/sim/app/w/error.tsx +++ /dev/null @@ -1,5 +0,0 @@ -'use client' - -import { NextError } from './[id]/components/error' - -export default NextError diff --git a/apps/sim/app/w/global-error.tsx b/apps/sim/app/w/global-error.tsx deleted file mode 100644 index 9c7bd975759..00000000000 --- a/apps/sim/app/w/global-error.tsx +++ /dev/null @@ -1,5 +0,0 @@ -'use client' - -import { NextGlobalError } from './[id]/components/error' - -export default NextGlobalError diff --git a/apps/sim/app/w/hooks/use-registry-loading.ts b/apps/sim/app/w/hooks/use-registry-loading.ts deleted file mode 100644 index 0ca7ba5134a..00000000000 --- a/apps/sim/app/w/hooks/use-registry-loading.ts +++ /dev/null @@ -1,44 +0,0 @@ -'use client' - -import { useEffect } from 'react' -import { useWorkflowRegistry } from '@/stores/workflows/registry/store' - -/** - * Custom hook to manage workflow registry loading state - * - * This hook initializes the loading state and automatically clears it - * when workflows are loaded or after a timeout - */ -export function useRegistryLoading() { - const { workflows, setLoading } = useWorkflowRegistry() - - useEffect(() => { - // Set loading state initially - setLoading(true) - - // If workflows are already loaded, clear loading state - if (Object.keys(workflows).length > 0) { - setTimeout(() => setLoading(false), 300) - return - } - - // Create a timeout to clear loading state after max time - const timeout = setTimeout(() => { - setLoading(false) - }, 3000) // 3 second maximum loading time - - // Listen for workflows to be loaded - const checkInterval = setInterval(() => { - const currentWorkflows = useWorkflowRegistry.getState().workflows - if (Object.keys(currentWorkflows).length > 0) { - setLoading(false) - clearInterval(checkInterval) - } - }, 200) - - return () => { - clearTimeout(timeout) - clearInterval(checkInterval) - } - }, [setLoading, workflows]) -} diff --git a/apps/sim/app/w/knowledge/components/create-modal/components/create-form/create-form.tsx b/apps/sim/app/w/knowledge/components/create-modal/components/create-form/create-form.tsx deleted file mode 100644 index 0cd1c9bd86d..00000000000 --- a/apps/sim/app/w/knowledge/components/create-modal/components/create-form/create-form.tsx +++ /dev/null @@ -1,570 +0,0 @@ -'use client' - -import { useEffect, useRef, useState } from 'react' -import { zodResolver } from '@hookform/resolvers/zod' -import { AlertCircle, CheckCircle2, X } from 'lucide-react' -import { useForm } from 'react-hook-form' -import { z } from 'zod' -import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' -import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' -import { Textarea } from '@/components/ui/textarea' -import { createLogger } from '@/lib/logs/console-logger' -import { getDocumentIcon } from '@/app/w/knowledge/components/icons/document-icons' -import type { DocumentData, KnowledgeBaseData } from '@/stores/knowledge/store' -import { useKnowledgeStore } from '@/stores/knowledge/store' - -const logger = createLogger('CreateForm') - -const MAX_FILE_SIZE = 50 * 1024 * 1024 // 50MB -const ACCEPTED_FILE_TYPES = [ - 'application/pdf', - 'application/msword', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'text/plain', - 'text/csv', - 'application/vnd.ms-excel', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', -] - -interface ProcessedDocumentResponse { - documentId: string - filename: string - status: string -} - -interface FileWithPreview extends File { - preview: string -} - -interface CreateFormProps { - onClose: () => void - onKnowledgeBaseCreated?: (knowledgeBase: KnowledgeBaseData) => void -} - -const FormSchema = z.object({ - name: z - .string() - .min(1, 'Name is required') - .max(100, 'Name must be less than 100 characters') - .refine((value) => value.trim().length > 0, 'Name cannot be empty'), - description: z.string().max(500, 'Description must be less than 500 characters').optional(), -}) - -type FormValues = z.infer - -interface SubmitStatus { - type: 'success' | 'error' - message: string -} - -export function CreateForm({ onClose, onKnowledgeBaseCreated }: CreateFormProps) { - const fileInputRef = useRef(null) - const [isSubmitting, setIsSubmitting] = useState(false) - const [submitStatus, setSubmitStatus] = useState(null) - const [files, setFiles] = useState([]) - const [fileError, setFileError] = useState(null) - const [isDragging, setIsDragging] = useState(false) - const [dragCounter, setDragCounter] = useState(0) // Track drag events to handle nested elements - const scrollContainerRef = useRef(null) - const dropZoneRef = useRef(null) - - // Cleanup file preview URLs when component unmounts to prevent memory leaks - useEffect(() => { - return () => { - files.forEach((file) => { - if (file.preview) { - URL.revokeObjectURL(file.preview) - } - }) - } - }, [files]) - - const { - register, - handleSubmit, - reset, - formState: { errors }, - } = useForm({ - resolver: zodResolver(FormSchema), - defaultValues: { - name: '', - description: '', - }, - mode: 'onChange', - }) - - const processFiles = async (fileList: FileList | File[]) => { - setFileError(null) - - if (!fileList || fileList.length === 0) return - - try { - const newFiles: FileWithPreview[] = [] - let hasError = false - - for (const file of Array.from(fileList)) { - // Check file size - if (file.size > MAX_FILE_SIZE) { - setFileError(`File ${file.name} is too large. Maximum size is 50MB.`) - hasError = true - continue - } - - // Check file type - if (!ACCEPTED_FILE_TYPES.includes(file.type)) { - setFileError( - `File ${file.name} has an unsupported format. Please use PDF, DOC, DOCX, TXT, CSV, XLS, or XLSX.` - ) - hasError = true - continue - } - - // Create file with preview (using file icon since these aren't images) - const fileWithPreview = Object.assign(file, { - preview: URL.createObjectURL(file), - }) as FileWithPreview - - newFiles.push(fileWithPreview) - } - - if (!hasError && newFiles.length > 0) { - setFiles((prev) => [...prev, ...newFiles]) - } - } catch (error) { - logger.error('Error processing files:', error) - setFileError('An error occurred while processing files. Please try again.') - } finally { - // Reset the input - if (fileInputRef.current) { - fileInputRef.current.value = '' - } - } - } - - const handleFileChange = async (e: React.ChangeEvent) => { - if (e.target.files) { - await processFiles(e.target.files) - } - } - - // Handle drag events - const handleDragEnter = (e: React.DragEvent) => { - e.preventDefault() - e.stopPropagation() - setDragCounter((prev) => { - const newCount = prev + 1 - if (newCount === 1) { - setIsDragging(true) - } - return newCount - }) - } - - const handleDragLeave = (e: React.DragEvent) => { - e.preventDefault() - e.stopPropagation() - setDragCounter((prev) => { - const newCount = prev - 1 - if (newCount === 0) { - setIsDragging(false) - } - return newCount - }) - } - - const handleDragOver = (e: React.DragEvent) => { - e.preventDefault() - e.stopPropagation() - // Add visual feedback for valid drop zone - e.dataTransfer.dropEffect = 'copy' - } - - const handleDrop = async (e: React.DragEvent) => { - e.preventDefault() - e.stopPropagation() - setIsDragging(false) - setDragCounter(0) - - if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { - await processFiles(e.dataTransfer.files) - } - } - - const removeFile = (index: number) => { - setFiles((prev) => { - // Revoke the URL to avoid memory leaks - URL.revokeObjectURL(prev[index].preview) - return prev.filter((_, i) => i !== index) - }) - } - - const getFileIcon = (mimeType: string, filename: string) => { - const IconComponent = getDocumentIcon(mimeType, filename) - return - } - - const formatFileSize = (bytes: number): string => { - if (bytes === 0) return '0 B' - const k = 1024 - const sizes = ['B', 'KB', 'MB', 'GB'] - const i = Math.floor(Math.log(bytes) / Math.log(k)) - return `${Number.parseFloat((bytes / k ** i).toFixed(1))} ${sizes[i]}` - } - - const onSubmit = async (data: FormValues) => { - setIsSubmitting(true) - setSubmitStatus(null) - - try { - // First create the knowledge base - const knowledgeBasePayload = { - name: data.name, - description: data.description || undefined, - } - - const response = await fetch('/api/knowledge', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(knowledgeBasePayload), - }) - - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.error || 'Failed to create knowledge base') - } - - const result = await response.json() - - if (!result.success) { - throw new Error(result.error || 'Failed to create knowledge base') - } - - const newKnowledgeBase = result.data - - // If files are uploaded, upload them and start processing - if (files.length > 0) { - // First, upload all files to get their URLs - interface UploadedFile { - filename: string - fileUrl: string - fileSize: number - mimeType: string - fileHash: string | undefined - } - - const uploadedFiles: UploadedFile[] = [] - - for (const file of files) { - const formData = new FormData() - formData.append('file', file) - - const uploadResponse = await fetch('/api/files/upload', { - method: 'POST', - body: formData, - }) - - if (!uploadResponse.ok) { - const errorData = await uploadResponse.json() - throw new Error(`Failed to upload ${file.name}: ${errorData.error || 'Unknown error'}`) - } - - const uploadResult = await uploadResponse.json() - uploadedFiles.push({ - filename: file.name, - fileUrl: uploadResult.path.startsWith('http') - ? uploadResult.path - : `${window.location.origin}${uploadResult.path}`, - fileSize: file.size, - mimeType: file.type, - fileHash: undefined, - }) - } - - // Start async document processing - const processResponse = await fetch( - `/api/knowledge/${newKnowledgeBase.id}/process-documents`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - documents: uploadedFiles, - processingOptions: { - chunkSize: 1024, - minCharactersPerChunk: 24, - recipe: 'default', - lang: 'en', - }, - }), - } - ) - - if (!processResponse.ok) { - throw new Error('Failed to start document processing') - } - - const processResult = await processResponse.json() - - // Create pending document objects and add them to the store immediately - if (processResult.success && processResult.data.documentsCreated) { - const pendingDocuments: DocumentData[] = processResult.data.documentsCreated.map( - (doc: ProcessedDocumentResponse, index: number) => ({ - id: doc.documentId, - knowledgeBaseId: newKnowledgeBase.id, - filename: doc.filename, - fileUrl: uploadedFiles[index].fileUrl, - fileSize: uploadedFiles[index].fileSize, - mimeType: uploadedFiles[index].mimeType, - fileHash: uploadedFiles[index].fileHash || null, - chunkCount: 0, - tokenCount: 0, - characterCount: 0, - processingStatus: 'pending' as const, - processingStartedAt: null, - processingCompletedAt: null, - processingError: null, - enabled: true, - uploadedAt: new Date().toISOString(), - }) - ) - - // Add pending documents to store for immediate UI update - useKnowledgeStore.getState().addPendingDocuments(newKnowledgeBase.id, pendingDocuments) - } - - // Update the knowledge base object with the correct document count - newKnowledgeBase.docCount = uploadedFiles.length - - logger.info(`Started processing ${uploadedFiles.length} documents in the background`) - } - - setSubmitStatus({ - type: 'success', - message: 'Your knowledge base has been created successfully!', - }) - reset() - - // Clean up file previews - files.forEach((file) => URL.revokeObjectURL(file.preview)) - setFiles([]) - - // Call the callback if provided - if (onKnowledgeBaseCreated) { - onKnowledgeBaseCreated(newKnowledgeBase) - } - - // Close modal after a short delay to show success message - setTimeout(() => { - onClose() - }, 1500) - } catch (error) { - logger.error('Error creating knowledge base:', error) - setSubmitStatus({ - type: 'error', - message: error instanceof Error ? error.message : 'An unknown error occurred', - }) - } finally { - setIsSubmitting(false) - } - } - - return ( -
- {/* Scrollable Content */} -
-
- {submitStatus && submitStatus.type === 'success' ? ( - -
-
- -
-
- - Success - - - {submitStatus.message} - -
-
-
- ) : submitStatus && submitStatus.type === 'error' ? ( - - - Error - {submitStatus.message} - - ) : null} - -
-
- - - {errors.name &&

{errors.name.message}

} -
- -
- -