mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-09 23:17:59 -05:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7192cdef6f | ||
|
|
8a9bc4e929 | ||
|
|
d65bdaf546 | ||
|
|
348b524d86 | ||
|
|
0361397879 | ||
|
|
ff2b1d33c8 | ||
|
|
db22e26662 | ||
|
|
f8000a747a | ||
|
|
9a954d1830 | ||
|
|
f16d759d8d | ||
|
|
dad72e3100 | ||
|
|
db4ad80a4c | ||
|
|
0023e8df80 | ||
|
|
2f726fa9f3 | ||
|
|
4d3dee7f0f | ||
|
|
7860894007 | ||
|
|
52ffc39194 | ||
|
|
f666ccad43 | ||
|
|
95dfe9e6d2 | ||
|
|
91a4c6d588 | ||
|
|
5f9bfdde06 |
74
.github/CONTRIBUTING.md
vendored
74
.github/CONTRIBUTING.md
vendored
@@ -15,8 +15,6 @@ Thank you for your interest in contributing to Sim Studio! Our goal is to provid
|
||||
- [Commit Message Guidelines](#commit-message-guidelines)
|
||||
- [Local Development Setup](#local-development-setup)
|
||||
- [Adding New Blocks and Tools](#adding-new-blocks-and-tools)
|
||||
- [Local Storage Mode](#local-storage-mode)
|
||||
- [Standalone Build](#standalone-build)
|
||||
- [License](#license)
|
||||
- [Contributor License Agreement (CLA)](#contributor-license-agreement-cla)
|
||||
|
||||
@@ -57,7 +55,7 @@ We strive to keep our workflow as simple as possible. To contribute:
|
||||
```
|
||||
|
||||
7. **Create a Pull Request**
|
||||
Open a pull request against the `main` branch on GitHub. Please provide a clear description of the changes and reference any relevant issues (e.g., `fixes #123`).
|
||||
Open a pull request against the `staging` branch on GitHub. Please provide a clear description of the changes and reference any relevant issues (e.g., `fixes #123`).
|
||||
|
||||
---
|
||||
|
||||
@@ -85,7 +83,7 @@ If you discover a bug or have a feature request, please open an issue in our Git
|
||||
Before creating a pull request:
|
||||
|
||||
- **Ensure Your Branch Is Up-to-Date:**
|
||||
Rebase your branch onto the latest `main` branch to prevent merge conflicts.
|
||||
Rebase your branch onto the latest `staging` branch to prevent merge conflicts.
|
||||
- **Follow the Guidelines:**
|
||||
Make sure your changes are well-tested, follow our coding standards, and include relevant documentation if necessary.
|
||||
|
||||
@@ -209,13 +207,14 @@ Dev Containers provide a consistent and easy-to-use development environment:
|
||||
|
||||
3. **Start Developing:**
|
||||
|
||||
- Run `bun run dev` in the terminal or use the `sim-start` alias
|
||||
- Run `bun run dev:full` in the terminal or use the `sim-start` alias
|
||||
- This starts both the main application and the realtime socket server
|
||||
- All dependencies and configurations are automatically set up
|
||||
- Your changes will be automatically hot-reloaded
|
||||
|
||||
4. **GitHub Codespaces:**
|
||||
- This setup also works with GitHub Codespaces if you prefer development in the browser
|
||||
- Just click "Code" → "Codespaces" → "Create codespace on main"
|
||||
- Just click "Code" → "Codespaces" → "Create codespace on staging"
|
||||
|
||||
### Option 4: Manual Setup
|
||||
|
||||
@@ -246,9 +245,11 @@ If you prefer not to use Docker or Dev Containers:
|
||||
4. **Run the Development Server:**
|
||||
|
||||
```bash
|
||||
bun run dev
|
||||
bun run dev:full
|
||||
```
|
||||
|
||||
This command starts both the main application and the realtime socket server required for full functionality.
|
||||
|
||||
5. **Make Your Changes and Test Locally.**
|
||||
|
||||
### Email Template Development
|
||||
@@ -379,7 +380,18 @@ In addition, you will need to update the registries:
|
||||
provider: 'pinecone', // ID of the OAuth provider
|
||||
|
||||
params: {
|
||||
// Tool parameters
|
||||
parameterName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm', // Controls parameter visibility
|
||||
description: 'Description of the parameter',
|
||||
},
|
||||
optionalParam: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Optional parameter only user can set',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
// Request configuration
|
||||
@@ -429,11 +441,57 @@ Maintaining consistent naming across the codebase is critical for auto-generatio
|
||||
- **Tool Exports:** Should be named `{toolName}Tool` (e.g., `fetchTool`)
|
||||
- **Tool IDs:** Should follow the format `{provider}_{tool_name}` (e.g., `pinecone_fetch`)
|
||||
|
||||
### Parameter Visibility System
|
||||
|
||||
Sim Studio implements a sophisticated parameter visibility system that controls how parameters are exposed to users and LLMs in agent workflows. Each parameter can have one of four visibility levels:
|
||||
|
||||
| Visibility | User Sees | LLM Sees | How It Gets Set |
|
||||
|-------------|-----------|----------|--------------------------------|
|
||||
| `user-only` | ✅ Yes | ❌ No | User provides in UI |
|
||||
| `user-or-llm` | ✅ Yes | ✅ Yes | User provides OR LLM generates |
|
||||
| `llm-only` | ❌ No | ✅ Yes | LLM generates only |
|
||||
| `hidden` | ❌ No | ❌ No | Application injects at runtime |
|
||||
|
||||
#### Visibility Guidelines
|
||||
|
||||
- **`user-or-llm`**: Use for core parameters that can be provided by users or intelligently filled by the LLM (e.g., search queries, email subjects)
|
||||
- **`user-only`**: Use for configuration parameters, API keys, and settings that only users should control (e.g., number of results, authentication credentials)
|
||||
- **`llm-only`**: Use for computed values that the LLM should handle internally (e.g., dynamic calculations, contextual data)
|
||||
- **`hidden`**: Use for system-level parameters injected at runtime (e.g., OAuth tokens, internal identifiers)
|
||||
|
||||
#### Example Implementation
|
||||
|
||||
```typescript
|
||||
params: {
|
||||
query: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm', // User can provide or LLM can generate
|
||||
description: 'Search query to execute',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only', // Only user provides this
|
||||
description: 'API key for authentication',
|
||||
},
|
||||
internalId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'hidden', // System provides this at runtime
|
||||
description: 'Internal tracking identifier',
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
This visibility system ensures clean user interfaces while maintaining full flexibility for LLM-driven workflows.
|
||||
|
||||
### Guidelines & Best Practices
|
||||
|
||||
- **Code Style:** Follow the project's ESLint and Prettier configurations. Use meaningful variable names and small, focused functions.
|
||||
- **Documentation:** Clearly document the purpose, inputs, outputs, and any special behavior for your block/tool.
|
||||
- **Error Handling:** Implement robust error handling and provide user-friendly error messages.
|
||||
- **Parameter Visibility:** Always specify the appropriate visibility level for each parameter to ensure proper UI behavior and LLM integration.
|
||||
- **Testing:** Add unit or integration tests to verify your changes when possible.
|
||||
- **Commit Changes:** Update all related components and registries, and describe your changes in your pull request.
|
||||
|
||||
|
||||
22
README.md
22
README.md
@@ -87,6 +87,7 @@ docker compose -f docker-compose.prod.yml up -d
|
||||
1. Open VS Code with the [Remote - Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
|
||||
2. Open the project and click "Reopen in Container" when prompted
|
||||
3. Run `bun run dev:full` in the terminal or use the `sim-start` alias
|
||||
- This starts both the main application and the realtime socket server
|
||||
|
||||
### Option 4: Manual Setup
|
||||
|
||||
@@ -113,24 +114,27 @@ bunx drizzle-kit push
|
||||
|
||||
4. Start the development servers:
|
||||
|
||||
Next.js app:
|
||||
**Recommended approach - run both servers together (from project root):**
|
||||
|
||||
```bash
|
||||
bun run dev:full
|
||||
```
|
||||
|
||||
This starts both the main Next.js application and the realtime socket server required for full functionality.
|
||||
|
||||
**Alternative - run servers separately:**
|
||||
|
||||
Next.js app (from project root):
|
||||
```bash
|
||||
bun run dev
|
||||
```
|
||||
|
||||
Start the realtime server:
|
||||
|
||||
Realtime socket server (from `apps/sim` directory in a separate terminal):
|
||||
```bash
|
||||
cd apps/sim
|
||||
bun run dev:sockets
|
||||
```
|
||||
|
||||
Run both together (recommended):
|
||||
|
||||
```bash
|
||||
bun run dev:full
|
||||
```
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Framework**: [Next.js](https://nextjs.org/) (App Router)
|
||||
|
||||
@@ -1,186 +0,0 @@
|
||||
---
|
||||
title: Autoblocks
|
||||
description: Manage and use versioned prompts with Autoblocks
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="autoblocks"
|
||||
color="#0D2929"
|
||||
icon={true}
|
||||
iconSvg={`<svg className="block-icon"
|
||||
|
||||
version='1.1'
|
||||
id='Layer_1'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
xmlnsXlink='http://www.w3.org/1999/xlink'
|
||||
x='0px'
|
||||
y='0px'
|
||||
|
||||
viewBox='0 0 1250 1250'
|
||||
enableBackground='new 0 0 1250 1250'
|
||||
xmlSpace='preserve'
|
||||
>
|
||||
<path
|
||||
fill='#FFFFFF'
|
||||
opacity='1.000000'
|
||||
stroke='none'
|
||||
d='
|
||||
M671.222290,1079.959839
|
||||
C671.176025,1077.962891 671.089233,1075.965820 671.089111,1073.968872
|
||||
C671.082825,918.318481 671.062683,762.668091 671.192322,607.017761
|
||||
C671.195862,602.748474 669.789551,600.693787 666.180847,598.638306
|
||||
C636.091125,581.500183 606.140991,564.117126 576.145508,546.813599
|
||||
C556.393311,535.419128 536.677856,523.960449 516.869568,512.664307
|
||||
C495.246002,500.332977 473.461487,488.282806 451.883911,475.872253
|
||||
C434.220825,465.713257 416.802856,455.129089 399.195587,444.871857
|
||||
C379.466736,433.378601 359.648438,422.038818 339.866608,410.636597
|
||||
C320.229004,399.317505 300.588470,388.003510 280.948822,376.688019
|
||||
C271.840149,371.440033 262.730530,366.193695 253.057938,360.622070
|
||||
C267.185272,352.478241 280.655273,344.713531 294.125092,336.948517
|
||||
C329.023163,316.830566 363.943237,296.750366 398.783295,276.532349
|
||||
C402.073059,274.623260 404.534790,274.139191 408.118988,276.252319
|
||||
C435.683502,292.503723 463.371948,308.546082 491.084290,324.545258
|
||||
C509.340118,335.084839 527.725525,345.399719 546.006958,355.895203
|
||||
C585.713440,378.690979 625.427124,401.474670 665.069397,424.381744
|
||||
C705.530884,447.762177 745.895203,471.310669 786.336243,494.726715
|
||||
C796.959717,500.877930 807.667236,506.888184 818.432190,512.787903
|
||||
C820.966064,514.176636 821.763611,515.816772 821.762329,518.659241
|
||||
C821.692932,676.145020 821.688171,833.630737 821.793762,991.116455
|
||||
C821.795837,994.184937 820.514771,995.521545 818.222412,996.837891
|
||||
C782.578491,1017.306641 746.954346,1037.809570 711.333679,1058.318848
|
||||
C698.839661,1065.512573 686.367554,1072.744629 673.219116,1079.994141
|
||||
C672.109314,1080.006104 671.665771,1079.982910 671.222290,1079.959839
|
||||
z'
|
||||
/>
|
||||
<path
|
||||
fill='#FFFFFF'
|
||||
opacity='1.000000'
|
||||
stroke='none'
|
||||
d='
|
||||
M684.421631,400.605865
|
||||
C600.749390,352.376038 517.388306,304.342010 433.717010,256.129181
|
||||
C455.858643,243.338989 477.724731,230.689346 499.608948,218.071136
|
||||
C526.744324,202.425217 553.916504,186.842911 581.002014,171.111252
|
||||
C583.487793,169.667450 585.282104,169.727783 587.700562,171.126724
|
||||
C627.018250,193.870560 666.389465,216.521790 705.739136,239.210449
|
||||
C744.537903,261.581543 783.343262,283.941437 822.113525,306.361786
|
||||
C854.544006,325.115936 886.886658,344.022156 919.345703,362.726379
|
||||
C945.337769,377.704102 971.415039,392.534851 997.539551,407.280151
|
||||
C1001.126465,409.304749 1002.459045,411.581146 1002.455444,415.839966
|
||||
C1002.322388,571.647339 1002.315430,727.454834 1002.468750,883.262207
|
||||
C1002.473694,888.329590 1001.184082,891.101135 996.646118,893.690186
|
||||
C949.437134,920.624695 902.383667,947.831665 855.284607,974.958862
|
||||
C854.453491,975.437500 853.591980,975.863708 851.884216,976.772095
|
||||
C851.884216,974.236023 851.884216,972.347290 851.884216,970.458557
|
||||
C851.884216,814.817688 851.876099,659.176880 851.927551,503.536011
|
||||
C851.928955,499.372650 851.416870,497.004883 846.802246,494.523651
|
||||
C829.014954,484.959839 811.879517,474.190002 794.417969,464.012421
|
||||
C774.549316,452.431854 754.597900,440.993225 734.670959,429.512817
|
||||
C718.033508,419.927551 701.379517,410.370911 684.421631,400.605865
|
||||
z'
|
||||
/>
|
||||
<path
|
||||
fill='#FFFFFF'
|
||||
opacity='1.000000'
|
||||
stroke='none'
|
||||
d='
|
||||
M398.927063,451.754761
|
||||
C400.510162,450.940521 401.764893,450.328430 403.700867,449.383972
|
||||
C403.700867,452.154175 403.700897,454.096252 403.700897,456.038330
|
||||
C403.700897,554.021851 403.720520,652.005371 403.628479,749.988831
|
||||
C403.624847,753.876892 404.584320,756.067810 408.236908,758.155518
|
||||
C451.188324,782.705505 493.996735,807.505737 536.834656,832.254150
|
||||
C575.355164,854.508362 613.866882,876.777893 652.379028,899.046387
|
||||
C658.236328,902.433167 664.075500,905.851257 670.506531,909.594543
|
||||
C660.506226,915.396240 650.958069,920.955383 641.391357,926.482483
|
||||
C602.367798,949.028442 563.293213,971.486938 524.376099,994.215210
|
||||
C520.155334,996.680237 517.203247,996.930176 512.863708,994.408752
|
||||
C454.421143,960.451721 395.851410,926.713562 337.314575,892.918823
|
||||
C319.777893,882.794556 302.245758,872.662292 284.710938,862.534790
|
||||
C274.721008,856.764954 264.759888,850.944214 254.717163,845.267761
|
||||
C252.338959,843.923462 251.216995,842.476929 251.219849,839.499817
|
||||
C251.315567,739.849976 251.312408,640.200073 251.234558,540.550232
|
||||
C251.232254,537.601685 252.346344,536.241150 254.806610,534.827820
|
||||
C302.775909,507.271362 350.680695,479.602600 398.927063,451.754761
|
||||
z'
|
||||
/>
|
||||
</svg>`}
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Autoblocks](https://www.autoblocks.ai/) is a comprehensive platform for managing, monitoring, and optimizing AI applications. It provides robust tools for prompt management that enable teams to collaborate effectively on AI prompts while maintaining version control and type safety.
|
||||
|
||||
With Autoblocks, you can:
|
||||
|
||||
- **Version and manage prompts**: Track changes, roll back to previous versions, and maintain a history of prompt iterations
|
||||
- **Collaborate across teams**: Enable product, engineering, and AI teams to work together on prompt development
|
||||
- **Ensure type safety**: Get autocomplete and validation for prompt variables
|
||||
- **Monitor prompt performance**: Track metrics and analyze how changes affect outcomes
|
||||
- **Test prompts**: Compare different versions and evaluate results before deployment
|
||||
|
||||
Autoblocks integrates seamlessly with your existing AI workflows in Sim Studio, providing a structured approach to prompt engineering that improves consistency and reduces errors.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Collaborate on prompts with type safety, autocomplete, and backwards-incompatibility protection. Autoblocks prompt management allows product teams to collaborate while maintaining excellent developer experience.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `autoblocks_prompt_manager`
|
||||
|
||||
Manage and render prompts using Autoblocks prompt management system
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `promptId` | string | Yes | The ID of the prompt to retrieve |
|
||||
| `version` | string | Yes | Version strategy \(latest or specific\) |
|
||||
| `specificVersion` | string | No | Specific version to use \(e.g., |
|
||||
| `templateParams` | object | No | Parameters to render the template with |
|
||||
| `apiKey` | string | Yes | Autoblocks API key |
|
||||
| `enableABTesting` | boolean | No | Whether to enable A/B testing between versions |
|
||||
| `abTestConfig` | object | No | Configuration for A/B testing between versions |
|
||||
| `environment` | string | Yes | Environment to use \(production, staging, development\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type |
|
||||
| --------- | ---- |
|
||||
| `promptId` | string |
|
||||
| `version` | string |
|
||||
| `renderedPrompt` | string |
|
||||
| `templates` | string |
|
||||
|
||||
|
||||
|
||||
## Block Configuration
|
||||
|
||||
### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `promptId` | string | Yes | Prompt ID - Enter the Autoblocks prompt ID |
|
||||
|
||||
|
||||
|
||||
### Outputs
|
||||
|
||||
| Output | Type | Description |
|
||||
| ------ | ---- | ----------- |
|
||||
| `promptId` | string | promptId output from the block |
|
||||
| `version` | string | version output from the block |
|
||||
| `renderedPrompt` | string | renderedPrompt output from the block |
|
||||
| `templates` | json | templates output from the block |
|
||||
|
||||
|
||||
## Notes
|
||||
|
||||
- Category: `tools`
|
||||
- Type: `autoblocks`
|
||||
@@ -214,7 +214,7 @@ Populate Clay with data from a JSON file. Enables direct communication and notif
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `webhookURL` | string | Yes | The webhook URL to populate |
|
||||
| `data` | json | Yes | The data to populate |
|
||||
| `authToken` | string | No | Optional auth token for WebhookURL |
|
||||
| `authToken` | string | Yes | Auth token for Clay webhook authentication |
|
||||
|
||||
#### Output
|
||||
|
||||
|
||||
@@ -53,10 +53,10 @@ Convert TTS using ElevenLabs voices
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Your ElevenLabs API key |
|
||||
| `text` | string | Yes | The text to convert to speech |
|
||||
| `voiceId` | string | Yes | The ID of the voice to use |
|
||||
| `modelId` | string | No | The ID of the model to use \(defaults to eleven_monolingual_v1\) |
|
||||
| `apiKey` | string | Yes | Your ElevenLabs API key |
|
||||
|
||||
#### Output
|
||||
|
||||
|
||||
@@ -65,9 +65,9 @@ Extract structured content from web pages with comprehensive metadata support. C
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Firecrawl API key |
|
||||
| `url` | string | Yes | The URL to scrape content from |
|
||||
| `scrapeOptions` | json | No | Options for content scraping |
|
||||
| `apiKey` | string | Yes | Firecrawl API key |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -85,8 +85,8 @@ Search for information on the web using Firecrawl
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Firecrawl API key |
|
||||
| `query` | string | Yes | The search query to use |
|
||||
| `apiKey` | string | Yes | Firecrawl API key |
|
||||
|
||||
#### Output
|
||||
|
||||
|
||||
@@ -85,15 +85,15 @@ Create comments on GitHub PRs
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `owner` | string | Yes | Repository owner |
|
||||
| `repo` | string | Yes | Repository name |
|
||||
| `pullNumber` | number | Yes | Pull request number |
|
||||
| `body` | string | Yes | Comment content |
|
||||
| `pullNumber` | number | Yes | Pull request number |
|
||||
| `path` | string | No | File path for review comment |
|
||||
| `position` | number | No | Line number for review comment |
|
||||
| `apiKey` | string | Yes | GitHub API token |
|
||||
| `commentType` | string | No | Type of comment \(pr_comment or file_comment\) |
|
||||
| `line` | number | No | Line number for review comment |
|
||||
| `side` | string | No | Side of the diff \(LEFT or RIGHT\) |
|
||||
| `commitId` | string | No | The SHA of the commit to comment on |
|
||||
| `apiKey` | string | Yes | GitHub API token |
|
||||
|
||||
#### Output
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ Draft emails using Gmail
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `operation` | string | Yes | Operation (e.g., 'send', 'draft') |
|
||||
| `operation` | string | Yes | Operation |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -135,7 +135,8 @@ Create a new Google Docs document
|
||||
| `accessToken` | string | Yes | The access token for the Google Docs API |
|
||||
| `title` | string | Yes | The title of the document to create |
|
||||
| `content` | string | No | The content of the document to create |
|
||||
| `folderId` | string | No | The ID of the folder to create the document in |
|
||||
| `folderSelector` | string | No | Select the folder to create the document in |
|
||||
| `folderId` | string | No | The ID of the folder to create the document in \(internal use\) |
|
||||
|
||||
#### Output
|
||||
|
||||
|
||||
@@ -91,7 +91,8 @@ Upload a file to Google Drive
|
||||
| `fileName` | string | Yes | The name of the file to upload |
|
||||
| `content` | string | Yes | The content of the file to upload |
|
||||
| `mimeType` | string | No | The MIME type of the file to upload |
|
||||
| `folderId` | string | No | The ID of the folder to upload the file to |
|
||||
| `folderSelector` | string | No | Select the folder to upload the file to |
|
||||
| `folderId` | string | No | The ID of the folder to upload the file to \(internal use\) |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -117,7 +118,8 @@ Create a new folder in Google Drive
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | The access token for the Google Drive API |
|
||||
| `fileName` | string | Yes | Name of the folder to create |
|
||||
| `folderId` | string | No | ID of the parent folder \(leave empty for root folder\) |
|
||||
| `folderSelector` | string | No | Select the parent folder to create the folder in |
|
||||
| `folderId` | string | No | ID of the parent folder \(internal use\) |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -142,7 +144,8 @@ List files and folders in Google Drive
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | The access token for the Google Drive API |
|
||||
| `folderId` | string | No | The ID of the folder to list files from |
|
||||
| `folderSelector` | string | No | Select the folder to list files from |
|
||||
| `folderId` | string | No | The ID of the folder to list files from \(internal use\) |
|
||||
| `query` | string | No | A query to filter the files |
|
||||
| `pageSize` | number | No | The number of files to return |
|
||||
| `pageToken` | string | No | The page token to use for pagination |
|
||||
|
||||
@@ -73,9 +73,9 @@ Search the web with the Custom Search API
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `query` | string | Yes | The search query to execute |
|
||||
| `apiKey` | string | Yes | Google API key |
|
||||
| `searchEngineId` | string | Yes | Custom Search Engine ID |
|
||||
| `num` | string | No | Number of results to return \(default: 10, max: 10\) |
|
||||
| `apiKey` | string | Yes | Google API key |
|
||||
|
||||
#### Output
|
||||
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
---
|
||||
title: Guesty
|
||||
description: Interact with Guesty property management system
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="guesty"
|
||||
color="#0051F8"
|
||||
icon={true}
|
||||
iconSvg={`<svg className="block-icon"
|
||||
|
||||
|
||||
|
||||
viewBox='0 0 101 100'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<path
|
||||
d='M56.6019 2.6685C53.2445 0.339792 48.8025 0.308905 45.413 2.5907L44.1323 3.45286C44.1309 3.45379 44.1296 3.45471 44.1282 3.45564L5.37916 29.5416C5.37801 29.5424 5.37687 29.5431 5.37572 29.5439L4.37839 30.2153C1.64126 32.058 0 35.1414 0 38.441V90.0841C0 95.5599 4.4395 100 9.91593 100H67.4737C72.9501 100 77.389 95.5605 77.389 90.0841V49.6765C77.389 46.3038 75.675 43.1622 72.8385 41.3373L56.3027 30.6989C53.0908 28.6325 48.9777 28.5944 45.728 30.6009L28.3986 41.301C25.4732 43.1073 23.6922 46.3001 23.6922 49.7382V75.553H33.3248V51.0025C33.3248 50.1189 33.7823 49.2983 34.5337 48.8337L34.535 48.8329L49.5731 39.5476C50.408 39.0322 51.4645 39.0414 52.29 39.5714L66.5886 48.7705C67.3167 49.24 67.7564 50.0471 67.7564 50.9134V87.8176C67.7564 89.2256 66.6152 90.3674 65.2072 90.3674H12.1824C10.7742 90.3674 9.63262 89.2256 9.63262 87.8176V39.6474C9.63262 38.7995 10.0541 38.0071 10.7571 37.5331L49.5075 11.4463C50.3783 10.8601 51.5192 10.8675 52.3822 11.4646L89.8995 37.4867C89.9007 37.4877 89.9024 37.4886 89.9035 37.4896C90.588 37.9663 90.9959 38.7476 90.9959 39.5819V100H100.629V38.3956C100.629 35.1448 99.0352 32.1005 96.3641 30.2478L95.3969 29.5767C95.3941 29.575 95.3918 29.5733 95.3895 29.5717L56.6019 2.6685Z'
|
||||
fill='currentColor'
|
||||
/>
|
||||
</svg>`}
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Guesty](https://www.guesty.com) is a comprehensive property management platform designed for short-term and vacation rental property managers. It provides a centralized system to manage listings, reservations, guest communications, and operations across multiple booking channels like Airbnb, Booking.com, and VRBO.
|
||||
|
||||
With Guesty, property managers can:
|
||||
|
||||
- **Centralize operations**: Manage multiple properties and listings from a single dashboard
|
||||
- **Automate workflows**: Set up automated messaging, task assignments, and cleaning schedules
|
||||
- **Synchronize calendars**: Keep availability updated across all booking channels
|
||||
- **Process payments**: Handle secure payment processing and financial reporting
|
||||
- **Manage guest communications**: Streamline guest interactions through unified inbox
|
||||
- **Generate reports**: Access analytics and insights to optimize property performance
|
||||
|
||||
In Sim Studio, the Guesty integration enables your agents to interact directly with your property management system programmatically. This allows for powerful automation scenarios such as reservation management, guest communication, and operational workflows. Your agents can retrieve detailed reservation information by ID, including guest details, booking dates, and property information. They can also search for guests by phone number to access their profiles and booking history. This integration bridges the gap between your AI workflows and your property management operations, enabling seamless handling of hospitality tasks without manual intervention. By connecting Sim Studio with Guesty, you can automate guest communications, streamline check-in processes, manage reservation details, and enhance the overall guest experience through intelligent automation.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Access Guesty property management data including reservations and guest information. Retrieve reservation details by ID or search for guests by phone number.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `guesty_reservation`
|
||||
|
||||
Fetch reservation details from Guesty by reservation ID
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Your Guesty API token |
|
||||
| `reservationId` | string | Yes | The ID of the reservation to fetch |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type |
|
||||
| --------- | ---- |
|
||||
| `id` | string |
|
||||
| `guest` | string |
|
||||
| `email` | string |
|
||||
| `phone` | string |
|
||||
|
||||
### `guesty_guest`
|
||||
|
||||
Search for guests in Guesty by phone number
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Your Guesty API token |
|
||||
| `phoneNumber` | string | Yes | The phone number to search for |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type |
|
||||
| --------- | ---- |
|
||||
| `guests` | string |
|
||||
| `fullName` | string |
|
||||
| `email` | string |
|
||||
| `phone` | string |
|
||||
| `address` | string |
|
||||
| `city` | string |
|
||||
| `country` | string |
|
||||
|
||||
|
||||
|
||||
## Block Configuration
|
||||
|
||||
### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `action` | string | Yes | Action |
|
||||
|
||||
|
||||
|
||||
### Outputs
|
||||
|
||||
| Output | Type | Description |
|
||||
| ------ | ---- | ----------- |
|
||||
| `id` | string | id output from the block |
|
||||
| `guest` | json | guest output from the block |
|
||||
| `checkIn` | string | checkIn output from the block |
|
||||
| `checkOut` | string | checkOut output from the block |
|
||||
| `status` | string | status output from the block |
|
||||
| `listing` | json | listing output from the block |
|
||||
| `money` | json | money output from the block |
|
||||
| `guests` | json | guests output from the block |
|
||||
|
||||
|
||||
## Notes
|
||||
|
||||
- Category: `tools`
|
||||
- Type: `guesty`
|
||||
@@ -80,14 +80,13 @@ Generate completions using Hugging Face Inference API
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Hugging Face API token |
|
||||
| `systemPrompt` | string | No | System prompt to guide the model behavior |
|
||||
| `content` | string | Yes | The user message content to send to the model |
|
||||
| `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 |
|
||||
| `apiKey` | string | Yes | Hugging Face API token |
|
||||
|
||||
#### Output
|
||||
|
||||
|
||||
@@ -60,8 +60,8 @@ Generate images using OpenAI
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `prompt` | string | Yes | A text description of the desired image |
|
||||
| `model` | string | Yes | The model to use \(gpt-image-1 or dall-e-3\) |
|
||||
| `prompt` | string | Yes | A text description of the desired image |
|
||||
| `size` | string | Yes | The size of the generated images \(1024x1024, 1024x1792, or 1792x1024\) |
|
||||
| `quality` | string | No | The quality of the image \(standard or hd\) |
|
||||
| `style` | string | No | The style of the image \(vivid or natural\) |
|
||||
|
||||
@@ -78,6 +78,10 @@ Extract and process web content into clean, LLM-friendly text using Jina AI Read
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `url` | string | Yes | The URL to read and convert to markdown |
|
||||
| `useReaderLMv2` | boolean | No | Whether to use ReaderLM-v2 for better quality |
|
||||
| `gatherLinks` | boolean | No | Whether to gather all links at the end |
|
||||
| `jsonResponse` | boolean | No | Whether to return response in JSON format |
|
||||
| `apiKey` | string | Yes | Your Jina AI API key |
|
||||
|
||||
#### Output
|
||||
|
||||
|
||||
@@ -81,6 +81,7 @@ Search for similar content in one or more knowledge bases using vector similarit
|
||||
| `results` | string |
|
||||
| `query` | string |
|
||||
| `totalResults` | string |
|
||||
| `cost` | string |
|
||||
|
||||
### `knowledge_upload_chunk`
|
||||
|
||||
|
||||
@@ -65,9 +65,9 @@ Search the web for information using Linkup
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `q` | string | Yes | The search query |
|
||||
| `apiKey` | string | Yes | Enter your Linkup API key |
|
||||
| `depth` | string | Yes | Search depth \(has to either be |
|
||||
| `outputType` | string | Yes | Type of output to return \(has to either be |
|
||||
| `apiKey` | string | Yes | Enter your Linkup API key |
|
||||
|
||||
#### Output
|
||||
|
||||
|
||||
@@ -58,9 +58,9 @@ Add memories to Mem0 for persistent storage and retrieval
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Your Mem0 API key |
|
||||
| `userId` | string | Yes | User ID associated with the memory |
|
||||
| `messages` | json | Yes | Array of message objects with role and content |
|
||||
| `apiKey` | string | Yes | Your Mem0 API key |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -76,10 +76,10 @@ Search for memories in Mem0 using semantic search
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Your Mem0 API key |
|
||||
| `userId` | string | Yes | User ID to search memories for |
|
||||
| `query` | string | Yes | Search query to find relevant memories |
|
||||
| `limit` | number | No | Maximum number of results to return |
|
||||
| `apiKey` | string | Yes | Your Mem0 API key |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -96,12 +96,12 @@ Retrieve memories from Mem0 by ID or filter criteria
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Your Mem0 API key |
|
||||
| `userId` | string | Yes | User ID to retrieve memories for |
|
||||
| `memoryId` | string | No | Specific memory ID to retrieve |
|
||||
| `startDate` | string | No | Start date for filtering by created_at \(format: YYYY-MM-DD\) |
|
||||
| `endDate` | string | No | End date for filtering by created_at \(format: YYYY-MM-DD\) |
|
||||
| `limit` | number | No | Maximum number of results to return |
|
||||
| `apiKey` | string | Yes | Your Mem0 API key |
|
||||
|
||||
#### Output
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"items": [
|
||||
"index",
|
||||
"airtable",
|
||||
"autoblocks",
|
||||
"browser_use",
|
||||
"clay",
|
||||
"confluence",
|
||||
@@ -18,7 +17,6 @@
|
||||
"google_drive",
|
||||
"google_search",
|
||||
"google_sheets",
|
||||
"guesty",
|
||||
"huggingface",
|
||||
"image_generator",
|
||||
"jina",
|
||||
@@ -50,6 +48,7 @@
|
||||
"twilio_sms",
|
||||
"typeform",
|
||||
"vision",
|
||||
"wealthbox",
|
||||
"whatsapp",
|
||||
"x",
|
||||
"youtube"
|
||||
|
||||
@@ -96,11 +96,11 @@ Parse PDF documents using Mistral OCR API
|
||||
| `filePath` | string | Yes | URL to a PDF document to be processed |
|
||||
| `fileUpload` | object | No | File upload data from file-upload component |
|
||||
| `resultType` | string | No | Type of parsed result \(markdown, text, or json\). Defaults to markdown. |
|
||||
| `apiKey` | string | Yes | Mistral API key \(MISTRAL_API_KEY\) |
|
||||
| `includeImageBase64` | boolean | No | Include base64-encoded images in the response |
|
||||
| `pages` | array | No | Specific pages to process \(array of page numbers, starting from 0\) |
|
||||
| `imageLimit` | number | No | Maximum number of images to extract from the PDF |
|
||||
| `imageMinSize` | number | No | Minimum height and width of images to extract from the PDF |
|
||||
| `apiKey` | string | Yes | Mistral API key \(MISTRAL_API_KEY\) |
|
||||
|
||||
#### Output
|
||||
|
||||
|
||||
@@ -49,8 +49,8 @@ Read content from a Notion page
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `pageId` | string | Yes | The ID of the Notion page to read |
|
||||
| `accessToken` | string | Yes | Notion OAuth access token |
|
||||
| `pageId` | string | Yes | The ID of the Notion page to read |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -70,9 +70,9 @@ Append content to a Notion page
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | Notion OAuth access token |
|
||||
| `pageId` | string | Yes | The ID of the Notion page to append content to |
|
||||
| `content` | string | Yes | The content to append to the page |
|
||||
| `accessToken` | string | Yes | Notion OAuth access token |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -88,12 +88,12 @@ Create a new page in Notion
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | Notion OAuth access token |
|
||||
| `parentType` | string | Yes | Type of parent: |
|
||||
| `parentId` | string | Yes | ID of the parent page or database |
|
||||
| `title` | string | No | Title of the page \(required for parent pages, not for databases\) |
|
||||
| `properties` | json | No | JSON object of properties for database pages |
|
||||
| `content` | string | No | Optional content to add to the page upon creation |
|
||||
| `accessToken` | string | Yes | Notion OAuth access token |
|
||||
|
||||
#### Output
|
||||
|
||||
|
||||
@@ -57,11 +57,10 @@ Generate embeddings from text using OpenAI
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | OpenAI API key |
|
||||
| `input` | string | Yes | Text to generate embeddings for |
|
||||
| `model` | string | No | Model to use for embeddings |
|
||||
| `encoding_format` | string | No | The format to return the embeddings in |
|
||||
| `user` | string | No | A unique identifier for the end-user |
|
||||
| `encodingFormat` | string | No | The format to return the embeddings in |
|
||||
| `apiKey` | string | Yes | OpenAI API key |
|
||||
|
||||
#### Output
|
||||
|
||||
|
||||
@@ -51,11 +51,12 @@ Generate completions using Perplexity AI chat models
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Perplexity API key |
|
||||
| `systemPrompt` | string | No | System prompt to guide the model behavior |
|
||||
| `content` | string | Yes | The user message content to send to the model |
|
||||
| `model` | string | Yes | Model to use for chat completions \(e.g., sonar, mistral\) |
|
||||
| `messages` | array | Yes | Array of message objects with role and content |
|
||||
| `max_tokens` | number | No | Maximum number of tokens to generate |
|
||||
| `temperature` | number | No | Sampling temperature between 0 and 1 |
|
||||
| `apiKey` | string | Yes | Perplexity API key |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -75,7 +76,7 @@ Generate completions using Perplexity AI chat models
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `prompt` | string | Yes | User Prompt - Enter your prompt here... |
|
||||
| `content` | string | Yes | User Prompt - Enter your prompt here... |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -59,9 +59,9 @@ Generate embeddings from text using Pinecone
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Pinecone API key |
|
||||
| `model` | string | Yes | Model to use for generating embeddings |
|
||||
| `inputs` | array | Yes | Array of text inputs to generate embeddings for |
|
||||
| `apiKey` | string | Yes | Pinecone API key |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -80,10 +80,10 @@ Insert or update text records in a Pinecone index
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Pinecone API key |
|
||||
| `indexHost` | string | Yes | Full Pinecone index host URL |
|
||||
| `namespace` | string | Yes | Namespace to upsert records into |
|
||||
| `records` | array | Yes | Record or array of records to upsert, each containing _id, text, and optional metadata |
|
||||
| `apiKey` | string | Yes | Pinecone API key |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -99,7 +99,6 @@ Search for similar text in a Pinecone index
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Pinecone API key |
|
||||
| `indexHost` | string | Yes | Full Pinecone index host URL |
|
||||
| `namespace` | string | No | Namespace to search in |
|
||||
| `searchQuery` | string | Yes | Text to search for |
|
||||
@@ -107,6 +106,7 @@ Search for similar text in a Pinecone index
|
||||
| `fields` | array | No | Fields to return in the results |
|
||||
| `filter` | object | No | Filter to apply to the search |
|
||||
| `rerank` | object | No | Reranking parameters |
|
||||
| `apiKey` | string | Yes | Pinecone API key |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -124,7 +124,6 @@ Search for similar vectors in a Pinecone index
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Pinecone API key |
|
||||
| `indexHost` | string | Yes | Full Pinecone index host URL |
|
||||
| `namespace` | string | No | Namespace to search in |
|
||||
| `vector` | array | Yes | Vector to search for |
|
||||
@@ -132,6 +131,7 @@ Search for similar vectors in a Pinecone index
|
||||
| `filter` | object | No | Filter to apply to the search |
|
||||
| `includeValues` | boolean | No | Include vector values in response |
|
||||
| `includeMetadata` | boolean | No | Include metadata in response |
|
||||
| `apiKey` | string | Yes | Pinecone API key |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -150,10 +150,10 @@ Fetch vectors by ID from a Pinecone index
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Pinecone API key |
|
||||
| `indexHost` | string | Yes | Full Pinecone index host URL |
|
||||
| `ids` | array | Yes | Array of vector IDs to fetch |
|
||||
| `namespace` | string | No | Namespace to fetch vectors from |
|
||||
| `apiKey` | string | Yes | Pinecone API key |
|
||||
|
||||
#### Output
|
||||
|
||||
|
||||
@@ -50,24 +50,6 @@ Access Reddit data to retrieve posts and comments from any subreddit. Get post t
|
||||
|
||||
## Tools
|
||||
|
||||
### `reddit_hot_posts`
|
||||
|
||||
Fetch the most popular (hot) posts from a specified subreddit.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `subreddit` | string | Yes | The name of the subreddit to fetch posts from \(without the r/ prefix\) |
|
||||
| `limit` | number | No | Maximum number of posts to return \(default: 10, max: 100\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type |
|
||||
| --------- | ---- |
|
||||
| `subreddit` | string |
|
||||
| `posts` | string |
|
||||
|
||||
### `reddit_get_posts`
|
||||
|
||||
Fetch posts from a subreddit with different sorting options
|
||||
@@ -76,6 +58,7 @@ Fetch posts from a subreddit with different sorting options
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | Access token for Reddit API |
|
||||
| `subreddit` | string | Yes | The name of the subreddit to fetch posts from \(without the r/ prefix\) |
|
||||
| `sort` | string | No | Sort method for posts: |
|
||||
| `limit` | number | No | Maximum number of posts to return \(default: 10, max: 100\) |
|
||||
@@ -96,6 +79,7 @@ Fetch comments from a specific Reddit post
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | Access token for Reddit API |
|
||||
| `postId` | string | Yes | The ID of the Reddit post to fetch comments from |
|
||||
| `subreddit` | string | Yes | The subreddit where the post is located \(without the r/ prefix\) |
|
||||
| `sort` | string | No | Sort method for comments: |
|
||||
@@ -121,7 +105,7 @@ Fetch comments from a specific Reddit post
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `action` | string | Yes | Action |
|
||||
| `operation` | string | Yes | Operation |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -93,11 +93,11 @@ A powerful web search tool that provides access to Google search results through
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `query` | string | Yes | The search query |
|
||||
| `apiKey` | string | Yes | Serper API Key |
|
||||
| `num` | number | No | Number of results to return |
|
||||
| `gl` | string | No | Country code for search results |
|
||||
| `hl` | string | No | Language code for search results |
|
||||
| `type` | string | No | Type of search to perform |
|
||||
| `apiKey` | string | Yes | Serper API Key |
|
||||
|
||||
#### Output
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ Send messages to Slack channels or users through the Slack API. Supports Slack m
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `authMethod` | string | No | Authentication method: oauth or bot_token |
|
||||
| `botToken` | string | No | Bot token for Custom Bot |
|
||||
| `accessToken` | string | No | OAuth access token or bot token for Slack API |
|
||||
| `channel` | string | Yes | Target Slack channel \(e.g., #general\) |
|
||||
|
||||
@@ -205,10 +205,10 @@ Extract structured data from a webpage using Stagehand
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `instruction` | string | Yes | Instructions for extraction |
|
||||
| `schema` | json | Yes | JSON schema defining the structure of the data to extract |
|
||||
| `apiKey` | string | Yes | OpenAI API key for extraction \(required by Stagehand\) |
|
||||
| `url` | string | Yes | URL of the webpage to extract data from |
|
||||
| `instruction` | string | Yes | Instructions for extraction |
|
||||
| `apiKey` | string | Yes | OpenAI API key for extraction \(required by Stagehand\) |
|
||||
| `schema` | json | Yes | JSON schema defining the structure of the data to extract |
|
||||
|
||||
#### Output
|
||||
|
||||
|
||||
@@ -209,11 +209,11 @@ Run an autonomous web agent to complete tasks and extract structured data
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `task` | string | Yes | The task to complete or goal to achieve on the website |
|
||||
| `startUrl` | string | Yes | URL of the webpage to start the agent on |
|
||||
| `outputSchema` | json | No | Optional JSON schema defining the structure of data the agent should return |
|
||||
| `task` | string | Yes | The task to complete or goal to achieve on the website |
|
||||
| `variables` | json | No | Optional variables to substitute in the task \(format: \{key: value\}\). Reference in task using %key% |
|
||||
| `apiKey` | string | Yes | OpenAI API key for agent execution \(required by Stagehand\) |
|
||||
| `outputSchema` | json | No | Optional JSON schema defining the structure of data the agent should return |
|
||||
|
||||
#### Output
|
||||
|
||||
|
||||
@@ -83,8 +83,10 @@ Query data from a Supabase table
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Your Supabase client anon key |
|
||||
| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | Yes | The name of the Supabase table to query |
|
||||
| `filter` | object | No | Filter to apply to the query |
|
||||
| `apiKey` | string | Yes | Your Supabase client anon key |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -101,8 +103,10 @@ Insert data into a Supabase table
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Your Supabase client anon key |
|
||||
| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | Yes | The name of the Supabase table to insert data into |
|
||||
| `data` | any | Yes | The data to insert |
|
||||
| `apiKey` | string | Yes | Your Supabase client anon key |
|
||||
|
||||
#### Output
|
||||
|
||||
|
||||
@@ -95,8 +95,8 @@ Extract raw content from multiple web pages simultaneously using Tavily
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `urls` | string | Yes | URL or array of URLs to extract content from |
|
||||
| `apiKey` | string | Yes | Tavily API Key |
|
||||
| `extract_depth` | string | No | The depth of extraction \(basic=1 credit/5 URLs, advanced=2 credits/5 URLs\) |
|
||||
| `apiKey` | string | Yes | Tavily API Key |
|
||||
|
||||
#### Output
|
||||
|
||||
|
||||
190
apps/docs/content/docs/tools/wealthbox.mdx
Normal file
190
apps/docs/content/docs/tools/wealthbox.mdx
Normal file
@@ -0,0 +1,190 @@
|
||||
---
|
||||
title: Wealthbox
|
||||
description: Interact with Wealthbox
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="wealthbox"
|
||||
color="#E0E0E0"
|
||||
icon={true}
|
||||
iconSvg={`<svg className="block-icon"
|
||||
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
version='1.0'
|
||||
|
||||
|
||||
viewBox='50 -50 200 200'
|
||||
>
|
||||
<g fill='#106ED4' stroke='none' transform='translate(0, 200) scale(0.15, -0.15)'>
|
||||
<path d='M764 1542 c-110 -64 -230 -134 -266 -156 -42 -24 -71 -49 -78 -65 -7 -19 -10 -126 -8 -334 3 -291 4 -307 23 -326 11 -11 103 -67 205 -126 102 -59 219 -127 261 -151 42 -24 85 -44 96 -44 23 0 527 288 561 320 22 22 22 23 22 340 0 288 -2 320 -17 338 -32 37 -537 322 -569 321 -18 0 -107 -46 -230 -117z m445 -144 c108 -62 206 -123 219 -135 22 -22 22 -26 22 -261 0 -214 -2 -242 -17 -260 -23 -26 -414 -252 -437 -252 -9 0 -70 31 -134 69 -64 37 -161 94 -215 125 l-97 57 2 261 3 261 210 123 c116 67 219 123 229 123 10 1 107 -50 215 -111z' />
|
||||
<path d='M700 1246 l-55 -32 -3 -211 -2 -211 37 -23 c21 -12 52 -30 69 -40 l30 -18 103 59 c56 33 109 60 117 60 8 0 62 -27 119 -60 l104 -60 63 37 c35 21 66 42 70 48 4 5 8 101 8 212 l0 202 -62 35 -63 35 -3 -197 c-1 -108 -6 -200 -11 -205 -5 -5 -54 17 -114 52 -58 34 -108 61 -111 61 -2 0 -51 -27 -107 -60 -56 -32 -106 -57 -111 -54 -4 3 -8 95 -8 205 0 109 -3 199 -7 199 -5 -1 -33 -16 -63 -34z' />
|
||||
</g>
|
||||
</svg>`}
|
||||
/>
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Wealthbox functionality to manage notes, contacts, and tasks. Read content from existing notes, contacts, and tasks and write to them using OAuth authentication. Supports text content manipulation for note creation and editing.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `wealthbox_read_note`
|
||||
|
||||
Read content from a Wealthbox note
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | The access token for the Wealthbox API |
|
||||
| `noteId` | string | No | The ID of the note to read \(optional\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type |
|
||||
| --------- | ---- |
|
||||
| `note` | string |
|
||||
| `metadata` | string |
|
||||
| `noteId` | string |
|
||||
| `itemType` | string |
|
||||
|
||||
### `wealthbox_write_note`
|
||||
|
||||
Create or update a Wealthbox note
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | The access token for the Wealthbox API |
|
||||
| `content` | string | Yes | The main body of the note |
|
||||
| `contactId` | string | No | ID of contact to link to this note |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type |
|
||||
| --------- | ---- |
|
||||
| `note` | string |
|
||||
| `metadata` | string |
|
||||
| `itemType` | string |
|
||||
|
||||
### `wealthbox_read_contact`
|
||||
|
||||
Read content from a Wealthbox contact
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | The access token for the Wealthbox API |
|
||||
| `contactId` | string | Yes | The ID of the contact to read |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type |
|
||||
| --------- | ---- |
|
||||
| `contact` | string |
|
||||
| `metadata` | string |
|
||||
| `contactId` | string |
|
||||
| `itemType` | string |
|
||||
|
||||
### `wealthbox_write_contact`
|
||||
|
||||
Create a new Wealthbox contact
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | The access token for the Wealthbox API |
|
||||
| `firstName` | string | Yes | The first name of the contact |
|
||||
| `lastName` | string | Yes | The last name of the contact |
|
||||
| `emailAddress` | string | No | The email address of the contact |
|
||||
| `backgroundInformation` | string | No | Background information about the contact |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type |
|
||||
| --------- | ---- |
|
||||
| `contact` | string |
|
||||
| `metadata` | string |
|
||||
| `itemType` | string |
|
||||
|
||||
### `wealthbox_read_task`
|
||||
|
||||
Read content from a Wealthbox task
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | The access token for the Wealthbox API |
|
||||
| `taskId` | string | No | The ID of the task to read \(optional\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type |
|
||||
| --------- | ---- |
|
||||
| `task` | string |
|
||||
| `metadata` | string |
|
||||
| `taskId` | string |
|
||||
| `itemType` | string |
|
||||
|
||||
### `wealthbox_write_task`
|
||||
|
||||
Create or update a Wealthbox task
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | The access token for the Wealthbox API |
|
||||
| `title` | string | Yes | The name/title of the task |
|
||||
| `dueDate` | string | Yes | The due date and time of the task |
|
||||
| `complete` | boolean | No | Whether the task is complete |
|
||||
| `category` | number | No | The category ID the task belongs to |
|
||||
| `contactId` | string | No | ID of contact to link to this task |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type |
|
||||
| --------- | ---- |
|
||||
| `task` | string |
|
||||
| `metadata` | string |
|
||||
| `taskId` | string |
|
||||
| `itemType` | string |
|
||||
|
||||
|
||||
|
||||
## Block Configuration
|
||||
|
||||
### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `operation` | string | Yes | Operation |
|
||||
|
||||
|
||||
|
||||
### Outputs
|
||||
|
||||
| Output | Type | Description |
|
||||
| ------ | ---- | ----------- |
|
||||
| `note` | any | note output from the block |
|
||||
| `notes` | any | notes output from the block |
|
||||
| `contact` | any | contact output from the block |
|
||||
| `contacts` | any | contacts output from the block |
|
||||
| `task` | any | task output from the block |
|
||||
| `tasks` | any | tasks output from the block |
|
||||
| `metadata` | json | metadata output from the block |
|
||||
| `success` | any | success output from the block |
|
||||
|
||||
|
||||
## Notes
|
||||
|
||||
- Category: `tools`
|
||||
- Type: `wealthbox`
|
||||
@@ -55,8 +55,8 @@ Search for videos on YouTube using the YouTube Data API.
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `query` | string | Yes | Search query for YouTube videos |
|
||||
| `apiKey` | string | Yes | YouTube API Key |
|
||||
| `maxResults` | number | No | Maximum number of videos to return |
|
||||
| `apiKey` | string | Yes | YouTube API Key |
|
||||
|
||||
#### Output
|
||||
|
||||
|
||||
153
apps/sim/app/api/auth/oauth/wealthbox/item/route.ts
Normal file
153
apps/sim/app/api/auth/oauth/wealthbox/item/route.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { db } from '@/db'
|
||||
import { account } from '@/db/schema'
|
||||
import { refreshAccessTokenIfNeeded } from '../../utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('WealthboxItemAPI')
|
||||
|
||||
/**
|
||||
* Get a single item (note, contact, task) from Wealthbox
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
// Get the session
|
||||
const session = await getSession()
|
||||
|
||||
// Check if the user is authenticated
|
||||
if (!session?.user?.id) {
|
||||
logger.warn(`[${requestId}] Unauthenticated request rejected`)
|
||||
return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
|
||||
}
|
||||
|
||||
// Get parameters from query
|
||||
const { searchParams } = new URL(request.url)
|
||||
const credentialId = searchParams.get('credentialId')
|
||||
const itemId = searchParams.get('itemId')
|
||||
const type = searchParams.get('type') || 'contact'
|
||||
|
||||
if (!credentialId || !itemId) {
|
||||
logger.warn(`[${requestId}] Missing required parameters`, { credentialId, itemId })
|
||||
return NextResponse.json({ error: 'Credential ID and Item ID are required' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Validate item type - only handle contacts now
|
||||
if (type !== 'contact') {
|
||||
logger.warn(`[${requestId}] Invalid item type: ${type}`)
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid item type. Only contact is supported.' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Get the credential from the database
|
||||
const credentials = await db.select().from(account).where(eq(account.id, credentialId)).limit(1)
|
||||
|
||||
if (!credentials.length) {
|
||||
logger.warn(`[${requestId}] Credential not found`, { credentialId })
|
||||
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
const credential = credentials[0]
|
||||
|
||||
// Check if the credential belongs to the user
|
||||
if (credential.userId !== session.user.id) {
|
||||
logger.warn(`[${requestId}] Unauthorized credential access attempt`, {
|
||||
credentialUserId: credential.userId,
|
||||
requestUserId: session.user.id,
|
||||
})
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
|
||||
}
|
||||
|
||||
// Refresh access token if needed
|
||||
const accessToken = await refreshAccessTokenIfNeeded(credentialId, session.user.id, requestId)
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error(`[${requestId}] Failed to obtain valid access token`)
|
||||
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
|
||||
}
|
||||
|
||||
// Determine the endpoint based on item type - only contacts
|
||||
const endpoints = {
|
||||
contact: 'contacts',
|
||||
}
|
||||
const endpoint = endpoints[type as keyof typeof endpoints]
|
||||
|
||||
logger.info(`[${requestId}] Fetching ${type} ${itemId} from Wealthbox`)
|
||||
|
||||
// Make request to Wealthbox API
|
||||
const response = await fetch(`https://api.crmworkspace.com/v1/${endpoint}/${itemId}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error(
|
||||
`[${requestId}] Wealthbox API error: ${response.status} ${response.statusText}`,
|
||||
{
|
||||
error: errorText,
|
||||
endpoint,
|
||||
itemId,
|
||||
}
|
||||
)
|
||||
|
||||
if (response.status === 404) {
|
||||
return NextResponse.json({ error: 'Item not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to fetch ${type} from Wealthbox` },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
logger.info(`[${requestId}] Wealthbox API response structure`, {
|
||||
type,
|
||||
dataKeys: Object.keys(data || {}),
|
||||
hasContacts: !!data.contacts,
|
||||
totalCount: data.meta?.total_count,
|
||||
})
|
||||
|
||||
// Transform the response to match our expected format
|
||||
let items: any[] = []
|
||||
|
||||
if (type === 'contact') {
|
||||
// Handle single contact response - API returns contact data directly when fetching by ID
|
||||
if (data?.id) {
|
||||
// Single contact response
|
||||
const item = {
|
||||
id: data.id?.toString() || '',
|
||||
name: `${data.first_name || ''} ${data.last_name || ''}`.trim() || `Contact ${data.id}`,
|
||||
type: 'contact',
|
||||
content: data.background_info || '',
|
||||
createdAt: data.created_at,
|
||||
updatedAt: data.updated_at,
|
||||
}
|
||||
items = [item]
|
||||
} else {
|
||||
logger.warn(`[${requestId}] Unexpected contact response format`, { data })
|
||||
items = []
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Successfully fetched ${items.length} ${type}s from Wealthbox (total: ${data.meta?.total_count || 'unknown'})`
|
||||
)
|
||||
|
||||
return NextResponse.json({ item: items[0] }, { status: 200 })
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error fetching Wealthbox item`, error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
168
apps/sim/app/api/auth/oauth/wealthbox/items/route.ts
Normal file
168
apps/sim/app/api/auth/oauth/wealthbox/items/route.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { db } from '@/db'
|
||||
import { account } from '@/db/schema'
|
||||
import { refreshAccessTokenIfNeeded } from '../../utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('WealthboxItemsAPI')
|
||||
|
||||
/**
|
||||
* Get items (notes, contacts, tasks) from Wealthbox
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
// Get the session
|
||||
const session = await getSession()
|
||||
|
||||
// Check if the user is authenticated
|
||||
if (!session?.user?.id) {
|
||||
logger.warn(`[${requestId}] Unauthenticated request rejected`)
|
||||
return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
|
||||
}
|
||||
|
||||
// Get parameters from query
|
||||
const { searchParams } = new URL(request.url)
|
||||
const credentialId = searchParams.get('credentialId')
|
||||
const type = searchParams.get('type') || 'contact'
|
||||
const query = searchParams.get('query') || ''
|
||||
|
||||
if (!credentialId) {
|
||||
logger.warn(`[${requestId}] Missing credential ID`)
|
||||
return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Validate item type - only handle contacts now
|
||||
if (type !== 'contact') {
|
||||
logger.warn(`[${requestId}] Invalid item type: ${type}`)
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid item type. Only contact is supported.' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Get the credential from the database
|
||||
const credentials = await db.select().from(account).where(eq(account.id, credentialId)).limit(1)
|
||||
|
||||
if (!credentials.length) {
|
||||
logger.warn(`[${requestId}] Credential not found`, { credentialId })
|
||||
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
const credential = credentials[0]
|
||||
|
||||
// Check if the credential belongs to the user
|
||||
if (credential.userId !== session.user.id) {
|
||||
logger.warn(`[${requestId}] Unauthorized credential access attempt`, {
|
||||
credentialUserId: credential.userId,
|
||||
requestUserId: session.user.id,
|
||||
})
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
|
||||
}
|
||||
|
||||
// Refresh access token if needed
|
||||
const accessToken = await refreshAccessTokenIfNeeded(credentialId, session.user.id, requestId)
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error(`[${requestId}] Failed to obtain valid access token`)
|
||||
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
|
||||
}
|
||||
|
||||
// Use correct endpoints based on documentation - only for contacts
|
||||
const endpoints = {
|
||||
contact: 'contacts',
|
||||
}
|
||||
const endpoint = endpoints[type as keyof typeof endpoints]
|
||||
|
||||
// Build URL - using correct API base URL
|
||||
const url = new URL(`https://api.crmworkspace.com/v1/${endpoint}`)
|
||||
|
||||
logger.info(`[${requestId}] Fetching ${type}s from Wealthbox`, {
|
||||
endpoint,
|
||||
url: url.toString(),
|
||||
hasQuery: !!query.trim(),
|
||||
})
|
||||
|
||||
// Make request to Wealthbox API
|
||||
const response = await fetch(url.toString(), {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error(
|
||||
`[${requestId}] Wealthbox API error: ${response.status} ${response.statusText}`,
|
||||
{
|
||||
error: errorText,
|
||||
endpoint,
|
||||
url: url.toString(),
|
||||
}
|
||||
)
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to fetch ${type}s from Wealthbox` },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
logger.info(`[${requestId}] Wealthbox API response structure`, {
|
||||
type,
|
||||
status: response.status,
|
||||
dataKeys: Object.keys(data || {}),
|
||||
hasContacts: !!data.contacts,
|
||||
dataStructure: typeof data === 'object' ? Object.keys(data) : 'not an object',
|
||||
})
|
||||
|
||||
// Transform the response based on type and correct response format
|
||||
let items: any[] = []
|
||||
|
||||
if (type === 'contact') {
|
||||
const contacts = data.contacts || []
|
||||
if (!Array.isArray(contacts)) {
|
||||
logger.warn(`[${requestId}] Contacts is not an array`, {
|
||||
contacts,
|
||||
dataType: typeof contacts,
|
||||
})
|
||||
return NextResponse.json({ items: [] }, { status: 200 })
|
||||
}
|
||||
|
||||
items = contacts.map((item: any) => ({
|
||||
id: item.id?.toString() || '',
|
||||
name: `${item.first_name || ''} ${item.last_name || ''}`.trim() || `Contact ${item.id}`,
|
||||
type: 'contact',
|
||||
content: item.background_information || '',
|
||||
createdAt: item.created_at,
|
||||
updatedAt: item.updated_at,
|
||||
}))
|
||||
}
|
||||
|
||||
// Apply client-side filtering if query is provided
|
||||
if (query.trim()) {
|
||||
const searchTerm = query.trim().toLowerCase()
|
||||
items = items.filter(
|
||||
(item) =>
|
||||
item.name.toLowerCase().includes(searchTerm) ||
|
||||
item.content.toLowerCase().includes(searchTerm)
|
||||
)
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Successfully fetched ${items.length} ${type}s from Wealthbox`, {
|
||||
totalItems: items.length,
|
||||
hasSearchQuery: !!query.trim(),
|
||||
})
|
||||
|
||||
return NextResponse.json({ items }, { status: 200 })
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error fetching Wealthbox items`, error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { and, eq } from 'drizzle-orm'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { env } from '@/lib/env'
|
||||
import { isDev } from '@/lib/environment'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { getBaseDomain } from '@/lib/urls/utils'
|
||||
import { encryptSecret } from '@/lib/utils'
|
||||
@@ -71,9 +71,7 @@ export async function GET(_request: NextRequest, { params }: { params: Promise<{
|
||||
// Create a new result object without the password
|
||||
const { password, ...safeData } = chatInstance[0]
|
||||
|
||||
const isDevelopment = env.NODE_ENV === 'development'
|
||||
|
||||
const chatUrl = isDevelopment
|
||||
const chatUrl = isDev
|
||||
? `http://${chatInstance[0].subdomain}.${getBaseDomain()}`
|
||||
: `https://${chatInstance[0].subdomain}.simstudio.ai`
|
||||
|
||||
@@ -221,9 +219,7 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
||||
|
||||
const updatedSubdomain = subdomain || existingChat[0].subdomain
|
||||
|
||||
const isDevelopment = env.NODE_ENV === 'development'
|
||||
|
||||
const chatUrl = isDevelopment
|
||||
const chatUrl = isDev
|
||||
? `http://${updatedSubdomain}.${getBaseDomain()}`
|
||||
: `https://${updatedSubdomain}.simstudio.ai`
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { v4 as uuidv4 } from 'uuid'
|
||||
import { z } from 'zod'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { env } from '@/lib/env'
|
||||
import { isDev } from '@/lib/environment'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { encryptSecret } from '@/lib/utils'
|
||||
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
|
||||
@@ -169,11 +170,10 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
// Return successful response with chat URL
|
||||
// Check if we're in development or production
|
||||
const isDevelopment = env.NODE_ENV === 'development'
|
||||
const baseUrl = env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
|
||||
|
||||
let chatUrl: string
|
||||
if (isDevelopment) {
|
||||
if (isDev) {
|
||||
try {
|
||||
const url = new URL(baseUrl)
|
||||
chatUrl = `${url.protocol}//${subdomain}.${url.host}`
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { eq, sql } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { env } from '@/lib/env'
|
||||
import { isDev } from '@/lib/environment'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { EnhancedLoggingSession } from '@/lib/logs/enhanced-logging-session'
|
||||
import { buildTraceSpans } from '@/lib/logs/trace-spans'
|
||||
@@ -20,7 +20,6 @@ declare global {
|
||||
}
|
||||
|
||||
const logger = createLogger('ChatAuthUtils')
|
||||
const isDevelopment = env.NODE_ENV === 'development'
|
||||
|
||||
export const encryptAuthToken = (subdomainId: string, type: string): string => {
|
||||
return Buffer.from(`${subdomainId}:${type}:${Date.now()}`).toString('base64')
|
||||
@@ -63,11 +62,11 @@ export const setChatAuthCookie = (
|
||||
name: `chat_auth_${subdomainId}`,
|
||||
value: token,
|
||||
httpOnly: true,
|
||||
secure: !isDevelopment,
|
||||
secure: !isDev,
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
// Using subdomain for the domain in production
|
||||
domain: isDevelopment ? undefined : '.simstudio.ai',
|
||||
domain: isDev ? undefined : '.simstudio.ai',
|
||||
maxAge: 60 * 60 * 24, // 24 hours
|
||||
})
|
||||
}
|
||||
@@ -78,7 +77,7 @@ export function addCorsHeaders(response: NextResponse, request: NextRequest) {
|
||||
const origin = request.headers.get('origin') || ''
|
||||
|
||||
// In development, allow any localhost subdomain
|
||||
if (isDevelopment && origin.includes('localhost')) {
|
||||
if (isDev && origin.includes('localhost')) {
|
||||
response.headers.set('Access-Control-Allow-Origin', origin)
|
||||
response.headers.set('Access-Control-Allow-Credentials', 'true')
|
||||
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
isBlobPath,
|
||||
isCloudPath,
|
||||
isS3Path,
|
||||
} from '../utils'
|
||||
} from '@/app/api/files/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
|
||||
@@ -447,7 +447,7 @@ async function handleCsvBuffer(
|
||||
logger.info(`Parsing CSV in memory: ${filename}`)
|
||||
|
||||
// Use the parseBuffer function from our library
|
||||
const { parseBuffer } = await import('../../../../lib/file-parsers')
|
||||
const { parseBuffer } = await import('@/lib/file-parsers')
|
||||
const result = await parseBuffer(fileBuffer, 'csv')
|
||||
|
||||
return {
|
||||
@@ -492,7 +492,7 @@ async function handleGenericTextBuffer(
|
||||
|
||||
// Try to use a specialized parser if available
|
||||
try {
|
||||
const { parseBuffer, isSupportedFileType } = await import('../../../../lib/file-parsers')
|
||||
const { parseBuffer, isSupportedFileType } = await import('@/lib/file-parsers')
|
||||
|
||||
if (isSupportedFileType(extension)) {
|
||||
const result = await parseBuffer(fileBuffer, extension)
|
||||
@@ -578,7 +578,7 @@ async function parseBufferAsPdf(buffer: Buffer) {
|
||||
// Import parsers dynamically to avoid initialization issues in tests
|
||||
// First try to use the main PDF parser
|
||||
try {
|
||||
const { PdfParser } = await import('../../../../lib/file-parsers/pdf-parser')
|
||||
const { PdfParser } = await import('@/lib/file-parsers/pdf-parser')
|
||||
const parser = new PdfParser()
|
||||
logger.info('Using main PDF parser for buffer')
|
||||
|
||||
@@ -589,7 +589,7 @@ async function parseBufferAsPdf(buffer: Buffer) {
|
||||
} catch (error) {
|
||||
// Fallback to raw PDF parser
|
||||
logger.warn('Main PDF parser failed, using raw parser for buffer:', error)
|
||||
const { RawPdfParser } = await import('../../../../lib/file-parsers/raw-pdf-parser')
|
||||
const { RawPdfParser } = await import('@/lib/file-parsers/raw-pdf-parser')
|
||||
const rawParser = new RawPdfParser()
|
||||
|
||||
return await rawParser.parseBuffer(buffer)
|
||||
|
||||
@@ -7,7 +7,7 @@ import { getStorageProvider, isUsingCloudStorage } from '@/lib/uploads'
|
||||
import { getBlobServiceClient } from '@/lib/uploads/blob/blob-client'
|
||||
import { getS3Client, sanitizeFilenameForMetadata } from '@/lib/uploads/s3/s3-client'
|
||||
import { BLOB_CONFIG, BLOB_KB_CONFIG, S3_CONFIG, S3_KB_CONFIG } from '@/lib/uploads/setup'
|
||||
import { createErrorResponse, createOptionsResponse } from '../utils'
|
||||
import { createErrorResponse, createOptionsResponse } from '@/app/api/files/utils'
|
||||
|
||||
const logger = createLogger('PresignedUploadAPI')
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
FileNotFoundError,
|
||||
findLocalFile,
|
||||
getContentType,
|
||||
} from '../../utils'
|
||||
} from '@/app/api/files/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
|
||||
@@ -513,7 +513,6 @@ export async function POST(req: NextRequest) {
|
||||
// } else {
|
||||
logger.info(`[${requestId}] Using VM for code execution`, {
|
||||
resolvedCode,
|
||||
executionParams,
|
||||
hasEnvVars: Object.keys(envVars).length > 0,
|
||||
})
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from '@/lib/billing/validation/seat-management'
|
||||
import { sendEmail } from '@/lib/email/mailer'
|
||||
import { validateAndNormalizeEmail } from '@/lib/email/utils'
|
||||
import { env } from '@/lib/env'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { hasWorkspaceAdminAccess } from '@/lib/permissions/utils'
|
||||
import { db } from '@/db'
|
||||
@@ -344,7 +345,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
organizationEntry[0]?.name || 'organization',
|
||||
role,
|
||||
workspaceInvitationsWithNames,
|
||||
`${process.env.NEXT_PUBLIC_BASE_URL}/api/organizations/invitations/accept?id=${orgInvitation.id}`
|
||||
`${env.NEXT_PUBLIC_APP_URL}/api/organizations/invitations/accept?id=${orgInvitation.id}`
|
||||
)
|
||||
|
||||
emailResult = await sendEmail({
|
||||
@@ -357,7 +358,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
const emailHtml = await renderInvitationEmail(
|
||||
inviter[0]?.name || 'Someone',
|
||||
organizationEntry[0]?.name || 'organization',
|
||||
`${process.env.NEXT_PUBLIC_BASE_URL}/api/organizations/invitations/accept?id=${orgInvitation.id}`,
|
||||
`${env.NEXT_PUBLIC_APP_URL}/api/organizations/invitations/accept?id=${orgInvitation.id}`,
|
||||
email
|
||||
)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { getSession } from '@/lib/auth'
|
||||
import { validateSeatAvailability } from '@/lib/billing/validation/seat-management'
|
||||
import { sendEmail } from '@/lib/email/mailer'
|
||||
import { validateAndNormalizeEmail } from '@/lib/email/utils'
|
||||
import { env } from '@/lib/env'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { db } from '@/db'
|
||||
import { invitation, member, organization, user, userStats } from '@/db/schema'
|
||||
@@ -246,7 +247,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
const emailHtml = await renderInvitationEmail(
|
||||
inviter[0]?.name || 'Someone',
|
||||
organizationEntry[0]?.name || 'organization',
|
||||
`${process.env.NEXT_PUBLIC_BASE_URL}/api/organizations/invitations/accept?id=${invitationId}`,
|
||||
`${env.NEXT_PUBLIC_APP_URL}/api/organizations/invitations/accept?id=${invitationId}`,
|
||||
normalizedEmail
|
||||
)
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { isDev } from '@/lib/environment'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { executeTool } from '@/tools'
|
||||
import { getTool, validateToolRequest } from '@/tools/utils'
|
||||
@@ -51,14 +52,14 @@ const createErrorResponse = (error: any, status = 500, additionalData = {}) => {
|
||||
logger.error('Creating error response', {
|
||||
errorMessage,
|
||||
status,
|
||||
stack: process.env.NODE_ENV === 'development' ? errorStack : undefined,
|
||||
stack: isDev ? errorStack : undefined,
|
||||
})
|
||||
|
||||
return formatResponse(
|
||||
{
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
stack: process.env.NODE_ENV === 'development' ? errorStack : undefined,
|
||||
stack: isDev ? errorStack : undefined,
|
||||
...additionalData,
|
||||
},
|
||||
status
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { env } from '@/lib/env'
|
||||
import { isProd } from '@/lib/environment'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
|
||||
const logger = createLogger('TelemetryAPI')
|
||||
@@ -101,7 +102,7 @@ async function forwardToCollector(data: any): Promise<boolean> {
|
||||
},
|
||||
{
|
||||
key: 'deployment.environment',
|
||||
value: { stringValue: env.NODE_ENV || 'production' },
|
||||
value: { stringValue: isProd ? 'production' : 'development' },
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
132
apps/sim/app/api/tools/wealthbox/item/route.ts
Normal file
132
apps/sim/app/api/tools/wealthbox/item/route.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
import { db } from '@/db'
|
||||
import { account } from '@/db/schema'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('WealthboxItemAPI')
|
||||
|
||||
/**
|
||||
* Get a single item (note, contact, task) from Wealthbox
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
// Get the session
|
||||
const session = await getSession()
|
||||
|
||||
// Check if the user is authenticated
|
||||
if (!session?.user?.id) {
|
||||
logger.warn(`[${requestId}] Unauthenticated request rejected`)
|
||||
return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
|
||||
}
|
||||
|
||||
// Get parameters from query
|
||||
const { searchParams } = new URL(request.url)
|
||||
const credentialId = searchParams.get('credentialId')
|
||||
const itemId = searchParams.get('itemId')
|
||||
const type = searchParams.get('type') || 'note'
|
||||
|
||||
if (!credentialId || !itemId) {
|
||||
logger.warn(`[${requestId}] Missing required parameters`, { credentialId, itemId })
|
||||
return NextResponse.json({ error: 'Credential ID and Item ID are required' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Validate item type
|
||||
if (!['note', 'contact', 'task'].includes(type)) {
|
||||
logger.warn(`[${requestId}] Invalid item type: ${type}`)
|
||||
return NextResponse.json({ error: 'Invalid item type' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Get the credential from the database
|
||||
const credentials = await db.select().from(account).where(eq(account.id, credentialId)).limit(1)
|
||||
|
||||
if (!credentials.length) {
|
||||
logger.warn(`[${requestId}] Credential not found`, { credentialId })
|
||||
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
const credential = credentials[0]
|
||||
|
||||
// Check if the credential belongs to the user
|
||||
if (credential.userId !== session.user.id) {
|
||||
logger.warn(`[${requestId}] Unauthorized credential access attempt`, {
|
||||
credentialUserId: credential.userId,
|
||||
requestUserId: session.user.id,
|
||||
})
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
|
||||
}
|
||||
|
||||
// Refresh access token if needed
|
||||
const accessToken = await refreshAccessTokenIfNeeded(credentialId, session.user.id, requestId)
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error(`[${requestId}] Failed to obtain valid access token`)
|
||||
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
|
||||
}
|
||||
|
||||
// Determine the endpoint based on item type
|
||||
const endpoints = {
|
||||
note: 'notes',
|
||||
contact: 'contacts',
|
||||
task: 'tasks',
|
||||
}
|
||||
const endpoint = endpoints[type as keyof typeof endpoints]
|
||||
|
||||
logger.info(`[${requestId}] Fetching ${type} ${itemId} from Wealthbox`)
|
||||
|
||||
// Make request to Wealthbox API
|
||||
const response = await fetch(`https://api.crmworkspace.com/v1/${endpoint}/${itemId}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error(
|
||||
`[${requestId}] Wealthbox API error: ${response.status} ${response.statusText}`,
|
||||
{
|
||||
error: errorText,
|
||||
endpoint,
|
||||
itemId,
|
||||
}
|
||||
)
|
||||
|
||||
if (response.status === 404) {
|
||||
return NextResponse.json({ error: 'Item not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to fetch ${type} from Wealthbox` },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// Transform the response to match our expected format
|
||||
const item = {
|
||||
id: data.id?.toString() || itemId,
|
||||
name:
|
||||
data.content || data.name || `${data.first_name} ${data.last_name}` || `${type} ${data.id}`,
|
||||
type,
|
||||
content: data.content || '',
|
||||
createdAt: data.created_at,
|
||||
updatedAt: data.updated_at,
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Successfully fetched ${type} ${itemId} from Wealthbox`)
|
||||
|
||||
return NextResponse.json({ item }, { status: 200 })
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error fetching Wealthbox item`, error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
166
apps/sim/app/api/tools/wealthbox/items/route.ts
Normal file
166
apps/sim/app/api/tools/wealthbox/items/route.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
import { db } from '@/db'
|
||||
import { account } from '@/db/schema'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('WealthboxItemsAPI')
|
||||
|
||||
// Interface for transformed Wealthbox items
|
||||
interface WealthboxItem {
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
content: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Get items (notes, contacts, tasks) from Wealthbox
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
const session = await getSession()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
logger.warn(`[${requestId}] Unauthenticated request rejected`)
|
||||
return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const credentialId = searchParams.get('credentialId')
|
||||
const type = searchParams.get('type') || 'contact'
|
||||
const query = searchParams.get('query') || ''
|
||||
|
||||
if (!credentialId) {
|
||||
logger.warn(`[${requestId}] Missing credential ID`)
|
||||
return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (type !== 'contact') {
|
||||
logger.warn(`[${requestId}] Invalid item type: ${type}`)
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid item type. Only contact is supported.' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const credentials = await db.select().from(account).where(eq(account.id, credentialId)).limit(1)
|
||||
|
||||
if (!credentials.length) {
|
||||
logger.warn(`[${requestId}] Credential not found`, { credentialId })
|
||||
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
const credential = credentials[0]
|
||||
|
||||
if (credential.userId !== session.user.id) {
|
||||
logger.warn(`[${requestId}] Unauthorized credential access attempt`, {
|
||||
credentialUserId: credential.userId,
|
||||
requestUserId: session.user.id,
|
||||
})
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
|
||||
}
|
||||
|
||||
const accessToken = await refreshAccessTokenIfNeeded(credentialId, session.user.id, requestId)
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error(`[${requestId}] Failed to obtain valid access token`)
|
||||
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
|
||||
}
|
||||
|
||||
const endpoints = {
|
||||
contact: 'contacts',
|
||||
}
|
||||
const endpoint = endpoints[type as keyof typeof endpoints]
|
||||
|
||||
const url = new URL(`https://api.crmworkspace.com/v1/${endpoint}`)
|
||||
|
||||
logger.info(`[${requestId}] Fetching ${type}s from Wealthbox`, {
|
||||
endpoint,
|
||||
url: url.toString(),
|
||||
hasQuery: !!query.trim(),
|
||||
})
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error(
|
||||
`[${requestId}] Wealthbox API error: ${response.status} ${response.statusText}`,
|
||||
{
|
||||
error: errorText,
|
||||
endpoint,
|
||||
url: url.toString(),
|
||||
}
|
||||
)
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to fetch ${type}s from Wealthbox` },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
logger.info(`[${requestId}] Wealthbox API raw response`, {
|
||||
type,
|
||||
status: response.status,
|
||||
dataKeys: Object.keys(data || {}),
|
||||
hasContacts: !!data.contacts,
|
||||
dataStructure: typeof data === 'object' ? Object.keys(data) : 'not an object',
|
||||
})
|
||||
|
||||
let items: WealthboxItem[] = []
|
||||
|
||||
if (type === 'contact') {
|
||||
const contacts = data.contacts || []
|
||||
if (!Array.isArray(contacts)) {
|
||||
logger.warn(`[${requestId}] Contacts is not an array`, {
|
||||
contacts,
|
||||
dataType: typeof contacts,
|
||||
})
|
||||
return NextResponse.json({ items: [] }, { status: 200 })
|
||||
}
|
||||
|
||||
items = contacts.map((item: any) => ({
|
||||
id: item.id?.toString() || '',
|
||||
name: `${item.first_name || ''} ${item.last_name || ''}`.trim() || `Contact ${item.id}`,
|
||||
type: 'contact',
|
||||
content: item.background_information || '',
|
||||
createdAt: item.created_at,
|
||||
updatedAt: item.updated_at,
|
||||
}))
|
||||
}
|
||||
|
||||
if (query.trim()) {
|
||||
const searchTerm = query.trim().toLowerCase()
|
||||
items = items.filter(
|
||||
(item) =>
|
||||
item.name.toLowerCase().includes(searchTerm) ||
|
||||
item.content.toLowerCase().includes(searchTerm)
|
||||
)
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Successfully fetched ${items.length} ${type}s from Wealthbox`, {
|
||||
totalItems: items.length,
|
||||
hasSearchQuery: !!query.trim(),
|
||||
})
|
||||
|
||||
return NextResponse.json({ items }, { status: 200 })
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error fetching Wealthbox items`, error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import crypto from 'crypto'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { env } from '@/lib/env'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { saveWorkflowToNormalizedTables } from '@/lib/workflows/db-helpers'
|
||||
import { db } from '@/db'
|
||||
@@ -88,7 +89,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
|
||||
// Notify socket server about the revert operation for real-time sync
|
||||
try {
|
||||
const socketServerUrl = process.env.SOCKET_SERVER_URL || 'http://localhost:3002'
|
||||
const socketServerUrl = env.SOCKET_SERVER_URL || 'http://localhost:3002'
|
||||
await fetch(`${socketServerUrl}/api/workflow-reverted`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
||||
@@ -328,124 +328,27 @@ export default function ChatClient({ subdomain }: { subdomain: string }) {
|
||||
throw new Error('Response body is missing')
|
||||
}
|
||||
|
||||
const messageIdMap = new Map<string, string>()
|
||||
// Use the streaming hook with audio support
|
||||
const shouldPlayAudio = isVoiceInput || isVoiceFirstMode
|
||||
const audioHandler = shouldPlayAudio
|
||||
? createAudioStreamHandler(streamTextToAudio, DEFAULT_VOICE_SETTINGS.voiceId)
|
||||
: undefined
|
||||
|
||||
// Get reader with proper cleanup
|
||||
const reader = response.body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
|
||||
const processStream = async () => {
|
||||
let streamAborted = false
|
||||
|
||||
// Add cleanup handler for abort
|
||||
const cleanup = () => {
|
||||
streamAborted = true
|
||||
try {
|
||||
reader.releaseLock()
|
||||
} catch (error) {
|
||||
// Reader might already be released
|
||||
logger.debug('Reader already released:', error)
|
||||
}
|
||||
setIsLoading(false)
|
||||
await handleStreamedResponse(
|
||||
response,
|
||||
setMessages,
|
||||
setIsLoading,
|
||||
scrollToBottom,
|
||||
userHasScrolled,
|
||||
{
|
||||
voiceSettings: {
|
||||
isVoiceEnabled: shouldPlayAudio,
|
||||
voiceId: DEFAULT_VOICE_SETTINGS.voiceId,
|
||||
autoPlayResponses: shouldPlayAudio,
|
||||
},
|
||||
audioStreamHandler: audioHandler,
|
||||
}
|
||||
|
||||
// Listen for abort events
|
||||
abortController.signal.addEventListener('abort', cleanup)
|
||||
|
||||
try {
|
||||
while (!streamAborted) {
|
||||
const { done, value } = await reader.read()
|
||||
|
||||
if (done) {
|
||||
setIsLoading(false)
|
||||
break
|
||||
}
|
||||
|
||||
if (streamAborted) {
|
||||
break
|
||||
}
|
||||
|
||||
const chunk = decoder.decode(value, { stream: true })
|
||||
const lines = chunk.split('\n\n')
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
try {
|
||||
const json = JSON.parse(line.substring(6))
|
||||
const { blockId, chunk: contentChunk, event: eventType } = json
|
||||
|
||||
if (eventType === 'final' && json.data) {
|
||||
setIsLoading(false)
|
||||
|
||||
// Process final execution result for field extraction
|
||||
const result = json.data
|
||||
const nonStreamingLogs =
|
||||
result.logs?.filter((log: any) => !messageIdMap.has(log.blockId)) || []
|
||||
|
||||
// Chat field extraction will be handled by the backend using deployment outputConfigs
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (blockId && contentChunk) {
|
||||
if (!messageIdMap.has(blockId)) {
|
||||
const newMessageId = crypto.randomUUID()
|
||||
messageIdMap.set(blockId, newMessageId)
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: newMessageId,
|
||||
content: contentChunk,
|
||||
type: 'assistant',
|
||||
timestamp: new Date(),
|
||||
isStreaming: true,
|
||||
},
|
||||
])
|
||||
} else {
|
||||
const messageId = messageIdMap.get(blockId)
|
||||
if (messageId) {
|
||||
setMessages((prev) =>
|
||||
prev.map((msg) =>
|
||||
msg.id === messageId
|
||||
? { ...msg, content: msg.content + contentChunk }
|
||||
: msg
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (blockId && eventType === 'end') {
|
||||
const messageId = messageIdMap.get(blockId)
|
||||
if (messageId) {
|
||||
setMessages((prev) =>
|
||||
prev.map((msg) =>
|
||||
msg.id === messageId ? { ...msg, isStreaming: false } : msg
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (parseError) {
|
||||
logger.error('Error parsing stream data:', parseError)
|
||||
// Continue processing other lines even if one fails
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (streamError: unknown) {
|
||||
if (streamError instanceof Error && streamError.name === 'AbortError') {
|
||||
logger.info('Stream processing aborted by user')
|
||||
return
|
||||
}
|
||||
|
||||
logger.error('Error processing stream:', streamError)
|
||||
throw streamError
|
||||
} finally {
|
||||
// Ensure cleanup always happens
|
||||
cleanup()
|
||||
abortController.signal.removeEventListener('abort', cleanup)
|
||||
}
|
||||
}
|
||||
|
||||
await processStream()
|
||||
)
|
||||
} catch (error: any) {
|
||||
// Clear timeout in case of error
|
||||
clearTimeout(timeoutId)
|
||||
|
||||
@@ -75,262 +75,149 @@ export function useChatStreaming() {
|
||||
userHasScrolled?: boolean,
|
||||
streamingOptions?: StreamingOptions
|
||||
) => {
|
||||
const messageId = crypto.randomUUID()
|
||||
|
||||
// Set streaming state before adding the assistant message
|
||||
// Set streaming state
|
||||
setIsStreamingResponse(true)
|
||||
|
||||
// Reset refs
|
||||
accumulatedTextRef.current = ''
|
||||
lastStreamedPositionRef.current = 0
|
||||
lastDisplayedPositionRef.current = 0
|
||||
audioStreamingActiveRef.current = false
|
||||
abortControllerRef.current = new AbortController()
|
||||
|
||||
// Check if we should stream audio
|
||||
const shouldStreamAudio =
|
||||
const shouldPlayAudio =
|
||||
streamingOptions?.voiceSettings?.isVoiceEnabled &&
|
||||
streamingOptions?.voiceSettings?.autoPlayResponses &&
|
||||
streamingOptions?.audioStreamHandler
|
||||
|
||||
// Get voice-first mode settings
|
||||
const voiceFirstMode = streamingOptions?.voiceSettings?.voiceFirstMode
|
||||
const textStreamingMode = streamingOptions?.voiceSettings?.textStreamingInVoiceMode || 'normal'
|
||||
const conversationMode = streamingOptions?.voiceSettings?.conversationMode
|
||||
const reader = response.body?.getReader()
|
||||
if (!reader) {
|
||||
setIsLoading(false)
|
||||
setIsStreamingResponse(false)
|
||||
return
|
||||
}
|
||||
|
||||
// In voice-first mode with hidden text, don't show text at all
|
||||
const shouldShowText = !voiceFirstMode || textStreamingMode !== 'hidden'
|
||||
const decoder = new TextDecoder()
|
||||
let accumulatedText = ''
|
||||
let lastAudioPosition = 0
|
||||
|
||||
// Add placeholder message
|
||||
const messageId = crypto.randomUUID()
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: messageId,
|
||||
content: shouldShowText ? '' : '🎵 Generating audio response...',
|
||||
content: '',
|
||||
type: 'assistant',
|
||||
timestamp: new Date(),
|
||||
isStreaming: true,
|
||||
isVoiceOnly: voiceFirstMode && textStreamingMode === 'hidden',
|
||||
},
|
||||
])
|
||||
|
||||
// Stop showing loading indicator once streaming begins
|
||||
setIsLoading(false)
|
||||
|
||||
// Start audio if in voice mode
|
||||
if (shouldStreamAudio) {
|
||||
streamingOptions.onAudioStart?.()
|
||||
audioStreamingActiveRef.current = true
|
||||
}
|
||||
try {
|
||||
while (true) {
|
||||
// Check if aborted
|
||||
if (abortControllerRef.current === null) {
|
||||
break
|
||||
}
|
||||
|
||||
// Helper function to update displayed text based on mode
|
||||
const updateDisplayedText = (fullText: string, audioPosition?: number) => {
|
||||
let displayText = fullText
|
||||
const { done, value } = await reader.read()
|
||||
|
||||
if (voiceFirstMode && textStreamingMode === 'synced') {
|
||||
// Only show text up to where audio has been streamed
|
||||
displayText = fullText.substring(0, audioPosition || lastStreamedPositionRef.current)
|
||||
} else if (voiceFirstMode && textStreamingMode === 'hidden') {
|
||||
// Don't update text content, keep voice indicator
|
||||
return
|
||||
}
|
||||
|
||||
setMessages((prev) =>
|
||||
prev.map((msg) => {
|
||||
if (msg.id === messageId) {
|
||||
return {
|
||||
...msg,
|
||||
content: displayText,
|
||||
if (done) {
|
||||
// Stream any remaining text for TTS
|
||||
if (
|
||||
shouldPlayAudio &&
|
||||
streamingOptions?.audioStreamHandler &&
|
||||
accumulatedText.length > lastAudioPosition
|
||||
) {
|
||||
const remainingText = accumulatedText.substring(lastAudioPosition).trim()
|
||||
if (remainingText) {
|
||||
try {
|
||||
await streamingOptions.audioStreamHandler(remainingText)
|
||||
} catch (error) {
|
||||
logger.error('TTS error for remaining text:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
return msg
|
||||
})
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// Helper function to clean up after streaming ends (success or error)
|
||||
const cleanupStreaming = (messageContent?: string, appendContent = false) => {
|
||||
// Reset streaming state and controller
|
||||
const chunk = decoder.decode(value, { stream: true })
|
||||
const lines = chunk.split('\n\n')
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
try {
|
||||
const json = JSON.parse(line.substring(6))
|
||||
const { blockId, chunk: contentChunk, event: eventType } = json
|
||||
|
||||
if (eventType === 'final' && json.data) {
|
||||
setMessages((prev) =>
|
||||
prev.map((msg) => (msg.id === messageId ? { ...msg, isStreaming: false } : msg))
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (blockId && contentChunk) {
|
||||
accumulatedText += contentChunk
|
||||
setMessages((prev) =>
|
||||
prev.map((msg) =>
|
||||
msg.id === messageId ? { ...msg, content: accumulatedText } : msg
|
||||
)
|
||||
)
|
||||
|
||||
// Real-time TTS for voice mode
|
||||
if (shouldPlayAudio && streamingOptions?.audioStreamHandler) {
|
||||
const newText = accumulatedText.substring(lastAudioPosition)
|
||||
const sentenceEndings = ['. ', '! ', '? ', '.\n', '!\n', '?\n', '.', '!', '?']
|
||||
let sentenceEnd = -1
|
||||
|
||||
for (const ending of sentenceEndings) {
|
||||
const index = newText.indexOf(ending)
|
||||
if (index > 0) {
|
||||
sentenceEnd = index + ending.length
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (sentenceEnd > 0) {
|
||||
const sentence = newText.substring(0, sentenceEnd).trim()
|
||||
if (sentence && sentence.length >= 3) {
|
||||
try {
|
||||
await streamingOptions.audioStreamHandler(sentence)
|
||||
lastAudioPosition += sentenceEnd
|
||||
} catch (error) {
|
||||
logger.error('TTS error:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (blockId && eventType === 'end') {
|
||||
setMessages((prev) =>
|
||||
prev.map((msg) => (msg.id === messageId ? { ...msg, isStreaming: false } : msg))
|
||||
)
|
||||
}
|
||||
} catch (parseError) {
|
||||
logger.error('Error parsing stream data:', parseError)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error processing stream:', error)
|
||||
setMessages((prev) =>
|
||||
prev.map((msg) => (msg.id === messageId ? { ...msg, isStreaming: false } : msg))
|
||||
)
|
||||
} finally {
|
||||
setIsStreamingResponse(false)
|
||||
abortControllerRef.current = null
|
||||
accumulatedTextRef.current = ''
|
||||
lastStreamedPositionRef.current = 0
|
||||
lastDisplayedPositionRef.current = 0
|
||||
audioStreamingActiveRef.current = false
|
||||
|
||||
// Update message content and remove isStreaming flag
|
||||
setMessages((prev) =>
|
||||
prev.map((msg) => {
|
||||
if (msg.id === messageId) {
|
||||
return {
|
||||
...msg,
|
||||
content: appendContent
|
||||
? msg.content + (messageContent || '')
|
||||
: messageContent || msg.content,
|
||||
isStreaming: false,
|
||||
isVoiceOnly: false,
|
||||
}
|
||||
}
|
||||
return msg
|
||||
})
|
||||
)
|
||||
|
||||
// Only scroll to bottom if user hasn't manually scrolled
|
||||
if (!userHasScrolled) {
|
||||
setTimeout(() => {
|
||||
scrollToBottom()
|
||||
}, 300)
|
||||
}
|
||||
|
||||
// End audio streaming
|
||||
if (shouldStreamAudio) {
|
||||
streamingOptions.onAudioEnd?.()
|
||||
if (shouldPlayAudio) {
|
||||
streamingOptions?.onAudioEnd?.()
|
||||
}
|
||||
}
|
||||
|
||||
// Check if response body exists and is a ReadableStream
|
||||
if (!response.body) {
|
||||
cleanupStreaming("Error: Couldn't receive streaming response from server.")
|
||||
return
|
||||
}
|
||||
|
||||
const reader = response.body.getReader()
|
||||
if (reader) {
|
||||
const decoder = new TextDecoder()
|
||||
let done = false
|
||||
|
||||
try {
|
||||
while (!done) {
|
||||
// Check if aborted before awaiting reader.read()
|
||||
if (abortControllerRef.current === null) {
|
||||
break
|
||||
}
|
||||
|
||||
const { value, done: readerDone } = await reader.read()
|
||||
done = readerDone
|
||||
|
||||
if (value) {
|
||||
const chunk = decoder.decode(value, { stream: true })
|
||||
if (chunk) {
|
||||
// Accumulate text
|
||||
accumulatedTextRef.current += chunk
|
||||
|
||||
// Update the message with the accumulated text based on mode
|
||||
if (shouldShowText) {
|
||||
updateDisplayedText(accumulatedTextRef.current)
|
||||
}
|
||||
|
||||
// Stream audio in real-time for meaningful sentences
|
||||
if (
|
||||
shouldStreamAudio &&
|
||||
streamingOptions.audioStreamHandler &&
|
||||
audioStreamingActiveRef.current
|
||||
) {
|
||||
const newText = accumulatedTextRef.current.substring(
|
||||
lastStreamedPositionRef.current
|
||||
)
|
||||
|
||||
// Use sentence-based streaming for natural audio flow
|
||||
const sentenceEndings = ['. ', '! ', '? ', '.\n', '!\n', '?\n', '.', '!', '?']
|
||||
let sentenceEnd = -1
|
||||
|
||||
// Find the first complete sentence
|
||||
for (const ending of sentenceEndings) {
|
||||
const index = newText.indexOf(ending)
|
||||
if (index > 0) {
|
||||
// Make sure we include the punctuation
|
||||
sentenceEnd = index + ending.length
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If we found a complete sentence, stream it
|
||||
if (sentenceEnd > 0) {
|
||||
const sentence = newText.substring(0, sentenceEnd).trim()
|
||||
if (sentence && sentence.length >= 3) {
|
||||
// Only send meaningful sentences
|
||||
try {
|
||||
// Stream this sentence to audio
|
||||
await streamingOptions.audioStreamHandler(sentence)
|
||||
lastStreamedPositionRef.current += sentenceEnd
|
||||
|
||||
// Update displayed text in synced mode
|
||||
if (voiceFirstMode && textStreamingMode === 'synced') {
|
||||
updateDisplayedText(
|
||||
accumulatedTextRef.current,
|
||||
lastStreamedPositionRef.current
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error streaming audio sentence:', error)
|
||||
// Don't stop on individual sentence errors, but log them
|
||||
if (error instanceof Error && error.message.includes('401')) {
|
||||
logger.warn('TTS authentication error, stopping audio streaming')
|
||||
audioStreamingActiveRef.current = false
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (newText.length > 200 && done) {
|
||||
// If streaming has ended and we have a long incomplete sentence, stream it anyway
|
||||
const incompleteSentence = newText.trim()
|
||||
if (incompleteSentence && incompleteSentence.length >= 10) {
|
||||
try {
|
||||
await streamingOptions.audioStreamHandler(incompleteSentence)
|
||||
lastStreamedPositionRef.current += newText.length
|
||||
|
||||
if (voiceFirstMode && textStreamingMode === 'synced') {
|
||||
updateDisplayedText(
|
||||
accumulatedTextRef.current,
|
||||
lastStreamedPositionRef.current
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error streaming incomplete sentence:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle any remaining text for audio streaming when streaming completes
|
||||
if (
|
||||
shouldStreamAudio &&
|
||||
streamingOptions.audioStreamHandler &&
|
||||
audioStreamingActiveRef.current
|
||||
) {
|
||||
const remainingText = accumulatedTextRef.current
|
||||
.substring(lastStreamedPositionRef.current)
|
||||
.trim()
|
||||
if (remainingText && remainingText.length >= 3) {
|
||||
try {
|
||||
await streamingOptions.audioStreamHandler(remainingText)
|
||||
|
||||
// Final update for synced mode
|
||||
if (voiceFirstMode && textStreamingMode === 'synced') {
|
||||
updateDisplayedText(accumulatedTextRef.current, accumulatedTextRef.current.length)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error streaming final remaining text:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Show error to user in the message
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Unknown error during streaming'
|
||||
logger.error('Error reading stream:', error)
|
||||
cleanupStreaming(`\n\n_Error: ${errorMessage}_`, true)
|
||||
return // Skip the finally block's cleanupStreaming call
|
||||
} finally {
|
||||
// Don't call cleanupStreaming here if we already called it in the catch block
|
||||
if (abortControllerRef.current !== null) {
|
||||
cleanupStreaming()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cleanupStreaming("Error: Couldn't process streaming response.")
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { getNodeEnv } from '@/lib/environment'
|
||||
import { isDev } from '@/lib/environment'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { useGeneralStore } from '@/stores/settings/general/store'
|
||||
|
||||
@@ -50,7 +50,6 @@ export function TelemetryConsentDialog() {
|
||||
const loadSettings = useGeneralStore((state) => state.loadSettings)
|
||||
|
||||
const hasShownDialogThisSession = useRef(false)
|
||||
const isDevelopment = getNodeEnv() === 'development'
|
||||
|
||||
const isChatSubdomainOrPath =
|
||||
typeof window !== 'undefined' &&
|
||||
@@ -116,7 +115,7 @@ export function TelemetryConsentDialog() {
|
||||
telemetryNotifiedUser,
|
||||
telemetryEnabled,
|
||||
hasShownInSession: hasShownDialogThisSession.current,
|
||||
environment: getNodeEnv(),
|
||||
environment: isDev,
|
||||
})
|
||||
|
||||
const localStorageNotified =
|
||||
@@ -134,11 +133,11 @@ export function TelemetryConsentDialog() {
|
||||
!localStorageNotified &&
|
||||
telemetryEnabled &&
|
||||
!hasShownDialogThisSession.current &&
|
||||
isDevelopment
|
||||
isDev
|
||||
) {
|
||||
setOpen(true)
|
||||
hasShownDialogThisSession.current = true
|
||||
} else if (settingsLoaded && !telemetryNotifiedUser && !isDevelopment) {
|
||||
} else if (settingsLoaded && !telemetryNotifiedUser && !isDev) {
|
||||
// Auto-notify in non-development environments
|
||||
setTelemetryNotifiedUser(true)
|
||||
if (typeof window !== 'undefined') {
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
ConditionalIcon,
|
||||
ConnectIcon,
|
||||
} from '@/components/icons'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { cn, redactApiKeys } from '@/lib/utils'
|
||||
import type { TraceSpan } from '../../stores/types'
|
||||
|
||||
interface TraceSpansDisplayProps {
|
||||
@@ -25,24 +25,7 @@ function transformBlockData(data: any, blockType: string, isInput: boolean) {
|
||||
|
||||
// For input data, filter out sensitive information
|
||||
if (isInput) {
|
||||
const cleanInput = { ...data }
|
||||
|
||||
// Remove sensitive fields (common API keys and tokens)
|
||||
if (cleanInput.apiKey) {
|
||||
cleanInput.apiKey = '***'
|
||||
}
|
||||
if (cleanInput.azureApiKey) {
|
||||
cleanInput.azureApiKey = '***'
|
||||
}
|
||||
if (cleanInput.token) {
|
||||
cleanInput.token = '***'
|
||||
}
|
||||
if (cleanInput.accessToken) {
|
||||
cleanInput.accessToken = '***'
|
||||
}
|
||||
if (cleanInput.authorization) {
|
||||
cleanInput.authorization = '***'
|
||||
}
|
||||
const cleanInput = redactApiKeys(data)
|
||||
|
||||
// Remove null/undefined values for cleaner display
|
||||
Object.keys(cleanInput).forEach((key) => {
|
||||
@@ -112,7 +95,7 @@ function CollapsibleInputOutput({ span, spanId }: CollapsibleInputOutputProps) {
|
||||
<div>
|
||||
<button
|
||||
onClick={() => setInputExpanded(!inputExpanded)}
|
||||
className='flex items-center gap-2 mb-2 font-medium text-muted-foreground text-xs hover:text-foreground transition-colors'
|
||||
className='mb-2 flex items-center gap-2 font-medium text-muted-foreground text-xs transition-colors hover:text-foreground'
|
||||
>
|
||||
{inputExpanded ? (
|
||||
<ChevronDown className='h-3 w-3' />
|
||||
@@ -134,7 +117,7 @@ function CollapsibleInputOutput({ span, spanId }: CollapsibleInputOutputProps) {
|
||||
<div>
|
||||
<button
|
||||
onClick={() => setOutputExpanded(!outputExpanded)}
|
||||
className='flex items-center gap-2 mb-2 font-medium text-muted-foreground text-xs hover:text-foreground transition-colors'
|
||||
className='mb-2 flex items-center gap-2 font-medium text-muted-foreground text-xs transition-colors hover:text-foreground'
|
||||
>
|
||||
{outputExpanded ? (
|
||||
<ChevronDown className='h-3 w-3' />
|
||||
|
||||
@@ -30,7 +30,7 @@ import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { env } from '@/lib/env'
|
||||
import { isDev } from '@/lib/environment'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { getBaseDomain } from '@/lib/urls/utils'
|
||||
import { cn } from '@/lib/utils'
|
||||
@@ -55,7 +55,7 @@ interface ChatDeployProps {
|
||||
type AuthType = 'public' | 'password' | 'email'
|
||||
|
||||
const getDomainSuffix = (() => {
|
||||
const suffix = env.NODE_ENV === 'development' ? `.${getBaseDomain()}` : '.simstudio.ai'
|
||||
const suffix = isDev ? `.${getBaseDomain()}` : '.simstudio.ai'
|
||||
return () => suffix
|
||||
})()
|
||||
|
||||
|
||||
@@ -7,17 +7,21 @@ import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/w/compo
|
||||
|
||||
interface ConnectionStatusProps {
|
||||
isConnected: boolean
|
||||
hasOperationError?: boolean
|
||||
}
|
||||
|
||||
export function ConnectionStatus({ isConnected }: ConnectionStatusProps) {
|
||||
export function ConnectionStatus({ isConnected, hasOperationError }: ConnectionStatusProps) {
|
||||
const userPermissions = useUserPermissionsContext()
|
||||
|
||||
const handleRefresh = () => {
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
// Don't render anything if not in offline mode
|
||||
if (!userPermissions.isOfflineMode) {
|
||||
// Show error if either offline mode OR operation error
|
||||
const shouldShowError = userPermissions.isOfflineMode || hasOperationError
|
||||
|
||||
// Don't render anything if no errors
|
||||
if (!shouldShowError) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -32,10 +36,14 @@ export function ConnectionStatus({ isConnected }: ConnectionStatusProps) {
|
||||
</div>
|
||||
<div className='flex flex-col'>
|
||||
<span className='font-medium text-xs leading-tight'>
|
||||
{isConnected ? 'Reconnected' : 'Connection lost - please refresh'}
|
||||
{hasOperationError
|
||||
? 'Workflow Edit Failed'
|
||||
: isConnected
|
||||
? 'Reconnected'
|
||||
: 'Connection lost - please refresh'}
|
||||
</span>
|
||||
<span className='text-red-600 text-xs leading-tight'>
|
||||
{isConnected ? 'Refresh to continue editing' : 'Read-only mode active'}
|
||||
Please refresh to continue editing
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useMemo } from 'react'
|
||||
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
|
||||
import { usePresence } from '../../../../hooks/use-presence'
|
||||
import { ConnectionStatus } from './components/connection-status/connection-status'
|
||||
import { UserAvatar } from './components/user-avatar/user-avatar'
|
||||
@@ -29,6 +30,9 @@ export function UserAvatarStack({
|
||||
const { users: presenceUsers, isConnected } = usePresence()
|
||||
const users = propUsers || presenceUsers
|
||||
|
||||
// Get operation error state from collaborative workflow
|
||||
const { hasOperationError } = useCollaborativeWorkflow()
|
||||
|
||||
// Memoize the processed users to avoid unnecessary re-renders
|
||||
const { visibleUsers, overflowCount } = useMemo(() => {
|
||||
if (users.length === 0) {
|
||||
@@ -53,8 +57,8 @@ export function UserAvatarStack({
|
||||
|
||||
return (
|
||||
<div className={`flex items-center gap-3 ${className}`}>
|
||||
{/* Connection status - always check, shows when offline */}
|
||||
<ConnectionStatus isConnected={isConnected} />
|
||||
{/* Connection status - always check, shows when offline or operation errors */}
|
||||
<ConnectionStatus isConnected={isConnected} hasOperationError={hasOperationError} />
|
||||
|
||||
{/* Only show avatar stack when there are multiple users (>1) */}
|
||||
{users.length > 1 && (
|
||||
|
||||
@@ -260,7 +260,7 @@ export function LoopBadges({ nodeId, data }: LoopBadgesProps) {
|
||||
<div className='relative min-h-[80px] rounded-md border border-input bg-background px-3 pt-2 pb-3 font-mono text-sm'>
|
||||
{editorValue === '' && (
|
||||
<div className='pointer-events-none absolute top-[8.5px] left-3 select-none text-muted-foreground/50'>
|
||||
["item1", "item2", "item3"]
|
||||
['item1', 'item2', 'item3']
|
||||
</div>
|
||||
)}
|
||||
<Editor
|
||||
|
||||
@@ -51,9 +51,11 @@ export function ComboBox({
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [cursorPosition, setCursorPosition] = useState(0)
|
||||
const [activeSourceBlockId, setActiveSourceBlockId] = useState<string | null>(null)
|
||||
const [highlightedIndex, setHighlightedIndex] = useState(-1)
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const overlayRef = useRef<HTMLDivElement>(null)
|
||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||
const reactFlowInstance = useReactFlow()
|
||||
|
||||
// Use preview value when in preview mode, otherwise use store value or prop value
|
||||
@@ -167,6 +169,7 @@ export function ComboBox({
|
||||
setStoreValue(selectedValue)
|
||||
}
|
||||
setOpen(false)
|
||||
setHighlightedIndex(-1)
|
||||
inputRef.current?.blur()
|
||||
}
|
||||
|
||||
@@ -184,9 +187,10 @@ export function ComboBox({
|
||||
const handleFocus = () => {
|
||||
setIsFocused(true)
|
||||
setOpen(true)
|
||||
setHighlightedIndex(-1)
|
||||
}
|
||||
|
||||
const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
||||
const handleBlur = () => {
|
||||
setIsFocused(false)
|
||||
setShowEnvVars(false)
|
||||
setShowTags(false)
|
||||
@@ -196,6 +200,7 @@ export function ComboBox({
|
||||
const activeElement = document.activeElement
|
||||
if (!activeElement || !activeElement.closest('.absolute.top-full')) {
|
||||
setOpen(false)
|
||||
setHighlightedIndex(-1)
|
||||
}
|
||||
}, 150)
|
||||
}
|
||||
@@ -205,12 +210,33 @@ export function ComboBox({
|
||||
setShowEnvVars(false)
|
||||
setShowTags(false)
|
||||
setOpen(false)
|
||||
setHighlightedIndex(-1)
|
||||
return
|
||||
}
|
||||
|
||||
if (e.key === 'ArrowDown' && !open) {
|
||||
setOpen(true)
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault()
|
||||
if (!open) {
|
||||
setOpen(true)
|
||||
setHighlightedIndex(0)
|
||||
} else {
|
||||
setHighlightedIndex((prev) => (prev < filteredOptions.length - 1 ? prev + 1 : 0))
|
||||
}
|
||||
}
|
||||
|
||||
if (e.key === 'ArrowUp') {
|
||||
e.preventDefault()
|
||||
if (open) {
|
||||
setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : filteredOptions.length - 1))
|
||||
}
|
||||
}
|
||||
|
||||
if (e.key === 'Enter' && open && highlightedIndex >= 0) {
|
||||
e.preventDefault()
|
||||
const selectedOption = filteredOptions[highlightedIndex]
|
||||
if (selectedOption) {
|
||||
handleSelect(getOptionValue(selectedOption))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,6 +341,31 @@ export function ComboBox({
|
||||
}
|
||||
}, [value])
|
||||
|
||||
// Reset highlighted index when filtered options change, but preserve if within bounds
|
||||
useEffect(() => {
|
||||
setHighlightedIndex((prev) => {
|
||||
if (prev >= 0 && prev < filteredOptions.length) {
|
||||
return prev
|
||||
}
|
||||
return -1
|
||||
})
|
||||
}, [filteredOptions])
|
||||
|
||||
// Scroll highlighted option into view
|
||||
useEffect(() => {
|
||||
if (highlightedIndex >= 0 && dropdownRef.current) {
|
||||
const highlightedElement = dropdownRef.current.querySelector(
|
||||
`[data-option-index="${highlightedIndex}"]`
|
||||
)
|
||||
if (highlightedElement) {
|
||||
highlightedElement.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'nearest',
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [highlightedIndex])
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
const target = event.target as Element
|
||||
@@ -325,6 +376,7 @@ export function ComboBox({
|
||||
!target.closest('.absolute.top-full')
|
||||
) {
|
||||
setOpen(false)
|
||||
setHighlightedIndex(-1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,6 +446,7 @@ export function ComboBox({
|
||||
<div className='absolute top-full left-0 z-[100] mt-1 w-full min-w-[286px]'>
|
||||
<div className='allow-scroll fade-in-0 zoom-in-95 animate-in rounded-md border bg-popover text-popover-foreground shadow-lg'>
|
||||
<div
|
||||
ref={dropdownRef}
|
||||
className='allow-scroll max-h-48 overflow-y-auto p-1'
|
||||
style={{ scrollbarWidth: 'thin' }}
|
||||
>
|
||||
@@ -402,7 +455,7 @@ export function ComboBox({
|
||||
No matching options found.
|
||||
</div>
|
||||
) : (
|
||||
filteredOptions.map((option) => {
|
||||
filteredOptions.map((option, index) => {
|
||||
const optionValue = getOptionValue(option)
|
||||
const optionLabel = getOptionLabel(option)
|
||||
const OptionIcon =
|
||||
@@ -410,16 +463,22 @@ export function ComboBox({
|
||||
? (option.icon as React.ComponentType<{ className?: string }>)
|
||||
: null
|
||||
const isSelected = displayValue === optionValue || displayValue === optionLabel
|
||||
const isHighlighted = index === highlightedIndex
|
||||
|
||||
return (
|
||||
<div
|
||||
key={optionValue}
|
||||
data-option-index={index}
|
||||
onClick={() => handleSelect(optionValue)}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault()
|
||||
handleSelect(optionValue)
|
||||
}}
|
||||
className='relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground'
|
||||
onMouseEnter={() => setHighlightedIndex(index)}
|
||||
className={cn(
|
||||
'relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground',
|
||||
isHighlighted && 'bg-accent text-accent-foreground'
|
||||
)}
|
||||
>
|
||||
{OptionIcon && <OptionIcon className='mr-2 h-3 w-3 opacity-60' />}
|
||||
<span className='flex-1 truncate'>{optionLabel}</span>
|
||||
|
||||
@@ -105,7 +105,7 @@ const SCOPE_DESCRIPTIONS: Record<string, string> = {
|
||||
'messages.read': 'Read your Discord messages',
|
||||
guilds: 'Read your Discord guilds',
|
||||
'guilds.members.read': 'Read your Discord guild members',
|
||||
read: 'Read access to your Linear workspace',
|
||||
read: 'Read access to your workspace',
|
||||
write: 'Write access to your Linear workspace',
|
||||
'channels:read': 'Read your Slack channels',
|
||||
'groups:read': 'Read your Slack private channels',
|
||||
|
||||
@@ -113,8 +113,8 @@ export function Dropdown({
|
||||
}}
|
||||
disabled={isPreview || disabled}
|
||||
>
|
||||
<SelectTrigger className='text-left'>
|
||||
<SelectValue placeholder='Select an option' />
|
||||
<SelectTrigger className='min-w-0 text-left'>
|
||||
<SelectValue placeholder='Select an option' className='truncate' />
|
||||
</SelectTrigger>
|
||||
<SelectContent className='max-h-48'>
|
||||
{evaluatedOptions.map((option) => (
|
||||
|
||||
@@ -376,20 +376,22 @@ export function ConfluenceFileSelector({
|
||||
variant='outline'
|
||||
role='combobox'
|
||||
aria-expanded={open}
|
||||
className='w-full justify-between'
|
||||
className='h-10 w-full min-w-0 justify-between'
|
||||
disabled={disabled || !domain}
|
||||
>
|
||||
{selectedFile ? (
|
||||
<div className='flex items-center gap-2 overflow-hidden'>
|
||||
<ConfluenceIcon className='h-4 w-4' />
|
||||
<span className='truncate font-normal'>{selectedFile.name}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className='flex items-center gap-2'>
|
||||
<ConfluenceIcon className='h-4 w-4' />
|
||||
<span className='text-muted-foreground'>{label}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className='flex min-w-0 items-center gap-2 overflow-hidden'>
|
||||
{selectedFile ? (
|
||||
<>
|
||||
<ConfluenceIcon className='h-4 w-4' />
|
||||
<span className='truncate font-normal'>{selectedFile.name}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ConfluenceIcon className='h-4 w-4' />
|
||||
<span className='truncate text-muted-foreground'>{label}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<ChevronDown className='ml-2 h-4 w-4 shrink-0 opacity-50' />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
||||
@@ -220,20 +220,22 @@ export function DiscordChannelSelector({
|
||||
variant='outline'
|
||||
role='combobox'
|
||||
aria-expanded={open}
|
||||
className='w-full justify-between'
|
||||
className='h-10 w-full min-w-0 justify-between'
|
||||
disabled={disabled || !botToken || !serverId}
|
||||
>
|
||||
{selectedChannel ? (
|
||||
<div className='flex items-center gap-2 overflow-hidden'>
|
||||
<span className='text-muted-foreground'>#</span>
|
||||
<span className='truncate font-normal'>{selectedChannel.name}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className='flex items-center gap-2'>
|
||||
<DiscordIcon className='h-4 w-4' />
|
||||
<span className='text-muted-foreground'>{label}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className='flex min-w-0 items-center gap-2 overflow-hidden'>
|
||||
{selectedChannel ? (
|
||||
<>
|
||||
<span className='text-muted-foreground'>#</span>
|
||||
<span className='truncate font-normal'>{selectedChannel.name}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<DiscordIcon className='h-4 w-4' />
|
||||
<span className='truncate text-muted-foreground'>{label}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<ChevronDown className='ml-2 h-4 w-4 shrink-0 opacity-50' />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
||||
@@ -206,27 +206,29 @@ export function GoogleCalendarSelector({
|
||||
variant='outline'
|
||||
role='combobox'
|
||||
aria-expanded={open}
|
||||
className='w-full justify-between'
|
||||
className='h-10 w-full min-w-0 justify-between'
|
||||
disabled={disabled || !credentialId}
|
||||
>
|
||||
{selectedCalendar ? (
|
||||
<div className='flex items-center gap-2 overflow-hidden'>
|
||||
<div
|
||||
className='h-3 w-3 flex-shrink-0 rounded-full'
|
||||
style={{
|
||||
backgroundColor: selectedCalendar.backgroundColor || '#4285f4',
|
||||
}}
|
||||
/>
|
||||
<span className='truncate font-normal'>
|
||||
{getCalendarDisplayName(selectedCalendar)}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className='flex items-center gap-2'>
|
||||
<GoogleCalendarIcon className='h-4 w-4' />
|
||||
<span className='text-muted-foreground'>{label}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className='flex min-w-0 items-center gap-2 overflow-hidden'>
|
||||
{selectedCalendar ? (
|
||||
<>
|
||||
<div
|
||||
className='h-3 w-3 flex-shrink-0 rounded-full'
|
||||
style={{
|
||||
backgroundColor: selectedCalendar.backgroundColor || '#4285f4',
|
||||
}}
|
||||
/>
|
||||
<span className='truncate font-normal'>
|
||||
{getCalendarDisplayName(selectedCalendar)}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<GoogleCalendarIcon className='h-4 w-4' />
|
||||
<span className='truncate text-muted-foreground'>{label}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<ChevronDown className='ml-2 h-4 w-4 shrink-0 opacity-50' />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
||||
@@ -432,25 +432,27 @@ export function GoogleDrivePicker({
|
||||
variant='outline'
|
||||
role='combobox'
|
||||
aria-expanded={open}
|
||||
className='w-full justify-between'
|
||||
className='h-10 w-full min-w-0 justify-between'
|
||||
disabled={disabled}
|
||||
>
|
||||
{selectedFile ? (
|
||||
<div className='flex items-center gap-2 overflow-hidden'>
|
||||
{getFileIcon(selectedFile, 'sm')}
|
||||
<span className='truncate font-normal'>{selectedFile.name}</span>
|
||||
</div>
|
||||
) : selectedFileId && isLoadingSelectedFile && selectedCredentialId ? (
|
||||
<div className='flex items-center gap-2'>
|
||||
<RefreshCw className='h-4 w-4 animate-spin' />
|
||||
<span className='text-muted-foreground'>Loading document...</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className='flex items-center gap-2'>
|
||||
{getProviderIcon(provider)}
|
||||
<span className='text-muted-foreground'>{label}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className='flex min-w-0 items-center gap-2 overflow-hidden'>
|
||||
{selectedFile ? (
|
||||
<>
|
||||
{getFileIcon(selectedFile, 'sm')}
|
||||
<span className='truncate font-normal'>{selectedFile.name}</span>
|
||||
</>
|
||||
) : selectedFileId && isLoadingSelectedFile && selectedCredentialId ? (
|
||||
<>
|
||||
<RefreshCw className='h-4 w-4 animate-spin' />
|
||||
<span className='truncate text-muted-foreground'>Loading document...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{getProviderIcon(provider)}
|
||||
<span className='truncate text-muted-foreground'>{label}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<ChevronDown className='ml-2 h-4 w-4 shrink-0 opacity-50' />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
||||
@@ -442,20 +442,22 @@ export function JiraIssueSelector({
|
||||
variant='outline'
|
||||
role='combobox'
|
||||
aria-expanded={open}
|
||||
className='w-full justify-between'
|
||||
className='h-10 w-full min-w-0 justify-between'
|
||||
disabled={disabled || !domain}
|
||||
>
|
||||
{selectedIssue ? (
|
||||
<div className='flex items-center gap-2 overflow-hidden'>
|
||||
<JiraIcon className='h-4 w-4' />
|
||||
<span className='truncate font-normal'>{selectedIssue.name}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className='flex items-center gap-2'>
|
||||
<JiraIcon className='h-4 w-4' />
|
||||
<span className='text-muted-foreground'>{label}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className='flex min-w-0 items-center gap-2 overflow-hidden'>
|
||||
{selectedIssue ? (
|
||||
<>
|
||||
<JiraIcon className='h-4 w-4' />
|
||||
<span className='truncate font-normal'>{selectedIssue.name}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<JiraIcon className='h-4 w-4' />
|
||||
<span className='truncate text-muted-foreground'>{label}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<ChevronDown className='ml-2 h-4 w-4 shrink-0 opacity-50' />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
||||
@@ -414,25 +414,27 @@ export function MicrosoftFileSelector({
|
||||
variant='outline'
|
||||
role='combobox'
|
||||
aria-expanded={open}
|
||||
className='w-full justify-between'
|
||||
className='h-10 w-full min-w-0 justify-between'
|
||||
disabled={disabled}
|
||||
>
|
||||
{selectedFile ? (
|
||||
<div className='flex items-center gap-2 overflow-hidden'>
|
||||
{getFileIcon(selectedFile, 'sm')}
|
||||
<span className='truncate font-normal'>{selectedFile.name}</span>
|
||||
</div>
|
||||
) : selectedFileId && isLoadingSelectedFile && selectedCredentialId ? (
|
||||
<div className='flex items-center gap-2'>
|
||||
<RefreshCw className='h-4 w-4 animate-spin' />
|
||||
<span className='text-muted-foreground'>Loading document...</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className='flex items-center gap-2'>
|
||||
{getProviderIcon(provider)}
|
||||
<span className='text-muted-foreground'>{label}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className='flex min-w-0 items-center gap-2 overflow-hidden'>
|
||||
{selectedFile ? (
|
||||
<>
|
||||
{getFileIcon(selectedFile, 'sm')}
|
||||
<span className='truncate font-normal'>{selectedFile.name}</span>
|
||||
</>
|
||||
) : selectedFileId && isLoadingSelectedFile && selectedCredentialId ? (
|
||||
<>
|
||||
<RefreshCw className='h-4 w-4 animate-spin' />
|
||||
<span className='truncate text-muted-foreground'>Loading document...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{getProviderIcon(provider)}
|
||||
<span className='truncate text-muted-foreground'>{label}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<ChevronDown className='ml-2 h-4 w-4 shrink-0 opacity-50' />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
||||
@@ -701,24 +701,26 @@ export function TeamsMessageSelector({
|
||||
variant='outline'
|
||||
role='combobox'
|
||||
aria-expanded={open}
|
||||
className='w-full justify-between'
|
||||
className='h-10 w-full min-w-0 justify-between'
|
||||
disabled={disabled}
|
||||
>
|
||||
{selectedMessage ? (
|
||||
<div className='flex items-center gap-2 overflow-hidden'>
|
||||
<MicrosoftTeamsIcon className='h-4 w-4' />
|
||||
<span className='truncate font-normal'>{selectedMessage.displayName}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className='flex items-center gap-2'>
|
||||
<MicrosoftTeamsIcon className='h-4 w-4' />
|
||||
<span className='text-muted-foreground'>
|
||||
{selectionType === 'channel' && selectionStage === 'team'
|
||||
? 'Select a team first'
|
||||
: label}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className='flex min-w-0 items-center gap-2 overflow-hidden'>
|
||||
{selectedMessage ? (
|
||||
<>
|
||||
<MicrosoftTeamsIcon className='h-4 w-4' />
|
||||
<span className='truncate font-normal'>{selectedMessage.displayName}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<MicrosoftTeamsIcon className='h-4 w-4' />
|
||||
<span className='truncate text-muted-foreground'>
|
||||
{selectionType === 'channel' && selectionStage === 'team'
|
||||
? 'Select a team first'
|
||||
: label}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<ChevronDown className='ml-2 h-4 w-4 shrink-0 opacity-50' />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
||||
@@ -0,0 +1,472 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Check, ChevronDown, RefreshCw, X } from 'lucide-react'
|
||||
import { WealthboxIcon } from '@/components/icons'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from '@/components/ui/command'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import {
|
||||
type Credential,
|
||||
getProviderIdFromServiceId,
|
||||
getServiceIdFromScopes,
|
||||
type OAuthProvider,
|
||||
} from '@/lib/oauth'
|
||||
import { OAuthRequiredModal } from '../../credential-selector/components/oauth-required-modal'
|
||||
|
||||
const logger = createLogger('WealthboxFileSelector')
|
||||
|
||||
export interface WealthboxItemInfo {
|
||||
id: string
|
||||
name: string
|
||||
type: 'contact'
|
||||
content?: string
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
}
|
||||
|
||||
interface WealthboxFileSelectorProps {
|
||||
value: string
|
||||
onChange: (value: string, itemInfo?: WealthboxItemInfo) => void
|
||||
provider: OAuthProvider
|
||||
requiredScopes?: string[]
|
||||
label?: string
|
||||
disabled?: boolean
|
||||
serviceId?: string
|
||||
showPreview?: boolean
|
||||
onFileInfoChange?: (itemInfo: WealthboxItemInfo | null) => void
|
||||
itemType?: 'contact'
|
||||
}
|
||||
|
||||
export function WealthboxFileSelector({
|
||||
value,
|
||||
onChange,
|
||||
provider,
|
||||
requiredScopes = [],
|
||||
label = 'Select item',
|
||||
disabled = false,
|
||||
serviceId,
|
||||
showPreview = true,
|
||||
onFileInfoChange,
|
||||
itemType = 'contact',
|
||||
}: WealthboxFileSelectorProps) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [credentials, setCredentials] = useState<Credential[]>([])
|
||||
const [selectedCredentialId, setSelectedCredentialId] = useState<string>('')
|
||||
const [selectedItemId, setSelectedItemId] = useState(value)
|
||||
const [selectedItem, setSelectedItem] = useState<WealthboxItemInfo | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [isLoadingSelectedItem, setIsLoadingSelectedItem] = useState(false)
|
||||
const [isLoadingItems, setIsLoadingItems] = useState(false)
|
||||
const [availableItems, setAvailableItems] = useState<WealthboxItemInfo[]>([])
|
||||
const [searchQuery, setSearchQuery] = useState<string>('')
|
||||
const [showOAuthModal, setShowOAuthModal] = useState(false)
|
||||
const [credentialsLoaded, setCredentialsLoaded] = useState(false)
|
||||
const initialFetchRef = useRef(false)
|
||||
|
||||
// Determine the appropriate service ID based on provider and scopes
|
||||
const getServiceId = (): string => {
|
||||
if (serviceId) return serviceId
|
||||
return getServiceIdFromScopes(provider, requiredScopes)
|
||||
}
|
||||
|
||||
// Determine the appropriate provider ID based on service and scopes
|
||||
const getProviderId = (): string => {
|
||||
const effectiveServiceId = getServiceId()
|
||||
return getProviderIdFromServiceId(effectiveServiceId)
|
||||
}
|
||||
|
||||
// Fetch available credentials for this provider
|
||||
const fetchCredentials = useCallback(async () => {
|
||||
setIsLoading(true)
|
||||
setCredentialsLoaded(false)
|
||||
try {
|
||||
const providerId = getProviderId()
|
||||
const response = await fetch(`/api/auth/oauth/credentials?provider=${providerId}`)
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setCredentials(data.credentials)
|
||||
|
||||
// Auto-select logic for credentials
|
||||
if (data.credentials.length > 0) {
|
||||
if (
|
||||
selectedCredentialId &&
|
||||
data.credentials.some((cred: Credential) => cred.id === selectedCredentialId)
|
||||
) {
|
||||
// Keep the current selection
|
||||
} else {
|
||||
const defaultCred = data.credentials.find((cred: Credential) => cred.isDefault)
|
||||
if (defaultCred) {
|
||||
setSelectedCredentialId(defaultCred.id)
|
||||
} else if (data.credentials.length === 1) {
|
||||
setSelectedCredentialId(data.credentials[0].id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error fetching credentials:', { error })
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
setCredentialsLoaded(true)
|
||||
}
|
||||
}, [provider, getProviderId, selectedCredentialId])
|
||||
|
||||
// Debounced search function
|
||||
const [searchTimeout, setSearchTimeout] = useState<NodeJS.Timeout | null>(null)
|
||||
|
||||
// Fetch available items for the selected credential
|
||||
const fetchAvailableItems = useCallback(async () => {
|
||||
if (!selectedCredentialId) return
|
||||
|
||||
setIsLoadingItems(true)
|
||||
try {
|
||||
const queryParams = new URLSearchParams({
|
||||
credentialId: selectedCredentialId,
|
||||
type: itemType,
|
||||
})
|
||||
|
||||
if (searchQuery.trim()) {
|
||||
queryParams.append('query', searchQuery.trim())
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/auth/oauth/wealthbox/items?${queryParams.toString()}`)
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setAvailableItems(data.items || [])
|
||||
} else {
|
||||
logger.error('Error fetching available items:', {
|
||||
error: await response.text(),
|
||||
})
|
||||
setAvailableItems([])
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error fetching available items:', { error })
|
||||
setAvailableItems([])
|
||||
} finally {
|
||||
setIsLoadingItems(false)
|
||||
}
|
||||
}, [selectedCredentialId, searchQuery, itemType])
|
||||
|
||||
// Fetch a single item by ID
|
||||
const fetchItemById = useCallback(
|
||||
async (itemId: string) => {
|
||||
if (!selectedCredentialId || !itemId) return null
|
||||
|
||||
setIsLoadingSelectedItem(true)
|
||||
try {
|
||||
const queryParams = new URLSearchParams({
|
||||
credentialId: selectedCredentialId,
|
||||
itemId: itemId,
|
||||
type: itemType,
|
||||
})
|
||||
|
||||
const response = await fetch(`/api/auth/oauth/wealthbox/item?${queryParams.toString()}`)
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
if (data.item) {
|
||||
setSelectedItem(data.item)
|
||||
onFileInfoChange?.(data.item)
|
||||
return data.item
|
||||
}
|
||||
} else {
|
||||
const errorText = await response.text()
|
||||
logger.error('Error fetching item by ID:', { error: errorText })
|
||||
|
||||
if (response.status === 404 || response.status === 403) {
|
||||
logger.info('Item not accessible, clearing selection')
|
||||
setSelectedItemId('')
|
||||
onChange('')
|
||||
onFileInfoChange?.(null)
|
||||
}
|
||||
}
|
||||
return null
|
||||
} catch (error) {
|
||||
logger.error('Error fetching item by ID:', { error })
|
||||
return null
|
||||
} finally {
|
||||
setIsLoadingSelectedItem(false)
|
||||
}
|
||||
},
|
||||
[selectedCredentialId, itemType, onFileInfoChange, onChange]
|
||||
)
|
||||
|
||||
// Fetch credentials on initial mount
|
||||
useEffect(() => {
|
||||
if (!initialFetchRef.current) {
|
||||
fetchCredentials()
|
||||
initialFetchRef.current = true
|
||||
}
|
||||
}, [fetchCredentials])
|
||||
|
||||
// Fetch available items only when dropdown is opened
|
||||
useEffect(() => {
|
||||
if (selectedCredentialId && open) {
|
||||
fetchAvailableItems()
|
||||
}
|
||||
}, [selectedCredentialId, open, fetchAvailableItems])
|
||||
|
||||
// Fetch the selected item metadata only once when needed
|
||||
useEffect(() => {
|
||||
if (
|
||||
value &&
|
||||
value !== selectedItemId &&
|
||||
selectedCredentialId &&
|
||||
credentialsLoaded &&
|
||||
!selectedItem &&
|
||||
!isLoadingSelectedItem
|
||||
) {
|
||||
fetchItemById(value)
|
||||
}
|
||||
}, [
|
||||
value,
|
||||
selectedItemId,
|
||||
selectedCredentialId,
|
||||
credentialsLoaded,
|
||||
selectedItem,
|
||||
isLoadingSelectedItem,
|
||||
fetchItemById,
|
||||
])
|
||||
|
||||
// Handle search input changes with debouncing
|
||||
const handleSearchChange = useCallback(
|
||||
(newQuery: string) => {
|
||||
setSearchQuery(newQuery)
|
||||
|
||||
// Clear existing timeout
|
||||
if (searchTimeout) {
|
||||
clearTimeout(searchTimeout)
|
||||
}
|
||||
|
||||
// Set new timeout for search
|
||||
const timeout = setTimeout(() => {
|
||||
if (selectedCredentialId) {
|
||||
fetchAvailableItems()
|
||||
}
|
||||
}, 300) // 300ms debounce
|
||||
|
||||
setSearchTimeout(timeout)
|
||||
},
|
||||
[selectedCredentialId, fetchAvailableItems, searchTimeout]
|
||||
)
|
||||
|
||||
// Cleanup timeout on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (searchTimeout) {
|
||||
clearTimeout(searchTimeout)
|
||||
}
|
||||
}
|
||||
}, [searchTimeout])
|
||||
|
||||
// Handle selecting an item
|
||||
const handleItemSelect = (item: WealthboxItemInfo) => {
|
||||
setSelectedItemId(item.id)
|
||||
setSelectedItem(item)
|
||||
onChange(item.id, item)
|
||||
onFileInfoChange?.(item)
|
||||
setOpen(false)
|
||||
setSearchQuery('')
|
||||
}
|
||||
|
||||
// Handle adding a new credential
|
||||
const handleAddCredential = () => {
|
||||
setShowOAuthModal(true)
|
||||
setOpen(false)
|
||||
setSearchQuery('')
|
||||
}
|
||||
|
||||
// Clear selection
|
||||
const handleClearSelection = () => {
|
||||
setSelectedItemId('')
|
||||
setSelectedItem(null)
|
||||
onChange('', undefined)
|
||||
onFileInfoChange?.(null)
|
||||
}
|
||||
|
||||
const getItemTypeLabel = () => {
|
||||
switch (itemType) {
|
||||
case 'contact':
|
||||
return 'Contacts'
|
||||
default:
|
||||
return 'Contacts'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='space-y-2'>
|
||||
<Popover
|
||||
open={open}
|
||||
onOpenChange={(isOpen) => {
|
||||
setOpen(isOpen)
|
||||
if (!isOpen) {
|
||||
setSearchQuery('')
|
||||
if (searchTimeout) {
|
||||
clearTimeout(searchTimeout)
|
||||
setSearchTimeout(null)
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant='outline'
|
||||
role='combobox'
|
||||
aria-expanded={open}
|
||||
className='w-full justify-between'
|
||||
disabled={disabled}
|
||||
>
|
||||
{selectedItem ? (
|
||||
<div className='flex items-center gap-2 overflow-hidden'>
|
||||
<WealthboxIcon className='h-4 w-4' />
|
||||
<span className='truncate font-normal'>{selectedItem.name}</span>
|
||||
</div>
|
||||
) : selectedItemId && isLoadingSelectedItem && selectedCredentialId ? (
|
||||
<div className='flex items-center gap-2'>
|
||||
<RefreshCw className='h-4 w-4 animate-spin' />
|
||||
<span className='text-muted-foreground'>Loading...</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className='flex items-center gap-2'>
|
||||
<WealthboxIcon className='h-4 w-4' />
|
||||
<span className='text-muted-foreground'>{label}</span>
|
||||
</div>
|
||||
)}
|
||||
<ChevronDown className='ml-2 h-4 w-4 shrink-0 opacity-50' />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className='w-[300px] p-0' align='start'>
|
||||
<Command shouldFilter={false}>
|
||||
<div className='flex items-center border-b px-3' cmdk-input-wrapper=''>
|
||||
<input
|
||||
placeholder={`Search ${itemType}s...`}
|
||||
value={searchQuery}
|
||||
onChange={(e) => handleSearchChange(e.target.value)}
|
||||
className='flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50'
|
||||
/>
|
||||
</div>
|
||||
<CommandList>
|
||||
<CommandEmpty>
|
||||
{isLoadingItems ? `Loading ${itemType}s...` : `No ${itemType}s found.`}
|
||||
</CommandEmpty>
|
||||
|
||||
{credentials.length > 1 && (
|
||||
<CommandGroup>
|
||||
<div className='px-2 py-1.5 font-medium text-muted-foreground text-xs'>
|
||||
Switch Account
|
||||
</div>
|
||||
{credentials.map((cred) => (
|
||||
<CommandItem
|
||||
key={cred.id}
|
||||
value={`account-${cred.id}`}
|
||||
onSelect={() => setSelectedCredentialId(cred.id)}
|
||||
>
|
||||
<div className='flex items-center gap-2'>
|
||||
<WealthboxIcon className='h-4 w-4' />
|
||||
<span className='font-normal'>{cred.name}</span>
|
||||
</div>
|
||||
{cred.id === selectedCredentialId && <Check className='ml-auto h-4 w-4' />}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
)}
|
||||
|
||||
{availableItems.length > 0 && (
|
||||
<CommandGroup>
|
||||
<div className='px-2 py-1.5 font-medium text-muted-foreground text-xs'>
|
||||
{getItemTypeLabel()}
|
||||
</div>
|
||||
{availableItems.map((item) => (
|
||||
<CommandItem
|
||||
key={item.id}
|
||||
value={`item-${item.id}-${item.name}`}
|
||||
onSelect={() => handleItemSelect(item)}
|
||||
>
|
||||
<div className='flex items-center gap-2 overflow-hidden'>
|
||||
<WealthboxIcon className='h-4 w-4' />
|
||||
<div className='min-w-0 flex-1'>
|
||||
<span className='truncate font-normal'>{item.name}</span>
|
||||
{item.updatedAt && (
|
||||
<div className='text-muted-foreground text-xs'>
|
||||
Updated {new Date(item.updatedAt).toLocaleDateString()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{item.id === selectedItemId && <Check className='ml-auto h-4 w-4' />}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
)}
|
||||
|
||||
{credentials.length === 0 && (
|
||||
<CommandGroup>
|
||||
<CommandItem onSelect={handleAddCredential}>
|
||||
<div className='flex items-center gap-2 text-primary'>
|
||||
<WealthboxIcon className='h-4 w-4' />
|
||||
<span>Connect Wealthbox account</span>
|
||||
</div>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
)}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
{showPreview && selectedItem && (
|
||||
<div className='relative mt-2 rounded-md border border-muted bg-muted/10 p-2'>
|
||||
<div className='absolute top-2 right-2'>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
className='h-5 w-5 hover:bg-muted'
|
||||
onClick={handleClearSelection}
|
||||
>
|
||||
<X className='h-3 w-3' />
|
||||
</Button>
|
||||
</div>
|
||||
<div className='flex items-center gap-3 pr-4'>
|
||||
<div className='flex h-6 w-6 flex-shrink-0 items-center justify-center rounded bg-muted/20'>
|
||||
<WealthboxIcon className='h-4 w-4' />
|
||||
</div>
|
||||
<div className='min-w-0 flex-1 overflow-hidden'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<h4 className='truncate font-medium text-xs'>{selectedItem.name}</h4>
|
||||
{selectedItem.updatedAt && (
|
||||
<span className='whitespace-nowrap text-muted-foreground text-xs'>
|
||||
{new Date(selectedItem.updatedAt).toLocaleDateString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className='text-muted-foreground text-xs capitalize'>{selectedItem.type}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showOAuthModal && (
|
||||
<OAuthRequiredModal
|
||||
isOpen={showOAuthModal}
|
||||
onClose={() => setShowOAuthModal(false)}
|
||||
toolName='Wealthbox'
|
||||
provider={provider}
|
||||
requiredScopes={requiredScopes}
|
||||
serviceId={getServiceId()}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -22,6 +22,8 @@ import type { MicrosoftFileInfo } from './components/microsoft-file-selector'
|
||||
import { MicrosoftFileSelector } from './components/microsoft-file-selector'
|
||||
import type { TeamsMessageInfo } from './components/teams-message-selector'
|
||||
import { TeamsMessageSelector } from './components/teams-message-selector'
|
||||
import type { WealthboxItemInfo } from './components/wealthbox-file-selector'
|
||||
import { WealthboxFileSelector } from './components/wealthbox-file-selector'
|
||||
|
||||
interface FileSelectorInputProps {
|
||||
blockId: string
|
||||
@@ -54,6 +56,8 @@ export function FileSelectorInput({
|
||||
const [messageInfo, setMessageInfo] = useState<TeamsMessageInfo | null>(null)
|
||||
const [selectedCalendarId, setSelectedCalendarId] = useState<string>('')
|
||||
const [calendarInfo, setCalendarInfo] = useState<GoogleCalendarInfo | null>(null)
|
||||
const [selectedWealthboxItemId, setSelectedWealthboxItemId] = useState<string>('')
|
||||
const [wealthboxItemInfo, setWealthboxItemInfo] = useState<WealthboxItemInfo | null>(null)
|
||||
|
||||
// Get provider-specific values
|
||||
const provider = subBlock.provider || 'google-drive'
|
||||
@@ -63,6 +67,7 @@ export function FileSelectorInput({
|
||||
const isMicrosoftTeams = provider === 'microsoft-teams'
|
||||
const isMicrosoftExcel = provider === 'microsoft-excel'
|
||||
const isGoogleCalendar = subBlock.provider === 'google-calendar'
|
||||
const isWealthbox = provider === 'wealthbox'
|
||||
// For Confluence and Jira, we need the domain and credentials
|
||||
const domain = isConfluence || isJira ? (getValue(blockId, 'domain') as string) || '' : ''
|
||||
// For Discord, we need the bot token and server ID
|
||||
@@ -85,6 +90,8 @@ export function FileSelectorInput({
|
||||
setSelectedMessageId(value)
|
||||
} else if (isGoogleCalendar) {
|
||||
setSelectedCalendarId(value)
|
||||
} else if (isWealthbox) {
|
||||
setSelectedWealthboxItemId(value)
|
||||
} else {
|
||||
setSelectedFileId(value)
|
||||
}
|
||||
@@ -100,6 +107,8 @@ export function FileSelectorInput({
|
||||
setSelectedMessageId(value)
|
||||
} else if (isGoogleCalendar) {
|
||||
setSelectedCalendarId(value)
|
||||
} else if (isWealthbox) {
|
||||
setSelectedWealthboxItemId(value)
|
||||
} else {
|
||||
setSelectedFileId(value)
|
||||
}
|
||||
@@ -113,6 +122,7 @@ export function FileSelectorInput({
|
||||
isDiscord,
|
||||
isMicrosoftTeams,
|
||||
isGoogleCalendar,
|
||||
isWealthbox,
|
||||
isPreview,
|
||||
previewValue,
|
||||
])
|
||||
@@ -151,6 +161,13 @@ export function FileSelectorInput({
|
||||
setStoreValue(calendarId)
|
||||
}
|
||||
|
||||
// Handle Wealthbox item selection
|
||||
const handleWealthboxItemChange = (itemId: string, info?: WealthboxItemInfo) => {
|
||||
setSelectedWealthboxItemId(itemId)
|
||||
setWealthboxItemInfo(info || null)
|
||||
setStoreValue(itemId)
|
||||
}
|
||||
|
||||
// For Google Drive
|
||||
const clientId = env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || ''
|
||||
const apiKey = env.NEXT_PUBLIC_GOOGLE_API_KEY || ''
|
||||
@@ -369,6 +386,47 @@ export function FileSelectorInput({
|
||||
)
|
||||
}
|
||||
|
||||
// Render Wealthbox selector
|
||||
if (isWealthbox) {
|
||||
// Get credential using the same pattern as other tools
|
||||
const credential = (getValue(blockId, 'credential') as string) || ''
|
||||
|
||||
// Only handle contacts now - both notes and tasks use short-input
|
||||
if (subBlock.id === 'contactId') {
|
||||
const itemType = 'contact'
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className='w-full'>
|
||||
<WealthboxFileSelector
|
||||
value={selectedWealthboxItemId}
|
||||
onChange={handleWealthboxItemChange}
|
||||
provider='wealthbox'
|
||||
requiredScopes={subBlock.requiredScopes || []}
|
||||
serviceId={subBlock.serviceId}
|
||||
label={subBlock.placeholder || `Select ${itemType}`}
|
||||
disabled={disabled || !credential}
|
||||
showPreview={true}
|
||||
onFileInfoChange={setWealthboxItemInfo}
|
||||
itemType={itemType}
|
||||
/>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
{!credential && (
|
||||
<TooltipContent side='top'>
|
||||
<p>Please select Wealthbox credentials first</p>
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
// If it's noteId or taskId, we should not render the file selector since they now use short-input
|
||||
return null
|
||||
}
|
||||
|
||||
// Default to Google Drive picker
|
||||
return (
|
||||
<GoogleDrivePicker
|
||||
|
||||
@@ -20,13 +20,15 @@ export function SliderInput({
|
||||
subBlockId,
|
||||
min = 0,
|
||||
max = 100,
|
||||
defaultValue = 50,
|
||||
defaultValue,
|
||||
step = 0.1,
|
||||
integer = false,
|
||||
isPreview = false,
|
||||
previewValue,
|
||||
disabled = false,
|
||||
}: SliderInputProps) {
|
||||
// Smart default value: if no default provided, use midpoint or 0.7 for 0-1 ranges
|
||||
const computedDefaultValue = defaultValue ?? (max <= 1 ? 0.7 : (min + max) / 2)
|
||||
const [storeValue, setStoreValue] = useSubBlockValue<number>(blockId, subBlockId)
|
||||
|
||||
// Use preview value when in preview mode, otherwise use store value
|
||||
@@ -34,9 +36,11 @@ export function SliderInput({
|
||||
|
||||
// Clamp the value within bounds while preserving relative position when possible
|
||||
const normalizedValue =
|
||||
value !== null && value !== undefined ? Math.max(min, Math.min(max, value)) : defaultValue
|
||||
value !== null && value !== undefined
|
||||
? Math.max(min, Math.min(max, value))
|
||||
: computedDefaultValue
|
||||
|
||||
const displayValue = normalizedValue ?? defaultValue
|
||||
const displayValue = normalizedValue ?? computedDefaultValue
|
||||
|
||||
// Ensure the normalized value is set if it differs from the current value
|
||||
useEffect(() => {
|
||||
|
||||
@@ -86,10 +86,28 @@ export function ToolCredentialSelector({
|
||||
const data = await response.json()
|
||||
setCredentials(data.credentials || [])
|
||||
|
||||
// If we have a selected value but it's not in the credentials list, clear it
|
||||
// If we have a value but it's not in the credentials, reset it
|
||||
if (value && !data.credentials?.some((cred: Credential) => cred.id === value)) {
|
||||
onChange('')
|
||||
}
|
||||
|
||||
// Auto-selection logic (like credential-selector):
|
||||
// 1. If we already have a valid selection, keep it
|
||||
// 2. If there's a default credential, select it
|
||||
// 3. If there's only one credential, select it
|
||||
if (
|
||||
(!value || !data.credentials?.some((cred: Credential) => cred.id === value)) &&
|
||||
data.credentials &&
|
||||
data.credentials.length > 0
|
||||
) {
|
||||
const defaultCred = data.credentials.find((cred: Credential) => cred.isDefault)
|
||||
if (defaultCred) {
|
||||
onChange(defaultCred.id)
|
||||
} else if (data.credentials.length === 1) {
|
||||
// If only one credential, select it
|
||||
onChange(data.credentials[0].id)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.error('Error fetching credentials:', { error: await response.text() })
|
||||
setCredentials([])
|
||||
@@ -102,9 +120,26 @@ export function ToolCredentialSelector({
|
||||
}
|
||||
}, [provider, value, onChange])
|
||||
|
||||
// Fetch credentials on mount and when provider changes
|
||||
// Fetch credentials on initial mount only
|
||||
useEffect(() => {
|
||||
fetchCredentials()
|
||||
// This effect should only run once on mount
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
// Listen for visibility changes to update credentials when user returns from settings
|
||||
useEffect(() => {
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
fetchCredentials()
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
||||
}
|
||||
}, [fetchCredentials])
|
||||
|
||||
const handleSelect = (credentialId: string) => {
|
||||
@@ -119,30 +154,41 @@ export function ToolCredentialSelector({
|
||||
fetchCredentials()
|
||||
}
|
||||
|
||||
// Handle popover open to fetch fresh credentials
|
||||
const handleOpenChange = (isOpen: boolean) => {
|
||||
setOpen(isOpen)
|
||||
if (isOpen) {
|
||||
// Fetch fresh credentials when opening the dropdown
|
||||
fetchCredentials()
|
||||
}
|
||||
}
|
||||
|
||||
const selectedCredential = credentials.find((cred) => cred.id === selectedId)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<Popover open={open} onOpenChange={handleOpenChange}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant='outline'
|
||||
role='combobox'
|
||||
aria-expanded={open}
|
||||
className='w-full justify-between'
|
||||
className='h-10 w-full min-w-0 justify-between'
|
||||
disabled={disabled}
|
||||
>
|
||||
{selectedCredential ? (
|
||||
<div className='flex items-center gap-2 overflow-hidden'>
|
||||
{getProviderIcon(provider)}
|
||||
<span className='truncate font-normal'>{selectedCredential.name}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className='flex items-center gap-2'>
|
||||
{getProviderIcon(provider)}
|
||||
<span className='text-muted-foreground'>{label}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className='flex min-w-0 items-center gap-2 overflow-hidden'>
|
||||
{selectedCredential ? (
|
||||
<>
|
||||
{getProviderIcon(provider)}
|
||||
<span className='truncate font-normal'>{selectedCredential.name}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{getProviderIcon(provider)}
|
||||
<span className='truncate text-muted-foreground'>{label}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<ChevronDown className='ml-2 h-4 w-4 shrink-0 opacity-50' />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -269,7 +269,8 @@ export function useSubBlockValue<T = any>(
|
||||
if (!isEqual(valueRef.current, newValue)) {
|
||||
valueRef.current = newValue
|
||||
|
||||
// Always update local store immediately for UI responsiveness
|
||||
// Update local store immediately for UI responsiveness
|
||||
// The collaborative function will also update it, but that's okay for idempotency
|
||||
useSubBlockStore.setState((state) => ({
|
||||
workflowValues: {
|
||||
...state.workflowValues,
|
||||
|
||||
@@ -115,7 +115,7 @@ export function SubBlock({
|
||||
<Dropdown
|
||||
blockId={blockId}
|
||||
subBlockId={config.id}
|
||||
options={config.options as string[]}
|
||||
options={config.options as { label: string; id: string }[]}
|
||||
isPreview={isPreview}
|
||||
previewValue={previewValue}
|
||||
disabled={isDisabled}
|
||||
@@ -128,7 +128,7 @@ export function SubBlock({
|
||||
<ComboBox
|
||||
blockId={blockId}
|
||||
subBlockId={config.id}
|
||||
options={config.options as string[]}
|
||||
options={config.options as { label: string; id: string }[]}
|
||||
placeholder={config.placeholder}
|
||||
isPreview={isPreview}
|
||||
previewValue={previewValue}
|
||||
|
||||
@@ -33,6 +33,7 @@ interface ExecutorOptions {
|
||||
selectedOutputIds?: string[]
|
||||
edges?: Array<{ source: string; target: string }>
|
||||
onStream?: (streamingExecution: StreamingExecution) => Promise<void>
|
||||
executionId?: string
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,7 +204,7 @@ export function useWorkflowExecution() {
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await executeWorkflow(workflowInput, onStream)
|
||||
const result = await executeWorkflow(workflowInput, onStream, executionId)
|
||||
|
||||
await Promise.all(streamReadingPromises)
|
||||
|
||||
@@ -216,14 +217,18 @@ export function useWorkflowExecution() {
|
||||
if (result.logs) {
|
||||
result.logs.forEach((log: BlockLog) => {
|
||||
if (streamedContent.has(log.blockId)) {
|
||||
const content = streamedContent.get(log.blockId) || ''
|
||||
// For console display, show the actual structured block output instead of formatted streaming content
|
||||
// This ensures console logs match the block state structure
|
||||
// Use replaceOutput to completely replace the output instead of merging
|
||||
useConsoleStore.getState().updateConsole(log.blockId, {
|
||||
replaceOutput: log.output,
|
||||
success: true,
|
||||
})
|
||||
// Use the executionId from this execution context
|
||||
useConsoleStore.getState().updateConsole(
|
||||
log.blockId,
|
||||
{
|
||||
replaceOutput: log.output,
|
||||
success: true,
|
||||
},
|
||||
executionId
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -262,7 +267,7 @@ export function useWorkflowExecution() {
|
||||
// For manual (non-chat) execution
|
||||
const executionId = uuidv4()
|
||||
try {
|
||||
const result = await executeWorkflow(workflowInput)
|
||||
const result = await executeWorkflow(workflowInput, undefined, executionId)
|
||||
if (result && 'metadata' in result && result.metadata?.isDebugSession) {
|
||||
setDebugContext(result.metadata.context || null)
|
||||
if (result.metadata.pendingBlocks) {
|
||||
@@ -330,7 +335,8 @@ export function useWorkflowExecution() {
|
||||
|
||||
const executeWorkflow = async (
|
||||
workflowInput?: any,
|
||||
onStream?: (se: StreamingExecution) => Promise<void>
|
||||
onStream?: (se: StreamingExecution) => Promise<void>,
|
||||
executionId?: string
|
||||
): Promise<ExecutionResult | StreamingExecution> => {
|
||||
// Use the mergeSubblockState utility to get all block states
|
||||
const mergedStates = mergeSubblockState(blocks)
|
||||
@@ -401,6 +407,7 @@ export function useWorkflowExecution() {
|
||||
target: conn.target,
|
||||
})),
|
||||
onStream,
|
||||
executionId,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ import { SkeletonLoading } from '@/app/workspace/[workspaceId]/w/[workflowId]/co
|
||||
import { Toolbar } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/toolbar/toolbar'
|
||||
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/w/components/providers/workspace-permissions-provider'
|
||||
import { getBlock } from '@/blocks'
|
||||
import { useSocket } from '@/contexts/socket-context'
|
||||
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
|
||||
import { useWorkspacePermissions } from '@/hooks/use-workspace-permissions'
|
||||
import { useExecutionStore } from '@/stores/execution/store'
|
||||
@@ -120,10 +119,9 @@ const WorkflowContent = React.memo(() => {
|
||||
collaborativeRemoveEdge: removeEdge,
|
||||
collaborativeUpdateBlockPosition,
|
||||
collaborativeUpdateParentId: updateParentId,
|
||||
isConnected,
|
||||
currentWorkflowId,
|
||||
collaborativeSetSubblockValue,
|
||||
} = useCollaborativeWorkflow()
|
||||
const { emitSubblockUpdate } = useSocket()
|
||||
|
||||
const { markAllAsRead } = useNotificationStore()
|
||||
const { resetLoaded: resetVariablesLoaded } = useVariablesStore()
|
||||
|
||||
@@ -1484,11 +1482,9 @@ const WorkflowContent = React.memo(() => {
|
||||
const handleSubBlockValueUpdate = (event: CustomEvent) => {
|
||||
const { blockId, subBlockId, value } = event.detail
|
||||
if (blockId && subBlockId) {
|
||||
// Only emit the socket update, don't update the store again
|
||||
// The store was already updated in the setValue function
|
||||
if (isConnected && currentWorkflowId && activeWorkflowId === currentWorkflowId) {
|
||||
emitSubblockUpdate(blockId, subBlockId, value)
|
||||
}
|
||||
// Use collaborative function to go through queue system
|
||||
// This ensures 5-second timeout and error detection work
|
||||
collaborativeSetSubblockValue(blockId, subBlockId, value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1500,7 +1496,7 @@ const WorkflowContent = React.memo(() => {
|
||||
handleSubBlockValueUpdate as EventListener
|
||||
)
|
||||
}
|
||||
}, [emitSubblockUpdate, isConnected, currentWorkflowId, activeWorkflowId])
|
||||
}, [collaborativeSetSubblockValue])
|
||||
|
||||
// Show skeleton UI while loading, then smoothly transition to real content
|
||||
const showSkeletonUI = !isWorkflowReady
|
||||
|
||||
@@ -4,12 +4,12 @@ import type React from 'react'
|
||||
import { createContext, useContext, useEffect, useMemo, useState } from 'react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
|
||||
import { useUserPermissions, type WorkspaceUserPermissions } from '@/hooks/use-user-permissions'
|
||||
import {
|
||||
useWorkspacePermissions,
|
||||
type WorkspacePermissions,
|
||||
} from '@/hooks/use-workspace-permissions'
|
||||
import { usePresence } from '../../[workflowId]/hooks/use-presence'
|
||||
|
||||
const logger = createLogger('WorkspacePermissionsProvider')
|
||||
|
||||
@@ -57,7 +57,16 @@ export function WorkspacePermissionsProvider({ children }: WorkspacePermissionsP
|
||||
|
||||
// Manage offline mode state locally
|
||||
const [isOfflineMode, setIsOfflineMode] = useState(false)
|
||||
const [hasBeenConnected, setHasBeenConnected] = useState(false)
|
||||
|
||||
// Get operation error state from collaborative workflow
|
||||
const { hasOperationError } = useCollaborativeWorkflow()
|
||||
|
||||
// Set offline mode when there are operation errors
|
||||
useEffect(() => {
|
||||
if (hasOperationError) {
|
||||
setIsOfflineMode(true)
|
||||
}
|
||||
}, [hasOperationError])
|
||||
|
||||
// Fetch workspace permissions and loading state
|
||||
const {
|
||||
@@ -74,26 +83,8 @@ export function WorkspacePermissionsProvider({ children }: WorkspacePermissionsP
|
||||
permissionsError
|
||||
)
|
||||
|
||||
// Get connection status and update offline mode accordingly
|
||||
const { isConnected } = usePresence()
|
||||
|
||||
useEffect(() => {
|
||||
if (isConnected) {
|
||||
// Mark that we've been connected at least once
|
||||
setHasBeenConnected(true)
|
||||
// On initial connection, allow going online
|
||||
if (!hasBeenConnected) {
|
||||
setIsOfflineMode(false)
|
||||
}
|
||||
// If we were previously connected and this is a reconnection, stay offline (user must refresh)
|
||||
} else if (hasBeenConnected) {
|
||||
const timeoutId = setTimeout(() => {
|
||||
setIsOfflineMode(true)
|
||||
}, 6000)
|
||||
return () => clearTimeout(timeoutId)
|
||||
}
|
||||
// If not connected and never been connected, stay in initial state (not offline mode)
|
||||
}, [isConnected, hasBeenConnected])
|
||||
// Note: Connection-based error detection removed - only rely on operation timeouts
|
||||
// The 5-second operation timeout system will handle all error cases
|
||||
|
||||
// Create connection-aware permissions that override user permissions when offline
|
||||
const userPermissions = useMemo((): WorkspaceUserPermissions & { isOfflineMode?: boolean } => {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useParams, usePathname, useRouter } from 'next/navigation'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { useSession } from '@/lib/auth-client'
|
||||
import { env } from '@/lib/env'
|
||||
import { isDev } from '@/lib/environment'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import {
|
||||
getKeyboardShortcutText,
|
||||
@@ -28,8 +28,6 @@ import { WorkspaceHeader } from './components/workspace-header/workspace-header'
|
||||
|
||||
const logger = createLogger('Sidebar')
|
||||
|
||||
const IS_DEV = env.NODE_ENV === 'development'
|
||||
|
||||
export function Sidebar() {
|
||||
useGlobalShortcuts()
|
||||
|
||||
@@ -239,7 +237,7 @@ export function Sidebar() {
|
||||
{isCollapsed ? (
|
||||
<div className='flex-shrink-0 px-3 pt-1 pb-3'>
|
||||
<div className='flex flex-col space-y-[1px]'>
|
||||
{!IS_DEV && (
|
||||
{!isDev && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
@@ -286,7 +284,7 @@ export function Sidebar() {
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{!IS_DEV && (
|
||||
{!isDev && (
|
||||
<div className='flex-shrink-0 px-3 pt-1'>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
@@ -343,7 +341,7 @@ export function Sidebar() {
|
||||
{/* Modals */}
|
||||
<SettingsModal open={showSettings} onOpenChange={setShowSettings} />
|
||||
<HelpModal open={showHelp} onOpenChange={setShowHelp} />
|
||||
{!IS_DEV && <InviteModal open={showInviteMembers} onOpenChange={setShowInviteMembers} />}
|
||||
{!isDev && <InviteModal open={showInviteMembers} onOpenChange={setShowInviteMembers} />}
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -25,7 +25,13 @@ export const ApiBlock: BlockConfig<RequestResponse> = {
|
||||
title: 'Method',
|
||||
type: 'dropdown',
|
||||
layout: 'half',
|
||||
options: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
|
||||
options: [
|
||||
{ label: 'GET', id: 'GET' },
|
||||
{ label: 'POST', id: 'POST' },
|
||||
{ label: 'PUT', id: 'PUT' },
|
||||
{ label: 'DELETE', id: 'DELETE' },
|
||||
{ label: 'PATCH', id: 'PATCH' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'params',
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
import { AutoblocksIcon } from '@/components/icons'
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
import type { BlockConfig } from '../types'
|
||||
|
||||
interface AutoblocksResponse extends ToolResponse {
|
||||
output: {
|
||||
promptId: string
|
||||
version: string
|
||||
renderedPrompt: string
|
||||
templates: Record<string, string>
|
||||
}
|
||||
}
|
||||
|
||||
export const AutoblocksBlock: BlockConfig<AutoblocksResponse> = {
|
||||
type: 'autoblocks',
|
||||
name: 'Autoblocks',
|
||||
description: 'Manage and use versioned prompts with Autoblocks',
|
||||
longDescription:
|
||||
'Collaborate on prompts with type safety, autocomplete, and backwards-incompatibility protection. Autoblocks prompt management allows product teams to collaborate while maintaining excellent developer experience.',
|
||||
category: 'tools',
|
||||
bgColor: '#0D2929',
|
||||
icon: AutoblocksIcon,
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'promptId',
|
||||
title: 'Prompt ID',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
placeholder: 'Enter the Autoblocks prompt ID',
|
||||
},
|
||||
{
|
||||
id: 'version',
|
||||
title: 'Version',
|
||||
type: 'dropdown',
|
||||
layout: 'half',
|
||||
options: [
|
||||
{ label: 'Latest Minor', id: 'latest' },
|
||||
{ label: 'Specific Version', id: 'specific' },
|
||||
],
|
||||
value: () => 'latest',
|
||||
},
|
||||
{
|
||||
id: 'specificVersion',
|
||||
title: 'Specific Version',
|
||||
type: 'short-input',
|
||||
layout: 'half',
|
||||
placeholder: 'e.g. 1.2 or 1.latest',
|
||||
condition: {
|
||||
field: 'version',
|
||||
value: 'specific',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'apiKey',
|
||||
title: 'API Key',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
placeholder: 'Enter your Autoblocks API key',
|
||||
password: true,
|
||||
},
|
||||
{
|
||||
id: 'templateParams',
|
||||
title: 'Template Parameters',
|
||||
type: 'code',
|
||||
layout: 'full',
|
||||
language: 'json',
|
||||
placeholder: '{"param1": "value1", "param2": "value2"}',
|
||||
},
|
||||
{
|
||||
id: 'enableABTesting',
|
||||
title: 'Enable A/B Testing',
|
||||
type: 'switch',
|
||||
layout: 'half',
|
||||
},
|
||||
{
|
||||
id: 'abTestConfig',
|
||||
title: 'A/B Test Configuration',
|
||||
type: 'code',
|
||||
layout: 'full',
|
||||
language: 'json',
|
||||
placeholder:
|
||||
'{"versions": [{"version": "0", "weight": 95}, {"version": "latest", "weight": 5}]}',
|
||||
condition: {
|
||||
field: 'enableABTesting',
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'environment',
|
||||
title: 'Environment',
|
||||
type: 'dropdown',
|
||||
layout: 'half',
|
||||
options: [
|
||||
{ label: 'Production', id: 'production' },
|
||||
{ label: 'Staging', id: 'staging' },
|
||||
{ label: 'Development', id: 'development' },
|
||||
],
|
||||
value: () => 'development',
|
||||
},
|
||||
],
|
||||
tools: {
|
||||
access: ['autoblocks_prompt_manager'],
|
||||
},
|
||||
inputs: {
|
||||
promptId: { type: 'string', required: true },
|
||||
version: { type: 'string', required: true },
|
||||
specificVersion: { type: 'string', required: false },
|
||||
templateParams: { type: 'json', required: false },
|
||||
apiKey: { type: 'string', required: true },
|
||||
enableABTesting: { type: 'boolean', required: false },
|
||||
abTestConfig: { type: 'json', required: false },
|
||||
environment: { type: 'string', required: true },
|
||||
},
|
||||
outputs: {
|
||||
promptId: 'string',
|
||||
version: 'string',
|
||||
renderedPrompt: 'string',
|
||||
templates: 'json',
|
||||
},
|
||||
}
|
||||
@@ -57,6 +57,19 @@ export const ElevenLabsBlock: BlockConfig<ElevenLabsBlockResponse> = {
|
||||
layout: 'full',
|
||||
placeholder: 'Enter the voice ID',
|
||||
},
|
||||
{
|
||||
id: 'modelId',
|
||||
title: 'Model ID',
|
||||
type: 'dropdown',
|
||||
layout: 'half',
|
||||
options: [
|
||||
{ label: 'eleven_monolingual_v1', id: 'eleven_monolingual_v1' },
|
||||
{ label: 'eleven_multilingual_v2', id: 'eleven_multilingual_v2' },
|
||||
{ label: 'eleven_turbo_v2', id: 'eleven_turbo_v2' },
|
||||
{ label: 'eleven_turbo_v2_5', id: 'eleven_turbo_v2_5' },
|
||||
{ label: 'eleven_flash_v2_5', id: 'eleven_flash_v2_5' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'apiKey',
|
||||
title: 'API Key',
|
||||
@@ -65,18 +78,5 @@ export const ElevenLabsBlock: BlockConfig<ElevenLabsBlockResponse> = {
|
||||
placeholder: 'Enter your ElevenLabs API key',
|
||||
password: true,
|
||||
},
|
||||
{
|
||||
id: 'modelId',
|
||||
title: 'Model ID (Optional)',
|
||||
type: 'dropdown',
|
||||
layout: 'half',
|
||||
options: [
|
||||
'eleven_monolingual_v1',
|
||||
'eleven_multilingual_v2',
|
||||
'eleven_turbo_v2',
|
||||
'eleven_turbo_v2_5',
|
||||
'eleven_flash_v2_5',
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -176,7 +176,10 @@ export const EvaluatorBlock: BlockConfig<EvaluatorResponse> = {
|
||||
options: () => {
|
||||
const ollamaModels = useOllamaStore.getState().models
|
||||
const baseModels = Object.keys(getBaseModelProviders())
|
||||
return [...baseModels, ...ollamaModels]
|
||||
return [...baseModels, ...ollamaModels].map((model) => ({
|
||||
label: model,
|
||||
id: model,
|
||||
}))
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { DocumentIcon } from '@/components/icons'
|
||||
import { env } from '@/lib/env'
|
||||
import { isProd } from '@/lib/environment'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import type { FileParserOutput } from '@/tools/file/types'
|
||||
import type { BlockConfig, SubBlockConfig, SubBlockLayout, SubBlockType } from '../types'
|
||||
|
||||
const logger = createLogger('FileBlock')
|
||||
|
||||
const shouldEnableURLInput = env.NODE_ENV === 'production'
|
||||
const shouldEnableURLInput = isProd
|
||||
|
||||
const inputMethodBlock: SubBlockConfig = {
|
||||
id: 'inputMethod',
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
import { GuestyIcon } from '@/components/icons'
|
||||
import type { GuestyGuestResponse, GuestyReservationResponse } from '@/tools/guesty/types'
|
||||
import type { BlockConfig } from '../types'
|
||||
|
||||
export const GuestyBlock: BlockConfig<GuestyReservationResponse | GuestyGuestResponse> = {
|
||||
type: 'guesty',
|
||||
name: 'Guesty',
|
||||
description: 'Interact with Guesty property management system',
|
||||
longDescription:
|
||||
'Access Guesty property management data including reservations and guest information. Retrieve reservation details by ID or search for guests by phone number.',
|
||||
docsLink: 'https://docs.simstudio.ai/tools/guesty',
|
||||
category: 'tools',
|
||||
bgColor: '#0051F8', // Guesty brand color
|
||||
icon: GuestyIcon,
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'action',
|
||||
title: 'Action',
|
||||
type: 'dropdown',
|
||||
layout: 'full',
|
||||
options: [
|
||||
{ label: 'Get Reservation', id: 'reservation' },
|
||||
{ label: 'Search Guest', id: 'guest' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'reservationId',
|
||||
title: 'Reservation ID',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
placeholder: 'Enter reservation ID',
|
||||
condition: {
|
||||
field: 'action',
|
||||
value: 'reservation',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'phoneNumber',
|
||||
title: 'Phone Number',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
placeholder: 'Enter phone number',
|
||||
condition: {
|
||||
field: 'action',
|
||||
value: 'guest',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'apiKey',
|
||||
title: 'API Key',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
placeholder: 'Enter your Guesty API key',
|
||||
password: true,
|
||||
connectionDroppable: false,
|
||||
},
|
||||
],
|
||||
tools: {
|
||||
access: ['guesty_reservation', 'guesty_guest'],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
return params.action === 'reservation' ? 'guesty_reservation' : 'guesty_guest'
|
||||
},
|
||||
params: (params) => {
|
||||
if (params.action === 'reservation') {
|
||||
return {
|
||||
apiKey: params.apiKey,
|
||||
reservationId: params.reservationId,
|
||||
}
|
||||
}
|
||||
return {
|
||||
apiKey: params.apiKey,
|
||||
phoneNumber: params.phoneNumber,
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
action: { type: 'string', required: true },
|
||||
apiKey: { type: 'string', required: true },
|
||||
reservationId: { type: 'string', required: false },
|
||||
phoneNumber: { type: 'string', required: false },
|
||||
},
|
||||
outputs: {
|
||||
id: 'string',
|
||||
guest: 'json',
|
||||
checkIn: 'string',
|
||||
checkOut: 'string',
|
||||
status: 'string',
|
||||
listing: 'json',
|
||||
money: 'json',
|
||||
guests: 'json',
|
||||
},
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { MistralIcon } from '@/components/icons'
|
||||
import { env } from '@/lib/env'
|
||||
import { isProd } from '@/lib/environment'
|
||||
import type { MistralParserOutput } from '@/tools/mistral/types'
|
||||
import type { BlockConfig, SubBlockConfig, SubBlockLayout, SubBlockType } from '../types'
|
||||
|
||||
const shouldEnableFileUpload = env.NODE_ENV === 'production'
|
||||
const shouldEnableFileUpload = isProd
|
||||
|
||||
const inputMethodBlock: SubBlockConfig = {
|
||||
id: 'inputMethod',
|
||||
|
||||
@@ -19,9 +19,9 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
|
||||
type: 'dropdown',
|
||||
layout: 'full',
|
||||
options: [
|
||||
{ label: 'Read Page', id: 'read_notion' },
|
||||
{ label: 'Append Content', id: 'write_notion' },
|
||||
{ label: 'Create Page', id: 'create_notion' },
|
||||
{ label: 'Read Page', id: 'notion_read' },
|
||||
{ label: 'Append Content', id: 'notion_write' },
|
||||
{ label: 'Create Page', id: 'notion_create_page' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -43,7 +43,7 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
|
||||
placeholder: 'Enter Notion page ID',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'read_notion',
|
||||
value: 'notion_read',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -54,7 +54,7 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
|
||||
placeholder: 'Enter Notion page ID',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'write_notion',
|
||||
value: 'notion_write',
|
||||
},
|
||||
},
|
||||
// Create operation fields
|
||||
@@ -67,7 +67,7 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
|
||||
{ label: 'Page', id: 'page' },
|
||||
{ label: 'Database', id: 'database' },
|
||||
],
|
||||
condition: { field: 'operation', value: 'create_notion' },
|
||||
condition: { field: 'operation', value: 'notion_create_page' },
|
||||
},
|
||||
{
|
||||
id: 'parentId',
|
||||
@@ -75,7 +75,7 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
placeholder: 'ID of parent page or database',
|
||||
condition: { field: 'operation', value: 'create_notion' },
|
||||
condition: { field: 'operation', value: 'notion_create_page' },
|
||||
},
|
||||
{
|
||||
id: 'title',
|
||||
@@ -85,7 +85,7 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
|
||||
placeholder: 'Title for the new page',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'create_notion',
|
||||
value: 'notion_create_page',
|
||||
and: { field: 'parentType', value: 'page' },
|
||||
},
|
||||
},
|
||||
@@ -97,7 +97,7 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
|
||||
placeholder: 'Enter page properties as JSON object',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'create_notion',
|
||||
value: 'notion_create_page',
|
||||
},
|
||||
},
|
||||
// Content input for write/create operations
|
||||
@@ -109,7 +109,7 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
|
||||
placeholder: 'Enter content to add to the page',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'write_notion',
|
||||
value: 'notion_write',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -120,7 +120,7 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
|
||||
placeholder: 'Enter content to add to the page',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'create_notion',
|
||||
value: 'notion_create_page',
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -129,11 +129,11 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
|
||||
config: {
|
||||
tool: (params) => {
|
||||
switch (params.operation) {
|
||||
case 'read_notion':
|
||||
case 'notion_read':
|
||||
return 'notion_read'
|
||||
case 'write_notion':
|
||||
case 'notion_write':
|
||||
return 'notion_write'
|
||||
case 'create_notion':
|
||||
case 'notion_create_page':
|
||||
return 'notion_create_page'
|
||||
default:
|
||||
return 'notion_read'
|
||||
@@ -144,7 +144,7 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
|
||||
|
||||
// Parse properties from JSON string for create operations
|
||||
let parsedProperties
|
||||
if (operation === 'create_notion' && properties) {
|
||||
if (operation === 'notion_create_page' && properties) {
|
||||
try {
|
||||
parsedProperties = JSON.parse(properties)
|
||||
} catch (error) {
|
||||
|
||||
@@ -26,14 +26,14 @@ export const PerplexityBlock: BlockConfig<PerplexityChatResponse> = {
|
||||
icon: PerplexityIcon,
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'system',
|
||||
id: 'systemPrompt',
|
||||
title: 'System Prompt',
|
||||
type: 'long-input',
|
||||
layout: 'full',
|
||||
placeholder: 'Optional system prompt to guide the model behavior...',
|
||||
},
|
||||
{
|
||||
id: 'prompt',
|
||||
id: 'content',
|
||||
title: 'User Prompt',
|
||||
type: 'long-input',
|
||||
layout: 'full',
|
||||
@@ -87,8 +87,8 @@ export const PerplexityBlock: BlockConfig<PerplexityChatResponse> = {
|
||||
const toolParams = {
|
||||
apiKey: params.apiKey,
|
||||
model: params.model,
|
||||
prompt: params.prompt,
|
||||
system: params.system,
|
||||
content: params.content,
|
||||
systemPrompt: params.systemPrompt,
|
||||
max_tokens: params.max_tokens ? Number.parseInt(params.max_tokens) : undefined,
|
||||
temperature: params.temperature ? Number.parseFloat(params.temperature) : undefined,
|
||||
}
|
||||
@@ -98,8 +98,8 @@ export const PerplexityBlock: BlockConfig<PerplexityChatResponse> = {
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
prompt: { type: 'string', required: true },
|
||||
system: { type: 'string', required: false },
|
||||
content: { type: 'string', required: true },
|
||||
systemPrompt: { type: 'string', required: false },
|
||||
model: { type: 'string', required: true },
|
||||
max_tokens: { type: 'string', required: false },
|
||||
temperature: { type: 'string', required: false },
|
||||
|
||||
@@ -19,10 +19,10 @@ export const RedditBlock: BlockConfig<
|
||||
bgColor: '#FF5700',
|
||||
icon: RedditIcon,
|
||||
subBlocks: [
|
||||
// Action selection
|
||||
// Operation selection
|
||||
{
|
||||
id: 'action',
|
||||
title: 'Action',
|
||||
id: 'operation',
|
||||
title: 'Operation',
|
||||
type: 'dropdown',
|
||||
layout: 'full',
|
||||
options: [
|
||||
@@ -51,7 +51,7 @@ export const RedditBlock: BlockConfig<
|
||||
layout: 'full',
|
||||
placeholder: 'Enter subreddit name (without r/)',
|
||||
condition: {
|
||||
field: 'action',
|
||||
field: 'operation',
|
||||
value: ['get_posts', 'get_comments'],
|
||||
},
|
||||
},
|
||||
@@ -69,7 +69,7 @@ export const RedditBlock: BlockConfig<
|
||||
{ label: 'Rising', id: 'rising' },
|
||||
],
|
||||
condition: {
|
||||
field: 'action',
|
||||
field: 'operation',
|
||||
value: 'get_posts',
|
||||
},
|
||||
},
|
||||
@@ -86,7 +86,7 @@ export const RedditBlock: BlockConfig<
|
||||
{ label: 'All Time', id: 'all' },
|
||||
],
|
||||
condition: {
|
||||
field: 'action',
|
||||
field: 'operation',
|
||||
value: 'get_posts',
|
||||
and: {
|
||||
field: 'sort',
|
||||
@@ -101,7 +101,7 @@ export const RedditBlock: BlockConfig<
|
||||
layout: 'full',
|
||||
placeholder: '10',
|
||||
condition: {
|
||||
field: 'action',
|
||||
field: 'operation',
|
||||
value: 'get_posts',
|
||||
},
|
||||
},
|
||||
@@ -114,7 +114,7 @@ export const RedditBlock: BlockConfig<
|
||||
layout: 'full',
|
||||
placeholder: 'Enter post ID',
|
||||
condition: {
|
||||
field: 'action',
|
||||
field: 'operation',
|
||||
value: 'get_comments',
|
||||
},
|
||||
},
|
||||
@@ -133,7 +133,7 @@ export const RedditBlock: BlockConfig<
|
||||
{ label: 'Q&A', id: 'qa' },
|
||||
],
|
||||
condition: {
|
||||
field: 'action',
|
||||
field: 'operation',
|
||||
value: 'get_comments',
|
||||
},
|
||||
},
|
||||
@@ -144,28 +144,28 @@ export const RedditBlock: BlockConfig<
|
||||
layout: 'full',
|
||||
placeholder: '50',
|
||||
condition: {
|
||||
field: 'action',
|
||||
field: 'operation',
|
||||
value: 'get_comments',
|
||||
},
|
||||
},
|
||||
],
|
||||
tools: {
|
||||
access: ['reddit_hot_posts', 'reddit_get_posts', 'reddit_get_comments'],
|
||||
access: ['reddit_get_posts', 'reddit_get_comments'],
|
||||
config: {
|
||||
tool: (inputs) => {
|
||||
const action = inputs.action || 'get_posts'
|
||||
const operation = inputs.operation || 'get_posts'
|
||||
|
||||
if (action === 'get_comments') {
|
||||
if (operation === 'get_comments') {
|
||||
return 'reddit_get_comments'
|
||||
}
|
||||
|
||||
return 'reddit_get_posts'
|
||||
},
|
||||
params: (inputs) => {
|
||||
const action = inputs.action || 'get_posts'
|
||||
const operation = inputs.operation || 'get_posts'
|
||||
const { credential, ...rest } = inputs
|
||||
|
||||
if (action === 'get_comments') {
|
||||
if (operation === 'get_comments') {
|
||||
return {
|
||||
postId: rest.postId,
|
||||
subreddit: rest.subreddit,
|
||||
@@ -186,7 +186,7 @@ export const RedditBlock: BlockConfig<
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
action: { type: 'string', required: true },
|
||||
operation: { type: 'string', required: true },
|
||||
credential: { type: 'string', required: true },
|
||||
subreddit: { type: 'string', required: true },
|
||||
sort: { type: 'string', required: true },
|
||||
|
||||
@@ -120,7 +120,10 @@ export const RouterBlock: BlockConfig<RouterResponse> = {
|
||||
options: () => {
|
||||
const ollamaModels = useOllamaStore.getState().models
|
||||
const baseModels = Object.keys(getBaseModelProviders())
|
||||
return [...baseModels, ...ollamaModels]
|
||||
return [...baseModels, ...ollamaModels].map((model) => ({
|
||||
label: model,
|
||||
id: model,
|
||||
}))
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -25,28 +25,53 @@ export const SerperBlock: BlockConfig<SearchResponse> = {
|
||||
title: 'Search Type',
|
||||
type: 'dropdown',
|
||||
layout: 'half',
|
||||
options: ['search', 'news', 'places', 'images'],
|
||||
options: [
|
||||
{ label: 'search', id: 'search' },
|
||||
{ label: 'news', id: 'news' },
|
||||
{ label: 'places', id: 'places' },
|
||||
{ label: 'images', id: 'images' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'num',
|
||||
title: 'Number of Results',
|
||||
type: 'dropdown',
|
||||
layout: 'half',
|
||||
options: ['10', '20', '30', '40', '50', '100'],
|
||||
options: [
|
||||
{ label: '10', id: '10' },
|
||||
{ label: '20', id: '20' },
|
||||
{ label: '30', id: '30' },
|
||||
{ label: '40', id: '40' },
|
||||
{ label: '50', id: '50' },
|
||||
{ label: '100', id: '100' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'gl',
|
||||
title: 'Country',
|
||||
type: 'dropdown',
|
||||
layout: 'half',
|
||||
options: ['US', 'GB', 'CA', 'AU', 'DE', 'FR', 'ES', 'IT', 'JP', 'KR'],
|
||||
options: [
|
||||
{ label: 'US', id: 'US' },
|
||||
{ label: 'GB', id: 'GB' },
|
||||
{ label: 'CA', id: 'CA' },
|
||||
{ label: 'AU', id: 'AU' },
|
||||
{ label: 'DE', id: 'DE' },
|
||||
{ label: 'FR', id: 'FR' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'hl',
|
||||
title: 'Language',
|
||||
type: 'dropdown',
|
||||
layout: 'half',
|
||||
options: ['en', 'es', 'fr', 'de', 'it', 'pt', 'ja', 'ko', 'zh'],
|
||||
options: [
|
||||
{ label: 'en', id: 'en' },
|
||||
{ label: 'es', id: 'es' },
|
||||
{ label: 'fr', id: 'fr' },
|
||||
{ label: 'de', id: 'de' },
|
||||
{ label: 'it', id: 'it' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'apiKey',
|
||||
|
||||
@@ -76,10 +76,6 @@ export const SlackBlock: BlockConfig<SlackResponse> = {
|
||||
layout: 'full',
|
||||
provider: 'slack',
|
||||
placeholder: 'Select Slack channel',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['send'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'text',
|
||||
@@ -87,10 +83,6 @@ export const SlackBlock: BlockConfig<SlackResponse> = {
|
||||
type: 'long-input',
|
||||
layout: 'full',
|
||||
placeholder: 'Enter your message (supports Slack mrkdwn)',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['send'],
|
||||
},
|
||||
},
|
||||
],
|
||||
tools: {
|
||||
|
||||
@@ -101,12 +101,12 @@ export const SupabaseBlock: BlockConfig<SupabaseResponse> = {
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
operation: { type: 'string', required: true, requiredForToolCall: true },
|
||||
projectId: { type: 'string', required: true, requiredForToolCall: true },
|
||||
table: { type: 'string', required: true, requiredForToolCall: true },
|
||||
apiKey: { type: 'string', required: true, requiredForToolCall: true },
|
||||
operation: { type: 'string', required: true },
|
||||
projectId: { type: 'string', required: true },
|
||||
table: { type: 'string', required: true },
|
||||
apiKey: { type: 'string', required: true },
|
||||
// Insert operation inputs
|
||||
data: { type: 'string', required: false, requiredForToolCall: true },
|
||||
data: { type: 'string', required: false },
|
||||
},
|
||||
outputs: {
|
||||
message: 'string',
|
||||
|
||||
@@ -67,7 +67,10 @@ export const TavilyBlock: BlockConfig<TavilyResponse> = {
|
||||
title: 'Extract Depth',
|
||||
type: 'dropdown',
|
||||
layout: 'full',
|
||||
options: ['basic', 'advanced'],
|
||||
options: [
|
||||
{ label: 'basic', id: 'basic' },
|
||||
{ label: 'advanced', id: 'advanced' },
|
||||
],
|
||||
value: () => 'basic',
|
||||
condition: { field: 'operation', value: 'tavily_extract' },
|
||||
},
|
||||
|
||||
@@ -44,7 +44,7 @@ export const TranslateBlock: BlockConfig = {
|
||||
title: 'Model',
|
||||
type: 'dropdown',
|
||||
layout: 'half',
|
||||
options: Object.keys(getBaseModelProviders()),
|
||||
options: Object.keys(getBaseModelProviders()).map((key) => ({ label: key, id: key })),
|
||||
},
|
||||
{
|
||||
id: 'apiKey',
|
||||
|
||||
@@ -25,7 +25,11 @@ export const VisionBlock: BlockConfig<VisionResponse> = {
|
||||
title: 'Vision Model',
|
||||
type: 'dropdown',
|
||||
layout: 'half',
|
||||
options: ['gpt-4o', 'claude-3-opus-20240229', 'claude-3-sonnet-20240229'],
|
||||
options: [
|
||||
{ label: 'gpt-4o', id: 'gpt-4o' },
|
||||
{ label: 'claude-3-opus', id: 'claude-3-opus-20240229' },
|
||||
{ label: 'claude-3-sonnet', id: 'claude-3-sonnet-20240229' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'prompt',
|
||||
|
||||
221
apps/sim/blocks/blocks/wealthbox.ts
Normal file
221
apps/sim/blocks/blocks/wealthbox.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
import { WealthboxIcon } from '@/components/icons'
|
||||
import type { WealthboxReadResponse, WealthboxWriteResponse } from '@/tools/wealthbox/types'
|
||||
import type { BlockConfig } from '../types'
|
||||
|
||||
type WealthboxResponse = WealthboxReadResponse | WealthboxWriteResponse
|
||||
|
||||
export const WealthboxBlock: BlockConfig<WealthboxResponse> = {
|
||||
type: 'wealthbox',
|
||||
name: 'Wealthbox',
|
||||
description: 'Interact with Wealthbox',
|
||||
longDescription:
|
||||
'Integrate Wealthbox functionality to manage notes, contacts, and tasks. Read content from existing notes, contacts, and tasks and write to them using OAuth authentication. Supports text content manipulation for note creation and editing.',
|
||||
docsLink: 'https://docs.simstudio.ai/tools/wealthbox',
|
||||
category: 'tools',
|
||||
bgColor: '#E0E0E0',
|
||||
icon: WealthboxIcon,
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'operation',
|
||||
title: 'Operation',
|
||||
type: 'dropdown',
|
||||
layout: 'full',
|
||||
options: [
|
||||
{ label: 'Read Note', id: 'read_note' },
|
||||
{ label: 'Write Note', id: 'write_note' },
|
||||
{ label: 'Read Contact', id: 'read_contact' },
|
||||
{ label: 'Write Contact', id: 'write_contact' },
|
||||
{ label: 'Read Task', id: 'read_task' },
|
||||
{ label: 'Write Task', id: 'write_task' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'credential',
|
||||
title: 'Wealthbox Account',
|
||||
type: 'oauth-input',
|
||||
layout: 'full',
|
||||
provider: 'wealthbox',
|
||||
serviceId: 'wealthbox',
|
||||
requiredScopes: ['login', 'data'],
|
||||
placeholder: 'Select Wealthbox account',
|
||||
},
|
||||
{
|
||||
id: 'noteId',
|
||||
title: 'Note ID',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
placeholder: 'Enter Note ID (optional)',
|
||||
condition: { field: 'operation', value: ['read_note'] },
|
||||
},
|
||||
{
|
||||
id: 'contactId',
|
||||
title: 'Select Contact',
|
||||
type: 'file-selector',
|
||||
provider: 'wealthbox',
|
||||
serviceId: 'wealthbox',
|
||||
requiredScopes: ['login', 'data'],
|
||||
layout: 'full',
|
||||
placeholder: 'Enter Contact ID',
|
||||
condition: { field: 'operation', value: ['read_contact', 'write_task', 'write_note'] },
|
||||
},
|
||||
{
|
||||
id: 'taskId',
|
||||
title: 'Select Task',
|
||||
type: 'short-input',
|
||||
provider: 'wealthbox',
|
||||
serviceId: 'wealthbox',
|
||||
requiredScopes: ['login', 'data'],
|
||||
layout: 'full',
|
||||
placeholder: 'Enter Task ID',
|
||||
condition: { field: 'operation', value: ['read_task'] },
|
||||
},
|
||||
{
|
||||
id: 'title',
|
||||
title: 'Title',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
placeholder: 'Enter Title',
|
||||
condition: { field: 'operation', value: ['write_task'] },
|
||||
},
|
||||
{
|
||||
id: 'dueDate',
|
||||
title: 'Due Date',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
placeholder: 'Enter due date (e.g., 2015-05-24 11:00 AM -0400)',
|
||||
condition: { field: 'operation', value: ['write_task'] },
|
||||
},
|
||||
{
|
||||
id: 'firstName',
|
||||
title: 'First Name',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
placeholder: 'Enter First Name',
|
||||
condition: { field: 'operation', value: ['write_contact'] },
|
||||
},
|
||||
{
|
||||
id: 'lastName',
|
||||
title: 'Last Name',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
placeholder: 'Enter Last Name',
|
||||
condition: { field: 'operation', value: ['write_contact'] },
|
||||
},
|
||||
{
|
||||
id: 'emailAddress',
|
||||
title: 'Email Address',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
placeholder: 'Enter Email Address',
|
||||
condition: { field: 'operation', value: ['write_contact'] },
|
||||
},
|
||||
{
|
||||
id: 'content',
|
||||
title: 'Content',
|
||||
type: 'long-input',
|
||||
layout: 'full',
|
||||
placeholder: 'Enter Content',
|
||||
condition: { field: 'operation', value: ['write_note', 'write_event', 'write_task'] },
|
||||
},
|
||||
{
|
||||
id: 'backgroundInformation',
|
||||
title: 'Background Information',
|
||||
type: 'long-input',
|
||||
layout: 'full',
|
||||
placeholder: 'Enter Background Information',
|
||||
condition: { field: 'operation', value: ['write_contact'] },
|
||||
},
|
||||
],
|
||||
tools: {
|
||||
access: [
|
||||
'wealthbox_read_note',
|
||||
'wealthbox_write_note',
|
||||
'wealthbox_read_contact',
|
||||
'wealthbox_write_contact',
|
||||
'wealthbox_read_task',
|
||||
'wealthbox_write_task',
|
||||
],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
switch (params.operation) {
|
||||
case 'read_note':
|
||||
return 'wealthbox_read_note'
|
||||
case 'write_note':
|
||||
return 'wealthbox_write_note'
|
||||
case 'read_contact':
|
||||
return 'wealthbox_read_contact'
|
||||
case 'write_contact':
|
||||
return 'wealthbox_write_contact'
|
||||
case 'read_task':
|
||||
return 'wealthbox_read_task'
|
||||
case 'write_task':
|
||||
return 'wealthbox_write_task'
|
||||
default:
|
||||
throw new Error(`Unknown operation: ${params.operation}`)
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
const { credential, operation, ...rest } = params
|
||||
|
||||
// Build the parameters based on operation type
|
||||
const baseParams = {
|
||||
...rest,
|
||||
credential,
|
||||
}
|
||||
|
||||
// For note operations, we need noteId
|
||||
if (operation === 'read_note' || operation === 'write_note') {
|
||||
return {
|
||||
...baseParams,
|
||||
noteId: params.noteId,
|
||||
}
|
||||
}
|
||||
|
||||
// For contact operations, we need contactId
|
||||
if (operation === 'read_contact') {
|
||||
if (!params.contactId) {
|
||||
throw new Error('Contact ID is required for contact operations')
|
||||
}
|
||||
return {
|
||||
...baseParams,
|
||||
contactId: params.contactId,
|
||||
}
|
||||
}
|
||||
|
||||
// For task operations, we need taskId
|
||||
if (operation === 'read_task') {
|
||||
return {
|
||||
...baseParams,
|
||||
taskId: params.taskId,
|
||||
}
|
||||
}
|
||||
|
||||
return baseParams
|
||||
},
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
operation: { type: 'string', required: true },
|
||||
credential: { type: 'string', required: true },
|
||||
noteId: { type: 'string', required: false },
|
||||
contactId: { type: 'string', required: false },
|
||||
taskId: { type: 'string', required: false },
|
||||
content: { type: 'string', required: false },
|
||||
firstName: { type: 'string', required: false },
|
||||
lastName: { type: 'string', required: false },
|
||||
emailAddress: { type: 'string', required: false },
|
||||
backgroundInformation: { type: 'string', required: false },
|
||||
title: { type: 'string', required: false },
|
||||
dueDate: { type: 'string', required: false },
|
||||
},
|
||||
outputs: {
|
||||
note: 'any',
|
||||
notes: 'any',
|
||||
contact: 'any',
|
||||
contacts: 'any',
|
||||
task: 'any',
|
||||
tasks: 'any',
|
||||
metadata: 'json',
|
||||
success: 'any',
|
||||
},
|
||||
}
|
||||
@@ -79,7 +79,10 @@ export const XBlock: BlockConfig<XResponse> = {
|
||||
title: 'Include Replies',
|
||||
type: 'dropdown',
|
||||
layout: 'full',
|
||||
options: ['true', 'false'],
|
||||
options: [
|
||||
{ label: 'true', id: 'true' },
|
||||
{ label: 'false', id: 'false' },
|
||||
],
|
||||
value: () => 'false',
|
||||
condition: { field: 'operation', value: 'x_read' },
|
||||
},
|
||||
@@ -105,7 +108,10 @@ export const XBlock: BlockConfig<XResponse> = {
|
||||
title: 'Sort Order',
|
||||
type: 'dropdown',
|
||||
layout: 'full',
|
||||
options: ['recency', 'relevancy'],
|
||||
options: [
|
||||
{ label: 'recency', id: 'recency' },
|
||||
{ label: 'relevancy', id: 'relevancy' },
|
||||
],
|
||||
value: () => 'recency',
|
||||
condition: { field: 'operation', value: 'x_search' },
|
||||
},
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
import { AgentBlock } from './blocks/agent'
|
||||
import { AirtableBlock } from './blocks/airtable'
|
||||
import { ApiBlock } from './blocks/api'
|
||||
// import { AutoblocksBlock } from './blocks/autoblocks'
|
||||
import { BrowserUseBlock } from './blocks/browser_use'
|
||||
import { ClayBlock } from './blocks/clay'
|
||||
import { ConditionBlock } from './blocks/condition'
|
||||
@@ -26,7 +25,6 @@ import { GoogleDocsBlock } from './blocks/google_docs'
|
||||
import { GoogleDriveBlock } from './blocks/google_drive'
|
||||
import { GoogleSheetsBlock } from './blocks/google_sheets'
|
||||
import { HuggingFaceBlock } from './blocks/huggingface'
|
||||
// import { GuestyBlock } from './blocks/guesty'
|
||||
import { ImageGeneratorBlock } from './blocks/image_generator'
|
||||
import { JinaBlock } from './blocks/jina'
|
||||
import { JiraBlock } from './blocks/jira'
|
||||
@@ -34,7 +32,6 @@ import { KnowledgeBlock } from './blocks/knowledge'
|
||||
import { LinearBlock } from './blocks/linear'
|
||||
import { LinkupBlock } from './blocks/linkup'
|
||||
import { Mem0Block } from './blocks/mem0'
|
||||
// import { GuestyBlock } from './blocks/guesty'
|
||||
import { MemoryBlock } from './blocks/memory'
|
||||
import { MicrosoftExcelBlock } from './blocks/microsoft_excel'
|
||||
import { MicrosoftTeamsBlock } from './blocks/microsoft_teams'
|
||||
@@ -61,6 +58,7 @@ import { TranslateBlock } from './blocks/translate'
|
||||
import { TwilioSMSBlock } from './blocks/twilio'
|
||||
import { TypeformBlock } from './blocks/typeform'
|
||||
import { VisionBlock } from './blocks/vision'
|
||||
import { WealthboxBlock } from './blocks/wealthbox'
|
||||
import { WhatsAppBlock } from './blocks/whatsapp'
|
||||
import { WorkflowBlock } from './blocks/workflow'
|
||||
import { XBlock } from './blocks/x'
|
||||
@@ -72,7 +70,6 @@ export const registry: Record<string, BlockConfig> = {
|
||||
agent: AgentBlock,
|
||||
airtable: AirtableBlock,
|
||||
api: ApiBlock,
|
||||
// autoblocks: AutoblocksBlock,
|
||||
browser_use: BrowserUseBlock,
|
||||
clay: ClayBlock,
|
||||
condition: ConditionBlock,
|
||||
@@ -91,15 +88,17 @@ export const registry: Record<string, BlockConfig> = {
|
||||
google_drive: GoogleDriveBlock,
|
||||
google_search: GoogleSearchBlock,
|
||||
google_sheets: GoogleSheetsBlock,
|
||||
microsoft_excel: MicrosoftExcelBlock,
|
||||
microsoft_teams: MicrosoftTeamsBlock,
|
||||
// guesty: GuestyBlock,
|
||||
huggingface: HuggingFaceBlock,
|
||||
image_generator: ImageGeneratorBlock,
|
||||
jina: JinaBlock,
|
||||
jira: JiraBlock,
|
||||
knowledge: KnowledgeBlock,
|
||||
linear: LinearBlock,
|
||||
linkup: LinkupBlock,
|
||||
mem0: Mem0Block,
|
||||
memory: MemoryBlock,
|
||||
microsoft_excel: MicrosoftExcelBlock,
|
||||
microsoft_teams: MicrosoftTeamsBlock,
|
||||
mistral_parse: MistralParseBlock,
|
||||
notion: NotionBlock,
|
||||
openai: OpenAIBlock,
|
||||
@@ -107,15 +106,14 @@ export const registry: Record<string, BlockConfig> = {
|
||||
perplexity: PerplexityBlock,
|
||||
pinecone: PineconeBlock,
|
||||
reddit: RedditBlock,
|
||||
response: ResponseBlock,
|
||||
router: RouterBlock,
|
||||
memory: MemoryBlock,
|
||||
s3: S3Block,
|
||||
serper: SerperBlock,
|
||||
stagehand: StagehandBlock,
|
||||
stagehand_agent: StagehandAgentBlock,
|
||||
slack: SlackBlock,
|
||||
starter: StarterBlock,
|
||||
knowledge: KnowledgeBlock,
|
||||
supabase: SupabaseBlock,
|
||||
tavily: TavilyBlock,
|
||||
telegram: TelegramBlock,
|
||||
@@ -124,12 +122,11 @@ export const registry: Record<string, BlockConfig> = {
|
||||
twilio_sms: TwilioSMSBlock,
|
||||
typeform: TypeformBlock,
|
||||
vision: VisionBlock,
|
||||
wealthbox: WealthboxBlock,
|
||||
whatsapp: WhatsAppBlock,
|
||||
workflow: WorkflowBlock,
|
||||
x: XBlock,
|
||||
youtube: YouTubeBlock,
|
||||
huggingface: HuggingFaceBlock,
|
||||
response: ResponseBlock,
|
||||
}
|
||||
|
||||
// Helper functions to access the registry
|
||||
|
||||
@@ -68,7 +68,6 @@ export type BlockOutput =
|
||||
export interface ParamConfig {
|
||||
type: ParamType
|
||||
required: boolean
|
||||
requiredForToolCall?: boolean
|
||||
description?: string
|
||||
schema?: {
|
||||
type: string
|
||||
@@ -92,11 +91,8 @@ export interface SubBlockConfig {
|
||||
layout?: SubBlockLayout
|
||||
mode?: 'basic' | 'advanced' | 'both' // Default is 'both' if not specified
|
||||
options?:
|
||||
| string[]
|
||||
| { label: string; id: string; icon?: React.ComponentType<{ className?: string }> }[]
|
||||
| (() =>
|
||||
| string[]
|
||||
| { label: string; id: string; icon?: React.ComponentType<{ className?: string }> }[])
|
||||
| (() => { label: string; id: string; icon?: React.ComponentType<{ className?: string }> }[])
|
||||
min?: number
|
||||
max?: number
|
||||
columns?: string[]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user