mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-31 09:48:06 -05:00
Compare commits
6 Commits
fix/billin
...
feat/gm
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b24a47308 | ||
|
|
0e7b8430e9 | ||
|
|
cf2f1abcaf | ||
|
|
4109feecf6 | ||
|
|
37d5e01f5f | ||
|
|
2d799b3272 |
File diff suppressed because one or more lines are too long
@@ -24,6 +24,7 @@ import {
|
||||
DiscordIcon,
|
||||
DocumentIcon,
|
||||
DropboxIcon,
|
||||
DsPyIcon,
|
||||
DuckDuckGoIcon,
|
||||
DynamoDBIcon,
|
||||
ElasticsearchIcon,
|
||||
@@ -41,6 +42,7 @@ import {
|
||||
GoogleFormsIcon,
|
||||
GoogleGroupsIcon,
|
||||
GoogleIcon,
|
||||
GoogleMapsIcon,
|
||||
GoogleSheetsIcon,
|
||||
GoogleSlidesIcon,
|
||||
GoogleVaultIcon,
|
||||
@@ -153,6 +155,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
datadog: DatadogIcon,
|
||||
discord: DiscordIcon,
|
||||
dropbox: DropboxIcon,
|
||||
dspy: DsPyIcon,
|
||||
duckduckgo: DuckDuckGoIcon,
|
||||
dynamodb: DynamoDBIcon,
|
||||
elasticsearch: ElasticsearchIcon,
|
||||
@@ -169,6 +172,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
google_drive: GoogleDriveIcon,
|
||||
google_forms: GoogleFormsIcon,
|
||||
google_groups: GoogleGroupsIcon,
|
||||
google_maps: GoogleMapsIcon,
|
||||
google_search: GoogleIcon,
|
||||
google_sheets_v2: GoogleSheetsIcon,
|
||||
google_slides: GoogleSlidesIcon,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: CalCom
|
||||
title: Cal Com
|
||||
description: Manage Cal.com bookings, event types, schedules, and availability
|
||||
---
|
||||
|
||||
|
||||
112
apps/docs/content/docs/en/tools/dspy.mdx
Normal file
112
apps/docs/content/docs/en/tools/dspy.mdx
Normal file
@@ -0,0 +1,112 @@
|
||||
---
|
||||
title: DSPy
|
||||
description: Run predictions using self-hosted DSPy programs
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="dspy"
|
||||
color="#1E293B"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[DSPy](https://github.com/stanford-oval/dspy) is an open-source framework for programming—rather than prompting—language models. DSPy enables you to build interpretable and modular LLM-powered agents using Python functions, structured modules, and declarative signatures, making it easy to compose, debug, and reliably deploy language model applications.
|
||||
|
||||
With DSPy in Sim, you can:
|
||||
|
||||
- **Run custom predictions**: Connect your self-hosted DSPy server and invoke prediction endpoints for a variety of natural language tasks.
|
||||
- **Chain of Thought and ReAct reasoning**: Leverage advanced DSPy modules for step-by-step reasoning, multi-turn dialogs, and action-observation loops.
|
||||
- **Integrate with your workflows**: Automate LLM predictions and reasoning as part of any Sim automation or agent routine.
|
||||
- **Provide custom endpoints and context**: Flexibly call your own DSPy-powered APIs with custom authentication, endpoints, input fields, and context.
|
||||
|
||||
These features let your Sim agents access modular, interpretable LLM-based programs for tasks like question answering, document analysis, decision support, and more—where you remain in control of the model, data, and logic.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate with your self-hosted DSPy programs for LLM-powered predictions. Supports Predict, Chain of Thought, and ReAct agents. DSPy is the framework for programming—not prompting—language models.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `dspy_predict`
|
||||
|
||||
Run a prediction using a self-hosted DSPy program endpoint
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `baseUrl` | string | Yes | Base URL of the DSPy server \(e.g., https://your-dspy-server.com\) |
|
||||
| `apiKey` | string | No | API key for authentication \(if required by your server\) |
|
||||
| `endpoint` | string | No | API endpoint path \(defaults to /predict\) |
|
||||
| `input` | string | Yes | The input text to send to the DSPy program |
|
||||
| `inputField` | string | No | Name of the input field expected by the DSPy program \(defaults to "text"\) |
|
||||
| `context` | string | No | Additional context to provide to the DSPy program |
|
||||
| `additionalInputs` | json | No | Additional key-value pairs to include in the request body |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `answer` | string | The main output/answer from the DSPy program |
|
||||
| `reasoning` | string | The reasoning or rationale behind the answer \(if available\) |
|
||||
| `status` | string | Response status from the DSPy server \(success or error\) |
|
||||
| `rawOutput` | json | The complete raw output from the DSPy program \(result.toDict\(\)\) |
|
||||
|
||||
### `dspy_chain_of_thought`
|
||||
|
||||
Run a Chain of Thought prediction using a self-hosted DSPy ChainOfThought program endpoint
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `baseUrl` | string | Yes | Base URL of the DSPy server \(e.g., https://your-dspy-server.com\) |
|
||||
| `apiKey` | string | No | API key for authentication \(if required by your server\) |
|
||||
| `endpoint` | string | No | API endpoint path \(defaults to /predict\) |
|
||||
| `question` | string | Yes | The question to answer using chain of thought reasoning |
|
||||
| `context` | string | No | Additional context to provide for answering the question |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `answer` | string | The answer generated through chain of thought reasoning |
|
||||
| `reasoning` | string | The step-by-step reasoning that led to the answer |
|
||||
| `status` | string | Response status from the DSPy server \(success or error\) |
|
||||
| `rawOutput` | json | The complete raw output from the DSPy program \(result.toDict\(\)\) |
|
||||
|
||||
### `dspy_react`
|
||||
|
||||
Run a ReAct agent using a self-hosted DSPy ReAct program endpoint for multi-step reasoning and action
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `baseUrl` | string | Yes | Base URL of the DSPy server \(e.g., https://your-dspy-server.com\) |
|
||||
| `apiKey` | string | No | API key for authentication \(if required by your server\) |
|
||||
| `endpoint` | string | No | API endpoint path \(defaults to /predict\) |
|
||||
| `task` | string | Yes | The task or question for the ReAct agent to work on |
|
||||
| `context` | string | No | Additional context to provide for the task |
|
||||
| `maxIterations` | number | No | Maximum number of reasoning iterations \(defaults to server setting\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `answer` | string | The final answer or result from the ReAct agent |
|
||||
| `reasoning` | string | The overall reasoning summary from the agent |
|
||||
| `trajectory` | array | The step-by-step trajectory of thoughts, actions, and observations |
|
||||
| ↳ `thought` | string | The reasoning thought at this step |
|
||||
| ↳ `toolName` | string | The name of the tool/action called |
|
||||
| ↳ `toolArgs` | json | Arguments passed to the tool |
|
||||
| ↳ `observation` | string | The observation/result from the tool execution |
|
||||
| `status` | string | Response status from the DSPy server \(success or error\) |
|
||||
| `rawOutput` | json | The complete raw output from the DSPy program \(result.toDict\(\)\) |
|
||||
|
||||
|
||||
@@ -10,6 +10,23 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
color="#E8F0FE"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Google Groups](https://groups.google.com) is part of Google Workspace, providing email-based group communication, collaboration, and access control for teams and organizations. Google Groups lets you create mailing lists, manage membership, and control permissions for both internal and external users.
|
||||
|
||||
This page explains how you can use Sim to automate the management of Google Groups in your workflows. With Sim, agents can create and configure groups, add or remove members, update group settings, and keep directory lists up-to-date automatically—ideal for onboarding workflows, syncing IT systems, or dynamically managing project teams.
|
||||
|
||||
With Google Groups, you can:
|
||||
|
||||
- **Centralize communications**: Create team or project mailing lists for group conversations
|
||||
- **Manage group membership**: Add, remove, or update members with granular roles (owner, manager, member)
|
||||
- **Control access**: Manage who can view, post, or join; set permissions for public/private visibility
|
||||
- **Collaborate across teams**: Streamline communication and document sharing via group-based access
|
||||
- **Automate IT tasks**: Use Sim to keep group memberships current as teams change
|
||||
|
||||
In Sim, the Google Groups integration gives your agents API-driven control to automate common administrative tasks. Connect directly to your Google Workspace domain to add users to groups, manage lists, audit group settings, and ensure your organization’s access controls are always up-to-date—without manual overhead.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Connect to Google Workspace to create, update, and manage groups and their members using the Admin SDK Directory API.
|
||||
|
||||
450
apps/docs/content/docs/en/tools/google_maps.mdx
Normal file
450
apps/docs/content/docs/en/tools/google_maps.mdx
Normal file
@@ -0,0 +1,450 @@
|
||||
---
|
||||
title: Google Maps
|
||||
description: Geocoding, directions, places, and distance calculations
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="google_maps"
|
||||
color="#E0E0E0"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Google Maps](https://maps.google.com) is a comprehensive platform offering a wide array of APIs for mapping, geocoding, routing, places, environment data, and more. Through Sim, your agents can leverage key Google Maps Platform APIs to automate a variety of location-based workflows.
|
||||
|
||||
**The following Google Maps APIs are included in this integration:**
|
||||
|
||||
- **Geocoding API:** Convert addresses into latitude/longitude coordinates and perform reverse geocoding.
|
||||
- **Directions API:** Calculate driving, walking, cycling, or transit directions and routes between locations.
|
||||
- **Distance Matrix API:** Compute travel distances and times for multiple origin and destination combinations.
|
||||
- **Places API:** Search for places (businesses, landmarks, establishments) by name, type, or proximity.
|
||||
- **Place Details API:** Retrieve detailed information for a specific place, such as address, ratings, hours, and contact info.
|
||||
- **Elevation API:** Obtain elevation data (height above sea level) for any set of locations globally.
|
||||
- **Time Zone API:** Look up time zone information for any geographic location.
|
||||
- **Air Quality API:** Fetch real-time air quality data for specific coordinates.
|
||||
|
||||
With these APIs, your Sim agents can automate location lookup and enrichment, plan optimal routes and deliveries, estimate times and distances, analyze place data, enrich records with geographic context, get environmental conditions, and more—all without manual work or external tools.
|
||||
|
||||
If you need capabilities beyond what's listed here or want to request support for additional Google Maps APIs, let us know!
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Google Maps Platform APIs into your workflow. Supports geocoding addresses to coordinates, reverse geocoding, getting directions between locations, calculating distance matrices, searching for places, retrieving place details, elevation data, and timezone information.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `google_maps_air_quality`
|
||||
|
||||
Get current air quality data for a location
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Google Maps API key with Air Quality API enabled |
|
||||
| `lat` | number | Yes | Latitude coordinate |
|
||||
| `lng` | number | Yes | Longitude coordinate |
|
||||
| `languageCode` | string | No | Language code for the response \(e.g., "en", "es"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `dateTime` | string | Timestamp of the air quality data |
|
||||
| `regionCode` | string | Region code for the location |
|
||||
| `indexes` | array | Array of air quality indexes |
|
||||
| ↳ `code` | string | Index code \(e.g., "uaqi", "usa_epa"\) |
|
||||
| ↳ `displayName` | string | Display name of the index |
|
||||
| ↳ `aqi` | number | Air quality index value |
|
||||
| ↳ `aqiDisplay` | string | Formatted AQI display string |
|
||||
| ↳ `color` | object | RGB color for the AQI level |
|
||||
| ↳ `category` | string | Category description \(e.g., "Good", "Moderate"\) |
|
||||
| ↳ `dominantPollutant` | string | The dominant pollutant |
|
||||
| `pollutants` | array | Array of pollutant concentrations |
|
||||
| ↳ `code` | string | Pollutant code \(e.g., "pm25", "o3"\) |
|
||||
| ↳ `displayName` | string | Display name |
|
||||
| ↳ `fullName` | string | Full pollutant name |
|
||||
| ↳ `concentration` | object | Concentration info |
|
||||
| ↳ `value` | number | Concentration value |
|
||||
| ↳ `units` | string | Units \(e.g., "PARTS_PER_BILLION"\) |
|
||||
| ↳ `additionalInfo` | object | Additional info about sources and effects |
|
||||
| `healthRecommendations` | object | Health recommendations for different populations |
|
||||
|
||||
### `google_maps_directions`
|
||||
|
||||
Get directions and route information between two locations
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Google Maps API key |
|
||||
| `origin` | string | Yes | Starting location \(address or lat,lng\) |
|
||||
| `destination` | string | Yes | Destination location \(address or lat,lng\) |
|
||||
| `mode` | string | No | Travel mode: driving, walking, bicycling, or transit |
|
||||
| `avoid` | string | No | Features to avoid: tolls, highways, or ferries |
|
||||
| `waypoints` | json | No | Array of intermediate waypoints |
|
||||
| `units` | string | No | Unit system: metric or imperial |
|
||||
| `language` | string | No | Language code for results \(e.g., en, es, fr\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `routes` | array | All available routes |
|
||||
| ↳ `summary` | string | Route summary \(main road names\) |
|
||||
| ↳ `legs` | array | Route legs \(segments between waypoints\) |
|
||||
| ↳ `overviewPolyline` | string | Encoded polyline for the entire route |
|
||||
| ↳ `warnings` | array | Route warnings |
|
||||
| ↳ `waypointOrder` | array | Optimized waypoint order \(if requested\) |
|
||||
| `distanceText` | string | Total distance as human-readable text \(e.g., "5.2 km"\) |
|
||||
| `distanceMeters` | number | Total distance in meters |
|
||||
| `durationText` | string | Total duration as human-readable text \(e.g., "15 mins"\) |
|
||||
| `durationSeconds` | number | Total duration in seconds |
|
||||
| `startAddress` | string | Resolved starting address |
|
||||
| `endAddress` | string | Resolved ending address |
|
||||
| `steps` | array | Turn-by-turn navigation instructions |
|
||||
| ↳ `instruction` | string | Navigation instruction \(HTML stripped\) |
|
||||
| ↳ `distanceText` | string | Step distance as text |
|
||||
| ↳ `distanceMeters` | number | Step distance in meters |
|
||||
| ↳ `durationText` | string | Step duration as text |
|
||||
| ↳ `durationSeconds` | number | Step duration in seconds |
|
||||
| ↳ `startLocation` | object | Step start coordinates |
|
||||
| ↳ `endLocation` | object | Step end coordinates |
|
||||
| ↳ `travelMode` | string | Travel mode for this step |
|
||||
| ↳ `maneuver` | string | Maneuver type \(turn-left, etc.\) |
|
||||
| `polyline` | string | Encoded polyline for the primary route |
|
||||
|
||||
### `google_maps_distance_matrix`
|
||||
|
||||
Calculate travel distance and time between multiple origins and destinations
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Google Maps API key |
|
||||
| `origin` | string | Yes | Origin location \(address or lat,lng\) |
|
||||
| `destinations` | json | Yes | Array of destination locations |
|
||||
| `mode` | string | No | Travel mode: driving, walking, bicycling, or transit |
|
||||
| `avoid` | string | No | Features to avoid: tolls, highways, or ferries |
|
||||
| `units` | string | No | Unit system: metric or imperial |
|
||||
| `language` | string | No | Language code for results \(e.g., en, es, fr\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `originAddresses` | array | Resolved origin addresses |
|
||||
| `destinationAddresses` | array | Resolved destination addresses |
|
||||
| `rows` | array | Distance matrix rows \(one per origin\) |
|
||||
| ↳ `elements` | array | Elements \(one per destination\) |
|
||||
| ↳ `distanceText` | string | Distance as text \(e.g., "5.2 km"\) |
|
||||
| ↳ `distanceMeters` | number | Distance in meters |
|
||||
| ↳ `durationText` | string | Duration as text \(e.g., "15 mins"\) |
|
||||
| ↳ `durationSeconds` | number | Duration in seconds |
|
||||
| ↳ `durationInTrafficText` | string | Duration in traffic as text |
|
||||
| ↳ `durationInTrafficSeconds` | number | Duration in traffic in seconds |
|
||||
| ↳ `status` | string | Element status \(OK, NOT_FOUND, ZERO_RESULTS\) |
|
||||
|
||||
### `google_maps_elevation`
|
||||
|
||||
Get elevation data for a location
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Google Maps API key |
|
||||
| `lat` | number | Yes | Latitude coordinate |
|
||||
| `lng` | number | Yes | Longitude coordinate |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `elevation` | number | Elevation in meters above sea level \(negative for below\) |
|
||||
| `lat` | number | Latitude of the elevation sample |
|
||||
| `lng` | number | Longitude of the elevation sample |
|
||||
| `resolution` | number | Maximum distance between data points \(meters\) from which elevation was interpolated |
|
||||
|
||||
### `google_maps_geocode`
|
||||
|
||||
Convert an address into geographic coordinates (latitude and longitude)
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Google Maps API key |
|
||||
| `address` | string | Yes | The address to geocode |
|
||||
| `language` | string | No | Language code for results \(e.g., en, es, fr\) |
|
||||
| `region` | string | No | Region bias as a ccTLD code \(e.g., us, uk\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `formattedAddress` | string | The formatted address string |
|
||||
| `lat` | number | Latitude coordinate |
|
||||
| `lng` | number | Longitude coordinate |
|
||||
| `location` | json | Location object with lat and lng |
|
||||
| `placeId` | string | Google Place ID for this location |
|
||||
| `addressComponents` | array | Detailed address components |
|
||||
| ↳ `longName` | string | Full name of the component |
|
||||
| ↳ `shortName` | string | Abbreviated name |
|
||||
| ↳ `types` | array | Component types |
|
||||
| `locationType` | string | Location accuracy type \(ROOFTOP, RANGE_INTERPOLATED, etc.\) |
|
||||
|
||||
### `google_maps_geolocate`
|
||||
|
||||
Geolocate a device using WiFi access points, cell towers, or IP address
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Google Maps API key with Geolocation API enabled |
|
||||
| `homeMobileCountryCode` | number | No | Home mobile country code \(MCC\) |
|
||||
| `homeMobileNetworkCode` | number | No | Home mobile network code \(MNC\) |
|
||||
| `radioType` | string | No | Radio type: lte, gsm, cdma, wcdma, or nr |
|
||||
| `carrier` | string | No | Carrier name |
|
||||
| `considerIp` | boolean | No | Whether to use IP address for geolocation \(default: true\) |
|
||||
| `cellTowers` | array | No | Array of cell tower objects with cellId, locationAreaCode, mobileCountryCode, mobileNetworkCode |
|
||||
| `wifiAccessPoints` | array | No | Array of WiFi access point objects with macAddress \(required\), signalStrength, etc. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `lat` | number | Latitude coordinate |
|
||||
| `lng` | number | Longitude coordinate |
|
||||
| `accuracy` | number | Accuracy radius in meters |
|
||||
|
||||
### `google_maps_place_details`
|
||||
|
||||
Get detailed information about a specific place
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Google Maps API key |
|
||||
| `placeId` | string | Yes | Google Place ID |
|
||||
| `fields` | string | No | Comma-separated list of fields to return |
|
||||
| `language` | string | No | Language code for results \(e.g., en, es, fr\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `placeId` | string | Google Place ID |
|
||||
| `name` | string | Place name |
|
||||
| `formattedAddress` | string | Formatted street address |
|
||||
| `lat` | number | Latitude coordinate |
|
||||
| `lng` | number | Longitude coordinate |
|
||||
| `types` | array | Place types \(e.g., restaurant, cafe\) |
|
||||
| `rating` | number | Average rating \(1.0 to 5.0\) |
|
||||
| `userRatingsTotal` | number | Total number of user ratings |
|
||||
| `priceLevel` | number | Price level \(0=Free, 1=Inexpensive, 2=Moderate, 3=Expensive, 4=Very Expensive\) |
|
||||
| `website` | string | Place website URL |
|
||||
| `phoneNumber` | string | Local formatted phone number |
|
||||
| `internationalPhoneNumber` | string | International formatted phone number |
|
||||
| `openNow` | boolean | Whether the place is currently open |
|
||||
| `weekdayText` | array | Opening hours formatted by day of week |
|
||||
| `reviews` | array | User reviews \(up to 5 most relevant\) |
|
||||
| ↳ `authorName` | string | Reviewer name |
|
||||
| ↳ `authorUrl` | string | Reviewer profile URL |
|
||||
| ↳ `profilePhotoUrl` | string | Reviewer photo URL |
|
||||
| ↳ `rating` | number | Rating given \(1-5\) |
|
||||
| ↳ `text` | string | Review text |
|
||||
| ↳ `time` | number | Review timestamp \(Unix epoch\) |
|
||||
| ↳ `relativeTimeDescription` | string | Relative time \(e.g., "a month ago"\) |
|
||||
| `photos` | array | Place photos |
|
||||
| ↳ `photoReference` | string | Photo reference for Place Photos API |
|
||||
| ↳ `height` | number | Photo height in pixels |
|
||||
| ↳ `width` | number | Photo width in pixels |
|
||||
| ↳ `htmlAttributions` | array | Required attributions |
|
||||
| `url` | string | Google Maps URL for the place |
|
||||
| `utcOffset` | number | UTC offset in minutes |
|
||||
| `vicinity` | string | Simplified address \(neighborhood/street\) |
|
||||
| `businessStatus` | string | Business status \(OPERATIONAL, CLOSED_TEMPORARILY, CLOSED_PERMANENTLY\) |
|
||||
|
||||
### `google_maps_places_search`
|
||||
|
||||
Search for places using a text query
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Google Maps API key |
|
||||
| `query` | string | Yes | Search query \(e.g., "restaurants in Times Square"\) |
|
||||
| `location` | json | No | Location to bias results towards \(\{lat, lng\}\) |
|
||||
| `radius` | number | No | Search radius in meters |
|
||||
| `type` | string | No | Place type filter \(e.g., restaurant, cafe, hotel\) |
|
||||
| `language` | string | No | Language code for results \(e.g., en, es, fr\) |
|
||||
| `region` | string | No | Region bias as a ccTLD code \(e.g., us, uk\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `places` | array | List of places found |
|
||||
| ↳ `placeId` | string | Google Place ID |
|
||||
| ↳ `name` | string | Place name |
|
||||
| ↳ `formattedAddress` | string | Formatted address |
|
||||
| ↳ `lat` | number | Latitude |
|
||||
| ↳ `lng` | number | Longitude |
|
||||
| ↳ `types` | array | Place types |
|
||||
| ↳ `rating` | number | Average rating \(1-5\) |
|
||||
| ↳ `userRatingsTotal` | number | Number of ratings |
|
||||
| ↳ `priceLevel` | number | Price level \(0-4\) |
|
||||
| ↳ `openNow` | boolean | Whether currently open |
|
||||
| ↳ `photoReference` | string | Photo reference for Photos API |
|
||||
| ↳ `businessStatus` | string | Business status |
|
||||
| `nextPageToken` | string | Token for fetching the next page of results |
|
||||
|
||||
### `google_maps_reverse_geocode`
|
||||
|
||||
Convert geographic coordinates (latitude and longitude) into a human-readable address
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Google Maps API key |
|
||||
| `lat` | number | Yes | Latitude coordinate |
|
||||
| `lng` | number | Yes | Longitude coordinate |
|
||||
| `language` | string | No | Language code for results \(e.g., en, es, fr\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `formattedAddress` | string | The formatted address string |
|
||||
| `placeId` | string | Google Place ID for this location |
|
||||
| `addressComponents` | array | Detailed address components |
|
||||
| ↳ `longName` | string | Full name of the component |
|
||||
| ↳ `shortName` | string | Abbreviated name |
|
||||
| ↳ `types` | array | Component types |
|
||||
| `types` | array | Address types \(e.g., street_address, route\) |
|
||||
|
||||
### `google_maps_snap_to_roads`
|
||||
|
||||
Snap GPS coordinates to the nearest road segment
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Google Maps API key with Roads API enabled |
|
||||
| `path` | string | Yes | Pipe-separated list of lat,lng coordinates \(e.g., "60.170880,24.942795\|60.170879,24.942796"\) |
|
||||
| `interpolate` | boolean | No | Whether to interpolate additional points along the road |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `snappedPoints` | array | Array of snapped points on roads |
|
||||
| ↳ `location` | object | Snapped location coordinates |
|
||||
| ↳ `lat` | number | Latitude |
|
||||
| ↳ `lng` | number | Longitude |
|
||||
| ↳ `originalIndex` | number | Index in the original path \(if not interpolated\) |
|
||||
| ↳ `placeId` | string | Place ID for this road segment |
|
||||
| `warningMessage` | string | Warning message if any \(e.g., if points could not be snapped\) |
|
||||
|
||||
### `google_maps_speed_limits`
|
||||
|
||||
Get speed limits for road segments
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Google Maps API key with Roads API enabled |
|
||||
| `path` | string | No | Pipe-separated list of lat,lng coordinates \(e.g., "60.170880,24.942795\|60.170879,24.942796"\) |
|
||||
| `placeIds` | array | No | Array of Place IDs for road segments \(alternative to path\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `speedLimits` | array | Array of speed limits for road segments |
|
||||
| ↳ `placeId` | string | Place ID for the road segment |
|
||||
| ↳ `speedLimit` | number | Speed limit value |
|
||||
| ↳ `units` | string | Speed limit units \(KPH or MPH\) |
|
||||
| `snappedPoints` | array | Array of snapped points corresponding to the speed limits |
|
||||
| ↳ `location` | object | Snapped location coordinates |
|
||||
| ↳ `lat` | number | Latitude |
|
||||
| ↳ `lng` | number | Longitude |
|
||||
| ↳ `originalIndex` | number | Index in the original path |
|
||||
| ↳ `placeId` | string | Place ID for this road segment |
|
||||
|
||||
### `google_maps_timezone`
|
||||
|
||||
Get timezone information for a location
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Google Maps API key |
|
||||
| `lat` | number | Yes | Latitude coordinate |
|
||||
| `lng` | number | Yes | Longitude coordinate |
|
||||
| `timestamp` | number | No | Unix timestamp to determine DST offset \(defaults to current time\) |
|
||||
| `language` | string | No | Language code for timezone name \(e.g., en, es, fr\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `timeZoneId` | string | IANA timezone ID \(e.g., "America/New_York", "Europe/London"\) |
|
||||
| `timeZoneName` | string | Localized timezone name \(e.g., "Eastern Daylight Time"\) |
|
||||
| `rawOffset` | number | UTC offset in seconds \(without DST\) |
|
||||
| `dstOffset` | number | Daylight Saving Time offset in seconds \(0 if not in DST\) |
|
||||
| `totalOffsetSeconds` | number | Total UTC offset in seconds \(rawOffset + dstOffset\) |
|
||||
| `totalOffsetHours` | number | Total UTC offset in hours \(e.g., -5 for EST, -4 for EDT\) |
|
||||
|
||||
### `google_maps_validate_address`
|
||||
|
||||
Validate and standardize a postal address
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Google Maps API key with Address Validation API enabled |
|
||||
| `address` | string | Yes | The address to validate \(as a single string\) |
|
||||
| `regionCode` | string | No | ISO 3166-1 alpha-2 country code \(e.g., "US", "CA"\) |
|
||||
| `locality` | string | No | City or locality name |
|
||||
| `enableUspsCass` | boolean | No | Enable USPS CASS validation for US addresses |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `formattedAddress` | string | The standardized formatted address |
|
||||
| `lat` | number | Latitude coordinate |
|
||||
| `lng` | number | Longitude coordinate |
|
||||
| `placeId` | string | Google Place ID for this address |
|
||||
| `addressComplete` | boolean | Whether the address is complete and deliverable |
|
||||
| `hasUnconfirmedComponents` | boolean | Whether some address components could not be confirmed |
|
||||
| `hasInferredComponents` | boolean | Whether some components were inferred \(not in input\) |
|
||||
| `hasReplacedComponents` | boolean | Whether some components were replaced with canonical values |
|
||||
| `validationGranularity` | string | Granularity of validation \(PREMISE, SUB_PREMISE, ROUTE, etc.\) |
|
||||
| `geocodeGranularity` | string | Granularity of the geocode result |
|
||||
| `addressComponents` | array | Detailed address components |
|
||||
| ↳ `longName` | string | Full name of the component |
|
||||
| ↳ `shortName` | string | Abbreviated name |
|
||||
| ↳ `types` | array | Component types |
|
||||
| `missingComponentTypes` | array | Types of address components that are missing |
|
||||
| `unconfirmedComponentTypes` | array | Types of components that could not be confirmed |
|
||||
| `unresolvedTokens` | array | Input tokens that could not be resolved |
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"datadog",
|
||||
"discord",
|
||||
"dropbox",
|
||||
"dspy",
|
||||
"duckduckgo",
|
||||
"dynamodb",
|
||||
"elasticsearch",
|
||||
@@ -35,6 +36,7 @@
|
||||
"google_drive",
|
||||
"google_forms",
|
||||
"google_groups",
|
||||
"google_maps",
|
||||
"google_search",
|
||||
"google_sheets",
|
||||
"google_slides",
|
||||
|
||||
@@ -61,12 +61,12 @@ Get comprehensive website analytics including traffic, rankings, engagement, and
|
||||
| ↳ `country` | string | Country code |
|
||||
| ↳ `share` | number | Traffic share \(0-1\) |
|
||||
| `trafficSources` | json | Traffic source breakdown |
|
||||
| ↳ `direct` | number | Direct traffic share |
|
||||
| ↳ `referrals` | number | Referral traffic share |
|
||||
| ↳ `search` | number | Search traffic share |
|
||||
| ↳ `social` | number | Social traffic share |
|
||||
| ↳ `mail` | number | Email traffic share |
|
||||
| ↳ `paidReferrals` | number | Paid referral traffic share |
|
||||
| ↳ `direct` | number | Direct traffic share |
|
||||
| ↳ `referrals` | number | Referral traffic share |
|
||||
| ↳ `search` | number | Search traffic share |
|
||||
| ↳ `social` | number | Social traffic share |
|
||||
| ↳ `mail` | number | Email traffic share |
|
||||
| ↳ `paidReferrals` | number | Paid referral traffic share |
|
||||
|
||||
### `similarweb_traffic_visits`
|
||||
|
||||
|
||||
@@ -217,7 +217,7 @@ Search for videos on YouTube using the YouTube Data API. Supports advanced filte
|
||||
| ↳ `channelId` | string | Channel ID that uploaded the video |
|
||||
| ↳ `channelTitle` | string | Channel name |
|
||||
| ↳ `publishedAt` | string | Video publish date |
|
||||
| ↳ `liveBroadcastContent` | string | Live broadcast status: |
|
||||
| ↳ `liveBroadcastContent` | string | Live broadcast status: "none", "live", or "upcoming" |
|
||||
| `totalResults` | number | Total number of search results available |
|
||||
| `nextPageToken` | string | Token for accessing the next page of results |
|
||||
|
||||
@@ -271,7 +271,7 @@ Get a list of video categories available on YouTube. Use this to discover valid
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `items` | array | Array of video categories available in the specified region |
|
||||
| ↳ `categoryId` | string | Category ID to use in search/trending filters \(e.g., |
|
||||
| ↳ `categoryId` | string | Category ID to use in search/trending filters \(e.g., "10" for Music\) |
|
||||
| ↳ `title` | string | Human-readable category name |
|
||||
| ↳ `assignable` | boolean | Whether videos can be tagged with this category |
|
||||
| `totalResults` | number | Total number of categories available |
|
||||
@@ -297,7 +297,7 @@ Get detailed information about a specific YouTube video including statistics, co
|
||||
| `channelId` | string | Channel ID |
|
||||
| `channelTitle` | string | Channel name |
|
||||
| `publishedAt` | string | Published date and time |
|
||||
| `duration` | string | Video duration in ISO 8601 format \(e.g., |
|
||||
| `duration` | string | Video duration in ISO 8601 format \(e.g., "PT4M13S" for 4 min 13 sec\) |
|
||||
| `viewCount` | number | Number of views |
|
||||
| `likeCount` | number | Number of likes |
|
||||
| `commentCount` | number | Number of comments |
|
||||
@@ -305,11 +305,11 @@ Get detailed information about a specific YouTube video including statistics, co
|
||||
| `thumbnail` | string | Video thumbnail URL |
|
||||
| `tags` | array | Video tags |
|
||||
| `categoryId` | string | YouTube video category ID |
|
||||
| `definition` | string | Video definition: |
|
||||
| `caption` | string | Whether captions are available: |
|
||||
| `definition` | string | Video definition: "hd" or "sd" |
|
||||
| `caption` | string | Whether captions are available: "true" or "false" |
|
||||
| `licensedContent` | boolean | Whether the video is licensed content |
|
||||
| `privacyStatus` | string | Video privacy status: |
|
||||
| `liveBroadcastContent` | string | Live broadcast status: |
|
||||
| `privacyStatus` | string | Video privacy status: "public", "private", or "unlisted" |
|
||||
| `liveBroadcastContent` | string | Live broadcast status: "live", "upcoming", or "none" |
|
||||
| `defaultLanguage` | string | Default language of the video metadata |
|
||||
| `defaultAudioLanguage` | string | Default audio language of the video |
|
||||
| `isLiveContent` | boolean | Whether this video is or was a live stream |
|
||||
|
||||
@@ -264,7 +264,7 @@ async function handleToolsCall(
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ input: params.arguments || {}, triggerType: 'mcp' }),
|
||||
signal: AbortSignal.timeout(300000), // 5 minute timeout
|
||||
signal: AbortSignal.timeout(600000), // 10 minute timeout
|
||||
})
|
||||
|
||||
const executeResult = await response.json()
|
||||
|
||||
@@ -32,8 +32,7 @@ import {
|
||||
useTestNotification,
|
||||
useUpdateNotification,
|
||||
} from '@/hooks/queries/notifications'
|
||||
import { useConnectOAuthService } from '@/hooks/queries/oauth-connections'
|
||||
import { useSlackAccounts } from '@/hooks/use-slack-accounts'
|
||||
import { useConnectedAccounts, useConnectOAuthService } from '@/hooks/queries/oauth-connections'
|
||||
import { CORE_TRIGGER_TYPES, type CoreTriggerType } from '@/stores/logs/filters/types'
|
||||
import { SlackChannelSelector } from './components/slack-channel-selector'
|
||||
import { WorkflowSelector } from './components/workflow-selector'
|
||||
@@ -167,7 +166,8 @@ export function NotificationSettings({
|
||||
const deleteNotification = useDeleteNotification()
|
||||
const testNotification = useTestNotification()
|
||||
|
||||
const { accounts: slackAccounts, isLoading: isLoadingSlackAccounts } = useSlackAccounts()
|
||||
const { data: slackAccounts = [], isLoading: isLoadingSlackAccounts } =
|
||||
useConnectedAccounts('slack')
|
||||
const connectSlack = useConnectOAuthService()
|
||||
|
||||
useEffect(() => {
|
||||
@@ -530,7 +530,7 @@ export function NotificationSettings({
|
||||
message:
|
||||
result.data?.error || (result.data?.success ? 'Test sent successfully' : 'Test failed'),
|
||||
})
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
setTestStatus({ id, success: false, message: 'Failed to send test' })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,28 @@
|
||||
'use client'
|
||||
|
||||
import type React from 'react'
|
||||
import { createContext, useContext, useEffect, useMemo, useState } from 'react'
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { useUserPermissions, type WorkspaceUserPermissions } from '@/hooks/use-user-permissions'
|
||||
import {
|
||||
useWorkspacePermissions,
|
||||
useWorkspacePermissionsQuery,
|
||||
type WorkspacePermissions,
|
||||
} from '@/hooks/use-workspace-permissions'
|
||||
workspaceKeys,
|
||||
} from '@/hooks/queries/workspace'
|
||||
import { useUserPermissions, type WorkspaceUserPermissions } from '@/hooks/use-user-permissions'
|
||||
import { useNotificationStore } from '@/stores/notifications'
|
||||
import { useOperationQueueStore } from '@/stores/operation-queue/store'
|
||||
|
||||
const logger = createLogger('WorkspacePermissionsProvider')
|
||||
|
||||
interface WorkspacePermissionsContextType {
|
||||
// Raw workspace permissions data
|
||||
workspacePermissions: WorkspacePermissions | null
|
||||
permissionsLoading: boolean
|
||||
permissionsError: string | null
|
||||
updatePermissions: (newPermissions: WorkspacePermissions) => void
|
||||
refetchPermissions: () => Promise<void>
|
||||
|
||||
// Computed user permissions (connection-aware)
|
||||
userPermissions: WorkspaceUserPermissions & { isOfflineMode?: boolean }
|
||||
|
||||
// Connection state management
|
||||
setOfflineMode: (isOffline: boolean) => void
|
||||
}
|
||||
|
||||
const WorkspacePermissionsContext = createContext<WorkspacePermissionsContextType>({
|
||||
@@ -43,7 +39,6 @@ const WorkspacePermissionsContext = createContext<WorkspacePermissionsContextTyp
|
||||
isLoading: false,
|
||||
error: null,
|
||||
},
|
||||
setOfflineMode: () => {},
|
||||
})
|
||||
|
||||
interface WorkspacePermissionsProviderProps {
|
||||
@@ -51,35 +46,20 @@ interface WorkspacePermissionsProviderProps {
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider that manages workspace permissions and user access
|
||||
* Also provides connection-aware permissions that enforce read-only mode when offline
|
||||
* Provides workspace permissions and connection-aware user access throughout the app.
|
||||
* Enforces read-only mode when offline to prevent data loss.
|
||||
*/
|
||||
export function WorkspacePermissionsProvider({ children }: WorkspacePermissionsProviderProps) {
|
||||
const params = useParams()
|
||||
const workspaceId = params?.workspaceId as string
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
// Manage offline mode state locally
|
||||
const [isOfflineMode, setIsOfflineMode] = useState(false)
|
||||
|
||||
// Track whether we've already surfaced an offline notification to avoid duplicates
|
||||
const [hasShownOfflineNotification, setHasShownOfflineNotification] = useState(false)
|
||||
|
||||
// Get operation error state directly from the store (avoid full useCollaborativeWorkflow subscription)
|
||||
const hasOperationError = useOperationQueueStore((state) => state.hasOperationError)
|
||||
|
||||
const addNotification = useNotificationStore((state) => state.addNotification)
|
||||
|
||||
// Set offline mode when there are operation errors
|
||||
useEffect(() => {
|
||||
if (hasOperationError) {
|
||||
setIsOfflineMode(true)
|
||||
}
|
||||
}, [hasOperationError])
|
||||
const isOfflineMode = hasOperationError
|
||||
|
||||
/**
|
||||
* Surface a global notification when entering offline mode.
|
||||
* Uses the shared notifications system instead of bespoke UI in individual components.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!isOfflineMode || hasShownOfflineNotification) {
|
||||
return
|
||||
@@ -89,7 +69,6 @@ export function WorkspacePermissionsProvider({ children }: WorkspacePermissionsP
|
||||
addNotification({
|
||||
level: 'error',
|
||||
message: 'Connection unavailable',
|
||||
// Global notification (no workflowId) so it is visible regardless of the active workflow
|
||||
action: {
|
||||
type: 'refresh',
|
||||
message: '',
|
||||
@@ -101,40 +80,44 @@ export function WorkspacePermissionsProvider({ children }: WorkspacePermissionsP
|
||||
}
|
||||
}, [addNotification, hasShownOfflineNotification, isOfflineMode])
|
||||
|
||||
// Fetch workspace permissions and loading state
|
||||
const {
|
||||
permissions: workspacePermissions,
|
||||
loading: permissionsLoading,
|
||||
error: permissionsError,
|
||||
updatePermissions,
|
||||
refetch: refetchPermissions,
|
||||
} = useWorkspacePermissions(workspaceId)
|
||||
data: workspacePermissions,
|
||||
isLoading: permissionsLoading,
|
||||
error: permissionsErrorObj,
|
||||
refetch,
|
||||
} = useWorkspacePermissionsQuery(workspaceId)
|
||||
|
||||
const permissionsError = permissionsErrorObj?.message ?? null
|
||||
|
||||
const updatePermissions = useCallback(
|
||||
(newPermissions: WorkspacePermissions) => {
|
||||
if (!workspaceId) return
|
||||
queryClient.setQueryData(workspaceKeys.permissions(workspaceId), newPermissions)
|
||||
},
|
||||
[workspaceId, queryClient]
|
||||
)
|
||||
|
||||
const refetchPermissions = useCallback(async () => {
|
||||
await refetch()
|
||||
}, [refetch])
|
||||
|
||||
// Get base user permissions from workspace permissions
|
||||
const baseUserPermissions = useUserPermissions(
|
||||
workspacePermissions,
|
||||
workspacePermissions ?? null,
|
||||
permissionsLoading,
|
||||
permissionsError
|
||||
)
|
||||
|
||||
// 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 } => {
|
||||
if (isOfflineMode) {
|
||||
// In offline mode, force read-only permissions regardless of actual user permissions
|
||||
return {
|
||||
...baseUserPermissions,
|
||||
canEdit: false,
|
||||
canAdmin: false,
|
||||
// Keep canRead true so users can still view content
|
||||
canRead: baseUserPermissions.canRead,
|
||||
isOfflineMode: true,
|
||||
}
|
||||
}
|
||||
|
||||
// When online, use normal permissions
|
||||
return {
|
||||
...baseUserPermissions,
|
||||
isOfflineMode: false,
|
||||
@@ -143,13 +126,12 @@ export function WorkspacePermissionsProvider({ children }: WorkspacePermissionsP
|
||||
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
workspacePermissions,
|
||||
workspacePermissions: workspacePermissions ?? null,
|
||||
permissionsLoading,
|
||||
permissionsError,
|
||||
updatePermissions,
|
||||
refetchPermissions,
|
||||
userPermissions,
|
||||
setOfflineMode: setIsOfflineMode,
|
||||
}),
|
||||
[
|
||||
workspacePermissions,
|
||||
@@ -169,8 +151,8 @@ export function WorkspacePermissionsProvider({ children }: WorkspacePermissionsP
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to access workspace permissions and data from context
|
||||
* This provides both raw workspace permissions and computed user permissions
|
||||
* Accesses workspace permissions data and operations from context.
|
||||
* Must be used within a WorkspacePermissionsProvider.
|
||||
*/
|
||||
export function useWorkspacePermissionsContext(): WorkspacePermissionsContextType {
|
||||
const context = useContext(WorkspacePermissionsContext)
|
||||
@@ -183,8 +165,8 @@ export function useWorkspacePermissionsContext(): WorkspacePermissionsContextTyp
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to access user permissions from context
|
||||
* This replaces individual useUserPermissions calls and includes connection-aware permissions
|
||||
* Accesses the current user's computed permissions including offline mode status.
|
||||
* Convenience hook that extracts userPermissions from the context.
|
||||
*/
|
||||
export function useUserPermissionsContext(): WorkspaceUserPermissions & {
|
||||
isOfflineMode?: boolean
|
||||
|
||||
@@ -21,14 +21,13 @@ import { cn } from '@/lib/core/utils/cn'
|
||||
import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace'
|
||||
import { getFileExtension } from '@/lib/uploads/utils/file-utils'
|
||||
import { getDocumentIcon } from '@/app/workspace/[workspaceId]/knowledge/components'
|
||||
import { useWorkspacePermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
|
||||
import {
|
||||
useDeleteWorkspaceFile,
|
||||
useStorageInfo,
|
||||
useUploadWorkspaceFile,
|
||||
useWorkspaceFiles,
|
||||
} from '@/hooks/queries/workspace-files'
|
||||
import { useUserPermissions } from '@/hooks/use-user-permissions'
|
||||
import { useWorkspacePermissions } from '@/hooks/use-workspace-permissions'
|
||||
|
||||
const logger = createLogger('FileUploadsSettings')
|
||||
const isBillingEnabled = isTruthy(getEnv('NEXT_PUBLIC_BILLING_ENABLED'))
|
||||
@@ -94,9 +93,7 @@ export function Files() {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const { permissions: workspacePermissions, loading: permissionsLoading } =
|
||||
useWorkspacePermissions(workspaceId)
|
||||
const userPermissions = useUserPermissions(workspacePermissions, permissionsLoading)
|
||||
const { userPermissions, permissionsLoading } = useWorkspacePermissionsContext()
|
||||
|
||||
const handleUploadClick = () => {
|
||||
fileInputRef.current?.click()
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
|
||||
export const PermissionsTableSkeleton = React.memo(() => (
|
||||
<div className='scrollbar-hide max-h-[300px] overflow-y-auto'>
|
||||
<div className='flex items-center justify-between gap-[8px] py-[8px]'>
|
||||
<div className='min-w-0 flex-1'>
|
||||
<div className='flex items-center gap-[8px]'>
|
||||
<Skeleton className='h-[14px] w-40 rounded-[4px]' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-shrink-0 items-center'>
|
||||
<div className='inline-flex gap-[2px]'>
|
||||
<Skeleton className='h-[28px] w-[44px] rounded-[5px]' />
|
||||
<Skeleton className='h-[28px] w-[44px] rounded-[5px]' />
|
||||
<Skeleton className='h-[28px] w-[44px] rounded-[5px]' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
|
||||
PermissionsTableSkeleton.displayName = 'PermissionsTableSkeleton'
|
||||
@@ -1,20 +1,39 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { Loader2, RotateCw, X } from 'lucide-react'
|
||||
import { Badge, Button, Tooltip } from '@/components/emcn'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { useSession } from '@/lib/auth/auth-client'
|
||||
import type { PermissionType } from '@/lib/workspaces/permissions/utils'
|
||||
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
|
||||
import type { WorkspacePermissions } from '@/hooks/use-workspace-permissions'
|
||||
import type { WorkspacePermissions } from '@/hooks/queries/workspace'
|
||||
import { PermissionSelector } from './permission-selector'
|
||||
import { PermissionsTableSkeleton } from './permissions-table-skeleton'
|
||||
import type { UserPermissions } from './types'
|
||||
|
||||
const PermissionsTableSkeleton = () => (
|
||||
<div className='scrollbar-hide max-h-[300px] overflow-y-auto'>
|
||||
<div className='flex items-center justify-between gap-[8px] py-[8px]'>
|
||||
<div className='min-w-0 flex-1'>
|
||||
<div className='flex items-center gap-[8px]'>
|
||||
<Skeleton className='h-[14px] w-40 rounded-[4px]' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-shrink-0 items-center'>
|
||||
<div className='inline-flex gap-[2px]'>
|
||||
<Skeleton className='h-[28px] w-[44px] rounded-[5px]' />
|
||||
<Skeleton className='h-[28px] w-[44px] rounded-[5px]' />
|
||||
<Skeleton className='h-[28px] w-[44px] rounded-[5px]' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
export interface PermissionsTableProps {
|
||||
userPermissions: UserPermissions[]
|
||||
onPermissionChange: (userId: string, permissionType: PermissionType) => void
|
||||
onRemoveMember?: (userId: string, email: string) => void
|
||||
onRemoveInvitation?: (invitationId: string, email: string) => void
|
||||
onResendInvitation?: (invitationId: string, email: string) => void
|
||||
onResendInvitation?: (invitationId: string) => void
|
||||
disabled?: boolean
|
||||
existingUserPermissionChanges: Record<string, Partial<UserPermissions>>
|
||||
isSaving?: boolean
|
||||
@@ -143,7 +162,6 @@ export const PermissionsTable = ({
|
||||
<div>
|
||||
{allUsers.map((user) => {
|
||||
const isCurrentUser = user.isCurrentUser === true
|
||||
const isExistingUser = filteredExistingUsers.some((eu) => eu.email === user.email)
|
||||
const isPendingInvitation = user.isPendingInvitation === true
|
||||
const userIdentifier = user.userId || user.email
|
||||
const originalPermission = workspacePermissions?.users?.find(
|
||||
@@ -205,7 +223,7 @@ export const PermissionsTable = ({
|
||||
<span className='inline-flex'>
|
||||
<Button
|
||||
variant='ghost'
|
||||
onClick={() => onResendInvitation(user.invitationId!, user.email)}
|
||||
onClick={() => onResendInvitation(user.invitationId!)}
|
||||
disabled={
|
||||
disabled ||
|
||||
isSaving ||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export { PermissionSelector } from './components/permission-selector'
|
||||
export { PermissionsTable } from './components/permissions-table'
|
||||
export { PermissionsTableSkeleton } from './components/permissions-table-skeleton'
|
||||
export type { PermissionType, UserPermissions } from './components/types'
|
||||
export { InviteModal } from './invite-modal'
|
||||
|
||||
@@ -19,7 +19,14 @@ import { useSession } from '@/lib/auth/auth-client'
|
||||
import { quickValidateEmail } from '@/lib/messaging/email/validation'
|
||||
import { useWorkspacePermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
|
||||
import { PermissionsTable } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/components/permissions-table'
|
||||
import { API_ENDPOINTS } from '@/stores/constants'
|
||||
import {
|
||||
useBatchSendWorkspaceInvitations,
|
||||
useCancelWorkspaceInvitation,
|
||||
usePendingInvitations,
|
||||
useRemoveWorkspaceMember,
|
||||
useResendWorkspaceInvitation,
|
||||
useUpdateWorkspacePermissions,
|
||||
} from '@/hooks/queries/invitations'
|
||||
import type { PermissionType, UserPermissions } from './components/types'
|
||||
|
||||
const logger = createLogger('InviteModal')
|
||||
@@ -30,40 +37,25 @@ interface InviteModalProps {
|
||||
workspaceName?: string
|
||||
}
|
||||
|
||||
interface PendingInvitation {
|
||||
id: string
|
||||
workspaceId: string
|
||||
email: string
|
||||
permissions: PermissionType
|
||||
status: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export function InviteModal({ open, onOpenChange, workspaceName }: InviteModalProps) {
|
||||
const formRef = useRef<HTMLFormElement>(null)
|
||||
const [emailItems, setEmailItems] = useState<TagItem[]>([])
|
||||
const [userPermissions, setUserPermissions] = useState<UserPermissions[]>([])
|
||||
const [pendingInvitations, setPendingInvitations] = useState<UserPermissions[]>([])
|
||||
const [isPendingInvitationsLoading, setIsPendingInvitationsLoading] = useState(false)
|
||||
const [existingUserPermissionChanges, setExistingUserPermissionChanges] = useState<
|
||||
Record<string, Partial<UserPermissions>>
|
||||
>({})
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const cooldownIntervalsRef = useRef<Map<string, NodeJS.Timeout>>(new Map())
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null)
|
||||
const [memberToRemove, setMemberToRemove] = useState<{ userId: string; email: string } | null>(
|
||||
null
|
||||
)
|
||||
const [isRemovingMember, setIsRemovingMember] = useState(false)
|
||||
const [invitationToRemove, setInvitationToRemove] = useState<{
|
||||
invitationId: string
|
||||
email: string
|
||||
} | null>(null)
|
||||
const [isRemovingInvitation, setIsRemovingInvitation] = useState(false)
|
||||
const [resendingInvitationIds, setResendingInvitationIds] = useState<Record<string, boolean>>({})
|
||||
const [resendCooldowns, setResendCooldowns] = useState<Record<string, number>>({})
|
||||
const [resentInvitationIds, setResentInvitationIds] = useState<Record<string, boolean>>({})
|
||||
const [resendingInvitationIds, setResendingInvitationIds] = useState<Record<string, boolean>>({})
|
||||
const params = useParams()
|
||||
const workspaceId = params.workspaceId as string
|
||||
|
||||
@@ -72,50 +64,26 @@ export function InviteModal({ open, onOpenChange, workspaceName }: InviteModalPr
|
||||
workspacePermissions,
|
||||
permissionsLoading,
|
||||
updatePermissions,
|
||||
refetchPermissions,
|
||||
userPermissions: userPerms,
|
||||
} = useWorkspacePermissionsContext()
|
||||
|
||||
const { data: pendingInvitations = [], isLoading: isPendingInvitationsLoading } =
|
||||
usePendingInvitations(open ? workspaceId : undefined)
|
||||
|
||||
const batchSendInvitations = useBatchSendWorkspaceInvitations()
|
||||
const cancelInvitation = useCancelWorkspaceInvitation()
|
||||
const resendInvitation = useResendWorkspaceInvitation()
|
||||
const removeMember = useRemoveWorkspaceMember()
|
||||
const updatePermissionsMutation = useUpdateWorkspacePermissions()
|
||||
|
||||
const hasPendingChanges = Object.keys(existingUserPermissionChanges).length > 0
|
||||
const validEmails = emailItems.filter((item) => item.isValid).map((item) => item.value)
|
||||
const hasNewInvites = validEmails.length > 0
|
||||
|
||||
const fetchPendingInvitations = useCallback(async () => {
|
||||
if (!workspaceId) return
|
||||
|
||||
setIsPendingInvitationsLoading(true)
|
||||
try {
|
||||
const response = await fetch('/api/workspaces/invitations')
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
const workspacePendingInvitations =
|
||||
data.invitations
|
||||
?.filter(
|
||||
(inv: PendingInvitation) =>
|
||||
inv.status === 'pending' && inv.workspaceId === workspaceId
|
||||
)
|
||||
.map((inv: PendingInvitation) => ({
|
||||
email: inv.email,
|
||||
permissionType: inv.permissions,
|
||||
isPendingInvitation: true,
|
||||
invitationId: inv.id,
|
||||
})) || []
|
||||
|
||||
setPendingInvitations(workspacePendingInvitations)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error fetching pending invitations:', error)
|
||||
} finally {
|
||||
setIsPendingInvitationsLoading(false)
|
||||
}
|
||||
}, [workspaceId])
|
||||
|
||||
useEffect(() => {
|
||||
if (open && workspaceId) {
|
||||
fetchPendingInvitations()
|
||||
refetchPermissions()
|
||||
}
|
||||
}, [open, workspaceId, fetchPendingInvitations, refetchPermissions])
|
||||
const isSubmitting = batchSendInvitations.isPending
|
||||
const isSaving = updatePermissionsMutation.isPending
|
||||
const isRemovingMember = removeMember.isPending
|
||||
const isRemovingInvitation = cancelInvitation.isPending
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
@@ -180,16 +148,12 @@ export function InviteModal({ open, onOpenChange, workspaceName }: InviteModalPr
|
||||
[emailItems, pendingInvitations, workspacePermissions?.users, session?.user?.email]
|
||||
)
|
||||
|
||||
const removeEmailItem = useCallback(
|
||||
(_value: string, index: number, isValid?: boolean) => {
|
||||
const itemToRemove = emailItems[index]
|
||||
setEmailItems((prev) => prev.filter((_, i) => i !== index))
|
||||
if (isValid ?? itemToRemove?.isValid) {
|
||||
setUserPermissions((prev) => prev.filter((user) => user.email !== itemToRemove?.value))
|
||||
}
|
||||
},
|
||||
[emailItems]
|
||||
)
|
||||
const removeEmailItem = useCallback((value: string, index: number, isValid?: boolean) => {
|
||||
setEmailItems((prev) => prev.filter((_, i) => i !== index))
|
||||
if (isValid) {
|
||||
setUserPermissions((prev) => prev.filter((user) => user.email !== value))
|
||||
}
|
||||
}, [])
|
||||
|
||||
const fileInputOptions: FileInputOptions = useMemo(
|
||||
() => ({
|
||||
@@ -198,7 +162,8 @@ export function InviteModal({ open, onOpenChange, workspaceName }: InviteModalPr
|
||||
extractValues: (text: string) => {
|
||||
const emailRegex = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g
|
||||
const matches = text.match(emailRegex) || []
|
||||
return [...new Set(matches.map((e) => e.toLowerCase()))]
|
||||
const uniqueEmails = [...new Set(matches.map((e) => e.toLowerCase()))]
|
||||
return uniqueEmails.filter((email) => quickValidateEmail(email).isValid)
|
||||
},
|
||||
tooltip: 'Upload emails',
|
||||
}),
|
||||
@@ -230,53 +195,38 @@ export function InviteModal({ open, onOpenChange, workspaceName }: InviteModalPr
|
||||
[workspacePermissions?.users]
|
||||
)
|
||||
|
||||
const handleSaveChanges = useCallback(async () => {
|
||||
const handleSaveChanges = useCallback(() => {
|
||||
if (!userPerms.canAdmin || !hasPendingChanges || !workspaceId) return
|
||||
|
||||
setIsSaving(true)
|
||||
setErrorMessage(null)
|
||||
|
||||
try {
|
||||
const updates = Object.entries(existingUserPermissionChanges).map(([userId, changes]) => ({
|
||||
userId,
|
||||
permissions: changes.permissionType || 'read',
|
||||
}))
|
||||
const updates = Object.entries(existingUserPermissionChanges).map(([userId, changes]) => ({
|
||||
userId,
|
||||
permissions: (changes.permissionType || 'read') as 'admin' | 'write' | 'read',
|
||||
}))
|
||||
|
||||
const response = await fetch(API_ENDPOINTS.WORKSPACE_PERMISSIONS(workspaceId), {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
updatePermissionsMutation.mutate(
|
||||
{ workspaceId, updates },
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
if (data.users && data.total !== undefined) {
|
||||
updatePermissions({ users: data.users, total: data.total })
|
||||
}
|
||||
setExistingUserPermissionChanges({})
|
||||
},
|
||||
onError: (error) => {
|
||||
logger.error('Error saving permission changes:', error)
|
||||
setErrorMessage(error.message || 'Failed to save permission changes. Please try again.')
|
||||
},
|
||||
body: JSON.stringify({ updates }),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to update permissions')
|
||||
}
|
||||
|
||||
if (data.users && data.total !== undefined) {
|
||||
updatePermissions({ users: data.users, total: data.total })
|
||||
}
|
||||
|
||||
setExistingUserPermissionChanges({})
|
||||
} catch (error) {
|
||||
logger.error('Error saving permission changes:', error)
|
||||
const errorMsg =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Failed to save permission changes. Please try again.'
|
||||
setErrorMessage(errorMsg)
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
)
|
||||
}, [
|
||||
userPerms.canAdmin,
|
||||
hasPendingChanges,
|
||||
workspaceId,
|
||||
existingUserPermissionChanges,
|
||||
updatePermissions,
|
||||
updatePermissionsMutation,
|
||||
])
|
||||
|
||||
const handleRestoreChanges = useCallback(() => {
|
||||
@@ -289,62 +239,57 @@ export function InviteModal({ open, onOpenChange, workspaceName }: InviteModalPr
|
||||
setMemberToRemove({ userId, email })
|
||||
}, [])
|
||||
|
||||
const handleRemoveMemberConfirm = useCallback(async () => {
|
||||
const handleRemoveMemberConfirm = useCallback(() => {
|
||||
if (!memberToRemove || !workspaceId || !userPerms.canAdmin) return
|
||||
|
||||
setIsRemovingMember(true)
|
||||
setErrorMessage(null)
|
||||
|
||||
try {
|
||||
const userRecord = workspacePermissions?.users?.find(
|
||||
(user) => user.userId === memberToRemove.userId
|
||||
)
|
||||
const userRecord = workspacePermissions?.users?.find(
|
||||
(user) => user.userId === memberToRemove.userId
|
||||
)
|
||||
|
||||
if (!userRecord) {
|
||||
throw new Error('User is not a member of this workspace')
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/workspaces/members/${memberToRemove.userId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
workspaceId: workspaceId,
|
||||
}),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to remove member')
|
||||
}
|
||||
|
||||
if (workspacePermissions) {
|
||||
const updatedUsers = workspacePermissions.users.filter(
|
||||
(user) => user.userId !== memberToRemove.userId
|
||||
)
|
||||
updatePermissions({
|
||||
users: updatedUsers,
|
||||
total: workspacePermissions.total - 1,
|
||||
})
|
||||
}
|
||||
|
||||
setExistingUserPermissionChanges((prev) => {
|
||||
const updated = { ...prev }
|
||||
delete updated[memberToRemove.userId]
|
||||
return updated
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error removing member:', error)
|
||||
const errorMsg =
|
||||
error instanceof Error ? error.message : 'Failed to remove member. Please try again.'
|
||||
setErrorMessage(errorMsg)
|
||||
} finally {
|
||||
setIsRemovingMember(false)
|
||||
if (!userRecord) {
|
||||
setErrorMessage('User is not a member of this workspace')
|
||||
setMemberToRemove(null)
|
||||
return
|
||||
}
|
||||
}, [memberToRemove, workspaceId, userPerms.canAdmin, workspacePermissions, updatePermissions])
|
||||
|
||||
removeMember.mutate(
|
||||
{ userId: memberToRemove.userId, workspaceId },
|
||||
{
|
||||
onSuccess: () => {
|
||||
if (workspacePermissions) {
|
||||
const updatedUsers = workspacePermissions.users.filter(
|
||||
(user) => user.userId !== memberToRemove.userId
|
||||
)
|
||||
updatePermissions({
|
||||
users: updatedUsers,
|
||||
total: workspacePermissions.total - 1,
|
||||
})
|
||||
}
|
||||
|
||||
setExistingUserPermissionChanges((prev) => {
|
||||
const updated = { ...prev }
|
||||
delete updated[memberToRemove.userId]
|
||||
return updated
|
||||
})
|
||||
setMemberToRemove(null)
|
||||
},
|
||||
onError: (error) => {
|
||||
logger.error('Error removing member:', error)
|
||||
setErrorMessage(error.message || 'Failed to remove member. Please try again.')
|
||||
setMemberToRemove(null)
|
||||
},
|
||||
}
|
||||
)
|
||||
}, [
|
||||
memberToRemove,
|
||||
workspaceId,
|
||||
userPerms.canAdmin,
|
||||
workspacePermissions,
|
||||
updatePermissions,
|
||||
removeMember,
|
||||
])
|
||||
|
||||
const handleRemoveMemberCancel = useCallback(() => {
|
||||
setMemberToRemove(null)
|
||||
@@ -354,120 +299,101 @@ export function InviteModal({ open, onOpenChange, workspaceName }: InviteModalPr
|
||||
setInvitationToRemove({ invitationId, email })
|
||||
}, [])
|
||||
|
||||
const handleRemoveInvitationConfirm = useCallback(async () => {
|
||||
const handleRemoveInvitationConfirm = useCallback(() => {
|
||||
if (!invitationToRemove || !workspaceId || !userPerms.canAdmin) return
|
||||
|
||||
setIsRemovingInvitation(true)
|
||||
setErrorMessage(null)
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/workspaces/invitations/${invitationToRemove.invitationId}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to cancel invitation')
|
||||
cancelInvitation.mutate(
|
||||
{ invitationId: invitationToRemove.invitationId, workspaceId },
|
||||
{
|
||||
onSuccess: () => {
|
||||
setInvitationToRemove(null)
|
||||
},
|
||||
onError: (error) => {
|
||||
logger.error('Error cancelling invitation:', error)
|
||||
setErrorMessage(error.message || 'Failed to cancel invitation. Please try again.')
|
||||
setInvitationToRemove(null)
|
||||
},
|
||||
}
|
||||
|
||||
setPendingInvitations((prev) =>
|
||||
prev.filter((inv) => inv.invitationId !== invitationToRemove.invitationId)
|
||||
)
|
||||
} catch (error) {
|
||||
logger.error('Error cancelling invitation:', error)
|
||||
const errorMsg =
|
||||
error instanceof Error ? error.message : 'Failed to cancel invitation. Please try again.'
|
||||
setErrorMessage(errorMsg)
|
||||
} finally {
|
||||
setIsRemovingInvitation(false)
|
||||
setInvitationToRemove(null)
|
||||
}
|
||||
}, [invitationToRemove, workspaceId, userPerms.canAdmin])
|
||||
)
|
||||
}, [invitationToRemove, workspaceId, userPerms.canAdmin, cancelInvitation])
|
||||
|
||||
const handleRemoveInvitationCancel = useCallback(() => {
|
||||
setInvitationToRemove(null)
|
||||
}, [])
|
||||
|
||||
const handleResendInvitation = useCallback(
|
||||
async (invitationId: string, email: string) => {
|
||||
(invitationId: string) => {
|
||||
if (!workspaceId || !userPerms.canAdmin) return
|
||||
|
||||
const secondsLeft = resendCooldowns[invitationId]
|
||||
if (secondsLeft && secondsLeft > 0) return
|
||||
|
||||
setResendingInvitationIds((prev) => ({ ...prev, [invitationId]: true }))
|
||||
if (resendingInvitationIds[invitationId]) return
|
||||
|
||||
setErrorMessage(null)
|
||||
setResendingInvitationIds((prev) => ({ ...prev, [invitationId]: true }))
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/workspaces/invitations/${invitationId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to resend invitation')
|
||||
}
|
||||
|
||||
setResentInvitationIds((prev) => ({ ...prev, [invitationId]: true }))
|
||||
setTimeout(() => {
|
||||
setResentInvitationIds((prev) => {
|
||||
const next = { ...prev }
|
||||
delete next[invitationId]
|
||||
return next
|
||||
})
|
||||
}, 4000)
|
||||
} catch (error) {
|
||||
logger.error('Error resending invitation:', error)
|
||||
const errorMsg =
|
||||
error instanceof Error ? error.message : 'Failed to resend invitation. Please try again.'
|
||||
setErrorMessage(errorMsg)
|
||||
} finally {
|
||||
setResendingInvitationIds((prev) => {
|
||||
const next = { ...prev }
|
||||
delete next[invitationId]
|
||||
return next
|
||||
})
|
||||
setResendCooldowns((prev) => ({ ...prev, [invitationId]: 60 }))
|
||||
|
||||
const existingInterval = cooldownIntervalsRef.current.get(invitationId)
|
||||
if (existingInterval) {
|
||||
clearInterval(existingInterval)
|
||||
}
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setResendCooldowns((prev) => {
|
||||
const current = prev[invitationId]
|
||||
if (current === undefined) return prev
|
||||
if (current <= 1) {
|
||||
resendInvitation.mutate(
|
||||
{ invitationId, workspaceId },
|
||||
{
|
||||
onSuccess: () => {
|
||||
setResendingInvitationIds((prev) => {
|
||||
const next = { ...prev }
|
||||
delete next[invitationId]
|
||||
clearInterval(interval)
|
||||
cooldownIntervalsRef.current.delete(invitationId)
|
||||
return next
|
||||
}
|
||||
return { ...prev, [invitationId]: current - 1 }
|
||||
})
|
||||
}, 1000)
|
||||
})
|
||||
setResentInvitationIds((prev) => ({ ...prev, [invitationId]: true }))
|
||||
setTimeout(() => {
|
||||
setResentInvitationIds((prev) => {
|
||||
const next = { ...prev }
|
||||
delete next[invitationId]
|
||||
return next
|
||||
})
|
||||
}, 4000)
|
||||
|
||||
cooldownIntervalsRef.current.set(invitationId, interval)
|
||||
}
|
||||
setResendCooldowns((prev) => ({ ...prev, [invitationId]: 60 }))
|
||||
|
||||
const existingInterval = cooldownIntervalsRef.current.get(invitationId)
|
||||
if (existingInterval) {
|
||||
clearInterval(existingInterval)
|
||||
}
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setResendCooldowns((prev) => {
|
||||
const current = prev[invitationId]
|
||||
if (current === undefined) return prev
|
||||
if (current <= 1) {
|
||||
const next = { ...prev }
|
||||
delete next[invitationId]
|
||||
clearInterval(interval)
|
||||
cooldownIntervalsRef.current.delete(invitationId)
|
||||
return next
|
||||
}
|
||||
return { ...prev, [invitationId]: current - 1 }
|
||||
})
|
||||
}, 1000)
|
||||
|
||||
cooldownIntervalsRef.current.set(invitationId, interval)
|
||||
},
|
||||
onError: (error) => {
|
||||
setResendingInvitationIds((prev) => {
|
||||
const next = { ...prev }
|
||||
delete next[invitationId]
|
||||
return next
|
||||
})
|
||||
logger.error('Error resending invitation:', error)
|
||||
setErrorMessage(error.message || 'Failed to resend invitation. Please try again.')
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
[workspaceId, userPerms.canAdmin, resendCooldowns]
|
||||
[workspaceId, userPerms.canAdmin, resendCooldowns, resendingInvitationIds, resendInvitation]
|
||||
)
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
async (e: React.FormEvent) => {
|
||||
(e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
setErrorMessage(null)
|
||||
@@ -476,122 +402,65 @@ export function InviteModal({ open, onOpenChange, workspaceName }: InviteModalPr
|
||||
return
|
||||
}
|
||||
|
||||
setIsSubmitting(true)
|
||||
|
||||
try {
|
||||
const failedInvites: string[] = []
|
||||
|
||||
const results = await Promise.all(
|
||||
validEmails.map(async (email) => {
|
||||
try {
|
||||
const userPermission = userPermissions.find((up) => up.email === email)
|
||||
const permissionType = userPermission?.permissionType || 'read'
|
||||
|
||||
const response = await fetch('/api/workspaces/invitations', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
workspaceId,
|
||||
email: email,
|
||||
role: 'member',
|
||||
permission: permissionType,
|
||||
}),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
failedInvites.push(email)
|
||||
|
||||
if (data.error) {
|
||||
setErrorMessage(data.error)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
} catch {
|
||||
failedInvites.push(email)
|
||||
return false
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const successCount = results.filter(Boolean).length
|
||||
const successfulEmails = validEmails.filter((_, index) => results[index])
|
||||
|
||||
if (successCount > 0) {
|
||||
if (successfulEmails.length > 0) {
|
||||
const newPendingInvitations: UserPermissions[] = successfulEmails.map((email) => {
|
||||
const userPermission = userPermissions.find((up) => up.email === email)
|
||||
const permissionType = userPermission?.permissionType || 'read'
|
||||
|
||||
return {
|
||||
email,
|
||||
permissionType,
|
||||
isPendingInvitation: true,
|
||||
}
|
||||
})
|
||||
|
||||
setPendingInvitations((prev) => {
|
||||
const existingEmails = new Set(prev.map((inv) => inv.email))
|
||||
const merged = [...prev]
|
||||
|
||||
newPendingInvitations.forEach((inv) => {
|
||||
if (!existingEmails.has(inv.email)) {
|
||||
merged.push(inv)
|
||||
}
|
||||
})
|
||||
|
||||
return merged
|
||||
})
|
||||
}
|
||||
|
||||
fetchPendingInvitations()
|
||||
|
||||
if (failedInvites.length > 0) {
|
||||
setEmailItems(failedInvites.map((email) => ({ value: email, isValid: true })))
|
||||
setUserPermissions((prev) => prev.filter((user) => failedInvites.includes(user.email)))
|
||||
} else {
|
||||
setEmailItems([])
|
||||
setUserPermissions([])
|
||||
}
|
||||
const invitations = validEmails.map((email) => {
|
||||
const userPermission = userPermissions.find((up) => up.email === email)
|
||||
return {
|
||||
email,
|
||||
permission: (userPermission?.permissionType || 'read') as 'admin' | 'write' | 'read',
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('Error inviting members:', err)
|
||||
const errorMessage =
|
||||
err instanceof Error ? err.message : 'An unexpected error occurred. Please try again.'
|
||||
setErrorMessage(errorMessage)
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
})
|
||||
|
||||
batchSendInvitations.mutate(
|
||||
{ workspaceId, invitations },
|
||||
{
|
||||
onSuccess: (result) => {
|
||||
if (result.failed.length > 0) {
|
||||
setEmailItems(result.failed.map((f) => ({ value: f.email, isValid: true })))
|
||||
setUserPermissions((prev) =>
|
||||
prev.filter((user) => result.failed.some((f) => f.email === user.email))
|
||||
)
|
||||
setErrorMessage(result.failed[0].error)
|
||||
} else {
|
||||
setEmailItems([])
|
||||
setUserPermissions([])
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
logger.error('Error inviting members:', error)
|
||||
setErrorMessage(error.message || 'An unexpected error occurred. Please try again.')
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
[validEmails, workspaceId, userPermissions, fetchPendingInvitations]
|
||||
[validEmails, workspaceId, userPermissions, batchSendInvitations]
|
||||
)
|
||||
|
||||
const resetState = useCallback(() => {
|
||||
setEmailItems([])
|
||||
setUserPermissions([])
|
||||
setPendingInvitations([])
|
||||
setIsPendingInvitationsLoading(false)
|
||||
setExistingUserPermissionChanges({})
|
||||
setIsSubmitting(false)
|
||||
setIsSaving(false)
|
||||
setErrorMessage(null)
|
||||
setMemberToRemove(null)
|
||||
setIsRemovingMember(false)
|
||||
setInvitationToRemove(null)
|
||||
setIsRemovingInvitation(false)
|
||||
setResendCooldowns({})
|
||||
setResentInvitationIds({})
|
||||
setResendingInvitationIds({})
|
||||
|
||||
cooldownIntervalsRef.current.forEach((interval) => clearInterval(interval))
|
||||
cooldownIntervalsRef.current.clear()
|
||||
}, [])
|
||||
|
||||
const pendingInvitationsForTable: UserPermissions[] = useMemo(
|
||||
() =>
|
||||
pendingInvitations.map((inv) => ({
|
||||
email: inv.email,
|
||||
permissionType: inv.permissionType,
|
||||
isPendingInvitation: true,
|
||||
invitationId: inv.invitationId,
|
||||
})),
|
||||
[pendingInvitations]
|
||||
)
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
@@ -681,7 +550,7 @@ export function InviteModal({ open, onOpenChange, workspaceName }: InviteModalPr
|
||||
isSaving={isSaving}
|
||||
workspacePermissions={workspacePermissions}
|
||||
permissionsLoading={permissionsLoading}
|
||||
pendingInvitations={pendingInvitations}
|
||||
pendingInvitations={pendingInvitationsForTable}
|
||||
isPendingInvitationsLoading={isPendingInvitationsLoading}
|
||||
resendingInvitationIds={resendingInvitationIds}
|
||||
resentInvitationIds={resentInvitationIds}
|
||||
@@ -691,26 +560,29 @@ export function InviteModal({ open, onOpenChange, workspaceName }: InviteModalPr
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter className='justify-between'>
|
||||
{hasPendingChanges && userPerms.canAdmin && (
|
||||
<div className='flex gap-[8px]'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='default'
|
||||
disabled={isSaving || isSubmitting}
|
||||
onClick={handleRestoreChanges}
|
||||
>
|
||||
Restore Changes
|
||||
</Button>
|
||||
<Button
|
||||
type='button'
|
||||
variant='tertiary'
|
||||
disabled={isSaving || isSubmitting}
|
||||
onClick={handleSaveChanges}
|
||||
>
|
||||
{isSaving ? 'Saving...' : 'Save Changes'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`flex gap-[8px] ${hasPendingChanges && userPerms.canAdmin ? '' : 'pointer-events-none invisible'}`}
|
||||
aria-hidden={!(hasPendingChanges && userPerms.canAdmin)}
|
||||
>
|
||||
<Button
|
||||
type='button'
|
||||
variant='default'
|
||||
disabled={isSaving || isSubmitting}
|
||||
onClick={handleRestoreChanges}
|
||||
tabIndex={hasPendingChanges && userPerms.canAdmin ? 0 : -1}
|
||||
>
|
||||
Restore Changes
|
||||
</Button>
|
||||
<Button
|
||||
type='button'
|
||||
variant='tertiary'
|
||||
disabled={isSaving || isSubmitting}
|
||||
onClick={handleSaveChanges}
|
||||
tabIndex={hasPendingChanges && userPerms.canAdmin ? 0 : -1}
|
||||
>
|
||||
{isSaving ? 'Saving...' : 'Save Changes'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type='button'
|
||||
|
||||
@@ -14,4 +14,4 @@ export {
|
||||
export { useSidebarResize } from './use-sidebar-resize'
|
||||
export { useWorkflowOperations } from './use-workflow-operations'
|
||||
export { useWorkflowSelection } from './use-workflow-selection'
|
||||
export { useWorkspaceManagement } from './use-workspace-management'
|
||||
export { useWorkspaceManagement, type Workspace } from './use-workspace-management'
|
||||
|
||||
@@ -1,31 +1,33 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { usePathname, useRouter } from 'next/navigation'
|
||||
import { generateWorkspaceName } from '@/lib/workspaces/naming'
|
||||
import { useLeaveWorkspace } from '@/hooks/queries/invitations'
|
||||
import {
|
||||
useCreateWorkspace,
|
||||
useDeleteWorkspace,
|
||||
useUpdateWorkspaceName,
|
||||
useWorkspacesQuery,
|
||||
type Workspace,
|
||||
workspaceKeys,
|
||||
} from '@/hooks/queries/workspace'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
|
||||
const logger = createLogger('useWorkspaceManagement')
|
||||
|
||||
interface Workspace {
|
||||
id: string
|
||||
name: string
|
||||
ownerId: string
|
||||
role?: string
|
||||
membershipId?: string
|
||||
permissions?: 'admin' | 'write' | 'read' | null
|
||||
}
|
||||
|
||||
interface UseWorkspaceManagementProps {
|
||||
workspaceId: string
|
||||
sessionUserId?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook to manage workspace operations including fetching, switching, creating, deleting, and leaving workspaces.
|
||||
* Manages workspace operations including fetching, switching, creating, deleting, and leaving workspaces.
|
||||
* Handles workspace validation and URL synchronization.
|
||||
*
|
||||
* @param props - Configuration object containing workspaceId and sessionUserId
|
||||
* @returns Workspace management state and operations
|
||||
* @param props.workspaceId - The current workspace ID from the URL
|
||||
* @param props.sessionUserId - The current user's session ID
|
||||
* @returns Workspace state and operations
|
||||
*/
|
||||
export function useWorkspaceManagement({
|
||||
workspaceId,
|
||||
@@ -33,140 +35,68 @@ export function useWorkspaceManagement({
|
||||
}: UseWorkspaceManagementProps) {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const queryClient = useQueryClient()
|
||||
const switchToWorkspace = useWorkflowRegistry((state) => state.switchToWorkspace)
|
||||
|
||||
// Workspace management state
|
||||
const [workspaces, setWorkspaces] = useState<Workspace[]>([])
|
||||
const [activeWorkspace, setActiveWorkspace] = useState<Workspace | null>(null)
|
||||
const [isWorkspacesLoading, setIsWorkspacesLoading] = useState(true)
|
||||
const [isCreatingWorkspace, setIsCreatingWorkspace] = useState(false)
|
||||
const [isDeleting, setIsDeleting] = useState(false)
|
||||
const [isLeaving, setIsLeaving] = useState(false)
|
||||
const {
|
||||
data: workspaces = [],
|
||||
isLoading: isWorkspacesLoading,
|
||||
refetch: refetchWorkspaces,
|
||||
} = useWorkspacesQuery(Boolean(sessionUserId))
|
||||
|
||||
const leaveWorkspaceMutation = useLeaveWorkspace()
|
||||
const createWorkspaceMutation = useCreateWorkspace()
|
||||
const deleteWorkspaceMutation = useDeleteWorkspace()
|
||||
const updateWorkspaceNameMutation = useUpdateWorkspaceName()
|
||||
|
||||
// Refs to avoid dependency issues
|
||||
const workspaceIdRef = useRef<string>(workspaceId)
|
||||
const routerRef = useRef<ReturnType<typeof useRouter>>(router)
|
||||
const pathnameRef = useRef<string | null>(pathname || null)
|
||||
const activeWorkspaceRef = useRef<Workspace | null>(null)
|
||||
const isInitializedRef = useRef<boolean>(false)
|
||||
const hasValidatedRef = useRef<boolean>(false)
|
||||
|
||||
// Update refs when values change
|
||||
workspaceIdRef.current = workspaceId
|
||||
routerRef.current = router
|
||||
pathnameRef.current = pathname || null
|
||||
|
||||
const activeWorkspace = useMemo(() => {
|
||||
if (!workspaces.length) return null
|
||||
return workspaces.find((w) => w.id === workspaceId) ?? null
|
||||
}, [workspaces, workspaceId])
|
||||
|
||||
const activeWorkspaceRef = useRef<Workspace | null>(activeWorkspace)
|
||||
activeWorkspaceRef.current = activeWorkspace
|
||||
|
||||
/**
|
||||
* Refresh workspace list without validation logic - used for non-current workspace operations
|
||||
*/
|
||||
const refreshWorkspaceList = useCallback(async () => {
|
||||
setIsWorkspacesLoading(true)
|
||||
try {
|
||||
const response = await fetch('/api/workspaces')
|
||||
const data = await response.json()
|
||||
|
||||
if (data.workspaces && Array.isArray(data.workspaces)) {
|
||||
const fetchedWorkspaces = data.workspaces as Workspace[]
|
||||
setWorkspaces(fetchedWorkspaces)
|
||||
|
||||
// Only update activeWorkspace if it still exists in the fetched workspaces
|
||||
// Use functional update to avoid dependency on activeWorkspace
|
||||
setActiveWorkspace((currentActive) => {
|
||||
if (!currentActive) {
|
||||
return currentActive
|
||||
}
|
||||
|
||||
const matchingWorkspace = fetchedWorkspaces.find(
|
||||
(workspace) => workspace.id === currentActive.id
|
||||
)
|
||||
if (matchingWorkspace) {
|
||||
return matchingWorkspace
|
||||
}
|
||||
|
||||
// Active workspace was deleted, clear it
|
||||
logger.warn(`Active workspace ${currentActive.id} no longer exists`)
|
||||
return null
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('Error refreshing workspace list:', err)
|
||||
} finally {
|
||||
setIsWorkspacesLoading(false)
|
||||
useEffect(() => {
|
||||
if (isWorkspacesLoading || hasValidatedRef.current || !workspaces.length) {
|
||||
return
|
||||
}
|
||||
}, [])
|
||||
|
||||
const currentWorkspaceId = workspaceIdRef.current
|
||||
const matchingWorkspace = workspaces.find((w) => w.id === currentWorkspaceId)
|
||||
|
||||
if (!matchingWorkspace) {
|
||||
logger.warn(`Workspace ${currentWorkspaceId} not found in user's workspaces`)
|
||||
const fallbackWorkspace = workspaces[0]
|
||||
logger.info(`Redirecting to fallback workspace: ${fallbackWorkspace.id}`)
|
||||
routerRef.current?.push(`/workspace/${fallbackWorkspace.id}/w`)
|
||||
}
|
||||
|
||||
hasValidatedRef.current = true
|
||||
}, [workspaces, isWorkspacesLoading])
|
||||
|
||||
const refreshWorkspaceList = useCallback(async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: workspaceKeys.lists() })
|
||||
}, [queryClient])
|
||||
|
||||
const fetchWorkspaces = useCallback(async () => {
|
||||
setIsWorkspacesLoading(true)
|
||||
try {
|
||||
const response = await fetch('/api/workspaces')
|
||||
const data = await response.json()
|
||||
hasValidatedRef.current = false
|
||||
await refetchWorkspaces()
|
||||
}, [refetchWorkspaces])
|
||||
|
||||
if (data.workspaces && Array.isArray(data.workspaces)) {
|
||||
const fetchedWorkspaces = data.workspaces as Workspace[]
|
||||
setWorkspaces(fetchedWorkspaces)
|
||||
|
||||
// Handle active workspace selection with URL validation using refs
|
||||
const currentWorkspaceId = workspaceIdRef.current
|
||||
const currentRouter = routerRef.current
|
||||
|
||||
if (currentWorkspaceId) {
|
||||
const matchingWorkspace = fetchedWorkspaces.find(
|
||||
(workspace) => workspace.id === currentWorkspaceId
|
||||
)
|
||||
if (matchingWorkspace) {
|
||||
setActiveWorkspace(matchingWorkspace)
|
||||
} else {
|
||||
logger.warn(`Workspace ${currentWorkspaceId} not found in user's workspaces`)
|
||||
|
||||
// Fallback to first workspace if current not found
|
||||
if (fetchedWorkspaces.length > 0) {
|
||||
const fallbackWorkspace = fetchedWorkspaces[0]
|
||||
setActiveWorkspace(fallbackWorkspace)
|
||||
|
||||
// Update URL to match the fallback workspace
|
||||
logger.info(`Redirecting to fallback workspace: ${fallbackWorkspace.id}`)
|
||||
currentRouter?.push(`/workspace/${fallbackWorkspace.id}/w`)
|
||||
} else {
|
||||
logger.error('No workspaces available for user')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('Error fetching workspaces:', err)
|
||||
} finally {
|
||||
setIsWorkspacesLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* Update workspace name both in API and local state
|
||||
*/
|
||||
const updateWorkspaceName = useCallback(
|
||||
async (workspaceId: string, newName: string): Promise<boolean> => {
|
||||
try {
|
||||
const response = await fetch(`/api/workspaces/${workspaceId}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: newName.trim() }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.error || 'Failed to update workspace name')
|
||||
}
|
||||
|
||||
// Update local state immediately after successful API call
|
||||
// Only update activeWorkspace if it's the one being renamed
|
||||
setActiveWorkspace((prev) =>
|
||||
prev && prev.id === workspaceId ? { ...prev, name: newName.trim() } : prev
|
||||
)
|
||||
setWorkspaces((prev) =>
|
||||
prev.map((workspace) =>
|
||||
workspace.id === workspaceId ? { ...workspace, name: newName.trim() } : workspace
|
||||
)
|
||||
)
|
||||
|
||||
await updateWorkspaceNameMutation.mutateAsync({ workspaceId, name: newName })
|
||||
logger.info('Successfully updated workspace name to:', newName.trim())
|
||||
return true
|
||||
} catch (error) {
|
||||
@@ -174,21 +104,18 @@ export function useWorkspaceManagement({
|
||||
return false
|
||||
}
|
||||
},
|
||||
[]
|
||||
[updateWorkspaceNameMutation]
|
||||
)
|
||||
|
||||
const switchWorkspace = useCallback(
|
||||
async (workspace: Workspace) => {
|
||||
// If already on this workspace, return
|
||||
if (activeWorkspaceRef.current?.id === workspace.id) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// Switch workspace and update URL
|
||||
await switchToWorkspace(workspace.id)
|
||||
const currentPath = pathnameRef.current || ''
|
||||
// Preserve templates route if user is on templates or template detail
|
||||
const templateDetailMatch = currentPath.match(/^\/workspace\/[^/]+\/templates\/([^/]+)$/)
|
||||
if (templateDetailMatch) {
|
||||
const templateId = templateDetailMatch[1]
|
||||
@@ -206,208 +133,122 @@ export function useWorkspaceManagement({
|
||||
[switchToWorkspace]
|
||||
)
|
||||
|
||||
/**
|
||||
* Handle create workspace
|
||||
*/
|
||||
const handleCreateWorkspace = useCallback(async () => {
|
||||
if (isCreatingWorkspace) {
|
||||
if (createWorkspaceMutation.isPending) {
|
||||
logger.info('Workspace creation already in progress, ignoring request')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setIsCreatingWorkspace(true)
|
||||
logger.info('Creating new workspace')
|
||||
|
||||
// Generate workspace name using utility function
|
||||
const workspaceName = await generateWorkspaceName()
|
||||
|
||||
logger.info(`Generated workspace name: ${workspaceName}`)
|
||||
|
||||
const response = await fetch('/api/workspaces', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: workspaceName,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(errorData.error || 'Failed to create workspace')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const newWorkspace = data.workspace
|
||||
|
||||
const newWorkspace = await createWorkspaceMutation.mutateAsync({ name: workspaceName })
|
||||
logger.info('Created new workspace:', newWorkspace)
|
||||
|
||||
// Refresh workspace list (no URL validation needed for creation)
|
||||
await refreshWorkspaceList()
|
||||
|
||||
// Switch to the new workspace
|
||||
await switchWorkspace(newWorkspace)
|
||||
} catch (error) {
|
||||
logger.error('Error creating workspace:', error)
|
||||
} finally {
|
||||
setIsCreatingWorkspace(false)
|
||||
}
|
||||
}, [refreshWorkspaceList, switchWorkspace, isCreatingWorkspace])
|
||||
}, [createWorkspaceMutation, switchWorkspace])
|
||||
|
||||
/**
|
||||
* Confirm delete workspace
|
||||
*/
|
||||
const confirmDeleteWorkspace = useCallback(
|
||||
async (workspaceToDelete: Workspace, templateAction?: 'keep' | 'delete') => {
|
||||
setIsDeleting(true)
|
||||
try {
|
||||
logger.info('Deleting workspace:', workspaceToDelete.id)
|
||||
|
||||
const deleteTemplates = templateAction === 'delete'
|
||||
|
||||
const response = await fetch(`/api/workspaces/${workspaceToDelete.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ deleteTemplates }),
|
||||
await deleteWorkspaceMutation.mutateAsync({
|
||||
workspaceId: workspaceToDelete.id,
|
||||
deleteTemplates,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(errorData.error || 'Failed to delete workspace')
|
||||
}
|
||||
|
||||
logger.info('Workspace deleted successfully:', workspaceToDelete.id)
|
||||
|
||||
// Check if we're deleting the current workspace (either active or in URL)
|
||||
const isDeletingCurrentWorkspace =
|
||||
workspaceIdRef.current === workspaceToDelete.id ||
|
||||
activeWorkspaceRef.current?.id === workspaceToDelete.id
|
||||
|
||||
if (isDeletingCurrentWorkspace) {
|
||||
// For current workspace deletion, use full fetchWorkspaces with URL validation
|
||||
logger.info(
|
||||
'Deleting current workspace - using full workspace refresh with URL validation'
|
||||
)
|
||||
await fetchWorkspaces()
|
||||
hasValidatedRef.current = false
|
||||
const { data: updatedWorkspaces } = await refetchWorkspaces()
|
||||
|
||||
// If we deleted the active workspace, switch to the first available workspace
|
||||
if (activeWorkspaceRef.current?.id === workspaceToDelete.id) {
|
||||
const remainingWorkspaces = workspaces.filter((w) => w.id !== workspaceToDelete.id)
|
||||
if (remainingWorkspaces.length > 0) {
|
||||
await switchWorkspace(remainingWorkspaces[0])
|
||||
}
|
||||
const remainingWorkspaces = (updatedWorkspaces || []).filter(
|
||||
(w) => w.id !== workspaceToDelete.id
|
||||
)
|
||||
if (remainingWorkspaces.length > 0) {
|
||||
await switchWorkspace(remainingWorkspaces[0])
|
||||
}
|
||||
} else {
|
||||
// For non-current workspace deletion, just refresh the list without URL validation
|
||||
logger.info('Deleting non-current workspace - using simple list refresh')
|
||||
await refreshWorkspaceList()
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error deleting workspace:', error)
|
||||
} finally {
|
||||
setIsDeleting(false)
|
||||
}
|
||||
},
|
||||
[fetchWorkspaces, refreshWorkspaceList, workspaces, switchWorkspace]
|
||||
[deleteWorkspaceMutation, refetchWorkspaces, switchWorkspace]
|
||||
)
|
||||
|
||||
/**
|
||||
* Handle leave workspace
|
||||
*/
|
||||
const handleLeaveWorkspace = useCallback(
|
||||
async (workspaceToLeave: Workspace) => {
|
||||
setIsLeaving(true)
|
||||
if (!sessionUserId) {
|
||||
logger.error('Cannot leave workspace: no session user ID')
|
||||
return
|
||||
}
|
||||
|
||||
logger.info('Leaving workspace:', workspaceToLeave.id)
|
||||
|
||||
try {
|
||||
logger.info('Leaving workspace:', workspaceToLeave.id)
|
||||
|
||||
// Use the existing member removal API with current user's ID
|
||||
const response = await fetch(`/api/workspaces/members/${sessionUserId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
workspaceId: workspaceToLeave.id,
|
||||
}),
|
||||
await leaveWorkspaceMutation.mutateAsync({
|
||||
userId: sessionUserId,
|
||||
workspaceId: workspaceToLeave.id,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(errorData.error || 'Failed to leave workspace')
|
||||
}
|
||||
|
||||
logger.info('Left workspace successfully:', workspaceToLeave.id)
|
||||
|
||||
// Check if we're leaving the current workspace (either active or in URL)
|
||||
const isLeavingCurrentWorkspace =
|
||||
workspaceIdRef.current === workspaceToLeave.id ||
|
||||
activeWorkspaceRef.current?.id === workspaceToLeave.id
|
||||
|
||||
if (isLeavingCurrentWorkspace) {
|
||||
// For current workspace leaving, use full fetchWorkspaces with URL validation
|
||||
logger.info(
|
||||
'Leaving current workspace - using full workspace refresh with URL validation'
|
||||
)
|
||||
await fetchWorkspaces()
|
||||
hasValidatedRef.current = false
|
||||
const { data: updatedWorkspaces } = await refetchWorkspaces()
|
||||
|
||||
// If we left the active workspace, switch to the first available workspace
|
||||
if (activeWorkspaceRef.current?.id === workspaceToLeave.id) {
|
||||
const remainingWorkspaces = workspaces.filter((w) => w.id !== workspaceToLeave.id)
|
||||
if (remainingWorkspaces.length > 0) {
|
||||
await switchWorkspace(remainingWorkspaces[0])
|
||||
}
|
||||
const remainingWorkspaces = (updatedWorkspaces || []).filter(
|
||||
(w) => w.id !== workspaceToLeave.id
|
||||
)
|
||||
if (remainingWorkspaces.length > 0) {
|
||||
await switchWorkspace(remainingWorkspaces[0])
|
||||
}
|
||||
} else {
|
||||
// For non-current workspace leaving, just refresh the list without URL validation
|
||||
logger.info('Leaving non-current workspace - using simple list refresh')
|
||||
await refreshWorkspaceList()
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error leaving workspace:', error)
|
||||
} finally {
|
||||
setIsLeaving(false)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
[fetchWorkspaces, refreshWorkspaceList, workspaces, switchWorkspace, sessionUserId]
|
||||
[refetchWorkspaces, switchWorkspace, sessionUserId, leaveWorkspaceMutation]
|
||||
)
|
||||
|
||||
/**
|
||||
* Validate workspace exists before making API calls
|
||||
*/
|
||||
const isWorkspaceValid = useCallback(async (workspaceId: string) => {
|
||||
try {
|
||||
const response = await fetch(`/api/workspaces/${workspaceId}`)
|
||||
return response.ok
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* Initialize workspace data on mount (uses full validation with URL handling)
|
||||
* fetchWorkspaces is stable (empty deps array), so it's safe to call without including it
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (sessionUserId && !isInitializedRef.current) {
|
||||
isInitializedRef.current = true
|
||||
fetchWorkspaces()
|
||||
}
|
||||
}, [sessionUserId, fetchWorkspaces])
|
||||
const isWorkspaceValid = useCallback(
|
||||
(targetWorkspaceId: string) => {
|
||||
return workspaces.some((w) => w.id === targetWorkspaceId)
|
||||
},
|
||||
[workspaces]
|
||||
)
|
||||
|
||||
return {
|
||||
// State
|
||||
workspaces,
|
||||
activeWorkspace,
|
||||
isWorkspacesLoading,
|
||||
isCreatingWorkspace,
|
||||
isDeleting,
|
||||
isLeaving,
|
||||
|
||||
// Operations
|
||||
isCreatingWorkspace: createWorkspaceMutation.isPending,
|
||||
isDeleting: deleteWorkspaceMutation.isPending,
|
||||
isLeaving: leaveWorkspaceMutation.isPending,
|
||||
fetchWorkspaces,
|
||||
refreshWorkspaceList,
|
||||
updateWorkspaceName,
|
||||
@@ -418,3 +259,5 @@ export function useWorkspaceManagement({
|
||||
isWorkspaceValid,
|
||||
}
|
||||
}
|
||||
|
||||
export type { Workspace }
|
||||
|
||||
@@ -6,7 +6,7 @@ import { getTrigger } from '@/triggers'
|
||||
|
||||
export const CalComBlock: BlockConfig<ToolResponse> = {
|
||||
type: 'calcom',
|
||||
name: 'CalCom',
|
||||
name: 'Cal Com',
|
||||
description: 'Manage Cal.com bookings, event types, schedules, and availability',
|
||||
authMode: AuthMode.OAuth,
|
||||
triggerAllowed: true,
|
||||
|
||||
175
apps/sim/blocks/blocks/dspy.ts
Normal file
175
apps/sim/blocks/blocks/dspy.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import { DsPyIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
|
||||
export const DSPyBlock: BlockConfig = {
|
||||
type: 'dspy',
|
||||
name: 'DSPy',
|
||||
description: 'Run predictions using self-hosted DSPy programs',
|
||||
longDescription:
|
||||
'Integrate with your self-hosted DSPy programs for LLM-powered predictions. Supports Predict, Chain of Thought, and ReAct agents. DSPy is the framework for programming—not prompting—language models.',
|
||||
category: 'tools',
|
||||
bgColor: '#1E293B',
|
||||
icon: DsPyIcon,
|
||||
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'operation',
|
||||
title: 'Operation',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Predict', id: 'predict' },
|
||||
{ label: 'Chain of Thought', id: 'chain_of_thought' },
|
||||
{ label: 'ReAct Agent', id: 'react' },
|
||||
],
|
||||
value: () => 'predict',
|
||||
},
|
||||
|
||||
{
|
||||
id: 'baseUrl',
|
||||
title: 'Base URL',
|
||||
type: 'short-input',
|
||||
placeholder: 'https://your-dspy-server.com',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'apiKey',
|
||||
title: 'API Key',
|
||||
type: 'short-input',
|
||||
password: true,
|
||||
placeholder: 'Optional API key for authentication',
|
||||
},
|
||||
{
|
||||
id: 'endpoint',
|
||||
title: 'Endpoint',
|
||||
type: 'short-input',
|
||||
placeholder: '/predict',
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
// Predict operation fields
|
||||
{
|
||||
id: 'input',
|
||||
title: 'Input',
|
||||
type: 'long-input',
|
||||
placeholder: 'Enter your input text',
|
||||
condition: { field: 'operation', value: 'predict' },
|
||||
required: { field: 'operation', value: 'predict' },
|
||||
rows: 4,
|
||||
},
|
||||
{
|
||||
id: 'inputField',
|
||||
title: 'Input Field Name',
|
||||
type: 'short-input',
|
||||
placeholder: 'text (defaults to "text")',
|
||||
condition: { field: 'operation', value: 'predict' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'additionalInputs',
|
||||
title: 'Additional Inputs',
|
||||
type: 'long-input',
|
||||
placeholder: '{"key": "value"} - JSON object with extra fields',
|
||||
condition: { field: 'operation', value: 'predict' },
|
||||
mode: 'advanced',
|
||||
rows: 3,
|
||||
},
|
||||
|
||||
// Chain of Thought operation fields
|
||||
{
|
||||
id: 'question',
|
||||
title: 'Question',
|
||||
type: 'long-input',
|
||||
placeholder: 'Enter your question',
|
||||
condition: { field: 'operation', value: 'chain_of_thought' },
|
||||
required: { field: 'operation', value: 'chain_of_thought' },
|
||||
rows: 4,
|
||||
},
|
||||
|
||||
// ReAct operation fields
|
||||
{
|
||||
id: 'task',
|
||||
title: 'Task',
|
||||
type: 'long-input',
|
||||
placeholder: 'Describe the task for the ReAct agent',
|
||||
condition: { field: 'operation', value: 'react' },
|
||||
required: { field: 'operation', value: 'react' },
|
||||
rows: 4,
|
||||
},
|
||||
{
|
||||
id: 'maxIterations',
|
||||
title: 'Max Iterations',
|
||||
type: 'short-input',
|
||||
placeholder: 'Maximum reasoning iterations',
|
||||
condition: { field: 'operation', value: 'react' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
// Common optional fields
|
||||
{
|
||||
id: 'context',
|
||||
title: 'Context',
|
||||
type: 'long-input',
|
||||
placeholder: 'Additional context for the DSPy program',
|
||||
mode: 'advanced',
|
||||
rows: 4,
|
||||
},
|
||||
],
|
||||
|
||||
tools: {
|
||||
access: ['dspy_predict', 'dspy_chain_of_thought', 'dspy_react'],
|
||||
config: {
|
||||
tool: (params) => `dspy_${params.operation}`,
|
||||
params: (params) => {
|
||||
const { operation, additionalInputs, maxIterations, ...rest } = params
|
||||
|
||||
let parsedAdditionalInputs: Record<string, unknown> | undefined
|
||||
if (additionalInputs && typeof additionalInputs === 'string') {
|
||||
try {
|
||||
parsedAdditionalInputs = JSON.parse(additionalInputs)
|
||||
} catch {
|
||||
// Ignore parse errors
|
||||
}
|
||||
}
|
||||
|
||||
let parsedMaxIterations: number | undefined
|
||||
if (maxIterations) {
|
||||
const parsed = Number.parseInt(maxIterations as string, 10)
|
||||
if (!Number.isNaN(parsed)) {
|
||||
parsedMaxIterations = parsed
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...rest,
|
||||
additionalInputs: parsedAdditionalInputs,
|
||||
maxIterations: parsedMaxIterations,
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
inputs: {
|
||||
operation: { type: 'string', description: 'DSPy operation to perform' },
|
||||
baseUrl: { type: 'string', description: 'Base URL of the DSPy server' },
|
||||
apiKey: { type: 'string', description: 'API key for authentication' },
|
||||
endpoint: { type: 'string', description: 'API endpoint path' },
|
||||
input: { type: 'string', description: 'Input text for Predict operation' },
|
||||
inputField: { type: 'string', description: 'Name of the input field' },
|
||||
context: { type: 'string', description: 'Additional context for the program' },
|
||||
additionalInputs: { type: 'string', description: 'JSON object with extra fields' },
|
||||
question: { type: 'string', description: 'Question for Chain of Thought' },
|
||||
task: { type: 'string', description: 'Task for ReAct agent' },
|
||||
maxIterations: { type: 'string', description: 'Max iterations for ReAct' },
|
||||
},
|
||||
|
||||
outputs: {
|
||||
answer: { type: 'string', description: 'The answer/output from the DSPy program' },
|
||||
reasoning: { type: 'string', description: 'The reasoning or rationale behind the answer' },
|
||||
trajectory: {
|
||||
type: 'json',
|
||||
description: 'Step-by-step trajectory for ReAct (thoughts, actions, observations)',
|
||||
},
|
||||
status: { type: 'string', description: 'Response status from the DSPy server' },
|
||||
rawOutput: { type: 'json', description: 'Complete raw output from the DSPy program' },
|
||||
},
|
||||
}
|
||||
626
apps/sim/blocks/blocks/google_maps.ts
Normal file
626
apps/sim/blocks/blocks/google_maps.ts
Normal file
@@ -0,0 +1,626 @@
|
||||
import { GoogleMapsIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
|
||||
export const GoogleMapsBlock: BlockConfig = {
|
||||
type: 'google_maps',
|
||||
name: 'Google Maps',
|
||||
description: 'Geocoding, directions, places, and distance calculations',
|
||||
longDescription:
|
||||
'Integrate Google Maps Platform APIs into your workflow. Supports geocoding addresses to coordinates, reverse geocoding, getting directions between locations, calculating distance matrices, searching for places, retrieving place details, elevation data, and timezone information.',
|
||||
docsLink: 'https://docs.sim.ai/tools/google_maps',
|
||||
category: 'tools',
|
||||
bgColor: '#E0E0E0',
|
||||
icon: GoogleMapsIcon,
|
||||
|
||||
subBlocks: [
|
||||
// Operation selector
|
||||
{
|
||||
id: 'operation',
|
||||
title: 'Operation',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Geocode Address', id: 'geocode' },
|
||||
{ label: 'Reverse Geocode', id: 'reverse_geocode' },
|
||||
{ label: 'Get Directions', id: 'directions' },
|
||||
{ label: 'Distance Matrix', id: 'distance_matrix' },
|
||||
{ label: 'Search Places', id: 'places_search' },
|
||||
{ label: 'Place Details', id: 'place_details' },
|
||||
{ label: 'Get Elevation', id: 'elevation' },
|
||||
{ label: 'Get Timezone', id: 'timezone' },
|
||||
{ label: 'Snap to Roads', id: 'snap_to_roads' },
|
||||
{ label: 'Speed Limits', id: 'speed_limits' },
|
||||
{ label: 'Validate Address', id: 'validate_address' },
|
||||
{ label: 'Geolocate (WiFi/Cell)', id: 'geolocate' },
|
||||
{ label: 'Air Quality', id: 'air_quality' },
|
||||
],
|
||||
value: () => 'geocode',
|
||||
},
|
||||
|
||||
// API Key
|
||||
{
|
||||
id: 'apiKey',
|
||||
title: 'API Key',
|
||||
type: 'short-input',
|
||||
password: true,
|
||||
placeholder: 'Enter your Google Maps API key',
|
||||
required: true,
|
||||
},
|
||||
|
||||
// ========== Geocode ==========
|
||||
{
|
||||
id: 'address',
|
||||
title: 'Address',
|
||||
type: 'long-input',
|
||||
placeholder: '1600 Amphitheatre Parkway, Mountain View, CA',
|
||||
condition: { field: 'operation', value: 'geocode' },
|
||||
required: { field: 'operation', value: 'geocode' },
|
||||
rows: 2,
|
||||
},
|
||||
|
||||
{
|
||||
id: 'latitude',
|
||||
title: 'Latitude',
|
||||
type: 'short-input',
|
||||
placeholder: '37.4224764',
|
||||
condition: { field: 'operation', value: ['reverse_geocode', 'elevation', 'timezone'] },
|
||||
required: { field: 'operation', value: ['reverse_geocode', 'elevation', 'timezone'] },
|
||||
},
|
||||
{
|
||||
id: 'longitude',
|
||||
title: 'Longitude',
|
||||
type: 'short-input',
|
||||
placeholder: '-122.0842499',
|
||||
condition: { field: 'operation', value: ['reverse_geocode', 'elevation', 'timezone'] },
|
||||
required: { field: 'operation', value: ['reverse_geocode', 'elevation', 'timezone'] },
|
||||
},
|
||||
|
||||
{
|
||||
id: 'timestamp',
|
||||
title: 'Timestamp',
|
||||
type: 'short-input',
|
||||
placeholder: 'Unix timestamp (defaults to current time)',
|
||||
condition: { field: 'operation', value: 'timezone' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
{
|
||||
id: 'origin',
|
||||
title: 'Origin',
|
||||
type: 'short-input',
|
||||
placeholder: 'Starting address or coordinates (lat,lng)',
|
||||
condition: { field: 'operation', value: ['directions', 'distance_matrix'] },
|
||||
required: { field: 'operation', value: ['directions', 'distance_matrix'] },
|
||||
},
|
||||
{
|
||||
id: 'destination',
|
||||
title: 'Destination',
|
||||
type: 'short-input',
|
||||
placeholder: 'Destination address or coordinates (lat,lng)',
|
||||
condition: { field: 'operation', value: 'directions' },
|
||||
required: { field: 'operation', value: 'directions' },
|
||||
},
|
||||
{
|
||||
id: 'destinations',
|
||||
title: 'Destinations',
|
||||
type: 'long-input',
|
||||
placeholder: 'Destination addresses separated by | (e.g., New York, NY|Boston, MA)',
|
||||
condition: { field: 'operation', value: 'distance_matrix' },
|
||||
required: { field: 'operation', value: 'distance_matrix' },
|
||||
rows: 3,
|
||||
},
|
||||
{
|
||||
id: 'mode',
|
||||
title: 'Travel Mode',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Driving', id: 'driving' },
|
||||
{ label: 'Walking', id: 'walking' },
|
||||
{ label: 'Bicycling', id: 'bicycling' },
|
||||
{ label: 'Transit', id: 'transit' },
|
||||
],
|
||||
value: () => 'driving',
|
||||
condition: { field: 'operation', value: ['directions', 'distance_matrix'] },
|
||||
},
|
||||
{
|
||||
id: 'avoid',
|
||||
title: 'Avoid',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'None', id: '' },
|
||||
{ label: 'Tolls', id: 'tolls' },
|
||||
{ label: 'Highways', id: 'highways' },
|
||||
{ label: 'Ferries', id: 'ferries' },
|
||||
],
|
||||
condition: { field: 'operation', value: ['directions', 'distance_matrix'] },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'waypoints',
|
||||
title: 'Waypoints',
|
||||
type: 'long-input',
|
||||
placeholder: 'Optional stops separated by | (e.g., Stop 1|Stop 2)',
|
||||
condition: { field: 'operation', value: 'directions' },
|
||||
rows: 2,
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'units',
|
||||
title: 'Units',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Metric (km)', id: 'metric' },
|
||||
{ label: 'Imperial (miles)', id: 'imperial' },
|
||||
],
|
||||
value: () => 'metric',
|
||||
condition: { field: 'operation', value: ['directions', 'distance_matrix'] },
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
{
|
||||
id: 'query',
|
||||
title: 'Search Query',
|
||||
type: 'short-input',
|
||||
placeholder: 'restaurants near Times Square',
|
||||
condition: { field: 'operation', value: 'places_search' },
|
||||
required: { field: 'operation', value: 'places_search' },
|
||||
},
|
||||
{
|
||||
id: 'locationBias',
|
||||
title: 'Location Bias',
|
||||
type: 'short-input',
|
||||
placeholder: 'lat,lng to bias results (e.g., 40.7580,-73.9855)',
|
||||
condition: { field: 'operation', value: 'places_search' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'radius',
|
||||
title: 'Radius (meters)',
|
||||
type: 'short-input',
|
||||
placeholder: 'Search radius in meters (e.g., 5000)',
|
||||
condition: { field: 'operation', value: 'places_search' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'placeType',
|
||||
title: 'Place Type',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Any', id: '' },
|
||||
{ label: 'Restaurant', id: 'restaurant' },
|
||||
{ label: 'Cafe', id: 'cafe' },
|
||||
{ label: 'Bar', id: 'bar' },
|
||||
{ label: 'Hotel', id: 'lodging' },
|
||||
{ label: 'Gas Station', id: 'gas_station' },
|
||||
{ label: 'Hospital', id: 'hospital' },
|
||||
{ label: 'Pharmacy', id: 'pharmacy' },
|
||||
{ label: 'Bank', id: 'bank' },
|
||||
{ label: 'ATM', id: 'atm' },
|
||||
{ label: 'Grocery Store', id: 'supermarket' },
|
||||
{ label: 'Shopping Mall', id: 'shopping_mall' },
|
||||
{ label: 'Gym', id: 'gym' },
|
||||
{ label: 'Park', id: 'park' },
|
||||
{ label: 'Museum', id: 'museum' },
|
||||
{ label: 'Movie Theater', id: 'movie_theater' },
|
||||
{ label: 'Airport', id: 'airport' },
|
||||
{ label: 'Train Station', id: 'train_station' },
|
||||
{ label: 'Bus Station', id: 'bus_station' },
|
||||
{ label: 'Parking', id: 'parking' },
|
||||
],
|
||||
condition: { field: 'operation', value: 'places_search' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
{
|
||||
id: 'placeId',
|
||||
title: 'Place ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Google Place ID (e.g., ChIJN1t_tDeuEmsRUsoyG83frY4)',
|
||||
condition: { field: 'operation', value: 'place_details' },
|
||||
required: { field: 'operation', value: 'place_details' },
|
||||
},
|
||||
{
|
||||
id: 'fields',
|
||||
title: 'Fields',
|
||||
type: 'short-input',
|
||||
placeholder: 'name,formatted_address,rating,opening_hours',
|
||||
condition: { field: 'operation', value: 'place_details' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
{
|
||||
id: 'path',
|
||||
title: 'Path',
|
||||
type: 'long-input',
|
||||
placeholder: 'Pipe-separated lat,lng pairs (e.g., 60.170880,24.942795|60.170879,24.942796)',
|
||||
condition: { field: 'operation', value: ['snap_to_roads', 'speed_limits'] },
|
||||
required: { field: 'operation', value: 'snap_to_roads' },
|
||||
rows: 3,
|
||||
},
|
||||
{
|
||||
id: 'interpolate',
|
||||
title: 'Interpolate',
|
||||
type: 'switch',
|
||||
condition: { field: 'operation', value: 'snap_to_roads' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
{
|
||||
id: 'placeIds',
|
||||
title: 'Place IDs',
|
||||
type: 'long-input',
|
||||
placeholder: 'Pipe-separated Place IDs (alternative to path)',
|
||||
condition: { field: 'operation', value: 'speed_limits' },
|
||||
rows: 2,
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
{
|
||||
id: 'addressToValidate',
|
||||
title: 'Address',
|
||||
type: 'long-input',
|
||||
placeholder: '1600 Amphitheatre Parkway, Mountain View, CA 94043',
|
||||
condition: { field: 'operation', value: 'validate_address' },
|
||||
required: { field: 'operation', value: 'validate_address' },
|
||||
rows: 2,
|
||||
},
|
||||
{
|
||||
id: 'regionCode',
|
||||
title: 'Region Code',
|
||||
type: 'short-input',
|
||||
placeholder: 'ISO country code (e.g., US, CA, GB)',
|
||||
condition: { field: 'operation', value: 'validate_address' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'locality',
|
||||
title: 'Locality',
|
||||
type: 'short-input',
|
||||
placeholder: 'City name (optional hint)',
|
||||
condition: { field: 'operation', value: 'validate_address' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'enableUspsCass',
|
||||
title: 'Enable USPS CASS',
|
||||
type: 'switch',
|
||||
condition: { field: 'operation', value: 'validate_address' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
{
|
||||
id: 'considerIp',
|
||||
title: 'Use IP Address',
|
||||
type: 'switch',
|
||||
condition: { field: 'operation', value: 'geolocate' },
|
||||
},
|
||||
{
|
||||
id: 'radioType',
|
||||
title: 'Radio Type',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'None', id: '' },
|
||||
{ label: 'LTE', id: 'lte' },
|
||||
{ label: 'GSM', id: 'gsm' },
|
||||
{ label: 'CDMA', id: 'cdma' },
|
||||
{ label: 'WCDMA', id: 'wcdma' },
|
||||
{ label: '5G NR', id: 'nr' },
|
||||
],
|
||||
condition: { field: 'operation', value: 'geolocate' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'carrier',
|
||||
title: 'Carrier',
|
||||
type: 'short-input',
|
||||
placeholder: 'Carrier name',
|
||||
condition: { field: 'operation', value: 'geolocate' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'wifiAccessPoints',
|
||||
title: 'WiFi Access Points',
|
||||
type: 'long-input',
|
||||
placeholder: 'JSON array of WiFi APs: [{"macAddress": "..."}]',
|
||||
condition: { field: 'operation', value: 'geolocate' },
|
||||
rows: 3,
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'cellTowers',
|
||||
title: 'Cell Towers',
|
||||
type: 'long-input',
|
||||
placeholder: 'JSON array of cell towers: [{"cellId": ..., "locationAreaCode": ...}]',
|
||||
condition: { field: 'operation', value: 'geolocate' },
|
||||
rows: 3,
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
{
|
||||
id: 'aqLatitude',
|
||||
title: 'Latitude',
|
||||
type: 'short-input',
|
||||
placeholder: '37.4224764',
|
||||
condition: { field: 'operation', value: 'air_quality' },
|
||||
required: { field: 'operation', value: 'air_quality' },
|
||||
},
|
||||
{
|
||||
id: 'aqLongitude',
|
||||
title: 'Longitude',
|
||||
type: 'short-input',
|
||||
placeholder: '-122.0842499',
|
||||
condition: { field: 'operation', value: 'air_quality' },
|
||||
required: { field: 'operation', value: 'air_quality' },
|
||||
},
|
||||
{
|
||||
id: 'languageCode',
|
||||
title: 'Language Code',
|
||||
type: 'short-input',
|
||||
placeholder: 'Language code (e.g., en, es)',
|
||||
condition: { field: 'operation', value: 'air_quality' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
{
|
||||
id: 'language',
|
||||
title: 'Language',
|
||||
type: 'short-input',
|
||||
placeholder: 'Language code (e.g., en, es, fr, de)',
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'region',
|
||||
title: 'Region Bias',
|
||||
type: 'short-input',
|
||||
placeholder: 'Country code (e.g., us, uk, de)',
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: ['geocode', 'places_search'] },
|
||||
},
|
||||
],
|
||||
|
||||
tools: {
|
||||
access: [
|
||||
'google_maps_air_quality',
|
||||
'google_maps_directions',
|
||||
'google_maps_distance_matrix',
|
||||
'google_maps_elevation',
|
||||
'google_maps_geocode',
|
||||
'google_maps_geolocate',
|
||||
'google_maps_place_details',
|
||||
'google_maps_places_search',
|
||||
'google_maps_reverse_geocode',
|
||||
'google_maps_snap_to_roads',
|
||||
'google_maps_speed_limits',
|
||||
'google_maps_timezone',
|
||||
'google_maps_validate_address',
|
||||
],
|
||||
config: {
|
||||
tool: (params) => `google_maps_${params.operation}`,
|
||||
params: (params) => {
|
||||
const { operation, locationBias, ...rest } = params
|
||||
|
||||
let location: { lat: number; lng: number } | undefined
|
||||
if (locationBias && typeof locationBias === 'string' && locationBias.includes(',')) {
|
||||
const [lat, lng] = locationBias.split(',').map((s) => Number.parseFloat(s.trim()))
|
||||
if (!Number.isNaN(lat) && !Number.isNaN(lng)) {
|
||||
location = { lat, lng }
|
||||
}
|
||||
}
|
||||
|
||||
let lat: number | undefined
|
||||
let lng: number | undefined
|
||||
if (params.latitude) {
|
||||
lat = Number.parseFloat(params.latitude)
|
||||
}
|
||||
if (params.longitude) {
|
||||
lng = Number.parseFloat(params.longitude)
|
||||
}
|
||||
|
||||
if (params.aqLatitude) {
|
||||
lat = Number.parseFloat(params.aqLatitude)
|
||||
}
|
||||
if (params.aqLongitude) {
|
||||
lng = Number.parseFloat(params.aqLongitude)
|
||||
}
|
||||
|
||||
let timestamp: number | undefined
|
||||
if (params.timestamp) {
|
||||
timestamp = Number.parseInt(params.timestamp, 10)
|
||||
}
|
||||
|
||||
let destinations: string[] | undefined
|
||||
if (params.destinations && typeof params.destinations === 'string') {
|
||||
destinations = params.destinations.split('|').map((d: string) => d.trim())
|
||||
}
|
||||
|
||||
let waypoints: string[] | undefined
|
||||
if (params.waypoints && typeof params.waypoints === 'string') {
|
||||
waypoints = params.waypoints
|
||||
.split('|')
|
||||
.map((w: string) => w.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
let radius: number | undefined
|
||||
if (params.radius) {
|
||||
radius = Number.parseInt(params.radius, 10)
|
||||
}
|
||||
|
||||
let placeIds: string[] | undefined
|
||||
if (params.placeIds && typeof params.placeIds === 'string') {
|
||||
placeIds = params.placeIds
|
||||
.split('|')
|
||||
.map((p: string) => p.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
let wifiAccessPoints: unknown[] | undefined
|
||||
if (params.wifiAccessPoints && typeof params.wifiAccessPoints === 'string') {
|
||||
try {
|
||||
wifiAccessPoints = JSON.parse(params.wifiAccessPoints)
|
||||
} catch {
|
||||
// Ignore parse errors
|
||||
}
|
||||
}
|
||||
|
||||
let cellTowers: unknown[] | undefined
|
||||
if (params.cellTowers && typeof params.cellTowers === 'string') {
|
||||
try {
|
||||
cellTowers = JSON.parse(params.cellTowers)
|
||||
} catch {
|
||||
// Ignore parse errors
|
||||
}
|
||||
}
|
||||
|
||||
const address = params.addressToValidate || params.address
|
||||
|
||||
// Parse boolean switches (can come as string or boolean from form)
|
||||
let interpolate: boolean | undefined
|
||||
if (params.interpolate !== undefined) {
|
||||
interpolate = params.interpolate === 'true' || params.interpolate === true
|
||||
}
|
||||
|
||||
let enableUspsCass: boolean | undefined
|
||||
if (params.enableUspsCass !== undefined) {
|
||||
enableUspsCass = params.enableUspsCass === 'true' || params.enableUspsCass === true
|
||||
}
|
||||
|
||||
let considerIp: boolean | undefined
|
||||
if (params.considerIp !== undefined) {
|
||||
considerIp = params.considerIp === 'true' || params.considerIp === true
|
||||
}
|
||||
|
||||
return {
|
||||
...rest,
|
||||
address,
|
||||
location,
|
||||
lat,
|
||||
lng,
|
||||
timestamp,
|
||||
destinations,
|
||||
waypoints,
|
||||
radius,
|
||||
placeIds,
|
||||
wifiAccessPoints,
|
||||
cellTowers,
|
||||
interpolate,
|
||||
enableUspsCass,
|
||||
considerIp,
|
||||
type: params.placeType || undefined,
|
||||
avoid: params.avoid || undefined,
|
||||
radioType: params.radioType || undefined,
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
inputs: {
|
||||
operation: { type: 'string', description: 'Operation to perform' },
|
||||
apiKey: { type: 'string', description: 'Google Maps API key' },
|
||||
address: { type: 'string', description: 'Address to geocode' },
|
||||
latitude: { type: 'string', description: 'Latitude coordinate' },
|
||||
longitude: { type: 'string', description: 'Longitude coordinate' },
|
||||
timestamp: { type: 'string', description: 'Unix timestamp for timezone' },
|
||||
origin: { type: 'string', description: 'Starting location' },
|
||||
destination: { type: 'string', description: 'Destination location' },
|
||||
destinations: { type: 'string', description: 'Multiple destinations (pipe-separated)' },
|
||||
mode: { type: 'string', description: 'Travel mode' },
|
||||
avoid: { type: 'string', description: 'Features to avoid' },
|
||||
waypoints: { type: 'string', description: 'Waypoints (pipe-separated)' },
|
||||
query: { type: 'string', description: 'Places search query' },
|
||||
locationBias: { type: 'string', description: 'Location bias for search' },
|
||||
radius: { type: 'string', description: 'Search radius in meters' },
|
||||
placeType: { type: 'string', description: 'Place type filter' },
|
||||
placeId: { type: 'string', description: 'Google Place ID' },
|
||||
fields: { type: 'string', description: 'Fields to retrieve' },
|
||||
units: { type: 'string', description: 'Unit system' },
|
||||
language: { type: 'string', description: 'Response language' },
|
||||
region: { type: 'string', description: 'Region bias' },
|
||||
path: { type: 'string', description: 'Pipe-separated lat,lng coordinates' },
|
||||
interpolate: { type: 'boolean', description: 'Interpolate points along road' },
|
||||
placeIds: { type: 'string', description: 'Pipe-separated Place IDs for speed limits' },
|
||||
addressToValidate: { type: 'string', description: 'Address to validate' },
|
||||
regionCode: { type: 'string', description: 'ISO country code for address' },
|
||||
locality: { type: 'string', description: 'City name hint' },
|
||||
enableUspsCass: { type: 'boolean', description: 'Enable USPS CASS validation' },
|
||||
considerIp: { type: 'boolean', description: 'Use IP for geolocation' },
|
||||
radioType: { type: 'string', description: 'Radio type (lte, gsm, etc.)' },
|
||||
carrier: { type: 'string', description: 'Carrier name' },
|
||||
wifiAccessPoints: { type: 'string', description: 'WiFi access points JSON' },
|
||||
cellTowers: { type: 'string', description: 'Cell towers JSON' },
|
||||
aqLatitude: { type: 'string', description: 'Latitude for air quality' },
|
||||
aqLongitude: { type: 'string', description: 'Longitude for air quality' },
|
||||
languageCode: { type: 'string', description: 'Language code for air quality' },
|
||||
},
|
||||
|
||||
outputs: {
|
||||
formattedAddress: { type: 'string', description: 'Formatted address string' },
|
||||
lat: { type: 'number', description: 'Latitude coordinate' },
|
||||
lng: { type: 'number', description: 'Longitude coordinate' },
|
||||
placeId: { type: 'string', description: 'Google Place ID' },
|
||||
addressComponents: { type: 'json', description: 'Detailed address components' },
|
||||
locationType: { type: 'string', description: 'Location accuracy type' },
|
||||
types: { type: 'json', description: 'Address or place types' },
|
||||
|
||||
routes: { type: 'json', description: 'Available routes' },
|
||||
distanceText: { type: 'string', description: 'Distance as text (e.g., "5.2 km")' },
|
||||
distanceMeters: { type: 'number', description: 'Distance in meters' },
|
||||
durationText: { type: 'string', description: 'Duration as text (e.g., "15 mins")' },
|
||||
durationSeconds: { type: 'number', description: 'Duration in seconds' },
|
||||
startAddress: { type: 'string', description: 'Starting address' },
|
||||
endAddress: { type: 'string', description: 'Ending address' },
|
||||
steps: { type: 'json', description: 'Turn-by-turn directions' },
|
||||
polyline: { type: 'string', description: 'Encoded polyline for the route' },
|
||||
|
||||
rows: { type: 'json', description: 'Distance matrix rows' },
|
||||
originAddresses: { type: 'json', description: 'Resolved origin addresses' },
|
||||
destinationAddresses: { type: 'json', description: 'Resolved destination addresses' },
|
||||
|
||||
places: { type: 'json', description: 'List of places found' },
|
||||
nextPageToken: { type: 'string', description: 'Token for next page of results' },
|
||||
|
||||
name: { type: 'string', description: 'Place name' },
|
||||
rating: { type: 'number', description: 'Place rating (1-5)' },
|
||||
userRatingsTotal: { type: 'number', description: 'Number of user ratings' },
|
||||
priceLevel: { type: 'number', description: 'Price level (0-4)' },
|
||||
website: { type: 'string', description: 'Place website' },
|
||||
phoneNumber: { type: 'string', description: 'Place phone number' },
|
||||
internationalPhoneNumber: { type: 'string', description: 'International phone number' },
|
||||
openNow: { type: 'boolean', description: 'Whether place is currently open' },
|
||||
weekdayText: { type: 'json', description: 'Opening hours by day' },
|
||||
reviews: { type: 'json', description: 'Place reviews' },
|
||||
photos: { type: 'json', description: 'Place photos' },
|
||||
url: { type: 'string', description: 'Google Maps URL for the place' },
|
||||
vicinity: { type: 'string', description: 'Simplified address' },
|
||||
|
||||
elevation: { type: 'number', description: 'Elevation in meters' },
|
||||
resolution: { type: 'number', description: 'Data resolution in meters' },
|
||||
|
||||
timeZoneId: { type: 'string', description: 'Timezone ID (e.g., America/New_York)' },
|
||||
timeZoneName: { type: 'string', description: 'Timezone display name' },
|
||||
rawOffset: { type: 'number', description: 'UTC offset in seconds (without DST)' },
|
||||
dstOffset: { type: 'number', description: 'DST offset in seconds' },
|
||||
|
||||
snappedPoints: { type: 'json', description: 'Snapped road coordinates' },
|
||||
warningMessage: { type: 'string', description: 'Warning message if any' },
|
||||
|
||||
speedLimits: { type: 'json', description: 'Speed limits for road segments' },
|
||||
|
||||
addressComplete: { type: 'boolean', description: 'Whether address is complete' },
|
||||
hasUnconfirmedComponents: { type: 'boolean', description: 'Has unconfirmed components' },
|
||||
hasInferredComponents: { type: 'boolean', description: 'Has inferred components' },
|
||||
hasReplacedComponents: { type: 'boolean', description: 'Has replaced components' },
|
||||
validationGranularity: { type: 'string', description: 'Validation granularity level' },
|
||||
geocodeGranularity: { type: 'string', description: 'Geocode granularity level' },
|
||||
missingComponentTypes: { type: 'json', description: 'Missing address component types' },
|
||||
unconfirmedComponentTypes: { type: 'json', description: 'Unconfirmed component types' },
|
||||
unresolvedTokens: { type: 'json', description: 'Unresolved input tokens' },
|
||||
|
||||
accuracy: { type: 'number', description: 'Location accuracy in meters' },
|
||||
|
||||
dateTime: { type: 'string', description: 'Air quality data timestamp' },
|
||||
regionCode: { type: 'string', description: 'Region code' },
|
||||
indexes: { type: 'json', description: 'Air quality indexes' },
|
||||
pollutants: { type: 'json', description: 'Pollutant concentrations' },
|
||||
healthRecommendations: { type: 'json', description: 'Health recommendations' },
|
||||
},
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import { CursorBlock, CursorV2Block } from '@/blocks/blocks/cursor'
|
||||
import { DatadogBlock } from '@/blocks/blocks/datadog'
|
||||
import { DiscordBlock } from '@/blocks/blocks/discord'
|
||||
import { DropboxBlock } from '@/blocks/blocks/dropbox'
|
||||
import { DSPyBlock } from '@/blocks/blocks/dspy'
|
||||
import { DuckDuckGoBlock } from '@/blocks/blocks/duckduckgo'
|
||||
import { DynamoDBBlock } from '@/blocks/blocks/dynamodb'
|
||||
import { ElasticsearchBlock } from '@/blocks/blocks/elasticsearch'
|
||||
@@ -41,6 +42,7 @@ import { GoogleDocsBlock } from '@/blocks/blocks/google_docs'
|
||||
import { GoogleDriveBlock } from '@/blocks/blocks/google_drive'
|
||||
import { GoogleFormsBlock } from '@/blocks/blocks/google_forms'
|
||||
import { GoogleGroupsBlock } from '@/blocks/blocks/google_groups'
|
||||
import { GoogleMapsBlock } from '@/blocks/blocks/google_maps'
|
||||
import { GoogleSheetsBlock, GoogleSheetsV2Block } from '@/blocks/blocks/google_sheets'
|
||||
import { GoogleSlidesBlock } from '@/blocks/blocks/google_slides'
|
||||
import { GoogleVaultBlock } from '@/blocks/blocks/google_vault'
|
||||
@@ -181,6 +183,7 @@ export const registry: Record<string, BlockConfig> = {
|
||||
datadog: DatadogBlock,
|
||||
discord: DiscordBlock,
|
||||
dropbox: DropboxBlock,
|
||||
dspy: DSPyBlock,
|
||||
duckduckgo: DuckDuckGoBlock,
|
||||
dynamodb: DynamoDBBlock,
|
||||
elasticsearch: ElasticsearchBlock,
|
||||
@@ -204,6 +207,7 @@ export const registry: Record<string, BlockConfig> = {
|
||||
google_drive: GoogleDriveBlock,
|
||||
google_forms: GoogleFormsBlock,
|
||||
google_groups: GoogleGroupsBlock,
|
||||
google_maps: GoogleMapsBlock,
|
||||
google_search: GoogleSearchBlock,
|
||||
google_sheets: GoogleSheetsBlock,
|
||||
google_sheets_v2: GoogleSheetsV2Block,
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -322,7 +322,8 @@ describe('ConditionBlockHandler', () => {
|
||||
|
||||
await handler.execute(mockContext, mockBlock, inputs)
|
||||
|
||||
expect(mockCollectBlockData).toHaveBeenCalledWith(mockContext)
|
||||
// collectBlockData is now called with the current node ID for parallel branch context
|
||||
expect(mockCollectBlockData).toHaveBeenCalledWith(mockContext, mockBlock.id)
|
||||
})
|
||||
|
||||
it('should handle function_execute tool failure', async () => {
|
||||
@@ -620,4 +621,248 @@ describe('ConditionBlockHandler', () => {
|
||||
expect(mockContext.decisions.condition.has(mockBlock.id)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Parallel branch handling', () => {
|
||||
it('should resolve connections and block data correctly when inside a parallel branch', async () => {
|
||||
// Simulate a condition block inside a parallel branch
|
||||
// Virtual block ID uses subscript notation: blockId₍branchIndex₎
|
||||
const parallelConditionBlock: SerializedBlock = {
|
||||
id: 'cond-block-1₍0₎', // Virtual ID for branch 0
|
||||
metadata: { id: 'condition', name: 'Condition' },
|
||||
position: { x: 0, y: 0 },
|
||||
config: {},
|
||||
}
|
||||
|
||||
// Source block also has a virtual ID in the same branch
|
||||
const sourceBlockVirtualId = 'agent-block-1₍0₎'
|
||||
|
||||
// Set up workflow with connections using BASE block IDs (as they are in the workflow definition)
|
||||
const parallelWorkflow: SerializedWorkflow = {
|
||||
blocks: [
|
||||
{
|
||||
id: 'agent-block-1',
|
||||
metadata: { id: 'agent', name: 'Agent' },
|
||||
position: { x: 0, y: 0 },
|
||||
config: {},
|
||||
},
|
||||
{
|
||||
id: 'cond-block-1',
|
||||
metadata: { id: 'condition', name: 'Condition' },
|
||||
position: { x: 100, y: 0 },
|
||||
config: {},
|
||||
},
|
||||
{
|
||||
id: 'target-block-1',
|
||||
metadata: { id: 'api', name: 'Target' },
|
||||
position: { x: 200, y: 0 },
|
||||
config: {},
|
||||
},
|
||||
],
|
||||
connections: [
|
||||
// Connections use base IDs, not virtual IDs
|
||||
{ source: 'agent-block-1', target: 'cond-block-1' },
|
||||
{ source: 'cond-block-1', target: 'target-block-1', sourceHandle: 'condition-cond1' },
|
||||
],
|
||||
loops: [],
|
||||
parallels: [],
|
||||
}
|
||||
|
||||
// Block states use virtual IDs (as outputs are stored per-branch)
|
||||
const parallelBlockStates = new Map<string, BlockState>([
|
||||
[
|
||||
sourceBlockVirtualId,
|
||||
{ output: { response: 'hello from branch 0', success: true }, executed: true },
|
||||
],
|
||||
])
|
||||
|
||||
const parallelContext: ExecutionContext = {
|
||||
workflowId: 'test-workflow-id',
|
||||
workspaceId: 'test-workspace-id',
|
||||
workflow: parallelWorkflow,
|
||||
blockStates: parallelBlockStates,
|
||||
blockLogs: [],
|
||||
completedBlocks: new Set(),
|
||||
decisions: {
|
||||
router: new Map(),
|
||||
condition: new Map(),
|
||||
},
|
||||
environmentVariables: {},
|
||||
workflowVariables: {},
|
||||
}
|
||||
|
||||
const conditions = [
|
||||
{ id: 'cond1', title: 'if', value: 'context.response === "hello from branch 0"' },
|
||||
{ id: 'else1', title: 'else', value: '' },
|
||||
]
|
||||
const inputs = { conditions: JSON.stringify(conditions) }
|
||||
|
||||
const result = await handler.execute(parallelContext, parallelConditionBlock, inputs)
|
||||
|
||||
// The condition should evaluate to true because:
|
||||
// 1. Connection lookup uses base ID 'cond-block-1' (extracted from 'cond-block-1₍0₎')
|
||||
// 2. Source block output is found at virtual ID 'agent-block-1₍0₎' (same branch)
|
||||
// 3. The evaluation context contains { response: 'hello from branch 0' }
|
||||
expect((result as any).conditionResult).toBe(true)
|
||||
expect((result as any).selectedOption).toBe('cond1')
|
||||
expect((result as any).selectedPath).toEqual({
|
||||
blockId: 'target-block-1',
|
||||
blockType: 'api',
|
||||
blockTitle: 'Target',
|
||||
})
|
||||
})
|
||||
|
||||
it('should find correct source block output in parallel branch context', async () => {
|
||||
// Test that when multiple branches exist, the correct branch output is used
|
||||
const parallelConditionBlock: SerializedBlock = {
|
||||
id: 'cond-block-1₍1₎', // Virtual ID for branch 1
|
||||
metadata: { id: 'condition', name: 'Condition' },
|
||||
position: { x: 0, y: 0 },
|
||||
config: {},
|
||||
}
|
||||
|
||||
const parallelWorkflow: SerializedWorkflow = {
|
||||
blocks: [
|
||||
{
|
||||
id: 'agent-block-1',
|
||||
metadata: { id: 'agent', name: 'Agent' },
|
||||
position: { x: 0, y: 0 },
|
||||
config: {},
|
||||
},
|
||||
{
|
||||
id: 'cond-block-1',
|
||||
metadata: { id: 'condition', name: 'Condition' },
|
||||
position: { x: 100, y: 0 },
|
||||
config: {},
|
||||
},
|
||||
{
|
||||
id: 'target-block-1',
|
||||
metadata: { id: 'api', name: 'Target' },
|
||||
position: { x: 200, y: 0 },
|
||||
config: {},
|
||||
},
|
||||
],
|
||||
connections: [
|
||||
{ source: 'agent-block-1', target: 'cond-block-1' },
|
||||
{ source: 'cond-block-1', target: 'target-block-1', sourceHandle: 'condition-cond1' },
|
||||
],
|
||||
loops: [],
|
||||
parallels: [],
|
||||
}
|
||||
|
||||
// Multiple branches have executed - each has different output
|
||||
const parallelBlockStates = new Map<string, BlockState>([
|
||||
['agent-block-1₍0₎', { output: { value: 10 }, executed: true }],
|
||||
['agent-block-1₍1₎', { output: { value: 25 }, executed: true }], // Branch 1 has value 25
|
||||
['agent-block-1₍2₎', { output: { value: 5 }, executed: true }],
|
||||
])
|
||||
|
||||
const parallelContext: ExecutionContext = {
|
||||
workflowId: 'test-workflow-id',
|
||||
workspaceId: 'test-workspace-id',
|
||||
workflow: parallelWorkflow,
|
||||
blockStates: parallelBlockStates,
|
||||
blockLogs: [],
|
||||
completedBlocks: new Set(),
|
||||
decisions: {
|
||||
router: new Map(),
|
||||
condition: new Map(),
|
||||
},
|
||||
environmentVariables: {},
|
||||
workflowVariables: {},
|
||||
}
|
||||
|
||||
// Condition checks if value > 20 - should be true for branch 1 (value=25)
|
||||
const conditions = [
|
||||
{ id: 'cond1', title: 'if', value: 'context.value > 20' },
|
||||
{ id: 'else1', title: 'else', value: '' },
|
||||
]
|
||||
const inputs = { conditions: JSON.stringify(conditions) }
|
||||
|
||||
const result = await handler.execute(parallelContext, parallelConditionBlock, inputs)
|
||||
|
||||
// Should evaluate using branch 1's data (value=25), not branch 0 (value=10) or branch 2 (value=5)
|
||||
expect((result as any).conditionResult).toBe(true)
|
||||
expect((result as any).selectedOption).toBe('cond1')
|
||||
})
|
||||
|
||||
it('should fall back to else when condition is false in parallel branch', async () => {
|
||||
const parallelConditionBlock: SerializedBlock = {
|
||||
id: 'cond-block-1₍2₎', // Virtual ID for branch 2
|
||||
metadata: { id: 'condition', name: 'Condition' },
|
||||
position: { x: 0, y: 0 },
|
||||
config: {},
|
||||
}
|
||||
|
||||
const parallelWorkflow: SerializedWorkflow = {
|
||||
blocks: [
|
||||
{
|
||||
id: 'agent-block-1',
|
||||
metadata: { id: 'agent', name: 'Agent' },
|
||||
position: { x: 0, y: 0 },
|
||||
config: {},
|
||||
},
|
||||
{
|
||||
id: 'cond-block-1',
|
||||
metadata: { id: 'condition', name: 'Condition' },
|
||||
position: { x: 100, y: 0 },
|
||||
config: {},
|
||||
},
|
||||
{
|
||||
id: 'target-true',
|
||||
metadata: { id: 'api', name: 'True Path' },
|
||||
position: { x: 200, y: 0 },
|
||||
config: {},
|
||||
},
|
||||
{
|
||||
id: 'target-false',
|
||||
metadata: { id: 'api', name: 'False Path' },
|
||||
position: { x: 200, y: 100 },
|
||||
config: {},
|
||||
},
|
||||
],
|
||||
connections: [
|
||||
{ source: 'agent-block-1', target: 'cond-block-1' },
|
||||
{ source: 'cond-block-1', target: 'target-true', sourceHandle: 'condition-cond1' },
|
||||
{ source: 'cond-block-1', target: 'target-false', sourceHandle: 'condition-else1' },
|
||||
],
|
||||
loops: [],
|
||||
parallels: [],
|
||||
}
|
||||
|
||||
const parallelBlockStates = new Map<string, BlockState>([
|
||||
['agent-block-1₍0₎', { output: { value: 100 }, executed: true }],
|
||||
['agent-block-1₍1₎', { output: { value: 50 }, executed: true }],
|
||||
['agent-block-1₍2₎', { output: { value: 5 }, executed: true }], // Branch 2 has value 5
|
||||
])
|
||||
|
||||
const parallelContext: ExecutionContext = {
|
||||
workflowId: 'test-workflow-id',
|
||||
workspaceId: 'test-workspace-id',
|
||||
workflow: parallelWorkflow,
|
||||
blockStates: parallelBlockStates,
|
||||
blockLogs: [],
|
||||
completedBlocks: new Set(),
|
||||
decisions: {
|
||||
router: new Map(),
|
||||
condition: new Map(),
|
||||
},
|
||||
environmentVariables: {},
|
||||
workflowVariables: {},
|
||||
}
|
||||
|
||||
// Condition checks if value > 20 - should be false for branch 2 (value=5)
|
||||
const conditions = [
|
||||
{ id: 'cond1', title: 'if', value: 'context.value > 20' },
|
||||
{ id: 'else1', title: 'else', value: '' },
|
||||
]
|
||||
const inputs = { conditions: JSON.stringify(conditions) }
|
||||
|
||||
const result = await handler.execute(parallelContext, parallelConditionBlock, inputs)
|
||||
|
||||
// Should fall back to else path because branch 2's value (5) is not > 20
|
||||
expect((result as any).conditionResult).toBe(true)
|
||||
expect((result as any).selectedOption).toBe('else1')
|
||||
expect((result as any).selectedPath.blockId).toBe('target-false')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,6 +3,12 @@ import type { BlockOutput } from '@/blocks/types'
|
||||
import { BlockType, CONDITION, DEFAULTS, EDGE } from '@/executor/constants'
|
||||
import type { BlockHandler, ExecutionContext } from '@/executor/types'
|
||||
import { collectBlockData } from '@/executor/utils/block-data'
|
||||
import {
|
||||
buildBranchNodeId,
|
||||
extractBaseBlockId,
|
||||
extractBranchIndex,
|
||||
isBranchNodeId,
|
||||
} from '@/executor/utils/subflow-utils'
|
||||
import type { SerializedBlock } from '@/serializer/types'
|
||||
import { executeTool } from '@/tools'
|
||||
|
||||
@@ -18,7 +24,8 @@ const CONDITION_TIMEOUT_MS = 5000
|
||||
export async function evaluateConditionExpression(
|
||||
ctx: ExecutionContext,
|
||||
conditionExpression: string,
|
||||
providedEvalContext?: Record<string, any>
|
||||
providedEvalContext?: Record<string, any>,
|
||||
currentNodeId?: string
|
||||
): Promise<boolean> {
|
||||
const evalContext = providedEvalContext || {}
|
||||
|
||||
@@ -26,7 +33,7 @@ export async function evaluateConditionExpression(
|
||||
const contextSetup = `const context = ${JSON.stringify(evalContext)};`
|
||||
const code = `${contextSetup}\nreturn Boolean(${conditionExpression})`
|
||||
|
||||
const { blockData, blockNameMapping, blockOutputSchemas } = collectBlockData(ctx)
|
||||
const { blockData, blockNameMapping, blockOutputSchemas } = collectBlockData(ctx, currentNodeId)
|
||||
|
||||
const result = await executeTool(
|
||||
'function_execute',
|
||||
@@ -83,7 +90,19 @@ export class ConditionBlockHandler implements BlockHandler {
|
||||
): Promise<BlockOutput> {
|
||||
const conditions = this.parseConditions(inputs.conditions)
|
||||
|
||||
const sourceBlockId = ctx.workflow?.connections.find((conn) => conn.target === block.id)?.source
|
||||
const baseBlockId = extractBaseBlockId(block.id)
|
||||
const branchIndex = isBranchNodeId(block.id) ? extractBranchIndex(block.id) : null
|
||||
|
||||
const sourceConnection = ctx.workflow?.connections.find((conn) => conn.target === baseBlockId)
|
||||
let sourceBlockId = sourceConnection?.source
|
||||
|
||||
if (sourceBlockId && branchIndex !== null) {
|
||||
const virtualSourceId = buildBranchNodeId(sourceBlockId, branchIndex)
|
||||
if (ctx.blockStates.has(virtualSourceId)) {
|
||||
sourceBlockId = virtualSourceId
|
||||
}
|
||||
}
|
||||
|
||||
const evalContext = this.buildEvaluationContext(ctx, sourceBlockId)
|
||||
const rawSourceOutput = sourceBlockId ? ctx.blockStates.get(sourceBlockId)?.output : null
|
||||
|
||||
@@ -91,13 +110,16 @@ export class ConditionBlockHandler implements BlockHandler {
|
||||
// thinking this block is pausing (it was already resumed by the HITL block)
|
||||
const sourceOutput = this.filterPauseMetadata(rawSourceOutput)
|
||||
|
||||
const outgoingConnections = ctx.workflow?.connections.filter((conn) => conn.source === block.id)
|
||||
const outgoingConnections = ctx.workflow?.connections.filter(
|
||||
(conn) => conn.source === baseBlockId
|
||||
)
|
||||
|
||||
const { selectedConnection, selectedCondition } = await this.evaluateConditions(
|
||||
conditions,
|
||||
outgoingConnections || [],
|
||||
evalContext,
|
||||
ctx
|
||||
ctx,
|
||||
block.id
|
||||
)
|
||||
|
||||
if (!selectedConnection || !selectedCondition) {
|
||||
@@ -170,7 +192,8 @@ export class ConditionBlockHandler implements BlockHandler {
|
||||
conditions: Array<{ id: string; title: string; value: string }>,
|
||||
outgoingConnections: Array<{ source: string; target: string; sourceHandle?: string }>,
|
||||
evalContext: Record<string, any>,
|
||||
ctx: ExecutionContext
|
||||
ctx: ExecutionContext,
|
||||
currentNodeId?: string
|
||||
): Promise<{
|
||||
selectedConnection: { target: string; sourceHandle?: string } | null
|
||||
selectedCondition: { id: string; title: string; value: string } | null
|
||||
@@ -189,7 +212,8 @@ export class ConditionBlockHandler implements BlockHandler {
|
||||
const conditionMet = await evaluateConditionExpression(
|
||||
ctx,
|
||||
conditionValueString,
|
||||
evalContext
|
||||
evalContext,
|
||||
currentNodeId
|
||||
)
|
||||
|
||||
if (conditionMet) {
|
||||
|
||||
@@ -2,6 +2,11 @@ import { normalizeInputFormatValue } from '@/lib/workflows/input-format'
|
||||
import { isTriggerBehavior, normalizeName } from '@/executor/constants'
|
||||
import type { ExecutionContext } from '@/executor/types'
|
||||
import type { OutputSchema } from '@/executor/utils/block-reference'
|
||||
import {
|
||||
extractBaseBlockId,
|
||||
extractBranchIndex,
|
||||
isBranchNodeId,
|
||||
} from '@/executor/utils/subflow-utils'
|
||||
import type { SerializedBlock } from '@/serializer/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import { getTool } from '@/tools/utils'
|
||||
@@ -86,14 +91,30 @@ export function getBlockSchema(
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function collectBlockData(ctx: ExecutionContext): BlockDataCollection {
|
||||
export function collectBlockData(
|
||||
ctx: ExecutionContext,
|
||||
currentNodeId?: string
|
||||
): BlockDataCollection {
|
||||
const blockData: Record<string, unknown> = {}
|
||||
const blockNameMapping: Record<string, string> = {}
|
||||
const blockOutputSchemas: Record<string, OutputSchema> = {}
|
||||
|
||||
const branchIndex =
|
||||
currentNodeId && isBranchNodeId(currentNodeId) ? extractBranchIndex(currentNodeId) : null
|
||||
|
||||
for (const [id, state] of ctx.blockStates.entries()) {
|
||||
if (state.output !== undefined) {
|
||||
blockData[id] = state.output
|
||||
|
||||
if (branchIndex !== null && isBranchNodeId(id)) {
|
||||
const stateBranchIndex = extractBranchIndex(id)
|
||||
if (stateBranchIndex === branchIndex) {
|
||||
const baseId = extractBaseBlockId(id)
|
||||
if (blockData[baseId] === undefined) {
|
||||
blockData[baseId] = state.output
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
309
apps/sim/hooks/queries/invitations.ts
Normal file
309
apps/sim/hooks/queries/invitations.ts
Normal file
@@ -0,0 +1,309 @@
|
||||
import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { workspaceKeys } from './workspace'
|
||||
|
||||
/**
|
||||
* Query key factory for invitation-related queries.
|
||||
* Provides hierarchical cache keys for workspace invitations.
|
||||
*/
|
||||
export const invitationKeys = {
|
||||
all: ['invitations'] as const,
|
||||
lists: () => [...invitationKeys.all, 'list'] as const,
|
||||
list: (workspaceId: string) => [...invitationKeys.lists(), workspaceId] as const,
|
||||
}
|
||||
|
||||
/** Raw invitation data from the API. */
|
||||
export interface PendingInvitation {
|
||||
id: string
|
||||
workspaceId: string
|
||||
email: string
|
||||
permissions: 'admin' | 'write' | 'read'
|
||||
status: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
/** Normalized invitation for display in the UI. */
|
||||
export interface WorkspaceInvitation {
|
||||
email: string
|
||||
permissionType: 'admin' | 'write' | 'read'
|
||||
isPendingInvitation: boolean
|
||||
invitationId?: string
|
||||
}
|
||||
|
||||
async function fetchPendingInvitations(workspaceId: string): Promise<WorkspaceInvitation[]> {
|
||||
const response = await fetch('/api/workspaces/invitations')
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch pending invitations')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return (
|
||||
data.invitations
|
||||
?.filter(
|
||||
(inv: PendingInvitation) => inv.status === 'pending' && inv.workspaceId === workspaceId
|
||||
)
|
||||
.map((inv: PendingInvitation) => ({
|
||||
email: inv.email,
|
||||
permissionType: inv.permissions,
|
||||
isPendingInvitation: true,
|
||||
invitationId: inv.id,
|
||||
})) || []
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches pending invitations for a workspace.
|
||||
* @param workspaceId - The workspace ID to fetch invitations for
|
||||
*/
|
||||
export function usePendingInvitations(workspaceId: string | undefined) {
|
||||
return useQuery({
|
||||
queryKey: invitationKeys.list(workspaceId ?? ''),
|
||||
queryFn: () => fetchPendingInvitations(workspaceId as string),
|
||||
enabled: Boolean(workspaceId),
|
||||
staleTime: 30 * 1000,
|
||||
placeholderData: keepPreviousData,
|
||||
})
|
||||
}
|
||||
|
||||
interface BatchSendInvitationsParams {
|
||||
workspaceId: string
|
||||
invitations: Array<{ email: string; permission: 'admin' | 'write' | 'read' }>
|
||||
}
|
||||
|
||||
interface BatchInvitationResult {
|
||||
successful: string[]
|
||||
failed: Array<{ email: string; error: string }>
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends multiple workspace invitations in parallel.
|
||||
* Returns results for each invitation indicating success or failure.
|
||||
*/
|
||||
export function useBatchSendWorkspaceInvitations() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
workspaceId,
|
||||
invitations,
|
||||
}: BatchSendInvitationsParams): Promise<BatchInvitationResult> => {
|
||||
const results = await Promise.allSettled(
|
||||
invitations.map(async ({ email, permission }) => {
|
||||
const response = await fetch('/api/workspaces/invitations', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
workspaceId,
|
||||
email,
|
||||
role: 'member',
|
||||
permission,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.error || 'Failed to send invitation')
|
||||
}
|
||||
|
||||
return { email, data: await response.json() }
|
||||
})
|
||||
)
|
||||
|
||||
const successful: string[] = []
|
||||
const failed: Array<{ email: string; error: string }> = []
|
||||
|
||||
results.forEach((result, index) => {
|
||||
const email = invitations[index].email
|
||||
if (result.status === 'fulfilled') {
|
||||
successful.push(email)
|
||||
} else {
|
||||
failed.push({ email, error: result.reason?.message || 'Unknown error' })
|
||||
}
|
||||
})
|
||||
|
||||
return { successful, failed }
|
||||
},
|
||||
onSuccess: (_data, variables) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: invitationKeys.list(variables.workspaceId),
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
interface CancelInvitationParams {
|
||||
invitationId: string
|
||||
workspaceId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels a pending workspace invitation.
|
||||
* Invalidates the invitation list cache on success.
|
||||
*/
|
||||
export function useCancelWorkspaceInvitation() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ invitationId }: CancelInvitationParams) => {
|
||||
const response = await fetch(`/api/workspaces/invitations/${invitationId}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.error || 'Failed to cancel invitation')
|
||||
}
|
||||
|
||||
return response.json()
|
||||
},
|
||||
onSuccess: (_data, variables) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: invitationKeys.list(variables.workspaceId),
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
interface ResendInvitationParams {
|
||||
invitationId: string
|
||||
workspaceId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Resends a pending workspace invitation email.
|
||||
* Invalidates the invitation list cache on success.
|
||||
*/
|
||||
export function useResendWorkspaceInvitation() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ invitationId }: ResendInvitationParams) => {
|
||||
const response = await fetch(`/api/workspaces/invitations/${invitationId}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.error || 'Failed to resend invitation')
|
||||
}
|
||||
|
||||
return response.json()
|
||||
},
|
||||
onSuccess: (_data, variables) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: invitationKeys.list(variables.workspaceId),
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
interface RemoveMemberParams {
|
||||
userId: string
|
||||
workspaceId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a member from a workspace.
|
||||
* Invalidates the workspace permissions cache on success.
|
||||
*/
|
||||
export function useRemoveWorkspaceMember() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ userId, workspaceId }: RemoveMemberParams) => {
|
||||
const response = await fetch(`/api/workspaces/members/${userId}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ workspaceId }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.error || 'Failed to remove member')
|
||||
}
|
||||
|
||||
return response.json()
|
||||
},
|
||||
onSuccess: (_data, variables) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: workspaceKeys.permissions(variables.workspaceId),
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
interface LeaveWorkspaceParams {
|
||||
userId: string
|
||||
workspaceId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows the current user to leave a workspace.
|
||||
* Invalidates both permissions and workspace list caches on success.
|
||||
*/
|
||||
export function useLeaveWorkspace() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ userId, workspaceId }: LeaveWorkspaceParams) => {
|
||||
const response = await fetch(`/api/workspaces/members/${userId}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ workspaceId }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.error || 'Failed to leave workspace')
|
||||
}
|
||||
|
||||
return response.json()
|
||||
},
|
||||
onSuccess: (_data, variables) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: workspaceKeys.permissions(variables.workspaceId),
|
||||
})
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: workspaceKeys.all,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
interface UpdatePermissionsParams {
|
||||
workspaceId: string
|
||||
updates: Array<{ userId: string; permissions: 'admin' | 'write' | 'read' }>
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates permissions for one or more workspace members.
|
||||
* Invalidates the workspace permissions cache on success.
|
||||
*/
|
||||
export function useUpdateWorkspacePermissions() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ workspaceId, updates }: UpdatePermissionsParams) => {
|
||||
const response = await fetch(`/api/workspaces/${workspaceId}/permissions`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ updates }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.error || 'Failed to update permissions')
|
||||
}
|
||||
|
||||
return response.json()
|
||||
},
|
||||
onSuccess: (_data, variables) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: workspaceKeys.permissions(variables.workspaceId),
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -6,27 +6,32 @@ import { OAUTH_PROVIDERS, type OAuthServiceConfig } from '@/lib/oauth'
|
||||
const logger = createLogger('OAuthConnectionsQuery')
|
||||
|
||||
/**
|
||||
* Query key factories for OAuth connections
|
||||
* Query key factory for OAuth connection queries.
|
||||
* Provides hierarchical cache keys for connections and provider-specific accounts.
|
||||
*/
|
||||
export const oauthConnectionsKeys = {
|
||||
all: ['oauthConnections'] as const,
|
||||
connections: () => [...oauthConnectionsKeys.all, 'connections'] as const,
|
||||
accounts: (provider: string) => [...oauthConnectionsKeys.all, 'accounts', provider] as const,
|
||||
}
|
||||
|
||||
/**
|
||||
* Service info type - extends OAuthServiceConfig with connection status and the service key
|
||||
*/
|
||||
/** OAuth service with connection status and linked accounts. */
|
||||
export interface ServiceInfo extends OAuthServiceConfig {
|
||||
/** The service key from OAUTH_PROVIDERS (e.g., 'gmail', 'google-drive') */
|
||||
id: string
|
||||
isConnected: boolean
|
||||
lastConnected?: string
|
||||
accounts?: { id: string; name: string }[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Define available services from standardized OAuth providers
|
||||
*/
|
||||
/** OAuth connection data returned from the API. */
|
||||
interface OAuthConnectionResponse {
|
||||
provider: string
|
||||
baseProvider?: string
|
||||
accounts?: { id: string; name: string }[]
|
||||
lastConnected?: string
|
||||
scopes?: string[]
|
||||
}
|
||||
|
||||
function defineServices(): ServiceInfo[] {
|
||||
const servicesList: ServiceInfo[] = []
|
||||
|
||||
@@ -44,9 +49,6 @@ function defineServices(): ServiceInfo[] {
|
||||
return servicesList
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch OAuth connections and merge with service definitions
|
||||
*/
|
||||
async function fetchOAuthConnections(): Promise<ServiceInfo[]> {
|
||||
try {
|
||||
const serviceDefinitions = defineServices()
|
||||
@@ -65,7 +67,9 @@ async function fetchOAuthConnections(): Promise<ServiceInfo[]> {
|
||||
const connections = data.connections || []
|
||||
|
||||
const updatedServices = serviceDefinitions.map((service) => {
|
||||
const connection = connections.find((conn: any) => conn.provider === service.providerId)
|
||||
const connection = connections.find(
|
||||
(conn: OAuthConnectionResponse) => conn.provider === service.providerId
|
||||
)
|
||||
|
||||
if (connection) {
|
||||
return {
|
||||
@@ -76,13 +80,14 @@ async function fetchOAuthConnections(): Promise<ServiceInfo[]> {
|
||||
}
|
||||
}
|
||||
|
||||
const connectionWithScopes = connections.find((conn: any) => {
|
||||
const connectionWithScopes = connections.find((conn: OAuthConnectionResponse) => {
|
||||
if (!conn.baseProvider || !service.providerId.startsWith(conn.baseProvider)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (conn.scopes && service.scopes) {
|
||||
return service.scopes.every((scope) => conn.scopes.includes(scope))
|
||||
const connScopes = conn.scopes
|
||||
return service.scopes.every((scope) => connScopes.includes(scope))
|
||||
}
|
||||
|
||||
return false
|
||||
@@ -108,26 +113,28 @@ async function fetchOAuthConnections(): Promise<ServiceInfo[]> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch OAuth connections
|
||||
* Fetches all OAuth service connections with their status.
|
||||
* Returns service definitions merged with connection data.
|
||||
*/
|
||||
export function useOAuthConnections() {
|
||||
return useQuery({
|
||||
queryKey: oauthConnectionsKeys.connections(),
|
||||
queryFn: fetchOAuthConnections,
|
||||
staleTime: 30 * 1000, // 30 seconds - connections don't change often
|
||||
retry: false, // Don't retry on 404
|
||||
placeholderData: keepPreviousData, // Show cached data immediately
|
||||
staleTime: 30 * 1000,
|
||||
retry: false,
|
||||
placeholderData: keepPreviousData,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect OAuth service mutation
|
||||
*/
|
||||
interface ConnectServiceParams {
|
||||
providerId: string
|
||||
callbackURL: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates OAuth connection flow for a service.
|
||||
* Redirects the user to the provider's authorization page.
|
||||
*/
|
||||
export function useConnectOAuthService() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
@@ -138,7 +145,6 @@ export function useConnectOAuthService() {
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
// Shopify requires a custom OAuth flow with shop domain input
|
||||
if (providerId === 'shopify') {
|
||||
const returnUrl = encodeURIComponent(callbackURL)
|
||||
window.location.href = `/api/auth/shopify/authorize?returnUrl=${returnUrl}`
|
||||
@@ -161,9 +167,6 @@ export function useConnectOAuthService() {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect OAuth service mutation
|
||||
*/
|
||||
interface DisconnectServiceParams {
|
||||
provider: string
|
||||
providerId: string
|
||||
@@ -171,6 +174,10 @@ interface DisconnectServiceParams {
|
||||
accountId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects an OAuth service account.
|
||||
* Performs optimistic update and rolls back on failure.
|
||||
*/
|
||||
export function useDisconnectOAuthService() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
@@ -230,3 +237,38 @@ export function useDisconnectOAuthService() {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** Connected OAuth account for a specific provider. */
|
||||
export interface ConnectedAccount {
|
||||
id: string
|
||||
accountId: string
|
||||
providerId: string
|
||||
displayName?: string
|
||||
}
|
||||
|
||||
async function fetchConnectedAccounts(provider: string): Promise<ConnectedAccount[]> {
|
||||
const response = await fetch(`/api/auth/accounts?provider=${provider}`)
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}))
|
||||
throw new Error(data.error || `Failed to load ${provider} accounts`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return data.accounts || []
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches connected accounts for a specific OAuth provider.
|
||||
* @param provider - The provider ID (e.g., 'slack', 'google')
|
||||
* @param options - Query options including enabled flag
|
||||
*/
|
||||
export function useConnectedAccounts(provider: string, options?: { enabled?: boolean }) {
|
||||
return useQuery({
|
||||
queryKey: oauthConnectionsKeys.accounts(provider),
|
||||
queryFn: () => fetchConnectedAccounts(provider),
|
||||
enabled: options?.enabled ?? true,
|
||||
staleTime: 60 * 1000,
|
||||
placeholderData: keepPreviousData,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
|
||||
/**
|
||||
* Query key factories for workspace-related queries
|
||||
* Query key factory for workspace-related queries.
|
||||
* Provides hierarchical cache keys for workspaces, settings, and permissions.
|
||||
*/
|
||||
export const workspaceKeys = {
|
||||
all: ['workspace'] as const,
|
||||
lists: () => [...workspaceKeys.all, 'list'] as const,
|
||||
list: () => [...workspaceKeys.lists(), 'user'] as const,
|
||||
details: () => [...workspaceKeys.all, 'detail'] as const,
|
||||
detail: (id: string) => [...workspaceKeys.details(), id] as const,
|
||||
settings: (id: string) => [...workspaceKeys.detail(id), 'settings'] as const,
|
||||
@@ -13,9 +16,186 @@ export const workspaceKeys = {
|
||||
adminList: (userId: string | undefined) => [...workspaceKeys.adminLists(), userId ?? ''] as const,
|
||||
}
|
||||
|
||||
/** Represents a workspace in the user's workspace list. */
|
||||
export interface Workspace {
|
||||
id: string
|
||||
name: string
|
||||
ownerId: string
|
||||
role?: string
|
||||
membershipId?: string
|
||||
permissions?: 'admin' | 'write' | 'read' | null
|
||||
}
|
||||
|
||||
async function fetchWorkspaces(): Promise<Workspace[]> {
|
||||
const response = await fetch('/api/workspaces')
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch workspaces')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return data.workspaces || []
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch workspace settings
|
||||
* Fetches the current user's workspaces.
|
||||
* @param enabled - Whether the query should execute (defaults to true)
|
||||
*/
|
||||
export function useWorkspacesQuery(enabled = true) {
|
||||
return useQuery({
|
||||
queryKey: workspaceKeys.list(),
|
||||
queryFn: fetchWorkspaces,
|
||||
enabled,
|
||||
staleTime: 30 * 1000,
|
||||
placeholderData: keepPreviousData,
|
||||
})
|
||||
}
|
||||
|
||||
interface CreateWorkspaceParams {
|
||||
name: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new workspace.
|
||||
* Automatically invalidates the workspace list cache on success.
|
||||
*/
|
||||
export function useCreateWorkspace() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ name }: CreateWorkspaceParams) => {
|
||||
const response = await fetch('/api/workspaces', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.error || 'Failed to create workspace')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return data.workspace as Workspace
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: workspaceKeys.lists() })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
interface DeleteWorkspaceParams {
|
||||
workspaceId: string
|
||||
deleteTemplates?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a workspace.
|
||||
* Automatically invalidates the workspace list cache on success.
|
||||
*/
|
||||
export function useDeleteWorkspace() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ workspaceId, deleteTemplates = false }: DeleteWorkspaceParams) => {
|
||||
const response = await fetch(`/api/workspaces/${workspaceId}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ deleteTemplates }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.error || 'Failed to delete workspace')
|
||||
}
|
||||
|
||||
return response.json()
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: workspaceKeys.lists() })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
interface UpdateWorkspaceNameParams {
|
||||
workspaceId: string
|
||||
name: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a workspace's name.
|
||||
* Invalidates both the workspace list and the specific workspace detail cache.
|
||||
*/
|
||||
export function useUpdateWorkspaceName() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ workspaceId, name }: UpdateWorkspaceNameParams) => {
|
||||
const response = await fetch(`/api/workspaces/${workspaceId}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: name.trim() }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.error || 'Failed to update workspace name')
|
||||
}
|
||||
|
||||
return response.json()
|
||||
},
|
||||
onSuccess: (_data, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: workspaceKeys.lists() })
|
||||
queryClient.invalidateQueries({ queryKey: workspaceKeys.detail(variables.workspaceId) })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** Represents a user with permissions in a workspace. */
|
||||
export interface WorkspaceUser {
|
||||
userId: string
|
||||
email: string
|
||||
name: string | null
|
||||
image: string | null
|
||||
permissionType: 'admin' | 'write' | 'read'
|
||||
}
|
||||
|
||||
/** Workspace permissions data containing all users and their access levels. */
|
||||
export interface WorkspacePermissions {
|
||||
users: WorkspaceUser[]
|
||||
total: number
|
||||
}
|
||||
|
||||
async function fetchWorkspacePermissions(workspaceId: string): Promise<WorkspacePermissions> {
|
||||
const response = await fetch(`/api/workspaces/${workspaceId}/permissions`)
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
throw new Error('Workspace not found or access denied')
|
||||
}
|
||||
if (response.status === 401) {
|
||||
throw new Error('Authentication required')
|
||||
}
|
||||
throw new Error(`Failed to fetch permissions: ${response.statusText}`)
|
||||
}
|
||||
|
||||
return response.json()
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches permissions for a specific workspace.
|
||||
* @param workspaceId - The workspace ID to fetch permissions for
|
||||
*/
|
||||
export function useWorkspacePermissionsQuery(workspaceId: string | null | undefined) {
|
||||
return useQuery({
|
||||
queryKey: workspaceKeys.permissions(workspaceId ?? ''),
|
||||
queryFn: () => fetchWorkspacePermissions(workspaceId as string),
|
||||
enabled: Boolean(workspaceId),
|
||||
staleTime: 30 * 1000,
|
||||
placeholderData: keepPreviousData,
|
||||
})
|
||||
}
|
||||
|
||||
async function fetchWorkspaceSettings(workspaceId: string) {
|
||||
const [settingsResponse, permissionsResponse] = await Promise.all([
|
||||
fetch(`/api/workspaces/${workspaceId}`),
|
||||
@@ -38,7 +218,8 @@ async function fetchWorkspaceSettings(workspaceId: string) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch workspace settings
|
||||
* Fetches workspace settings including permissions.
|
||||
* @param workspaceId - The workspace ID to fetch settings for
|
||||
*/
|
||||
export function useWorkspaceSettings(workspaceId: string) {
|
||||
return useQuery({
|
||||
@@ -50,15 +231,16 @@ export function useWorkspaceSettings(workspaceId: string) {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Update workspace settings mutation
|
||||
*/
|
||||
interface UpdateWorkspaceSettingsParams {
|
||||
workspaceId: string
|
||||
billedAccountUserId?: string
|
||||
billingAccountUserEmail?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates workspace settings (e.g., billing configuration).
|
||||
* Invalidates the workspace settings cache on success.
|
||||
*/
|
||||
export function useUpdateWorkspaceSettings() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
@@ -85,9 +267,7 @@ export function useUpdateWorkspaceSettings() {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Workspace type returned by admin workspaces query
|
||||
*/
|
||||
/** Workspace with admin access metadata. */
|
||||
export interface AdminWorkspace {
|
||||
id: string
|
||||
name: string
|
||||
@@ -96,9 +276,6 @@ export interface AdminWorkspace {
|
||||
canInvite: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch workspaces where user has admin access
|
||||
*/
|
||||
async function fetchAdminWorkspaces(userId: string | undefined): Promise<AdminWorkspace[]> {
|
||||
if (!userId) {
|
||||
return []
|
||||
@@ -121,7 +298,7 @@ async function fetchAdminWorkspaces(userId: string | undefined): Promise<AdminWo
|
||||
}
|
||||
const permissionData = await permissionResponse.json()
|
||||
return { workspace, permissionData }
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -161,14 +338,15 @@ async function fetchAdminWorkspaces(userId: string | undefined): Promise<AdminWo
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch workspaces where user has admin access
|
||||
* Fetches workspaces where the user has admin access.
|
||||
* @param userId - The user ID to check admin access for
|
||||
*/
|
||||
export function useAdminWorkspaces(userId: string | undefined) {
|
||||
return useQuery({
|
||||
queryKey: workspaceKeys.adminList(userId),
|
||||
queryFn: () => fetchAdminWorkspaces(userId),
|
||||
enabled: Boolean(userId),
|
||||
staleTime: 60 * 1000, // Cache for 60 seconds
|
||||
staleTime: 60 * 1000,
|
||||
placeholderData: keepPreviousData,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
interface SlackAccount {
|
||||
id: string
|
||||
accountId: string
|
||||
providerId: string
|
||||
displayName?: string
|
||||
}
|
||||
|
||||
interface UseSlackAccountsResult {
|
||||
accounts: SlackAccount[]
|
||||
isLoading: boolean
|
||||
error: string | null
|
||||
refetch: () => Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches and manages connected Slack accounts for the current user.
|
||||
* @returns Object containing accounts array, loading state, error state, and refetch function
|
||||
*/
|
||||
export function useSlackAccounts(): UseSlackAccountsResult {
|
||||
const [accounts, setAccounts] = useState<SlackAccount[]>([])
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const fetchAccounts = useCallback(async () => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
const response = await fetch('/api/auth/accounts?provider=slack')
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setAccounts(data.accounts || [])
|
||||
} else {
|
||||
const data = await response.json().catch(() => ({}))
|
||||
setError(data.error || 'Failed to load Slack accounts')
|
||||
setAccounts([])
|
||||
}
|
||||
} catch {
|
||||
setError('Failed to load Slack accounts')
|
||||
setAccounts([])
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
fetchAccounts()
|
||||
}, [])
|
||||
|
||||
return { accounts, isLoading, error, refetch: fetchAccounts }
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useMemo } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { useSession } from '@/lib/auth/auth-client'
|
||||
import type { PermissionType, WorkspacePermissions } from '@/hooks/use-workspace-permissions'
|
||||
import type { WorkspacePermissions } from '@/hooks/queries/workspace'
|
||||
|
||||
export type PermissionType = 'admin' | 'write' | 'read'
|
||||
|
||||
const logger = createLogger('useUserPermissions')
|
||||
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import type { permissionTypeEnum } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { API_ENDPOINTS } from '@/stores/constants'
|
||||
|
||||
const logger = createLogger('useWorkspacePermissions')
|
||||
|
||||
export type PermissionType = (typeof permissionTypeEnum.enumValues)[number]
|
||||
|
||||
export interface WorkspaceUser {
|
||||
userId: string
|
||||
email: string
|
||||
name: string | null
|
||||
image: string | null
|
||||
permissionType: PermissionType
|
||||
}
|
||||
|
||||
export interface WorkspacePermissions {
|
||||
users: WorkspaceUser[]
|
||||
total: number
|
||||
}
|
||||
|
||||
interface UseWorkspacePermissionsReturn {
|
||||
permissions: WorkspacePermissions | null
|
||||
loading: boolean
|
||||
error: string | null
|
||||
updatePermissions: (newPermissions: WorkspacePermissions) => void
|
||||
refetch: () => Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook to fetch and manage workspace permissions
|
||||
*
|
||||
* @param workspaceId - The workspace ID to fetch permissions for
|
||||
* @returns Object containing permissions data, loading state, error state, and refetch function
|
||||
*/
|
||||
export function useWorkspacePermissions(workspaceId: string | null): UseWorkspacePermissionsReturn {
|
||||
const [permissions, setPermissions] = useState<WorkspacePermissions | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const fetchPermissions = async (id: string): Promise<void> => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
const response = await fetch(API_ENDPOINTS.WORKSPACE_PERMISSIONS(id))
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
throw new Error('Workspace not found or access denied')
|
||||
}
|
||||
if (response.status === 401) {
|
||||
throw new Error('Authentication required')
|
||||
}
|
||||
throw new Error(`Failed to fetch permissions: ${response.statusText}`)
|
||||
}
|
||||
|
||||
const data: WorkspacePermissions = await response.json()
|
||||
setPermissions(data)
|
||||
|
||||
logger.info('Workspace permissions loaded', {
|
||||
workspaceId: id,
|
||||
userCount: data.total,
|
||||
users: data.users.map((u) => ({ email: u.email, permissions: u.permissionType })),
|
||||
})
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred'
|
||||
setError(errorMessage)
|
||||
logger.error('Failed to fetch workspace permissions', {
|
||||
workspaceId: id,
|
||||
error: errorMessage,
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const updatePermissions = useCallback((newPermissions: WorkspacePermissions): void => {
|
||||
setPermissions(newPermissions)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (workspaceId) {
|
||||
fetchPermissions(workspaceId)
|
||||
} else {
|
||||
// Clear state if no workspace ID
|
||||
setPermissions(null)
|
||||
setError(null)
|
||||
setLoading(false)
|
||||
}
|
||||
}, [workspaceId])
|
||||
|
||||
const refetch = useCallback(async () => {
|
||||
if (workspaceId) {
|
||||
await fetchPermissions(workspaceId)
|
||||
}
|
||||
}, [workspaceId])
|
||||
|
||||
return {
|
||||
permissions,
|
||||
loading,
|
||||
error,
|
||||
updatePermissions,
|
||||
refetch,
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
ensureOrganizationForTeamSubscription,
|
||||
syncSubscriptionUsageLimits,
|
||||
} from '@/lib/billing/organization'
|
||||
import { getPlans } from '@/lib/billing/plans'
|
||||
import { getPlans, resolvePlanFromStripeSubscription } from '@/lib/billing/plans'
|
||||
import { syncSeatsFromStripeQuantity } from '@/lib/billing/validation/seat-management'
|
||||
import { handleChargeDispute, handleDisputeClosed } from '@/lib/billing/webhooks/disputes'
|
||||
import { handleManualEnterpriseSubscription } from '@/lib/billing/webhooks/enterprise'
|
||||
@@ -2641,29 +2641,42 @@ export const auth = betterAuth({
|
||||
}
|
||||
},
|
||||
onSubscriptionComplete: async ({
|
||||
stripeSubscription,
|
||||
subscription,
|
||||
}: {
|
||||
event: Stripe.Event
|
||||
stripeSubscription: Stripe.Subscription
|
||||
subscription: any
|
||||
}) => {
|
||||
const { priceId, planFromStripe, isTeamPlan } =
|
||||
resolvePlanFromStripeSubscription(stripeSubscription)
|
||||
|
||||
logger.info('[onSubscriptionComplete] Subscription created', {
|
||||
subscriptionId: subscription.id,
|
||||
referenceId: subscription.referenceId,
|
||||
plan: subscription.plan,
|
||||
dbPlan: subscription.plan,
|
||||
planFromStripe,
|
||||
priceId,
|
||||
status: subscription.status,
|
||||
})
|
||||
|
||||
const subscriptionForOrgCreation = isTeamPlan
|
||||
? { ...subscription, plan: 'team' }
|
||||
: subscription
|
||||
|
||||
let resolvedSubscription = subscription
|
||||
try {
|
||||
resolvedSubscription = await ensureOrganizationForTeamSubscription(subscription)
|
||||
resolvedSubscription = await ensureOrganizationForTeamSubscription(
|
||||
subscriptionForOrgCreation
|
||||
)
|
||||
} catch (orgError) {
|
||||
logger.error(
|
||||
'[onSubscriptionComplete] Failed to ensure organization for team subscription',
|
||||
{
|
||||
subscriptionId: subscription.id,
|
||||
referenceId: subscription.referenceId,
|
||||
plan: subscription.plan,
|
||||
dbPlan: subscription.plan,
|
||||
planFromStripe,
|
||||
error: orgError instanceof Error ? orgError.message : String(orgError),
|
||||
stack: orgError instanceof Error ? orgError.stack : undefined,
|
||||
}
|
||||
@@ -2684,22 +2697,67 @@ export const auth = betterAuth({
|
||||
event: Stripe.Event
|
||||
subscription: any
|
||||
}) => {
|
||||
const stripeSubscription = event.data.object as Stripe.Subscription
|
||||
const { priceId, planFromStripe, isTeamPlan } =
|
||||
resolvePlanFromStripeSubscription(stripeSubscription)
|
||||
|
||||
if (priceId && !planFromStripe) {
|
||||
logger.warn(
|
||||
'[onSubscriptionUpdate] Could not determine plan from Stripe price ID',
|
||||
{
|
||||
subscriptionId: subscription.id,
|
||||
priceId,
|
||||
dbPlan: subscription.plan,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const isUpgradeToTeam =
|
||||
isTeamPlan &&
|
||||
subscription.plan !== 'team' &&
|
||||
!subscription.referenceId.startsWith('org_')
|
||||
|
||||
const effectivePlanForTeamFeatures = planFromStripe ?? subscription.plan
|
||||
|
||||
logger.info('[onSubscriptionUpdate] Subscription updated', {
|
||||
subscriptionId: subscription.id,
|
||||
status: subscription.status,
|
||||
plan: subscription.plan,
|
||||
dbPlan: subscription.plan,
|
||||
planFromStripe,
|
||||
isUpgradeToTeam,
|
||||
referenceId: subscription.referenceId,
|
||||
})
|
||||
|
||||
const subscriptionForOrgCreation = isUpgradeToTeam
|
||||
? { ...subscription, plan: 'team' }
|
||||
: subscription
|
||||
|
||||
let resolvedSubscription = subscription
|
||||
try {
|
||||
resolvedSubscription = await ensureOrganizationForTeamSubscription(subscription)
|
||||
resolvedSubscription = await ensureOrganizationForTeamSubscription(
|
||||
subscriptionForOrgCreation
|
||||
)
|
||||
|
||||
if (isUpgradeToTeam) {
|
||||
logger.info(
|
||||
'[onSubscriptionUpdate] Detected Pro -> Team upgrade, ensured organization creation',
|
||||
{
|
||||
subscriptionId: subscription.id,
|
||||
originalPlan: subscription.plan,
|
||||
newPlan: planFromStripe,
|
||||
resolvedReferenceId: resolvedSubscription.referenceId,
|
||||
}
|
||||
)
|
||||
}
|
||||
} catch (orgError) {
|
||||
logger.error(
|
||||
'[onSubscriptionUpdate] Failed to ensure organization for team subscription',
|
||||
{
|
||||
subscriptionId: subscription.id,
|
||||
referenceId: subscription.referenceId,
|
||||
plan: subscription.plan,
|
||||
dbPlan: subscription.plan,
|
||||
planFromStripe,
|
||||
isUpgradeToTeam,
|
||||
error: orgError instanceof Error ? orgError.message : String(orgError),
|
||||
stack: orgError instanceof Error ? orgError.stack : undefined,
|
||||
}
|
||||
@@ -2717,9 +2775,8 @@ export const auth = betterAuth({
|
||||
})
|
||||
}
|
||||
|
||||
if (resolvedSubscription.plan === 'team') {
|
||||
if (effectivePlanForTeamFeatures === 'team') {
|
||||
try {
|
||||
const stripeSubscription = event.data.object as Stripe.Subscription
|
||||
const quantity = stripeSubscription.items?.data?.[0]?.quantity || 1
|
||||
|
||||
const result = await syncSeatsFromStripeQuantity(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type Stripe from 'stripe'
|
||||
import {
|
||||
getFreeTierLimit,
|
||||
getProTierLimit,
|
||||
@@ -56,6 +57,13 @@ export function getPlanByName(planName: string): BillingPlan | undefined {
|
||||
return getPlans().find((plan) => plan.name === planName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific plan by Stripe price ID
|
||||
*/
|
||||
export function getPlanByPriceId(priceId: string): BillingPlan | undefined {
|
||||
return getPlans().find((plan) => plan.priceId === priceId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get plan limits for a given plan name
|
||||
*/
|
||||
@@ -63,3 +71,26 @@ export function getPlanLimits(planName: string): number {
|
||||
const plan = getPlanByName(planName)
|
||||
return plan?.limits.cost ?? getFreeTierLimit()
|
||||
}
|
||||
|
||||
export interface StripePlanResolution {
|
||||
priceId: string | undefined
|
||||
planFromStripe: string | null
|
||||
isTeamPlan: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve plan information from a Stripe subscription object.
|
||||
* Used to get the authoritative plan from Stripe rather than relying on DB state.
|
||||
*/
|
||||
export function resolvePlanFromStripeSubscription(
|
||||
stripeSubscription: Stripe.Subscription
|
||||
): StripePlanResolution {
|
||||
const priceId = stripeSubscription?.items?.data?.[0]?.price?.id
|
||||
const plan = priceId ? getPlanByPriceId(priceId) : undefined
|
||||
|
||||
return {
|
||||
priceId,
|
||||
planFromStripe: plan?.name ?? null,
|
||||
isTeamPlan: plan?.name === 'team',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ export function sanitizeHeaders(
|
||||
* Client-safe MCP constants
|
||||
*/
|
||||
export const MCP_CLIENT_CONSTANTS = {
|
||||
CLIENT_TIMEOUT: 60000,
|
||||
CLIENT_TIMEOUT: 600000,
|
||||
MAX_RETRIES: 3,
|
||||
RECONNECT_DELAY: 1000,
|
||||
} as const
|
||||
|
||||
@@ -81,8 +81,8 @@ describe('generateMcpServerId', () => {
|
||||
})
|
||||
|
||||
describe('MCP_CONSTANTS', () => {
|
||||
it.concurrent('has correct execution timeout', () => {
|
||||
expect(MCP_CONSTANTS.EXECUTION_TIMEOUT).toBe(60000)
|
||||
it.concurrent('has correct execution timeout (10 minutes)', () => {
|
||||
expect(MCP_CONSTANTS.EXECUTION_TIMEOUT).toBe(600000)
|
||||
})
|
||||
|
||||
it.concurrent('has correct cache timeout (5 minutes)', () => {
|
||||
@@ -107,8 +107,8 @@ describe('MCP_CONSTANTS', () => {
|
||||
})
|
||||
|
||||
describe('MCP_CLIENT_CONSTANTS', () => {
|
||||
it.concurrent('has correct client timeout', () => {
|
||||
expect(MCP_CLIENT_CONSTANTS.CLIENT_TIMEOUT).toBe(60000)
|
||||
it.concurrent('has correct client timeout (10 minutes)', () => {
|
||||
expect(MCP_CLIENT_CONSTANTS.CLIENT_TIMEOUT).toBe(600000)
|
||||
})
|
||||
|
||||
it.concurrent('has correct auto refresh interval (5 minutes)', () => {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { isMcpTool, MCP } from '@/executor/constants'
|
||||
* MCP-specific constants
|
||||
*/
|
||||
export const MCP_CONSTANTS = {
|
||||
EXECUTION_TIMEOUT: 60000,
|
||||
EXECUTION_TIMEOUT: 600000,
|
||||
CACHE_TIMEOUT: 5 * 60 * 1000,
|
||||
DEFAULT_RETRIES: 3,
|
||||
DEFAULT_CONNECTION_TIMEOUT: 30000,
|
||||
@@ -49,7 +49,7 @@ export function sanitizeHeaders(
|
||||
* Client-safe MCP constants
|
||||
*/
|
||||
export const MCP_CLIENT_CONSTANTS = {
|
||||
CLIENT_TIMEOUT: 60000,
|
||||
CLIENT_TIMEOUT: 600000,
|
||||
AUTO_REFRESH_INTERVAL: 5 * 60 * 1000,
|
||||
} as const
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { A2ACancelTaskParams, A2ACancelTaskResponse } from '@/tools/a2a/types'
|
||||
import { A2A_OUTPUT_PROPERTIES } from '@/tools/a2a/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { A2ACancelTaskParams, A2ACancelTaskResponse } from './types'
|
||||
import { A2A_OUTPUT_PROPERTIES } from './types'
|
||||
|
||||
export const a2aCancelTaskTool: ToolConfig<A2ACancelTaskParams, A2ACancelTaskResponse> = {
|
||||
id: 'a2a_cancel_task',
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import type {
|
||||
A2ADeletePushNotificationParams,
|
||||
A2ADeletePushNotificationResponse,
|
||||
} from '@/tools/a2a/types'
|
||||
import { A2A_OUTPUT_PROPERTIES } from '@/tools/a2a/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { A2ADeletePushNotificationParams, A2ADeletePushNotificationResponse } from './types'
|
||||
import { A2A_OUTPUT_PROPERTIES } from './types'
|
||||
|
||||
export const a2aDeletePushNotificationTool: ToolConfig<
|
||||
A2ADeletePushNotificationParams,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { A2AGetAgentCardParams, A2AGetAgentCardResponse } from '@/tools/a2a/types'
|
||||
import { A2A_OUTPUT_PROPERTIES } from '@/tools/a2a/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { A2AGetAgentCardParams, A2AGetAgentCardResponse } from './types'
|
||||
import { A2A_OUTPUT_PROPERTIES } from './types'
|
||||
|
||||
export const a2aGetAgentCardTool: ToolConfig<A2AGetAgentCardParams, A2AGetAgentCardResponse> = {
|
||||
id: 'a2a_get_agent_card',
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import type {
|
||||
A2AGetPushNotificationParams,
|
||||
A2AGetPushNotificationResponse,
|
||||
} from '@/tools/a2a/types'
|
||||
import { A2A_OUTPUT_PROPERTIES } from '@/tools/a2a/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { A2AGetPushNotificationParams, A2AGetPushNotificationResponse } from './types'
|
||||
import { A2A_OUTPUT_PROPERTIES } from './types'
|
||||
|
||||
export const a2aGetPushNotificationTool: ToolConfig<
|
||||
A2AGetPushNotificationParams,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { A2AGetTaskParams, A2AGetTaskResponse } from '@/tools/a2a/types'
|
||||
import { A2A_OUTPUT_PROPERTIES } from '@/tools/a2a/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { A2AGetTaskParams, A2AGetTaskResponse } from './types'
|
||||
import { A2A_OUTPUT_PROPERTIES } from './types'
|
||||
|
||||
export const a2aGetTaskTool: ToolConfig<A2AGetTaskParams, A2AGetTaskResponse> = {
|
||||
id: 'a2a_get_task',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { A2AResubscribeParams, A2AResubscribeResponse } from '@/tools/a2a/types'
|
||||
import { A2A_OUTPUT_PROPERTIES } from '@/tools/a2a/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { A2AResubscribeParams, A2AResubscribeResponse } from './types'
|
||||
import { A2A_OUTPUT_PROPERTIES } from './types'
|
||||
|
||||
export const a2aResubscribeTool: ToolConfig<A2AResubscribeParams, A2AResubscribeResponse> = {
|
||||
id: 'a2a_resubscribe',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { A2ASendMessageParams, A2ASendMessageResponse } from '@/tools/a2a/types'
|
||||
import { A2A_OUTPUT_PROPERTIES } from '@/tools/a2a/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { A2ASendMessageParams, A2ASendMessageResponse } from './types'
|
||||
import { A2A_OUTPUT_PROPERTIES } from './types'
|
||||
|
||||
export const a2aSendMessageTool: ToolConfig<A2ASendMessageParams, A2ASendMessageResponse> = {
|
||||
id: 'a2a_send_message',
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import type {
|
||||
A2ASetPushNotificationParams,
|
||||
A2ASetPushNotificationResponse,
|
||||
} from '@/tools/a2a/types'
|
||||
import { A2A_OUTPUT_PROPERTIES } from '@/tools/a2a/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { A2ASetPushNotificationParams, A2ASetPushNotificationResponse } from './types'
|
||||
import { A2A_OUTPUT_PROPERTIES } from './types'
|
||||
|
||||
export const a2aSetPushNotificationTool: ToolConfig<
|
||||
A2ASetPushNotificationParams,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { RunActorParams, RunActorResult } from '@/tools/apify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { RunActorParams, RunActorResult } from './types'
|
||||
|
||||
const POLL_INTERVAL_MS = 5000 // 5 seconds between polls
|
||||
const MAX_POLL_TIME_MS = 300000 // 5 minutes maximum polling time
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { RunActorParams, RunActorResult } from '@/tools/apify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { RunActorParams, RunActorResult } from './types'
|
||||
|
||||
export const apifyRunActorSyncTool: ToolConfig<RunActorParams, RunActorResult> = {
|
||||
id: 'apify_run_actor_sync',
|
||||
|
||||
109
apps/sim/tools/dspy/chain_of_thought.ts
Normal file
109
apps/sim/tools/dspy/chain_of_thought.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import type { DSPyChainOfThoughtParams, DSPyChainOfThoughtResponse } from '@/tools/dspy/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const chainOfThoughtTool: ToolConfig<DSPyChainOfThoughtParams, DSPyChainOfThoughtResponse> =
|
||||
{
|
||||
id: 'dspy_chain_of_thought',
|
||||
name: 'DSPy Chain of Thought',
|
||||
description:
|
||||
'Run a Chain of Thought prediction using a self-hosted DSPy ChainOfThought program endpoint',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
baseUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Base URL of the DSPy server (e.g., https://your-dspy-server.com)',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'API key for authentication (if required by your server)',
|
||||
},
|
||||
endpoint: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'API endpoint path (defaults to /predict)',
|
||||
},
|
||||
question: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The question to answer using chain of thought reasoning',
|
||||
},
|
||||
context: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Additional context to provide for answering the question',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: (params) => {
|
||||
const baseUrl = params.baseUrl.replace(/\/$/, '')
|
||||
const endpoint = params.endpoint || '/predict'
|
||||
return `${baseUrl}${endpoint}`
|
||||
},
|
||||
headers: (params) => {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
if (params.apiKey) {
|
||||
headers.Authorization = `Bearer ${params.apiKey}`
|
||||
}
|
||||
return headers
|
||||
},
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = {
|
||||
text: params.question,
|
||||
}
|
||||
|
||||
if (params.context) {
|
||||
body.context = params.context
|
||||
}
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
const status = data.status ?? 'success'
|
||||
const outputData = data.data ?? data
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
answer: outputData.answer ?? outputData.output ?? outputData.response ?? '',
|
||||
reasoning: outputData.reasoning ?? outputData.rationale ?? outputData.thought ?? '',
|
||||
status,
|
||||
rawOutput: outputData,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
answer: {
|
||||
type: 'string',
|
||||
description: 'The answer generated through chain of thought reasoning',
|
||||
},
|
||||
reasoning: {
|
||||
type: 'string',
|
||||
description: 'The step-by-step reasoning that led to the answer',
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
description: 'Response status from the DSPy server (success or error)',
|
||||
},
|
||||
rawOutput: {
|
||||
type: 'json',
|
||||
description: 'The complete raw output from the DSPy program (result.toDict())',
|
||||
},
|
||||
},
|
||||
}
|
||||
4
apps/sim/tools/dspy/index.ts
Normal file
4
apps/sim/tools/dspy/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { chainOfThoughtTool } from '@/tools/dspy/chain-of-thought'
|
||||
export { predictTool } from '@/tools/dspy/predict'
|
||||
export { reactTool } from '@/tools/dspy/react'
|
||||
export * from '@/tools/dspy/types'
|
||||
125
apps/sim/tools/dspy/predict.ts
Normal file
125
apps/sim/tools/dspy/predict.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import type { DSPyPredictParams, DSPyPredictResponse } from '@/tools/dspy/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const predictTool: ToolConfig<DSPyPredictParams, DSPyPredictResponse> = {
|
||||
id: 'dspy_predict',
|
||||
name: 'DSPy Predict',
|
||||
description: 'Run a prediction using a self-hosted DSPy program endpoint',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
baseUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Base URL of the DSPy server (e.g., https://your-dspy-server.com)',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'API key for authentication (if required by your server)',
|
||||
},
|
||||
endpoint: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'API endpoint path (defaults to /predict)',
|
||||
},
|
||||
input: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The input text to send to the DSPy program',
|
||||
},
|
||||
inputField: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Name of the input field expected by the DSPy program (defaults to "text")',
|
||||
},
|
||||
context: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Additional context to provide to the DSPy program',
|
||||
},
|
||||
additionalInputs: {
|
||||
type: 'json',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Additional key-value pairs to include in the request body',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: (params) => {
|
||||
const baseUrl = params.baseUrl.replace(/\/$/, '')
|
||||
const endpoint = params.endpoint || '/predict'
|
||||
return `${baseUrl}${endpoint}`
|
||||
},
|
||||
headers: (params) => {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
if (params.apiKey) {
|
||||
headers.Authorization = `Bearer ${params.apiKey}`
|
||||
}
|
||||
return headers
|
||||
},
|
||||
body: (params) => {
|
||||
const inputField = params.inputField || 'text'
|
||||
const body: Record<string, unknown> = {
|
||||
[inputField]: params.input,
|
||||
}
|
||||
|
||||
if (params.context) {
|
||||
body.context = params.context
|
||||
}
|
||||
|
||||
if (params.additionalInputs) {
|
||||
Object.assign(body, params.additionalInputs)
|
||||
}
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
const status = data.status ?? 'success'
|
||||
const outputData = data.data ?? data
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
answer: outputData.answer ?? outputData.output ?? outputData.response ?? '',
|
||||
reasoning: outputData.reasoning ?? outputData.rationale ?? null,
|
||||
status,
|
||||
rawOutput: outputData,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
answer: {
|
||||
type: 'string',
|
||||
description: 'The main output/answer from the DSPy program',
|
||||
},
|
||||
reasoning: {
|
||||
type: 'string',
|
||||
description: 'The reasoning or rationale behind the answer (if available)',
|
||||
optional: true,
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
description: 'Response status from the DSPy server (success or error)',
|
||||
},
|
||||
rawOutput: {
|
||||
type: 'json',
|
||||
description: 'The complete raw output from the DSPy program (result.toDict())',
|
||||
},
|
||||
},
|
||||
}
|
||||
156
apps/sim/tools/dspy/react.ts
Normal file
156
apps/sim/tools/dspy/react.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import type { DSPyReActParams, DSPyReActResponse } from '@/tools/dspy/types'
|
||||
import { parseTrajectory } from '@/tools/dspy/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const reactTool: ToolConfig<DSPyReActParams, DSPyReActResponse> = {
|
||||
id: 'dspy_react',
|
||||
name: 'DSPy ReAct',
|
||||
description:
|
||||
'Run a ReAct agent using a self-hosted DSPy ReAct program endpoint for multi-step reasoning and action',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
baseUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Base URL of the DSPy server (e.g., https://your-dspy-server.com)',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'API key for authentication (if required by your server)',
|
||||
},
|
||||
endpoint: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'API endpoint path (defaults to /predict)',
|
||||
},
|
||||
task: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The task or question for the ReAct agent to work on',
|
||||
},
|
||||
context: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Additional context to provide for the task',
|
||||
},
|
||||
maxIterations: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Maximum number of reasoning iterations (defaults to server setting)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: (params) => {
|
||||
const baseUrl = params.baseUrl.replace(/\/$/, '')
|
||||
const endpoint = params.endpoint || '/predict'
|
||||
return `${baseUrl}${endpoint}`
|
||||
},
|
||||
headers: (params) => {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
if (params.apiKey) {
|
||||
headers.Authorization = `Bearer ${params.apiKey}`
|
||||
}
|
||||
return headers
|
||||
},
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = {
|
||||
text: params.task,
|
||||
}
|
||||
|
||||
if (params.context) {
|
||||
body.context = params.context
|
||||
}
|
||||
|
||||
if (params.maxIterations !== undefined) {
|
||||
body.max_iters = params.maxIterations
|
||||
}
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
const status = data.status ?? 'success'
|
||||
const outputData = data.data ?? data
|
||||
|
||||
const rawTrajectory = outputData.trajectory ?? {}
|
||||
const trajectory = Array.isArray(rawTrajectory)
|
||||
? rawTrajectory.map((step: Record<string, unknown>) => ({
|
||||
thought: (step.thought as string) ?? (step.reasoning as string) ?? '',
|
||||
toolName: (step.tool_name as string) ?? (step.selected_fn as string) ?? '',
|
||||
toolArgs:
|
||||
(step.tool_args as Record<string, unknown>) ??
|
||||
(step.args as Record<string, unknown>) ??
|
||||
{},
|
||||
observation: step.observation !== undefined ? String(step.observation) : null,
|
||||
}))
|
||||
: parseTrajectory(rawTrajectory)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
answer:
|
||||
outputData.answer ??
|
||||
outputData.process_result ??
|
||||
outputData.output ??
|
||||
outputData.response ??
|
||||
'',
|
||||
reasoning: outputData.reasoning ?? null,
|
||||
trajectory,
|
||||
status,
|
||||
rawOutput: outputData,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
answer: {
|
||||
type: 'string',
|
||||
description: 'The final answer or result from the ReAct agent',
|
||||
},
|
||||
reasoning: {
|
||||
type: 'string',
|
||||
description: 'The overall reasoning summary from the agent',
|
||||
optional: true,
|
||||
},
|
||||
trajectory: {
|
||||
type: 'array',
|
||||
description: 'The step-by-step trajectory of thoughts, actions, and observations',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
thought: { type: 'string', description: 'The reasoning thought at this step' },
|
||||
toolName: { type: 'string', description: 'The name of the tool/action called' },
|
||||
toolArgs: { type: 'json', description: 'Arguments passed to the tool' },
|
||||
observation: {
|
||||
type: 'string',
|
||||
description: 'The observation/result from the tool execution',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
description: 'Response status from the DSPy server (success or error)',
|
||||
},
|
||||
rawOutput: {
|
||||
type: 'json',
|
||||
description: 'The complete raw output from the DSPy program (result.toDict())',
|
||||
},
|
||||
},
|
||||
}
|
||||
84
apps/sim/tools/dspy/types.ts
Normal file
84
apps/sim/tools/dspy/types.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
/**
|
||||
* Parameters for running a DSPy prediction
|
||||
*/
|
||||
export interface DSPyPredictParams {
|
||||
baseUrl: string
|
||||
apiKey?: string
|
||||
endpoint?: string
|
||||
input: string
|
||||
inputField?: string
|
||||
context?: string
|
||||
additionalInputs?: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* Response from a DSPy prediction
|
||||
*/
|
||||
export interface DSPyPredictResponse extends ToolResponse {
|
||||
output: {
|
||||
answer: string
|
||||
reasoning: string | null
|
||||
status: string
|
||||
rawOutput: Record<string, unknown>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for running a DSPy Chain of Thought prediction
|
||||
*/
|
||||
export interface DSPyChainOfThoughtParams {
|
||||
baseUrl: string
|
||||
apiKey?: string
|
||||
endpoint?: string
|
||||
question: string
|
||||
context?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Response from a DSPy Chain of Thought prediction
|
||||
*/
|
||||
export interface DSPyChainOfThoughtResponse extends ToolResponse {
|
||||
output: {
|
||||
answer: string
|
||||
reasoning: string
|
||||
status: string
|
||||
rawOutput: Record<string, unknown>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for running a DSPy ReAct agent
|
||||
*/
|
||||
export interface DSPyReActParams {
|
||||
baseUrl: string
|
||||
apiKey?: string
|
||||
endpoint?: string
|
||||
task: string
|
||||
context?: string
|
||||
maxIterations?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* ReAct trajectory step structure (matches DSPy output format)
|
||||
*/
|
||||
export interface DSPyTrajectoryStep {
|
||||
thought: string
|
||||
toolName: string
|
||||
toolArgs: Record<string, unknown>
|
||||
observation: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Response from a DSPy ReAct agent
|
||||
*/
|
||||
export interface DSPyReActResponse extends ToolResponse {
|
||||
output: {
|
||||
answer: string
|
||||
reasoning: string | null
|
||||
trajectory: DSPyTrajectoryStep[]
|
||||
status: string
|
||||
rawOutput: Record<string, unknown>
|
||||
}
|
||||
}
|
||||
28
apps/sim/tools/dspy/utils.ts
Normal file
28
apps/sim/tools/dspy/utils.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { DSPyTrajectoryStep } from '@/tools/dspy/types'
|
||||
|
||||
/**
|
||||
* Parse DSPy ReAct trajectory format into structured steps
|
||||
* DSPy trajectory format: { thought_0, tool_name_0, tool_args_0, observation_0, thought_1, ... }
|
||||
*/
|
||||
export function parseTrajectory(trajectory: Record<string, unknown>): DSPyTrajectoryStep[] {
|
||||
const steps: DSPyTrajectoryStep[] = []
|
||||
let idx = 0
|
||||
|
||||
while (
|
||||
trajectory[`thought_${idx}`] !== undefined ||
|
||||
trajectory[`tool_name_${idx}`] !== undefined
|
||||
) {
|
||||
steps.push({
|
||||
thought: (trajectory[`thought_${idx}`] as string) ?? '',
|
||||
toolName: (trajectory[`tool_name_${idx}`] as string) ?? '',
|
||||
toolArgs: (trajectory[`tool_args_${idx}`] as Record<string, unknown>) ?? {},
|
||||
observation:
|
||||
trajectory[`observation_${idx}`] !== undefined
|
||||
? String(trajectory[`observation_${idx}`])
|
||||
: null,
|
||||
})
|
||||
idx++
|
||||
}
|
||||
|
||||
return steps
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
import type {
|
||||
GoogleGroupsAddAliasParams,
|
||||
GoogleGroupsAddAliasResponse,
|
||||
} from '@/tools/google_groups/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { GoogleGroupsAddAliasParams, GoogleGroupsAddAliasResponse } from './types'
|
||||
|
||||
export const addAliasTool: ToolConfig<GoogleGroupsAddAliasParams, GoogleGroupsAddAliasResponse> = {
|
||||
id: 'google_groups_add_alias',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { GoogleGroupsAddMemberParams, GoogleGroupsResponse } from '@/tools/google_groups/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { GoogleGroupsAddMemberParams, GoogleGroupsResponse } from './types'
|
||||
|
||||
export const addMemberTool: ToolConfig<GoogleGroupsAddMemberParams, GoogleGroupsResponse> = {
|
||||
id: 'google_groups_add_member',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { GoogleGroupsCreateParams, GoogleGroupsResponse } from '@/tools/google_groups/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { GoogleGroupsCreateParams, GoogleGroupsResponse } from './types'
|
||||
|
||||
export const createGroupTool: ToolConfig<GoogleGroupsCreateParams, GoogleGroupsResponse> = {
|
||||
id: 'google_groups_create_group',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { GoogleGroupsDeleteParams, GoogleGroupsResponse } from '@/tools/google_groups/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { GoogleGroupsDeleteParams, GoogleGroupsResponse } from './types'
|
||||
|
||||
export const deleteGroupTool: ToolConfig<GoogleGroupsDeleteParams, GoogleGroupsResponse> = {
|
||||
id: 'google_groups_delete_group',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { GoogleGroupsGetParams, GoogleGroupsResponse } from '@/tools/google_groups/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { GoogleGroupsGetParams, GoogleGroupsResponse } from './types'
|
||||
|
||||
export const getGroupTool: ToolConfig<GoogleGroupsGetParams, GoogleGroupsResponse> = {
|
||||
id: 'google_groups_get_group',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { GoogleGroupsGetMemberParams, GoogleGroupsResponse } from '@/tools/google_groups/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { GoogleGroupsGetMemberParams, GoogleGroupsResponse } from './types'
|
||||
|
||||
export const getMemberTool: ToolConfig<GoogleGroupsGetMemberParams, GoogleGroupsResponse> = {
|
||||
id: 'google_groups_get_member',
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type {
|
||||
GoogleGroupsGetSettingsParams,
|
||||
GoogleGroupsGetSettingsResponse,
|
||||
} from '@/tools/google_groups/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { GoogleGroupsGetSettingsParams, GoogleGroupsGetSettingsResponse } from './types'
|
||||
|
||||
export const getSettingsTool: ToolConfig<
|
||||
GoogleGroupsGetSettingsParams,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { GoogleGroupsHasMemberParams, GoogleGroupsResponse } from '@/tools/google_groups/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { GoogleGroupsHasMemberParams, GoogleGroupsResponse } from './types'
|
||||
|
||||
export const hasMemberTool: ToolConfig<GoogleGroupsHasMemberParams, GoogleGroupsResponse> = {
|
||||
id: 'google_groups_has_member',
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type {
|
||||
GoogleGroupsListAliasesParams,
|
||||
GoogleGroupsListAliasesResponse,
|
||||
} from '@/tools/google_groups/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { GoogleGroupsListAliasesParams, GoogleGroupsListAliasesResponse } from './types'
|
||||
|
||||
export const listAliasesTool: ToolConfig<
|
||||
GoogleGroupsListAliasesParams,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { GoogleGroupsListParams, GoogleGroupsResponse } from '@/tools/google_groups/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { GoogleGroupsListParams, GoogleGroupsResponse } from './types'
|
||||
|
||||
export const listGroupsTool: ToolConfig<GoogleGroupsListParams, GoogleGroupsResponse> = {
|
||||
id: 'google_groups_list_groups',
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type {
|
||||
GoogleGroupsListMembersParams,
|
||||
GoogleGroupsResponse,
|
||||
} from '@/tools/google_groups/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { GoogleGroupsListMembersParams, GoogleGroupsResponse } from './types'
|
||||
|
||||
export const listMembersTool: ToolConfig<GoogleGroupsListMembersParams, GoogleGroupsResponse> = {
|
||||
id: 'google_groups_list_members',
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type {
|
||||
GoogleGroupsRemoveAliasParams,
|
||||
GoogleGroupsRemoveAliasResponse,
|
||||
} from '@/tools/google_groups/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { GoogleGroupsRemoveAliasParams, GoogleGroupsRemoveAliasResponse } from './types'
|
||||
|
||||
export const removeAliasTool: ToolConfig<
|
||||
GoogleGroupsRemoveAliasParams,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type {
|
||||
GoogleGroupsRemoveMemberParams,
|
||||
GoogleGroupsResponse,
|
||||
} from '@/tools/google_groups/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { GoogleGroupsRemoveMemberParams, GoogleGroupsResponse } from './types'
|
||||
|
||||
export const removeMemberTool: ToolConfig<GoogleGroupsRemoveMemberParams, GoogleGroupsResponse> = {
|
||||
id: 'google_groups_remove_member',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { GoogleGroupsResponse, GoogleGroupsUpdateParams } from '@/tools/google_groups/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { GoogleGroupsResponse, GoogleGroupsUpdateParams } from './types'
|
||||
|
||||
export const updateGroupTool: ToolConfig<GoogleGroupsUpdateParams, GoogleGroupsResponse> = {
|
||||
id: 'google_groups_update_group',
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type {
|
||||
GoogleGroupsResponse,
|
||||
GoogleGroupsUpdateMemberParams,
|
||||
} from '@/tools/google_groups/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { GoogleGroupsResponse, GoogleGroupsUpdateMemberParams } from './types'
|
||||
|
||||
export const updateMemberTool: ToolConfig<GoogleGroupsUpdateMemberParams, GoogleGroupsResponse> = {
|
||||
id: 'google_groups_update_member',
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type {
|
||||
GoogleGroupsUpdateSettingsParams,
|
||||
GoogleGroupsUpdateSettingsResponse,
|
||||
} from '@/tools/google_groups/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { GoogleGroupsUpdateSettingsParams, GoogleGroupsUpdateSettingsResponse } from './types'
|
||||
|
||||
export const updateSettingsTool: ToolConfig<
|
||||
GoogleGroupsUpdateSettingsParams,
|
||||
|
||||
223
apps/sim/tools/google_maps/air_quality.ts
Normal file
223
apps/sim/tools/google_maps/air_quality.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
import type {
|
||||
GoogleMapsAirQualityParams,
|
||||
GoogleMapsAirQualityResponse,
|
||||
} from '@/tools/google_maps/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const googleMapsAirQualityTool: ToolConfig<
|
||||
GoogleMapsAirQualityParams,
|
||||
GoogleMapsAirQualityResponse
|
||||
> = {
|
||||
id: 'google_maps_air_quality',
|
||||
name: 'Google Maps Air Quality',
|
||||
description: 'Get current air quality data for a location',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Google Maps API key with Air Quality API enabled',
|
||||
},
|
||||
lat: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Latitude coordinate',
|
||||
},
|
||||
lng: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Longitude coordinate',
|
||||
},
|
||||
languageCode: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Language code for the response (e.g., "en", "es")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
return `https://airquality.googleapis.com/v1/currentConditions:lookup?key=${params.apiKey.trim()}`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: {
|
||||
location: { latitude: number; longitude: number }
|
||||
extraComputations: string[]
|
||||
languageCode?: string
|
||||
} = {
|
||||
location: {
|
||||
latitude: params.lat,
|
||||
longitude: params.lng,
|
||||
},
|
||||
extraComputations: [
|
||||
'HEALTH_RECOMMENDATIONS',
|
||||
'DOMINANT_POLLUTANT_CONCENTRATION',
|
||||
'POLLUTANT_CONCENTRATION',
|
||||
'LOCAL_AQI',
|
||||
'POLLUTANT_ADDITIONAL_INFO',
|
||||
],
|
||||
}
|
||||
|
||||
if (params.languageCode) {
|
||||
body.languageCode = params.languageCode.trim()
|
||||
}
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.error) {
|
||||
throw new Error(`Air Quality failed: ${data.error.message || 'Unknown error'}`)
|
||||
}
|
||||
|
||||
const indexes = (data.indexes || []).map(
|
||||
(index: {
|
||||
code: string
|
||||
displayName: string
|
||||
aqi: number
|
||||
aqiDisplay: string
|
||||
color: { red?: number; green?: number; blue?: number }
|
||||
category: string
|
||||
dominantPollutant: string
|
||||
}) => ({
|
||||
code: index.code,
|
||||
displayName: index.displayName,
|
||||
aqi: index.aqi,
|
||||
aqiDisplay: index.aqiDisplay,
|
||||
color: {
|
||||
red: index.color?.red || 0,
|
||||
green: index.color?.green || 0,
|
||||
blue: index.color?.blue || 0,
|
||||
},
|
||||
category: index.category,
|
||||
dominantPollutant: index.dominantPollutant,
|
||||
})
|
||||
)
|
||||
|
||||
const pollutants = (data.pollutants || []).map(
|
||||
(pollutant: {
|
||||
code: string
|
||||
displayName: string
|
||||
fullName: string
|
||||
concentration: { value: number; units: string }
|
||||
additionalInfo?: { sources: string; effects: string }
|
||||
}) => ({
|
||||
code: pollutant.code,
|
||||
displayName: pollutant.displayName,
|
||||
fullName: pollutant.fullName,
|
||||
concentration: {
|
||||
value: pollutant.concentration?.value || 0,
|
||||
units: pollutant.concentration?.units || '',
|
||||
},
|
||||
additionalInfo: pollutant.additionalInfo
|
||||
? {
|
||||
sources: pollutant.additionalInfo.sources,
|
||||
effects: pollutant.additionalInfo.effects,
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
)
|
||||
|
||||
const healthRecs = data.healthRecommendations
|
||||
const healthRecommendations = healthRecs
|
||||
? {
|
||||
generalPopulation: healthRecs.generalPopulation || '',
|
||||
elderly: healthRecs.elderly || '',
|
||||
lungDiseasePopulation: healthRecs.lungDiseasePopulation || '',
|
||||
heartDiseasePopulation: healthRecs.heartDiseasePopulation || '',
|
||||
athletes: healthRecs.athletes || '',
|
||||
pregnantWomen: healthRecs.pregnantWomen || '',
|
||||
children: healthRecs.children || '',
|
||||
}
|
||||
: null
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
dateTime: data.dateTime || '',
|
||||
regionCode: data.regionCode || '',
|
||||
indexes,
|
||||
pollutants,
|
||||
healthRecommendations,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
dateTime: {
|
||||
type: 'string',
|
||||
description: 'Timestamp of the air quality data',
|
||||
},
|
||||
regionCode: {
|
||||
type: 'string',
|
||||
description: 'Region code for the location',
|
||||
},
|
||||
indexes: {
|
||||
type: 'array',
|
||||
description: 'Array of air quality indexes',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
code: { type: 'string', description: 'Index code (e.g., "uaqi", "usa_epa")' },
|
||||
displayName: { type: 'string', description: 'Display name of the index' },
|
||||
aqi: { type: 'number', description: 'Air quality index value' },
|
||||
aqiDisplay: { type: 'string', description: 'Formatted AQI display string' },
|
||||
color: {
|
||||
type: 'object',
|
||||
description: 'RGB color for the AQI level',
|
||||
properties: {
|
||||
red: { type: 'number' },
|
||||
green: { type: 'number' },
|
||||
blue: { type: 'number' },
|
||||
},
|
||||
},
|
||||
category: {
|
||||
type: 'string',
|
||||
description: 'Category description (e.g., "Good", "Moderate")',
|
||||
},
|
||||
dominantPollutant: { type: 'string', description: 'The dominant pollutant' },
|
||||
},
|
||||
},
|
||||
},
|
||||
pollutants: {
|
||||
type: 'array',
|
||||
description: 'Array of pollutant concentrations',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
code: { type: 'string', description: 'Pollutant code (e.g., "pm25", "o3")' },
|
||||
displayName: { type: 'string', description: 'Display name' },
|
||||
fullName: { type: 'string', description: 'Full pollutant name' },
|
||||
concentration: {
|
||||
type: 'object',
|
||||
description: 'Concentration info',
|
||||
properties: {
|
||||
value: { type: 'number', description: 'Concentration value' },
|
||||
units: { type: 'string', description: 'Units (e.g., "PARTS_PER_BILLION")' },
|
||||
},
|
||||
},
|
||||
additionalInfo: {
|
||||
type: 'object',
|
||||
description: 'Additional info about sources and effects',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
healthRecommendations: {
|
||||
type: 'object',
|
||||
description: 'Health recommendations for different populations',
|
||||
},
|
||||
},
|
||||
}
|
||||
259
apps/sim/tools/google_maps/directions.ts
Normal file
259
apps/sim/tools/google_maps/directions.ts
Normal file
@@ -0,0 +1,259 @@
|
||||
import type {
|
||||
GoogleMapsDirectionsParams,
|
||||
GoogleMapsDirectionsResponse,
|
||||
} from '@/tools/google_maps/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const googleMapsDirectionsTool: ToolConfig<
|
||||
GoogleMapsDirectionsParams,
|
||||
GoogleMapsDirectionsResponse
|
||||
> = {
|
||||
id: 'google_maps_directions',
|
||||
name: 'Google Maps Directions',
|
||||
description: 'Get directions and route information between two locations',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Google Maps API key',
|
||||
},
|
||||
origin: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Starting location (address or lat,lng)',
|
||||
},
|
||||
destination: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Destination location (address or lat,lng)',
|
||||
},
|
||||
mode: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Travel mode: driving, walking, bicycling, or transit',
|
||||
},
|
||||
avoid: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Features to avoid: tolls, highways, or ferries',
|
||||
},
|
||||
waypoints: {
|
||||
type: 'json',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Array of intermediate waypoints',
|
||||
},
|
||||
units: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Unit system: metric or imperial',
|
||||
},
|
||||
language: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Language code for results (e.g., en, es, fr)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://maps.googleapis.com/maps/api/directions/json')
|
||||
url.searchParams.set('origin', params.origin.trim())
|
||||
url.searchParams.set('destination', params.destination.trim())
|
||||
url.searchParams.set('key', params.apiKey.trim())
|
||||
|
||||
if (params.mode) {
|
||||
url.searchParams.set('mode', params.mode)
|
||||
}
|
||||
if (params.avoid) {
|
||||
url.searchParams.set('avoid', params.avoid)
|
||||
}
|
||||
if (params.waypoints && params.waypoints.length > 0) {
|
||||
url.searchParams.set('waypoints', params.waypoints.join('|'))
|
||||
}
|
||||
if (params.units) {
|
||||
url.searchParams.set('units', params.units)
|
||||
}
|
||||
if (params.language) {
|
||||
url.searchParams.set('language', params.language.trim())
|
||||
}
|
||||
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.status !== 'OK') {
|
||||
throw new Error(
|
||||
`Directions request failed: ${data.status} - ${data.error_message || 'Unknown error'}`
|
||||
)
|
||||
}
|
||||
|
||||
const routes = data.routes.map(
|
||||
(route: {
|
||||
summary: string
|
||||
legs: Array<{
|
||||
start_address: string
|
||||
end_address: string
|
||||
start_location: { lat: number; lng: number }
|
||||
end_location: { lat: number; lng: number }
|
||||
distance: { text: string; value: number }
|
||||
duration: { text: string; value: number }
|
||||
steps: Array<{
|
||||
html_instructions: string
|
||||
distance: { text: string; value: number }
|
||||
duration: { text: string; value: number }
|
||||
start_location: { lat: number; lng: number }
|
||||
end_location: { lat: number; lng: number }
|
||||
travel_mode: string
|
||||
maneuver?: string
|
||||
}>
|
||||
}>
|
||||
overview_polyline: { points: string }
|
||||
warnings: string[]
|
||||
waypoint_order: number[]
|
||||
}) => ({
|
||||
summary: route.summary,
|
||||
legs: route.legs.map((leg) => ({
|
||||
startAddress: leg.start_address,
|
||||
endAddress: leg.end_address,
|
||||
startLocation: {
|
||||
lat: leg.start_location.lat,
|
||||
lng: leg.start_location.lng,
|
||||
},
|
||||
endLocation: {
|
||||
lat: leg.end_location.lat,
|
||||
lng: leg.end_location.lng,
|
||||
},
|
||||
distanceText: leg.distance.text,
|
||||
distanceMeters: leg.distance.value,
|
||||
durationText: leg.duration.text,
|
||||
durationSeconds: leg.duration.value,
|
||||
steps: leg.steps.map((step) => ({
|
||||
instruction: step.html_instructions.replace(/<[^>]*>/g, ''),
|
||||
distanceText: step.distance.text,
|
||||
distanceMeters: step.distance.value,
|
||||
durationText: step.duration.text,
|
||||
durationSeconds: step.duration.value,
|
||||
startLocation: {
|
||||
lat: step.start_location.lat,
|
||||
lng: step.start_location.lng,
|
||||
},
|
||||
endLocation: {
|
||||
lat: step.end_location.lat,
|
||||
lng: step.end_location.lng,
|
||||
},
|
||||
travelMode: step.travel_mode,
|
||||
maneuver: step.maneuver ?? null,
|
||||
})),
|
||||
})),
|
||||
overviewPolyline: route.overview_polyline.points,
|
||||
warnings: route.warnings ?? [],
|
||||
waypointOrder: route.waypoint_order ?? [],
|
||||
})
|
||||
)
|
||||
|
||||
const primaryRoute = routes[0]
|
||||
const primaryLeg = primaryRoute?.legs[0]
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
routes,
|
||||
distanceText: primaryLeg?.distanceText ?? '',
|
||||
distanceMeters: primaryLeg?.distanceMeters ?? 0,
|
||||
durationText: primaryLeg?.durationText ?? '',
|
||||
durationSeconds: primaryLeg?.durationSeconds ?? 0,
|
||||
startAddress: primaryLeg?.startAddress ?? '',
|
||||
endAddress: primaryLeg?.endAddress ?? '',
|
||||
steps: primaryLeg?.steps ?? [],
|
||||
polyline: primaryRoute?.overviewPolyline ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
routes: {
|
||||
type: 'array',
|
||||
description: 'All available routes',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
summary: { type: 'string', description: 'Route summary (main road names)' },
|
||||
legs: { type: 'array', description: 'Route legs (segments between waypoints)' },
|
||||
overviewPolyline: {
|
||||
type: 'string',
|
||||
description: 'Encoded polyline for the entire route',
|
||||
},
|
||||
warnings: { type: 'array', description: 'Route warnings' },
|
||||
waypointOrder: { type: 'array', description: 'Optimized waypoint order (if requested)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
distanceText: {
|
||||
type: 'string',
|
||||
description: 'Total distance as human-readable text (e.g., "5.2 km")',
|
||||
},
|
||||
distanceMeters: {
|
||||
type: 'number',
|
||||
description: 'Total distance in meters',
|
||||
},
|
||||
durationText: {
|
||||
type: 'string',
|
||||
description: 'Total duration as human-readable text (e.g., "15 mins")',
|
||||
},
|
||||
durationSeconds: {
|
||||
type: 'number',
|
||||
description: 'Total duration in seconds',
|
||||
},
|
||||
startAddress: {
|
||||
type: 'string',
|
||||
description: 'Resolved starting address',
|
||||
},
|
||||
endAddress: {
|
||||
type: 'string',
|
||||
description: 'Resolved ending address',
|
||||
},
|
||||
steps: {
|
||||
type: 'array',
|
||||
description: 'Turn-by-turn navigation instructions',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
instruction: { type: 'string', description: 'Navigation instruction (HTML stripped)' },
|
||||
distanceText: { type: 'string', description: 'Step distance as text' },
|
||||
distanceMeters: { type: 'number', description: 'Step distance in meters' },
|
||||
durationText: { type: 'string', description: 'Step duration as text' },
|
||||
durationSeconds: { type: 'number', description: 'Step duration in seconds' },
|
||||
startLocation: { type: 'object', description: 'Step start coordinates' },
|
||||
endLocation: { type: 'object', description: 'Step end coordinates' },
|
||||
travelMode: { type: 'string', description: 'Travel mode for this step' },
|
||||
maneuver: {
|
||||
type: 'string',
|
||||
description: 'Maneuver type (turn-left, etc.)',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
polyline: {
|
||||
type: 'string',
|
||||
description: 'Encoded polyline for the primary route',
|
||||
},
|
||||
},
|
||||
}
|
||||
181
apps/sim/tools/google_maps/distance_matrix.ts
Normal file
181
apps/sim/tools/google_maps/distance_matrix.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import type {
|
||||
GoogleMapsDistanceMatrixParams,
|
||||
GoogleMapsDistanceMatrixResponse,
|
||||
} from '@/tools/google_maps/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const googleMapsDistanceMatrixTool: ToolConfig<
|
||||
GoogleMapsDistanceMatrixParams,
|
||||
GoogleMapsDistanceMatrixResponse
|
||||
> = {
|
||||
id: 'google_maps_distance_matrix',
|
||||
name: 'Google Maps Distance Matrix',
|
||||
description: 'Calculate travel distance and time between multiple origins and destinations',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Google Maps API key',
|
||||
},
|
||||
origin: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Origin location (address or lat,lng)',
|
||||
},
|
||||
destinations: {
|
||||
type: 'json',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Array of destination locations',
|
||||
},
|
||||
mode: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Travel mode: driving, walking, bicycling, or transit',
|
||||
},
|
||||
avoid: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Features to avoid: tolls, highways, or ferries',
|
||||
},
|
||||
units: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Unit system: metric or imperial',
|
||||
},
|
||||
language: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Language code for results (e.g., en, es, fr)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://maps.googleapis.com/maps/api/distancematrix/json')
|
||||
url.searchParams.set('origins', params.origin.trim())
|
||||
url.searchParams.set('destinations', params.destinations.join('|'))
|
||||
url.searchParams.set('key', params.apiKey.trim())
|
||||
|
||||
if (params.mode) {
|
||||
url.searchParams.set('mode', params.mode)
|
||||
}
|
||||
if (params.avoid) {
|
||||
url.searchParams.set('avoid', params.avoid)
|
||||
}
|
||||
if (params.units) {
|
||||
url.searchParams.set('units', params.units)
|
||||
}
|
||||
if (params.language) {
|
||||
url.searchParams.set('language', params.language.trim())
|
||||
}
|
||||
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.status !== 'OK') {
|
||||
throw new Error(
|
||||
`Distance matrix request failed: ${data.status} - ${data.error_message || 'Unknown error'}`
|
||||
)
|
||||
}
|
||||
|
||||
const rows = data.rows.map(
|
||||
(row: {
|
||||
elements: Array<{
|
||||
distance?: { text: string; value: number }
|
||||
duration?: { text: string; value: number }
|
||||
duration_in_traffic?: { text: string; value: number }
|
||||
status: string
|
||||
}>
|
||||
}) => ({
|
||||
elements: row.elements.map((element) => ({
|
||||
distanceText: element.distance?.text ?? 'N/A',
|
||||
distanceMeters: element.distance?.value ?? 0,
|
||||
durationText: element.duration?.text ?? 'N/A',
|
||||
durationSeconds: element.duration?.value ?? 0,
|
||||
durationInTrafficText: element.duration_in_traffic?.text ?? null,
|
||||
durationInTrafficSeconds: element.duration_in_traffic?.value ?? null,
|
||||
status: element.status,
|
||||
})),
|
||||
})
|
||||
)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
originAddresses: data.origin_addresses ?? [],
|
||||
destinationAddresses: data.destination_addresses ?? [],
|
||||
rows,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
originAddresses: {
|
||||
type: 'array',
|
||||
description: 'Resolved origin addresses',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
destinationAddresses: {
|
||||
type: 'array',
|
||||
description: 'Resolved destination addresses',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
rows: {
|
||||
type: 'array',
|
||||
description: 'Distance matrix rows (one per origin)',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
elements: {
|
||||
type: 'array',
|
||||
description: 'Elements (one per destination)',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
distanceText: { type: 'string', description: 'Distance as text (e.g., "5.2 km")' },
|
||||
distanceMeters: { type: 'number', description: 'Distance in meters' },
|
||||
durationText: { type: 'string', description: 'Duration as text (e.g., "15 mins")' },
|
||||
durationSeconds: { type: 'number', description: 'Duration in seconds' },
|
||||
durationInTrafficText: {
|
||||
type: 'string',
|
||||
description: 'Duration in traffic as text',
|
||||
optional: true,
|
||||
},
|
||||
durationInTrafficSeconds: {
|
||||
type: 'number',
|
||||
description: 'Duration in traffic in seconds',
|
||||
optional: true,
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
description: 'Element status (OK, NOT_FOUND, ZERO_RESULTS)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
91
apps/sim/tools/google_maps/elevation.ts
Normal file
91
apps/sim/tools/google_maps/elevation.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import type {
|
||||
GoogleMapsElevationParams,
|
||||
GoogleMapsElevationResponse,
|
||||
} from '@/tools/google_maps/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const googleMapsElevationTool: ToolConfig<
|
||||
GoogleMapsElevationParams,
|
||||
GoogleMapsElevationResponse
|
||||
> = {
|
||||
id: 'google_maps_elevation',
|
||||
name: 'Google Maps Elevation',
|
||||
description: 'Get elevation data for a location',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Google Maps API key',
|
||||
},
|
||||
lat: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Latitude coordinate',
|
||||
},
|
||||
lng: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Longitude coordinate',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://maps.googleapis.com/maps/api/elevation/json')
|
||||
url.searchParams.set('locations', `${params.lat},${params.lng}`)
|
||||
url.searchParams.set('key', params.apiKey.trim())
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.status !== 'OK') {
|
||||
throw new Error(
|
||||
`Elevation request failed: ${data.status} - ${data.error_message || 'Unknown error'}`
|
||||
)
|
||||
}
|
||||
|
||||
const result = data.results[0]
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
elevation: result.elevation,
|
||||
lat: result.location.lat,
|
||||
lng: result.location.lng,
|
||||
resolution: result.resolution,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
elevation: {
|
||||
type: 'number',
|
||||
description: 'Elevation in meters above sea level (negative for below)',
|
||||
},
|
||||
lat: {
|
||||
type: 'number',
|
||||
description: 'Latitude of the elevation sample',
|
||||
},
|
||||
lng: {
|
||||
type: 'number',
|
||||
description: 'Longitude of the elevation sample',
|
||||
},
|
||||
resolution: {
|
||||
type: 'number',
|
||||
description:
|
||||
'Maximum distance between data points (meters) from which elevation was interpolated',
|
||||
},
|
||||
},
|
||||
}
|
||||
130
apps/sim/tools/google_maps/geocode.ts
Normal file
130
apps/sim/tools/google_maps/geocode.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import type { GoogleMapsGeocodeParams, GoogleMapsGeocodeResponse } from '@/tools/google_maps/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const googleMapsGeocodeTool: ToolConfig<GoogleMapsGeocodeParams, GoogleMapsGeocodeResponse> =
|
||||
{
|
||||
id: 'google_maps_geocode',
|
||||
name: 'Google Maps Geocode',
|
||||
description: 'Convert an address into geographic coordinates (latitude and longitude)',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Google Maps API key',
|
||||
},
|
||||
address: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The address to geocode',
|
||||
},
|
||||
language: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Language code for results (e.g., en, es, fr)',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Region bias as a ccTLD code (e.g., us, uk)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://maps.googleapis.com/maps/api/geocode/json')
|
||||
url.searchParams.set('address', params.address.trim())
|
||||
url.searchParams.set('key', params.apiKey.trim())
|
||||
if (params.language) {
|
||||
url.searchParams.set('language', params.language.trim())
|
||||
}
|
||||
if (params.region) {
|
||||
url.searchParams.set('region', params.region.trim())
|
||||
}
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.status !== 'OK') {
|
||||
throw new Error(
|
||||
`Geocoding failed: ${data.status} - ${data.error_message || 'Unknown error'}`
|
||||
)
|
||||
}
|
||||
|
||||
const result = data.results[0]
|
||||
const location = result.geometry.location
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
formattedAddress: result.formatted_address,
|
||||
lat: location.lat,
|
||||
lng: location.lng,
|
||||
location: {
|
||||
lat: location.lat,
|
||||
lng: location.lng,
|
||||
},
|
||||
placeId: result.place_id,
|
||||
addressComponents: (result.address_components || []).map(
|
||||
(comp: { long_name: string; short_name: string; types: string[] }) => ({
|
||||
longName: comp.long_name,
|
||||
shortName: comp.short_name,
|
||||
types: comp.types,
|
||||
})
|
||||
),
|
||||
locationType: result.geometry.location_type,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
formattedAddress: {
|
||||
type: 'string',
|
||||
description: 'The formatted address string',
|
||||
},
|
||||
lat: {
|
||||
type: 'number',
|
||||
description: 'Latitude coordinate',
|
||||
},
|
||||
lng: {
|
||||
type: 'number',
|
||||
description: 'Longitude coordinate',
|
||||
},
|
||||
location: {
|
||||
type: 'json',
|
||||
description: 'Location object with lat and lng',
|
||||
},
|
||||
placeId: {
|
||||
type: 'string',
|
||||
description: 'Google Place ID for this location',
|
||||
},
|
||||
addressComponents: {
|
||||
type: 'array',
|
||||
description: 'Detailed address components',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
longName: { type: 'string', description: 'Full name of the component' },
|
||||
shortName: { type: 'string', description: 'Abbreviated name' },
|
||||
types: { type: 'array', description: 'Component types' },
|
||||
},
|
||||
},
|
||||
},
|
||||
locationType: {
|
||||
type: 'string',
|
||||
description: 'Location accuracy type (ROOFTOP, RANGE_INTERPOLATED, etc.)',
|
||||
},
|
||||
},
|
||||
}
|
||||
165
apps/sim/tools/google_maps/geolocate.ts
Normal file
165
apps/sim/tools/google_maps/geolocate.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import type {
|
||||
GoogleMapsGeolocateParams,
|
||||
GoogleMapsGeolocateResponse,
|
||||
} from '@/tools/google_maps/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const googleMapsGeolocateTool: ToolConfig<
|
||||
GoogleMapsGeolocateParams,
|
||||
GoogleMapsGeolocateResponse
|
||||
> = {
|
||||
id: 'google_maps_geolocate',
|
||||
name: 'Google Maps Geolocate',
|
||||
description: 'Geolocate a device using WiFi access points, cell towers, or IP address',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Google Maps API key with Geolocation API enabled',
|
||||
},
|
||||
homeMobileCountryCode: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Home mobile country code (MCC)',
|
||||
},
|
||||
homeMobileNetworkCode: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Home mobile network code (MNC)',
|
||||
},
|
||||
radioType: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Radio type: lte, gsm, cdma, wcdma, or nr',
|
||||
},
|
||||
carrier: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Carrier name',
|
||||
},
|
||||
considerIp: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether to use IP address for geolocation (default: true)',
|
||||
},
|
||||
cellTowers: {
|
||||
type: 'array',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Array of cell tower objects with cellId, locationAreaCode, mobileCountryCode, mobileNetworkCode',
|
||||
},
|
||||
wifiAccessPoints: {
|
||||
type: 'array',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Array of WiFi access point objects with macAddress (required), signalStrength, etc.',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
return `https://www.googleapis.com/geolocation/v1/geolocate?key=${params.apiKey.trim()}`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: {
|
||||
homeMobileCountryCode?: number
|
||||
homeMobileNetworkCode?: number
|
||||
radioType?: string
|
||||
carrier?: string
|
||||
considerIp?: boolean
|
||||
cellTowers?: Array<{
|
||||
cellId: number
|
||||
locationAreaCode: number
|
||||
mobileCountryCode: number
|
||||
mobileNetworkCode: number
|
||||
age?: number
|
||||
signalStrength?: number
|
||||
timingAdvance?: number
|
||||
}>
|
||||
wifiAccessPoints?: Array<{
|
||||
macAddress: string
|
||||
signalStrength?: number
|
||||
age?: number
|
||||
channel?: number
|
||||
signalToNoiseRatio?: number
|
||||
}>
|
||||
} = {}
|
||||
|
||||
if (params.homeMobileCountryCode !== undefined) {
|
||||
body.homeMobileCountryCode = params.homeMobileCountryCode
|
||||
}
|
||||
|
||||
if (params.homeMobileNetworkCode !== undefined) {
|
||||
body.homeMobileNetworkCode = params.homeMobileNetworkCode
|
||||
}
|
||||
|
||||
if (params.radioType) {
|
||||
body.radioType = params.radioType
|
||||
}
|
||||
|
||||
if (params.carrier) {
|
||||
body.carrier = params.carrier
|
||||
}
|
||||
|
||||
if (params.considerIp !== undefined) {
|
||||
body.considerIp = params.considerIp
|
||||
}
|
||||
|
||||
if (params.cellTowers && params.cellTowers.length > 0) {
|
||||
body.cellTowers = params.cellTowers
|
||||
}
|
||||
|
||||
if (params.wifiAccessPoints && params.wifiAccessPoints.length > 0) {
|
||||
body.wifiAccessPoints = params.wifiAccessPoints
|
||||
}
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.error) {
|
||||
throw new Error(`Geolocation failed: ${data.error.message || 'Unknown error'}`)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
lat: data.location?.lat || 0,
|
||||
lng: data.location?.lng || 0,
|
||||
accuracy: data.accuracy || 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
lat: {
|
||||
type: 'number',
|
||||
description: 'Latitude coordinate',
|
||||
},
|
||||
lng: {
|
||||
type: 'number',
|
||||
description: 'Longitude coordinate',
|
||||
},
|
||||
accuracy: {
|
||||
type: 'number',
|
||||
description: 'Accuracy radius in meters',
|
||||
},
|
||||
},
|
||||
}
|
||||
32
apps/sim/tools/google_maps/index.ts
Normal file
32
apps/sim/tools/google_maps/index.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { googleMapsAirQualityTool } from '@/tools/google_maps/air_quality'
|
||||
import { googleMapsDirectionsTool } from '@/tools/google_maps/directions'
|
||||
import { googleMapsDistanceMatrixTool } from '@/tools/google_maps/distance_matrix'
|
||||
import { googleMapsElevationTool } from '@/tools/google_maps/elevation'
|
||||
import { googleMapsGeocodeTool } from '@/tools/google_maps/geocode'
|
||||
import { googleMapsGeolocateTool } from '@/tools/google_maps/geolocate'
|
||||
import { googleMapsPlaceDetailsTool } from '@/tools/google_maps/place_details'
|
||||
import { googleMapsPlacesSearchTool } from '@/tools/google_maps/places_search'
|
||||
import { googleMapsReverseGeocodeTool } from '@/tools/google_maps/reverse_geocode'
|
||||
import { googleMapsSnapToRoadsTool } from '@/tools/google_maps/snap_to_roads'
|
||||
import { googleMapsSpeedLimitsTool } from '@/tools/google_maps/speed_limits'
|
||||
import { googleMapsTimezoneTool } from '@/tools/google_maps/timezone'
|
||||
import { googleMapsValidateAddressTool } from '@/tools/google_maps/validate_address'
|
||||
|
||||
export {
|
||||
googleMapsAirQualityTool,
|
||||
googleMapsDirectionsTool,
|
||||
googleMapsDistanceMatrixTool,
|
||||
googleMapsElevationTool,
|
||||
googleMapsGeocodeTool,
|
||||
googleMapsGeolocateTool,
|
||||
googleMapsPlaceDetailsTool,
|
||||
googleMapsPlacesSearchTool,
|
||||
googleMapsReverseGeocodeTool,
|
||||
googleMapsSnapToRoadsTool,
|
||||
googleMapsSpeedLimitsTool,
|
||||
googleMapsTimezoneTool,
|
||||
googleMapsValidateAddressTool,
|
||||
}
|
||||
|
||||
// Export types
|
||||
export * from './types'
|
||||
274
apps/sim/tools/google_maps/place_details.ts
Normal file
274
apps/sim/tools/google_maps/place_details.ts
Normal file
@@ -0,0 +1,274 @@
|
||||
import type {
|
||||
GoogleMapsPlaceDetailsParams,
|
||||
GoogleMapsPlaceDetailsResponse,
|
||||
} from '@/tools/google_maps/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const googleMapsPlaceDetailsTool: ToolConfig<
|
||||
GoogleMapsPlaceDetailsParams,
|
||||
GoogleMapsPlaceDetailsResponse
|
||||
> = {
|
||||
id: 'google_maps_place_details',
|
||||
name: 'Google Maps Place Details',
|
||||
description: 'Get detailed information about a specific place',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Google Maps API key',
|
||||
},
|
||||
placeId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Google Place ID',
|
||||
},
|
||||
fields: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated list of fields to return',
|
||||
},
|
||||
language: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Language code for results (e.g., en, es, fr)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://maps.googleapis.com/maps/api/place/details/json')
|
||||
url.searchParams.set('place_id', params.placeId.trim())
|
||||
url.searchParams.set('key', params.apiKey.trim())
|
||||
|
||||
// Default fields if not specified - comprehensive list
|
||||
const fields =
|
||||
params.fields ||
|
||||
'place_id,name,formatted_address,geometry,types,rating,user_ratings_total,price_level,website,formatted_phone_number,international_phone_number,opening_hours,reviews,photos,url,utc_offset,vicinity,business_status'
|
||||
url.searchParams.set('fields', fields)
|
||||
|
||||
if (params.language) {
|
||||
url.searchParams.set('language', params.language.trim())
|
||||
}
|
||||
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.status !== 'OK') {
|
||||
throw new Error(
|
||||
`Place details request failed: ${data.status} - ${data.error_message || 'Unknown error'}`
|
||||
)
|
||||
}
|
||||
|
||||
const place = data.result
|
||||
|
||||
const reviews = (place.reviews || []).map(
|
||||
(review: {
|
||||
author_name: string
|
||||
author_url?: string
|
||||
profile_photo_url?: string
|
||||
rating: number
|
||||
text: string
|
||||
time: number
|
||||
relative_time_description: string
|
||||
}) => ({
|
||||
authorName: review.author_name,
|
||||
authorUrl: review.author_url ?? null,
|
||||
profilePhotoUrl: review.profile_photo_url ?? null,
|
||||
rating: review.rating,
|
||||
text: review.text,
|
||||
time: review.time,
|
||||
relativeTimeDescription: review.relative_time_description,
|
||||
})
|
||||
)
|
||||
|
||||
const photos = (place.photos || []).map(
|
||||
(photo: {
|
||||
photo_reference: string
|
||||
height: number
|
||||
width: number
|
||||
html_attributions: string[]
|
||||
}) => ({
|
||||
photoReference: photo.photo_reference,
|
||||
height: photo.height,
|
||||
width: photo.width,
|
||||
htmlAttributions: photo.html_attributions ?? [],
|
||||
})
|
||||
)
|
||||
|
||||
// Destructure opening hours
|
||||
const openNow = place.opening_hours?.open_now ?? null
|
||||
const weekdayText = place.opening_hours?.weekday_text ?? []
|
||||
|
||||
// Extract location
|
||||
const lat = place.geometry?.location?.lat ?? null
|
||||
const lng = place.geometry?.location?.lng ?? null
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
placeId: place.place_id,
|
||||
name: place.name ?? null,
|
||||
formattedAddress: place.formatted_address ?? null,
|
||||
lat,
|
||||
lng,
|
||||
types: place.types ?? [],
|
||||
rating: place.rating ?? null,
|
||||
userRatingsTotal: place.user_ratings_total ?? null,
|
||||
priceLevel: place.price_level ?? null,
|
||||
website: place.website ?? null,
|
||||
phoneNumber: place.formatted_phone_number ?? null,
|
||||
internationalPhoneNumber: place.international_phone_number ?? null,
|
||||
openNow,
|
||||
weekdayText,
|
||||
reviews,
|
||||
photos,
|
||||
url: place.url ?? null,
|
||||
utcOffset: place.utc_offset ?? null,
|
||||
vicinity: place.vicinity ?? null,
|
||||
businessStatus: place.business_status ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
placeId: {
|
||||
type: 'string',
|
||||
description: 'Google Place ID',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Place name',
|
||||
optional: true,
|
||||
},
|
||||
formattedAddress: {
|
||||
type: 'string',
|
||||
description: 'Formatted street address',
|
||||
optional: true,
|
||||
},
|
||||
lat: {
|
||||
type: 'number',
|
||||
description: 'Latitude coordinate',
|
||||
optional: true,
|
||||
},
|
||||
lng: {
|
||||
type: 'number',
|
||||
description: 'Longitude coordinate',
|
||||
optional: true,
|
||||
},
|
||||
types: {
|
||||
type: 'array',
|
||||
description: 'Place types (e.g., restaurant, cafe)',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
rating: {
|
||||
type: 'number',
|
||||
description: 'Average rating (1.0 to 5.0)',
|
||||
optional: true,
|
||||
},
|
||||
userRatingsTotal: {
|
||||
type: 'number',
|
||||
description: 'Total number of user ratings',
|
||||
optional: true,
|
||||
},
|
||||
priceLevel: {
|
||||
type: 'number',
|
||||
description: 'Price level (0=Free, 1=Inexpensive, 2=Moderate, 3=Expensive, 4=Very Expensive)',
|
||||
optional: true,
|
||||
},
|
||||
website: {
|
||||
type: 'string',
|
||||
description: 'Place website URL',
|
||||
optional: true,
|
||||
},
|
||||
phoneNumber: {
|
||||
type: 'string',
|
||||
description: 'Local formatted phone number',
|
||||
optional: true,
|
||||
},
|
||||
internationalPhoneNumber: {
|
||||
type: 'string',
|
||||
description: 'International formatted phone number',
|
||||
optional: true,
|
||||
},
|
||||
openNow: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the place is currently open',
|
||||
optional: true,
|
||||
},
|
||||
weekdayText: {
|
||||
type: 'array',
|
||||
description: 'Opening hours formatted by day of week',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
reviews: {
|
||||
type: 'array',
|
||||
description: 'User reviews (up to 5 most relevant)',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
authorName: { type: 'string', description: 'Reviewer name' },
|
||||
authorUrl: { type: 'string', description: 'Reviewer profile URL', optional: true },
|
||||
profilePhotoUrl: { type: 'string', description: 'Reviewer photo URL', optional: true },
|
||||
rating: { type: 'number', description: 'Rating given (1-5)' },
|
||||
text: { type: 'string', description: 'Review text' },
|
||||
time: { type: 'number', description: 'Review timestamp (Unix epoch)' },
|
||||
relativeTimeDescription: {
|
||||
type: 'string',
|
||||
description: 'Relative time (e.g., "a month ago")',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
photos: {
|
||||
type: 'array',
|
||||
description: 'Place photos',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
photoReference: { type: 'string', description: 'Photo reference for Place Photos API' },
|
||||
height: { type: 'number', description: 'Photo height in pixels' },
|
||||
width: { type: 'number', description: 'Photo width in pixels' },
|
||||
htmlAttributions: { type: 'array', description: 'Required attributions' },
|
||||
},
|
||||
},
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'Google Maps URL for the place',
|
||||
optional: true,
|
||||
},
|
||||
utcOffset: {
|
||||
type: 'number',
|
||||
description: 'UTC offset in minutes',
|
||||
optional: true,
|
||||
},
|
||||
vicinity: {
|
||||
type: 'string',
|
||||
description: 'Simplified address (neighborhood/street)',
|
||||
optional: true,
|
||||
},
|
||||
businessStatus: {
|
||||
type: 'string',
|
||||
description: 'Business status (OPERATIONAL, CLOSED_TEMPORARILY, CLOSED_PERMANENTLY)',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
170
apps/sim/tools/google_maps/places_search.ts
Normal file
170
apps/sim/tools/google_maps/places_search.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
import type {
|
||||
GoogleMapsPlacesSearchParams,
|
||||
GoogleMapsPlacesSearchResponse,
|
||||
} from '@/tools/google_maps/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const googleMapsPlacesSearchTool: ToolConfig<
|
||||
GoogleMapsPlacesSearchParams,
|
||||
GoogleMapsPlacesSearchResponse
|
||||
> = {
|
||||
id: 'google_maps_places_search',
|
||||
name: 'Google Maps Places Search',
|
||||
description: 'Search for places using a text query',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Google Maps API key',
|
||||
},
|
||||
query: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Search query (e.g., "restaurants in Times Square")',
|
||||
},
|
||||
location: {
|
||||
type: 'json',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Location to bias results towards ({lat, lng})',
|
||||
},
|
||||
radius: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Search radius in meters',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Place type filter (e.g., restaurant, cafe, hotel)',
|
||||
},
|
||||
language: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Language code for results (e.g., en, es, fr)',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Region bias as a ccTLD code (e.g., us, uk)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://maps.googleapis.com/maps/api/place/textsearch/json')
|
||||
url.searchParams.set('query', params.query.trim())
|
||||
url.searchParams.set('key', params.apiKey.trim())
|
||||
|
||||
if (params.location) {
|
||||
url.searchParams.set('location', `${params.location.lat},${params.location.lng}`)
|
||||
}
|
||||
if (params.radius) {
|
||||
url.searchParams.set('radius', params.radius.toString())
|
||||
}
|
||||
if (params.type) {
|
||||
url.searchParams.set('type', params.type)
|
||||
}
|
||||
if (params.language) {
|
||||
url.searchParams.set('language', params.language.trim())
|
||||
}
|
||||
if (params.region) {
|
||||
url.searchParams.set('region', params.region.trim())
|
||||
}
|
||||
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.status !== 'OK' && data.status !== 'ZERO_RESULTS') {
|
||||
throw new Error(
|
||||
`Places search failed: ${data.status} - ${data.error_message || 'Unknown error'}`
|
||||
)
|
||||
}
|
||||
|
||||
const places = (data.results || []).map(
|
||||
(place: {
|
||||
place_id: string
|
||||
name: string
|
||||
formatted_address: string
|
||||
geometry: { location: { lat: number; lng: number } }
|
||||
types: string[]
|
||||
rating?: number
|
||||
user_ratings_total?: number
|
||||
price_level?: number
|
||||
opening_hours?: { open_now: boolean }
|
||||
photos?: Array<{ photo_reference: string; height: number; width: number }>
|
||||
business_status?: string
|
||||
}) => ({
|
||||
placeId: place.place_id,
|
||||
name: place.name,
|
||||
formattedAddress: place.formatted_address,
|
||||
lat: place.geometry.location.lat,
|
||||
lng: place.geometry.location.lng,
|
||||
types: place.types ?? [],
|
||||
rating: place.rating ?? null,
|
||||
userRatingsTotal: place.user_ratings_total ?? null,
|
||||
priceLevel: place.price_level ?? null,
|
||||
openNow: place.opening_hours?.open_now ?? null,
|
||||
photoReference: place.photos?.[0]?.photo_reference ?? null,
|
||||
businessStatus: place.business_status ?? null,
|
||||
})
|
||||
)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
places,
|
||||
nextPageToken: data.next_page_token ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
places: {
|
||||
type: 'array',
|
||||
description: 'List of places found',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
placeId: { type: 'string', description: 'Google Place ID' },
|
||||
name: { type: 'string', description: 'Place name' },
|
||||
formattedAddress: { type: 'string', description: 'Formatted address' },
|
||||
lat: { type: 'number', description: 'Latitude' },
|
||||
lng: { type: 'number', description: 'Longitude' },
|
||||
types: { type: 'array', description: 'Place types' },
|
||||
rating: { type: 'number', description: 'Average rating (1-5)', optional: true },
|
||||
userRatingsTotal: { type: 'number', description: 'Number of ratings', optional: true },
|
||||
priceLevel: { type: 'number', description: 'Price level (0-4)', optional: true },
|
||||
openNow: { type: 'boolean', description: 'Whether currently open', optional: true },
|
||||
photoReference: {
|
||||
type: 'string',
|
||||
description: 'Photo reference for Photos API',
|
||||
optional: true,
|
||||
},
|
||||
businessStatus: { type: 'string', description: 'Business status', optional: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
nextPageToken: {
|
||||
type: 'string',
|
||||
description: 'Token for fetching the next page of results',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
117
apps/sim/tools/google_maps/reverse_geocode.ts
Normal file
117
apps/sim/tools/google_maps/reverse_geocode.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import type {
|
||||
GoogleMapsReverseGeocodeParams,
|
||||
GoogleMapsReverseGeocodeResponse,
|
||||
} from '@/tools/google_maps/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const googleMapsReverseGeocodeTool: ToolConfig<
|
||||
GoogleMapsReverseGeocodeParams,
|
||||
GoogleMapsReverseGeocodeResponse
|
||||
> = {
|
||||
id: 'google_maps_reverse_geocode',
|
||||
name: 'Google Maps Reverse Geocode',
|
||||
description:
|
||||
'Convert geographic coordinates (latitude and longitude) into a human-readable address',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Google Maps API key',
|
||||
},
|
||||
lat: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Latitude coordinate',
|
||||
},
|
||||
lng: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Longitude coordinate',
|
||||
},
|
||||
language: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Language code for results (e.g., en, es, fr)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://maps.googleapis.com/maps/api/geocode/json')
|
||||
url.searchParams.set('latlng', `${params.lat},${params.lng}`)
|
||||
url.searchParams.set('key', params.apiKey.trim())
|
||||
if (params.language) {
|
||||
url.searchParams.set('language', params.language.trim())
|
||||
}
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.status !== 'OK') {
|
||||
throw new Error(
|
||||
`Reverse geocoding failed: ${data.status} - ${data.error_message || 'Unknown error'}`
|
||||
)
|
||||
}
|
||||
|
||||
const result = data.results[0]
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
formattedAddress: result.formatted_address,
|
||||
placeId: result.place_id,
|
||||
addressComponents: (result.address_components || []).map(
|
||||
(comp: { long_name: string; short_name: string; types: string[] }) => ({
|
||||
longName: comp.long_name,
|
||||
shortName: comp.short_name,
|
||||
types: comp.types,
|
||||
})
|
||||
),
|
||||
types: result.types || [],
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
formattedAddress: {
|
||||
type: 'string',
|
||||
description: 'The formatted address string',
|
||||
},
|
||||
placeId: {
|
||||
type: 'string',
|
||||
description: 'Google Place ID for this location',
|
||||
},
|
||||
addressComponents: {
|
||||
type: 'array',
|
||||
description: 'Detailed address components',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
longName: { type: 'string', description: 'Full name of the component' },
|
||||
shortName: { type: 'string', description: 'Abbreviated name' },
|
||||
types: { type: 'array', description: 'Component types' },
|
||||
},
|
||||
},
|
||||
},
|
||||
types: {
|
||||
type: 'array',
|
||||
description: 'Address types (e.g., street_address, route)',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
113
apps/sim/tools/google_maps/snap_to_roads.ts
Normal file
113
apps/sim/tools/google_maps/snap_to_roads.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import type {
|
||||
GoogleMapsSnapToRoadsParams,
|
||||
GoogleMapsSnapToRoadsResponse,
|
||||
} from '@/tools/google_maps/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const googleMapsSnapToRoadsTool: ToolConfig<
|
||||
GoogleMapsSnapToRoadsParams,
|
||||
GoogleMapsSnapToRoadsResponse
|
||||
> = {
|
||||
id: 'google_maps_snap_to_roads',
|
||||
name: 'Google Maps Snap to Roads',
|
||||
description: 'Snap GPS coordinates to the nearest road segment',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Google Maps API key with Roads API enabled',
|
||||
},
|
||||
path: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Pipe-separated list of lat,lng coordinates (e.g., "60.170880,24.942795|60.170879,24.942796")',
|
||||
},
|
||||
interpolate: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether to interpolate additional points along the road',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://roads.googleapis.com/v1/snapToRoads')
|
||||
url.searchParams.set('path', params.path.trim())
|
||||
url.searchParams.set('key', params.apiKey.trim())
|
||||
if (params.interpolate !== undefined) {
|
||||
url.searchParams.set('interpolate', String(params.interpolate))
|
||||
}
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.error) {
|
||||
throw new Error(`Snap to Roads failed: ${data.error.message || 'Unknown error'}`)
|
||||
}
|
||||
|
||||
const snappedPoints = (data.snappedPoints || []).map(
|
||||
(point: {
|
||||
location: { latitude: number; longitude: number }
|
||||
originalIndex?: number
|
||||
placeId: string
|
||||
}) => ({
|
||||
location: {
|
||||
lat: point.location.latitude,
|
||||
lng: point.location.longitude,
|
||||
},
|
||||
originalIndex: point.originalIndex,
|
||||
placeId: point.placeId,
|
||||
})
|
||||
)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
snappedPoints,
|
||||
warningMessage: data.warningMessage || null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
snappedPoints: {
|
||||
type: 'array',
|
||||
description: 'Array of snapped points on roads',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
location: {
|
||||
type: 'object',
|
||||
description: 'Snapped location coordinates',
|
||||
properties: {
|
||||
lat: { type: 'number', description: 'Latitude' },
|
||||
lng: { type: 'number', description: 'Longitude' },
|
||||
},
|
||||
},
|
||||
originalIndex: {
|
||||
type: 'number',
|
||||
description: 'Index in the original path (if not interpolated)',
|
||||
},
|
||||
placeId: { type: 'string', description: 'Place ID for this road segment' },
|
||||
},
|
||||
},
|
||||
},
|
||||
warningMessage: {
|
||||
type: 'string',
|
||||
description: 'Warning message if any (e.g., if points could not be snapped)',
|
||||
},
|
||||
},
|
||||
}
|
||||
133
apps/sim/tools/google_maps/speed_limits.ts
Normal file
133
apps/sim/tools/google_maps/speed_limits.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import type {
|
||||
GoogleMapsSpeedLimitsParams,
|
||||
GoogleMapsSpeedLimitsResponse,
|
||||
} from '@/tools/google_maps/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const googleMapsSpeedLimitsTool: ToolConfig<
|
||||
GoogleMapsSpeedLimitsParams,
|
||||
GoogleMapsSpeedLimitsResponse
|
||||
> = {
|
||||
id: 'google_maps_speed_limits',
|
||||
name: 'Google Maps Speed Limits',
|
||||
description: 'Get speed limits for road segments',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Google Maps API key with Roads API enabled',
|
||||
},
|
||||
path: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Pipe-separated list of lat,lng coordinates (e.g., "60.170880,24.942795|60.170879,24.942796")',
|
||||
},
|
||||
placeIds: {
|
||||
type: 'array',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Array of Place IDs for road segments (alternative to path)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://roads.googleapis.com/v1/speedLimits')
|
||||
url.searchParams.set('key', params.apiKey.trim())
|
||||
|
||||
if (params.path) {
|
||||
url.searchParams.set('path', params.path.trim())
|
||||
}
|
||||
|
||||
if (params.placeIds && params.placeIds.length > 0) {
|
||||
for (const placeId of params.placeIds) {
|
||||
url.searchParams.append('placeId', placeId.trim())
|
||||
}
|
||||
}
|
||||
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.error) {
|
||||
throw new Error(`Speed Limits failed: ${data.error.message || 'Unknown error'}`)
|
||||
}
|
||||
|
||||
const speedLimits = (data.speedLimits || []).map(
|
||||
(limit: { placeId: string; speedLimit: number; units: 'KPH' | 'MPH' }) => ({
|
||||
placeId: limit.placeId,
|
||||
speedLimit: limit.speedLimit,
|
||||
units: limit.units,
|
||||
})
|
||||
)
|
||||
|
||||
const snappedPoints = (data.snappedPoints || []).map(
|
||||
(point: {
|
||||
location: { latitude: number; longitude: number }
|
||||
originalIndex?: number
|
||||
placeId: string
|
||||
}) => ({
|
||||
location: {
|
||||
lat: point.location.latitude,
|
||||
lng: point.location.longitude,
|
||||
},
|
||||
originalIndex: point.originalIndex,
|
||||
placeId: point.placeId,
|
||||
})
|
||||
)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
speedLimits,
|
||||
snappedPoints,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
speedLimits: {
|
||||
type: 'array',
|
||||
description: 'Array of speed limits for road segments',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
placeId: { type: 'string', description: 'Place ID for the road segment' },
|
||||
speedLimit: { type: 'number', description: 'Speed limit value' },
|
||||
units: { type: 'string', description: 'Speed limit units (KPH or MPH)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
snappedPoints: {
|
||||
type: 'array',
|
||||
description: 'Array of snapped points corresponding to the speed limits',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
location: {
|
||||
type: 'object',
|
||||
description: 'Snapped location coordinates',
|
||||
properties: {
|
||||
lat: { type: 'number', description: 'Latitude' },
|
||||
lng: { type: 'number', description: 'Longitude' },
|
||||
},
|
||||
},
|
||||
originalIndex: { type: 'number', description: 'Index in the original path' },
|
||||
placeId: { type: 'string', description: 'Place ID for this road segment' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
120
apps/sim/tools/google_maps/timezone.ts
Normal file
120
apps/sim/tools/google_maps/timezone.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import type {
|
||||
GoogleMapsTimezoneParams,
|
||||
GoogleMapsTimezoneResponse,
|
||||
} from '@/tools/google_maps/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const googleMapsTimezoneTool: ToolConfig<
|
||||
GoogleMapsTimezoneParams,
|
||||
GoogleMapsTimezoneResponse
|
||||
> = {
|
||||
id: 'google_maps_timezone',
|
||||
name: 'Google Maps Timezone',
|
||||
description: 'Get timezone information for a location',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Google Maps API key',
|
||||
},
|
||||
lat: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Latitude coordinate',
|
||||
},
|
||||
lng: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Longitude coordinate',
|
||||
},
|
||||
timestamp: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Unix timestamp to determine DST offset (defaults to current time)',
|
||||
},
|
||||
language: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Language code for timezone name (e.g., en, es, fr)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://maps.googleapis.com/maps/api/timezone/json')
|
||||
url.searchParams.set('location', `${params.lat},${params.lng}`)
|
||||
// Use provided timestamp or current time
|
||||
const timestamp = params.timestamp ?? Math.floor(Date.now() / 1000)
|
||||
url.searchParams.set('timestamp', timestamp.toString())
|
||||
url.searchParams.set('key', params.apiKey.trim())
|
||||
if (params.language) {
|
||||
url.searchParams.set('language', params.language.trim())
|
||||
}
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.status !== 'OK') {
|
||||
throw new Error(
|
||||
`Timezone request failed: ${data.status} - ${data.errorMessage || 'Unknown error'}`
|
||||
)
|
||||
}
|
||||
|
||||
// Calculate total offset
|
||||
const totalOffsetSeconds = data.rawOffset + data.dstOffset
|
||||
const totalOffsetHours = totalOffsetSeconds / 3600
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
timeZoneId: data.timeZoneId,
|
||||
timeZoneName: data.timeZoneName,
|
||||
rawOffset: data.rawOffset,
|
||||
dstOffset: data.dstOffset,
|
||||
totalOffsetSeconds,
|
||||
totalOffsetHours,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
timeZoneId: {
|
||||
type: 'string',
|
||||
description: 'IANA timezone ID (e.g., "America/New_York", "Europe/London")',
|
||||
},
|
||||
timeZoneName: {
|
||||
type: 'string',
|
||||
description: 'Localized timezone name (e.g., "Eastern Daylight Time")',
|
||||
},
|
||||
rawOffset: {
|
||||
type: 'number',
|
||||
description: 'UTC offset in seconds (without DST)',
|
||||
},
|
||||
dstOffset: {
|
||||
type: 'number',
|
||||
description: 'Daylight Saving Time offset in seconds (0 if not in DST)',
|
||||
},
|
||||
totalOffsetSeconds: {
|
||||
type: 'number',
|
||||
description: 'Total UTC offset in seconds (rawOffset + dstOffset)',
|
||||
},
|
||||
totalOffsetHours: {
|
||||
type: 'number',
|
||||
description: 'Total UTC offset in hours (e.g., -5 for EST, -4 for EDT)',
|
||||
},
|
||||
},
|
||||
}
|
||||
481
apps/sim/tools/google_maps/types.ts
Normal file
481
apps/sim/tools/google_maps/types.ts
Normal file
@@ -0,0 +1,481 @@
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
/**
|
||||
* Common location type
|
||||
*/
|
||||
export interface LatLng {
|
||||
lat: number
|
||||
lng: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Address component from geocoding
|
||||
*/
|
||||
export interface AddressComponent {
|
||||
longName: string
|
||||
shortName: string
|
||||
types: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Snapped point from Roads API
|
||||
*/
|
||||
export interface SnappedPoint {
|
||||
location: LatLng
|
||||
originalIndex?: number
|
||||
placeId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Speed limit info from Roads API
|
||||
*/
|
||||
export interface SpeedLimit {
|
||||
placeId: string
|
||||
speedLimit: number
|
||||
units: 'KPH' | 'MPH'
|
||||
}
|
||||
|
||||
/**
|
||||
* Cell tower info for geolocation
|
||||
*/
|
||||
export interface CellTower {
|
||||
cellId: number
|
||||
locationAreaCode: number
|
||||
mobileCountryCode: number
|
||||
mobileNetworkCode: number
|
||||
age?: number
|
||||
signalStrength?: number
|
||||
timingAdvance?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* WiFi access point info for geolocation
|
||||
*/
|
||||
export interface WifiAccessPoint {
|
||||
macAddress: string
|
||||
signalStrength?: number
|
||||
age?: number
|
||||
channel?: number
|
||||
signalToNoiseRatio?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Air quality index info
|
||||
*/
|
||||
export interface AirQualityIndex {
|
||||
code: string
|
||||
displayName: string
|
||||
aqi: number
|
||||
aqiDisplay: string
|
||||
color: {
|
||||
red: number
|
||||
green: number
|
||||
blue: number
|
||||
}
|
||||
category: string
|
||||
dominantPollutant: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Pollutant concentration info
|
||||
*/
|
||||
export interface Pollutant {
|
||||
code: string
|
||||
displayName: string
|
||||
fullName: string
|
||||
concentration: {
|
||||
value: number
|
||||
units: string
|
||||
}
|
||||
additionalInfo?: {
|
||||
sources: string
|
||||
effects: string
|
||||
}
|
||||
}
|
||||
|
||||
// Geocode
|
||||
// ============================================================================
|
||||
|
||||
export interface GoogleMapsGeocodeParams {
|
||||
apiKey: string
|
||||
address: string
|
||||
language?: string
|
||||
region?: string
|
||||
}
|
||||
|
||||
export interface GoogleMapsGeocodeResponse extends ToolResponse {
|
||||
output: {
|
||||
formattedAddress: string
|
||||
lat: number
|
||||
lng: number
|
||||
location: LatLng
|
||||
placeId: string
|
||||
addressComponents: AddressComponent[]
|
||||
locationType: string
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Reverse Geocode
|
||||
// ============================================================================
|
||||
|
||||
export interface GoogleMapsReverseGeocodeParams {
|
||||
apiKey: string
|
||||
lat: number
|
||||
lng: number
|
||||
language?: string
|
||||
}
|
||||
|
||||
export interface GoogleMapsReverseGeocodeResponse extends ToolResponse {
|
||||
output: {
|
||||
formattedAddress: string
|
||||
placeId: string
|
||||
addressComponents: AddressComponent[]
|
||||
types: string[]
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Directions
|
||||
// ============================================================================
|
||||
|
||||
export interface DirectionsStep {
|
||||
instruction: string
|
||||
distanceText: string
|
||||
distanceMeters: number
|
||||
durationText: string
|
||||
durationSeconds: number
|
||||
startLocation: LatLng
|
||||
endLocation: LatLng
|
||||
travelMode: string
|
||||
maneuver: string | null
|
||||
}
|
||||
|
||||
export interface DirectionsLeg {
|
||||
startAddress: string
|
||||
endAddress: string
|
||||
startLocation: LatLng
|
||||
endLocation: LatLng
|
||||
distanceText: string
|
||||
distanceMeters: number
|
||||
durationText: string
|
||||
durationSeconds: number
|
||||
steps: DirectionsStep[]
|
||||
}
|
||||
|
||||
export interface DirectionsRoute {
|
||||
summary: string
|
||||
legs: DirectionsLeg[]
|
||||
overviewPolyline: string
|
||||
warnings: string[]
|
||||
waypointOrder: number[]
|
||||
}
|
||||
|
||||
export interface GoogleMapsDirectionsParams {
|
||||
apiKey: string
|
||||
origin: string
|
||||
destination: string
|
||||
mode?: 'driving' | 'walking' | 'bicycling' | 'transit'
|
||||
avoid?: string
|
||||
waypoints?: string[]
|
||||
units?: 'metric' | 'imperial'
|
||||
language?: string
|
||||
}
|
||||
|
||||
export interface GoogleMapsDirectionsResponse extends ToolResponse {
|
||||
output: {
|
||||
routes: DirectionsRoute[]
|
||||
distanceText: string
|
||||
distanceMeters: number
|
||||
durationText: string
|
||||
durationSeconds: number
|
||||
startAddress: string
|
||||
endAddress: string
|
||||
steps: DirectionsStep[]
|
||||
polyline: string
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Distance Matrix
|
||||
// ============================================================================
|
||||
|
||||
export interface DistanceMatrixElement {
|
||||
distanceText: string
|
||||
distanceMeters: number
|
||||
durationText: string
|
||||
durationSeconds: number
|
||||
durationInTrafficText: string | null
|
||||
durationInTrafficSeconds: number | null
|
||||
status: string
|
||||
}
|
||||
|
||||
export interface DistanceMatrixRow {
|
||||
elements: DistanceMatrixElement[]
|
||||
}
|
||||
|
||||
export interface GoogleMapsDistanceMatrixParams {
|
||||
apiKey: string
|
||||
origin: string
|
||||
destinations: string[]
|
||||
mode?: 'driving' | 'walking' | 'bicycling' | 'transit'
|
||||
avoid?: string
|
||||
units?: 'metric' | 'imperial'
|
||||
language?: string
|
||||
}
|
||||
|
||||
export interface GoogleMapsDistanceMatrixResponse extends ToolResponse {
|
||||
output: {
|
||||
originAddresses: string[]
|
||||
destinationAddresses: string[]
|
||||
rows: DistanceMatrixRow[]
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Places Search
|
||||
// ============================================================================
|
||||
|
||||
export interface PlaceResult {
|
||||
placeId: string
|
||||
name: string
|
||||
formattedAddress: string
|
||||
lat: number
|
||||
lng: number
|
||||
types: string[]
|
||||
rating: number | null
|
||||
userRatingsTotal: number | null
|
||||
priceLevel: number | null
|
||||
openNow: boolean | null
|
||||
photoReference: string | null
|
||||
businessStatus: string | null
|
||||
}
|
||||
|
||||
export interface GoogleMapsPlacesSearchParams {
|
||||
apiKey: string
|
||||
query: string
|
||||
location?: LatLng
|
||||
radius?: number
|
||||
type?: string
|
||||
language?: string
|
||||
region?: string
|
||||
}
|
||||
|
||||
export interface GoogleMapsPlacesSearchResponse extends ToolResponse {
|
||||
output: {
|
||||
places: PlaceResult[]
|
||||
nextPageToken: string | null
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Place Details
|
||||
// ============================================================================
|
||||
|
||||
export interface PlaceReview {
|
||||
authorName: string
|
||||
authorUrl: string | null
|
||||
profilePhotoUrl: string | null
|
||||
rating: number
|
||||
text: string
|
||||
time: number
|
||||
relativeTimeDescription: string
|
||||
}
|
||||
|
||||
export interface PlacePhoto {
|
||||
photoReference: string
|
||||
height: number
|
||||
width: number
|
||||
htmlAttributions: string[]
|
||||
}
|
||||
|
||||
export interface GoogleMapsPlaceDetailsParams {
|
||||
apiKey: string
|
||||
placeId: string
|
||||
fields?: string
|
||||
language?: string
|
||||
}
|
||||
|
||||
export interface GoogleMapsPlaceDetailsResponse extends ToolResponse {
|
||||
output: {
|
||||
placeId: string
|
||||
name: string | null
|
||||
formattedAddress: string | null
|
||||
lat: number | null
|
||||
lng: number | null
|
||||
types: string[]
|
||||
rating: number | null
|
||||
userRatingsTotal: number | null
|
||||
priceLevel: number | null
|
||||
website: string | null
|
||||
phoneNumber: string | null
|
||||
internationalPhoneNumber: string | null
|
||||
openNow: boolean | null
|
||||
weekdayText: string[]
|
||||
reviews: PlaceReview[]
|
||||
photos: PlacePhoto[]
|
||||
url: string | null
|
||||
utcOffset: number | null
|
||||
vicinity: string | null
|
||||
businessStatus: string | null
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Elevation
|
||||
// ============================================================================
|
||||
|
||||
export interface GoogleMapsElevationParams {
|
||||
apiKey: string
|
||||
lat: number
|
||||
lng: number
|
||||
}
|
||||
|
||||
export interface GoogleMapsElevationResponse extends ToolResponse {
|
||||
output: {
|
||||
elevation: number
|
||||
lat: number
|
||||
lng: number
|
||||
resolution: number
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Timezone
|
||||
// ============================================================================
|
||||
|
||||
export interface GoogleMapsTimezoneParams {
|
||||
apiKey: string
|
||||
lat: number
|
||||
lng: number
|
||||
timestamp?: number
|
||||
language?: string
|
||||
}
|
||||
|
||||
export interface GoogleMapsTimezoneResponse extends ToolResponse {
|
||||
output: {
|
||||
timeZoneId: string
|
||||
timeZoneName: string
|
||||
rawOffset: number
|
||||
dstOffset: number
|
||||
totalOffsetSeconds: number
|
||||
totalOffsetHours: number
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Snap to Roads
|
||||
// ============================================================================
|
||||
|
||||
export interface GoogleMapsSnapToRoadsParams {
|
||||
apiKey: string
|
||||
path: string
|
||||
interpolate?: boolean
|
||||
}
|
||||
|
||||
export interface GoogleMapsSnapToRoadsResponse extends ToolResponse {
|
||||
output: {
|
||||
snappedPoints: SnappedPoint[]
|
||||
warningMessage: string | null
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Speed Limits
|
||||
// ============================================================================
|
||||
|
||||
export interface GoogleMapsSpeedLimitsParams {
|
||||
apiKey: string
|
||||
path?: string
|
||||
placeIds?: string[]
|
||||
}
|
||||
|
||||
export interface GoogleMapsSpeedLimitsResponse extends ToolResponse {
|
||||
output: {
|
||||
speedLimits: SpeedLimit[]
|
||||
snappedPoints: SnappedPoint[]
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Validate Address
|
||||
// ============================================================================
|
||||
|
||||
export interface GoogleMapsValidateAddressParams {
|
||||
apiKey: string
|
||||
address: string
|
||||
regionCode?: string
|
||||
locality?: string
|
||||
enableUspsCass?: boolean
|
||||
}
|
||||
|
||||
export interface GoogleMapsValidateAddressResponse extends ToolResponse {
|
||||
output: {
|
||||
formattedAddress: string
|
||||
lat: number
|
||||
lng: number
|
||||
placeId: string
|
||||
addressComplete: boolean
|
||||
hasUnconfirmedComponents: boolean
|
||||
hasInferredComponents: boolean
|
||||
hasReplacedComponents: boolean
|
||||
validationGranularity: string
|
||||
geocodeGranularity: string
|
||||
addressComponents: AddressComponent[]
|
||||
missingComponentTypes: string[]
|
||||
unconfirmedComponentTypes: string[]
|
||||
unresolvedTokens: string[]
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Geolocate
|
||||
// ============================================================================
|
||||
|
||||
export interface GoogleMapsGeolocateParams {
|
||||
apiKey: string
|
||||
homeMobileCountryCode?: number
|
||||
homeMobileNetworkCode?: number
|
||||
radioType?: 'lte' | 'gsm' | 'cdma' | 'wcdma' | 'nr'
|
||||
carrier?: string
|
||||
considerIp?: boolean
|
||||
cellTowers?: CellTower[]
|
||||
wifiAccessPoints?: WifiAccessPoint[]
|
||||
}
|
||||
|
||||
export interface GoogleMapsGeolocateResponse extends ToolResponse {
|
||||
output: {
|
||||
lat: number
|
||||
lng: number
|
||||
accuracy: number
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Air Quality
|
||||
// ============================================================================
|
||||
|
||||
export interface GoogleMapsAirQualityParams {
|
||||
apiKey: string
|
||||
lat: number
|
||||
lng: number
|
||||
languageCode?: string
|
||||
}
|
||||
|
||||
export interface GoogleMapsAirQualityResponse extends ToolResponse {
|
||||
output: {
|
||||
dateTime: string
|
||||
regionCode: string
|
||||
indexes: AirQualityIndex[]
|
||||
pollutants: Pollutant[]
|
||||
healthRecommendations: {
|
||||
generalPopulation: string
|
||||
elderly: string
|
||||
lungDiseasePopulation: string
|
||||
heartDiseasePopulation: string
|
||||
athletes: string
|
||||
pregnantWomen: string
|
||||
children: string
|
||||
} | null
|
||||
}
|
||||
}
|
||||
194
apps/sim/tools/google_maps/validate_address.ts
Normal file
194
apps/sim/tools/google_maps/validate_address.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
import type {
|
||||
GoogleMapsValidateAddressParams,
|
||||
GoogleMapsValidateAddressResponse,
|
||||
} from '@/tools/google_maps/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const googleMapsValidateAddressTool: ToolConfig<
|
||||
GoogleMapsValidateAddressParams,
|
||||
GoogleMapsValidateAddressResponse
|
||||
> = {
|
||||
id: 'google_maps_validate_address',
|
||||
name: 'Google Maps Validate Address',
|
||||
description: 'Validate and standardize a postal address',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Google Maps API key with Address Validation API enabled',
|
||||
},
|
||||
address: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The address to validate (as a single string)',
|
||||
},
|
||||
regionCode: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ISO 3166-1 alpha-2 country code (e.g., "US", "CA")',
|
||||
},
|
||||
locality: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'City or locality name',
|
||||
},
|
||||
enableUspsCass: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Enable USPS CASS validation for US addresses',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
return `https://addressvalidation.googleapis.com/v1:validateAddress?key=${params.apiKey.trim()}`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: {
|
||||
address: { addressLines: string[]; regionCode?: string; locality?: string }
|
||||
enableUspsCass?: boolean
|
||||
} = {
|
||||
address: {
|
||||
addressLines: [params.address.trim()],
|
||||
},
|
||||
}
|
||||
|
||||
if (params.regionCode) {
|
||||
body.address.regionCode = params.regionCode.trim()
|
||||
}
|
||||
|
||||
if (params.locality) {
|
||||
body.address.locality = params.locality.trim()
|
||||
}
|
||||
|
||||
if (params.enableUspsCass !== undefined) {
|
||||
body.enableUspsCass = params.enableUspsCass
|
||||
}
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.error) {
|
||||
throw new Error(`Address Validation failed: ${data.error.message || 'Unknown error'}`)
|
||||
}
|
||||
|
||||
const result = data.result
|
||||
const verdict = result?.verdict || {}
|
||||
const address = result?.address || {}
|
||||
const geocode = result?.geocode || {}
|
||||
|
||||
const addressComponents = (address.addressComponents || []).map(
|
||||
(comp: {
|
||||
componentName: { text: string; languageCode?: string }
|
||||
componentType: string
|
||||
confirmationLevel: string
|
||||
}) => ({
|
||||
longName: comp.componentName?.text || '',
|
||||
shortName: comp.componentName?.text || '',
|
||||
types: [comp.componentType],
|
||||
})
|
||||
)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
formattedAddress: address.formattedAddress || '',
|
||||
lat: geocode.location?.latitude || 0,
|
||||
lng: geocode.location?.longitude || 0,
|
||||
placeId: geocode.placeId || '',
|
||||
addressComplete: verdict.addressComplete || false,
|
||||
hasUnconfirmedComponents: verdict.hasUnconfirmedComponents || false,
|
||||
hasInferredComponents: verdict.hasInferredComponents || false,
|
||||
hasReplacedComponents: verdict.hasReplacedComponents || false,
|
||||
validationGranularity: verdict.validationGranularity || '',
|
||||
geocodeGranularity: verdict.geocodeGranularity || '',
|
||||
addressComponents,
|
||||
missingComponentTypes: address.missingComponentTypes || [],
|
||||
unconfirmedComponentTypes: address.unconfirmedComponentTypes || [],
|
||||
unresolvedTokens: address.unresolvedTokens || [],
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
formattedAddress: {
|
||||
type: 'string',
|
||||
description: 'The standardized formatted address',
|
||||
},
|
||||
lat: {
|
||||
type: 'number',
|
||||
description: 'Latitude coordinate',
|
||||
},
|
||||
lng: {
|
||||
type: 'number',
|
||||
description: 'Longitude coordinate',
|
||||
},
|
||||
placeId: {
|
||||
type: 'string',
|
||||
description: 'Google Place ID for this address',
|
||||
},
|
||||
addressComplete: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the address is complete and deliverable',
|
||||
},
|
||||
hasUnconfirmedComponents: {
|
||||
type: 'boolean',
|
||||
description: 'Whether some address components could not be confirmed',
|
||||
},
|
||||
hasInferredComponents: {
|
||||
type: 'boolean',
|
||||
description: 'Whether some components were inferred (not in input)',
|
||||
},
|
||||
hasReplacedComponents: {
|
||||
type: 'boolean',
|
||||
description: 'Whether some components were replaced with canonical values',
|
||||
},
|
||||
validationGranularity: {
|
||||
type: 'string',
|
||||
description: 'Granularity of validation (PREMISE, SUB_PREMISE, ROUTE, etc.)',
|
||||
},
|
||||
geocodeGranularity: {
|
||||
type: 'string',
|
||||
description: 'Granularity of the geocode result',
|
||||
},
|
||||
addressComponents: {
|
||||
type: 'array',
|
||||
description: 'Detailed address components',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
longName: { type: 'string', description: 'Full name of the component' },
|
||||
shortName: { type: 'string', description: 'Abbreviated name' },
|
||||
types: { type: 'array', description: 'Component types' },
|
||||
},
|
||||
},
|
||||
},
|
||||
missingComponentTypes: {
|
||||
type: 'array',
|
||||
description: 'Types of address components that are missing',
|
||||
},
|
||||
unconfirmedComponentTypes: {
|
||||
type: 'array',
|
||||
description: 'Types of components that could not be confirmed',
|
||||
},
|
||||
unresolvedTokens: {
|
||||
type: 'array',
|
||||
description: 'Input tokens that could not be resolved',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createHmac } from 'crypto'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import type { RequestResponse, WebhookRequestParams } from '@/tools/http/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { RequestResponse, WebhookRequestParams } from './types'
|
||||
|
||||
/**
|
||||
* Generates HMAC-SHA256 signature for webhook payload
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type {
|
||||
IncidentioIncidentStatusesListParams,
|
||||
IncidentioIncidentStatusesListResponse,
|
||||
} from './types'
|
||||
} from '@/tools/incidentio/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const incidentStatusesListTool: ToolConfig<
|
||||
IncidentioIncidentStatusesListParams,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type {
|
||||
IncidentioIncidentTypesListParams,
|
||||
IncidentioIncidentTypesListResponse,
|
||||
} from './types'
|
||||
} from '@/tools/incidentio/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const incidentTypesListTool: ToolConfig<
|
||||
IncidentioIncidentTypesListParams,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type {
|
||||
IncidentioSeveritiesListParams,
|
||||
IncidentioSeveritiesListResponse,
|
||||
} from '@/tools/incidentio/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { IncidentioSeveritiesListParams, IncidentioSeveritiesListResponse } from './types'
|
||||
|
||||
export const severitiesListTool: ToolConfig<
|
||||
IncidentioSeveritiesListParams,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type {
|
||||
IncidentioUsersListParams,
|
||||
IncidentioUsersListResponse,
|
||||
} from '@/tools/incidentio/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { IncidentioUsersListParams, IncidentioUsersListResponse } from './types'
|
||||
|
||||
export const usersListTool: ToolConfig<IncidentioUsersListParams, IncidentioUsersListResponse> = {
|
||||
id: 'incidentio_users_list',
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type {
|
||||
IncidentioUsersShowParams,
|
||||
IncidentioUsersShowResponse,
|
||||
} from '@/tools/incidentio/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { IncidentioUsersShowParams, IncidentioUsersShowResponse } from './types'
|
||||
|
||||
export const usersShowTool: ToolConfig<IncidentioUsersShowParams, IncidentioUsersShowResponse> = {
|
||||
id: 'incidentio_users_show',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { WorkflowsCreateParams, WorkflowsCreateResponse } from '@/tools/incidentio/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { WorkflowsCreateParams, WorkflowsCreateResponse } from './types'
|
||||
|
||||
export const workflowsCreateTool: ToolConfig<WorkflowsCreateParams, WorkflowsCreateResponse> = {
|
||||
id: 'incidentio_workflows_create',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { WorkflowsDeleteParams, WorkflowsDeleteResponse } from '@/tools/incidentio/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { WorkflowsDeleteParams, WorkflowsDeleteResponse } from './types'
|
||||
|
||||
export const workflowsDeleteTool: ToolConfig<WorkflowsDeleteParams, WorkflowsDeleteResponse> = {
|
||||
id: 'incidentio_workflows_delete',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { WorkflowsListParams, WorkflowsListResponse } from '@/tools/incidentio/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { WorkflowsListParams, WorkflowsListResponse } from './types'
|
||||
|
||||
export const workflowsListTool: ToolConfig<WorkflowsListParams, WorkflowsListResponse> = {
|
||||
id: 'incidentio_workflows_list',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { WorkflowsShowParams, WorkflowsShowResponse } from '@/tools/incidentio/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { WorkflowsShowParams, WorkflowsShowResponse } from './types'
|
||||
|
||||
export const workflowsShowTool: ToolConfig<WorkflowsShowParams, WorkflowsShowResponse> = {
|
||||
id: 'incidentio_workflows_show',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { WorkflowsUpdateParams, WorkflowsUpdateResponse } from '@/tools/incidentio/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { WorkflowsUpdateParams, WorkflowsUpdateResponse } from './types'
|
||||
|
||||
export const workflowsUpdateTool: ToolConfig<WorkflowsUpdateParams, WorkflowsUpdateResponse> = {
|
||||
id: 'incidentio_workflows_update',
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { buildIntercomUrl, handleIntercomError } from '@/tools/intercom/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('IntercomGetCompany')
|
||||
|
||||
export interface IntercomGetCompanyParams {
|
||||
accessToken: string
|
||||
companyId: string
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { buildIntercomUrl, handleIntercomError } from '@/tools/intercom/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('IntercomGetConversation')
|
||||
|
||||
export interface IntercomGetConversationParams {
|
||||
accessToken: string
|
||||
conversationId: string
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { buildIntercomUrl, handleIntercomError } from '@/tools/intercom/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('IntercomListCompanies')
|
||||
|
||||
export interface IntercomListCompaniesParams {
|
||||
accessToken: string
|
||||
per_page?: number
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { buildIntercomUrl, handleIntercomError } from '@/tools/intercom/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('IntercomListContacts')
|
||||
|
||||
export interface IntercomListContactsParams {
|
||||
accessToken: string
|
||||
per_page?: number
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user