(
{getKeyboardKeyNameBySystem(key)}
diff --git a/web/i18n/de-DE/workflow.ts b/web/i18n/de-DE/workflow.ts
index d40c640df8..e693d8a64a 100644
--- a/web/i18n/de-DE/workflow.ts
+++ b/web/i18n/de-DE/workflow.ts
@@ -8,7 +8,7 @@ const translation = {
published: 'Veröffentlicht',
publish: 'Veröffentlichen',
update: 'Aktualisieren',
- run: 'Ausführen',
+ run: 'Test ausführen',
running: 'Wird ausgeführt',
inRunMode: 'Im Ausführungsmodus',
inPreview: 'In der Vorschau',
diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts
index d24f2905e3..d7133bec41 100644
--- a/web/i18n/en-US/workflow.ts
+++ b/web/i18n/en-US/workflow.ts
@@ -9,7 +9,7 @@ const translation = {
publish: 'Publish',
update: 'Update',
publishUpdate: 'Publish Update',
- run: 'Run',
+ run: 'Test Run',
running: 'Running',
inRunMode: 'In Run Mode',
inPreview: 'In Preview',
diff --git a/web/i18n/es-ES/workflow.ts b/web/i18n/es-ES/workflow.ts
index aff794f1f6..0cad3627d9 100644
--- a/web/i18n/es-ES/workflow.ts
+++ b/web/i18n/es-ES/workflow.ts
@@ -8,7 +8,7 @@ const translation = {
published: 'Publicado',
publish: 'Publicar',
update: 'Actualizar',
- run: 'Ejecutar',
+ run: 'Ejecutar prueba',
running: 'Ejecutando',
inRunMode: 'En modo de ejecución',
inPreview: 'En vista previa',
diff --git a/web/i18n/fa-IR/workflow.ts b/web/i18n/fa-IR/workflow.ts
index ee3ce148cf..8e1ada19aa 100644
--- a/web/i18n/fa-IR/workflow.ts
+++ b/web/i18n/fa-IR/workflow.ts
@@ -8,7 +8,7 @@ const translation = {
published: 'منتشر شده',
publish: 'انتشار',
update: 'بهروزرسانی',
- run: 'اجرا',
+ run: 'اجرای تست',
running: 'در حال اجرا',
inRunMode: 'در حالت اجرا',
inPreview: 'در پیشنمایش',
diff --git a/web/i18n/fr-FR/workflow.ts b/web/i18n/fr-FR/workflow.ts
index 8022768d44..83a3ddac56 100644
--- a/web/i18n/fr-FR/workflow.ts
+++ b/web/i18n/fr-FR/workflow.ts
@@ -8,7 +8,7 @@ const translation = {
published: 'Publié',
publish: 'Publier',
update: 'Mettre à jour',
- run: 'Exécuter',
+ run: 'Exécuter test',
running: 'En cours d\'exécution',
inRunMode: 'En mode exécution',
inPreview: 'En aperçu',
diff --git a/web/i18n/hi-IN/workflow.ts b/web/i18n/hi-IN/workflow.ts
index 2d04883762..d3b1cc432d 100644
--- a/web/i18n/hi-IN/workflow.ts
+++ b/web/i18n/hi-IN/workflow.ts
@@ -8,7 +8,7 @@ const translation = {
published: 'प्रकाशित',
publish: 'प्रकाशित करें',
update: 'अपडेट करें',
- run: 'चलाएं',
+ run: 'परीक्षण चलाएं',
running: 'चल रहा है',
inRunMode: 'रन मोड में',
inPreview: 'पूर्वावलोकन में',
diff --git a/web/i18n/it-IT/workflow.ts b/web/i18n/it-IT/workflow.ts
index 0b687906fd..6c5aed7693 100644
--- a/web/i18n/it-IT/workflow.ts
+++ b/web/i18n/it-IT/workflow.ts
@@ -8,7 +8,7 @@ const translation = {
published: 'Pubblicato',
publish: 'Pubblica',
update: 'Aggiorna',
- run: 'Esegui',
+ run: 'Esegui test',
running: 'In esecuzione',
inRunMode: 'In modalità di esecuzione',
inPreview: 'In anteprima',
diff --git a/web/i18n/ja-JP/workflow.ts b/web/i18n/ja-JP/workflow.ts
index 7bbd8eeea5..16d3b2f789 100644
--- a/web/i18n/ja-JP/workflow.ts
+++ b/web/i18n/ja-JP/workflow.ts
@@ -9,7 +9,7 @@ const translation = {
publish: '公開する',
update: '更新',
publishUpdate: '更新を公開',
- run: '実行',
+ run: 'テスト実行',
running: '実行中',
inRunMode: '実行モード中',
inPreview: 'プレビュー中',
diff --git a/web/i18n/ko-KR/workflow.ts b/web/i18n/ko-KR/workflow.ts
index c2ec86f059..da735703c0 100644
--- a/web/i18n/ko-KR/workflow.ts
+++ b/web/i18n/ko-KR/workflow.ts
@@ -8,7 +8,7 @@ const translation = {
published: '게시됨',
publish: '게시하기',
update: '업데이트',
- run: '실행',
+ run: '테스트 실행',
running: '실행 중',
inRunMode: '실행 모드',
inPreview: '미리보기 중',
diff --git a/web/i18n/pl-PL/workflow.ts b/web/i18n/pl-PL/workflow.ts
index 8f1d76dcf2..7450388803 100644
--- a/web/i18n/pl-PL/workflow.ts
+++ b/web/i18n/pl-PL/workflow.ts
@@ -8,7 +8,7 @@ const translation = {
published: 'Opublikowane',
publish: 'Opublikuj',
update: 'Aktualizuj',
- run: 'Uruchom',
+ run: 'Uruchom test',
running: 'Uruchamianie',
inRunMode: 'W trybie uruchamiania',
inPreview: 'W podglądzie',
diff --git a/web/i18n/pt-BR/workflow.ts b/web/i18n/pt-BR/workflow.ts
index a490bc0687..ab872893d8 100644
--- a/web/i18n/pt-BR/workflow.ts
+++ b/web/i18n/pt-BR/workflow.ts
@@ -8,7 +8,7 @@ const translation = {
published: 'Publicado',
publish: 'Publicar',
update: 'Atualizar',
- run: 'Executar',
+ run: 'Executar teste',
running: 'Executando',
inRunMode: 'No modo de execução',
inPreview: 'Em visualização',
diff --git a/web/i18n/ro-RO/workflow.ts b/web/i18n/ro-RO/workflow.ts
index 1e55d53235..0720cf8873 100644
--- a/web/i18n/ro-RO/workflow.ts
+++ b/web/i18n/ro-RO/workflow.ts
@@ -8,7 +8,7 @@ const translation = {
published: 'Publicat',
publish: 'Publică',
update: 'Actualizează',
- run: 'Rulează',
+ run: 'Rulează test',
running: 'Rulând',
inRunMode: 'În modul de rulare',
inPreview: 'În previzualizare',
diff --git a/web/i18n/ru-RU/workflow.ts b/web/i18n/ru-RU/workflow.ts
index 48aa9b6e58..37cfb731ef 100644
--- a/web/i18n/ru-RU/workflow.ts
+++ b/web/i18n/ru-RU/workflow.ts
@@ -8,7 +8,7 @@ const translation = {
published: 'Опубликовано',
publish: 'Опубликовать',
update: 'Обновить',
- run: 'Запустить',
+ run: 'Тестовый запуск',
running: 'Выполняется',
inRunMode: 'В режиме выполнения',
inPreview: 'В режиме предпросмотра',
diff --git a/web/i18n/sl-SI/workflow.ts b/web/i18n/sl-SI/workflow.ts
index c7602b1349..c4de67225b 100644
--- a/web/i18n/sl-SI/workflow.ts
+++ b/web/i18n/sl-SI/workflow.ts
@@ -18,7 +18,7 @@ const translation = {
},
versionHistory: 'Zgodovina različic',
published: 'Objavljeno',
- run: 'Teči',
+ run: 'Testni tek',
featuresDocLink: 'Nauči se več',
notRunning: 'Še ne teče',
exportImage: 'Izvozi sliko',
diff --git a/web/i18n/th-TH/workflow.ts b/web/i18n/th-TH/workflow.ts
index 8d4ca5e7ca..fb620883c7 100644
--- a/web/i18n/th-TH/workflow.ts
+++ b/web/i18n/th-TH/workflow.ts
@@ -8,7 +8,7 @@ const translation = {
published: 'เผย แพร่',
publish: 'ตีพิมพ์',
update: 'อัพเดต',
- run: 'วิ่ง',
+ run: 'ทดสอบการทำงาน',
running: 'กำลัง เรียก ใช้',
inRunMode: 'ในโหมดเรียกใช้',
inPreview: 'ในการแสดงตัวอย่าง',
diff --git a/web/i18n/tr-TR/workflow.ts b/web/i18n/tr-TR/workflow.ts
index ca4ab32ebf..c8f16287f3 100644
--- a/web/i18n/tr-TR/workflow.ts
+++ b/web/i18n/tr-TR/workflow.ts
@@ -8,7 +8,7 @@ const translation = {
published: 'Yayınlandı',
publish: 'Yayınla',
update: 'Güncelle',
- run: 'Çalıştır',
+ run: 'Test çalıştır',
running: 'Çalışıyor',
inRunMode: 'Çalıştırma Modunda',
inPreview: 'Ön İzlemede',
diff --git a/web/i18n/uk-UA/workflow.ts b/web/i18n/uk-UA/workflow.ts
index 47c1a12128..2b81ce2a86 100644
--- a/web/i18n/uk-UA/workflow.ts
+++ b/web/i18n/uk-UA/workflow.ts
@@ -8,7 +8,7 @@ const translation = {
published: 'Опубліковано',
publish: 'Опублікувати',
update: 'Оновити',
- run: 'Запустити',
+ run: 'Тестовий запуск',
running: 'Запущено',
inRunMode: 'У режимі запуску',
inPreview: 'У режимі попереднього перегляду',
diff --git a/web/i18n/vi-VN/workflow.ts b/web/i18n/vi-VN/workflow.ts
index ea4488d020..f70dbd5493 100644
--- a/web/i18n/vi-VN/workflow.ts
+++ b/web/i18n/vi-VN/workflow.ts
@@ -8,7 +8,7 @@ const translation = {
published: 'Đã xuất bản',
publish: 'Xuất bản',
update: 'Cập nhật',
- run: 'Chạy',
+ run: 'Chạy thử nghiệm',
running: 'Đang chạy',
inRunMode: 'Chế độ chạy',
inPreview: 'Trong chế độ xem trước',
diff --git a/web/i18n/zh-Hans/workflow.ts b/web/i18n/zh-Hans/workflow.ts
index 85dc8065d9..8c293dd6aa 100644
--- a/web/i18n/zh-Hans/workflow.ts
+++ b/web/i18n/zh-Hans/workflow.ts
@@ -9,7 +9,7 @@ const translation = {
publish: '发布',
update: '更新',
publishUpdate: '发布更新',
- run: '运行',
+ run: '测试运行',
running: '运行中',
inRunMode: '在运行模式中',
inPreview: '预览中',
diff --git a/web/i18n/zh-Hant/workflow.ts b/web/i18n/zh-Hant/workflow.ts
index 659bffa390..505dad8bd1 100644
--- a/web/i18n/zh-Hant/workflow.ts
+++ b/web/i18n/zh-Hant/workflow.ts
@@ -8,7 +8,7 @@ const translation = {
published: '已發佈',
publish: '發佈',
update: '更新',
- run: '運行',
+ run: '測試運行',
running: '運行中',
inRunMode: '在運行模式中',
inPreview: '預覽中',
From 5c4bf7aabdc389cfbbf1873d01e0e78fa2c70b20 Mon Sep 17 00:00:00 2001
From: lyzno1 <92089059+lyzno1@users.noreply.github.com>
Date: Mon, 18 Aug 2025 17:46:36 +0800
Subject: [PATCH 005/180] feat: Test Run dropdown with dynamic trigger
selection (#24113)
---
.../icons/assets/vender/workflow/schedule.svg | 5 +
.../assets/vender/workflow/webhook-line.svg | 3 +
.../icons/src/vender/workflow/Schedule.json | 46 +++++
.../icons/src/vender/workflow/Schedule.tsx | 20 ++
.../src/vender/workflow/WebhookLine.json | 26 +++
.../icons/src/vender/workflow/WebhookLine.tsx | 20 ++
.../base/icons/src/vender/workflow/index.ts | 2 +
web/app/components/workflow/block-icon.tsx | 6 +
.../workflow/header/run-and-history.tsx | 57 +++---
.../workflow/header/test-run-dropdown.tsx | 186 ++++++++++++++++++
.../_base/components/trigger-container.tsx | 8 +-
web/i18n/en-US/workflow.ts | 1 +
web/i18n/ja-JP/workflow.ts | 1 +
web/i18n/zh-Hans/workflow.ts | 1 +
14 files changed, 354 insertions(+), 28 deletions(-)
create mode 100644 web/app/components/base/icons/assets/vender/workflow/schedule.svg
create mode 100644 web/app/components/base/icons/assets/vender/workflow/webhook-line.svg
create mode 100644 web/app/components/base/icons/src/vender/workflow/Schedule.json
create mode 100644 web/app/components/base/icons/src/vender/workflow/Schedule.tsx
create mode 100644 web/app/components/base/icons/src/vender/workflow/WebhookLine.json
create mode 100644 web/app/components/base/icons/src/vender/workflow/WebhookLine.tsx
create mode 100644 web/app/components/workflow/header/test-run-dropdown.tsx
diff --git a/web/app/components/base/icons/assets/vender/workflow/schedule.svg b/web/app/components/base/icons/assets/vender/workflow/schedule.svg
new file mode 100644
index 0000000000..69977c4c7f
--- /dev/null
+++ b/web/app/components/base/icons/assets/vender/workflow/schedule.svg
@@ -0,0 +1,5 @@
+
diff --git a/web/app/components/base/icons/assets/vender/workflow/webhook-line.svg b/web/app/components/base/icons/assets/vender/workflow/webhook-line.svg
new file mode 100644
index 0000000000..16fd30a961
--- /dev/null
+++ b/web/app/components/base/icons/assets/vender/workflow/webhook-line.svg
@@ -0,0 +1,3 @@
+
diff --git a/web/app/components/base/icons/src/vender/workflow/Schedule.json b/web/app/components/base/icons/src/vender/workflow/Schedule.json
new file mode 100644
index 0000000000..1c2d181dc4
--- /dev/null
+++ b/web/app/components/base/icons/src/vender/workflow/Schedule.json
@@ -0,0 +1,46 @@
+{
+ "icon": {
+ "type": "element",
+ "isRootNode": true,
+ "name": "svg",
+ "attributes": {
+ "width": "16",
+ "height": "16",
+ "viewBox": "0 0 16 16",
+ "fill": "none",
+ "xmlns": "http://www.w3.org/2000/svg"
+ },
+ "children": [
+ {
+ "type": "element",
+ "name": "path",
+ "attributes": {
+ "d": "M11.3333 9.33337C11.7015 9.33337 11.9999 9.63193 12 10V11.0573L12.8047 11.862L12.8503 11.9128C13.0638 12.1746 13.0487 12.5607 12.8047 12.8047C12.5606 13.0488 12.1746 13.0639 11.9128 12.8503L11.862 12.8047L10.862 11.8047C10.7371 11.6798 10.6667 11.5101 10.6667 11.3334V10C10.6668 9.63193 10.9652 9.33337 11.3333 9.33337Z",
+ "fill": "currentColor"
+ },
+ "children": []
+ },
+ {
+ "type": "element",
+ "name": "path",
+ "attributes": {
+ "fill-rule": "evenodd",
+ "clip-rule": "evenodd",
+ "d": "M11.3333 7.33337C13.5425 7.33337 15.3333 9.12424 15.3333 11.3334C15.3333 13.5425 13.5425 15.3334 11.3333 15.3334C9.12419 15.3334 7.33333 13.5425 7.33333 11.3334C7.33333 9.12424 9.12419 7.33337 11.3333 7.33337ZM11.3333 8.66671C9.86057 8.66671 8.66667 9.86061 8.66667 11.3334C8.66667 12.8061 9.86057 14 11.3333 14C12.8061 14 14 12.8061 14 11.3334C14 9.86061 12.8061 8.66671 11.3333 8.66671Z",
+ "fill": "currentColor"
+ },
+ "children": []
+ },
+ {
+ "type": "element",
+ "name": "path",
+ "attributes": {
+ "d": "M10.6667 1.33337C11.0349 1.33337 11.3333 1.63185 11.3333 2.00004V2.66671H12.6667C13.4031 2.66671 14 3.26367 14 4.00004V5.66671C14 6.0349 13.7015 6.33337 13.3333 6.33337C12.9651 6.33337 12.6667 6.0349 12.6667 5.66671V4.00004H3.33333V12.6667H5.66667C6.03486 12.6667 6.33333 12.9652 6.33333 13.3334C6.33333 13.7016 6.03486 14 5.66667 14H3.33333C2.59697 14 2 13.4031 2 12.6667V4.00004C2 3.26366 2.59696 2.66671 3.33333 2.66671H4.66667V2.00004C4.66667 1.63185 4.96514 1.33337 5.33333 1.33337C5.70152 1.33337 6 1.63185 6 2.00004V2.66671H10V2.00004C10 1.63185 10.2985 1.33337 10.6667 1.33337Z",
+ "fill": "currentColor"
+ },
+ "children": []
+ }
+ ]
+ },
+ "name": "Schedule"
+}
diff --git a/web/app/components/base/icons/src/vender/workflow/Schedule.tsx b/web/app/components/base/icons/src/vender/workflow/Schedule.tsx
new file mode 100644
index 0000000000..86b8506d6e
--- /dev/null
+++ b/web/app/components/base/icons/src/vender/workflow/Schedule.tsx
@@ -0,0 +1,20 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './Schedule.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = (
+ {
+ ref,
+ ...props
+ }: React.SVGProps
& {
+ ref?: React.RefObject>;
+ },
+) =>
+
+Icon.displayName = 'Schedule'
+
+export default Icon
diff --git a/web/app/components/base/icons/src/vender/workflow/WebhookLine.json b/web/app/components/base/icons/src/vender/workflow/WebhookLine.json
new file mode 100644
index 0000000000..8319fd25f3
--- /dev/null
+++ b/web/app/components/base/icons/src/vender/workflow/WebhookLine.json
@@ -0,0 +1,26 @@
+{
+ "icon": {
+ "type": "element",
+ "isRootNode": true,
+ "name": "svg",
+ "attributes": {
+ "width": "16",
+ "height": "16",
+ "viewBox": "0 0 16 16",
+ "fill": "none",
+ "xmlns": "http://www.w3.org/2000/svg"
+ },
+ "children": [
+ {
+ "type": "element",
+ "name": "path",
+ "attributes": {
+ "d": "M5.91246 9.42618C5.77036 9.66084 5.70006 9.85191 5.81358 10.1502C6.12696 10.9742 5.68488 11.776 4.85394 11.9937C4.07033 12.199 3.30686 11.684 3.15138 10.8451C3.01362 10.1025 3.58988 9.37451 4.40859 9.25851C4.45305 9.25211 4.49808 9.24938 4.55563 9.24591C4.58692 9.24404 4.62192 9.24191 4.66252 9.23884L5.90792 7.15051C5.12463 6.37166 4.65841 5.46114 4.7616 4.33295C4.83455 3.53543 5.14813 2.84626 5.72135 2.28138C6.81916 1.19968 8.49403 1.02449 9.78663 1.85479C11.0282 2.65232 11.5967 4.20582 11.112 5.53545L9.97403 5.22671C10.1263 4.48748 10.0137 3.82362 9.5151 3.25494C9.1857 2.87947 8.76303 2.68267 8.28236 2.61015C7.31883 2.46458 6.37278 3.08364 6.09207 4.02937C5.77342 5.10275 6.25566 5.97954 7.5735 6.64023C7.0207 7.56944 6.47235 8.50124 5.91246 9.42618ZM9.18916 5.51562C9.5877 6.2187 9.99236 6.93244 10.3934 7.63958C12.4206 7.01244 13.9491 8.13458 14.4974 9.33604C15.1597 10.7873 14.707 12.5062 13.4062 13.4016C12.0711 14.3207 10.3827 14.1636 9.19976 12.983L10.1279 12.2063C11.2962 12.963 12.3181 12.9274 13.0767 12.0314C13.7236 11.2669 13.7096 10.1271 13.0439 9.37871C12.2757 8.51511 11.2467 8.48878 10.0029 9.31784C9.48696 8.40251 8.96196 7.49424 8.46236 6.57234C8.2939 6.2616 8.10783 6.08135 7.72816 6.01558C7.09403 5.90564 6.68463 5.36109 6.66007 4.75099C6.63593 4.14763 6.99136 3.60224 7.54696 3.38974C8.0973 3.17924 8.74316 3.34916 9.11336 3.81707C9.4159 4.19938 9.51203 4.62966 9.35283 5.10116C9.32283 5.19018 9.28689 5.27727 9.2475 5.37261C9.22869 5.418 9.20916 5.46538 9.18916 5.51562ZM7.7013 11.2634H10.1417C10.1757 11.3087 10.2075 11.3536 10.2386 11.3973C10.3034 11.4887 10.3649 11.5755 10.4367 11.6526C10.9536 12.2052 11.8263 12.2326 12.3788 11.7197C12.9514 11.1881 12.9773 10.2951 12.4362 9.74011C11.9068 9.19704 11.0019 9.14518 10.5103 9.72018C10.2117 10.0696 9.9057 10.1107 9.50936 10.1045C8.49423 10.0888 7.47843 10.0994 6.46346 10.0994C6.52934 11.5273 5.98953 12.417 4.9189 12.6283C3.87051 12.8352 2.90496 12.3003 2.56502 11.3243C2.17891 10.2153 2.65641 9.32838 4.0361 8.62444C3.93228 8.24838 3.8274 7.86778 3.72357 7.49071C2.21981 7.81844 1.09162 9.27738 1.20809 10.9187C1.31097 12.3676 2.47975 13.6544 3.90909 13.8849C4.68542 14.0102 5.41485 13.88 6.09157 13.4962C6.96216 13.0022 7.46736 12.2254 7.7013 11.2634Z",
+ "fill": "currentColor"
+ },
+ "children": []
+ }
+ ]
+ },
+ "name": "WebhookLine"
+}
diff --git a/web/app/components/base/icons/src/vender/workflow/WebhookLine.tsx b/web/app/components/base/icons/src/vender/workflow/WebhookLine.tsx
new file mode 100644
index 0000000000..da1143c16e
--- /dev/null
+++ b/web/app/components/base/icons/src/vender/workflow/WebhookLine.tsx
@@ -0,0 +1,20 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './WebhookLine.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = (
+ {
+ ref,
+ ...props
+ }: React.SVGProps & {
+ ref?: React.RefObject>;
+ },
+) =>
+
+Icon.displayName = 'WebhookLine'
+
+export default Icon
diff --git a/web/app/components/base/icons/src/vender/workflow/index.ts b/web/app/components/base/icons/src/vender/workflow/index.ts
index 61fbd4b21c..3e7a46dd99 100644
--- a/web/app/components/base/icons/src/vender/workflow/index.ts
+++ b/web/app/components/base/icons/src/vender/workflow/index.ts
@@ -17,6 +17,8 @@ export { default as LoopEnd } from './LoopEnd'
export { default as Loop } from './Loop'
export { default as ParameterExtractor } from './ParameterExtractor'
export { default as QuestionClassifier } from './QuestionClassifier'
+export { default as Schedule } from './Schedule'
export { default as TemplatingTransform } from './TemplatingTransform'
export { default as VariableX } from './VariableX'
+export { default as WebhookLine } from './WebhookLine'
export { default as WindowCursor } from './WindowCursor'
diff --git a/web/app/components/workflow/block-icon.tsx b/web/app/components/workflow/block-icon.tsx
index 1e76efc2aa..d8961c6826 100644
--- a/web/app/components/workflow/block-icon.tsx
+++ b/web/app/components/workflow/block-icon.tsx
@@ -19,8 +19,10 @@ import {
LoopEnd,
ParameterExtractor,
QuestionClassifier,
+ Schedule,
TemplatingTransform,
VariableX,
+ WebhookLine,
} from '@/app/components/base/icons/src/vender/workflow'
import AppIcon from '@/app/components/base/app-icon'
@@ -60,6 +62,8 @@ const getIcon = (type: BlockEnum, className: string) => {
[BlockEnum.DocExtractor]: ,
[BlockEnum.ListFilter]: ,
[BlockEnum.Agent]: ,
+ [BlockEnum.TriggerSchedule]: ,
+ [BlockEnum.TriggerWebhook]: ,
}[type]
}
const ICON_CONTAINER_BG_COLOR_MAP: Record = {
@@ -83,6 +87,8 @@ const ICON_CONTAINER_BG_COLOR_MAP: Record = {
[BlockEnum.DocExtractor]: 'bg-util-colors-green-green-500',
[BlockEnum.ListFilter]: 'bg-util-colors-cyan-cyan-500',
[BlockEnum.Agent]: 'bg-util-colors-indigo-indigo-500',
+ [BlockEnum.TriggerSchedule]: 'bg-util-colors-violet-violet-500',
+ [BlockEnum.TriggerWebhook]: 'bg-util-colors-blue-blue-500',
}
const BlockIcon: FC = ({
type,
diff --git a/web/app/components/workflow/header/run-and-history.tsx b/web/app/components/workflow/header/run-and-history.tsx
index 0667a8af89..bb50734024 100644
--- a/web/app/components/workflow/header/run-and-history.tsx
+++ b/web/app/components/workflow/header/run-and-history.tsx
@@ -15,6 +15,8 @@ import {
import { WorkflowRunningStatus } from '../types'
import ViewHistory from './view-history'
import Checklist from './checklist'
+import TestRunDropdown, { createMockOptions } from './test-run-dropdown'
+import type { TriggerOption } from './test-run-dropdown'
import cn from '@/utils/classnames'
import {
StopCircle,
@@ -35,6 +37,11 @@ const RunMode = memo(() => {
handleStopRun(workflowRunningData?.task_id || '')
}
+ const handleTriggerSelect = (option: TriggerOption) => {
+ console.log('Selected trigger:', option)
+ handleWorkflowStartRunInWorkflow()
+ }
+
const { eventEmitter } = useEventEmitterContextContext()
eventEmitter?.useSubscription((v: any) => {
if (v.type === EVENT_WORKFLOW_STOP)
@@ -43,33 +50,35 @@ const RunMode = memo(() => {
return (
<>
- {
- handleWorkflowStartRunInWorkflow()
- }}
- >
- {
- isRunning
- ? (
- <>
-
- {t('workflow.common.running')}
- >
- )
- : (
- <>
+ {
+ isRunning
+ ? (
+
+
+ {t('workflow.common.running')}
+
+ )
+ : (
+
+
{t('workflow.common.run')}
- >
- )
- }
-
+
+
+ )
+ }
{
isRunning && (
void
+ children: React.ReactNode
+}
+
+const createMockOptions = (): TestRunOptions => {
+ const userInput: TriggerOption = {
+ id: 'user-input-1',
+ type: 'user_input',
+ name: 'User Input',
+ icon: (
+
+
+
+ ),
+ nodeId: 'start-node-1',
+ enabled: true,
+ }
+
+ const runAll: TriggerOption = {
+ id: 'run-all',
+ type: 'all',
+ name: 'Run all triggers',
+ icon: (
+
+ ),
+ enabled: true,
+ }
+
+ const triggers: TriggerOption[] = [
+ {
+ id: 'slack-trigger-1',
+ type: 'plugin',
+ name: 'Slack Trigger',
+ icon: (
+
+ ),
+ nodeId: 'slack-trigger-1',
+ enabled: true,
+ },
+ {
+ id: 'zapier-trigger-1',
+ type: 'plugin',
+ name: 'Zapier Trigger',
+ icon: (
+
+ ),
+ nodeId: 'zapier-trigger-1',
+ enabled: true,
+ },
+ {
+ id: 'gmail-trigger-1',
+ type: 'plugin',
+ name: 'Gmail Sender',
+ icon: (
+
+
+
+ ),
+ nodeId: 'gmail-trigger-1',
+ enabled: true,
+ },
+ ]
+
+ return {
+ userInput,
+ triggers,
+ runAll: triggers.length > 1 ? runAll : undefined,
+ }
+}
+
+const TestRunDropdown: FC
= ({
+ options,
+ onSelect,
+ children,
+}) => {
+ const { t } = useTranslation()
+ const [open, setOpen] = useState(false)
+
+ const handleSelect = (option: TriggerOption) => {
+ onSelect(option)
+ setOpen(false)
+ }
+
+ const renderOption = (option: TriggerOption, numberDisplay: string) => (
+ handleSelect(option)}
+ >
+
+ {option.icon}
+ {option.name}
+
+
+ {numberDisplay}
+
+
+ )
+
+ const hasUserInput = !!options.userInput
+ const hasTriggers = options.triggers.length > 0
+ const hasRunAll = !!options.runAll
+
+ let currentIndex = 0
+
+ return (
+
+ setOpen(!open)}>
+
+ {children}
+
+
+
+
+
+ {t('workflow.common.chooseStartNodeToRun')}
+
+
+ {hasUserInput && renderOption(options.userInput!, '~')}
+
+ {(hasTriggers || hasRunAll) && hasUserInput && (
+
+ )}
+
+ {hasRunAll && renderOption(options.runAll!, String(currentIndex++))}
+
+ {hasTriggers && options.triggers.map(trigger =>
+ renderOption(trigger, String(currentIndex++)),
+ )}
+
+
+
+
+ )
+}
+
+export { createMockOptions }
+export default TestRunDropdown
diff --git a/web/app/components/workflow/nodes/_base/components/trigger-container.tsx b/web/app/components/workflow/nodes/_base/components/trigger-container.tsx
index 97853126c0..a4c3224eab 100644
--- a/web/app/components/workflow/nodes/_base/components/trigger-container.tsx
+++ b/web/app/components/workflow/nodes/_base/components/trigger-container.tsx
@@ -28,10 +28,10 @@ const TriggerContainer: FC = ({
}, [status, customLabel, t])
return (
-
-
-
-
+
+
diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts
index d7133bec41..ce058668ff 100644
--- a/web/i18n/en-US/workflow.ts
+++ b/web/i18n/en-US/workflow.ts
@@ -11,6 +11,7 @@ const translation = {
publishUpdate: 'Publish Update',
run: 'Test Run',
running: 'Running',
+ chooseStartNodeToRun: 'Choose the start node to run',
inRunMode: 'In Run Mode',
inPreview: 'In Preview',
inPreviewMode: 'In Preview Mode',
diff --git a/web/i18n/ja-JP/workflow.ts b/web/i18n/ja-JP/workflow.ts
index 16d3b2f789..83885d0134 100644
--- a/web/i18n/ja-JP/workflow.ts
+++ b/web/i18n/ja-JP/workflow.ts
@@ -11,6 +11,7 @@ const translation = {
publishUpdate: '更新を公開',
run: 'テスト実行',
running: '実行中',
+ chooseStartNodeToRun: '実行する開始ノードを選択',
inRunMode: '実行モード中',
inPreview: 'プレビュー中',
inPreviewMode: 'プレビューモード中',
diff --git a/web/i18n/zh-Hans/workflow.ts b/web/i18n/zh-Hans/workflow.ts
index 8c293dd6aa..d5a1f2115a 100644
--- a/web/i18n/zh-Hans/workflow.ts
+++ b/web/i18n/zh-Hans/workflow.ts
@@ -11,6 +11,7 @@ const translation = {
publishUpdate: '发布更新',
run: '测试运行',
running: '运行中',
+ chooseStartNodeToRun: '选择启动节点进行运行',
inRunMode: '在运行模式中',
inPreview: '预览中',
inPreviewMode: '预览中',
From 6a3d135d4936e0e645e8324cd64a6bc9286fd13b Mon Sep 17 00:00:00 2001
From: lyzno1 <92089059+lyzno1@users.noreply.github.com>
Date: Mon, 18 Aug 2025 23:37:57 +0800
Subject: [PATCH 006/180] fix: simplify trigger-schedule hourly mode
calculation and improve UI consistency (#24082)
Co-authored-by: zhangxuhe1
---
.../date-picker/index.tsx | 21 +-
.../time-picker/index.tsx | 21 +-
.../base/date-and-time-picker/types.ts | 2 +
.../components/date-time-picker.spec.tsx | 139 ----------
.../components/date-time-picker.tsx | 158 ------------
.../components/frequency-selector.tsx | 3 +-
.../components/monthly-days-selector.tsx | 8 +-
.../components/next-execution-times.tsx | 4 +-
.../components/time-picker.tsx | 230 -----------------
.../components/weekday-selector.tsx | 8 +-
.../workflow/nodes/trigger-schedule/panel.tsx | 43 +++-
.../nodes/trigger-schedule/use-config.ts | 9 +
.../utils/execution-time-calculator.spec.ts | 243 +++++++++++++++++-
.../utils/execution-time-calculator.ts | 22 +-
14 files changed, 320 insertions(+), 591 deletions(-)
delete mode 100644 web/app/components/workflow/nodes/trigger-schedule/components/date-time-picker.spec.tsx
delete mode 100644 web/app/components/workflow/nodes/trigger-schedule/components/date-time-picker.tsx
delete mode 100644 web/app/components/workflow/nodes/trigger-schedule/components/time-picker.tsx
diff --git a/web/app/components/base/date-and-time-picker/date-picker/index.tsx b/web/app/components/base/date-and-time-picker/date-picker/index.tsx
index f99b8257c1..53cf383dad 100644
--- a/web/app/components/base/date-and-time-picker/date-picker/index.tsx
+++ b/web/app/components/base/date-and-time-picker/date-picker/index.tsx
@@ -36,6 +36,7 @@ const DatePicker = ({
renderTrigger,
triggerWrapClassName,
popupZIndexClassname = 'z-[11]',
+ notClearable = false,
}: DatePickerProps) => {
const { t } = useTranslation()
const [isOpen, setIsOpen] = useState(false)
@@ -200,7 +201,7 @@ const DatePicker = ({
{renderTrigger ? (renderTrigger({
@@ -224,15 +225,17 @@ const DatePicker = ({
-
+ {!notClearable && (
+
+ )}
)}
diff --git a/web/app/components/base/date-and-time-picker/time-picker/index.tsx b/web/app/components/base/date-and-time-picker/time-picker/index.tsx
index 8ef10abc2e..830ba4bf0b 100644
--- a/web/app/components/base/date-and-time-picker/time-picker/index.tsx
+++ b/web/app/components/base/date-and-time-picker/time-picker/index.tsx
@@ -23,6 +23,7 @@ const TimePicker = ({
title,
minuteFilter,
popupClassName,
+ notClearable = false,
}: TimePickerProps) => {
const { t } = useTranslation()
const [isOpen, setIsOpen] = useState(false)
@@ -123,7 +124,7 @@ const TimePicker = ({
{renderTrigger ? (renderTrigger({
@@ -139,15 +140,17 @@ const TimePicker = ({
-
+ {!notClearable && (
+
+ )}
)}
diff --git a/web/app/components/base/date-and-time-picker/types.ts b/web/app/components/base/date-and-time-picker/types.ts
index 4ac01c142a..68d6967c2b 100644
--- a/web/app/components/base/date-and-time-picker/types.ts
+++ b/web/app/components/base/date-and-time-picker/types.ts
@@ -30,6 +30,7 @@ export type DatePickerProps = {
renderTrigger?: (props: TriggerProps) => React.ReactNode
minuteFilter?: (minutes: string[]) => string[]
popupZIndexClassname?: string
+ notClearable?: boolean
}
export type DatePickerHeaderProps = {
@@ -63,6 +64,7 @@ export type TimePickerProps = {
title?: string
minuteFilter?: (minutes: string[]) => string[]
popupClassName?: string
+ notClearable?: boolean
}
export type TimePickerFooterProps = {
diff --git a/web/app/components/workflow/nodes/trigger-schedule/components/date-time-picker.spec.tsx b/web/app/components/workflow/nodes/trigger-schedule/components/date-time-picker.spec.tsx
deleted file mode 100644
index 4d5a55029a..0000000000
--- a/web/app/components/workflow/nodes/trigger-schedule/components/date-time-picker.spec.tsx
+++ /dev/null
@@ -1,139 +0,0 @@
-import React from 'react'
-import { fireEvent, render, screen, waitFor } from '@testing-library/react'
-import DateTimePicker from './date-time-picker'
-
-jest.mock('react-i18next', () => ({
- useTranslation: () => ({
- t: (key: string) => {
- const translations: Record
= {
- 'workflow.nodes.triggerSchedule.selectDateTime': 'Select Date & Time',
- 'common.operation.now': 'Now',
- 'common.operation.ok': 'OK',
- }
- return translations[key] || key
- },
- }),
-}))
-
-describe('DateTimePicker', () => {
- const mockOnChange = jest.fn()
-
- beforeEach(() => {
- jest.clearAllMocks()
- })
-
- test('renders with default value', () => {
- render()
-
- const button = screen.getByRole('button')
- expect(button).toBeInTheDocument()
- expect(button.textContent).toMatch(/\d+, \d{4} \d{1,2}:\d{2} [AP]M/)
- })
-
- test('renders with provided value', () => {
- const testDate = new Date('2024-01-15T14:30:00.000Z')
- render()
-
- const button = screen.getByRole('button')
- expect(button).toBeInTheDocument()
- })
-
- test('opens picker when button is clicked', () => {
- render()
-
- const button = screen.getByRole('button')
- fireEvent.click(button)
-
- expect(screen.getByText('Select Date & Time')).toBeInTheDocument()
- expect(screen.getByText('Now')).toBeInTheDocument()
- expect(screen.getByText('OK')).toBeInTheDocument()
- })
-
- test('closes picker when clicking outside', () => {
- render()
-
- const button = screen.getByRole('button')
- fireEvent.click(button)
-
- expect(screen.getByText('Select Date & Time')).toBeInTheDocument()
-
- const overlay = document.querySelector('.fixed.inset-0')
- fireEvent.click(overlay!)
-
- expect(screen.queryByText('Select Date & Time')).not.toBeInTheDocument()
- })
-
- test('does not call onChange when input changes without clicking OK', () => {
- render()
-
- const button = screen.getByRole('button')
- fireEvent.click(button)
-
- const input = screen.getByDisplayValue(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/)
- fireEvent.change(input, { target: { value: '2024-12-25T15:30' } })
-
- const overlay = document.querySelector('.fixed.inset-0')
- fireEvent.click(overlay!)
-
- expect(mockOnChange).not.toHaveBeenCalled()
- })
-
- test('calls onChange when clicking OK button', () => {
- render()
-
- const button = screen.getByRole('button')
- fireEvent.click(button)
-
- const input = screen.getByDisplayValue(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/)
- fireEvent.change(input, { target: { value: '2024-12-25T15:30' } })
-
- const okButton = screen.getByText('OK')
- fireEvent.click(okButton)
-
- expect(mockOnChange).toHaveBeenCalledWith(expect.stringMatching(/2024-12-25T.*:30.*Z/))
- })
-
- test('calls onChange when clicking Now button', () => {
- render()
-
- const button = screen.getByRole('button')
- fireEvent.click(button)
-
- const nowButton = screen.getByText('Now')
- fireEvent.click(nowButton)
-
- expect(mockOnChange).toHaveBeenCalledWith(expect.stringMatching(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/))
- })
-
- test('resets temp value when reopening picker', async () => {
- render()
-
- const button = screen.getByRole('button')
- fireEvent.click(button)
-
- const input = screen.getByDisplayValue(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/)
- const originalValue = input.getAttribute('value')
-
- fireEvent.change(input, { target: { value: '2024-12-25T15:30' } })
- expect(input.getAttribute('value')).toBe('2024-12-25T15:30')
-
- const overlay = document.querySelector('.fixed.inset-0')
- fireEvent.click(overlay!)
-
- fireEvent.click(button)
-
- await waitFor(() => {
- const newInput = screen.getByDisplayValue(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/)
- expect(newInput.getAttribute('value')).toBe(originalValue)
- })
- })
-
- test('displays current value in button text', () => {
- const testDate = new Date('2024-01-15T14:30:00.000Z')
- render()
-
- const button = screen.getByRole('button')
- expect(button.textContent).toMatch(/January 15, 2024/)
- expect(button.textContent).toMatch(/\d{1,2}:30 [AP]M/)
- })
-})
diff --git a/web/app/components/workflow/nodes/trigger-schedule/components/date-time-picker.tsx b/web/app/components/workflow/nodes/trigger-schedule/components/date-time-picker.tsx
deleted file mode 100644
index 5c8dffadd0..0000000000
--- a/web/app/components/workflow/nodes/trigger-schedule/components/date-time-picker.tsx
+++ /dev/null
@@ -1,158 +0,0 @@
-import React, { useState } from 'react'
-import { useTranslation } from 'react-i18next'
-import { RiCalendarLine } from '@remixicon/react'
-import { getDefaultDateTime } from '../utils/execution-time-calculator'
-
-type DateTimePickerProps = {
- value?: string
- onChange: (datetime: string) => void
-}
-
-const DateTimePicker = ({ value, onChange }: DateTimePickerProps) => {
- const { t } = useTranslation()
- const [isOpen, setIsOpen] = useState(false)
- const [tempValue, setTempValue] = useState('')
-
- React.useEffect(() => {
- if (isOpen)
- setTempValue('')
- }, [isOpen])
-
- const getCurrentDateTime = () => {
- if (value) {
- try {
- const date = new Date(value)
- return `${date.toLocaleDateString('en-US', {
- year: 'numeric',
- month: 'long',
- day: 'numeric',
- })} ${date.toLocaleTimeString('en-US', {
- hour: 'numeric',
- minute: '2-digit',
- hour12: true,
- })}`
- }
- catch {
- // fallback
- }
- }
-
- const defaultDate = getDefaultDateTime()
-
- return `${defaultDate.toLocaleDateString('en-US', {
- year: 'numeric',
- month: 'long',
- day: 'numeric',
- })} ${defaultDate.toLocaleTimeString('en-US', {
- hour: 'numeric',
- minute: '2-digit',
- hour12: true,
- })}`
- }
-
- const handleDateTimeChange = (event: React.ChangeEvent) => {
- const dateTimeValue = event.target.value
- setTempValue(dateTimeValue)
- }
-
- const getInputValue = () => {
- if (tempValue)
- return tempValue
-
- if (value) {
- try {
- const date = new Date(value)
- const year = date.getFullYear()
- const month = String(date.getMonth() + 1).padStart(2, '0')
- const day = String(date.getDate()).padStart(2, '0')
- const hours = String(date.getHours()).padStart(2, '0')
- const minutes = String(date.getMinutes()).padStart(2, '0')
- return `${year}-${month}-${day}T${hours}:${minutes}`
- }
- catch {
- // fallback
- }
- }
-
- const defaultDate = getDefaultDateTime()
- const year = defaultDate.getFullYear()
- const month = String(defaultDate.getMonth() + 1).padStart(2, '0')
- const day = String(defaultDate.getDate()).padStart(2, '0')
- const hours = String(defaultDate.getHours()).padStart(2, '0')
- const minutes = String(defaultDate.getMinutes()).padStart(2, '0')
- return `${year}-${month}-${day}T${hours}:${minutes}`
- }
-
- return (
-
-
-
- {isOpen && (
-
-
-
{t('workflow.nodes.triggerSchedule.selectDateTime')}
-
-
-
-
-
-
-
-
-
-
-
-
-
- )}
-
- {isOpen && (
-
{
- setTempValue('')
- setIsOpen(false)
- }}
- />
- )}
-
- )
-}
-
-export default DateTimePicker
diff --git a/web/app/components/workflow/nodes/trigger-schedule/components/frequency-selector.tsx b/web/app/components/workflow/nodes/trigger-schedule/components/frequency-selector.tsx
index fa48a66350..2eabb6d85f 100644
--- a/web/app/components/workflow/nodes/trigger-schedule/components/frequency-selector.tsx
+++ b/web/app/components/workflow/nodes/trigger-schedule/components/frequency-selector.tsx
@@ -27,7 +27,8 @@ const FrequencySelector = ({ frequency, onChange }: FrequencySelectorProps) => {
defaultValue={frequency}
onSelect={item => onChange(item.value as ScheduleFrequency)}
placeholder={t('workflow.nodes.triggerSchedule.selectFrequency')}
- className="w-full"
+ className="w-full py-2"
+ wrapperClassName="h-auto"
optionWrapClassName="min-w-40"
notClearable={true}
allowSearch={false}
diff --git a/web/app/components/workflow/nodes/trigger-schedule/components/monthly-days-selector.tsx b/web/app/components/workflow/nodes/trigger-schedule/components/monthly-days-selector.tsx
index 4c9c8b75b6..68936bf253 100644
--- a/web/app/components/workflow/nodes/trigger-schedule/components/monthly-days-selector.tsx
+++ b/web/app/components/workflow/nodes/trigger-schedule/components/monthly-days-selector.tsx
@@ -22,7 +22,7 @@ const MonthlyDaysSelector = ({ selectedDay, onChange }: MonthlyDaysSelectorProps
return (
-
diff --git a/web/app/components/app/configuration/config/automatic/get-automatic-res.tsx b/web/app/components/app/configuration/config/automatic/get-automatic-res.tsx
index aacaa81ac2..31f81d274d 100644
--- a/web/app/components/app/configuration/config/automatic/get-automatic-res.tsx
+++ b/web/app/components/app/configuration/config/automatic/get-automatic-res.tsx
@@ -2,7 +2,7 @@
import type { FC } from 'react'
import React, { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
-import { useBoolean } from 'ahooks'
+import { useBoolean, useSessionStorageState } from 'ahooks'
import {
RiDatabase2Line,
RiFileExcel2Line,
@@ -14,24 +14,18 @@ import {
RiTranslate,
RiUser2Line,
} from '@remixicon/react'
-import cn from 'classnames'
import s from './style.module.css'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
-import Textarea from '@/app/components/base/textarea'
import Toast from '@/app/components/base/toast'
-import { generateRule } from '@/service/debug'
-import ConfigPrompt from '@/app/components/app/configuration/config-prompt'
+import { generateBasicAppFistTimeRule, generateRule } from '@/service/debug'
import type { CompletionParams, Model } from '@/types/app'
-import { AppType } from '@/types/app'
-import ConfigVar from '@/app/components/app/configuration/config-var'
-import GroupName from '@/app/components/app/configuration/base/group-name'
+import type { AppType } from '@/types/app'
import Loading from '@/app/components/base/loading'
import Confirm from '@/app/components/base/confirm'
-import { LoveMessage } from '@/app/components/base/icons/src/vender/features'
// type
-import type { AutomaticRes } from '@/service/debug'
+import type { GenRes } from '@/service/debug'
import { Generator } from '@/app/components/base/icons/src/vender/other'
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
@@ -39,13 +33,25 @@ import { ModelTypeEnum } from '@/app/components/header/account-setting/model-pro
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
import type { ModelModeType } from '@/types/app'
import type { FormValue } from '@/app/components/header/account-setting/model-provider-page/declarations'
+import InstructionEditorInWorkflow from './instruction-editor-in-workflow'
+import InstructionEditorInBasic from './instruction-editor'
+import { GeneratorType } from './types'
+import Result from './result'
+import useGenData from './use-gen-data'
+import IdeaOutput from './idea-output'
+import ResPlaceholder from './res-placeholder'
+import { useGenerateRuleTemplate } from '@/service/use-apps'
+const i18nPrefix = 'appDebug.generate'
export type IGetAutomaticResProps = {
mode: AppType
isShow: boolean
onClose: () => void
- onFinished: (res: AutomaticRes) => void
- isInLLMNode?: boolean
+ onFinished: (res: GenRes) => void
+ flowId?: string
+ nodeId?: string
+ currentPrompt?: string
+ isBasicMode?: boolean
}
const TryLabel: FC<{
@@ -68,7 +74,10 @@ const GetAutomaticRes: FC
= ({
mode,
isShow,
onClose,
- isInLLMNode,
+ flowId,
+ nodeId,
+ currentPrompt,
+ isBasicMode,
onFinished,
}) => {
const { t } = useTranslation()
@@ -123,13 +132,27 @@ const GetAutomaticRes: FC = ({
},
]
- const [instruction, setInstruction] = useState('')
+ const [instructionFromSessionStorage, setInstruction] = useSessionStorageState(`improve-instruction-${flowId}${isBasicMode ? '' : `-${nodeId}`}`)
+ const instruction = instructionFromSessionStorage || ''
+ const [ideaOutput, setIdeaOutput] = useState('')
+
+ const [editorKey, setEditorKey] = useState(`${flowId}-0`)
const handleChooseTemplate = useCallback((key: string) => {
return () => {
const template = t(`appDebug.generate.template.${key}.instruction`)
setInstruction(template)
+ setEditorKey(`${flowId}-${Date.now()}`)
}
}, [t])
+
+ const { data: instructionTemplate } = useGenerateRuleTemplate(GeneratorType.prompt, isBasicMode)
+ useEffect(() => {
+ if (!instruction && instructionTemplate)
+ setInstruction(instructionTemplate.data)
+
+ setEditorKey(`${flowId}-${Date.now()}`)
+ }, [instructionTemplate])
+
const isValid = () => {
if (instruction.trim() === '') {
Toast.notify({
@@ -143,7 +166,10 @@ const GetAutomaticRes: FC = ({
return true
}
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false)
- const [res, setRes] = useState(null)
+ const storageKey = `${flowId}${isBasicMode ? '' : `-${nodeId}`}`
+ const { addVersion, current, currentVersionIndex, setCurrentVersionIndex, versions } = useGenData({
+ storageKey,
+ })
useEffect(() => {
if (defaultModel) {
@@ -170,16 +196,6 @@ const GetAutomaticRes: FC = ({
)
- const renderNoData = (
-
-
-
-
{t('appDebug.generate.noDataLine1')}
-
{t('appDebug.generate.noDataLine2')}
-
-
- )
-
const handleModelChange = useCallback((newValue: { modelId: string; provider: string; mode?: string; features?: string[] }) => {
const newModel = {
...model,
@@ -207,28 +223,59 @@ const GetAutomaticRes: FC = ({
return
setLoadingTrue()
try {
- const { error, ...res } = await generateRule({
- instruction,
- model_config: model,
- no_variable: !!isInLLMNode,
- })
- setRes(res)
- if (error) {
- Toast.notify({
- type: 'error',
- message: error,
+ let apiRes: GenRes
+ let hasError = false
+ if (isBasicMode || !currentPrompt) {
+ const { error, ...res } = await generateBasicAppFistTimeRule({
+ instruction,
+ model_config: model,
+ no_variable: false,
})
+ apiRes = {
+ ...res,
+ modified: res.prompt,
+ } as GenRes
+ if (error) {
+ hasError = true
+ Toast.notify({
+ type: 'error',
+ message: error,
+ })
+ }
}
+ else {
+ const { error, ...res } = await generateRule({
+ flow_id: flowId,
+ node_id: nodeId,
+ current: currentPrompt,
+ instruction,
+ ideal_output: ideaOutput,
+ model_config: model,
+ })
+ apiRes = res
+ if (error) {
+ hasError = true
+ Toast.notify({
+ type: 'error',
+ message: error,
+ })
+ }
+ }
+ if (!hasError)
+ addVersion(apiRes)
}
finally {
setLoadingFalse()
}
}
- const [showConfirmOverwrite, setShowConfirmOverwrite] = React.useState(false)
+ const [isShowConfirmOverwrite, {
+ setTrue: showConfirmOverwrite,
+ setFalse: hideShowConfirmOverwrite,
+ }] = useBoolean(false)
const isShowAutoPromptResPlaceholder = () => {
- return !isLoading && !res
+ return !isLoading && !current
}
return (
@@ -236,15 +283,14 @@ const GetAutomaticRes: FC = ({
isShow={isShow}
onClose={onClose}
className='min-w-[1140px] !p-0'
- closable
>
-
+
{t('appDebug.generate.title')}
{t('appDebug.generate.description')}
-
+
= ({
hideDebugWithMultipleModel
/>
-
-
-
{t('appDebug.generate.tryIt')}
-
-
-
- {tryList.map(item => (
-
- ))}
-
-
- {/* inputs */}
-
-
-
{t('appDebug.generate.instruction')}
-
)
- const renderNoData = (
-
-
-
-
{t('appDebug.codegen.noDataLine1')}
-
{t('appDebug.codegen.noDataLine2')}
-
-
- )
return (
-
-
-
{t('appDebug.codegen.title')}
+
+
+
{t('appDebug.codegen.title')}
{t('appDebug.codegen.description')}
-
+
= (
-
{t('appDebug.codegen.instruction')}
-
+
setInstruction(e.target.value)}
+ onChange={setInstruction}
+ nodeId={nodeId}
+ generatorType={GeneratorType.code}
+ isShowCurrentBlock={!!currentCode}
/>
+
-
+
+
{isLoading && renderLoading}
- {!isLoading && !res && renderNoData}
- {(!isLoading && res) && (
-
-
{t('appDebug.codegen.resTitle')}
-
-
- {!isInLLMNode && (
- <>
- {res?.code && (
-
-
{t('appDebug.codegen.generatedCode')}
-
-
- {res.code}
-
-
-
- )}
- {res?.error && (
-
- )}
- >
- )}
-
-
-
-
-
-
+ {!isLoading && !current &&
}
+ {(!isLoading && current) && (
+
+
)}
- {showConfirmOverwrite && (
+ {isShowConfirmOverwrite && (
{
- setShowConfirmOverwrite(false)
- onFinished(res!)
+ hideShowConfirmOverwrite()
+ onFinished(current!)
}}
- onCancel={() => setShowConfirmOverwrite(false)}
+ onCancel={hideShowConfirmOverwrite}
/>
)}
diff --git a/web/app/components/app/configuration/features/experience-enhance-group/index.tsx b/web/app/components/app/configuration/features/experience-enhance-group/index.tsx
deleted file mode 100644
index 4a629a6b0e..0000000000
--- a/web/app/components/app/configuration/features/experience-enhance-group/index.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-'use client'
-import type { FC } from 'react'
-import React from 'react'
-import { useTranslation } from 'react-i18next'
-import GroupName from '../../base/group-name'
-import TextToSpeech from '../chat-group/text-to-speech'
-import MoreLikeThis from './more-like-this'
-
-/*
-* Include
-* 1. More like this
-*/
-
-type ExperienceGroupProps = {
- isShowTextToSpeech: boolean
- isShowMoreLike: boolean
-}
-
-const ExperienceEnhanceGroup: FC = ({
- isShowTextToSpeech,
- isShowMoreLike,
-}) => {
- const { t } = useTranslation()
-
- return (
-
-
-
- {
- isShowMoreLike && (
-
- )
- }
- {
- isShowTextToSpeech && (
-
- )
- }
-
-
- )
-}
-export default React.memo(ExperienceEnhanceGroup)
diff --git a/web/app/components/app/configuration/features/experience-enhance-group/more-like-this/index.tsx b/web/app/components/app/configuration/features/experience-enhance-group/more-like-this/index.tsx
deleted file mode 100644
index e110d0c9cc..0000000000
--- a/web/app/components/app/configuration/features/experience-enhance-group/more-like-this/index.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-'use client'
-import type { FC } from 'react'
-import React from 'react'
-import { useTranslation } from 'react-i18next'
-import { XMarkIcon } from '@heroicons/react/24/outline'
-import { useLocalStorageState } from 'ahooks'
-import MoreLikeThisIcon from '../../../base/icons/more-like-this-icon'
-import Panel from '@/app/components/app/configuration/base/feature-panel'
-
-const GENERATE_NUM = 1
-
-const warningIcon = (
-
-
-)
-const MoreLikeThis: FC = () => {
- const { t } = useTranslation()
-
- const [isHideTip, setIsHideTip] = useLocalStorageState('isHideMoreLikeThisTip', {
- defaultValue: false,
- })
-
- const headerRight = (
- {t('appDebug.feature.moreLikeThis.generateNumTip')} {GENERATE_NUM}
- )
- return (
- }
- headerRight={headerRight}
- noBodySpacing
- >
- {!isHideTip && (
-
-
-
{warningIcon}
-
{t('appDebug.feature.moreLikeThis.tip')}
-
-
setIsHideTip(true)}>
-
-
-
- )}
-
-
- )
-}
-export default React.memo(MoreLikeThis)
diff --git a/web/app/components/app/log/list.tsx b/web/app/components/app/log/list.tsx
index b83e9e6a2a..2ea5bfb769 100644
--- a/web/app/components/app/log/list.tsx
+++ b/web/app/components/app/log/list.tsx
@@ -8,7 +8,6 @@ import {
} from '@heroicons/react/24/outline'
import { RiCloseLine, RiEditFill } from '@remixicon/react'
import { get } from 'lodash-es'
-import InfiniteScroll from 'react-infinite-scroll-component'
import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'
import timezone from 'dayjs/plugin/timezone'
@@ -111,7 +110,8 @@ const statusTdRender = (statusCount: StatusCount) => {
const getFormattedChatList = (messages: ChatMessage[], conversationId: string, timezone: string, format: string) => {
const newChatList: IChatItem[] = []
- messages.forEach((item: ChatMessage) => {
+ try {
+ messages.forEach((item: ChatMessage) => {
const questionFiles = item.message_files?.filter((file: any) => file.belongs_to === 'user') || []
newChatList.push({
id: `question-${item.id}`,
@@ -178,7 +178,13 @@ const getFormattedChatList = (messages: ChatMessage[], conversationId: string, t
parentMessageId: `question-${item.id}`,
})
})
- return newChatList
+
+ return newChatList
+ }
+ catch (error) {
+ console.error('getFormattedChatList processing failed:', error)
+ throw error
+ }
}
type IDetailPanel = {
@@ -188,6 +194,9 @@ type IDetailPanel = {
}
function DetailPanel({ detail, onFeedback }: IDetailPanel) {
+ const MIN_ITEMS_FOR_SCROLL_LOADING = 8
+ const SCROLL_THRESHOLD_PX = 50
+ const SCROLL_DEBOUNCE_MS = 200
const { userProfile: { timezone } } = useAppContext()
const { formatTime } = useTimestamp()
const { onClose, appDetail } = useContext(DrawerContext)
@@ -204,13 +213,19 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
const { t } = useTranslation()
const [hasMore, setHasMore] = useState(true)
const [varValues, setVarValues] = useState>({})
+ const isLoadingRef = useRef(false)
const [allChatItems, setAllChatItems] = useState([])
const [chatItemTree, setChatItemTree] = useState([])
const [threadChatItems, setThreadChatItems] = useState([])
const fetchData = useCallback(async () => {
+ if (isLoadingRef.current)
+ return
+
try {
+ isLoadingRef.current = true
+
if (!hasMore)
return
@@ -218,8 +233,11 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
conversation_id: detail.id,
limit: 10,
}
- if (allChatItems[0]?.id)
- params.first_id = allChatItems[0]?.id.replace('question-', '')
+ // Use the oldest answer item ID for pagination
+ const answerItems = allChatItems.filter(item => item.isAnswer)
+ const oldestAnswerItem = answerItems[answerItems.length - 1]
+ if (oldestAnswerItem?.id)
+ params.first_id = oldestAnswerItem.id
const messageRes = await fetchChatMessages({
url: `/apps/${appDetail?.id}/chat-messages`,
params,
@@ -249,15 +267,20 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
}
setChatItemTree(tree)
- setThreadChatItems(getThreadMessages(tree, newAllChatItems.at(-1)?.id))
+ const lastMessageId = newAllChatItems.length > 0 ? newAllChatItems[newAllChatItems.length - 1].id : undefined
+ setThreadChatItems(getThreadMessages(tree, lastMessageId))
}
catch (err) {
- console.error(err)
+ console.error('fetchData execution failed:', err)
+ }
+ finally {
+ isLoadingRef.current = false
}
}, [allChatItems, detail.id, hasMore, timezone, t, appDetail, detail?.model_config?.configs?.introduction])
const switchSibling = useCallback((siblingMessageId: string) => {
- setThreadChatItems(getThreadMessages(chatItemTree, siblingMessageId))
+ const newThreadChatItems = getThreadMessages(chatItemTree, siblingMessageId)
+ setThreadChatItems(newThreadChatItems)
}, [chatItemTree])
const handleAnnotationEdited = useCallback((query: string, answer: string, index: number) => {
@@ -344,13 +367,217 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
const fetchInitiated = useRef(false)
+ // Only load initial messages, don't auto-load more
useEffect(() => {
if (appDetail?.id && detail.id && appDetail?.mode !== 'completion' && !fetchInitiated.current) {
+ // Mark as initialized, but don't auto-load more messages
fetchInitiated.current = true
+ // Still call fetchData to get initial messages
fetchData()
}
}, [appDetail?.id, detail.id, appDetail?.mode, fetchData])
+ const [isLoading, setIsLoading] = useState(false)
+
+ const loadMoreMessages = useCallback(async () => {
+ if (isLoading || !hasMore || !appDetail?.id || !detail.id)
+ return
+
+ setIsLoading(true)
+
+ try {
+ const params: ChatMessagesRequest = {
+ conversation_id: detail.id,
+ limit: 10,
+ }
+
+ // Use the earliest response item as the first_id
+ const answerItems = allChatItems.filter(item => item.isAnswer)
+ const oldestAnswerItem = answerItems[answerItems.length - 1]
+ if (oldestAnswerItem?.id) {
+ params.first_id = oldestAnswerItem.id
+ }
+ else if (allChatItems.length > 0 && allChatItems[0]?.id) {
+ const firstId = allChatItems[0].id.replace('question-', '').replace('answer-', '')
+ params.first_id = firstId
+ }
+
+ const messageRes = await fetchChatMessages({
+ url: `/apps/${appDetail.id}/chat-messages`,
+ params,
+ })
+
+ if (!messageRes.data || messageRes.data.length === 0) {
+ setHasMore(false)
+ return
+ }
+
+ if (messageRes.data.length > 0) {
+ const varValues = messageRes.data.at(-1)!.inputs
+ setVarValues(varValues)
+ }
+
+ setHasMore(messageRes.has_more)
+
+ const newItems = getFormattedChatList(
+ messageRes.data,
+ detail.id,
+ timezone!,
+ t('appLog.dateTimeFormat') as string,
+ )
+
+ // Check for duplicate messages
+ const existingIds = new Set(allChatItems.map(item => item.id))
+ const uniqueNewItems = newItems.filter(item => !existingIds.has(item.id))
+
+ if (uniqueNewItems.length === 0) {
+ if (allChatItems.length > 1) {
+ const nextId = allChatItems[1].id.replace('question-', '').replace('answer-', '')
+
+ const retryParams = {
+ ...params,
+ first_id: nextId,
+ }
+
+ const retryRes = await fetchChatMessages({
+ url: `/apps/${appDetail.id}/chat-messages`,
+ params: retryParams,
+ })
+
+ if (retryRes.data && retryRes.data.length > 0) {
+ const retryItems = getFormattedChatList(
+ retryRes.data,
+ detail.id,
+ timezone!,
+ t('appLog.dateTimeFormat') as string,
+ )
+
+ const retryUniqueItems = retryItems.filter(item => !existingIds.has(item.id))
+ if (retryUniqueItems.length > 0) {
+ const newAllChatItems = [
+ ...retryUniqueItems,
+ ...allChatItems,
+ ]
+
+ setAllChatItems(newAllChatItems)
+
+ let tree = buildChatItemTree(newAllChatItems)
+ if (retryRes.has_more === false && detail?.model_config?.configs?.introduction) {
+ tree = [{
+ id: 'introduction',
+ isAnswer: true,
+ isOpeningStatement: true,
+ content: detail?.model_config?.configs?.introduction ?? 'hello',
+ feedbackDisabled: true,
+ children: tree,
+ }]
+ }
+ setChatItemTree(tree)
+ setHasMore(retryRes.has_more)
+ setThreadChatItems(getThreadMessages(tree, newAllChatItems.at(-1)?.id))
+ return
+ }
+ }
+ }
+ }
+
+ const newAllChatItems = [
+ ...uniqueNewItems,
+ ...allChatItems,
+ ]
+
+ setAllChatItems(newAllChatItems)
+
+ let tree = buildChatItemTree(newAllChatItems)
+ if (messageRes.has_more === false && detail?.model_config?.configs?.introduction) {
+ tree = [{
+ id: 'introduction',
+ isAnswer: true,
+ isOpeningStatement: true,
+ content: detail?.model_config?.configs?.introduction ?? 'hello',
+ feedbackDisabled: true,
+ children: tree,
+ }]
+ }
+ setChatItemTree(tree)
+
+ setThreadChatItems(getThreadMessages(tree, newAllChatItems.at(-1)?.id))
+ }
+ catch (error) {
+ console.error(error)
+ setHasMore(false)
+ }
+ finally {
+ setIsLoading(false)
+ }
+ }, [allChatItems, detail.id, hasMore, isLoading, timezone, t, appDetail])
+
+ useEffect(() => {
+ const scrollableDiv = document.getElementById('scrollableDiv')
+ const outerDiv = scrollableDiv?.parentElement
+ const chatContainer = document.querySelector('.mx-1.mb-1.grow.overflow-auto') as HTMLElement
+
+ let scrollContainer: HTMLElement | null = null
+
+ if (outerDiv && outerDiv.scrollHeight > outerDiv.clientHeight) {
+ scrollContainer = outerDiv
+ }
+ else if (scrollableDiv && scrollableDiv.scrollHeight > scrollableDiv.clientHeight) {
+ scrollContainer = scrollableDiv
+ }
+ else if (chatContainer && chatContainer.scrollHeight > chatContainer.clientHeight) {
+ scrollContainer = chatContainer
+ }
+ else {
+ const possibleContainers = document.querySelectorAll('.overflow-auto, .overflow-y-auto')
+ for (let i = 0; i < possibleContainers.length; i++) {
+ const container = possibleContainers[i] as HTMLElement
+ if (container.scrollHeight > container.clientHeight) {
+ scrollContainer = container
+ break
+ }
+ }
+ }
+
+ if (!scrollContainer)
+ return
+
+ let lastLoadTime = 0
+ const throttleDelay = 200
+
+ const handleScroll = () => {
+ const currentScrollTop = scrollContainer!.scrollTop
+ const scrollHeight = scrollContainer!.scrollHeight
+ const clientHeight = scrollContainer!.clientHeight
+
+ const distanceFromTop = currentScrollTop
+ const distanceFromBottom = scrollHeight - currentScrollTop - clientHeight
+
+ const now = Date.now()
+
+ const isNearTop = distanceFromTop < 30
+ // eslint-disable-next-line sonarjs/no-unused-vars
+ const _distanceFromBottom = distanceFromBottom < 30
+ if (isNearTop && hasMore && !isLoading && (now - lastLoadTime > throttleDelay)) {
+ lastLoadTime = now
+ loadMoreMessages()
+ }
+ }
+
+ scrollContainer.addEventListener('scroll', handleScroll, { passive: true })
+
+ const handleWheel = (e: WheelEvent) => {
+ if (e.deltaY < 0)
+ handleScroll()
+ }
+ scrollContainer.addEventListener('wheel', handleWheel, { passive: true })
+
+ return () => {
+ scrollContainer!.removeEventListener('scroll', handleScroll)
+ scrollContainer!.removeEventListener('wheel', handleWheel)
+ }
+ }, [hasMore, isLoading, loadMoreMessages])
+
const isChatMode = appDetail?.mode !== 'completion'
const isAdvanced = appDetail?.mode === 'advanced-chat'
@@ -378,6 +605,36 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
return () => cancelAnimationFrame(raf)
}, [])
+ // Add scroll listener to ensure loading is triggered
+ useEffect(() => {
+ if (threadChatItems.length >= MIN_ITEMS_FOR_SCROLL_LOADING && hasMore) {
+ const scrollableDiv = document.getElementById('scrollableDiv')
+
+ if (scrollableDiv) {
+ let loadingTimeout: NodeJS.Timeout | null = null
+
+ const handleScroll = () => {
+ const { scrollTop } = scrollableDiv
+
+ // Trigger loading when scrolling near the top
+ if (scrollTop < SCROLL_THRESHOLD_PX && !isLoadingRef.current) {
+ if (loadingTimeout)
+ clearTimeout(loadingTimeout)
+
+ loadingTimeout = setTimeout(fetchData, SCROLL_DEBOUNCE_MS) // 200ms debounce
+ }
+ }
+
+ scrollableDiv.addEventListener('scroll', handleScroll)
+ return () => {
+ scrollableDiv.removeEventListener('scroll', handleScroll)
+ if (loadingTimeout)
+ clearTimeout(loadingTimeout)
+ }
+ }
+ }
+ }, [threadChatItems.length, hasMore, fetchData])
+
return (
{/* Panel Header */}
@@ -439,8 +696,8 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
siteInfo={null}
/>
- : threadChatItems.length < 8
- ?
+ : threadChatItems.length < MIN_ITEMS_FOR_SCROLL_LOADING ? (
+
- :
{/* Put the scroll bar always on the bottom */}
- {t('appLog.detail.loading')}...
}
- // endMessage={
Nothing more to show
}
- // below props only if you need pull down functionality
- refreshFunction={fetchData}
- pullDownToRefresh
- pullDownToRefreshThreshold={50}
- // pullDownToRefreshContent={
- //
Pull down to refresh
- // }
- // releaseToRefreshContent={
- //
Release to refresh
- // }
- // To put endMessage and loader to the top.
- style={{ display: 'flex', flexDirection: 'column-reverse' }}
- inverse={true}
- >
+
+ {/* Loading state indicator - only shown when loading */}
+ {hasMore && isLoading && (
+
+
+ {t('appLog.detail.loading')}...
+
+
+ )}
+
-
+
+ )
}
{showMessageLogModal && (
diff --git a/web/app/components/base/chat/chat/loading-anim/index.tsx b/web/app/components/base/chat/chat/loading-anim/index.tsx
index dd43ef9c14..801c89fce7 100644
--- a/web/app/components/base/chat/chat/loading-anim/index.tsx
+++ b/web/app/components/base/chat/chat/loading-anim/index.tsx
@@ -2,6 +2,7 @@
import type { FC } from 'react'
import React from 'react'
import s from './style.module.css'
+import cn from '@/utils/classnames'
export type ILoadingAnimProps = {
type: 'text' | 'avatar'
@@ -11,7 +12,7 @@ const LoadingAnim: FC
= ({
type,
}) => {
return (
-
+
)
}
export default React.memo(LoadingAnim)
diff --git a/web/app/components/base/features/new-feature-panel/conversation-opener/modal.tsx b/web/app/components/base/features/new-feature-panel/conversation-opener/modal.tsx
index 51e33c43d2..53db991e71 100644
--- a/web/app/components/base/features/new-feature-panel/conversation-opener/modal.tsx
+++ b/web/app/components/base/features/new-feature-panel/conversation-opener/modal.tsx
@@ -8,6 +8,7 @@ import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import Divider from '@/app/components/base/divider'
import ConfirmAddVar from '@/app/components/app/configuration/config-prompt/confirm-add-var'
+import PromptEditor from '@/app/components/base/prompt-editor'
import type { OpeningStatement } from '@/app/components/base/features/types'
import { getInputKeys } from '@/app/components/base/block-input'
import type { PromptVariable } from '@/models/debug'
@@ -101,7 +102,7 @@ const OpeningSettingModal = ({
·
{tempSuggestedQuestions.length}/{MAX_QUESTION_NUM}
-
+
{t('appDebug.feature.conversationOpener.title')}
-
+
-
diff --git a/web/app/components/base/icons/assets/vender/line/general/code-assistant.svg b/web/app/components/base/icons/assets/vender/line/general/code-assistant.svg
new file mode 100644
index 0000000000..0051dfc271
--- /dev/null
+++ b/web/app/components/base/icons/assets/vender/line/general/code-assistant.svg
@@ -0,0 +1,6 @@
+
diff --git a/web/app/components/base/icons/assets/vender/line/general/magic-edit.svg b/web/app/components/base/icons/assets/vender/line/general/magic-edit.svg
new file mode 100644
index 0000000000..9bc82f4853
--- /dev/null
+++ b/web/app/components/base/icons/assets/vender/line/general/magic-edit.svg
@@ -0,0 +1,6 @@
+
diff --git a/web/app/components/base/icons/src/vender/line/general/CodeAssistant.json b/web/app/components/base/icons/src/vender/line/general/CodeAssistant.json
new file mode 100644
index 0000000000..03dca1bd11
--- /dev/null
+++ b/web/app/components/base/icons/src/vender/line/general/CodeAssistant.json
@@ -0,0 +1,53 @@
+{
+ "icon": {
+ "type": "element",
+ "isRootNode": true,
+ "name": "svg",
+ "attributes": {
+ "width": "24",
+ "height": "24",
+ "viewBox": "0 0 24 24",
+ "fill": "none",
+ "xmlns": "http://www.w3.org/2000/svg"
+ },
+ "children": [
+ {
+ "type": "element",
+ "name": "path",
+ "attributes": {
+ "d": "M12 3C12.5523 3 13 3.44772 13 4C13 4.55228 12.5523 5 12 5H5.59962C5.30336 5 5.14096 5.00122 5.02443 5.01074C5.01998 5.01111 5.01573 5.01135 5.01173 5.01172C5.01136 5.01572 5.01112 5.01996 5.01076 5.02441C5.00124 5.14095 5.00001 5.30334 5.00001 5.59961V18.4004C5.00001 18.6967 5.00124 18.8591 5.01076 18.9756C5.01109 18.9797 5.01139 18.9836 5.01173 18.9873C5.01575 18.9877 5.01995 18.9889 5.02443 18.9893C5.14096 18.9988 5.30336 19 5.59962 19H18.4004C18.6967 19 18.8591 18.9988 18.9756 18.9893C18.9797 18.9889 18.9836 18.9876 18.9873 18.9873C18.9877 18.9836 18.9889 18.9797 18.9893 18.9756C18.9988 18.8591 19 18.6967 19 18.4004V12C19 11.4477 19.4477 11 20 11C20.5523 11 21 11.4477 21 12V18.4004C21 18.6638 21.0011 18.9219 20.9834 19.1387C20.9647 19.3672 20.9205 19.6369 20.7822 19.9082C20.5905 20.2845 20.2845 20.5905 19.9082 20.7822C19.6369 20.9205 19.3672 20.9647 19.1387 20.9834C18.9219 21.0011 18.6638 21 18.4004 21H5.59962C5.33625 21 5.07815 21.0011 4.86134 20.9834C4.63284 20.9647 4.36307 20.9204 4.09181 20.7822C3.71579 20.5906 3.40963 20.2847 3.21779 19.9082C3.07958 19.6369 3.03531 19.3672 3.01662 19.1387C2.9989 18.9219 3.00001 18.6638 3.00001 18.4004V5.59961C3.00001 5.33623 2.9989 5.07814 3.01662 4.86133C3.03531 4.63283 3.07958 4.36305 3.21779 4.0918C3.40953 3.71555 3.71557 3.40952 4.09181 3.21777C4.36306 3.07957 4.63285 3.0353 4.86134 3.0166C5.07815 2.99889 5.33624 3 5.59962 3H12Z",
+ "fill": "currentColor"
+ },
+ "children": []
+ },
+ {
+ "type": "element",
+ "name": "path",
+ "attributes": {
+ "d": "M13.293 9.29297C13.6835 8.90244 14.3165 8.90244 14.707 9.29297L16.707 11.293C17.0975 11.6835 17.0975 12.3165 16.707 12.707L14.707 14.707C14.3165 15.0975 13.6835 15.0975 13.293 14.707C12.9025 14.3165 12.9025 13.6835 13.293 13.293L14.586 12L13.293 10.707C12.9025 10.3165 12.9025 9.68349 13.293 9.29297Z",
+ "fill": "currentColor"
+ },
+ "children": []
+ },
+ {
+ "type": "element",
+ "name": "path",
+ "attributes": {
+ "d": "M9.29298 9.29297C9.68351 8.90244 10.3165 8.90244 10.707 9.29297C11.0975 9.6835 11.0975 10.3165 10.707 10.707L9.41408 12L10.707 13.293L10.7754 13.3691C11.0957 13.7619 11.0731 14.3409 10.707 14.707C10.341 15.0731 9.76192 15.0957 9.36915 14.7754L9.29298 14.707L7.29298 12.707C6.90246 12.3165 6.90246 11.6835 7.29298 11.293L9.29298 9.29297Z",
+ "fill": "currentColor"
+ },
+ "children": []
+ },
+ {
+ "type": "element",
+ "name": "path",
+ "attributes": {
+ "d": "M18.501 2C18.7487 2.00048 18.9564 2.18654 18.9844 2.43262C19.1561 3.94883 20.0165 4.87694 21.5557 5.01367C21.8075 5.03614 22.0003 5.24816 22 5.50098C21.9995 5.75356 21.8063 5.96437 21.5547 5.98633C20.0372 6.11768 19.1176 7.03726 18.9863 8.55469C18.9644 8.80629 18.7536 8.99948 18.501 9C18.2483 9.00028 18.0362 8.80743 18.0137 8.55566C17.877 7.01647 16.9488 6.15613 15.4326 5.98438C15.1866 5.95634 15.0006 5.74863 15 5.50098C14.9997 5.25311 15.1855 5.04417 15.4317 5.01563C16.9697 4.83823 17.8382 3.96966 18.0156 2.43164C18.0442 2.18545 18.2531 1.99975 18.501 2Z",
+ "fill": "currentColor"
+ },
+ "children": []
+ }
+ ]
+ },
+ "name": "CodeAssistant"
+}
diff --git a/web/app/components/base/icons/src/vender/line/general/CodeAssistant.tsx b/web/app/components/base/icons/src/vender/line/general/CodeAssistant.tsx
new file mode 100644
index 0000000000..71adb145fb
--- /dev/null
+++ b/web/app/components/base/icons/src/vender/line/general/CodeAssistant.tsx
@@ -0,0 +1,20 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './CodeAssistant.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = (
+ {
+ ref,
+ ...props
+ }: React.SVGProps
& {
+ ref?: React.RefObject>;
+ },
+) =>
+
+Icon.displayName = 'CodeAssistant'
+
+export default Icon
diff --git a/web/app/components/base/icons/src/vender/line/general/MagicEdit.json b/web/app/components/base/icons/src/vender/line/general/MagicEdit.json
new file mode 100644
index 0000000000..3c2be09743
--- /dev/null
+++ b/web/app/components/base/icons/src/vender/line/general/MagicEdit.json
@@ -0,0 +1,55 @@
+{
+ "icon": {
+ "type": "element",
+ "isRootNode": true,
+ "name": "svg",
+ "attributes": {
+ "width": "24",
+ "height": "24",
+ "viewBox": "0 0 24 24",
+ "fill": "none",
+ "xmlns": "http://www.w3.org/2000/svg"
+ },
+ "children": [
+ {
+ "type": "element",
+ "name": "path",
+ "attributes": {
+ "fill-rule": "evenodd",
+ "clip-rule": "evenodd",
+ "d": "M15.3691 3.22448C15.7619 2.90422 16.3409 2.92682 16.707 3.29284L20.707 7.29284C21.0975 7.68334 21.0975 8.31637 20.707 8.7069L8.70703 20.7069C8.5195 20.8944 8.26521 20.9999 8 20.9999H4C3.44771 20.9999 3 20.5522 3 19.9999V15.9999L3.00488 15.9012C3.0276 15.6723 3.12886 15.4569 3.29297 15.2928L15.293 3.29284L15.3691 3.22448ZM5 16.4139V18.9999H7.58593L18.5859 7.99987L16 5.41394L5 16.4139Z",
+ "fill": "currentColor"
+ },
+ "children": []
+ },
+ {
+ "type": "element",
+ "name": "path",
+ "attributes": {
+ "d": "M18.2451 15.1581C18.3502 14.9478 18.6498 14.9478 18.7549 15.1581L19.4082 16.4637C19.4358 16.5189 19.4809 16.5641 19.5361 16.5917L20.8428 17.245C21.0525 17.3502 21.0524 17.6495 20.8428 17.7548L19.5361 18.4081C19.4809 18.4357 19.4358 18.4808 19.4082 18.536L18.7549 19.8426C18.6497 20.0525 18.3503 20.0525 18.2451 19.8426L17.5918 18.536C17.5642 18.4808 17.5191 18.4357 17.4639 18.4081L16.1572 17.7548C15.9476 17.6495 15.9475 17.3502 16.1572 17.245L17.4639 16.5917C17.5191 16.5641 17.5642 16.5189 17.5918 16.4637L18.2451 15.1581Z",
+ "fill": "currentColor"
+ },
+ "children": []
+ },
+ {
+ "type": "element",
+ "name": "path",
+ "attributes": {
+ "d": "M4.24511 5.15808C4.35024 4.94783 4.64975 4.94783 4.75488 5.15808L5.4082 6.46374C5.4358 6.51895 5.48092 6.56406 5.53613 6.59167L6.84277 7.24499C7.05248 7.3502 7.05238 7.64946 6.84277 7.75476L5.53613 8.40808C5.48092 8.43568 5.4358 8.4808 5.4082 8.53601L4.75488 9.84265C4.64966 10.0525 4.35033 10.0525 4.24511 9.84265L3.59179 8.53601C3.56418 8.4808 3.51907 8.43568 3.46386 8.40808L2.15722 7.75476C1.94764 7.64945 1.94755 7.35021 2.15722 7.24499L3.46386 6.59167C3.51907 6.56406 3.56418 6.51895 3.59179 6.46374L4.24511 5.15808Z",
+ "fill": "currentColor"
+ },
+ "children": []
+ },
+ {
+ "type": "element",
+ "name": "path",
+ "attributes": {
+ "d": "M8.73535 2.16394C8.84432 1.94601 9.15568 1.94601 9.26465 2.16394L9.74414 3.12292C9.77275 3.18014 9.81973 3.22712 9.87695 3.25573L10.8369 3.73522C11.0546 3.84424 11.0545 4.15544 10.8369 4.26452L9.87695 4.74401C9.81973 4.77262 9.77275 4.81961 9.74414 4.87683L9.26465 5.83679C9.15565 6.05458 8.84435 6.05457 8.73535 5.83679L8.25586 4.87683C8.22725 4.81961 8.18026 4.77262 8.12304 4.74401L7.16308 4.26452C6.94547 4.15546 6.94539 3.84422 7.16308 3.73522L8.12304 3.25573C8.18026 3.22712 8.22725 3.18014 8.25586 3.12292L8.73535 2.16394Z",
+ "fill": "currentColor"
+ },
+ "children": []
+ }
+ ]
+ },
+ "name": "MagicEdit"
+}
diff --git a/web/app/components/base/icons/src/vender/line/general/MagicEdit.tsx b/web/app/components/base/icons/src/vender/line/general/MagicEdit.tsx
new file mode 100644
index 0000000000..4e49c55277
--- /dev/null
+++ b/web/app/components/base/icons/src/vender/line/general/MagicEdit.tsx
@@ -0,0 +1,20 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './MagicEdit.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = (
+ {
+ ref,
+ ...props
+ }: React.SVGProps & {
+ ref?: React.RefObject>;
+ },
+) =>
+
+Icon.displayName = 'MagicEdit'
+
+export default Icon
diff --git a/web/app/components/base/icons/src/vender/line/general/index.ts b/web/app/components/base/icons/src/vender/line/general/index.ts
index 1b6c7e7303..c73d7cce05 100644
--- a/web/app/components/base/icons/src/vender/line/general/index.ts
+++ b/web/app/components/base/icons/src/vender/line/general/index.ts
@@ -3,6 +3,7 @@ export { default as Bookmark } from './Bookmark'
export { default as CheckDone01 } from './CheckDone01'
export { default as Check } from './Check'
export { default as ChecklistSquare } from './ChecklistSquare'
+export { default as CodeAssistant } from './CodeAssistant'
export { default as DotsGrid } from './DotsGrid'
export { default as Edit02 } from './Edit02'
export { default as Edit04 } from './Edit04'
@@ -14,6 +15,7 @@ export { default as LinkExternal02 } from './LinkExternal02'
export { default as LogIn04 } from './LogIn04'
export { default as LogOut01 } from './LogOut01'
export { default as LogOut04 } from './LogOut04'
+export { default as MagicEdit } from './MagicEdit'
export { default as Menu01 } from './Menu01'
export { default as Pin01 } from './Pin01'
export { default as Pin02 } from './Pin02'
diff --git a/web/app/components/base/prompt-editor/constants.tsx b/web/app/components/base/prompt-editor/constants.tsx
index 48e67a4ee7..54e2f1aa21 100644
--- a/web/app/components/base/prompt-editor/constants.tsx
+++ b/web/app/components/base/prompt-editor/constants.tsx
@@ -3,6 +3,10 @@ import { SupportUploadFileTypes, type ValueSelector } from '../../workflow/types
export const CONTEXT_PLACEHOLDER_TEXT = '{{#context#}}'
export const HISTORY_PLACEHOLDER_TEXT = '{{#histories#}}'
export const QUERY_PLACEHOLDER_TEXT = '{{#query#}}'
+export const CURRENT_PLACEHOLDER_TEXT = '{{#current#}}'
+export const ERROR_MESSAGE_PLACEHOLDER_TEXT = '{{#error_message#}}'
+export const LAST_RUN_PLACEHOLDER_TEXT = '{{#last_run#}}'
+
export const PRE_PROMPT_PLACEHOLDER_TEXT = '{{#pre_prompt#}}'
export const UPDATE_DATASETS_EVENT_EMITTER = 'prompt-editor-context-block-update-datasets'
export const UPDATE_HISTORY_EVENT_EMITTER = 'prompt-editor-history-block-update-role'
diff --git a/web/app/components/base/prompt-editor/index.tsx b/web/app/components/base/prompt-editor/index.tsx
index a87a51cd50..7f67abb597 100644
--- a/web/app/components/base/prompt-editor/index.tsx
+++ b/web/app/components/base/prompt-editor/index.tsx
@@ -1,7 +1,7 @@
'use client'
import type { FC } from 'react'
-import { useEffect } from 'react'
+import React, { useEffect } from 'react'
import type {
EditorState,
} from 'lexical'
@@ -39,6 +39,22 @@ import {
WorkflowVariableBlockNode,
WorkflowVariableBlockReplacementBlock,
} from './plugins/workflow-variable-block'
+import {
+ CurrentBlock,
+ CurrentBlockNode,
+ CurrentBlockReplacementBlock,
+} from './plugins/current-block'
+import {
+ ErrorMessageBlock,
+ ErrorMessageBlockNode,
+ ErrorMessageBlockReplacementBlock,
+} from './plugins/error-message-block'
+import {
+ LastRunBlock,
+ LastRunBlockNode,
+ LastRunReplacementBlock,
+} from './plugins/last-run-block'
+
import VariableBlock from './plugins/variable-block'
import VariableValueBlock from './plugins/variable-value-block'
import { VariableValueBlockNode } from './plugins/variable-value-block/node'
@@ -48,8 +64,11 @@ import UpdateBlock from './plugins/update-block'
import { textToEditorState } from './utils'
import type {
ContextBlockType,
+ CurrentBlockType,
+ ErrorMessageBlockType,
ExternalToolBlockType,
HistoryBlockType,
+ LastRunBlockType,
QueryBlockType,
VariableBlockType,
WorkflowVariableBlockType,
@@ -66,7 +85,7 @@ export type PromptEditorProps = {
compact?: boolean
wrapperClassName?: string
className?: string
- placeholder?: string | JSX.Element
+ placeholder?: string | React.JSX.Element
placeholderClassName?: string
style?: React.CSSProperties
value?: string
@@ -80,6 +99,9 @@ export type PromptEditorProps = {
variableBlock?: VariableBlockType
externalToolBlock?: ExternalToolBlockType
workflowVariableBlock?: WorkflowVariableBlockType
+ currentBlock?: CurrentBlockType
+ errorMessageBlock?: ErrorMessageBlockType
+ lastRunBlock?: LastRunBlockType
isSupportFileVar?: boolean
}
@@ -102,6 +124,9 @@ const PromptEditor: FC = ({
variableBlock,
externalToolBlock,
workflowVariableBlock,
+ currentBlock,
+ errorMessageBlock,
+ lastRunBlock,
isSupportFileVar,
}) => {
const { eventEmitter } = useEventEmitterContextContext()
@@ -119,6 +144,9 @@ const PromptEditor: FC = ({
QueryBlockNode,
WorkflowVariableBlockNode,
VariableValueBlockNode,
+ CurrentBlockNode,
+ ErrorMessageBlockNode,
+ LastRunBlockNode, // LastRunBlockNode is used for error message block replacement
],
editorState: textToEditorState(value || ''),
onError: (error: Error) => {
@@ -178,6 +206,9 @@ const PromptEditor: FC = ({
variableBlock={variableBlock}
externalToolBlock={externalToolBlock}
workflowVariableBlock={workflowVariableBlock}
+ currentBlock={currentBlock}
+ errorMessageBlock={errorMessageBlock}
+ lastRunBlock={lastRunBlock}
isSupportFileVar={isSupportFileVar}
/>
= ({
variableBlock={variableBlock}
externalToolBlock={externalToolBlock}
workflowVariableBlock={workflowVariableBlock}
+ currentBlock={currentBlock}
+ errorMessageBlock={errorMessageBlock}
+ lastRunBlock={lastRunBlock}
isSupportFileVar={isSupportFileVar}
/>
{
@@ -230,6 +264,35 @@ const PromptEditor: FC = ({
>
)
}
+ {
+ currentBlock?.show && (
+ <>
+
+
+ >
+ )
+ }
+ {
+ errorMessageBlock?.show && (
+ <>
+
+
+ >
+ )
+ }
+ {
+ lastRunBlock?.show && (
+ <>
+
+
+ >
+ )
+ }
+ {
+ isSupportFileVar && (
+
+ )
+ }
diff --git a/web/app/components/base/prompt-editor/plugins/component-picker-block/hooks.tsx b/web/app/components/base/prompt-editor/plugins/component-picker-block/hooks.tsx
index 7332a0d39b..2032f22ce9 100644
--- a/web/app/components/base/prompt-editor/plugins/component-picker-block/hooks.tsx
+++ b/web/app/components/base/prompt-editor/plugins/component-picker-block/hooks.tsx
@@ -4,8 +4,11 @@ import { $insertNodes } from 'lexical'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import type {
ContextBlockType,
+ CurrentBlockType,
+ ErrorMessageBlockType,
ExternalToolBlockType,
HistoryBlockType,
+ LastRunBlockType,
QueryBlockType,
VariableBlockType,
WorkflowVariableBlockType,
@@ -27,6 +30,7 @@ import { BracketsX } from '@/app/components/base/icons/src/vender/line/developme
import { UserEdit02 } from '@/app/components/base/icons/src/vender/solid/users'
import { ArrowUpRight } from '@/app/components/base/icons/src/vender/line/arrows'
import AppIcon from '@/app/components/base/app-icon'
+import { VarType } from '@/app/components/workflow/types'
export const usePromptOptions = (
contextBlock?: ContextBlockType,
@@ -267,17 +271,61 @@ export const useOptions = (
variableBlock?: VariableBlockType,
externalToolBlockType?: ExternalToolBlockType,
workflowVariableBlockType?: WorkflowVariableBlockType,
+ currentBlockType?: CurrentBlockType,
+ errorMessageBlockType?: ErrorMessageBlockType,
+ lastRunBlockType?: LastRunBlockType,
queryString?: string,
) => {
const promptOptions = usePromptOptions(contextBlock, queryBlock, historyBlock)
const variableOptions = useVariableOptions(variableBlock, queryString)
const externalToolOptions = useExternalToolOptions(externalToolBlockType, queryString)
+
const workflowVariableOptions = useMemo(() => {
if (!workflowVariableBlockType?.show)
return []
-
- return workflowVariableBlockType.variables || []
- }, [workflowVariableBlockType])
+ const res = workflowVariableBlockType.variables || []
+ if(errorMessageBlockType?.show && res.findIndex(v => v.nodeId === 'error_message') === -1) {
+ res.unshift({
+ nodeId: 'error_message',
+ title: 'error_message',
+ isFlat: true,
+ vars: [
+ {
+ variable: 'error_message',
+ type: VarType.string,
+ },
+ ],
+ })
+ }
+ if(lastRunBlockType?.show && res.findIndex(v => v.nodeId === 'last_run') === -1) {
+ res.unshift({
+ nodeId: 'last_run',
+ title: 'last_run',
+ isFlat: true,
+ vars: [
+ {
+ variable: 'last_run',
+ type: VarType.object,
+ },
+ ],
+ })
+ }
+ if(currentBlockType?.show && res.findIndex(v => v.nodeId === 'current') === -1) {
+ const title = currentBlockType.generatorType === 'prompt' ? 'current_prompt' : 'current_code'
+ res.unshift({
+ nodeId: 'current',
+ title,
+ isFlat: true,
+ vars: [
+ {
+ variable: 'current',
+ type: VarType.string,
+ },
+ ],
+ })
+ }
+ return res
+ }, [workflowVariableBlockType?.show, workflowVariableBlockType?.variables, errorMessageBlockType?.show, lastRunBlockType?.show, currentBlockType?.show, currentBlockType?.generatorType])
return useMemo(() => {
return {
diff --git a/web/app/components/base/prompt-editor/plugins/component-picker-block/index.tsx b/web/app/components/base/prompt-editor/plugins/component-picker-block/index.tsx
index bffcdc60d2..cffb2762c2 100644
--- a/web/app/components/base/prompt-editor/plugins/component-picker-block/index.tsx
+++ b/web/app/components/base/prompt-editor/plugins/component-picker-block/index.tsx
@@ -17,8 +17,11 @@ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext
import { LexicalTypeaheadMenuPlugin } from '@lexical/react/LexicalTypeaheadMenuPlugin'
import type {
ContextBlockType,
+ CurrentBlockType,
+ ErrorMessageBlockType,
ExternalToolBlockType,
HistoryBlockType,
+ LastRunBlockType,
QueryBlockType,
VariableBlockType,
WorkflowVariableBlockType,
@@ -32,6 +35,10 @@ import type { PickerBlockMenuOption } from './menu'
import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import { KEY_ESCAPE_COMMAND } from 'lexical'
+import { INSERT_CURRENT_BLOCK_COMMAND } from '../current-block'
+import { GeneratorType } from '@/app/components/app/configuration/config/automatic/types'
+import { INSERT_ERROR_MESSAGE_BLOCK_COMMAND } from '../error-message-block'
+import { INSERT_LAST_RUN_BLOCK_COMMAND } from '../last-run-block'
type ComponentPickerProps = {
triggerString: string
@@ -41,6 +48,9 @@ type ComponentPickerProps = {
variableBlock?: VariableBlockType
externalToolBlock?: ExternalToolBlockType
workflowVariableBlock?: WorkflowVariableBlockType
+ currentBlock?: CurrentBlockType
+ errorMessageBlock?: ErrorMessageBlockType
+ lastRunBlock?: LastRunBlockType
isSupportFileVar?: boolean
}
const ComponentPicker = ({
@@ -51,6 +61,9 @@ const ComponentPicker = ({
variableBlock,
externalToolBlock,
workflowVariableBlock,
+ currentBlock,
+ errorMessageBlock,
+ lastRunBlock,
isSupportFileVar,
}: ComponentPickerProps) => {
const { eventEmitter } = useEventEmitterContextContext()
@@ -87,6 +100,9 @@ const ComponentPicker = ({
variableBlock,
externalToolBlock,
workflowVariableBlock,
+ currentBlock,
+ errorMessageBlock,
+ lastRunBlock,
)
const onSelectOption = useCallback(
@@ -112,12 +128,23 @@ const ComponentPicker = ({
if (needRemove)
needRemove.remove()
})
-
- if (variables[1] === 'sys.query' || variables[1] === 'sys.files')
+ const isFlat = variables.length === 1
+ if(isFlat) {
+ const varName = variables[0]
+ if(varName === 'current')
+ editor.dispatchCommand(INSERT_CURRENT_BLOCK_COMMAND, currentBlock?.generatorType)
+ else if (varName === 'error_message')
+ editor.dispatchCommand(INSERT_ERROR_MESSAGE_BLOCK_COMMAND, null)
+ else if (varName === 'last_run')
+ editor.dispatchCommand(INSERT_LAST_RUN_BLOCK_COMMAND, null)
+ }
+ else if (variables[1] === 'sys.query' || variables[1] === 'sys.files') {
editor.dispatchCommand(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, [variables[1]])
- else
+ }
+ else {
editor.dispatchCommand(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, variables)
- }, [editor, checkForTriggerMatch, triggerString])
+ }
+ }, [editor, currentBlock?.generatorType, checkForTriggerMatch, triggerString])
const handleClose = useCallback(() => {
const escapeEvent = new KeyboardEvent('keydown', { key: 'Escape' })
@@ -166,6 +193,7 @@ const ComponentPicker = ({
onClose={handleClose}
onBlur={handleClose}
autoFocus={false}
+ isInCodeGeneratorInstructionEditor={currentBlock?.generatorType === GeneratorType.code}
/>
)
@@ -206,7 +234,7 @@ const ComponentPicker = ({
}
>
)
- }, [allFlattenOptions.length, workflowVariableBlock?.show, refs, isPositioned, floatingStyles, queryString, workflowVariableOptions, handleSelectWorkflowVariable, handleClose, isSupportFileVar])
+ }, [allFlattenOptions.length, workflowVariableBlock?.show, floatingStyles, isPositioned, refs, workflowVariableOptions, isSupportFileVar, handleClose, currentBlock?.generatorType, handleSelectWorkflowVariable, queryString])
return (
= ({
+ nodeKey,
+ generatorType,
+}) => {
+ const [editor] = useLexicalComposerContext()
+ const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_CURRENT_BLOCK_COMMAND)
+
+ const Icon = generatorType === GeneratorType.prompt ? MagicEdit : CodeAssistant
+ useEffect(() => {
+ if (!editor.hasNodes([CurrentBlockNode]))
+ throw new Error('WorkflowVariableBlockPlugin: WorkflowVariableBlock not registered on editor')
+ }, [editor])
+
+ return (
+ {
+ e.stopPropagation()
+ }}
+ ref={ref}
+ >
+
+
{generatorType === GeneratorType.prompt ? 'current_prompt' : 'current_code'}
+
+ )
+}
+
+export default CurrentBlockComponent
diff --git a/web/app/components/base/prompt-editor/plugins/current-block/current-block-replacement-block.tsx b/web/app/components/base/prompt-editor/plugins/current-block/current-block-replacement-block.tsx
new file mode 100644
index 0000000000..aa56360365
--- /dev/null
+++ b/web/app/components/base/prompt-editor/plugins/current-block/current-block-replacement-block.tsx
@@ -0,0 +1,61 @@
+import {
+ memo,
+ useCallback,
+ useEffect,
+} from 'react'
+import { $applyNodeReplacement } from 'lexical'
+import { mergeRegister } from '@lexical/utils'
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
+import { decoratorTransform } from '../../utils'
+import { CURRENT_PLACEHOLDER_TEXT } from '../../constants'
+import type { CurrentBlockType } from '../../types'
+import {
+ $createCurrentBlockNode,
+ CurrentBlockNode,
+} from './node'
+import { CustomTextNode } from '../custom-text/node'
+
+const REGEX = new RegExp(CURRENT_PLACEHOLDER_TEXT)
+
+const CurrentBlockReplacementBlock = ({
+ generatorType,
+ onInsert,
+}: CurrentBlockType) => {
+ const [editor] = useLexicalComposerContext()
+
+ useEffect(() => {
+ if (!editor.hasNodes([CurrentBlockNode]))
+ throw new Error('CurrentBlockNodePlugin: CurrentBlockNode not registered on editor')
+ }, [editor])
+
+ const createCurrentBlockNode = useCallback((): CurrentBlockNode => {
+ if (onInsert)
+ onInsert()
+ return $applyNodeReplacement($createCurrentBlockNode(generatorType))
+ }, [onInsert, generatorType])
+
+ const getMatch = useCallback((text: string) => {
+ const matchArr = REGEX.exec(text)
+
+ if (matchArr === null)
+ return null
+
+ const startOffset = matchArr.index
+ const endOffset = startOffset + CURRENT_PLACEHOLDER_TEXT.length
+ return {
+ end: endOffset,
+ start: startOffset,
+ }
+ }, [])
+
+ useEffect(() => {
+ REGEX.lastIndex = 0
+ return mergeRegister(
+ editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createCurrentBlockNode)),
+ )
+ }, [])
+
+ return null
+}
+
+export default memo(CurrentBlockReplacementBlock)
diff --git a/web/app/components/base/prompt-editor/plugins/current-block/index.tsx b/web/app/components/base/prompt-editor/plugins/current-block/index.tsx
new file mode 100644
index 0000000000..0b9ed6c5a4
--- /dev/null
+++ b/web/app/components/base/prompt-editor/plugins/current-block/index.tsx
@@ -0,0 +1,66 @@
+import {
+ memo,
+ useEffect,
+} from 'react'
+import {
+ $insertNodes,
+ COMMAND_PRIORITY_EDITOR,
+ createCommand,
+} from 'lexical'
+import { mergeRegister } from '@lexical/utils'
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
+import type { CurrentBlockType } from '../../types'
+import {
+ $createCurrentBlockNode,
+ CurrentBlockNode,
+} from './node'
+
+export const INSERT_CURRENT_BLOCK_COMMAND = createCommand('INSERT_CURRENT_BLOCK_COMMAND')
+export const DELETE_CURRENT_BLOCK_COMMAND = createCommand('DELETE_CURRENT_BLOCK_COMMAND')
+
+const CurrentBlock = memo(({
+ generatorType,
+ onInsert,
+ onDelete,
+}: CurrentBlockType) => {
+ const [editor] = useLexicalComposerContext()
+
+ useEffect(() => {
+ if (!editor.hasNodes([CurrentBlockNode]))
+ throw new Error('CURRENTBlockPlugin: CURRENTBlock not registered on editor')
+
+ return mergeRegister(
+ editor.registerCommand(
+ INSERT_CURRENT_BLOCK_COMMAND,
+ () => {
+ const currentBlockNode = $createCurrentBlockNode(generatorType)
+
+ $insertNodes([currentBlockNode])
+
+ if (onInsert)
+ onInsert()
+
+ return true
+ },
+ COMMAND_PRIORITY_EDITOR,
+ ),
+ editor.registerCommand(
+ DELETE_CURRENT_BLOCK_COMMAND,
+ () => {
+ if (onDelete)
+ onDelete()
+
+ return true
+ },
+ COMMAND_PRIORITY_EDITOR,
+ ),
+ )
+ }, [editor, generatorType, onDelete, onInsert])
+
+ return null
+})
+CurrentBlock.displayName = 'CurrentBlock'
+
+export { CurrentBlock }
+export { CurrentBlockNode } from './node'
+export { default as CurrentBlockReplacementBlock } from './current-block-replacement-block'
diff --git a/web/app/components/base/prompt-editor/plugins/current-block/node.tsx b/web/app/components/base/prompt-editor/plugins/current-block/node.tsx
new file mode 100644
index 0000000000..eb0239f613
--- /dev/null
+++ b/web/app/components/base/prompt-editor/plugins/current-block/node.tsx
@@ -0,0 +1,78 @@
+import type { LexicalNode, NodeKey, SerializedLexicalNode } from 'lexical'
+import { DecoratorNode } from 'lexical'
+import CurrentBlockComponent from './component'
+import type { GeneratorType } from '@/app/components/app/configuration/config/automatic/types'
+
+export type SerializedNode = SerializedLexicalNode & { generatorType: GeneratorType; }
+
+export class CurrentBlockNode extends DecoratorNode {
+ __generatorType: GeneratorType
+ static getType(): string {
+ return 'current-block'
+ }
+
+ static clone(node: CurrentBlockNode): CurrentBlockNode {
+ return new CurrentBlockNode(node.__generatorType, node.getKey())
+ }
+
+ isInline(): boolean {
+ return true
+ }
+
+ constructor(generatorType: GeneratorType, key?: NodeKey) {
+ super(key)
+
+ this.__generatorType = generatorType
+ }
+
+ createDOM(): HTMLElement {
+ const div = document.createElement('div')
+ div.classList.add('inline-flex', 'items-center', 'align-middle')
+ return div
+ }
+
+ updateDOM(): false {
+ return false
+ }
+
+ decorate(): React.JSX.Element {
+ return (
+
+ )
+ }
+
+ getGeneratorType(): GeneratorType {
+ const self = this.getLatest()
+ return self.__generatorType
+ }
+
+ static importJSON(serializedNode: SerializedNode): CurrentBlockNode {
+ const node = $createCurrentBlockNode(serializedNode.generatorType)
+
+ return node
+ }
+
+ exportJSON(): SerializedNode {
+ return {
+ type: 'current-block',
+ version: 1,
+ generatorType: this.getGeneratorType(),
+ }
+ }
+
+ getTextContent(): string {
+ return '{{#current#}}'
+ }
+}
+export function $createCurrentBlockNode(type: GeneratorType): CurrentBlockNode {
+ return new CurrentBlockNode(type)
+}
+
+export function $isCurrentBlockNode(
+ node: CurrentBlockNode | LexicalNode | null | undefined,
+): boolean {
+ return node instanceof CurrentBlockNode
+}
diff --git a/web/app/components/base/prompt-editor/plugins/error-message-block/component.tsx b/web/app/components/base/prompt-editor/plugins/error-message-block/component.tsx
new file mode 100644
index 0000000000..e5d113f680
--- /dev/null
+++ b/web/app/components/base/prompt-editor/plugins/error-message-block/component.tsx
@@ -0,0 +1,40 @@
+import { type FC, useEffect } from 'react'
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
+import { useSelectOrDelete } from '../../hooks'
+import { DELETE_ERROR_MESSAGE_COMMAND, ErrorMessageBlockNode } from '.'
+import cn from '@/utils/classnames'
+import { Variable02 } from '../../../icons/src/vender/solid/development'
+
+type Props = {
+ nodeKey: string
+}
+
+const ErrorMessageBlockComponent: FC = ({
+ nodeKey,
+}) => {
+ const [editor] = useLexicalComposerContext()
+ const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_ERROR_MESSAGE_COMMAND)
+
+ useEffect(() => {
+ if (!editor.hasNodes([ErrorMessageBlockNode]))
+ throw new Error('WorkflowVariableBlockPlugin: WorkflowVariableBlock not registered on editor')
+ }, [editor])
+
+ return (
+ {
+ e.stopPropagation()
+ }}
+ ref={ref}
+ >
+
+
error_message
+
+ )
+}
+
+export default ErrorMessageBlockComponent
diff --git a/web/app/components/base/prompt-editor/plugins/error-message-block/error-message-block-replacement-block.tsx b/web/app/components/base/prompt-editor/plugins/error-message-block/error-message-block-replacement-block.tsx
new file mode 100644
index 0000000000..cd8df107ff
--- /dev/null
+++ b/web/app/components/base/prompt-editor/plugins/error-message-block/error-message-block-replacement-block.tsx
@@ -0,0 +1,60 @@
+import {
+ memo,
+ useCallback,
+ useEffect,
+} from 'react'
+import { $applyNodeReplacement } from 'lexical'
+import { mergeRegister } from '@lexical/utils'
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
+import { decoratorTransform } from '../../utils'
+import { ERROR_MESSAGE_PLACEHOLDER_TEXT } from '../../constants'
+import type { ErrorMessageBlockType } from '../../types'
+import {
+ $createErrorMessageBlockNode,
+ ErrorMessageBlockNode,
+} from './node'
+import { CustomTextNode } from '../custom-text/node'
+
+const REGEX = new RegExp(ERROR_MESSAGE_PLACEHOLDER_TEXT)
+
+const ErrorMessageBlockReplacementBlock = ({
+ onInsert,
+}: ErrorMessageBlockType) => {
+ const [editor] = useLexicalComposerContext()
+
+ useEffect(() => {
+ if (!editor.hasNodes([ErrorMessageBlockNode]))
+ throw new Error('ErrorMessageBlockNodePlugin: ErrorMessageBlockNode not registered on editor')
+ }, [editor])
+
+ const createErrorMessageBlockNode = useCallback((): ErrorMessageBlockNode => {
+ if (onInsert)
+ onInsert()
+ return $applyNodeReplacement($createErrorMessageBlockNode())
+ }, [onInsert])
+
+ const getMatch = useCallback((text: string) => {
+ const matchArr = REGEX.exec(text)
+
+ if (matchArr === null)
+ return null
+
+ const startOffset = matchArr.index
+ const endOffset = startOffset + ERROR_MESSAGE_PLACEHOLDER_TEXT.length
+ return {
+ end: endOffset,
+ start: startOffset,
+ }
+ }, [])
+
+ useEffect(() => {
+ REGEX.lastIndex = 0
+ return mergeRegister(
+ editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createErrorMessageBlockNode)),
+ )
+ }, [])
+
+ return null
+}
+
+export default memo(ErrorMessageBlockReplacementBlock)
diff --git a/web/app/components/base/prompt-editor/plugins/error-message-block/index.tsx b/web/app/components/base/prompt-editor/plugins/error-message-block/index.tsx
new file mode 100644
index 0000000000..25bb55c3f0
--- /dev/null
+++ b/web/app/components/base/prompt-editor/plugins/error-message-block/index.tsx
@@ -0,0 +1,65 @@
+import {
+ memo,
+ useEffect,
+} from 'react'
+import {
+ $insertNodes,
+ COMMAND_PRIORITY_EDITOR,
+ createCommand,
+} from 'lexical'
+import { mergeRegister } from '@lexical/utils'
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
+import type { ErrorMessageBlockType } from '../../types'
+import {
+ $createErrorMessageBlockNode,
+ ErrorMessageBlockNode,
+} from './node'
+
+export const INSERT_ERROR_MESSAGE_BLOCK_COMMAND = createCommand('INSERT_ERROR_MESSAGE_BLOCK_COMMAND')
+export const DELETE_ERROR_MESSAGE_COMMAND = createCommand('DELETE_ERROR_MESSAGE_COMMAND')
+
+const ErrorMessageBlock = memo(({
+ onInsert,
+ onDelete,
+}: ErrorMessageBlockType) => {
+ const [editor] = useLexicalComposerContext()
+
+ useEffect(() => {
+ if (!editor.hasNodes([ErrorMessageBlockNode]))
+ throw new Error('ERROR_MESSAGEBlockPlugin: ERROR_MESSAGEBlock not registered on editor')
+
+ return mergeRegister(
+ editor.registerCommand(
+ INSERT_ERROR_MESSAGE_BLOCK_COMMAND,
+ () => {
+ const Node = $createErrorMessageBlockNode()
+
+ $insertNodes([Node])
+
+ if (onInsert)
+ onInsert()
+
+ return true
+ },
+ COMMAND_PRIORITY_EDITOR,
+ ),
+ editor.registerCommand(
+ DELETE_ERROR_MESSAGE_COMMAND,
+ () => {
+ if (onDelete)
+ onDelete()
+
+ return true
+ },
+ COMMAND_PRIORITY_EDITOR,
+ ),
+ )
+ }, [editor, onDelete, onInsert])
+
+ return null
+})
+ErrorMessageBlock.displayName = 'ErrorMessageBlock'
+
+export { ErrorMessageBlock }
+export { ErrorMessageBlockNode } from './node'
+export { default as ErrorMessageBlockReplacementBlock } from './error-message-block-replacement-block'
diff --git a/web/app/components/base/prompt-editor/plugins/error-message-block/node.tsx b/web/app/components/base/prompt-editor/plugins/error-message-block/node.tsx
new file mode 100644
index 0000000000..b8042e5e54
--- /dev/null
+++ b/web/app/components/base/prompt-editor/plugins/error-message-block/node.tsx
@@ -0,0 +1,67 @@
+import type { LexicalNode, NodeKey, SerializedLexicalNode } from 'lexical'
+import { DecoratorNode } from 'lexical'
+import ErrorMessageBlockComponent from './component'
+
+export type SerializedNode = SerializedLexicalNode
+
+export class ErrorMessageBlockNode extends DecoratorNode {
+ static getType(): string {
+ return 'error-message-block'
+ }
+
+ static clone(node: ErrorMessageBlockNode): ErrorMessageBlockNode {
+ return new ErrorMessageBlockNode(node.getKey())
+ }
+
+ isInline(): boolean {
+ return true
+ }
+
+ constructor(key?: NodeKey) {
+ super(key)
+ }
+
+ createDOM(): HTMLElement {
+ const div = document.createElement('div')
+ div.classList.add('inline-flex', 'items-center', 'align-middle')
+ return div
+ }
+
+ updateDOM(): false {
+ return false
+ }
+
+ decorate(): React.JSX.Element {
+ return (
+
+ )
+ }
+
+ static importJSON(): ErrorMessageBlockNode {
+ const node = $createErrorMessageBlockNode()
+
+ return node
+ }
+
+ exportJSON(): SerializedNode {
+ return {
+ type: 'error-message-block',
+ version: 1,
+ }
+ }
+
+ getTextContent(): string {
+ return '{{#error_message#}}'
+ }
+}
+export function $createErrorMessageBlockNode(): ErrorMessageBlockNode {
+ return new ErrorMessageBlockNode()
+}
+
+export function $isErrorMessageBlockNode(
+ node: ErrorMessageBlockNode | LexicalNode | null | undefined,
+): boolean {
+ return node instanceof ErrorMessageBlockNode
+}
diff --git a/web/app/components/base/prompt-editor/plugins/last-run-block/component.tsx b/web/app/components/base/prompt-editor/plugins/last-run-block/component.tsx
new file mode 100644
index 0000000000..748a66e5a8
--- /dev/null
+++ b/web/app/components/base/prompt-editor/plugins/last-run-block/component.tsx
@@ -0,0 +1,40 @@
+import { type FC, useEffect } from 'react'
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
+import { useSelectOrDelete } from '../../hooks'
+import { DELETE_LAST_RUN_COMMAND, LastRunBlockNode } from '.'
+import cn from '@/utils/classnames'
+import { Variable02 } from '../../../icons/src/vender/solid/development'
+
+type Props = {
+ nodeKey: string
+}
+
+const LastRunBlockComponent: FC = ({
+ nodeKey,
+}) => {
+ const [editor] = useLexicalComposerContext()
+ const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_LAST_RUN_COMMAND)
+
+ useEffect(() => {
+ if (!editor.hasNodes([LastRunBlockNode]))
+ throw new Error('WorkflowVariableBlockPlugin: WorkflowVariableBlock not registered on editor')
+ }, [editor])
+
+ return (
+ {
+ e.stopPropagation()
+ }}
+ ref={ref}
+ >
+
+
last_run
+
+ )
+}
+
+export default LastRunBlockComponent
diff --git a/web/app/components/base/prompt-editor/plugins/last-run-block/index.tsx b/web/app/components/base/prompt-editor/plugins/last-run-block/index.tsx
new file mode 100644
index 0000000000..8bbbb8d4dd
--- /dev/null
+++ b/web/app/components/base/prompt-editor/plugins/last-run-block/index.tsx
@@ -0,0 +1,65 @@
+import {
+ memo,
+ useEffect,
+} from 'react'
+import {
+ $insertNodes,
+ COMMAND_PRIORITY_EDITOR,
+ createCommand,
+} from 'lexical'
+import { mergeRegister } from '@lexical/utils'
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
+import type { LastRunBlockType } from '../../types'
+import {
+ $createLastRunBlockNode,
+ LastRunBlockNode,
+} from './node'
+
+export const INSERT_LAST_RUN_BLOCK_COMMAND = createCommand('INSERT_LAST_RUN_BLOCK_COMMAND')
+export const DELETE_LAST_RUN_COMMAND = createCommand('DELETE_LAST_RUN_COMMAND')
+
+const LastRunBlock = memo(({
+ onInsert,
+ onDelete,
+}: LastRunBlockType) => {
+ const [editor] = useLexicalComposerContext()
+
+ useEffect(() => {
+ if (!editor.hasNodes([LastRunBlockNode]))
+ throw new Error('Last_RunBlockPlugin: Last_RunBlock not registered on editor')
+
+ return mergeRegister(
+ editor.registerCommand(
+ INSERT_LAST_RUN_BLOCK_COMMAND,
+ () => {
+ const Node = $createLastRunBlockNode()
+
+ $insertNodes([Node])
+
+ if (onInsert)
+ onInsert()
+
+ return true
+ },
+ COMMAND_PRIORITY_EDITOR,
+ ),
+ editor.registerCommand(
+ DELETE_LAST_RUN_COMMAND,
+ () => {
+ if (onDelete)
+ onDelete()
+
+ return true
+ },
+ COMMAND_PRIORITY_EDITOR,
+ ),
+ )
+ }, [editor, onDelete, onInsert])
+
+ return null
+})
+LastRunBlock.displayName = 'LastRunBlock'
+
+export { LastRunBlock }
+export { LastRunBlockNode } from './node'
+export { default as LastRunReplacementBlock } from './last-run-block-replacement-block'
diff --git a/web/app/components/base/prompt-editor/plugins/last-run-block/last-run-block-replacement-block.tsx b/web/app/components/base/prompt-editor/plugins/last-run-block/last-run-block-replacement-block.tsx
new file mode 100644
index 0000000000..2e5f92e2a1
--- /dev/null
+++ b/web/app/components/base/prompt-editor/plugins/last-run-block/last-run-block-replacement-block.tsx
@@ -0,0 +1,60 @@
+import {
+ memo,
+ useCallback,
+ useEffect,
+} from 'react'
+import { $applyNodeReplacement } from 'lexical'
+import { mergeRegister } from '@lexical/utils'
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
+import { decoratorTransform } from '../../utils'
+import { LAST_RUN_PLACEHOLDER_TEXT } from '../../constants'
+import type { LastRunBlockType } from '../../types'
+import {
+ $createLastRunBlockNode,
+ LastRunBlockNode,
+} from './node'
+import { CustomTextNode } from '../custom-text/node'
+
+const REGEX = new RegExp(LAST_RUN_PLACEHOLDER_TEXT)
+
+const LastRunReplacementBlock = ({
+ onInsert,
+}: LastRunBlockType) => {
+ const [editor] = useLexicalComposerContext()
+
+ useEffect(() => {
+ if (!editor.hasNodes([LastRunBlockNode]))
+ throw new Error('LastRunMessageBlockNodePlugin: LastRunMessageBlockNode not registered on editor')
+ }, [editor])
+
+ const createLastRunBlockNode = useCallback((): LastRunBlockNode => {
+ if (onInsert)
+ onInsert()
+ return $applyNodeReplacement($createLastRunBlockNode())
+ }, [onInsert])
+
+ const getMatch = useCallback((text: string) => {
+ const matchArr = REGEX.exec(text)
+
+ if (matchArr === null)
+ return null
+
+ const startOffset = matchArr.index
+ const endOffset = startOffset + LAST_RUN_PLACEHOLDER_TEXT.length
+ return {
+ end: endOffset,
+ start: startOffset,
+ }
+ }, [])
+
+ useEffect(() => {
+ REGEX.lastIndex = 0
+ return mergeRegister(
+ editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createLastRunBlockNode)),
+ )
+ }, [])
+
+ return null
+}
+
+export default memo(LastRunReplacementBlock)
diff --git a/web/app/components/base/prompt-editor/plugins/last-run-block/node.tsx b/web/app/components/base/prompt-editor/plugins/last-run-block/node.tsx
new file mode 100644
index 0000000000..5f61c3138b
--- /dev/null
+++ b/web/app/components/base/prompt-editor/plugins/last-run-block/node.tsx
@@ -0,0 +1,67 @@
+import type { LexicalNode, NodeKey, SerializedLexicalNode } from 'lexical'
+import { DecoratorNode } from 'lexical'
+import LastRunBlockComponent from './component'
+
+export type SerializedNode = SerializedLexicalNode
+
+export class LastRunBlockNode extends DecoratorNode {
+ static getType(): string {
+ return 'last-run-block'
+ }
+
+ static clone(node: LastRunBlockNode): LastRunBlockNode {
+ return new LastRunBlockNode(node.getKey())
+ }
+
+ isInline(): boolean {
+ return true
+ }
+
+ constructor(key?: NodeKey) {
+ super(key)
+ }
+
+ createDOM(): HTMLElement {
+ const div = document.createElement('div')
+ div.classList.add('inline-flex', 'items-center', 'align-middle')
+ return div
+ }
+
+ updateDOM(): false {
+ return false
+ }
+
+ decorate(): React.JSX.Element {
+ return (
+
+ )
+ }
+
+ static importJSON(): LastRunBlockNode {
+ const node = $createLastRunBlockNode()
+
+ return node
+ }
+
+ exportJSON(): SerializedNode {
+ return {
+ type: 'last-run-block',
+ version: 1,
+ }
+ }
+
+ getTextContent(): string {
+ return '{{#last_run#}}'
+ }
+}
+export function $createLastRunBlockNode(): LastRunBlockNode {
+ return new LastRunBlockNode()
+}
+
+export function $isLastRunBlockNode(
+ node: LastRunBlockNode | LexicalNode | null | undefined,
+): boolean {
+ return node instanceof LastRunBlockNode
+}
diff --git a/web/app/components/base/prompt-editor/types.ts b/web/app/components/base/prompt-editor/types.ts
index e82ec1da16..45ae2e2af7 100644
--- a/web/app/components/base/prompt-editor/types.ts
+++ b/web/app/components/base/prompt-editor/types.ts
@@ -1,3 +1,4 @@
+import type { GeneratorType } from '../../app/configuration/config/automatic/types'
import type { Type } from '../../workflow/nodes/llm/types'
import type { Dataset } from './plugins/context-block'
import type { RoleName } from './plugins/history-block'
@@ -75,3 +76,22 @@ export type MenuTextMatch = {
matchingString: string
replaceableString: string
}
+
+export type CurrentBlockType = {
+ show?: boolean
+ generatorType: GeneratorType
+ onInsert?: () => void
+ onDelete?: () => void
+}
+
+export type ErrorMessageBlockType = {
+ show?: boolean
+ onInsert?: () => void
+ onDelete?: () => void
+}
+
+export type LastRunBlockType = {
+ show?: boolean
+ onInsert?: () => void
+ onDelete?: () => void
+}
diff --git a/web/app/components/datasets/documents/detail/completed/common/tag.tsx b/web/app/components/datasets/documents/detail/completed/common/tag.tsx
index 306087d2e3..914f8dc61c 100644
--- a/web/app/components/datasets/documents/detail/completed/common/tag.tsx
+++ b/web/app/components/datasets/documents/detail/completed/common/tag.tsx
@@ -5,7 +5,7 @@ const Tag = ({ text, className }: { text: string; className?: string }) => {
return (
#
- {text}
+ {text}
)
}
diff --git a/web/app/components/datasets/documents/list.tsx b/web/app/components/datasets/documents/list.tsx
index 83effd446c..94010d32e4 100644
--- a/web/app/components/datasets/documents/list.tsx
+++ b/web/app/components/datasets/documents/list.tsx
@@ -301,6 +301,7 @@ export const OperationAction: FC<{