Compare commits

...

8 Commits

Author SHA1 Message Date
Waleed
9b2490c4b1 v0.4.23: webflow tools + triggers, copilot api key fix (#1723)
* fix(debug-mode): remove duplicate debug mode flag (#1714)

* feat(i18n): update translations (#1709)

* improvement(condition): added variable and envvar highlighting for condition input (#1718)

* fix(dashboard): add additional context for paginated logs in dashboard, add empty state when selected cell has no data (#1719)

* fix(dashboard): add additional context for paginated logs in dashboard, add empty state when selected cell has no data

* apps/sim

* renaming

* remove relative import

* feat(tools): added webflow OAuth + tools (#1720)

* feat(tools): added webflow OAuth + tools

* remove itemId from delete item

* remove siteId

* added webhook triggers + oauth scopes + site/collection selector

* update sample payload for webflow triggers

* cleanup

* fix discord color

* feat(i18n): update translations (#1721)

* improvement(schedule): fix UI bug with schedule modal (#1722)
2025-10-23 14:48:09 -07:00
Waleed
dba7514350 improvement(schedule): fix UI bug with schedule modal (#1722) 2025-10-23 14:16:59 -07:00
Waleed
e94de1dd26 feat(i18n): update translations (#1721) 2025-10-23 13:15:05 -07:00
Waleed
a4e874b266 feat(tools): added webflow OAuth + tools (#1720)
* feat(tools): added webflow OAuth + tools

* remove itemId from delete item

* remove siteId

* added webhook triggers + oauth scopes + site/collection selector

* update sample payload for webflow triggers

* cleanup

* fix discord color
2025-10-23 13:08:32 -07:00
Waleed
ec034f3fc7 fix(dashboard): add additional context for paginated logs in dashboard, add empty state when selected cell has no data (#1719)
* fix(dashboard): add additional context for paginated logs in dashboard, add empty state when selected cell has no data

* apps/sim

* renaming

* remove relative import
2025-10-22 14:07:52 -07:00
Waleed
e425d064c0 improvement(condition): added variable and envvar highlighting for condition input (#1718) 2025-10-22 13:12:09 -07:00
Waleed
bcd1a2faf6 feat(i18n): update translations (#1709) 2025-10-22 12:55:36 -07:00
Vikhyath Mondreti
989a77261c fix(debug-mode): remove duplicate debug mode flag (#1714) 2025-10-22 00:49:18 -10:00
107 changed files with 3770 additions and 525 deletions

View File

@@ -72,6 +72,7 @@ Eine Nachricht an einen Discord-Kanal senden
| `channelId` | string | Ja | Die Discord-Kanal-ID, an die die Nachricht gesendet werden soll |
| `content` | string | Nein | Der Textinhalt der Nachricht |
| `serverId` | string | Ja | Die Discord-Server-ID \(Guild-ID\) |
| `files` | file[] | Nein | Dateien, die an die Nachricht angehängt werden sollen |
#### Ausgabe

View File

@@ -85,10 +85,11 @@ Eine Datei zu Google Drive hochladen
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `fileName` | string | Ja | Der Name der hochzuladenden Datei |
| `content` | string | Ja | Der Inhalt der hochzuladenden Datei |
| `mimeType` | string | Nein | Der MIME-Typ der hochzuladenden Datei |
| `file` | file | Nein | Binärdatei zum Hochladen (UserFile-Objekt) |
| `content` | string | Nein | Textinhalt zum Hochladen (verwenden Sie entweder diesen ODER file, nicht beides) |
| `mimeType` | string | Nein | Der MIME-Typ der hochzuladenden Datei (wird automatisch aus der Datei erkannt, wenn nicht angegeben) |
| `folderSelector` | string | Nein | Wählen Sie den Ordner aus, in den die Datei hochgeladen werden soll |
| `folderId` | string | Nein | Die ID des Ordners, in den die Datei hochgeladen werden soll \(interne Verwendung\) |
| `folderId` | string | Nein | Die ID des Ordners, in den die Datei hochgeladen werden soll (interne Verwendung) |
#### Ausgabe

View File

@@ -135,6 +135,7 @@ Inhalte in einem Microsoft Teams-Chat schreiben oder aktualisieren
| --------- | ---- | -------- | ----------- |
| `chatId` | string | Ja | Die ID des Chats, in den geschrieben werden soll |
| `content` | string | Ja | Der Inhalt, der in die Nachricht geschrieben werden soll |
| `files` | file[] | Nein | Dateien, die der Nachricht angehängt werden sollen |
#### Ausgabe
@@ -181,9 +182,10 @@ Schreiben oder senden einer Nachricht an einen Microsoft Teams-Kanal
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `teamId` | string | Ja | Die ID des Teams, an das geschrieben werden soll |
| `channelId` | string | Ja | Die ID des Kanals, an den geschrieben werden soll |
| `content` | string | Ja | Der Inhalt, der an den Kanal gesendet werden soll |
| `teamId` | string | Ja | Die ID des Teams, in das geschrieben werden soll |
| `channelId` | string | Ja | Die ID des Kanals, in den geschrieben werden soll |
| `content` | string | Ja | Der Inhalt, der in den Kanal geschrieben werden soll |
| `files` | file[] | Nein | Dateien, die der Nachricht angehängt werden sollen |
#### Ausgabe

View File

@@ -63,9 +63,10 @@ Eine Datei auf OneDrive hochladen
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `fileName` | string | Ja | Der Name der hochzuladenden Datei |
| `content` | string | Ja | Der Inhalt der hochzuladenden Datei |
| `file` | file | Nein | Die hochzuladende Datei (binär) |
| `content` | string | Nein | Der hochzuladende Textinhalt (falls keine Datei bereitgestellt wird) |
| `folderSelector` | string | Nein | Wählen Sie den Ordner aus, in den die Datei hochgeladen werden soll |
| `manualFolderId` | string | Nein | Manuell eingegebene Ordner-ID \(erweiterter Modus\) |
| `manualFolderId` | string | Nein | Manuell eingegebene Ordner-ID (erweiterter Modus) |
#### Ausgabe

View File

@@ -154,10 +154,11 @@ E-Mails über Outlook versenden
| `to` | string | Ja | E-Mail-Adresse des Empfängers |
| `subject` | string | Ja | E-Mail-Betreff |
| `body` | string | Ja | E-Mail-Inhalt |
| `replyToMessageId` | string | Nein | Nachrichten-ID für Antworten \(für Threading\) |
| `replyToMessageId` | string | Nein | Nachrichten-ID, auf die geantwortet wird \(für Threading\) |
| `conversationId` | string | Nein | Konversations-ID für Threading |
| `cc` | string | Nein | CC-Empfänger \(durch Kommas getrennt\) |
| `bcc` | string | Nein | BCC-Empfänger \(durch Kommas getrennt\) |
| `attachments` | file[] | Nein | Dateien, die an die E-Mail angehängt werden sollen |
#### Ausgabe
@@ -177,10 +178,11 @@ E-Mails mit Outlook erstellen
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `to` | string | Ja | E-Mail-Adresse des Empfängers |
| `subject` | string | Ja | Betreff der E-Mail |
| `body` | string | Ja | Inhalt der E-Mail |
| `cc` | string | Nein | CC-Empfänger \(durch Komma getrennt\) |
| `bcc` | string | Nein | BCC-Empfänger \(durch Komma getrennt\) |
| `subject` | string | Ja | E-Mail-Betreff |
| `body` | string | Ja | E-Mail-Inhalt |
| `cc` | string | Nein | CC-Empfänger \(durch Kommas getrennt\) |
| `bcc` | string | Nein | BCC-Empfänger \(durch Kommas getrennt\) |
| `attachments` | file[] | Nein | Dateien, die an den E-Mail-Entwurf angehängt werden sollen |
#### Ausgabe

View File

@@ -199,6 +199,26 @@ Ein neues Element zu einer SharePoint-Liste hinzufügen
| --------- | ---- | ----------- |
| `item` | object | Erstelltes SharePoint-Listenelement |
### `sharepoint_upload_file`
Dateien in eine SharePoint-Dokumentenbibliothek hochladen
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `siteId` | string | Nein | Die ID der SharePoint-Website |
| `driveId` | string | Nein | Die ID der Dokumentenbibliothek (Laufwerk). Wenn nicht angegeben, wird das Standardlaufwerk verwendet. |
| `folderPath` | string | Nein | Optionaler Ordnerpfad innerhalb der Dokumentenbibliothek (z.B. /Documents/Subfolder) |
| `fileName` | string | Nein | Optional: Überschreiben des hochgeladenen Dateinamens |
| `files` | file[] | Nein | Dateien, die nach SharePoint hochgeladen werden sollen |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `uploadedFiles` | array | Array von hochgeladenen Dateiobjekten |
## Hinweise
- Kategorie: `tools`

View File

@@ -78,7 +78,8 @@ Senden Sie Nachrichten an Slack-Kanäle oder Benutzer über die Slack-API. Unter
| `authMethod` | string | Nein | Authentifizierungsmethode: oauth oder bot_token |
| `botToken` | string | Nein | Bot-Token für Custom Bot |
| `channel` | string | Ja | Ziel-Slack-Kanal \(z.B. #general\) |
| `text` | string | Ja | Zu sendender Nachrichtentext \(unterstützt Slack mrkdwn-Formatierung\) |
| `text` | string | Ja | Nachrichtentext zum Senden \(unterstützt Slack mrkdwn-Formatierung\) |
| `files` | file[] | Nein | Dateien, die an die Nachricht angehängt werden sollen |
#### Ausgabe

View File

@@ -202,6 +202,28 @@ Daten in eine Supabase-Tabelle einfügen oder aktualisieren (Upsert-Operation)
| `message` | string | Statusmeldung der Operation |
| `results` | array | Array der eingefügten/aktualisierten Datensätze |
### `supabase_vector_search`
Ähnlichkeitssuche mit pgvector in einer Supabase-Tabelle durchführen
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `projectId` | string | Ja | Ihre Supabase-Projekt-ID \(z.B. jdrkgepadsdopsntdlom\) |
| `functionName` | string | Ja | Der Name der PostgreSQL-Funktion, die die Vektorsuche durchführt \(z.B. match_documents\) |
| `queryEmbedding` | array | Ja | Der Abfragevektor/Embedding, nach dem ähnliche Elemente gesucht werden sollen |
| `matchThreshold` | number | Nein | Minimaler Ähnlichkeitsschwellenwert \(0-1\), typischerweise 0,7-0,9 |
| `matchCount` | number | Nein | Maximale Anzahl der zurückzugebenden Ergebnisse \(Standard: 10\) |
| `apiKey` | string | Ja | Ihr Supabase Service Role Secret Key |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `message` | string | Statusmeldung der Operation |
| `results` | array | Array von Datensätzen mit Ähnlichkeitswerten aus der Vektorsuche. Jeder Datensatz enthält ein Ähnlichkeitsfeld \(0-1\), das angibt, wie ähnlich er dem Abfragevektor ist. |
## Hinweise
- Kategorie: `tools`

View File

@@ -190,6 +190,26 @@ Senden Sie Animationen (GIFs) an Telegram-Kanäle oder Benutzer über die Telegr
| `message` | string | Erfolgs- oder Fehlermeldung |
| `data` | object | Telegram-Nachrichtendaten einschließlich optionaler Medien |
### `telegram_send_document`
Senden Sie Dokumente (PDF, ZIP, DOC, etc.) an Telegram-Kanäle oder -Nutzer über die Telegram Bot API.
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `botToken` | string | Ja | Ihr Telegram Bot API-Token |
| `chatId` | string | Ja | Ziel-Telegram-Chat-ID |
| `files` | file[] | Nein | Zu sendende Dokumentdatei \(PDF, ZIP, DOC, etc.\). Maximale Größe: 50MB |
| `caption` | string | Nein | Dokumentbeschreibung \(optional\) |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `message` | string | Erfolgs- oder Fehlermeldung |
| `data` | object | Telegram-Nachrichtendaten einschließlich Dokument |
## Hinweise
- Kategorie: `tools`

View File

@@ -59,8 +59,9 @@ Verarbeiten und analysieren Sie Bilder mit fortschrittlichen Vision-Modellen. F
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Ja | API-Schlüssel für den ausgewählten Modellanbieter |
| `imageUrl` | string | Ja | Öffentlich zugängliche Bild-URL |
| `model` | string | Nein | Zu verwendendes Vision-Modell \(gpt-4o, claude-3-opus-20240229, etc\) |
| `imageUrl` | string | Nein | Öffentlich zugängliche Bild-URL |
| `imageFile` | file | Nein | Zu analysierende Bilddatei |
| `model` | string | Nein | Zu verwendendes Vision-Modell \(gpt-4o, claude-3-opus-20240229, usw.\) |
| `prompt` | string | Nein | Benutzerdefinierte Eingabeaufforderung für die Bildanalyse |
#### Ausgabe

View File

@@ -0,0 +1,145 @@
---
title: Webflow
description: Webflow CMS-Sammlungen verwalten
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="webflow"
color="#E0E0E0"
icon={true}
iconSvg={`<svg className="block-icon"
viewBox='0 0 1080 674'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M1080 0L735.386 673.684H411.695L555.916 394.481H549.445C430.464 548.934 252.942 650.61 -0.000488281 673.684V398.344C-0.000488281 398.344 161.813 388.787 256.938 288.776H-0.000488281V0.0053214H288.771V237.515L295.252 237.489L413.254 0.0053214H631.644V236.009L638.126 235.999L760.555 0H1080Z'
fill='#146EF5'
/>
</svg>`}
/>
{/* MANUAL-CONTENT-START:intro */}
[Webflow](https://webflow.com/) ist eine leistungsstarke visuelle Webdesign-Plattform, mit der Sie responsive Websites ohne Programmierung erstellen können. Sie kombiniert eine visuelle Design-Oberfläche mit einem robusten CMS (Content Management System), das es Ihnen ermöglicht, dynamische Inhalte für Ihre Websites zu erstellen, zu verwalten und zu veröffentlichen.
Mit Webflow können Sie:
- **Visuell gestalten**: Erstellen Sie benutzerdefinierte Websites mit einem visuellen Editor, der sauberen, semantischen HTML/CSS-Code generiert
- **Inhalte dynamisch verwalten**: Nutzen Sie das CMS, um Sammlungen strukturierter Inhalte wie Blogbeiträge, Produkte, Teammitglieder oder beliebige benutzerdefinierte Daten zu erstellen
- **Sofort veröffentlichen**: Stellen Sie Ihre Websites auf Webflows Hosting bereit oder exportieren Sie den Code für benutzerdefiniertes Hosting
- **Responsive Designs erstellen**: Bauen Sie Websites, die nahtlos auf Desktop, Tablet und Mobilgeräten funktionieren
- **Sammlungen anpassen**: Definieren Sie benutzerdefinierte Felder und Datenstrukturen für Ihre Inhaltstypen
- **Inhaltsaktualisierungen automatisieren**: Verwalten Sie Ihre CMS-Inhalte programmgesteuert über APIs
In Sim ermöglicht die Webflow-Integration Ihren Agenten, nahtlos mit Ihren Webflow-CMS-Sammlungen über API-Authentifizierung zu interagieren. Dies ermöglicht leistungsstarke Automatisierungsszenarien wie das automatische Erstellen von Blogbeiträgen aus KI-generierten Inhalten, das Aktualisieren von Produktinformationen, das Verwalten von Teammitgliederprofilen und das Abrufen von CMS-Elementen für die dynamische Inhaltsgenerierung. Ihre Agenten können vorhandene Elemente auflisten, um Ihre Inhalte zu durchsuchen, bestimmte Elemente nach ID abrufen, neue Einträge erstellen, um frische Inhalte hinzuzufügen, bestehende Elemente aktualisieren, um Informationen aktuell zu halten, und veraltete Inhalte löschen. Diese Integration überbrückt die Lücke zwischen Ihren KI-Workflows und Ihrem Webflow-CMS und ermöglicht automatisierte Inhaltsverwaltung, dynamische Website-Aktualisierungen und optimierte Inhalts-Workflows, die Ihre Websites ohne manuelles Eingreifen frisch und aktuell halten.
{/* MANUAL-CONTENT-END */}
## Gebrauchsanweisung
Integriert Webflow CMS in den Workflow. Kann Elemente in Webflow CMS-Sammlungen erstellen, abrufen, auflisten, aktualisieren oder löschen. Verwalten Sie Ihre Webflow-Inhalte programmatisch. Kann im Trigger-Modus verwendet werden, um Workflows auszulösen, wenn sich Sammlungselemente ändern oder Formulare übermittelt werden.
## Tools
### `webflow_list_items`
Alle Elemente aus einer Webflow CMS-Sammlung auflisten
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `collectionId` | string | Ja | ID der Sammlung |
| `offset` | number | Nein | Offset für Paginierung \(optional\) |
| `limit` | number | Nein | Maximale Anzahl der zurückzugebenden Elemente \(optional, Standard: 100\) |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `items` | json | Array von Sammlungselementen |
| `metadata` | json | Metadaten über die Abfrage |
### `webflow_get_item`
Ein einzelnes Element aus einer Webflow CMS-Sammlung abrufen
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `collectionId` | string | Ja | ID der Sammlung |
| `itemId` | string | Ja | ID des abzurufenden Elements |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `item` | json | Das abgerufene Elementobjekt |
| `metadata` | json | Metadaten über das abgerufene Element |
### `webflow_create_item`
Ein neues Element in einer Webflow CMS-Sammlung erstellen
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `collectionId` | string | Ja | ID der Sammlung |
| `fieldData` | json | Ja | Felddaten für das neue Element als JSON-Objekt. Die Schlüssel sollten mit den Feldnamen der Sammlung übereinstimmen. |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `item` | json | Das erstellte Element-Objekt |
| `metadata` | json | Metadaten über das erstellte Element |
### `webflow_update_item`
Ein vorhandenes Element in einer Webflow CMS-Sammlung aktualisieren
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `collectionId` | string | Ja | ID der Sammlung |
| `itemId` | string | Ja | ID des zu aktualisierenden Elements |
| `fieldData` | json | Ja | Zu aktualisierende Felddaten als JSON-Objekt. Nur Felder einschließen, die geändert werden sollen. |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `item` | json | Das aktualisierte Element-Objekt |
| `metadata` | json | Metadaten über das aktualisierte Element |
### `webflow_delete_item`
Ein Element aus einer Webflow CMS-Sammlung löschen
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `collectionId` | string | Ja | ID der Sammlung |
| `itemId` | string | Ja | ID des zu löschenden Elements |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Ob die Löschung erfolgreich war |
| `metadata` | json | Metadaten über die Löschung |
## Hinweise
- Kategorie: `tools`
- Typ: `webflow`

View File

@@ -66,6 +66,7 @@
"typeform",
"vision",
"wealthbox",
"webflow",
"webhook",
"whatsapp",
"wikipedia",

View File

@@ -0,0 +1,150 @@
---
title: Webflow
description: Manage Webflow CMS collections
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="webflow"
color="#E0E0E0"
icon={true}
iconSvg={`<svg className="block-icon"
viewBox='0 0 1080 674'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M1080 0L735.386 673.684H411.695L555.916 394.481H549.445C430.464 548.934 252.942 650.61 -0.000488281 673.684V398.344C-0.000488281 398.344 161.813 388.787 256.938 288.776H-0.000488281V0.0053214H288.771V237.515L295.252 237.489L413.254 0.0053214H631.644V236.009L638.126 235.999L760.555 0H1080Z'
fill='#146EF5'
/>
</svg>`}
/>
{/* MANUAL-CONTENT-START:intro */}
[Webflow](https://webflow.com/) is a powerful visual web design platform that enables you to build responsive websites without writing code. It combines a visual design interface with a robust CMS (Content Management System) that allows you to create, manage, and publish dynamic content for your websites.
With Webflow, you can:
- **Design visually**: Create custom websites with a visual editor that generates clean, semantic HTML/CSS code
- **Manage content dynamically**: Use the CMS to create collections of structured content like blog posts, products, team members, or any custom data
- **Publish instantly**: Deploy your sites to Webflow's hosting or export the code for custom hosting
- **Create responsive designs**: Build sites that work seamlessly across desktop, tablet, and mobile devices
- **Customize collections**: Define custom fields and data structures for your content types
- **Automate content updates**: Programmatically manage your CMS content through APIs
In Sim, the Webflow integration enables your agents to seamlessly interact with your Webflow CMS collections through API authentication. This allows for powerful automation scenarios such as automatically creating blog posts from AI-generated content, updating product information, managing team member profiles, and retrieving CMS items for dynamic content generation. Your agents can list existing items to browse your content, retrieve specific items by ID, create new entries to add fresh content, update existing items to keep information current, and delete outdated content. This integration bridges the gap between your AI workflows and your Webflow CMS, enabling automated content management, dynamic website updates, and streamlined content workflows that keep your sites fresh and up-to-date without manual intervention.
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Integrates Webflow CMS into the workflow. Can create, get, list, update, or delete items in Webflow CMS collections. Manage your Webflow content programmatically. Can be used in trigger mode to trigger workflows when collection items change or forms are submitted.
## Tools
### `webflow_list_items`
List all items from a Webflow CMS collection
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `collectionId` | string | Yes | ID of the collection |
| `offset` | number | No | Offset for pagination \(optional\) |
| `limit` | number | No | Maximum number of items to return \(optional, default: 100\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `items` | json | Array of collection items |
| `metadata` | json | Metadata about the query |
### `webflow_get_item`
Get a single item from a Webflow CMS collection
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `collectionId` | string | Yes | ID of the collection |
| `itemId` | string | Yes | ID of the item to retrieve |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `item` | json | The retrieved item object |
| `metadata` | json | Metadata about the retrieved item |
### `webflow_create_item`
Create a new item in a Webflow CMS collection
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `collectionId` | string | Yes | ID of the collection |
| `fieldData` | json | Yes | Field data for the new item as a JSON object. Keys should match collection field names. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `item` | json | The created item object |
| `metadata` | json | Metadata about the created item |
### `webflow_update_item`
Update an existing item in a Webflow CMS collection
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `collectionId` | string | Yes | ID of the collection |
| `itemId` | string | Yes | ID of the item to update |
| `fieldData` | json | Yes | Field data to update as a JSON object. Only include fields you want to change. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `item` | json | The updated item object |
| `metadata` | json | Metadata about the updated item |
### `webflow_delete_item`
Delete an item from a Webflow CMS collection
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `collectionId` | string | Yes | ID of the collection |
| `itemId` | string | Yes | ID of the item to delete |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the deletion was successful |
| `metadata` | json | Metadata about the deletion |
## Notes
- Category: `tools`
- Type: `webflow`

View File

@@ -67,11 +67,12 @@ Enviar un mensaje a un canal de Discord
#### Entrada
| Parámetro | Tipo | Obligatorio | Descripción |
| --------- | ---- | ----------- | ----------- |
| --------- | ---- | -------- | ----------- |
| `botToken` | string | Sí | El token del bot para autenticación |
| `channelId` | string | Sí | El ID del canal de Discord donde enviar el mensaje |
| `channelId` | string | Sí | El ID del canal de Discord al que enviar el mensaje |
| `content` | string | No | El contenido de texto del mensaje |
| `serverId` | string | Sí | El ID del servidor de Discord \(ID del guild\) |
| `files` | file[] | No | Archivos para adjuntar al mensaje |
#### Salida

View File

@@ -85,9 +85,10 @@ Subir un archivo a Google Drive
| Parámetro | Tipo | Obligatorio | Descripción |
| --------- | ---- | ----------- | ----------- |
| `fileName` | string | Sí | El nombre del archivo a subir |
| `content` | string | | El contenido del archivo a subir |
| `mimeType` | string | No | El tipo MIME del archivo a subir |
| `folderSelector` | string | No | Selecciona la carpeta donde subir el archivo |
| `file` | file | No | Archivo binario para subir \(objeto UserFile\) |
| `content` | string | No | Contenido de texto para subir \(use esto O archivo, no ambos\) |
| `mimeType` | string | No | El tipo MIME del archivo a subir \(auto-detectado del archivo si no se proporciona\) |
| `folderSelector` | string | No | Seleccione la carpeta donde subir el archivo |
| `folderId` | string | No | El ID de la carpeta donde subir el archivo \(uso interno\) |
#### Salida

View File

@@ -133,8 +133,9 @@ Escribir o actualizar contenido en un chat de Microsoft Teams
| Parámetro | Tipo | Obligatorio | Descripción |
| --------- | ---- | -------- | ----------- |
| `chatId` | string | Sí | El ID del chat donde escribir |
| `content` | string | Sí | El contenido a escribir en el mensaje |
| `chatId` | string | Sí | El ID del chat en el que escribir |
| `content` | string | Sí | El contenido para escribir en el mensaje |
| `files` | file[] | No | Archivos para adjuntar al mensaje |
#### Salida
@@ -181,9 +182,10 @@ Escribir o enviar un mensaje a un canal de Microsoft Teams
| Parámetro | Tipo | Obligatorio | Descripción |
| --------- | ---- | -------- | ----------- |
| `teamId` | string | Sí | El ID del equipo al que escribir |
| `channelId` | string | Sí | El ID del canal al que escribir |
| `teamId` | string | Sí | El ID del equipo en el que escribir |
| `channelId` | string | Sí | El ID del canal en el que escribir |
| `content` | string | Sí | El contenido para escribir en el canal |
| `files` | file[] | No | Archivos para adjuntar al mensaje |
#### Salida

View File

@@ -61,11 +61,12 @@ Subir un archivo a OneDrive
#### Entrada
| Parámetro | Tipo | Obligatorio | Descripción |
| --------- | ---- | -------- | ----------- |
| --------- | ---- | ----------- | ----------- |
| `fileName` | string | Sí | El nombre del archivo a subir |
| `content` | string | | El contenido del archivo a subir |
| `file` | file | No | El archivo a subir \(binario\) |
| `content` | string | No | El contenido de texto a subir \(si no se proporciona un archivo\) |
| `folderSelector` | string | No | Seleccionar la carpeta donde subir el archivo |
| `manualFolderId` | string | No | ID de carpeta introducido manualmente \(modo avanzado\) |
| `manualFolderId` | string | No | ID de carpeta ingresado manualmente \(modo avanzado\) |
#### Salida

View File

@@ -150,14 +150,15 @@ Enviar correos electrónicos usando Outlook
#### Entrada
| Parámetro | Tipo | Obligatorio | Descripción |
| --------- | ---- | ----------- | ----------- |
| --------- | ---- | -------- | ----------- |
| `to` | string | Sí | Dirección de correo electrónico del destinatario |
| `subject` | string | Sí | Asunto del correo electrónico |
| `body` | string | Sí | Contenido del cuerpo del correo electrónico |
| `replyToMessageId` | string | No | ID del mensaje al que responder (para hilos) |
| `replyToMessageId` | string | No | ID del mensaje al que responder \(para hilos\) |
| `conversationId` | string | No | ID de conversación para hilos |
| `cc` | string | No | Destinatarios en CC (separados por comas) |
| `bcc` | string | No | Destinatarios en CCO (separados por comas) |
| `cc` | string | No | Destinatarios en CC \(separados por comas\) |
| `bcc` | string | No | Destinatarios en CCO \(separados por comas\) |
| `attachments` | file[] | No | Archivos para adjuntar al correo electrónico |
#### Salida
@@ -181,6 +182,7 @@ Crear borradores de correos electrónicos usando Outlook
| `body` | string | Sí | Contenido del cuerpo del correo electrónico |
| `cc` | string | No | Destinatarios en CC \(separados por comas\) |
| `bcc` | string | No | Destinatarios en CCO \(separados por comas\) |
| `attachments` | file[] | No | Archivos para adjuntar al borrador de correo electrónico |
#### Salida

View File

@@ -199,6 +199,26 @@ Añadir un nuevo elemento a una lista de SharePoint
| --------- | ---- | ----------- |
| `item` | object | Elemento de lista de SharePoint creado |
### `sharepoint_upload_file`
Subir archivos a una biblioteca de documentos de SharePoint
#### Entrada
| Parámetro | Tipo | Obligatorio | Descripción |
| --------- | ---- | -------- | ----------- |
| `siteId` | cadena | No | El ID del sitio de SharePoint |
| `driveId` | cadena | No | El ID de la biblioteca de documentos \(unidad\). Si no se proporciona, usa la unidad predeterminada. |
| `folderPath` | cadena | No | Ruta de carpeta opcional dentro de la biblioteca de documentos \(p. ej., /Documents/Subfolder\) |
| `fileName` | cadena | No | Opcional: sobrescribir el nombre del archivo subido |
| `files` | archivo[] | No | Archivos para subir a SharePoint |
#### Salida
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `uploadedFiles` | array | Array de objetos de archivos subidos |
## Notas
- Categoría: `tools`

View File

@@ -79,6 +79,7 @@ Envía mensajes a canales o usuarios de Slack a través de la API de Slack. Comp
| `botToken` | string | No | Token del bot para Bot personalizado |
| `channel` | string | Sí | Canal de Slack objetivo (p. ej., #general) |
| `text` | string | Sí | Texto del mensaje a enviar (admite formato mrkdwn de Slack) |
| `files` | file[] | No | Archivos para adjuntar al mensaje |
#### Salida

View File

@@ -202,6 +202,28 @@ Insertar o actualizar datos en una tabla de Supabase (operación upsert)
| `message` | string | Mensaje de estado de la operación |
| `results` | array | Array de registros insertados o actualizados |
### `supabase_vector_search`
Realizar búsqueda de similitud usando pgvector en una tabla de Supabase
#### Entrada
| Parámetro | Tipo | Obligatorio | Descripción |
| --------- | ---- | ----------- | ----------- |
| `projectId` | string | Sí | ID de tu proyecto Supabase \(p. ej., jdrkgepadsdopsntdlom\) |
| `functionName` | string | Sí | Nombre de la función PostgreSQL que realiza la búsqueda vectorial \(p. ej., match_documents\) |
| `queryEmbedding` | array | Sí | El vector/embedding de consulta para buscar elementos similares |
| `matchThreshold` | number | No | Umbral mínimo de similitud \(0-1\), típicamente 0.7-0.9 |
| `matchCount` | number | No | Número máximo de resultados a devolver \(predeterminado: 10\) |
| `apiKey` | string | Sí | Tu clave secreta de rol de servicio de Supabase |
#### Salida
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `message` | string | Mensaje de estado de la operación |
| `results` | array | Array de registros con puntuaciones de similitud de la búsqueda vectorial. Cada registro incluye un campo de similitud \(0-1\) que indica cuán similar es al vector de consulta. |
## Notas
- Categoría: `tools`

View File

@@ -190,6 +190,26 @@ Envía animaciones (GIFs) a canales o usuarios de Telegram a través de la API d
| `message` | string | Mensaje de éxito o error |
| `data` | object | Datos del mensaje de Telegram incluyendo medios opcionales |
### `telegram_send_document`
Envía documentos (PDF, ZIP, DOC, etc.) a canales o usuarios de Telegram a través de la API de Bot de Telegram.
#### Entrada
| Parámetro | Tipo | Obligatorio | Descripción |
| --------- | ---- | ----------- | ----------- |
| `botToken` | string | Sí | Tu token de API de Bot de Telegram |
| `chatId` | string | Sí | ID del chat de Telegram objetivo |
| `files` | file[] | No | Archivo de documento para enviar \(PDF, ZIP, DOC, etc.\). Tamaño máximo: 50MB |
| `caption` | string | No | Leyenda del documento \(opcional\) |
#### Salida
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `message` | string | Mensaje de éxito o error |
| `data` | object | Datos del mensaje de Telegram incluyendo documento |
## Notas
- Categoría: `tools`

View File

@@ -57,11 +57,12 @@ Procesa y analiza imágenes utilizando modelos avanzados de visión. Capaz de co
#### Entrada
| Parámetro | Tipo | Obligatorio | Descripción |
| --------- | ---- | ----------- | ----------- |
| --------- | ---- | ---------- | ----------- |
| `apiKey` | string | Sí | Clave API para el proveedor de modelo seleccionado |
| `imageUrl` | string | | URL de imagen de acceso público |
| `imageUrl` | string | No | URL de imagen accesible públicamente |
| `imageFile` | file | No | Archivo de imagen para analizar |
| `model` | string | No | Modelo de visión a utilizar \(gpt-4o, claude-3-opus-20240229, etc\) |
| `prompt` | string | No | Indicación personalizada para análisis de imágenes |
| `prompt` | string | No | Prompt personalizado para análisis de imagen |
#### Salida

View File

@@ -0,0 +1,145 @@
---
title: Webflow
description: Gestionar colecciones CMS de Webflow
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="webflow"
color="#E0E0E0"
icon={true}
iconSvg={`<svg className="block-icon"
viewBox='0 0 1080 674'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M1080 0L735.386 673.684H411.695L555.916 394.481H549.445C430.464 548.934 252.942 650.61 -0.000488281 673.684V398.344C-0.000488281 398.344 161.813 388.787 256.938 288.776H-0.000488281V0.0053214H288.771V237.515L295.252 237.489L413.254 0.0053214H631.644V236.009L638.126 235.999L760.555 0H1080Z'
fill='#146EF5'
/>
</svg>`}
/>
{/* MANUAL-CONTENT-START:intro */}
[Webflow](https://webflow.com/) es una potente plataforma visual de diseño web que te permite crear sitios web responsivos sin escribir código. Combina una interfaz de diseño visual con un robusto CMS (Sistema de Gestión de Contenidos) que te permite crear, gestionar y publicar contenido dinámico para tus sitios web.
Con Webflow, puedes:
- **Diseñar visualmente**: Crear sitios web personalizados con un editor visual que genera código HTML/CSS limpio y semántico
- **Gestionar contenido dinámicamente**: Usar el CMS para crear colecciones de contenido estructurado como entradas de blog, productos, miembros del equipo o cualquier dato personalizado
- **Publicar instantáneamente**: Implementar tus sitios en el alojamiento de Webflow o exportar el código para alojamiento personalizado
- **Crear diseños responsivos**: Construir sitios que funcionen perfectamente en dispositivos de escritorio, tabletas y móviles
- **Personalizar colecciones**: Definir campos personalizados y estructuras de datos para tus tipos de contenido
- **Automatizar actualizaciones de contenido**: Gestionar programáticamente el contenido de tu CMS a través de APIs
En Sim, la integración con Webflow permite a tus agentes interactuar sin problemas con tus colecciones CMS de Webflow mediante autenticación API. Esto permite potentes escenarios de automatización como la creación automática de entradas de blog a partir de contenido generado por IA, actualización de información de productos, gestión de perfiles de miembros del equipo y recuperación de elementos CMS para la generación de contenido dinámico. Tus agentes pueden listar elementos existentes para navegar por tu contenido, recuperar elementos específicos por ID, crear nuevas entradas para añadir contenido fresco, actualizar elementos existentes para mantener la información actualizada y eliminar contenido obsoleto. Esta integración cierra la brecha entre tus flujos de trabajo de IA y tu CMS de Webflow, permitiendo la gestión automatizada de contenido, actualizaciones dinámicas del sitio web y flujos de trabajo de contenido optimizados que mantienen tus sitios frescos y actualizados sin intervención manual.
{/* MANUAL-CONTENT-END */}
## Instrucciones de uso
Integra el CMS de Webflow en el flujo de trabajo. Puede crear, obtener, listar, actualizar o eliminar elementos en las colecciones del CMS de Webflow. Gestiona tu contenido de Webflow de forma programática. Se puede usar en modo de activación para iniciar flujos de trabajo cuando cambian los elementos de la colección o se envían formularios.
## Herramientas
### `webflow_list_items`
Listar todos los elementos de una colección del CMS de Webflow
#### Entrada
| Parámetro | Tipo | Obligatorio | Descripción |
| --------- | ---- | -------- | ----------- |
| `collectionId` | string | Sí | ID de la colección |
| `offset` | number | No | Desplazamiento para paginación \(opcional\) |
| `limit` | number | No | Número máximo de elementos a devolver \(opcional, predeterminado: 100\) |
#### Salida
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `items` | json | Array de elementos de la colección |
| `metadata` | json | Metadatos sobre la consulta |
### `webflow_get_item`
Obtener un solo elemento de una colección del CMS de Webflow
#### Entrada
| Parámetro | Tipo | Obligatorio | Descripción |
| --------- | ---- | -------- | ----------- |
| `collectionId` | string | Sí | ID de la colección |
| `itemId` | string | Sí | ID del elemento a recuperar |
#### Salida
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `item` | json | El objeto del elemento recuperado |
| `metadata` | json | Metadatos sobre el elemento recuperado |
### `webflow_create_item`
Crear un nuevo elemento en una colección del CMS de Webflow
#### Entrada
| Parámetro | Tipo | Obligatorio | Descripción |
| --------- | ---- | -------- | ----------- |
| `collectionId` | string | Sí | ID de la colección |
| `fieldData` | json | Sí | Datos de campo para el nuevo elemento como objeto JSON. Las claves deben coincidir con los nombres de campo de la colección. |
#### Salida
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `item` | json | El objeto del elemento creado |
| `metadata` | json | Metadatos sobre el elemento creado |
### `webflow_update_item`
Actualizar un elemento existente en una colección CMS de Webflow
#### Entrada
| Parámetro | Tipo | Obligatorio | Descripción |
| --------- | ---- | -------- | ----------- |
| `collectionId` | string | Sí | ID de la colección |
| `itemId` | string | Sí | ID del elemento a actualizar |
| `fieldData` | json | Sí | Datos de campo para actualizar como objeto JSON. Solo incluye los campos que quieres cambiar. |
#### Salida
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `item` | json | El objeto del elemento actualizado |
| `metadata` | json | Metadatos sobre el elemento actualizado |
### `webflow_delete_item`
Eliminar un elemento de una colección CMS de Webflow
#### Entrada
| Parámetro | Tipo | Obligatorio | Descripción |
| --------- | ---- | -------- | ----------- |
| `collectionId` | string | Sí | ID de la colección |
| `itemId` | string | Sí | ID del elemento a eliminar |
#### Salida
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Si la eliminación fue exitosa |
| `metadata` | json | Metadatos sobre la eliminación |
## Notas
- Categoría: `tools`
- Tipo: `webflow`

View File

@@ -72,6 +72,7 @@ Envoyer un message à un canal Discord
| `channelId` | chaîne | Oui | L'ID du canal Discord où envoyer le message |
| `content` | chaîne | Non | Le contenu textuel du message |
| `serverId` | chaîne | Oui | L'ID du serveur Discord \(ID de guilde\) |
| `files` | fichier[] | Non | Fichiers à joindre au message |
#### Sortie

View File

@@ -84,11 +84,12 @@ Téléverser un fichier vers Google Drive
| Paramètre | Type | Obligatoire | Description |
| --------- | ---- | ---------- | ----------- |
| `fileName` | chaîne | Oui | Le nom du fichier à téléverser |
| `content` | chaîne | Oui | Le contenu du fichier à téléverser |
| `mimeType` | chaîne | Non | Le type MIME du fichier à téléverser |
| `folderSelector` | chaîne | Non | Sélectionnez le dossier téléverser le fichier |
| `folderId` | chaîne | Non | L'ID du dossier où téléverser le fichier \(usage interne\) |
| `fileName` | string | Oui | Le nom du fichier à télécharger |
| `file` | file | Non | Fichier binaire à télécharger \(objet UserFile\) |
| `content` | string | Non | Contenu textuel à télécharger \(utilisez ceci OU fichier, pas les deux\) |
| `mimeType` | string | Non | Le type MIME du fichier à télécharger \(détecté automatiquement à partir du fichier si non fourni\) |
| `folderSelector` | string | Non | Sélectionnez le dossier dans lequel télécharger le fichier |
| `folderId` | string | Non | L'ID du dossier dans lequel télécharger le fichier \(usage interne\) |
#### Sortie

View File

@@ -135,6 +135,7 @@ Lire le contenu d'un chat Microsoft Teams
| --------- | ---- | -------- | ----------- |
| `chatId` | chaîne | Oui | L'ID de la conversation dans laquelle écrire |
| `content` | chaîne | Oui | Le contenu à écrire dans le message |
| `files` | fichier[] | Non | Fichiers à joindre au message |
#### Sortie
@@ -180,10 +181,11 @@ Lire le contenu d'un canal Microsoft Teams
#### Entrée
| Paramètre | Type | Obligatoire | Description |
| --------- | ---- | ---------- | ----------- |
| `teamId` | chaîne | Oui | L'ID de l'équipe à laquelle écrire |
| `channelId` | chaîne | Oui | L'ID du canal auquel écrire |
| --------- | ---- | -------- | ----------- |
| `teamId` | chaîne | Oui | L'ID de l'équipe dans laquelle écrire |
| `channelId` | chaîne | Oui | L'ID du canal dans lequel écrire |
| `content` | chaîne | Oui | Le contenu à écrire dans le canal |
| `files` | fichier[] | Non | Fichiers à joindre au message |
#### Sortie

View File

@@ -61,11 +61,12 @@ Télécharger un fichier vers OneDrive
#### Entrée
| Paramètre | Type | Obligatoire | Description |
| --------- | ---- | -------- | ----------- |
| `fileName` | chaîne | Oui | Le nom du fichier à télécharger |
| `content` | chaîne | Oui | Le contenu du fichier à télécharger |
| `folderSelector` | chaîne | Non | Sélectionnez le dossier où télécharger le fichier |
| `manualFolderId` | chaîne | Non | ID du dossier saisi manuellement \(mode avancé\) |
| --------- | ---- | ---------- | ----------- |
| `fileName` | string | Oui | Le nom du fichier à télécharger |
| `file` | file | Non | Le fichier à télécharger \(binaire\) |
| `content` | string | Non | Le contenu textuel à télécharger \(si aucun fichier n'est fourni\) |
| `folderSelector` | string | Non | Sélectionner le dossier dans lequel télécharger le fichier |
| `manualFolderId` | string | Non | ID de dossier saisi manuellement \(mode avancé\) |
#### Sortie

View File

@@ -150,14 +150,15 @@ Envoyer des e-mails avec Outlook
#### Entrée
| Paramètre | Type | Obligatoire | Description |
| --------- | ---- | ----------- | ----------- |
| `to` | chaîne | Oui | Adresse e-mail du destinataire |
| `subject` | chaîne | Oui | Objet de l'e-mail |
| `body` | chaîne | Oui | Contenu du corps de l'e-mail |
| `replyToMessageId` | chaîne | Non | ID du message auquel répondre \(pour le fil de discussion\) |
| `conversationId` | chaîne | Non | ID de conversation pour le fil de discussion |
| `cc` | chaîne | Non | Destinataires en CC \(séparés par des virgules\) |
| `bcc` | chaîne | Non | Destinataires en BCC \(séparés par des virgules\) |
| --------- | ---- | ---------- | ----------- |
| `to` | string | Oui | Adresse e-mail du destinataire |
| `subject` | string | Oui | Objet de l'e-mail |
| `body` | string | Oui | Contenu du corps de l'e-mail |
| `replyToMessageId` | string | Non | ID du message auquel répondre \(pour le fil de discussion\) |
| `conversationId` | string | Non | ID de conversation pour le fil de discussion |
| `cc` | string | Non | Destinataires en CC \(séparés par des virgules\) |
| `bcc` | string | Non | Destinataires en BCC \(séparés par des virgules\) |
| `attachments` | file[] | Non | Fichiers à joindre à l'e-mail |
#### Sortie
@@ -175,12 +176,13 @@ Rédiger des e-mails avec Outlook
#### Entrée
| Paramètre | Type | Obligatoire | Description |
| --------- | ---- | -------- | ----------- |
| --------- | ---- | ---------- | ----------- |
| `to` | string | Oui | Adresse e-mail du destinataire |
| `subject` | string | Oui | Objet de l'e-mail |
| `body` | string | Oui | Contenu du corps de l'e-mail |
| `cc` | string | Non | Destinataires en CC \(séparés par des virgules\) |
| `bcc` | string | Non | Destinataires en BCC \(séparés par des virgules\) |
| `attachments` | file[] | Non | Fichiers à joindre au brouillon d'e-mail |
#### Sortie

View File

@@ -199,6 +199,26 @@ Ajouter un nouvel élément à une liste SharePoint
| --------- | ---- | ----------- |
| `item` | object | Élément de liste SharePoint créé |
### `sharepoint_upload_file`
Télécharger des fichiers vers une bibliothèque de documents SharePoint
#### Entrée
| Paramètre | Type | Obligatoire | Description |
| --------- | ---- | ---------- | ----------- |
| `siteId` | chaîne | Non | L'ID du site SharePoint |
| `driveId` | chaîne | Non | L'ID de la bibliothèque de documents \(lecteur\). Si non fourni, utilise le lecteur par défaut. |
| `folderPath` | chaîne | Non | Chemin de dossier optionnel dans la bibliothèque de documents \(par exemple, /Documents/Sousdossier\) |
| `fileName` | chaîne | Non | Optionnel : remplacer le nom du fichier téléchargé |
| `files` | fichier[] | Non | Fichiers à télécharger vers SharePoint |
#### Sortie
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `uploadedFiles` | tableau | Tableau d'objets de fichiers téléchargés |
## Remarques
- Catégorie : `tools`

View File

@@ -75,11 +75,12 @@ Envoyez des messages aux canaux ou utilisateurs Slack via l'API Slack. Prend en
#### Entrée
| Paramètre | Type | Obligatoire | Description |
| --------- | ---- | ----------- | ----------- |
| --------- | ---- | ---------- | ----------- |
| `authMethod` | chaîne | Non | Méthode d'authentification : oauth ou bot_token |
| `botToken` | chaîne | Non | Jeton du bot pour le Bot personnalisé |
| `channel` | chaîne | Oui | Canal Slack cible (par ex., #general) |
| `text` | chaîne | Oui | Texte du message à envoyer (prend en charge le formatage mrkdwn de Slack) |
| `botToken` | chaîne | Non | Jeton du bot pour Bot personnalisé |
| `channel` | chaîne | Oui | Canal Slack cible \(par ex., #general\) |
| `text` | chaîne | Oui | Texte du message à envoyer \(prend en charge le formatage mrkdwn de Slack\) |
| `files` | fichier[] | Non | Fichiers à joindre au message |
#### Sortie

View File

@@ -202,6 +202,28 @@ Insérer ou mettre à jour des données dans une table Supabase (opération upse
| `message` | string | Message d'état de l'opération |
| `results` | array | Tableau des enregistrements insérés ou mis à jour |
### `supabase_vector_search`
Effectuer une recherche de similarité en utilisant pgvector dans une table Supabase
#### Entrée
| Paramètre | Type | Obligatoire | Description |
| --------- | ---- | ----------- | ----------- |
| `projectId` | chaîne | Oui | L'ID de votre projet Supabase (ex. : jdrkgepadsdopsntdlom) |
| `functionName` | chaîne | Oui | Le nom de la fonction PostgreSQL qui effectue la recherche vectorielle (ex. : match_documents) |
| `queryEmbedding` | tableau | Oui | Le vecteur/embedding de requête pour rechercher des éléments similaires |
| `matchThreshold` | nombre | Non | Seuil minimum de similarité (0-1), généralement 0,7-0,9 |
| `matchCount` | nombre | Non | Nombre maximum de résultats à retourner (par défaut : 10) |
| `apiKey` | chaîne | Oui | Votre clé secrète de rôle de service Supabase |
#### Sortie
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `message` | chaîne | Message d'état de l'opération |
| `results` | tableau | Tableau d'enregistrements avec scores de similarité issus de la recherche vectorielle. Chaque enregistrement inclut un champ de similarité (0-1) indiquant son degré de similarité avec le vecteur de requête. |
## Notes
- Catégorie : `tools`

View File

@@ -190,6 +190,26 @@ Envoyez des animations (GIF) aux canaux ou utilisateurs Telegram via l'API Bot T
| `message` | chaîne | Message de succès ou d'erreur |
| `data` | objet | Données du message Telegram incluant les médias optionnels |
### `telegram_send_document`
Envoyez des documents (PDF, ZIP, DOC, etc.) aux canaux ou utilisateurs Telegram via l'API Bot Telegram.
#### Entrée
| Paramètre | Type | Obligatoire | Description |
| --------- | ---- | ----------- | ----------- |
| `botToken` | chaîne | Oui | Votre jeton d'API Bot Telegram |
| `chatId` | chaîne | Oui | ID du chat Telegram cible |
| `files` | fichier[] | Non | Fichier document à envoyer \(PDF, ZIP, DOC, etc.\). Taille max : 50 Mo |
| `caption` | chaîne | Non | Légende du document \(facultatif\) |
#### Sortie
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `message` | chaîne | Message de succès ou d'erreur |
| `data` | objet | Données du message Telegram incluant le document |
## Notes
- Catégorie : `tools`

View File

@@ -58,10 +58,11 @@ Traitez et analysez des images en utilisant des modèles de vision avancés. Cap
| Paramètre | Type | Obligatoire | Description |
| --------- | ---- | ---------- | ----------- |
| `apiKey` | chaîne | Oui | Clé API pour le fournisseur de modèle sélectionné |
| `imageUrl` | chaîne | Oui | URL d'image accessible publiquement |
| `model` | chaîne | Non | Modèle de vision à utiliser \(gpt-4o, claude-3-opus-20240229, etc\) |
| `prompt` | chaîne | Non | Prompt personnalisé pour l'analyse d'image |
| `apiKey` | string | Oui | Clé API pour le fournisseur de modèle sélectionné |
| `imageUrl` | string | Non | URL d'image accessible publiquement |
| `imageFile` | file | Non | Fichier image à analyser |
| `model` | string | Non | Modèle de vision à utiliser \(gpt-4o, claude-3-opus-20240229, etc\) |
| `prompt` | string | Non | Invite personnalisée pour l'analyse d'image |
#### Sortie

View File

@@ -0,0 +1,145 @@
---
title: Webflow
description: Gérer les collections CMS de Webflow
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="webflow"
color="#E0E0E0"
icon={true}
iconSvg={`<svg className="block-icon"
viewBox='0 0 1080 674'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M1080 0L735.386 673.684H411.695L555.916 394.481H549.445C430.464 548.934 252.942 650.61 -0.000488281 673.684V398.344C-0.000488281 398.344 161.813 388.787 256.938 288.776H-0.000488281V0.0053214H288.771V237.515L295.252 237.489L413.254 0.0053214H631.644V236.009L638.126 235.999L760.555 0H1080Z'
fill='#146EF5'
/>
</svg>`}
/>
{/* MANUAL-CONTENT-START:intro */}
[Webflow](https://webflow.com/) est une plateforme puissante de conception web visuelle qui vous permet de créer des sites web responsifs sans écrire de code. Elle combine une interface de conception visuelle avec un CMS (système de gestion de contenu) robuste qui vous permet de créer, gérer et publier du contenu dynamique pour vos sites web.
Avec Webflow, vous pouvez :
- **Concevoir visuellement** : créer des sites web personnalisés avec un éditeur visuel qui génère du code HTML/CSS propre et sémantique
- **Gérer du contenu dynamiquement** : utiliser le CMS pour créer des collections de contenu structuré comme des articles de blog, des produits, des membres d'équipe ou toute donnée personnalisée
- **Publier instantanément** : déployer vos sites sur l'hébergement de Webflow ou exporter le code pour un hébergement personnalisé
- **Créer des designs responsifs** : construire des sites qui fonctionnent parfaitement sur ordinateur, tablette et appareils mobiles
- **Personnaliser les collections** : définir des champs personnalisés et des structures de données pour vos types de contenu
- **Automatiser les mises à jour de contenu** : gérer programmatiquement votre contenu CMS via des API
Dans Sim, l'intégration Webflow permet à vos agents d'interagir de manière transparente avec vos collections CMS Webflow grâce à l'authentification API. Cela permet des scénarios d'automatisation puissants tels que la création automatique d'articles de blog à partir de contenu généré par IA, la mise à jour d'informations sur les produits, la gestion des profils des membres de l'équipe et la récupération d'éléments CMS pour la génération de contenu dynamique. Vos agents peuvent lister les éléments existants pour parcourir votre contenu, récupérer des éléments spécifiques par ID, créer de nouvelles entrées pour ajouter du contenu frais, mettre à jour des éléments existants pour maintenir les informations à jour et supprimer du contenu obsolète. Cette intégration comble le fossé entre vos flux de travail IA et votre CMS Webflow, permettant une gestion automatisée du contenu, des mises à jour dynamiques de sites web et des flux de travail de contenu rationalisés qui maintiennent vos sites frais et à jour sans intervention manuelle.
{/* MANUAL-CONTENT-END */}
## Instructions d'utilisation
Intègre le CMS Webflow dans le flux de travail. Peut créer, obtenir, lister, mettre à jour ou supprimer des éléments dans les collections CMS Webflow. Gérez votre contenu Webflow par programmation. Peut être utilisé en mode déclencheur pour lancer des flux de travail lorsque les éléments de collection changent ou lorsque des formulaires sont soumis.
## Outils
### `webflow_list_items`
Lister tous les éléments d'une collection CMS Webflow
#### Entrée
| Paramètre | Type | Obligatoire | Description |
| --------- | ---- | -------- | ----------- |
| `collectionId` | string | Oui | ID de la collection |
| `offset` | number | Non | Décalage pour la pagination \(facultatif\) |
| `limit` | number | Non | Nombre maximum d'éléments à retourner \(facultatif, par défaut : 100\) |
#### Sortie
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `items` | json | Tableau des éléments de la collection |
| `metadata` | json | Métadonnées sur la requête |
### `webflow_get_item`
Obtenir un seul élément d'une collection CMS Webflow
#### Entrée
| Paramètre | Type | Obligatoire | Description |
| --------- | ---- | -------- | ----------- |
| `collectionId` | string | Oui | ID de la collection |
| `itemId` | string | Oui | ID de l'élément à récupérer |
#### Sortie
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `item` | json | L'objet de l'élément récupéré |
| `metadata` | json | Métadonnées sur l'élément récupéré |
### `webflow_create_item`
Créer un nouvel élément dans une collection CMS Webflow
#### Entrée
| Paramètre | Type | Obligatoire | Description |
| --------- | ---- | ---------- | ----------- |
| `collectionId` | string | Oui | ID de la collection |
| `fieldData` | json | Oui | Données de champ pour le nouvel élément sous forme d'objet JSON. Les clés doivent correspondre aux noms des champs de la collection. |
#### Sortie
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `item` | json | L'objet de l'élément créé |
| `metadata` | json | Métadonnées concernant l'élément créé |
### `webflow_update_item`
Mettre à jour un élément existant dans une collection CMS Webflow
#### Entrée
| Paramètre | Type | Obligatoire | Description |
| --------- | ---- | ---------- | ----------- |
| `collectionId` | string | Oui | ID de la collection |
| `itemId` | string | Oui | ID de l'élément à mettre à jour |
| `fieldData` | json | Oui | Données de champ à mettre à jour sous forme d'objet JSON. N'incluez que les champs que vous souhaitez modifier. |
#### Sortie
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `item` | json | L'objet de l'élément mis à jour |
| `metadata` | json | Métadonnées concernant l'élément mis à jour |
### `webflow_delete_item`
Supprimer un élément d'une collection CMS Webflow
#### Entrée
| Paramètre | Type | Obligatoire | Description |
| --------- | ---- | ---------- | ----------- |
| `collectionId` | string | Oui | ID de la collection |
| `itemId` | string | Oui | ID de l'élément à supprimer |
#### Sortie
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Indique si la suppression a réussi |
| `metadata` | json | Métadonnées concernant la suppression |
## Notes
- Catégorie : `tools`
- Type : `webflow`

View File

@@ -72,6 +72,7 @@ Discordチャンネルにメッセージを送信する
| `channelId` | string | はい | メッセージを送信するDiscordチャンネルID |
| `content` | string | いいえ | メッセージのテキスト内容 |
| `serverId` | string | はい | DiscordサーバーIDギルドID |
| `files` | file[] | いいえ | メッセージに添付するファイル |
#### 出力

View File

@@ -85,8 +85,9 @@ Google Driveをワークフローに統合します。ファイルの作成、
| パラメータ | 型 | 必須 | 説明 |
| --------- | ---- | -------- | ----------- |
| `fileName` | string | はい | アップロードするファイルの名前 |
| `content` | string | はい | アップロードするファイルの内容 |
| `mimeType` | string | いいえ | アップロードするファイルのMIMEタイプ |
| `file` | file | いいえ | アップロードするバイナリファイルUserFileオブジェクト |
| `content` | string | いいえ | アップロードするテキストコンテンツfileかこちらのどちらか一方を使用、両方は不可 |
| `mimeType` | string | いいえ | アップロードするファイルのMIMEタイプ指定がない場合はファイルから自動検出 |
| `folderSelector` | string | いいえ | ファイルをアップロードするフォルダを選択 |
| `folderId` | string | いいえ | ファイルをアップロードするフォルダのID内部使用 |

View File

@@ -134,7 +134,8 @@ Microsoft Teams チャットでコンテンツを作成または更新する
| パラメータ | 型 | 必須 | 説明 |
| --------- | ---- | -------- | ----------- |
| `chatId` | string | はい | 書き込み先のチャットID |
| `content` | string | はい | メッセージに書き込むコンテンツ |
| `content` | string | はい | メッセージに書き込む内容 |
| `files` | file[] | いいえ | メッセージに添付するファイル |
#### 出力
@@ -179,11 +180,12 @@ Microsoft Teamsチャネルにメッセージを書き込むまたは送信す
#### 入力
| パラメータ | 種類 | 必須 | 説明 |
| パラメータ | | 必須 | 説明 |
| --------- | ---- | -------- | ----------- |
| `teamId` | string | はい | 書き込み先のチームID |
| `channelId` | string | はい | 書き込み先のチャネルID |
| `content` | string | はい | チャネルに書き込む内容 |
| `files` | file[] | いいえ | メッセージに添付するファイル |
#### 出力

View File

@@ -63,7 +63,8 @@ OneDriveにファイルをアップロードする
| パラメータ | 型 | 必須 | 説明 |
| --------- | ---- | -------- | ----------- |
| `fileName` | string | はい | アップロードするファイルの名前 |
| `content` | string | はい | アップロードするファイルの内容 |
| `file` | file | いいえ | アップロードするファイル(バイナリ) |
| `content` | string | いいえ | アップロードするテキストコンテンツ(ファイルが提供されていない場合) |
| `folderSelector` | string | いいえ | ファイルをアップロードするフォルダを選択 |
| `manualFolderId` | string | いいえ | 手動で入力したフォルダID高度なモード |

View File

@@ -158,6 +158,7 @@ Outlookを使用してメールを送信する
| `conversationId` | string | いいえ | スレッド用の会話ID |
| `cc` | string | いいえ | CCの受信者カンマ区切り |
| `bcc` | string | いいえ | BCCの受信者カンマ区切り |
| `attachments` | file[] | いいえ | メールに添付するファイル |
#### 出力
@@ -181,6 +182,7 @@ Outlookを使用してメールを下書きする
| `body` | string | はい | メール本文の内容 |
| `cc` | string | いいえ | CCの受信者カンマ区切り |
| `bcc` | string | いいえ | BCCの受信者カンマ区切り |
| `attachments` | file[] | いいえ | メールの下書きに添付するファイル |
#### 出力

View File

@@ -199,7 +199,27 @@ SharePointリストに新しいアイテムを追加する
| --------- | ---- | ----------- |
| `item` | object | 作成されたSharePointリストアイテム |
### `sharepoint_upload_file`
SharePointドキュメントライブラリにファイルをアップロードする
#### 入力
| パラメータ | 型 | 必須 | 説明 |
| --------- | ---- | -------- | ----------- |
| `siteId` | string | いいえ | SharePointサイトのID |
| `driveId` | string | いいえ | ドキュメントライブラリドライブのID。提供されない場合、デフォルトドライブを使用します。 |
| `folderPath` | string | いいえ | ドキュメントライブラリ内のオプションのフォルダパス(例:/Documents/Subfolder |
| `fileName` | string | いいえ | オプション:アップロードされるファイル名を上書きする |
| `files` | file[] | いいえ | SharePointにアップロードするファイル |
#### 出力
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `uploadedFiles` | array | アップロードされたファイルオブジェクトの配列 |
## 注意事項
- カテゴリ: `tools`
- カテゴリ: `tools`
- タイプ: `sharepoint`

View File

@@ -78,7 +78,8 @@ Slack APIを通じてSlackチャンネルまたはユーザーにメッセージ
| `authMethod` | string | いいえ | 認証方法oauth または bot_token |
| `botToken` | string | いいえ | カスタムボット用のボットトークン |
| `channel` | string | はい | 対象のSlackチャンネル#general |
| `text` | string | はい | 送信するメッセージテキストSlack mrkdwnフォーマットをサポート) |
| `text` | string | はい | 送信するメッセージテキストSlack mrkdwn形式をサポート) |
| `files` | file[] | いいえ | メッセージに添付するファイル |
#### 出力

View File

@@ -202,7 +202,29 @@ Supabaseテーブルにデータを挿入または更新するアップサー
| `message` | string | 操作ステータスメッセージ |
| `results` | array | アップサートされたレコードの配列 |
### `supabase_vector_search`
Supabaseテーブルでpgvectorを使用して類似性検索を実行する
#### 入力
| パラメータ | 型 | 必須 | 説明 |
| --------- | ---- | -------- | ----------- |
| `projectId` | string | はい | あなたのSupabaseプロジェクトIDjdrkgepadsdopsntdlom |
| `functionName` | string | はい | ベクトル検索を実行するPostgreSQL関数の名前match_documents |
| `queryEmbedding` | array | はい | 類似アイテムを検索するためのクエリベクトル/埋め込み |
| `matchThreshold` | number | いいえ | 最小類似度しきい値0-1、通常は0.7-0.9 |
| `matchCount` | number | いいえ | 返す結果の最大数デフォルト10 |
| `apiKey` | string | はい | あなたのSupabaseサービスロールシークレットキー |
#### 出力
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `message` | string | 操作ステータスメッセージ |
| `results` | array | ベクトル検索からの類似度スコア付きレコードの配列。各レコードには、クエリベクトルとの類似度を示す類似度フィールド0-1が含まれます。 |
## 注意事項
- カテゴリ: `tools`
- カテゴリ: `tools`
- タイプ: `supabase`

View File

@@ -189,6 +189,26 @@ Telegram Bot APIを通じてTelegramチャンネルまたはユーザーにア
| `message` | string | 成功またはエラーメッセージ |
| `data` | object | オプションのメディアを含むTelegramメッセージデータ |
### `telegram_send_document`
Telegram Bot APIを通じて、TelegramチャンネルやユーザーにドキュメントPDF、ZIP、DOCなどを送信します。
#### 入力
| パラメータ | 型 | 必須 | 説明 |
| --------- | ---- | -------- | ----------- |
| `botToken` | string | はい | あなたのTelegram Bot APIトークン |
| `chatId` | string | はい | 対象のTelegramチャットID |
| `files` | file[] | いいえ | 送信するドキュメントファイルPDF、ZIP、DOCなど。最大サイズ50MB |
| `caption` | string | いいえ | ドキュメントのキャプション(任意) |
#### 出力
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `message` | string | 成功またはエラーメッセージ |
| `data` | object | ドキュメントを含むTelegramメッセージデータ |
## 注意事項
- カテゴリー: `tools`

View File

@@ -59,8 +59,9 @@ Visionをワークフローに統合します。ビジョンモデルで画像
| パラメータ | 型 | 必須 | 説明 |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | はい | 選択したモデルプロバイダーのAPIキー |
| `imageUrl` | string | い | 公開アクセス可能な画像URL |
| `model` | string | いいえ | 使用するビジョンモデル \(gpt-4o, claude-3-opus-20240229, など\) |
| `imageUrl` | string | いいえ | 公開アクセス可能な画像URL |
| `imageFile` | file | いいえ | 分析する画像ファイル |
| `model` | string | いいえ | 使用するビジョンモデルgpt-4o、claude-3-opus-20240229など |
| `prompt` | string | いいえ | 画像分析用のカスタムプロンプト |
#### 出力

View File

@@ -0,0 +1,145 @@
---
title: Webflow
description: Webflow CMSコレクションを管理する
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="webflow"
color="#E0E0E0"
icon={true}
iconSvg={`<svg className="block-icon"
viewBox='0 0 1080 674'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M1080 0L735.386 673.684H411.695L555.916 394.481H549.445C430.464 548.934 252.942 650.61 -0.000488281 673.684V398.344C-0.000488281 398.344 161.813 388.787 256.938 288.776H-0.000488281V0.0053214H288.771V237.515L295.252 237.489L413.254 0.0053214H631.644V236.009L638.126 235.999L760.555 0H1080Z'
fill='#146EF5'
/>
</svg>`}
/>
{/* MANUAL-CONTENT-START:intro */}
[Webflow](https://webflow.com/)は、コードを書かずに応答性の高いウェブサイトを構築できる強力なビジュアルウェブデザインプラットフォームです。視覚的なデザインインターフェースと堅牢なCMSコンテンツ管理システムを組み合わせており、ウェブサイト用の動的コンテンツを作成、管理、公開することができます。
Webflowでは以下のことが可能です
- **視覚的にデザインする**クリーンでセマンティックなHTML/CSSコードを生成する視覚エディタで、カスタムウェブサイトを作成
- **動的にコンテンツを管理する**CMSを使用してブログ投稿、製品、チームメンバー、またはカスタムデータなどの構造化されたコンテンツのコレクションを作成
- **即時に公開する**サイトをWebflowのホスティングにデプロイするか、カスタムホスティング用にコードをエクスポート
- **レスポンシブデザインを作成する**:デスクトップ、タブレット、モバイルデバイスでシームレスに動作するサイトを構築
- **コレクションをカスタマイズする**:コンテンツタイプのカスタムフィールドとデータ構造を定義
- **コンテンツ更新を自動化する**APIを通じてCMSコンテンツをプログラムで管理
Simでは、Webflow統合によりエージェントがAPI認証を通じてWebflow CMSコレクションとシームレスに連携できます。これにより、AIが生成したコンテンツからブログ投稿を自動作成したり、製品情報を更新したり、チームメンバープロフィールを管理したり、動的コンテンツ生成のためにCMSアイテムを取得したりするなど、強力な自動化シナリオが可能になります。エージェントは既存のアイテムをリストしてコンテンツを閲覧したり、IDで特定のアイテムを取得したり、新しいエントリを作成して新鮮なコンテンツを追加したり、既存のアイテムを更新して情報を最新の状態に保ったり、古いコンテンツを削除したりできます。この統合により、AIワークフローとWebflow CMSの間のギャップが埋まり、自動化されたコンテンツ管理、動的なウェブサイト更新、合理化されたコンテンツワークフローが可能になり、手動介入なしにサイトを常に新鮮で最新の状態に保つことができます。
{/* MANUAL-CONTENT-END */}
## 使用方法
Webflow CMSをワークフローに統合します。Webflow CMSコレクションのアイテムを作成、取得、一覧表示、更新、または削除できます。Webflowのコンテンツをプログラムで管理します。トリガーモードでは、コレクションアイテムが変更されたり、フォームが送信されたりしたときにワークフローをトリガーするために使用できます。
## ツール
### `webflow_list_items`
Webflow CMSコレクションからすべてのアイテムを一覧表示する
#### 入力
| パラメータ | 型 | 必須 | 説明 |
| --------- | ---- | -------- | ----------- |
| `collectionId` | string | はい | コレクションのID |
| `offset` | number | いいえ | ページネーション用のオフセット(オプション) |
| `limit` | number | いいえ | 返すアイテムの最大数オプション、デフォルト100 |
#### 出力
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `items` | json | コレクションアイテムの配列 |
| `metadata` | json | クエリに関するメタデータ |
### `webflow_get_item`
Webflow CMSコレクションから単一のアイテムを取得する
#### 入力
| パラメータ | 型 | 必須 | 説明 |
| --------- | ---- | -------- | ----------- |
| `collectionId` | string | はい | コレクションのID |
| `itemId` | string | はい | 取得するアイテムのID |
#### 出力
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `item` | json | 取得したアイテムオブジェクト |
| `metadata` | json | 取得したアイテムに関するメタデータ |
### `webflow_create_item`
Webflow CMSコレクションに新しいアイテムを作成する
#### 入力
| パラメータ | 型 | 必須 | 説明 |
| --------- | ---- | -------- | ----------- |
| `collectionId` | string | はい | コレクションのID |
| `fieldData` | json | はい | 新しいアイテムのフィールドデータJSONオブジェクト形式。キーはコレクションのフィールド名と一致する必要があります。 |
#### 出力
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `item` | json | 作成されたアイテムオブジェクト |
| `metadata` | json | 作成されたアイテムに関するメタデータ |
### `webflow_update_item`
Webflow CMSコレクション内の既存アイテムを更新する
#### 入力
| パラメータ | 型 | 必須 | 説明 |
| --------- | ---- | -------- | ----------- |
| `collectionId` | string | はい | コレクションのID |
| `itemId` | string | はい | 更新するアイテムのID |
| `fieldData` | json | はい | 更新するフィールドデータJSONオブジェクト形式。変更したいフィールドのみを含めてください。 |
#### 出力
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `item` | json | 更新されたアイテムオブジェクト |
| `metadata` | json | 更新されたアイテムに関するメタデータ |
### `webflow_delete_item`
Webflow CMSコレクションからアイテムを削除する
#### 入力
| パラメータ | 型 | 必須 | 説明 |
| --------- | ---- | -------- | ----------- |
| `collectionId` | string | はい | コレクションのID |
| `itemId` | string | はい | 削除するアイテムのID |
#### 出力
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 削除が成功したかどうか |
| `metadata` | json | 削除に関するメタデータ |
## 注意事項
- カテゴリー: `tools`
- タイプ: `webflow`

View File

@@ -71,7 +71,8 @@ Sim 中的 Discord 组件使用高效的延迟加载,仅在需要时获取数
| `botToken` | string | 是 | 用于身份验证的机器人令牌 |
| `channelId` | string | 是 | 要发送消息的 Discord 频道 ID |
| `content` | string | 否 | 消息的文本内容 |
| `serverId` | string | 是 | Discord 服务器 IDguild ID |
| `serverId` | string | 是 | Discord 服务器 ID \(公会 ID\) |
| `files` | file[] | 否 | 要附加到消息的文件 |
#### 输出

View File

@@ -84,11 +84,12 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 必需 | 描述 |
| --------- | ---- | -------- | ----------- |
| `fileName` | string | 是 | 要上传文件名称 |
| `content` | string | | 要上传的文件内容 |
| `mimeType` | string | 否 | 要上传文件的 MIME 类型 |
| `folderSelector` | string | 否 | 选择上传文件的文件夹 |
| `folderId` | string | 否 | 要上传文件的文件夹 ID内部使用 |
| `fileName` | string | 是 | 要上传文件名称 |
| `file` | file | | 要上传的二进制文件 \(UserFile 对象\) |
| `content` | string | 否 | 要上传的文本内容 \(使用此项或 file不可同时使用\) |
| `mimeType` | string | 否 | 上传文件的 MIME 类型 \(如果未提供,将从文件中自动检测\) |
| `folderSelector` | string | 否 | 选择要上传文件的文件夹 |
| `folderId` | string | 否 | 要上传文件的文件夹 ID \(内部使用\) |
#### 输出

View File

@@ -135,6 +135,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| --------- | ---- | -------- | ----------- |
| `chatId` | string | 是 | 要写入的聊天 ID |
| `content` | string | 是 | 要写入消息的内容 |
| `files` | file[] | 否 | 要附加到消息的文件 |
#### 输出
@@ -184,6 +185,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| `teamId` | string | 是 | 要写入的团队 ID |
| `channelId` | string | 是 | 要写入的频道 ID |
| `content` | string | 是 | 要写入频道的内容 |
| `files` | file[] | 否 | 要附加到消息的文件 |
#### 输出

View File

@@ -62,10 +62,11 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 必需 | 描述 |
| --------- | ---- | -------- | ----------- |
| `fileName` | string | 是 | 要上传文件 |
| `content` | string | | 要上传的文件内容 |
| `folderSelector` | string | 否 | 选择要上传文件的文件夹 |
| `manualFolderId` | string | 否 | 手动输入的文件夹 ID高级模式 |
| `fileName` | string | 是 | 要上传文件的名称 |
| `file` | file | | 要上传的文件 \(二进制\) |
| `content` | string | 否 | 要上传的文本内容 \(如果未提供文件\) |
| `folderSelector` | string | 否 | 选择上传文件的文件夹 |
| `manualFolderId` | string | 否 | 手动输入的文件夹 ID \(高级模式\) |
#### 输出

View File

@@ -152,12 +152,13 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 必需 | 描述 |
| --------- | ---- | -------- | ----------- |
| `to` | string | 是 | 收件人电子邮件地址 |
| `subject` | string | 是 | 电子邮件主题 |
| `body` | string | 是 | 电子邮件正文内容 |
| `subject` | string | 是 | 邮件主题 |
| `body` | string | 是 | 邮件正文内容 |
| `replyToMessageId` | string | 否 | 要回复的消息 ID用于线程化 |
| `conversationId` | string | 否 | 用于线程化的会话 ID |
| `cc` | string | 否 | 抄送收件人(逗号分隔) |
| `bcc` | string | 否 | 密送收件人(逗号分隔) |
| `cc` | string | 否 | 抄送收件人(逗号分隔) |
| `bcc` | string | 否 | 密送收件人(逗号分隔) |
| `attachments` | file[] | 否 | 要附加到邮件的文件 |
#### 输出
@@ -176,11 +177,12 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 必需 | 描述 |
| --------- | ---- | -------- | ----------- |
| `to` | string | 是 | 收件人邮箱地址 |
| `to` | string | 是 | 收件人电子邮件地址 |
| `subject` | string | 是 | 邮件主题 |
| `body` | string | 是 | 邮件正文内容 |
| `cc` | string | 否 | 抄送收件人(逗号分隔) |
| `bcc` | string | 否 | 密送收件人(逗号分隔) |
| `cc` | string | 否 | 抄送收件人(逗号分隔) |
| `bcc` | string | 否 | 密送收件人(逗号分隔) |
| `attachments` | file[] | 否 | 要附加到邮件草稿的文件 |
#### 输出

View File

@@ -199,6 +199,26 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| --------- | ---- | ----------- |
| `item` | object | 创建的 SharePoint 列表项 |
### `sharepoint_upload_file`
将文件上传到 SharePoint 文档库
#### 输入
| 参数 | 类型 | 必需 | 描述 |
| --------- | ---- | -------- | ----------- |
| `siteId` | string | 否 | SharePoint 站点的 ID |
| `driveId` | string | 否 | 文档库(驱动器)的 ID。如果未提供则使用默认驱动器。 |
| `folderPath` | string | 否 | 文档库中的可选文件夹路径(例如,/Documents/Subfolder |
| `fileName` | string | 否 | 可选:覆盖上传文件的名称 |
| `files` | file[] | 否 | 要上传到 SharePoint 的文件 |
#### 输出
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `uploadedFiles` | array | 上传文件对象的数组 |
## 注意事项
- 类别:`tools`

View File

@@ -78,6 +78,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| `botToken` | string | 否 | 自定义 Bot 的令牌 |
| `channel` | string | 是 | 目标 Slack 频道(例如,#general |
| `text` | string | 是 | 要发送的消息文本(支持 Slack mrkdwn 格式) |
| `files` | file[] | 否 | 要附加到消息的文件 |
#### 输出

View File

@@ -202,6 +202,28 @@ Sim 的 Supabase 集成使您能够轻松地将代理工作流连接到您的 Su
| `message` | string | 操作状态消息 |
| `results` | array | 已 upsert 的记录数组 |
### `supabase_vector_search`
在 Supabase 表中使用 pgvector 执行相似性搜索
#### 输入
| 参数 | 类型 | 必需 | 描述 |
| --------- | ---- | -------- | ----------- |
| `projectId` | string | 是 | 您的 Supabase 项目 ID \(例如jdrkgepadsdopsntdlom\) |
| `functionName` | string | 是 | 执行向量搜索的 PostgreSQL 函数名称 \(例如match_documents\) |
| `queryEmbedding` | array | 是 | 要搜索相似项的查询向量/嵌入 |
| `matchThreshold` | number | 否 | 最小相似度阈值 \(0-1\),通常为 0.7-0.9 |
| `matchCount` | number | 否 | 返回结果的最大数量 \(默认值10\) |
| `apiKey` | string | 是 | 您的 Supabase 服务角色密钥 |
#### 输出
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `message` | string | 操作状态消息 |
| `results` | array | 包含向量搜索相似度分数的记录数组。每条记录包括一个相似度字段 \(0-1\),表示与查询向量的相似程度。 |
## 注意事项
- 类别:`tools`

View File

@@ -189,6 +189,26 @@ Telegram 的主要功能包括:
| `message` | 字符串 | 成功或错误消息 |
| `data` | 对象 | 包含可选媒体的 Telegram 消息数据 |
### `telegram_send_document`
通过 Telegram Bot API 将文档PDF、ZIP、DOC 等)发送到 Telegram 频道或用户。
#### 输入
| 参数 | 类型 | 必需 | 描述 |
| --------- | ---- | -------- | ----------- |
| `botToken` | string | 是 | 您的 Telegram Bot API 令牌 |
| `chatId` | string | 是 | 目标 Telegram 聊天 ID |
| `files` | file[] | 否 | 要发送的文档文件PDF、ZIP、DOC 等。最大大小50MB |
| `caption` | string | 否 | 文档标题(可选) |
#### 输出
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `message` | string | 成功或错误消息 |
| `data` | object | 包含文档的 Telegram 消息数据 |
## 注意
- 类别:`tools`

View File

@@ -59,9 +59,10 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 必需 | 描述 |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | 是 | 所选模型提供商的 API 密钥 |
| `imageUrl` | string | | 可公开访问的图 URL |
| `model` | string | 否 | 要使用的视觉模型gpt-4o、claude-3-opus-20240229 等) |
| `prompt` | string | 否 | 图像分析的自定义提示 |
| `imageUrl` | string | | 可公开访问的图 URL |
| `imageFile` | file | 否 | 要分析的图片文件 |
| `model` | string | 否 | 要使用的视觉模型 \(gpt-4o, claude-3-opus-20240229 等\) |
| `prompt` | string | 否 | 用于图像分析的自定义提示 |
#### 输出

View File

@@ -0,0 +1,144 @@
---
title: Webflow
description: 管理 Webflow CMS 集合
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="webflow"
color="#E0E0E0"
icon={true}
iconSvg={`<svg className="block-icon"
viewBox='0 0 1080 674'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M1080 0L735.386 673.684H411.695L555.916 394.481H549.445C430.464 548.934 252.942 650.61 -0.000488281 673.684V398.344C-0.000488281 398.344 161.813 388.787 256.938 288.776H-0.000488281V0.0053214H288.771V237.515L295.252 237.489L413.254 0.0053214H631.644V236.009L638.126 235.999L760.555 0H1080Z'
fill='#146EF5'
/>
</svg>`}
/>
{/* MANUAL-CONTENT-START:intro */}
[Webflow](https://webflow.com/) 是一个强大的可视化网页设计平台,能够让您无需编写代码即可构建响应式网站。它结合了可视化设计界面和强大的 CMS内容管理系统使您能够为网站创建、管理和发布动态内容。
使用 Webflow您可以
- **可视化设计**:使用可视化编辑器创建自定义网站,该编辑器会生成干净、语义化的 HTML/CSS 代码
- **动态管理内容**:使用 CMS 创建结构化内容集合,例如博客文章、产品、团队成员或任何自定义数据
- **即时发布**:将您的网站部署到 Webflow 的托管服务,或导出代码以进行自定义托管
- **创建响应式设计**:构建在桌面、平板电脑和移动设备上无缝运行的网站
- **自定义集合**:为您的内容类型定义自定义字段和数据结构
- **自动更新内容**:通过 API 编程方式管理您的 CMS 内容
在 Sim 中Webflow 集成使您的代理能够通过 API 身份验证无缝与 Webflow CMS 集合交互。这支持强大的自动化场景,例如从 AI 生成的内容中自动创建博客文章、更新产品信息、管理团队成员资料以及检索 CMS 项目以生成动态内容。您的代理可以列出现有项目以浏览内容,通过 ID 检索特定项目,创建新条目以添加新内容,更新现有项目以保持信息最新,以及删除过时内容。此集成弥合了您的 AI 工作流与 Webflow CMS 之间的差距,实现了自动化内容管理、动态网站更新和简化的内容工作流,使您的网站无需人工干预即可保持新鲜和最新。{/* MANUAL-CONTENT-END */}
## 使用说明
将 Webflow CMS 集成到工作流程中。可以创建、获取、列出、更新或删除 Webflow CMS 集合中的项目。以编程方式管理您的 Webflow 内容。可以在触发模式下使用,当集合项目发生变化或提交表单时触发工作流程。
## 工具
### `webflow_list_items`
列出 Webflow CMS 集合中的所有项目
#### 输入
| 参数 | 类型 | 必需 | 描述 |
| --------- | ---- | -------- | ----------- |
| `collectionId` | string | 是 | 集合的 ID |
| `offset` | number | 否 | 分页偏移量(可选) |
| `limit` | number | 否 | 返回的最大项目数可选默认值100 |
#### 输出
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `items` | json | 集合项目的数组 |
| `metadata` | json | 查询的元数据 |
### `webflow_get_item`
从 Webflow CMS 集合中获取单个项目
#### 输入
| 参数 | 类型 | 必需 | 描述 |
| --------- | ---- | -------- | ----------- |
| `collectionId` | string | 是 | 集合的 ID |
| `itemId` | string | 是 | 要检索的项目 ID |
#### 输出
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `item` | json | 检索到的项目对象 |
| `metadata` | json | 检索到的项目的元数据 |
### `webflow_create_item`
在 Webflow CMS 集合中创建一个新项目
#### 输入
| 参数 | 类型 | 必需 | 描述 |
| --------- | ---- | -------- | ----------- |
| `collectionId` | string | 是 | 集合的 ID |
| `fieldData` | json | 是 | 新项目的字段数据,格式为 JSON 对象。键名应与集合字段名称匹配。 |
#### 输出
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `item` | json | 创建的项目对象 |
| `metadata` | json | 关于创建项目的元数据 |
### `webflow_update_item`
更新 Webflow CMS 集合中的现有项目
#### 输入
| 参数 | 类型 | 必需 | 描述 |
| --------- | ---- | -------- | ----------- |
| `collectionId` | string | 是 | 集合的 ID |
| `itemId` | string | 是 | 要更新项目的 ID |
| `fieldData` | json | 是 | 要更新的字段数据,格式为 JSON 对象。仅包含您想更改的字段。 |
#### 输出
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `item` | json | 更新的项目对象 |
| `metadata` | json | 关于更新项目的元数据 |
### `webflow_delete_item`
从 Webflow CMS 集合中删除项目
#### 输入
| 参数 | 类型 | 必需 | 描述 |
| --------- | ---- | -------- | ----------- |
| `collectionId` | string | 是 | 集合的 ID |
| `itemId` | string | 是 | 要删除项目的 ID |
#### 输出
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 是否删除成功 |
| `metadata` | json | 有关删除的元数据 |
## 注意
- 类别:`tools`
- 类型:`webflow`

View File

@@ -439,7 +439,7 @@ checksums:
content/9: 10ec1e8eaecc6f3d30cfa63749e968b7
content/10: f4b856e774ec5e995c437201d22036ee
content/11: 371d0e46b4bd2c23f559b8bc112f6955
content/12: 4e324b7b7dc434eea83f6b5d528ab930
content/12: ae0448bae89d0f81d8a28361acc2d67e
content/13: bcadfc362b69078beee0088e5936c98b
content/14: 0c8b7227f9df8b60ee36d80de899961e
content/15: b3f310d5ef115bea5a8b75bf25d7ea9a
@@ -594,8 +594,14 @@ checksums:
content/47: f18d03fa59e3997a6d951e333d99c822
content/48: bcadfc362b69078beee0088e5936c98b
content/49: 8779f29ccc257e421d64c071949b81fb
content/50: b3f310d5ef115bea5a8b75bf25d7ea9a
content/51: e1fa627fb51f09989752a9bddf0ebb58
content/50: 8782b269ede5cb41fecbd34072214c6c
content/51: 79a5a4e61b55cdb65a6915ef08daeb93
content/52: 371d0e46b4bd2c23f559b8bc112f6955
content/53: 7653b942236c8ad404c6962988fda3d3
content/54: bcadfc362b69078beee0088e5936c98b
content/55: 87cd2cb3a40d59957ca9ad1762c7a816
content/56: b3f310d5ef115bea5a8b75bf25d7ea9a
content/57: e1fa627fb51f09989752a9bddf0ebb58
ac9313ccf1478cdf17fe25081b6b78be:
meta/title: df20085ae7dc009c9b6532113064c6bb
meta/description: 0a0c4af79216512ddc68df973afed52c
@@ -672,8 +678,14 @@ checksums:
content/44: b37f6692238f3a45bfcf106f0d192b21
content/45: bcadfc362b69078beee0088e5936c98b
content/46: 1788748095a805b62a0e21403789dad7
content/47: b3f310d5ef115bea5a8b75bf25d7ea9a
content/48: d2ca5a540458c526aebabd2f9ff3fd03
content/47: dd231637d3d327f0daf546fe92594ac6
content/48: 97f32d4cd73f2375eb8540e71300dad1
content/49: 371d0e46b4bd2c23f559b8bc112f6955
content/50: ce131f6bcfe8a6aeb340229a107febc3
content/51: bcadfc362b69078beee0088e5936c98b
content/52: 67cb33e6ab252c7130cac02728311dc8
content/53: b3f310d5ef115bea5a8b75bf25d7ea9a
content/54: d2ca5a540458c526aebabd2f9ff3fd03
51862d3525bcbc53275ead2362197752:
meta/title: 64d1e9a0e5ae9fdb055a5366c22433fc
meta/description: 1f9e8e75ebe525fc5bb53d9f5e0dd3a8
@@ -732,7 +744,7 @@ checksums:
content/12: 8ee83eff32425b2c52929284e8485c20
content/13: 6cda87dc9837779f4572ed70b87a5654
content/14: 371d0e46b4bd2c23f559b8bc112f6955
content/15: 57e7f55217bb573b93ff0e6e37bd58a7
content/15: ba09630dea12ec37bbe8a4f490150c8a
content/16: bcadfc362b69078beee0088e5936c98b
content/17: 1f31e78210417a7f251f29e0b93a8528
content/18: 05540cb3028d4d781521c14e5f9e3835
@@ -803,8 +815,14 @@ checksums:
content/48: 5d7221454e6adf5eed5f0c02b24eb9e0
content/49: bcadfc362b69078beee0088e5936c98b
content/50: f42ef799c350c30117cb100b98a9d526
content/51: b3f310d5ef115bea5a8b75bf25d7ea9a
content/52: 6480fe7f4ab32b223c32deab16b6922b
content/51: a13d828344becb9f2f7c2a788dda30a5
content/52: 5d0ccd6bc37305555c6a48cec645fe48
content/53: 371d0e46b4bd2c23f559b8bc112f6955
content/54: bfdd461ff9444c6d81c5fb0b9cee3c97
content/55: bcadfc362b69078beee0088e5936c98b
content/56: ce194fa4c0560d03b31f53bbe0e1cf94
content/57: b3f310d5ef115bea5a8b75bf25d7ea9a
content/58: 6480fe7f4ab32b223c32deab16b6922b
d36fb89d311a649e49173c0998fad09f:
meta/title: 25b7006e238bd277acad0336b59e6581
meta/description: 4c972c767abaf134b90c2e9bf6619876
@@ -1081,13 +1099,13 @@ checksums:
content/9: 822829fd1c0500496b72d5a0abafeb41
content/10: 4a8b7fc9ab62f88061e4b21ac5b10947
content/11: 371d0e46b4bd2c23f559b8bc112f6955
content/12: ba245342edd3cc56355ba84be1d45aef
content/12: 9edeb4b9fca066dd5bf11d0c3a2cb8db
content/13: bcadfc362b69078beee0088e5936c98b
content/14: 558c49a1186440a5a2254dcfbd7b74f7
content/15: 64a8e14453f9063bd8d89d43b7833fa8
content/16: c5800c27cab920bd65932ffc703e6fc6
content/17: 371d0e46b4bd2c23f559b8bc112f6955
content/18: 96832958c23fb2c9cefb35ee9a63cded
content/18: c886c72190889c7b55a8f38a294ecf38
content/19: bcadfc362b69078beee0088e5936c98b
content/20: 9f1a5d98944fc79dbcd0b01e0a82fe28
content/21: d80aeca589a53111bf1695cdd331cf5b
@@ -1140,7 +1158,7 @@ checksums:
content/10: 6df7e096bafa5efda818db1ab65841fd
content/11: 646990da68ff77ca02dd172f830f12f0
content/12: 371d0e46b4bd2c23f559b8bc112f6955
content/13: 6e6d228bc03c82d986ab0f60470fd1ea
content/13: 6979ff1359b3ede098d71e35fabd6a05
content/14: bcadfc362b69078beee0088e5936c98b
content/15: c25b7418fd96b560b79af512cba4e7f1
content/16: 3d44fa28ed12c73c2c9178b8570e516d
@@ -1340,7 +1358,7 @@ checksums:
content/15: 9a44ad2a020c00d5c50250ee0c543ae2
content/16: 3f013e8cf7537231614d510c9bcdf1b2
content/17: 371d0e46b4bd2c23f559b8bc112f6955
content/18: e0e86275deb46b5c7b516849de4d70fa
content/18: 48389c9abe6b4ab2f836686265618a0e
content/19: bcadfc362b69078beee0088e5936c98b
content/20: 5e472d191f58641d1b0416383875f22b
content/21: 25d767f79c8b785d24657d66091e479b
@@ -1352,7 +1370,7 @@ checksums:
content/27: 89ae14a70cbc288e4f816b710c7ef48e
content/28: 311c355c6e43246e52112ee300df8db5
content/29: 371d0e46b4bd2c23f559b8bc112f6955
content/30: f28566e14c56ba74700c17a6ddebc994
content/30: 44d1f81db519e167c575e2ac6d34048c
content/31: bcadfc362b69078beee0088e5936c98b
content/32: d977e473bf5c4e014362b2f7c5e4ae7b
content/33: b3f310d5ef115bea5a8b75bf25d7ea9a
@@ -1820,7 +1838,7 @@ checksums:
content/11: 871d6ecf3f593f3b20f5c3c8ded1ecc6
content/12: 30072675a0dffbddc0e85bd3e34764ec
content/13: 371d0e46b4bd2c23f559b8bc112f6955
content/14: fb8b01d27288339ac6e87c630abca238
content/14: f07ac61557b436d01cbc1fbc2684d39f
content/15: bcadfc362b69078beee0088e5936c98b
content/16: 1e23d729d38b5da93b6598e73e4ab612
content/17: 24991dd332b7a6b8f8091e3e219b542f
@@ -2180,7 +2198,7 @@ checksums:
content/13: 4a01bece58a2e9ac96bea57ed39e35d5
content/14: f49c586052b88ec4bf80d844c0c7ca3e
content/15: 371d0e46b4bd2c23f559b8bc112f6955
content/16: f2a31106e2f78ba7e12a78e234ba1e9a
content/16: ff83558d232dd95d39838a2ca868dfed
content/17: bcadfc362b69078beee0088e5936c98b
content/18: 6b7691d4977f1b6520a8b3236b993358
content/19: 96f2aa27a8d3f74035575de3da6c47c4
@@ -4342,3 +4360,47 @@ checksums:
content/58: 74af50eceb2a5f514301c497a8e64030
content/59: b3f310d5ef115bea5a8b75bf25d7ea9a
content/60: 549a9bd7ff92264fbd18c9d6e616594b
65891ef7e29a3ad2464222a998549ff5:
meta/title: b35b5211a53c68cea3a3f0995099c24b
meta/description: c12cfd50357fba2ffebaf79750b7d76d
content/0: 1b031fb0c62c46b177aeed5c3d3f8f80
content/1: 49bca8f0502318620cbebb686a4fd5b3
content/2: 4d30444228f205f237d0c194fe4dd3d9
content/3: 8d8c30d8ee25edbf854d7d2ac1e2dbd8
content/4: e4d2f4146482f4724c177815103e026b
content/5: 68b1bf876a3ba2faf8fba18ae4e1bc19
content/6: 821e6394b0a953e2b0842b04ae8f3105
content/7: 22ab0fa161f448ca26998e101805e59e
content/8: 9c8aa3f09c9b2bd50ea4cdff3598ea4e
content/9: 5914baadfaf2ca26d54130a36dd5ed29
content/10: 25507380ac7d9c7f8cf9f5256c6a0dbb
content/11: 371d0e46b4bd2c23f559b8bc112f6955
content/12: e7fb612c3323c1e6b05eacfcea360d34
content/13: bcadfc362b69078beee0088e5936c98b
content/14: e5f830d6049ff79a318110098e5e0130
content/15: 711e90714806b91f93923018e82ad2e9
content/16: 0f3f7d9699d7397cb3a094c3229329ee
content/17: 371d0e46b4bd2c23f559b8bc112f6955
content/18: c53b5b8f901066e63fe159ad2fa5e6e0
content/19: bcadfc362b69078beee0088e5936c98b
content/20: 5f2afdd49c3ac13381401c69d1eca22a
content/21: cc4baa9096fafa4c6276f6136412ba66
content/22: 676f76e8a7154a576d7fa20b245cef70
content/23: 371d0e46b4bd2c23f559b8bc112f6955
content/24: c67c387eb7e274ee7c07b7e1748afce1
content/25: bcadfc362b69078beee0088e5936c98b
content/26: a6ffebda549ad5b903a66c7d9ac03a20
content/27: 0dadd51cde48d6ea75b29ec3ee4ade56
content/28: cdc74f6483a0b4e9933ecdd92ed7480f
content/29: 371d0e46b4bd2c23f559b8bc112f6955
content/30: 4cda10aa374e1a46d60ad14eeaa79100
content/31: bcadfc362b69078beee0088e5936c98b
content/32: 5f221421953a0e760ead7388cbf66561
content/33: a3c0372590cef72d5d983dbc8dbbc2cb
content/34: 1402e53c08bdd8a741f44b2d66fcd003
content/35: 371d0e46b4bd2c23f559b8bc112f6955
content/36: 028e579a28e55def4fbc59f39f4610b7
content/37: bcadfc362b69078beee0088e5936c98b
content/38: 4fe4260da2f137679ce2fa42cffcf56a
content/39: b3f310d5ef115bea5a8b75bf25d7ea9a
content/40: 89bdbd886b24f2aaec635a2b4119660a

View File

@@ -0,0 +1,69 @@
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { getOAuthToken } from '@/app/api/auth/oauth/utils'
const logger = createLogger('WebflowCollectionsAPI')
export const dynamic = 'force-dynamic'
export async function GET(request: NextRequest) {
try {
const session = await getSession()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { searchParams } = new URL(request.url)
const siteId = searchParams.get('siteId')
if (!siteId) {
return NextResponse.json({ error: 'Missing siteId parameter' }, { status: 400 })
}
const accessToken = await getOAuthToken(session.user.id, 'webflow')
if (!accessToken) {
return NextResponse.json(
{ error: 'No Webflow access token found. Please connect your Webflow account.' },
{ status: 404 }
)
}
const response = await fetch(`https://api.webflow.com/v2/sites/${siteId}/collections`, {
headers: {
Authorization: `Bearer ${accessToken}`,
accept: 'application/json',
},
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
logger.error('Failed to fetch Webflow collections', {
status: response.status,
error: errorData,
siteId,
})
return NextResponse.json(
{ error: 'Failed to fetch Webflow collections', details: errorData },
{ status: response.status }
)
}
const data = await response.json()
const collections = data.collections || []
const formattedCollections = collections.map((collection: any) => ({
id: collection.id,
name: collection.displayName || collection.slug || collection.id,
}))
return NextResponse.json({ collections: formattedCollections }, { status: 200 })
} catch (error: any) {
logger.error('Error fetching Webflow collections', error)
return NextResponse.json(
{ error: 'Internal server error', details: error.message },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,61 @@
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { getOAuthToken } from '@/app/api/auth/oauth/utils'
const logger = createLogger('WebflowSitesAPI')
export const dynamic = 'force-dynamic'
export async function GET(request: NextRequest) {
try {
const session = await getSession()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const accessToken = await getOAuthToken(session.user.id, 'webflow')
if (!accessToken) {
return NextResponse.json(
{ error: 'No Webflow access token found. Please connect your Webflow account.' },
{ status: 404 }
)
}
const response = await fetch('https://api.webflow.com/v2/sites', {
headers: {
Authorization: `Bearer ${accessToken}`,
accept: 'application/json',
},
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
logger.error('Failed to fetch Webflow sites', {
status: response.status,
error: errorData,
})
return NextResponse.json(
{ error: 'Failed to fetch Webflow sites', details: errorData },
{ status: response.status }
)
}
const data = await response.json()
const sites = data.sites || []
const formattedSites = sites.map((site: any) => ({
id: site.id,
name: site.displayName || site.shortName || site.id,
}))
return NextResponse.json({ sites: formattedSites }, { status: 200 })
} catch (error: any) {
logger.error('Error fetching Webflow sites', error)
return NextResponse.json(
{ error: 'Internal server error', details: error.message },
{ status: 500 }
)
}
}

View File

@@ -428,6 +428,26 @@ export async function POST(request: NextRequest) {
}
// --- End Outlook specific logic ---
// --- Webflow webhook setup ---
if (savedWebhook && provider === 'webflow') {
logger.info(
`[${requestId}] Webflow provider detected. Attempting to create webhook in Webflow.`
)
try {
await createWebflowWebhookSubscription(request, userId, savedWebhook, requestId)
} catch (err) {
logger.error(`[${requestId}] Error creating Webflow webhook`, err)
return NextResponse.json(
{
error: 'Failed to create webhook in Webflow',
details: err instanceof Error ? err.message : 'Unknown error',
},
{ status: 500 }
)
}
}
// --- End Webflow specific logic ---
const status = targetWebhookId ? 200 : 201
return NextResponse.json({ webhook: savedWebhook }, { status })
} catch (error: any) {
@@ -548,3 +568,136 @@ async function createAirtableWebhookSubscription(
)
}
}
// Helper function to create the webhook subscription in Webflow
async function createWebflowWebhookSubscription(
request: NextRequest,
userId: string,
webhookData: any,
requestId: string
) {
try {
const { path, providerConfig } = webhookData
const { siteId, triggerId, collectionId, formId } = providerConfig || {}
if (!siteId) {
logger.warn(`[${requestId}] Missing siteId for Webflow webhook creation.`, {
webhookId: webhookData.id,
})
throw new Error('Site ID is required to create Webflow webhook')
}
if (!triggerId) {
logger.warn(`[${requestId}] Missing triggerId for Webflow webhook creation.`, {
webhookId: webhookData.id,
})
throw new Error('Trigger type is required to create Webflow webhook')
}
const accessToken = await getOAuthToken(userId, 'webflow')
if (!accessToken) {
logger.warn(
`[${requestId}] Could not retrieve Webflow access token for user ${userId}. Cannot create webhook in Webflow.`
)
throw new Error(
'Webflow account connection required. Please connect your Webflow account in the trigger configuration and try again.'
)
}
const notificationUrl = `${getBaseUrl()}/api/webhooks/trigger/${path}`
// Map trigger IDs to Webflow trigger types
const triggerTypeMap: Record<string, string> = {
webflow_collection_item_created: 'collection_item_created',
webflow_collection_item_changed: 'collection_item_changed',
webflow_collection_item_deleted: 'collection_item_deleted',
webflow_form_submission: 'form_submission',
}
const webflowTriggerType = triggerTypeMap[triggerId]
if (!webflowTriggerType) {
logger.warn(`[${requestId}] Invalid triggerId for Webflow: ${triggerId}`, {
webhookId: webhookData.id,
})
throw new Error(`Invalid Webflow trigger type: ${triggerId}`)
}
const webflowApiUrl = `https://api.webflow.com/v2/sites/${siteId}/webhooks`
const requestBody: any = {
triggerType: webflowTriggerType,
url: notificationUrl,
}
// Add filter for collection-based triggers
if (collectionId && webflowTriggerType.startsWith('collection_item_')) {
requestBody.filter = {
resource_type: 'collection',
resource_id: collectionId,
}
}
// Add filter for form submissions
if (formId && webflowTriggerType === 'form_submission') {
requestBody.filter = {
resource_type: 'form',
resource_id: formId,
}
}
const webflowResponse = await fetch(webflowApiUrl, {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
accept: 'application/json',
},
body: JSON.stringify(requestBody),
})
const responseBody = await webflowResponse.json()
if (!webflowResponse.ok || responseBody.error) {
const errorMessage = responseBody.message || responseBody.error || 'Unknown Webflow API error'
logger.error(
`[${requestId}] Failed to create webhook in Webflow for webhook ${webhookData.id}. Status: ${webflowResponse.status}`,
{ message: errorMessage, response: responseBody }
)
throw new Error(errorMessage)
}
logger.info(
`[${requestId}] Successfully created webhook in Webflow for webhook ${webhookData.id}.`,
{
webflowWebhookId: responseBody.id || responseBody._id,
}
)
// Store the Webflow webhook ID in the providerConfig
try {
const currentConfig = (webhookData.providerConfig as Record<string, any>) || {}
const updatedConfig = {
...currentConfig,
externalId: responseBody.id || responseBody._id,
}
await db
.update(webhook)
.set({ providerConfig: updatedConfig, updatedAt: new Date() })
.where(eq(webhook.id, webhookData.id))
} catch (dbError: any) {
logger.error(
`[${requestId}] Failed to store externalId in providerConfig for webhook ${webhookData.id}.`,
dbError
)
// Even if saving fails, the webhook exists in Webflow. Log and continue.
}
} catch (error: any) {
logger.error(
`[${requestId}] Exception during Webflow webhook creation for webhook ${webhookData.id}.`,
{
message: error.message,
stack: error.stack,
}
)
throw error
}
}

View File

@@ -1,6 +1,6 @@
import { useEffect, useRef, useState } from 'react'
import { TooltipProvider } from '@/components/ui/tooltip'
import { formatDate as formatDashboardDate } from '../../utils/format-date'
import { formatDate } from '@/app/workspace/[workspaceId]/logs/utils'
export interface LineChartPoint {
timestamp: string
@@ -152,7 +152,7 @@ export function LineChart({
const getCompactDateLabel = (timestamp?: string) => {
if (!timestamp) return ''
try {
const f = formatDashboardDate(timestamp)
const f = formatDate(timestamp)
return `${f.compactDate} · ${f.compactTime}`
} catch (e) {
const d = new Date(timestamp)

View File

@@ -6,7 +6,7 @@ import LineChart, {
type LineChartPoint,
} from '@/app/workspace/[workspaceId]/logs/components/dashboard/line-chart'
import { getTriggerColor } from '@/app/workspace/[workspaceId]/logs/components/dashboard/utils'
import { formatDate } from '@/app/workspace/[workspaceId]/logs/utils/format-date'
import { formatDate } from '@/app/workspace/[workspaceId]/logs/utils'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
export interface ExecutionLogItem {
@@ -208,7 +208,7 @@ export function WorkflowDetails({
{hasDuration && (
<LineChart
data={details.durations!}
label='Workflow Duration'
label='Duration'
color='#3b82f6'
unit='ms'
series={
@@ -438,7 +438,7 @@ export function WorkflowDetails({
})
})()}
{/* Bottom loading / sentinel */}
{hasMore && (
{hasMore && details.logs.length > 0 && (
<div className='flex items-center justify-center py-3 text-muted-foreground'>
<div ref={loaderRef} className='flex items-center gap-2'>
{isLoadingMore ? (

View File

@@ -17,7 +17,7 @@ export function WorkflowsList({
filteredExecutions,
expandedWorkflowId,
onToggleWorkflow,
selectedSegmentIndex,
selectedSegments,
onSegmentClick,
searchQuery,
segmentDurationMs,
@@ -26,7 +26,7 @@ export function WorkflowsList({
filteredExecutions: WorkflowExecutionItem[]
expandedWorkflowId: string | null
onToggleWorkflow: (workflowId: string) => void
selectedSegmentIndex: number[] | null
selectedSegments: Record<string, number[]>
onSegmentClick: (
workflowId: string,
segmentIndex: number,
@@ -111,7 +111,7 @@ export function WorkflowsList({
<div className='flex-1'>
<StatusBar
segments={workflow.segments}
selectedSegmentIndices={isSelected ? selectedSegmentIndex : null}
selectedSegmentIndices={selectedSegments[workflow.workflowId] || null}
onSegmentClick={onSegmentClick as any}
workflowId={workflow.workflowId}
segmentDurationMs={segmentDurationMs}

View File

@@ -13,7 +13,7 @@ import { FileDownload } from '@/app/workspace/[workspaceId]/logs/components/side
import LogMarkdownRenderer from '@/app/workspace/[workspaceId]/logs/components/sidebar/components/markdown-renderer'
import { ToolCallsDisplay } from '@/app/workspace/[workspaceId]/logs/components/tool-calls/tool-calls-display'
import { TraceSpans } from '@/app/workspace/[workspaceId]/logs/components/trace-spans/trace-spans'
import { formatDate } from '@/app/workspace/[workspaceId]/logs/utils/format-date'
import { formatDate } from '@/app/workspace/[workspaceId]/logs/utils'
import { formatCost } from '@/providers/utils'
import type { WorkflowLog } from '@/stores/logs/filters/types'

View File

@@ -9,8 +9,10 @@ import KPIs from '@/app/workspace/[workspaceId]/logs/components/dashboard/kpis'
import WorkflowDetails from '@/app/workspace/[workspaceId]/logs/components/dashboard/workflow-details'
import WorkflowsList from '@/app/workspace/[workspaceId]/logs/components/dashboard/workflows-list'
import Timeline from '@/app/workspace/[workspaceId]/logs/components/filters/components/timeline'
import { mapToExecutionLog, mapToExecutionLogAlt } from '@/app/workspace/[workspaceId]/logs/utils'
import { formatCost } from '@/providers/utils'
import { useFilterStore } from '@/stores/logs/filters/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
type TimeFilter = '30m' | '1h' | '6h' | '12h' | '24h' | '3d' | '7d' | '14d' | '30d'
@@ -63,7 +65,7 @@ interface WorkflowDetailsDataLocal {
__meta?: { offset: number; hasMore: boolean }
}
export default function ExecutionsDashboard() {
export default function Dashboard() {
const params = useParams()
const workspaceId = params.workspaceId as string
const router = useRouter()
@@ -111,8 +113,8 @@ export default function ExecutionsDashboard() {
const [aggregateSegments, setAggregateSegments] = useState<
{ timestamp: string; totalExecutions: number; successfulExecutions: number }[]
>([])
const [selectedSegmentIndices, setSelectedSegmentIndices] = useState<number[]>([])
const [lastAnchorIndex, setLastAnchorIndex] = useState<number | null>(null)
const [selectedSegments, setSelectedSegments] = useState<Record<string, number[]>>({})
const [lastAnchorIndices, setLastAnchorIndices] = useState<Record<string, number>>({})
const [searchQuery, setSearchQuery] = useState('')
const [segmentCount, setSegmentCount] = useState<number>(DEFAULT_SEGMENTS)
const barsAreaRef = useRef<HTMLDivElement | null>(null)
@@ -126,6 +128,8 @@ export default function ExecutionsDashboard() {
timeRange: sidebarTimeRange,
} = useFilterStore()
const { workflows } = useWorkflowRegistry()
const timeFilter = getTimeFilterFromRange(sidebarTimeRange)
useEffect(() => {
@@ -346,78 +350,7 @@ export default function ExecutionsDashboard() {
let mappedLogs: ExecutionLog[] = []
if (logsResponse.ok) {
const logsData = await logsResponse.json()
mappedLogs = (logsData.data || []).map((l: any) => {
const started = l.startedAt
? new Date(l.startedAt)
: l.endedAt
? new Date(l.endedAt)
: null
const startedAt =
started && !Number.isNaN(started.getTime())
? started.toISOString()
: new Date().toISOString()
const durationCandidate =
typeof l.totalDurationMs === 'number'
? l.totalDurationMs
: typeof l.duration === 'number'
? l.duration
: typeof l.totalDurationMs === 'string'
? Number.parseInt(l.totalDurationMs.replace(/[^0-9]/g, ''), 10)
: typeof l.duration === 'string'
? Number.parseInt(l.duration.replace(/[^0-9]/g, ''), 10)
: null
let output: any = null
if (l.executionData?.finalOutput !== undefined) {
output = l.executionData.finalOutput
}
if (typeof l.output === 'string') {
output = l.output
} else if (l.executionData?.traceSpans && Array.isArray(l.executionData.traceSpans)) {
const spans: any[] = l.executionData.traceSpans
for (let i = spans.length - 1; i >= 0; i--) {
const s = spans[i]
if (s?.output && Object.keys(s.output).length > 0) {
output = s.output
break
}
if (s?.status === 'error' && (s?.output?.error || s?.error)) {
output = s.output?.error || s.error
break
}
}
if (!output && l.executionData?.output) {
output = l.executionData.output
}
}
if (!output) {
const be = l.executionData?.blockExecutions
if (Array.isArray(be) && be.length > 0) {
const last = be[be.length - 1]
output = last?.outputData || last?.errorMessage || null
}
}
if (!output) output = l.message || null
return {
id: l.id,
executionId: l.executionId,
startedAt,
level: l.level || 'info',
trigger: l.trigger || 'manual',
triggerUserId: l.triggerUserId || null,
triggerInputs: undefined,
outputs: output || undefined,
errorMessage: l.error || null,
duration: Number.isFinite(durationCandidate as number)
? (durationCandidate as number)
: null,
cost: l.cost
? { input: l.cost.input || 0, output: l.cost.output || 0, total: l.cost.total || 0 }
: null,
workflowName: l.workflowName || l.workflow?.name,
workflowColor: l.workflowColor || l.workflow?.color,
} as ExecutionLog
})
mappedLogs = (logsData.data || []).map(mapToExecutionLog)
}
setGlobalDetails({
@@ -471,68 +404,7 @@ export default function ExecutionsDashboard() {
}
const data = await response.json()
const mappedLogs: ExecutionLog[] = (data.data || []).map((l: any) => {
let durationCandidate: number | null = null
if (typeof l.totalDurationMs === 'number') durationCandidate = l.totalDurationMs
else if (typeof l.duration === 'number') durationCandidate = l.duration
else if (typeof l.totalDurationMs === 'string')
durationCandidate = Number.parseInt(
String(l.totalDurationMs).replace(/[^0-9]/g, ''),
10
)
else if (typeof l.duration === 'string')
durationCandidate = Number.parseInt(String(l.duration).replace(/[^0-9]/g, ''), 10)
let output: any = null
if (l.executionData?.finalOutput !== undefined) {
output = l.executionData.finalOutput
} else if (typeof l.output === 'string') {
output = l.output
} else if (l.executionData?.traceSpans && Array.isArray(l.executionData.traceSpans)) {
const spans: any[] = l.executionData.traceSpans
for (let i = spans.length - 1; i >= 0; i--) {
const s = spans[i]
if (s?.output && Object.keys(s.output).length > 0) {
output = s.output
break
}
if (s?.status === 'error' && (s?.output?.error || s?.error)) {
output = s.output?.error || s.error
break
}
}
if (!output && l.executionData?.output) {
output = l.executionData.output
}
}
if (!output) {
const be = l.executionData?.blockExecutions
if (Array.isArray(be) && be.length > 0) {
const last = be[be.length - 1]
output = last?.outputData || last?.errorMessage || null
}
}
return {
id: l.id,
executionId: l.executionId,
startedAt: l.createdAt || l.startedAt,
level: l.level || 'info',
trigger: l.trigger || 'manual',
triggerUserId: l.triggerUserId || null,
triggerInputs: undefined,
outputs: output || undefined,
errorMessage: l.error || null,
duration: Number.isFinite(durationCandidate as number)
? (durationCandidate as number)
: null,
cost: l.cost
? { input: l.cost.input || 0, output: l.cost.output || 0, total: l.cost.total || 0 }
: null,
workflowName: l.workflow?.name,
workflowColor: l.workflow?.color,
} as ExecutionLog
})
const mappedLogs: ExecutionLog[] = (data.data || []).map(mapToExecutionLogAlt)
setWorkflowDetails((prev) => ({
...prev,
@@ -581,64 +453,7 @@ export default function ExecutionsDashboard() {
const res = await fetch(`/api/logs?${qp.toString()}`)
if (!res.ok) return
const data = await res.json()
const more: ExecutionLog[] = (data.data || []).map((l: any) => {
let durationCandidate: number | null = null
if (typeof l.totalDurationMs === 'number') durationCandidate = l.totalDurationMs
else if (typeof l.duration === 'number') durationCandidate = l.duration
else if (typeof l.totalDurationMs === 'string')
durationCandidate = Number.parseInt(
String(l.totalDurationMs).replace(/[^0-9]/g, ''),
10
)
else if (typeof l.duration === 'string')
durationCandidate = Number.parseInt(String(l.duration).replace(/[^0-9]/g, ''), 10)
let output: any = null
if (l.executionData?.finalOutput !== undefined) {
output = l.executionData.finalOutput
} else if (l.executionData?.traceSpans && Array.isArray(l.executionData.traceSpans)) {
const spans: any[] = l.executionData.traceSpans
for (let i = spans.length - 1; i >= 0; i--) {
const s = spans[i]
if (s?.output && Object.keys(s.output).length > 0) {
output = s.output
break
}
if (s?.status === 'error' && (s?.output?.error || s?.error)) {
output = s.output?.error || s.error
break
}
}
if (!output && l.executionData?.output) {
output = l.executionData.output
}
}
if (!output) {
const be = l.executionData?.blockExecutions
if (Array.isArray(be) && be.length > 0) {
const last = be[be.length - 1]
output = last?.outputData || last?.errorMessage || null
}
}
return {
id: l.id,
executionId: l.executionId,
startedAt: l.createdAt || l.startedAt,
level: l.level || 'info',
trigger: l.trigger || 'manual',
triggerUserId: l.triggerUserId || null,
triggerInputs: undefined,
outputs: output || undefined,
errorMessage: l.error || null,
duration: Number.isFinite(durationCandidate as number)
? (durationCandidate as number)
: null,
cost: l.cost
? { input: l.cost.input || 0, output: l.cost.output || 0, total: l.cost.total || 0 }
: null,
workflowName: l.workflow?.name,
workflowColor: l.workflow?.color,
} as ExecutionLog
})
const more: ExecutionLog[] = (data.data || []).map(mapToExecutionLogAlt)
setWorkflowDetails((prev) => {
const cur = prev[workflowId]
@@ -695,34 +510,7 @@ export default function ExecutionsDashboard() {
const res = await fetch(`/api/logs?${qp.toString()}`)
if (!res.ok) return
const data = await res.json()
const more: ExecutionLog[] = (data.data || []).map((l: any) => {
let durationCandidate: number | null = null
if (typeof l.totalDurationMs === 'number') durationCandidate = l.totalDurationMs
else if (typeof l.duration === 'number') durationCandidate = l.duration
else if (typeof l.totalDurationMs === 'string')
durationCandidate = Number.parseInt(String(l.totalDurationMs).replace(/[^0-9]/g, ''), 10)
else if (typeof l.duration === 'string')
durationCandidate = Number.parseInt(String(l.duration).replace(/[^0-9]/g, ''), 10)
return {
id: l.id,
executionId: l.executionId,
startedAt: l.startedAt || l.createdAt,
level: l.level || 'info',
trigger: l.trigger || 'manual',
triggerUserId: l.triggerUserId || null,
triggerInputs: undefined,
outputs: l.executionData?.output || undefined,
errorMessage: l.error || null,
duration: Number.isFinite(durationCandidate as number)
? (durationCandidate as number)
: null,
cost: l.cost
? { input: l.cost.input || 0, output: l.cost.output || 0, total: l.cost.total || 0 }
: null,
workflowName: l.workflow?.name || l.workflowName,
workflowColor: l.workflow?.color || l.workflowColor,
} as ExecutionLog
})
const more: ExecutionLog[] = (data.data || []).map(mapToExecutionLog)
setGlobalDetails((prev) => {
if (!prev) return prev
@@ -760,12 +548,8 @@ export default function ExecutionsDashboard() {
(workflowId: string) => {
if (expandedWorkflowId === workflowId) {
setExpandedWorkflowId(null)
setSelectedSegmentIndices([])
setLastAnchorIndex(null)
} else {
setExpandedWorkflowId(workflowId)
setSelectedSegmentIndices([])
setLastAnchorIndex(null)
if (!workflowDetails[workflowId]) {
fetchWorkflowDetails(workflowId)
}
@@ -781,38 +565,69 @@ export default function ExecutionsDashboard() {
_timestamp: string,
mode: 'single' | 'toggle' | 'range'
) => {
if (expandedWorkflowId !== workflowId) {
setExpandedWorkflowId(workflowId)
if (!workflowDetails[workflowId]) {
fetchWorkflowDetails(workflowId)
}
setSelectedSegmentIndices([segmentIndex])
setLastAnchorIndex(segmentIndex)
} else {
setSelectedSegmentIndices((prev) => {
if (mode === 'single') {
setLastAnchorIndex(segmentIndex)
if (prev.includes(segmentIndex)) {
return prev.filter((i) => i !== segmentIndex)
// Fetch workflow details if not already loaded
if (!workflowDetails[workflowId]) {
fetchWorkflowDetails(workflowId)
}
if (mode === 'toggle') {
// Toggle mode: Add/remove segment from selection, allowing cross-workflow selection
setSelectedSegments((prev) => {
const currentSegments = prev[workflowId] || []
const exists = currentSegments.includes(segmentIndex)
const nextSegments = exists
? currentSegments.filter((i) => i !== segmentIndex)
: [...currentSegments, segmentIndex].sort((a, b) => a - b)
if (nextSegments.length === 0) {
const { [workflowId]: _, ...rest } = prev
// If this was the only workflow with selections, clear expanded
if (Object.keys(rest).length === 0) {
setExpandedWorkflowId(null)
}
return [segmentIndex]
return rest
}
if (mode === 'toggle') {
const exists = prev.includes(segmentIndex)
const next = exists ? prev.filter((i) => i !== segmentIndex) : [...prev, segmentIndex]
setLastAnchorIndex(segmentIndex)
return next.sort((a, b) => a - b)
const newState = { ...prev, [workflowId]: nextSegments }
// Set to multi-workflow mode if multiple workflows have selections
const selectedWorkflowIds = Object.keys(newState)
if (selectedWorkflowIds.length > 1) {
setExpandedWorkflowId('__multi__')
} else if (selectedWorkflowIds.length === 1) {
setExpandedWorkflowId(selectedWorkflowIds[0])
}
const anchor = lastAnchorIndex ?? segmentIndex
const [start, end] =
anchor < segmentIndex ? [anchor, segmentIndex] : [segmentIndex, anchor]
const range = Array.from({ length: end - start + 1 }, (_, i) => start + i)
const union = new Set([...(prev || []), ...range])
return Array.from(union).sort((a, b) => a - b)
return newState
})
setLastAnchorIndices((prev) => ({ ...prev, [workflowId]: segmentIndex }))
} else if (mode === 'single') {
// Single mode: Clear all selections and select only this segment
setExpandedWorkflowId(workflowId)
setSelectedSegments({ [workflowId]: [segmentIndex] })
setLastAnchorIndices({ [workflowId]: segmentIndex })
} else if (mode === 'range') {
// Range mode: Expand selection within the current workflow
if (expandedWorkflowId === workflowId) {
setSelectedSegments((prev) => {
const currentSegments = prev[workflowId] || []
const anchor = lastAnchorIndices[workflowId] ?? segmentIndex
const [start, end] =
anchor < segmentIndex ? [anchor, segmentIndex] : [segmentIndex, anchor]
const range = Array.from({ length: end - start + 1 }, (_, i) => start + i)
const union = new Set([...currentSegments, ...range])
return { ...prev, [workflowId]: Array.from(union).sort((a, b) => a - b) }
})
} else {
// If clicking range on a different workflow, treat as single click
setExpandedWorkflowId(workflowId)
setSelectedSegments({ [workflowId]: [segmentIndex] })
setLastAnchorIndices({ [workflowId]: segmentIndex })
}
}
},
[expandedWorkflowId, workflowDetails, fetchWorkflowDetails, lastAnchorIndex]
[expandedWorkflowId, workflowDetails, fetchWorkflowDetails, lastAnchorIndices]
)
const isInitialMount = useRef(true)
@@ -831,8 +646,8 @@ export default function ExecutionsDashboard() {
}, [expandedWorkflowId, timeFilter, endTime, workflowIds, folderIds, fetchWorkflowDetails])
useEffect(() => {
setSelectedSegmentIndices([])
setLastAnchorIndex(null)
setSelectedSegments({})
setLastAnchorIndices({})
}, [timeFilter, endTime, workflowIds, folderIds, triggers])
useEffect(() => {
@@ -1027,7 +842,7 @@ export default function ExecutionsDashboard() {
filteredExecutions={filteredExecutions as any}
expandedWorkflowId={expandedWorkflowId}
onToggleWorkflow={toggleWorkflow}
selectedSegmentIndex={selectedSegmentIndices as any}
selectedSegments={selectedSegments}
onSegmentClick={handleSegmentClick}
searchQuery={searchQuery}
segmentDurationMs={
@@ -1040,6 +855,170 @@ export default function ExecutionsDashboard() {
{/* Details section in its own scroll area */}
<div className='min-h-0 flex-1 overflow-auto'>
{(() => {
// Handle multi-workflow selection view
if (expandedWorkflowId === '__multi__') {
const selectedWorkflowIds = Object.keys(selectedSegments)
const totalMs = endTime.getTime() - getStartTime().getTime()
const segMs = totalMs / Math.max(1, segmentCount)
// Collect all unique segment indices across all workflows
const allSegmentIndices = new Set<number>()
for (const indices of Object.values(selectedSegments)) {
indices.forEach((idx) => allSegmentIndices.add(idx))
}
const sortedIndices = Array.from(allSegmentIndices).sort((a, b) => a - b)
// Aggregate logs from all selected workflows/segments
const allLogs: any[] = []
let totalExecutions = 0
let totalSuccess = 0
// Build aggregated chart series
const aggregatedSegments: Array<{
timestamp: string
totalExecutions: number
successfulExecutions: number
avgDurationMs: number
durationCount: number
}> = []
// Initialize aggregated segments for each unique index
for (const idx of sortedIndices) {
// Get the timestamp from the first workflow that has this index
let timestamp = ''
for (const wfId of selectedWorkflowIds) {
const wf = executions.find((w) => w.workflowId === wfId)
if (wf?.segments[idx]) {
timestamp = wf.segments[idx].timestamp
break
}
}
aggregatedSegments.push({
timestamp,
totalExecutions: 0,
successfulExecutions: 0,
avgDurationMs: 0,
durationCount: 0,
})
}
// Aggregate data from all workflows
for (const wfId of selectedWorkflowIds) {
const wf = executions.find((w) => w.workflowId === wfId)
const details = workflowDetails[wfId]
const indices = selectedSegments[wfId] || []
if (!wf || !details || indices.length === 0) continue
// Calculate time windows for this workflow's selected segments
const windows = indices
.map((idx) => wf.segments[idx])
.filter(Boolean)
.map((s) => {
const start = new Date(s.timestamp).getTime()
const end = start + segMs
totalExecutions += s.totalExecutions || 0
totalSuccess += s.successfulExecutions || 0
return { start, end }
})
const inAnyWindow = (t: number) =>
windows.some((w) => t >= w.start && t < w.end)
// Filter logs for this workflow's selected segments
const workflowLogs = details.allLogs
.filter((log) => inAnyWindow(new Date(log.startedAt).getTime()))
.map((log) => ({
...log,
workflowName: (log as any).workflowName || wf.workflowName,
workflowColor:
(log as any).workflowColor || workflows[wfId]?.color || '#64748b',
}))
allLogs.push(...workflowLogs)
// Aggregate segment metrics
indices.forEach((idx) => {
const segment = wf.segments[idx]
if (!segment) return
const aggIndex = sortedIndices.indexOf(idx)
if (aggIndex >= 0 && aggregatedSegments[aggIndex]) {
const agg = aggregatedSegments[aggIndex]
agg.totalExecutions += segment.totalExecutions || 0
agg.successfulExecutions += segment.successfulExecutions || 0
if (segment.avgDurationMs) {
agg.avgDurationMs += segment.avgDurationMs
agg.durationCount += 1
}
}
})
}
// Build chart series
const errorRates = aggregatedSegments.map((seg) => ({
timestamp: seg.timestamp,
value:
seg.totalExecutions > 0
? (1 - seg.successfulExecutions / seg.totalExecutions) * 100
: 0,
}))
const executionCounts = aggregatedSegments.map((seg) => ({
timestamp: seg.timestamp,
value: seg.totalExecutions,
}))
const durations = aggregatedSegments.map((seg) => ({
timestamp: seg.timestamp,
value: seg.durationCount > 0 ? seg.avgDurationMs / seg.durationCount : 0,
}))
// Sort logs by time (most recent first)
allLogs.sort(
(a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime()
)
const totalFailures = Math.max(totalExecutions - totalSuccess, 0)
const totalRate =
totalExecutions > 0 ? (totalSuccess / totalExecutions) * 100 : 100
return (
<WorkflowDetails
workspaceId={workspaceId}
expandedWorkflowId={'__multi__'}
workflowName={`${selectedWorkflowIds.length} workflows selected`}
overview={{
total: totalExecutions,
success: totalSuccess,
failures: totalFailures,
rate: totalRate,
}}
details={
{
errorRates,
durations,
executionCounts,
logs: allLogs,
allLogs: allLogs,
} as any
}
selectedSegmentIndex={[]}
selectedSegment={null}
clearSegmentSelection={() => {
setSelectedSegments({})
setLastAnchorIndices({})
setExpandedWorkflowId(null)
}}
formatCost={formatCost}
onLoadMore={undefined}
hasMore={false}
isLoadingMore={false}
/>
)
}
if (expandedWorkflowId) {
const wf = executions.find((w) => w.workflowId === expandedWorkflowId)
if (!wf) return null
@@ -1111,11 +1090,12 @@ export default function ExecutionsDashboard() {
}
}
if (details && selectedSegmentIndices.length > 0) {
const workflowSelectedIndices = selectedSegments[expandedWorkflowId] || []
if (details && workflowSelectedIndices.length > 0) {
const totalMs = endTime.getTime() - getStartTime().getTime()
const segMs = totalMs / Math.max(1, segmentCount)
const windows = selectedSegmentIndices
const windows = workflowSelectedIndices
.map((idx) => wf.segments[idx])
.filter(Boolean)
.map((s) => {
@@ -1134,11 +1114,8 @@ export default function ExecutionsDashboard() {
workflowName: (log as any).workflowName || wf.workflowName,
}))
const minStart = new Date(Math.min(...windows.map((w) => w.start)))
const maxEnd = new Date(Math.max(...windows.map((w) => w.end)))
// Build series from selected segments indices
const idxSet = new Set(selectedSegmentIndices)
const idxSet = new Set(workflowSelectedIndices)
const selectedSegs = wf.segments.filter((_, i) => idxSet.has(i))
;(details as any).__filtered = buildSeriesFromSegments(selectedSegs as any)
}
@@ -1164,8 +1141,8 @@ export default function ExecutionsDashboard() {
: undefined
const selectedSegment =
selectedSegmentIndices.length === 1
? wf.segments[selectedSegmentIndices[0]]
workflowSelectedIndices.length === 1
? wf.segments[workflowSelectedIndices[0]]
: null
return (
@@ -1175,7 +1152,7 @@ export default function ExecutionsDashboard() {
workflowName={wf.workflowName}
overview={{ total, success, failures, rate }}
details={detailsWithFilteredLogs as any}
selectedSegmentIndex={selectedSegmentIndices}
selectedSegmentIndex={workflowSelectedIndices}
selectedSegment={
selectedSegment
? {
@@ -1185,8 +1162,8 @@ export default function ExecutionsDashboard() {
: null
}
clearSegmentSelection={() => {
setSelectedSegmentIndices([])
setLastAnchorIndex(null)
setSelectedSegments({})
setLastAnchorIndices({})
}}
formatCost={formatCost}
onLoadMore={() => loadMoreLogs(expandedWorkflowId)}
@@ -1218,8 +1195,8 @@ export default function ExecutionsDashboard() {
selectedSegmentIndex={[]}
selectedSegment={null}
clearSegmentSelection={() => {
setSelectedSegmentIndices([])
setLastAnchorIndex(null)
setSelectedSegments({})
setLastAnchorIndices({})
}}
formatCost={formatCost}
onLoadMore={loadMoreGlobalLogs}

View File

@@ -9,8 +9,8 @@ import { cn } from '@/lib/utils'
import Controls from '@/app/workspace/[workspaceId]/logs/components/dashboard/controls'
import { AutocompleteSearch } from '@/app/workspace/[workspaceId]/logs/components/search/search'
import { Sidebar } from '@/app/workspace/[workspaceId]/logs/components/sidebar/sidebar'
import ExecutionsDashboard from '@/app/workspace/[workspaceId]/logs/executions-dashboard'
import { formatDate } from '@/app/workspace/[workspaceId]/logs/utils/format-date'
import Dashboard from '@/app/workspace/[workspaceId]/logs/dashboard'
import { formatDate } from '@/app/workspace/[workspaceId]/logs/utils'
import { useDebounce } from '@/hooks/use-debounce'
import { useFolderStore } from '@/stores/folders/store'
import { useFilterStore } from '@/stores/logs/filters/store'
@@ -680,7 +680,7 @@ export default function Logs() {
// If in dashboard mode, show the dashboard
if (viewMode === 'dashboard') {
return <ExecutionsDashboard />
return <Dashboard />
}
return (

View File

@@ -0,0 +1,212 @@
/**
* Parse duration from various log data formats
*/
export function parseDuration(log: any): number | null {
let durationCandidate: number | null = null
if (typeof log.totalDurationMs === 'number') {
durationCandidate = log.totalDurationMs
} else if (typeof log.duration === 'number') {
durationCandidate = log.duration
} else if (typeof log.totalDurationMs === 'string') {
durationCandidate = Number.parseInt(String(log.totalDurationMs).replace(/[^0-9]/g, ''), 10)
} else if (typeof log.duration === 'string') {
durationCandidate = Number.parseInt(String(log.duration).replace(/[^0-9]/g, ''), 10)
}
return Number.isFinite(durationCandidate) ? durationCandidate : null
}
/**
* Extract output from various sources in execution data
* Checks multiple locations in priority order:
* 1. executionData.finalOutput
* 2. output (as string)
* 3. executionData.traceSpans (iterates through spans)
* 4. executionData.blockExecutions (last block)
* 5. message (fallback)
*/
export function extractOutput(log: any): any {
let output: any = null
// Check finalOutput first
if (log.executionData?.finalOutput !== undefined) {
output = log.executionData.finalOutput
}
// Check direct output field
if (typeof log.output === 'string') {
output = log.output
} else if (log.executionData?.traceSpans && Array.isArray(log.executionData.traceSpans)) {
// Search through trace spans
const spans: any[] = log.executionData.traceSpans
for (let i = spans.length - 1; i >= 0; i--) {
const s = spans[i]
if (s?.output && Object.keys(s.output).length > 0) {
output = s.output
break
}
if (s?.status === 'error' && (s?.output?.error || s?.error)) {
output = s.output?.error || s.error
break
}
}
// Fallback to executionData.output
if (!output && log.executionData?.output) {
output = log.executionData.output
}
}
// Check block executions
if (!output) {
const blockExecutions = log.executionData?.blockExecutions
if (Array.isArray(blockExecutions) && blockExecutions.length > 0) {
const lastBlock = blockExecutions[blockExecutions.length - 1]
output = lastBlock?.outputData || lastBlock?.errorMessage || null
}
}
// Final fallback to message
if (!output) {
output = log.message || null
}
return output
}
/**
* Map raw log data to ExecutionLog format
*/
export interface ExecutionLog {
id: string
executionId: string
startedAt: string
level: string
trigger: string
triggerUserId: string | null
triggerInputs: any
outputs: any
errorMessage: string | null
duration: number | null
cost: {
input: number
output: number
total: number
} | null
workflowName?: string
workflowColor?: string
}
/**
* Convert raw API log response to ExecutionLog format
*/
export function mapToExecutionLog(log: any): ExecutionLog {
const started = log.startedAt
? new Date(log.startedAt)
: log.endedAt
? new Date(log.endedAt)
: null
const startedAt =
started && !Number.isNaN(started.getTime()) ? started.toISOString() : new Date().toISOString()
const duration = parseDuration(log)
const output = extractOutput(log)
return {
id: log.id,
executionId: log.executionId,
startedAt,
level: log.level || 'info',
trigger: log.trigger || 'manual',
triggerUserId: log.triggerUserId || null,
triggerInputs: undefined,
outputs: output || undefined,
errorMessage: log.error || null,
duration,
cost: log.cost
? {
input: log.cost.input || 0,
output: log.cost.output || 0,
total: log.cost.total || 0,
}
: null,
workflowName: log.workflowName || log.workflow?.name,
workflowColor: log.workflowColor || log.workflow?.color,
}
}
/**
* Alternative version that uses createdAt as fallback for startedAt
* (used in some API responses)
*/
export function mapToExecutionLogAlt(log: any): ExecutionLog {
const duration = parseDuration(log)
const output = extractOutput(log)
return {
id: log.id,
executionId: log.executionId,
startedAt: log.createdAt || log.startedAt,
level: log.level || 'info',
trigger: log.trigger || 'manual',
triggerUserId: log.triggerUserId || null,
triggerInputs: undefined,
outputs: output || undefined,
errorMessage: log.error || null,
duration,
cost: log.cost
? {
input: log.cost.input || 0,
output: log.cost.output || 0,
total: log.cost.total || 0,
}
: null,
workflowName: log.workflow?.name,
workflowColor: log.workflow?.color,
}
}
import { format } from 'date-fns'
export const formatDate = (dateString: string) => {
const date = new Date(dateString)
return {
full: date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
}),
time: date.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
}),
formatted: format(date, 'HH:mm:ss'),
compact: format(date, 'MMM d HH:mm:ss'),
compactDate: format(date, 'MMM d').toUpperCase(),
compactTime: format(date, 'HH:mm:ss'),
relative: (() => {
const now = new Date()
const diffMs = now.getTime() - date.getTime()
const diffMins = Math.floor(diffMs / 60000)
if (diffMins < 1) return 'just now'
if (diffMins < 60) return `${diffMins}m ago`
const diffHours = Math.floor(diffMins / 60)
if (diffHours < 24) return `${diffHours}h ago`
const diffDays = Math.floor(diffHours / 24)
if (diffDays === 1) return 'yesterday'
if (diffDays < 7) return `${diffDays}d ago`
return format(date, 'MMM d')
})(),
}
}

View File

@@ -1,46 +0,0 @@
import { format } from 'date-fns'
/**
* Helper function to format date in various formats
*/
export const formatDate = (dateString: string) => {
const date = new Date(dateString)
return {
full: date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
}),
time: date.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
}),
formatted: format(date, 'HH:mm:ss'),
compact: format(date, 'MMM d HH:mm:ss'),
compactDate: format(date, 'MMM d').toUpperCase(),
compactTime: format(date, 'HH:mm:ss'),
relative: (() => {
const now = new Date()
const diffMs = now.getTime() - date.getTime()
const diffMins = Math.floor(diffMs / 60000)
if (diffMins < 1) return 'just now'
if (diffMins < 60) return `${diffMins}m ago`
const diffHours = Math.floor(diffMins / 60)
if (diffHours < 24) return `${diffHours}h ago`
const diffDays = Math.floor(diffHours / 24)
if (diffDays === 1) return 'yesterday'
if (diffDays < 7) return `${diffDays}d ago`
return format(date, 'MMM d')
})(),
}
}

View File

@@ -50,7 +50,6 @@ import {
import { useFolderStore } from '@/stores/folders/store'
import { useOperationQueueStore } from '@/stores/operation-queue/store'
import { usePanelStore } from '@/stores/panel/store'
import { useGeneralStore } from '@/stores/settings/general/store'
import { useSubscriptionStore } from '@/stores/subscription/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
@@ -104,7 +103,6 @@ export function ControlBar({ hasValidationErrors = false }: ControlBarProps) {
const userPermissions = useUserPermissionsContext()
// Debug mode state
const { isDebugModeEnabled, toggleDebugMode } = useGeneralStore()
const { isDebugging, pendingBlocks, handleStepDebug, handleCancelDebug, handleResumeDebug } =
useWorkflowExecution()
@@ -874,9 +872,6 @@ export function ControlBar({ hasValidationErrors = false }: ControlBarProps) {
}
// Start debugging
if (!isDebugModeEnabled) {
toggleDebugMode()
}
if (usageExceeded) {
openSubscriptionSettings()
} else {
@@ -887,11 +882,9 @@ export function ControlBar({ hasValidationErrors = false }: ControlBarProps) {
}, [
userPermissions.canRead,
isDebugging,
isDebugModeEnabled,
usageExceeded,
blocks,
handleCancelDebug,
toggleDebugMode,
handleRunWorkflow,
openConsolePanel,
])

View File

@@ -14,8 +14,11 @@ import { checkTagTrigger, TagDropdown } from '@/components/ui/tag-dropdown'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { createLogger } from '@/lib/logs/console/logger'
import { cn } from '@/lib/utils'
import { isLikelyReferenceSegment, SYSTEM_REFERENCE_PREFIXES } from '@/lib/workflows/references'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value'
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
import { useTagSelection } from '@/hooks/use-tag-selection'
import { normalizeBlockName } from '@/stores/workflows/utils'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
const logger = createLogger('ConditionInput')
@@ -58,8 +61,33 @@ export function ConditionInput({
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId)
const emitTagSelection = useTagSelection(blockId, subBlockId)
const accessiblePrefixes = useAccessibleReferencePrefixes(blockId)
const containerRef = useRef<HTMLDivElement>(null)
const shouldHighlightReference = (part: string): boolean => {
if (!part.startsWith('<') || !part.endsWith('>')) {
return false
}
if (!isLikelyReferenceSegment(part)) {
return false
}
if (!accessiblePrefixes) {
return true
}
const inner = part.slice(1, -1)
const [prefix] = inner.split('.')
const normalizedPrefix = normalizeBlockName(prefix)
if (SYSTEM_REFERENCE_PREFIXES.has(normalizedPrefix)) {
return true
}
return accessiblePrefixes.has(normalizedPrefix)
}
const [visualLineHeights, setVisualLineHeights] = useState<{
[key: string]: number[]
}>({})
@@ -778,7 +806,57 @@ export function ConditionInput({
)
}
}}
highlight={(code) => highlight(code, languages.javascript, 'javascript')}
highlight={(codeToHighlight) => {
const placeholders: {
placeholder: string
original: string
type: 'var' | 'env'
}[] = []
let processedCode = codeToHighlight
// Replace environment variables with placeholders
processedCode = processedCode.replace(/\{\{([^}]+)\}\}/g, (match) => {
const placeholder = `__ENV_VAR_${placeholders.length}__`
placeholders.push({ placeholder, original: match, type: 'env' })
return placeholder
})
// Replace variable references with placeholders
processedCode = processedCode.replace(/<([^>]+)>/g, (match) => {
if (shouldHighlightReference(match)) {
const placeholder = `__VAR_REF_${placeholders.length}__`
placeholders.push({ placeholder, original: match, type: 'var' })
return placeholder
}
return match
})
// Apply Prism syntax highlighting
let highlightedCode = highlight(
processedCode,
languages.javascript,
'javascript'
)
// Restore and highlight the placeholders
placeholders.forEach(({ placeholder, original, type }) => {
if (type === 'env') {
highlightedCode = highlightedCode.replace(
placeholder,
`<span class="text-blue-500">${original}</span>`
)
} else if (type === 'var') {
// Escape the < and > for display
const escaped = original.replace(/</g, '&lt;').replace(/>/g, '&gt;')
highlightedCode = highlightedCode.replace(
placeholder,
`<span class="text-blue-500">${escaped}</span>`
)
}
})
return highlightedCode
}}
padding={12}
style={{
fontFamily: 'inherit',

View File

@@ -115,6 +115,10 @@ const SCOPE_DESCRIPTIONS: Record<string, string> = {
'users:read': 'View workspace users',
'files:write': 'Upload files',
'canvases:write': 'Create canvas documents',
'sites:read': 'View your Webflow sites',
'sites:write': 'Manage webhooks and site settings',
'cms:read': 'View your CMS content',
'cms:write': 'Manage your CMS content',
}
// Convert OAuth scope to user-friendly description

View File

@@ -318,7 +318,11 @@ export function ScheduleModal({
return (
<>
<DialogContent className='flex flex-col gap-0 p-0 sm:max-w-[600px]' hideCloseButton>
<DialogContent
className='flex max-h-[90vh] flex-col gap-0 overflow-hidden p-0 sm:max-w-[600px]'
hideCloseButton
onOpenAutoFocus={(e) => e.preventDefault()}
>
<DialogHeader className='border-b px-6 py-4'>
<div className='flex items-center justify-between'>
<DialogTitle className='font-medium text-lg'>Schedule Configuration</DialogTitle>
@@ -329,7 +333,7 @@ export function ScheduleModal({
</div>
</DialogHeader>
<div className='overflow-y-auto px-6 pt-4 pb-6'>
<div className='flex-1 overflow-y-auto px-6 py-6'>
{errorMessage && (
<Alert variant='destructive' className='mb-4'>
<AlertDescription>{errorMessage}</AlertDescription>
@@ -548,7 +552,7 @@ export function ScheduleModal({
</div>
</div>
<DialogFooter className='w-full px-6 pt-0 pb-6'>
<DialogFooter className='border-t px-6 py-4'>
<div className='flex w-full justify-between'>
<div>
{scheduleId && onDelete && (
@@ -558,23 +562,38 @@ export function ScheduleModal({
onClick={openDeleteConfirm}
disabled={isDeleting || isSaving}
size='default'
className='h-10'
className='h-9 rounded-[8px]'
>
<Trash2 className='mr-2 h-4 w-4' />
{isDeleting ? 'Deleting...' : 'Delete Schedule'}
{isDeleting ? (
<div className='mr-2 h-4 w-4 animate-spin rounded-full border-[1.5px] border-current border-t-transparent' />
) : (
<Trash2 className='mr-2 h-4 w-4' />
)}
{isDeleting ? 'Deleting...' : 'Delete'}
</Button>
)}
</div>
<div className='flex gap-2'>
<Button variant='outline' onClick={handleClose} size='default' className='h-10'>
<Button
variant='outline'
onClick={handleClose}
size='default'
className='h-9 rounded-[8px]'
>
Cancel
</Button>
<Button
onClick={handleSave}
disabled={!hasChanges || isSaving}
className={cn('h-10', hasChanges ? 'bg-primary hover:bg-primary/90' : '')}
size='default'
className={cn(
'w-[140px] rounded-[8px]',
hasChanges ? 'bg-primary hover:bg-primary/90' : ''
)}
size='sm'
>
{isSaving && (
<div className='h-4 w-4 animate-spin rounded-full border-[1.5px] border-current border-t-transparent' />
)}
{isSaving ? 'Saving...' : 'Save Changes'}
</Button>
</div>

View File

@@ -35,6 +35,7 @@ interface TriggerConfigSectionProps {
onChange: (fieldId: string, value: any) => void
webhookUrl: string
dynamicOptions?: Record<string, Array<{ id: string; name: string }> | string[]>
loadingFields?: Record<string, boolean>
}
export function TriggerConfigSection({
@@ -44,6 +45,7 @@ export function TriggerConfigSection({
onChange,
webhookUrl,
dynamicOptions = {},
loadingFields = {},
}: TriggerConfigSectionProps) {
const [showSecrets, setShowSecrets] = useState<Record<string, boolean>>({})
const [copied, setCopied] = useState<string | null>(null)
@@ -81,27 +83,43 @@ export function TriggerConfigSection({
)
case 'select': {
const rawOptions = dynamicOptions?.[fieldId] || fieldDef.options || []
const isLoading = loadingFields[fieldId] || false
const availableOptions = Array.isArray(rawOptions)
? rawOptions.map((option: any) => {
if (typeof option === 'string') {
return { id: option, name: option }
}
return option
})
: []
return (
<div className='space-y-2'>
<Label htmlFor={fieldId}>
<Label htmlFor={fieldId} className='font-medium text-sm'>
{fieldDef.label}
{fieldDef.required && <span className='ml-1 text-red-500'>*</span>}
</Label>
<Select value={value} onValueChange={(value) => onChange(fieldId, value)}>
<SelectTrigger>
<SelectValue placeholder={fieldDef.placeholder} />
{fieldDef.description && (
<p className='text-muted-foreground text-sm'>{fieldDef.description}</p>
)}
<Select
value={value}
onValueChange={(value) => onChange(fieldId, value)}
disabled={isLoading}
>
<SelectTrigger id={fieldId} className='h-10'>
<SelectValue placeholder={isLoading ? 'Loading...' : fieldDef.placeholder} />
</SelectTrigger>
<SelectContent>
{fieldDef.options?.map((option: string) => (
<SelectItem key={option} value={option}>
{option}
{availableOptions.map((option: any) => (
<SelectItem key={option.id} value={option.id}>
{option.name}
</SelectItem>
))}
</SelectContent>
</Select>
{fieldDef.description && (
<p className='text-muted-foreground text-sm'>{fieldDef.description}</p>
)}
</div>
)
}
@@ -352,9 +370,14 @@ export function TriggerConfigSection({
}
}
// Show webhook URL only for manual webhooks (have webhook config but no OAuth auto-registration)
// Auto-registered webhooks (like Webflow, Airtable) have requiresCredentials and register via API
// Polling triggers (like Gmail) don't have webhook property at all
const shouldShowWebhookUrl = webhookUrl && triggerDef.webhook && !triggerDef.requiresCredentials
return (
<div className='space-y-4 rounded-md border border-border bg-card p-4 shadow-sm'>
{webhookUrl && (
{shouldShowWebhookUrl && (
<div className='mb-4 space-y-1'>
<div className='flex items-center gap-2'>
<Label className='font-medium text-sm'>Webhook URL</Label>

View File

@@ -96,6 +96,7 @@ export function TriggerModal({
const [dynamicOptions, setDynamicOptions] = useState<
Record<string, Array<{ id: string; name: string }>>
>({})
const [loadingFields, setLoadingFields] = useState<Record<string, boolean>>({})
const lastCredentialIdRef = useRef<string | null>(null)
const [testUrl, setTestUrl] = useState<string | null>(null)
const [testUrlExpiresAt, setTestUrlExpiresAt] = useState<string | null>(null)
@@ -113,6 +114,9 @@ export function TriggerModal({
} else if (triggerDef.provider === 'airtable') {
if (typeof next.baseId === 'string') next.baseId = ''
if (typeof next.tableId === 'string') next.tableId = ''
} else if (triggerDef.provider === 'webflow') {
if (typeof next.siteId === 'string') next.siteId = ''
if (typeof next.collectionId === 'string') next.collectionId = ''
}
return next
})
@@ -177,6 +181,8 @@ export function TriggerModal({
void loadGmailLabels(currentCredentialId)
} else if (triggerDef.provider === 'outlook') {
void loadOutlookFolders(currentCredentialId)
} else if (triggerDef.provider === 'webflow') {
void loadWebflowSites()
}
}
return
@@ -197,6 +203,8 @@ export function TriggerModal({
void loadGmailLabels(currentCredentialId)
} else if (triggerDef.provider === 'outlook') {
void loadOutlookFolders(currentCredentialId)
} else if (triggerDef.provider === 'webflow') {
void loadWebflowSites()
}
}
}
@@ -262,9 +270,57 @@ export function TriggerModal({
}
}
// Generate webhook path and URL
const loadWebflowSites = async () => {
setLoadingFields((prev) => ({ ...prev, siteId: true }))
try {
const response = await fetch('/api/tools/webflow/sites')
if (response.ok) {
const data = await response.json()
if (data.sites && Array.isArray(data.sites)) {
setDynamicOptions((prev) => ({
...prev,
siteId: data.sites,
}))
}
} else {
logger.error('Failed to load Webflow sites:', response.statusText)
}
} catch (error) {
logger.error('Error loading Webflow sites:', error)
} finally {
setLoadingFields((prev) => ({ ...prev, siteId: false }))
}
}
const loadWebflowCollections = async (siteId: string) => {
setLoadingFields((prev) => ({ ...prev, collectionId: true }))
try {
const response = await fetch(`/api/tools/webflow/collections?siteId=${siteId}`)
if (response.ok) {
const data = await response.json()
if (data.collections && Array.isArray(data.collections)) {
setDynamicOptions((prev) => ({
...prev,
collectionId: data.collections,
}))
}
} else {
logger.error('Failed to load Webflow collections:', response.statusText)
}
} catch (error) {
logger.error('Error loading Webflow collections:', error)
} finally {
setLoadingFields((prev) => ({ ...prev, collectionId: false }))
}
}
useEffect(() => {
if (triggerDef.provider === 'webflow' && config.siteId) {
void loadWebflowCollections(config.siteId)
}
}, [config.siteId, triggerDef.provider])
useEffect(() => {
// For triggers that don't use webhooks (like Gmail polling), skip URL generation
if (triggerDef.requiresCredentials && !triggerDef.webhook) {
setWebhookUrl('')
setGeneratedPath('')
@@ -273,14 +329,11 @@ export function TriggerModal({
let finalPath = triggerPath
// If no path exists and we haven't generated one yet, generate one
if (!finalPath && !generatedPath) {
// Use UUID format consistent with other webhooks
const newPath = crypto.randomUUID()
setGeneratedPath(newPath)
finalPath = newPath
} else if (generatedPath && !triggerPath) {
// Use the already generated path
finalPath = generatedPath
}
@@ -538,6 +591,7 @@ export function TriggerModal({
onChange={handleConfigChange}
webhookUrl={webhookUrl}
dynamicOptions={dynamicOptions}
loadingFields={loadingFields}
/>
{triggerDef.webhook && (
@@ -684,17 +738,15 @@ export function TriggerModal({
(!(hasConfigChanged || hasCredentialChanged) && !!triggerId)
}
className={cn(
'h-9 rounded-[8px]',
'w-[140px] rounded-[8px]',
isConfigValid() && (hasConfigChanged || hasCredentialChanged || !triggerId)
? 'bg-primary hover:bg-primary/90'
: '',
isSaving &&
'relative after:absolute after:inset-0 after:animate-pulse after:bg-white/20'
: ''
)}
size='default'
size='sm'
>
{isSaving && (
<div className='mr-2 h-4 w-4 animate-spin rounded-full border-[1.5px] border-current border-t-transparent' />
<div className='h-4 w-4 animate-spin rounded-full border-[1.5px] border-current border-t-transparent' />
)}
{isSaving ? 'Saving...' : 'Save Changes'}
</Button>

View File

@@ -145,21 +145,14 @@ export function TriggerConfig({
throw new Error('Trigger definition not found')
}
// Set the trigger path and config in the block state
if (path && path !== triggerPath) {
setTriggerPath(path)
}
setTriggerConfig(config)
setStoredTriggerId(effectiveTriggerId)
// Map trigger ID to webhook provider name
const webhookProvider = effectiveTriggerId
.replace(/_chat_subscription$/, '')
.replace(/_webhook$/, '')
.replace(/_poller$/, '')
.replace(/_subscription$/, '') // e.g., 'slack_webhook' -> 'slack', 'gmail_poller' -> 'gmail', 'microsoftteams_chat_subscription' -> 'microsoftteams'
const webhookProvider = triggerDef.provider
// Include selected credential from the modal (if any)
const selectedCredentialId =
(useSubBlockStore.getState().getValue(blockId, 'triggerCredentials') as string | null) ||
null

View File

@@ -7,4 +7,5 @@ export { OutlookConfig } from './outlook'
export { SlackConfig } from './slack'
export { StripeConfig } from './stripe'
export { TelegramConfig } from './telegram'
export { WebflowConfig } from './webflow'
export { WhatsAppConfig } from './whatsapp'

View File

@@ -0,0 +1,145 @@
import { Input, Skeleton } from '@/components/ui'
import {
ConfigField,
ConfigSection,
InstructionsSection,
TestResultDisplay as WebhookTestResult,
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components'
interface WebflowConfigProps {
siteId: string
setSiteId: (value: string) => void
collectionId?: string
setCollectionId?: (value: string) => void
formId?: string
setFormId?: (value: string) => void
isLoadingToken: boolean
testResult: any
copied: string | null
copyToClipboard: (text: string, type: string) => void
testWebhook?: () => void
webhookId?: string
triggerType?: string // The selected trigger type to show relevant fields
}
export function WebflowConfig({
siteId,
setSiteId,
collectionId,
setCollectionId,
formId,
setFormId,
isLoadingToken,
testResult,
copied,
copyToClipboard,
testWebhook,
webhookId,
triggerType,
}: WebflowConfigProps) {
const isCollectionTrigger = triggerType?.includes('collection_item') || !triggerType
const isFormTrigger = triggerType?.includes('form_submission')
return (
<div className='space-y-4'>
<ConfigSection title='Webflow Configuration'>
<ConfigField
id='webflow-site-id'
label='Site ID *'
description='The ID of the Webflow site to monitor (found in site settings or URL)'
>
{isLoadingToken ? (
<Skeleton className='h-10 w-full' />
) : (
<Input
id='webflow-site-id'
value={siteId}
onChange={(e) => setSiteId(e.target.value)}
placeholder='6c3592'
required
/>
)}
</ConfigField>
{isCollectionTrigger && setCollectionId && (
<ConfigField
id='webflow-collection-id'
label='Collection ID'
description='The ID of the collection to monitor (optional - leave empty to monitor all collections)'
>
{isLoadingToken ? (
<Skeleton className='h-10 w-full' />
) : (
<Input
id='webflow-collection-id'
value={collectionId || ''}
onChange={(e) => setCollectionId(e.target.value)}
placeholder='68f9666257aa8abaa9b0b6d6'
/>
)}
</ConfigField>
)}
{isFormTrigger && setFormId && (
<ConfigField
id='webflow-form-id'
label='Form ID'
description='The ID of the specific form to monitor (optional - leave empty to monitor all forms)'
>
{isLoadingToken ? (
<Skeleton className='h-10 w-full' />
) : (
<Input
id='webflow-form-id'
value={formId || ''}
onChange={(e) => setFormId(e.target.value)}
placeholder='form-contact'
/>
)}
</ConfigField>
)}
</ConfigSection>
{testResult && (
<WebhookTestResult
testResult={testResult}
copied={copied}
copyToClipboard={copyToClipboard}
/>
)}
<InstructionsSection tip='Webflow webhooks monitor changes in your CMS and trigger your workflow automatically.'>
<ol className='list-inside list-decimal space-y-1'>
<li>Connect your Webflow account using the credential selector above.</li>
<li>Enter your Webflow Site ID (found in the site URL or site settings).</li>
{isCollectionTrigger && (
<>
<li>
Optionally enter a Collection ID to monitor only specific collections (leave empty
to monitor all).
</li>
<li>
The webhook will trigger when items are created, changed, or deleted in the
specified collection(s).
</li>
</>
)}
{isFormTrigger && (
<>
<li>
Optionally enter a Form ID to monitor only a specific form (leave empty to monitor
all forms).
</li>
<li>The webhook will trigger whenever a form is submitted on your site.</li>
</>
)}
<li>
Sim will automatically register the webhook with Webflow when you save this
configuration.
</li>
<li>Make sure your Webflow account has appropriate permissions for the site.</li>
</ol>
</InstructionsSection>
</div>
)
}

View File

@@ -13,7 +13,6 @@ import { useExecutionStore } from '@/stores/execution/store'
import { useConsoleStore } from '@/stores/panel/console/store'
import { useVariablesStore } from '@/stores/panel/variables/store'
import { useEnvironmentStore } from '@/stores/settings/environment/store'
import { useGeneralStore } from '@/stores/settings/general/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { mergeSubblockState } from '@/stores/workflows/utils'
import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/workflow/utils'
@@ -100,7 +99,6 @@ export function useWorkflowExecution() {
const { activeWorkflowId, workflows } = useWorkflowRegistry()
const { toggleConsole } = useConsoleStore()
const { getAllVariables, loadWorkspaceEnvironment } = useEnvironmentStore()
const { isDebugModeEnabled } = useGeneralStore()
const { getVariablesByWorkflowId, variables } = useVariablesStore()
const {
isExecuting,
@@ -145,11 +143,6 @@ export function useWorkflowExecution() {
setExecutor(null)
setPendingBlocks([])
setActiveBlocks(new Set())
// Reset debug mode setting if it was enabled
if (isDebugModeEnabled) {
useGeneralStore.getState().toggleDebugMode()
}
}, [
setIsExecuting,
setIsDebugging,
@@ -157,7 +150,6 @@ export function useWorkflowExecution() {
setExecutor,
setPendingBlocks,
setActiveBlocks,
isDebugModeEnabled,
])
/**
@@ -626,11 +618,10 @@ export function useWorkflowExecution() {
}
} else if (result && 'success' in result) {
setExecutionResult(result)
if (!isDebugModeEnabled) {
setIsExecuting(false)
setIsDebugging(false)
setActiveBlocks(new Set())
}
// Reset execution state after successful non-debug execution
setIsExecuting(false)
setIsDebugging(false)
setActiveBlocks(new Set())
if (isChatExecution) {
if (!result.metadata) {
@@ -659,7 +650,6 @@ export function useWorkflowExecution() {
getAllVariables,
loadWorkspaceEnvironment,
getVariablesByWorkflowId,
isDebugModeEnabled,
setIsExecuting,
setIsDebugging,
setDebugContext,

View File

@@ -252,8 +252,7 @@ const WorkflowContent = React.memo(() => {
} = useCollaborativeWorkflow()
// Execution and debug mode state
const { activeBlockIds, pendingBlocks } = useExecutionStore()
const { isDebugModeEnabled } = useGeneralStore()
const { activeBlockIds, pendingBlocks, isDebugging } = useExecutionStore()
const [dragStartParentId, setDragStartParentId] = useState<string | null>(null)
// Helper function to validate workflow for nested subflows
@@ -1273,7 +1272,7 @@ const WorkflowContent = React.memo(() => {
const position = block.position
const isActive = activeBlockIds.has(block.id)
const isPending = isDebugModeEnabled && pendingBlocks.includes(block.id)
const isPending = isDebugging && pendingBlocks.includes(block.id)
// Create stable node object - React Flow will handle shallow comparison
nodeArray.push({
@@ -1302,7 +1301,7 @@ const WorkflowContent = React.memo(() => {
blocks,
activeBlockIds,
pendingBlocks,
isDebugModeEnabled,
isDebugging,
nestedSubflowErrors,
getBlockConfig,
])

View File

@@ -11,7 +11,7 @@ export const DiscordBlock: BlockConfig<DiscordResponse> = {
longDescription:
'Integrate Discord into the workflow. Can send and get messages, get server information, and get a users information.',
category: 'tools',
bgColor: '#E0E0E0',
bgColor: '#5865F2',
icon: DiscordIcon,
subBlocks: [
{

View File

@@ -0,0 +1,190 @@
import { WebflowIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { WebflowResponse } from '@/tools/webflow/types'
export const WebflowBlock: BlockConfig<WebflowResponse> = {
type: 'webflow',
name: 'Webflow',
description: 'Manage Webflow CMS collections',
authMode: AuthMode.OAuth,
longDescription:
'Integrates Webflow CMS into the workflow. Can create, get, list, update, or delete items in Webflow CMS collections. Manage your Webflow content programmatically. Can be used in trigger mode to trigger workflows when collection items change or forms are submitted.',
docsLink: 'https://docs.sim.ai/tools/webflow',
category: 'tools',
triggerAllowed: true,
bgColor: '#E0E0E0',
icon: WebflowIcon,
subBlocks: [
{
id: 'operation',
title: 'Operation',
type: 'dropdown',
layout: 'full',
options: [
{ label: 'List Items', id: 'list' },
{ label: 'Get Item', id: 'get' },
{ label: 'Create Item', id: 'create' },
{ label: 'Update Item', id: 'update' },
{ label: 'Delete Item', id: 'delete' },
],
value: () => 'list',
},
{
id: 'credential',
title: 'Webflow Account',
type: 'oauth-input',
layout: 'full',
provider: 'webflow',
serviceId: 'webflow',
requiredScopes: ['sites:read', 'sites:write', 'cms:read', 'cms:write'],
placeholder: 'Select Webflow account',
required: true,
},
{
id: 'collectionId',
title: 'Collection ID',
type: 'short-input',
layout: 'full',
placeholder: 'Enter collection ID',
dependsOn: ['credential'],
required: true,
},
{
id: 'itemId',
title: 'Item ID',
type: 'short-input',
layout: 'full',
placeholder: 'ID of the item',
condition: { field: 'operation', value: ['get', 'update', 'delete'] },
required: true,
},
{
id: 'offset',
title: 'Offset',
type: 'short-input',
layout: 'half',
placeholder: 'Pagination offset (optional)',
condition: { field: 'operation', value: 'list' },
},
{
id: 'limit',
title: 'Limit',
type: 'short-input',
layout: 'half',
placeholder: 'Max items to return (optional)',
condition: { field: 'operation', value: 'list' },
},
{
id: 'fieldData',
title: 'Field Data',
type: 'code',
layout: 'full',
language: 'json',
placeholder: 'Field data as JSON: `{ "name": "Item Name", "slug": "item-slug" }`',
condition: { field: 'operation', value: ['create', 'update'] },
required: true,
},
{
id: 'triggerConfig',
title: 'Trigger Configuration',
type: 'trigger-config',
layout: 'full',
triggerProvider: 'webflow',
availableTriggers: [
'webflow_collection_item_created',
'webflow_collection_item_changed',
'webflow_collection_item_deleted',
'webflow_form_submission',
],
},
],
tools: {
access: [
'webflow_list_items',
'webflow_get_item',
'webflow_create_item',
'webflow_update_item',
'webflow_delete_item',
],
config: {
tool: (params) => {
switch (params.operation) {
case 'list':
return 'webflow_list_items'
case 'get':
return 'webflow_get_item'
case 'create':
return 'webflow_create_item'
case 'update':
return 'webflow_update_item'
case 'delete':
return 'webflow_delete_item'
default:
throw new Error(`Invalid Webflow operation: ${params.operation}`)
}
},
params: (params) => {
const { credential, fieldData, ...rest } = params
let parsedFieldData: any | undefined
try {
if (fieldData && (params.operation === 'create' || params.operation === 'update')) {
parsedFieldData = JSON.parse(fieldData)
}
} catch (error: any) {
throw new Error(`Invalid JSON input for ${params.operation} operation: ${error.message}`)
}
const baseParams = {
credential,
...rest,
}
switch (params.operation) {
case 'create':
case 'update':
return { ...baseParams, fieldData: parsedFieldData }
default:
return baseParams
}
},
},
},
inputs: {
operation: { type: 'string', description: 'Operation to perform' },
credential: { type: 'string', description: 'Webflow OAuth access token' },
collectionId: { type: 'string', description: 'Webflow collection identifier' },
// Conditional inputs
itemId: { type: 'string', description: 'Item identifier' }, // Required for get/update/delete
offset: { type: 'number', description: 'Pagination offset' }, // Optional for list
limit: { type: 'number', description: 'Maximum items to return' }, // Optional for list
fieldData: { type: 'json', description: 'Item field data' }, // Required for create/update
},
outputs: {
items: { type: 'json', description: 'Array of items (list operation)' },
item: { type: 'json', description: 'Single item data (get/create/update operations)' },
success: { type: 'boolean', description: 'Operation success status (delete operation)' },
metadata: { type: 'json', description: 'Operation metadata' },
// Trigger outputs
siteId: { type: 'string', description: 'Site ID where event occurred' },
workspaceId: { type: 'string', description: 'Workspace ID where event occurred' },
collectionId: { type: 'string', description: 'Collection ID (for collection events)' },
payload: { type: 'json', description: 'Event payload data (item data for collection events)' },
name: { type: 'string', description: 'Form name (for form submissions)' },
id: { type: 'string', description: 'Submission ID (for form submissions)' },
submittedAt: { type: 'string', description: 'Submission timestamp (for form submissions)' },
data: { type: 'json', description: 'Form field data (for form submissions)' },
schema: { type: 'json', description: 'Form schema (for form submissions)' },
formElementId: { type: 'string', description: 'Form element ID (for form submissions)' },
},
triggers: {
enabled: true,
available: [
'webflow_collection_item_created',
'webflow_collection_item_changed',
'webflow_collection_item_deleted',
'webflow_form_submission',
],
},
}

View File

@@ -1,8 +1,3 @@
/**
* Blocks Registry
*
*/
import { AgentBlock } from '@/blocks/blocks/agent'
import { AirtableBlock } from '@/blocks/blocks/airtable'
import { ApiBlock } from '@/blocks/blocks/api'
@@ -80,6 +75,7 @@ import { TwilioSMSBlock } from '@/blocks/blocks/twilio'
import { TypeformBlock } from '@/blocks/blocks/typeform'
import { VisionBlock } from '@/blocks/blocks/vision'
import { WealthboxBlock } from '@/blocks/blocks/wealthbox'
import { WebflowBlock } from '@/blocks/blocks/webflow'
import { WebhookBlock } from '@/blocks/blocks/webhook'
import { WhatsAppBlock } from '@/blocks/blocks/whatsapp'
import { WikipediaBlock } from '@/blocks/blocks/wikipedia'
@@ -171,6 +167,7 @@ export const registry: Record<string, BlockConfig> = {
typeform: TypeformBlock,
vision: VisionBlock,
wealthbox: WealthboxBlock,
webflow: WebflowBlock,
webhook: WebhookBlock,
whatsapp: WhatsAppBlock,
wikipedia: WikipediaBlock,

View File

@@ -3775,3 +3775,23 @@ export function ZepIcon(props: SVGProps<SVGSVGElement>) {
</svg>
)
}
export function WebflowIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
{...props}
width='1080'
height='674'
viewBox='0 0 1080 674'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M1080 0L735.386 673.684H411.695L555.916 394.481H549.445C430.464 548.934 252.942 650.61 -0.000488281 673.684V398.344C-0.000488281 398.344 161.813 388.787 256.938 288.776H-0.000488281V0.0053214H288.771V237.515L295.252 237.489L413.254 0.0053214H631.644V236.009L638.126 235.999L760.555 0H1080Z'
fill='#146EF5'
/>
</svg>
)
}

View File

@@ -57,7 +57,7 @@ export const setupHandlerMocks = () => {
* Setup store mocks with configurable options
*/
export const setupStoreMocks = (options?: {
isDebugModeEnabled?: boolean
isDebugging?: boolean
consoleAddFn?: ReturnType<typeof vi.fn>
consoleUpdateFn?: ReturnType<typeof vi.fn>
}) => {
@@ -66,15 +66,14 @@ export const setupStoreMocks = (options?: {
vi.doMock('@/stores/settings/general/store', () => ({
useGeneralStore: {
getState: () => ({
isDebugModeEnabled: options?.isDebugModeEnabled ?? false,
}),
getState: () => ({}),
},
}))
vi.doMock('@/stores/execution/store', () => ({
useExecutionStore: {
getState: () => ({
isDebugging: options?.isDebugging ?? false,
setIsExecuting: vi.fn(),
reset: vi.fn(),
setActiveBlocks: vi.fn(),
@@ -925,7 +924,7 @@ export const setupParallelTestMocks = (options?: {
* Sets up all standard mocks for executor tests
*/
export const setupAllMocks = (options?: {
isDebugModeEnabled?: boolean
isDebugging?: boolean
consoleAddFn?: ReturnType<typeof vi.fn>
consoleUpdateFn?: ReturnType<typeof vi.fn>
}) => {

View File

@@ -386,11 +386,11 @@ describe('Executor', () => {
* Debug mode tests
*/
describe('debug mode', () => {
it('should detect debug mode from settings', async () => {
it('should detect debug mode from execution store', async () => {
vi.resetModules()
vi.clearAllMocks()
setupAllMocks({ isDebugModeEnabled: true })
setupAllMocks({ isDebugging: true })
const { Executor } = await import('@/executor/index')
@@ -405,7 +405,7 @@ describe('Executor', () => {
vi.resetModules()
vi.clearAllMocks()
setupAllMocks({ isDebugModeEnabled: false })
setupAllMocks({ isDebugging: false })
const { Executor } = await import('@/executor/index')

View File

@@ -36,7 +36,6 @@ import { VirtualBlockUtils } from '@/executor/utils/virtual-blocks'
import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types'
import { useExecutionStore } from '@/stores/execution/store'
import { useConsoleStore } from '@/stores/panel/console/store'
import { useGeneralStore } from '@/stores/settings/general/store'
const logger = createLogger('Executor')
@@ -217,7 +216,7 @@ export class Executor {
new GenericBlockHandler(),
]
this.isDebugging = useGeneralStore.getState().isDebugModeEnabled
this.isDebugging = useExecutionStore.getState().isDebugging
}
/**

View File

@@ -152,6 +152,7 @@ export const auth = betterAuth({
'microsoft',
'slack',
'reddit',
'webflow',
// Common SSO provider patterns
...SSO_TRUSTED_PROVIDERS,
@@ -1214,6 +1215,56 @@ export const auth = betterAuth({
}
},
},
// Webflow provider
{
providerId: 'webflow',
clientId: env.WEBFLOW_CLIENT_ID as string,
clientSecret: env.WEBFLOW_CLIENT_SECRET as string,
authorizationUrl: 'https://webflow.com/oauth/authorize',
tokenUrl: 'https://api.webflow.com/oauth/access_token',
userInfoUrl: 'https://api.webflow.com/v2/token/introspect',
scopes: ['sites:read', 'sites:write', 'cms:read', 'cms:write'],
responseType: 'code',
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/webflow`,
getUserInfo: async (tokens) => {
try {
logger.info('Fetching Webflow user info')
const response = await fetch('https://api.webflow.com/v2/token/introspect', {
headers: {
Authorization: `Bearer ${tokens.accessToken}`,
},
})
if (!response.ok) {
logger.error('Error fetching Webflow user info:', {
status: response.status,
statusText: response.statusText,
})
return null
}
const data = await response.json()
const now = new Date()
const userId = data.user_id || `webflow-${Date.now()}`
const uniqueId = `webflow-${userId}`
return {
id: uniqueId,
name: data.user_name || 'Webflow User',
email: `${uniqueId.replace(/[^a-zA-Z0-9]/g, '')}@webflow.user`,
emailVerified: false,
createdAt: now,
updatedAt: now,
}
} catch (error) {
logger.error('Error in Webflow getUserInfo:', { error })
return null
}
},
},
],
}),
// Include SSO plugin when enabled

View File

@@ -201,6 +201,8 @@ export const env = createEnv({
SLACK_CLIENT_SECRET: z.string().optional(), // Slack OAuth client secret
REDDIT_CLIENT_ID: z.string().optional(), // Reddit OAuth client ID
REDDIT_CLIENT_SECRET: z.string().optional(), // Reddit OAuth client secret
WEBFLOW_CLIENT_ID: z.string().optional(), // Webflow OAuth client ID
WEBFLOW_CLIENT_SECRET: z.string().optional(), // Webflow OAuth client secret
// E2B Remote Code Execution
E2B_ENABLED: z.string().optional(), // Enable E2B remote code execution

View File

@@ -25,6 +25,7 @@ import {
SlackIcon,
SupabaseIcon,
WealthboxIcon,
WebflowIcon,
xIcon,
} from '@/components/icons'
import { env } from '@/lib/env'
@@ -47,6 +48,7 @@ export type OAuthProvider =
| 'slack'
| 'reddit'
| 'wealthbox'
| 'webflow'
| string
export type OAuthService =
@@ -76,6 +78,7 @@ export type OAuthService =
| 'reddit'
| 'wealthbox'
| 'onedrive'
| 'webflow'
export interface OAuthProviderConfig {
id: OAuthProvider
name: string
@@ -500,6 +503,23 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
},
defaultService: 'wealthbox',
},
webflow: {
id: 'webflow',
name: 'Webflow',
icon: (props) => WebflowIcon(props),
services: {
webflow: {
id: 'webflow',
name: 'Webflow',
description: 'Manage Webflow CMS collections, sites, and content.',
providerId: 'webflow',
icon: (props) => WebflowIcon(props),
baseProviderIcon: (props) => WebflowIcon(props),
scopes: ['cms:read', 'cms:write', 'sites:read', 'sites:write'],
},
},
defaultService: 'webflow',
},
}
// Helper function to get a service by provider and service ID
@@ -584,6 +604,8 @@ export function getServiceIdFromScopes(provider: OAuthProvider, scopes: string[]
return 'reddit'
} else if (provider === 'wealthbox') {
return 'wealthbox'
} else if (provider === 'webflow') {
return 'webflow'
}
return providerConfig.defaultService
@@ -886,6 +908,19 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig {
supportsRefreshTokenRotation: true,
}
}
case 'webflow': {
const { clientId, clientSecret } = getCredentials(
env.WEBFLOW_CLIENT_ID,
env.WEBFLOW_CLIENT_SECRET
)
return {
tokenEndpoint: 'https://api.webflow.com/oauth/access_token',
clientId,
clientSecret,
useBasicAuth: false,
supportsRefreshTokenRotation: false,
}
}
default:
throw new Error(`Unsupported provider: ${provider}`)
}

View File

@@ -900,6 +900,57 @@ export async function formatWebhookInput(
}
}
if (foundWebhook.provider === 'webflow') {
const triggerType = body?.triggerType || 'unknown'
const siteId = body?.siteId || ''
const workspaceId = body?.workspaceId || ''
const collectionId = body?.collectionId || ''
const payload = body?.payload || {}
const formId = body?.formId || ''
const formName = body?.name || ''
const formSubmissionId = body?.id || ''
const submittedAt = body?.submittedAt || ''
const formData = body?.data || {}
const schema = body?.schema || {}
return {
siteId,
workspaceId,
collectionId,
payload,
triggerType,
formId,
name: formName,
id: formSubmissionId,
submittedAt,
data: formData,
schema,
formElementId: body?.formElementId || '',
webflow: {
siteId,
workspaceId,
collectionId,
payload,
triggerType,
raw: body,
},
webhook: {
data: {
provider: 'webflow',
path: foundWebhook.path,
providerConfig: foundWebhook.providerConfig,
payload: body,
headers: Object.fromEntries(request.headers.entries()),
method: request.method,
},
},
workflowId: foundWorkflow.id,
}
}
if (foundWebhook.provider === 'generic') {
return body
}

View File

@@ -21,7 +21,6 @@ export const useGeneralStore = create<GeneralStore>()(
isAutoConnectEnabled: true,
isAutoPanEnabled: true,
isConsoleExpandedByDefault: true,
isDebugModeEnabled: false,
showFloatingControls: true,
showTrainingControls: false,
theme: 'system' as const, // Keep for compatibility but not used
@@ -101,10 +100,6 @@ export const useGeneralStore = create<GeneralStore>()(
)
},
toggleDebugMode: () => {
set({ isDebugModeEnabled: !get().isDebugModeEnabled })
},
toggleFloatingControls: async () => {
if (get().isFloatingControlsLoading) return
const newValue = !get().showFloatingControls

View File

@@ -2,7 +2,6 @@ export interface General {
isAutoConnectEnabled: boolean
isAutoPanEnabled: boolean
isConsoleExpandedByDefault: boolean
isDebugModeEnabled: boolean
showFloatingControls: boolean
showTrainingControls: boolean
theme: 'system' | 'light' | 'dark'
@@ -24,7 +23,6 @@ export interface GeneralActions {
toggleAutoConnect: () => Promise<void>
toggleAutoPan: () => Promise<void>
toggleConsoleExpandedByDefault: () => Promise<void>
toggleDebugMode: () => void
toggleFloatingControls: () => Promise<void>
toggleTrainingControls: () => Promise<void>
setTheme: (theme: 'system' | 'light' | 'dark') => Promise<void>

View File

@@ -205,6 +205,13 @@ import {
wealthboxWriteNoteTool,
wealthboxWriteTaskTool,
} from '@/tools/wealthbox'
import {
webflowCreateItemTool,
webflowDeleteItemTool,
webflowGetItemTool,
webflowListItemsTool,
webflowUpdateItemTool,
} from '@/tools/webflow'
import { whatsappSendMessageTool } from '@/tools/whatsapp'
import {
wikipediaPageContentTool,
@@ -420,6 +427,11 @@ export const tools: Record<string, ToolConfig> = {
wealthbox_write_task: wealthboxWriteTaskTool,
wealthbox_read_note: wealthboxReadNoteTool,
wealthbox_write_note: wealthboxWriteNoteTool,
webflow_list_items: webflowListItemsTool,
webflow_get_item: webflowGetItemTool,
webflow_create_item: webflowCreateItemTool,
webflow_update_item: webflowUpdateItemTool,
webflow_delete_item: webflowDeleteItemTool,
wikipedia_summary: wikipediaPageSummaryTool,
wikipedia_search: wikipediaSearchTool,
wikipedia_content: wikipediaPageContentTool,

View File

@@ -0,0 +1,73 @@
import type { ToolConfig } from '@/tools/types'
import type { WebflowCreateItemParams, WebflowCreateItemResponse } from '@/tools/webflow/types'
export const webflowCreateItemTool: ToolConfig<WebflowCreateItemParams, WebflowCreateItemResponse> =
{
id: 'webflow_create_item',
name: 'Webflow Create Item',
description: 'Create a new item in a Webflow CMS collection',
version: '1.0.0',
oauth: {
required: true,
provider: 'webflow',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token',
},
collectionId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ID of the collection',
},
fieldData: {
type: 'json',
required: true,
visibility: 'user-or-llm',
description:
'Field data for the new item as a JSON object. Keys should match collection field names.',
},
},
request: {
url: (params) => `https://api.webflow.com/v2/collections/${params.collectionId}/items`,
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => ({
fieldData: params.fieldData,
}),
},
transformResponse: async (response) => {
const data = await response.json()
return {
success: true,
output: {
item: data,
metadata: {
itemId: data.id || 'unknown',
},
},
}
},
outputs: {
item: {
type: 'json',
description: 'The created item object',
},
metadata: {
type: 'json',
description: 'Metadata about the created item',
},
},
}

View File

@@ -0,0 +1,70 @@
import type { ToolConfig } from '@/tools/types'
import type { WebflowDeleteItemParams, WebflowDeleteItemResponse } from '@/tools/webflow/types'
export const webflowDeleteItemTool: ToolConfig<WebflowDeleteItemParams, WebflowDeleteItemResponse> =
{
id: 'webflow_delete_item',
name: 'Webflow Delete Item',
description: 'Delete an item from a Webflow CMS collection',
version: '1.0.0',
oauth: {
required: true,
provider: 'webflow',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token',
},
collectionId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ID of the collection',
},
itemId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the item to delete',
},
},
request: {
url: (params) =>
`https://api.webflow.com/v2/collections/${params.collectionId}/items/${params.itemId}`,
method: 'DELETE',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response) => {
const isSuccess = response.status === 204 || response.ok
return {
success: isSuccess,
output: {
success: isSuccess,
metadata: {
deleted: isSuccess,
},
},
}
},
outputs: {
success: {
type: 'boolean',
description: 'Whether the deletion was successful',
},
metadata: {
type: 'json',
description: 'Metadata about the deletion',
},
},
}

View File

@@ -0,0 +1,68 @@
import type { ToolConfig } from '@/tools/types'
import type { WebflowGetItemParams, WebflowGetItemResponse } from '@/tools/webflow/types'
export const webflowGetItemTool: ToolConfig<WebflowGetItemParams, WebflowGetItemResponse> = {
id: 'webflow_get_item',
name: 'Webflow Get Item',
description: 'Get a single item from a Webflow CMS collection',
version: '1.0.0',
oauth: {
required: true,
provider: 'webflow',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token',
},
collectionId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ID of the collection',
},
itemId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the item to retrieve',
},
},
request: {
url: (params) =>
`https://api.webflow.com/v2/collections/${params.collectionId}/items/${params.itemId}`,
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response) => {
const data = await response.json()
return {
success: true,
output: {
item: data,
metadata: {
itemId: data.id || 'unknown',
},
},
}
},
outputs: {
item: {
type: 'json',
description: 'The retrieved item object',
},
metadata: {
type: 'json',
description: 'Metadata about the retrieved item',
},
},
}

View File

@@ -0,0 +1,13 @@
import { webflowCreateItemTool } from '@/tools/webflow/create_item'
import { webflowDeleteItemTool } from '@/tools/webflow/delete_item'
import { webflowGetItemTool } from '@/tools/webflow/get_item'
import { webflowListItemsTool } from '@/tools/webflow/list_items'
import { webflowUpdateItemTool } from '@/tools/webflow/update_item'
export {
webflowCreateItemTool,
webflowDeleteItemTool,
webflowGetItemTool,
webflowListItemsTool,
webflowUpdateItemTool,
}

View File

@@ -0,0 +1,88 @@
import type { ToolConfig } from '@/tools/types'
import type { WebflowListItemsParams, WebflowListItemsResponse } from '@/tools/webflow/types'
export const webflowListItemsTool: ToolConfig<WebflowListItemsParams, WebflowListItemsResponse> = {
id: 'webflow_list_items',
name: 'Webflow List Items',
description: 'List all items from a Webflow CMS collection',
version: '1.0.0',
oauth: {
required: true,
provider: 'webflow',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token',
},
collectionId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ID of the collection',
},
offset: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Offset for pagination (optional)',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of items to return (optional, default: 100)',
},
},
request: {
url: (params) => {
const baseUrl = `https://api.webflow.com/v2/collections/${params.collectionId}/items`
const queryParams = new URLSearchParams()
if (params.offset !== undefined) {
queryParams.append('offset', params.offset.toString())
}
if (params.limit !== undefined) {
queryParams.append('limit', params.limit.toString())
}
const query = queryParams.toString()
return query ? `${baseUrl}?${query}` : baseUrl
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response) => {
const data = await response.json()
return {
success: true,
output: {
items: data.items || [],
metadata: {
itemCount: (data.items || []).length,
offset: data.offset,
limit: data.limit,
},
},
}
},
outputs: {
items: {
type: 'json',
description: 'Array of collection items',
},
metadata: {
type: 'json',
description: 'Metadata about the query',
},
},
}

View File

@@ -0,0 +1,95 @@
export interface WebflowBaseParams {
accessToken: string
collectionId: string
}
export interface WebflowListItemsParams extends WebflowBaseParams {
offset?: number
limit?: number
}
export interface WebflowListItemsOutput {
items: any[]
metadata: {
itemCount: number
offset?: number
limit?: number
}
}
export interface WebflowListItemsResponse {
success: boolean
output: WebflowListItemsOutput
}
export interface WebflowGetItemParams extends WebflowBaseParams {
itemId: string
}
export interface WebflowGetItemOutput {
item: any
metadata: {
itemId: string
}
}
export interface WebflowGetItemResponse {
success: boolean
output: WebflowGetItemOutput
}
export interface WebflowCreateItemParams extends WebflowBaseParams {
fieldData: Record<string, any>
}
export interface WebflowCreateItemOutput {
item: any
metadata: {
itemId: string
}
}
export interface WebflowCreateItemResponse {
success: boolean
output: WebflowCreateItemOutput
}
export interface WebflowUpdateItemParams extends WebflowBaseParams {
itemId: string
fieldData: Record<string, any>
}
export interface WebflowUpdateItemOutput {
item: any
metadata: {
itemId: string
}
}
export interface WebflowUpdateItemResponse {
success: boolean
output: WebflowUpdateItemOutput
}
export interface WebflowDeleteItemParams extends WebflowBaseParams {
itemId: string
}
export interface WebflowDeleteItemOutput {
success: boolean
metadata: {
deleted: boolean
}
}
export interface WebflowDeleteItemResponse {
success: boolean
output: WebflowDeleteItemOutput
}
export type WebflowResponse =
| WebflowListItemsResponse
| WebflowGetItemResponse
| WebflowCreateItemResponse
| WebflowUpdateItemResponse
| WebflowDeleteItemResponse

Some files were not shown because too many files have changed in this diff Show More