Compare commits

..

11 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
f5528f2030 Initial plan 2025-11-18 16:22:22 +00:00
-LAN-
6efdc94661 refactor: consume events after pause/abort and improve API clarity (#28328)
Some checks are pending
autofix.ci / autofix (push) Waiting to run
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/amd64, build-api-amd64) (push) Waiting to run
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/arm64, build-api-arm64) (push) Waiting to run
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/amd64, build-web-amd64) (push) Waiting to run
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/arm64, build-web-arm64) (push) Waiting to run
Build and Push API & Web / create-manifest (api, DIFY_API_IMAGE_NAME, merge-api-images) (push) Blocked by required conditions
Build and Push API & Web / create-manifest (web, DIFY_WEB_IMAGE_NAME, merge-web-images) (push) Blocked by required conditions
Main CI Pipeline / Check Changed Files (push) Waiting to run
Main CI Pipeline / API Tests (push) Blocked by required conditions
Main CI Pipeline / Web Tests (push) Blocked by required conditions
Main CI Pipeline / Style Check (push) Waiting to run
Main CI Pipeline / VDB Tests (push) Blocked by required conditions
Main CI Pipeline / DB Migration Test (push) Blocked by required conditions
Co-authored-by: QuantumGhost <obelisk.reg+git@gmail.com>
2025-11-18 19:04:11 +08:00
github-actions[bot]
68526c09fc chore: translate i18n files and update type definitions (#28284)
Co-authored-by: zhsama <33454514+zhsama@users.noreply.github.com>
Co-authored-by: lyzno1 <92089059+lyzno1@users.noreply.github.com>
2025-11-18 18:52:36 +08:00
kenwoodjw
a78bc507c0 fix: dataset metadata counts when documents are deleted (#28305)
Signed-off-by: kenwoodjw <blackxin55+@gmail.com>
2025-11-18 17:36:07 +08:00
Joel
e83c7438cb doc: add doc for env config when site and backend are in different domains (#28318)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-18 17:29:54 +08:00
Jyong
82068a6918 add vdb-test workflow run filter (#28336) 2025-11-18 17:22:15 +08:00
Asuka Minato
108bcbeb7c add cnt script and one more example (#28272)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-11-18 16:44:14 +09:00
非法操作
c4b02be6d3 fix: published webhook can't receive inputs (#28205)
Some checks are pending
autofix.ci / autofix (push) Waiting to run
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/amd64, build-api-amd64) (push) Waiting to run
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/arm64, build-api-arm64) (push) Waiting to run
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/amd64, build-web-amd64) (push) Waiting to run
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/arm64, build-web-arm64) (push) Waiting to run
Build and Push API & Web / create-manifest (api, DIFY_API_IMAGE_NAME, merge-api-images) (push) Blocked by required conditions
Build and Push API & Web / create-manifest (web, DIFY_WEB_IMAGE_NAME, merge-web-images) (push) Blocked by required conditions
Main CI Pipeline / Check Changed Files (push) Waiting to run
Main CI Pipeline / API Tests (push) Blocked by required conditions
Main CI Pipeline / Web Tests (push) Blocked by required conditions
Main CI Pipeline / Style Check (push) Waiting to run
Main CI Pipeline / VDB Tests (push) Blocked by required conditions
Main CI Pipeline / DB Migration Test (push) Blocked by required conditions
2025-11-18 11:14:26 +08:00
lyzno1
30eebf804f chore: remove unused style.module.css from app-icon component (#28302) 2025-11-18 10:36:39 +08:00
Yessenia-d
ad7fdd18d0 fix: update currentTriggerPlugin check in BasePanel component (#28287)
Some checks failed
autofix.ci / autofix (push) Waiting to run
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/amd64, build-api-amd64) (push) Waiting to run
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/arm64, build-api-arm64) (push) Waiting to run
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/amd64, build-web-amd64) (push) Waiting to run
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/arm64, build-web-arm64) (push) Waiting to run
Build and Push API & Web / create-manifest (api, DIFY_API_IMAGE_NAME, merge-api-images) (push) Blocked by required conditions
Build and Push API & Web / create-manifest (web, DIFY_WEB_IMAGE_NAME, merge-web-images) (push) Blocked by required conditions
Main CI Pipeline / Check Changed Files (push) Waiting to run
Main CI Pipeline / API Tests (push) Blocked by required conditions
Main CI Pipeline / Web Tests (push) Blocked by required conditions
Main CI Pipeline / Style Check (push) Waiting to run
Main CI Pipeline / VDB Tests (push) Blocked by required conditions
Main CI Pipeline / DB Migration Test (push) Blocked by required conditions
Check i18n Files and Create PR / check-and-update (push) Has been cancelled
2025-11-17 17:19:35 +08:00
zhsama
5d2fbf5215 Perf/mutual node UI (#28282) 2025-11-17 16:23:04 +08:00
166 changed files with 1253 additions and 3315 deletions

View File

@@ -28,6 +28,11 @@ jobs:
# Format code
uv run ruff format ..
- name: count migration progress
run: |
cd api
./cnt_base.sh
- name: ast-grep
run: |
uvx --from ast-grep-cli sg --pattern 'db.session.query($WHATEVER).filter($HERE)' --rewrite 'db.session.query($WHATEVER).where($HERE)' -l py --update-all

View File

@@ -1,7 +1,10 @@
name: Run VDB Tests
on:
workflow_call:
push:
branches: [main]
paths:
- 'api/core/rag/*.py'
concurrency:
group: vdb-tests-${{ github.head_ref || github.run_id }}

View File

@@ -159,8 +159,7 @@ SUPABASE_URL=your-server-url
# CORS configuration
WEB_API_CORS_ALLOW_ORIGINS=http://localhost:3000,*
CONSOLE_CORS_ALLOW_ORIGINS=http://localhost:3000,*
# Set COOKIE_DOMAIN when the console frontend and API are on different subdomains.
# Provide the registrable domain (e.g. example.com); leading dots are optional.
# When the frontend and backend run on different subdomains, set COOKIE_DOMAIN to the sites top-level domain (e.g., `example.com`). Leading dots are optional.
COOKIE_DOMAIN=
# Vector database configuration

View File

@@ -26,6 +26,10 @@
cp .env.example .env
```
> [!IMPORTANT]
>
> When the frontend and backend run on different subdomains, set COOKIE_DOMAIN to the sites top-level domain (e.g., `example.com`). The frontend and backend must be under the same top-level domain in order to share authentication cookies.
1. Generate a `SECRET_KEY` in the `.env` file.
bash for Linux

7
api/cnt_base.sh Executable file
View File

@@ -0,0 +1,7 @@
#!/bin/bash
set -euxo pipefail
for pattern in "Base" "TypeBase"; do
printf "%s " "$pattern"
grep "($pattern):" -r --include='*.py' --exclude-dir=".venv" --exclude-dir="tests" . | wc -l
done

View File

@@ -192,7 +192,6 @@ class GraphEngine:
self._dispatcher = Dispatcher(
event_queue=self._event_queue,
event_handler=self._event_handler_registry,
event_collector=self._event_manager,
execution_coordinator=self._execution_coordinator,
event_emitter=self._event_manager,
)

View File

@@ -43,7 +43,6 @@ class Dispatcher:
self,
event_queue: queue.Queue[GraphNodeEventBase],
event_handler: "EventHandler",
event_collector: EventManager,
execution_coordinator: ExecutionCoordinator,
event_emitter: EventManager | None = None,
) -> None:
@@ -53,13 +52,11 @@ class Dispatcher:
Args:
event_queue: Queue of events from workers
event_handler: Event handler registry for processing events
event_collector: Event manager for collecting unhandled events
execution_coordinator: Coordinator for execution flow
event_emitter: Optional event manager to signal completion
"""
self._event_queue = event_queue
self._event_handler = event_handler
self._event_collector = event_collector
self._execution_coordinator = execution_coordinator
self._event_emitter = event_emitter
@@ -86,37 +83,31 @@ class Dispatcher:
def _dispatcher_loop(self) -> None:
"""Main dispatcher loop."""
try:
self._process_commands()
while not self._stop_event.is_set():
commands_checked = False
should_check_commands = False
should_break = False
if (
self._execution_coordinator.aborted
or self._execution_coordinator.paused
or self._execution_coordinator.execution_complete
):
break
if self._execution_coordinator.is_execution_complete():
should_check_commands = True
should_break = True
else:
# Check for scaling
self._execution_coordinator.check_scaling()
self._execution_coordinator.check_scaling()
try:
event = self._event_queue.get(timeout=0.1)
self._event_handler.dispatch(event)
self._event_queue.task_done()
self._process_commands(event)
except queue.Empty:
time.sleep(0.1)
# Process events
try:
event = self._event_queue.get(timeout=0.1)
# Route to the event handler
self._event_handler.dispatch(event)
should_check_commands = self._should_check_commands(event)
self._event_queue.task_done()
except queue.Empty:
# Process commands even when no new events arrive so abort requests are not missed
should_check_commands = True
time.sleep(0.1)
if should_check_commands and not commands_checked:
self._execution_coordinator.check_commands()
commands_checked = True
if should_break:
if not commands_checked:
self._execution_coordinator.check_commands()
self._process_commands()
while True:
try:
event = self._event_queue.get(block=False)
self._event_handler.dispatch(event)
self._event_queue.task_done()
except queue.Empty:
break
except Exception as e:
@@ -129,6 +120,6 @@ class Dispatcher:
if self._event_emitter:
self._event_emitter.mark_complete()
def _should_check_commands(self, event: GraphNodeEventBase) -> bool:
"""Return True if the event represents a node completion."""
return isinstance(event, self._COMMAND_TRIGGER_EVENTS)
def _process_commands(self, event: GraphNodeEventBase | None = None):
if event is None or isinstance(event, self._COMMAND_TRIGGER_EVENTS):
self._execution_coordinator.process_commands()

View File

@@ -40,7 +40,7 @@ class ExecutionCoordinator:
self._command_processor = command_processor
self._worker_pool = worker_pool
def check_commands(self) -> None:
def process_commands(self) -> None:
"""Process any pending commands."""
self._command_processor.process_commands()
@@ -48,24 +48,16 @@ class ExecutionCoordinator:
"""Check and perform worker scaling if needed."""
self._worker_pool.check_and_scale()
def is_execution_complete(self) -> bool:
"""
Check if execution is complete.
Returns:
True if execution is complete
"""
# Treat paused, aborted, or failed executions as terminal states
if self._graph_execution.is_paused:
return True
if self._graph_execution.aborted or self._graph_execution.has_error:
return True
@property
def execution_complete(self):
return self._state_manager.is_execution_complete()
@property
def is_paused(self) -> bool:
def aborted(self):
return self._graph_execution.aborted or self._graph_execution.has_error
@property
def paused(self) -> bool:
"""Expose whether the underlying graph execution is paused."""
return self._graph_execution.is_paused

View File

@@ -225,7 +225,7 @@ class Dataset(Base):
ExternalKnowledgeApis.id == external_knowledge_binding.external_knowledge_api_id
)
)
if not external_knowledge_api:
if external_knowledge_api is None or external_knowledge_api.settings is None:
return None
return {
"external_knowledge_id": external_knowledge_binding.external_knowledge_id,
@@ -945,18 +945,20 @@ class DatasetQuery(Base):
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=sa.func.current_timestamp())
class DatasetKeywordTable(Base):
class DatasetKeywordTable(TypeBase):
__tablename__ = "dataset_keyword_tables"
__table_args__ = (
sa.PrimaryKeyConstraint("id", name="dataset_keyword_table_pkey"),
sa.Index("dataset_keyword_table_dataset_id_idx", "dataset_id"),
)
id = mapped_column(StringUUID, primary_key=True, server_default=sa.text("uuid_generate_v4()"))
dataset_id = mapped_column(StringUUID, nullable=False, unique=True)
keyword_table = mapped_column(sa.Text, nullable=False)
data_source_type = mapped_column(
String(255), nullable=False, server_default=sa.text("'database'::character varying")
id: Mapped[str] = mapped_column(
StringUUID, primary_key=True, server_default=sa.text("uuid_generate_v4()"), init=False
)
dataset_id: Mapped[str] = mapped_column(StringUUID, nullable=False, unique=True)
keyword_table: Mapped[str] = mapped_column(sa.Text, nullable=False)
data_source_type: Mapped[str] = mapped_column(
String(255), nullable=False, server_default=sa.text("'database'::character varying"), default="database"
)
@property
@@ -1054,19 +1056,23 @@ class TidbAuthBinding(Base):
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
class Whitelist(Base):
class Whitelist(TypeBase):
__tablename__ = "whitelists"
__table_args__ = (
sa.PrimaryKeyConstraint("id", name="whitelists_pkey"),
sa.Index("whitelists_tenant_idx", "tenant_id"),
)
id = mapped_column(StringUUID, primary_key=True, server_default=sa.text("uuid_generate_v4()"))
tenant_id = mapped_column(StringUUID, nullable=True)
id: Mapped[str] = mapped_column(
StringUUID, primary_key=True, server_default=sa.text("uuid_generate_v4()"), init=False
)
tenant_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
category: Mapped[str] = mapped_column(String(255), nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
created_at: Mapped[datetime] = mapped_column(
DateTime, nullable=False, server_default=func.current_timestamp(), init=False
)
class DatasetPermission(Base):
class DatasetPermission(TypeBase):
__tablename__ = "dataset_permissions"
__table_args__ = (
sa.PrimaryKeyConstraint("id", name="dataset_permission_pkey"),
@@ -1075,15 +1081,21 @@ class DatasetPermission(Base):
sa.Index("idx_dataset_permissions_tenant_id", "tenant_id"),
)
id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()"), primary_key=True)
dataset_id = mapped_column(StringUUID, nullable=False)
account_id = mapped_column(StringUUID, nullable=False)
tenant_id = mapped_column(StringUUID, nullable=False)
has_permission: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=sa.text("true"))
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
id: Mapped[str] = mapped_column(
StringUUID, server_default=sa.text("uuid_generate_v4()"), primary_key=True, init=False
)
dataset_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
account_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
has_permission: Mapped[bool] = mapped_column(
sa.Boolean, nullable=False, server_default=sa.text("true"), default=True
)
created_at: Mapped[datetime] = mapped_column(
DateTime, nullable=False, server_default=func.current_timestamp(), init=False
)
class ExternalKnowledgeApis(Base):
class ExternalKnowledgeApis(TypeBase):
__tablename__ = "external_knowledge_apis"
__table_args__ = (
sa.PrimaryKeyConstraint("id", name="external_knowledge_apis_pkey"),
@@ -1091,16 +1103,20 @@ class ExternalKnowledgeApis(Base):
sa.Index("external_knowledge_apis_name_idx", "name"),
)
id = mapped_column(StringUUID, nullable=False, server_default=sa.text("uuid_generate_v4()"))
id: Mapped[str] = mapped_column(
StringUUID, nullable=False, server_default=sa.text("uuid_generate_v4()"), init=False
)
name: Mapped[str] = mapped_column(String(255), nullable=False)
description: Mapped[str] = mapped_column(String(255), nullable=False)
tenant_id = mapped_column(StringUUID, nullable=False)
settings = mapped_column(sa.Text, nullable=True)
created_by = mapped_column(StringUUID, nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
updated_by = mapped_column(StringUUID, nullable=True)
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
settings: Mapped[str | None] = mapped_column(sa.Text, nullable=True)
created_by: Mapped[str] = mapped_column(StringUUID, nullable=False)
created_at: Mapped[datetime] = mapped_column(
DateTime, nullable=False, server_default=func.current_timestamp(), init=False
)
updated_by: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
updated_at: Mapped[datetime] = mapped_column(
DateTime, nullable=False, server_default=func.current_timestamp(), onupdate=func.current_timestamp()
DateTime, nullable=False, server_default=func.current_timestamp(), onupdate=func.current_timestamp(), init=False
)
def to_dict(self) -> dict[str, Any]:
@@ -1178,7 +1194,7 @@ class DatasetAutoDisableLog(Base):
)
class RateLimitLog(Base):
class RateLimitLog(TypeBase):
__tablename__ = "rate_limit_logs"
__table_args__ = (
sa.PrimaryKeyConstraint("id", name="rate_limit_log_pkey"),
@@ -1186,12 +1202,12 @@ class RateLimitLog(Base):
sa.Index("rate_limit_log_operation_idx", "operation"),
)
id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()"))
tenant_id = mapped_column(StringUUID, nullable=False)
id: Mapped[str] = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()"), init=False)
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
subscription_plan: Mapped[str] = mapped_column(String(255), nullable=False)
operation: Mapped[str] = mapped_column(String(255), nullable=False)
created_at: Mapped[datetime] = mapped_column(
DateTime, nullable=False, server_default=sa.text("CURRENT_TIMESTAMP(0)")
DateTime, nullable=False, server_default=sa.text("CURRENT_TIMESTAMP(0)"), init=False
)

View File

@@ -14,7 +14,7 @@ from core.trigger.entities.api_entities import TriggerProviderSubscriptionApiEnt
from core.trigger.entities.entities import Subscription
from core.trigger.utils.endpoint import generate_plugin_trigger_endpoint_url, generate_webhook_trigger_endpoint
from libs.datetime_utils import naive_utc_now
from models.base import Base
from models.base import Base, TypeBase
from models.engine import db
from models.enums import AppTriggerStatus, AppTriggerType, CreatorUserRole, WorkflowTriggerStatus
from models.model import Account
@@ -399,7 +399,7 @@ class AppTrigger(Base):
)
class WorkflowSchedulePlan(Base):
class WorkflowSchedulePlan(TypeBase):
"""
Workflow Schedule Configuration
@@ -425,7 +425,7 @@ class WorkflowSchedulePlan(Base):
sa.Index("workflow_schedule_plan_next_idx", "next_run_at"),
)
id: Mapped[str] = mapped_column(StringUUID, server_default=sa.text("uuidv7()"))
id: Mapped[str] = mapped_column(StringUUID, primary_key=True, server_default=sa.text("uuidv7()"), init=False)
app_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
node_id: Mapped[str] = mapped_column(String(64), nullable=False)
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
@@ -436,9 +436,11 @@ class WorkflowSchedulePlan(Base):
# Schedule control
next_run_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
created_at: Mapped[datetime] = mapped_column(
DateTime, nullable=False, server_default=func.current_timestamp(), init=False
)
updated_at: Mapped[datetime] = mapped_column(
DateTime, nullable=False, server_default=func.current_timestamp(), onupdate=func.current_timestamp()
DateTime, nullable=False, server_default=func.current_timestamp(), onupdate=func.current_timestamp(), init=False
)
def to_dict(self) -> dict[str, Any]:

View File

@@ -62,7 +62,7 @@ class ExternalDatasetService:
tenant_id=tenant_id,
created_by=user_id,
updated_by=user_id,
name=args.get("name"),
name=str(args.get("name")),
description=args.get("description", ""),
settings=json.dumps(args.get("settings"), ensure_ascii=False),
)
@@ -163,7 +163,7 @@ class ExternalDatasetService:
external_knowledge_api = (
db.session.query(ExternalKnowledgeApis).filter_by(id=external_knowledge_api_id, tenant_id=tenant_id).first()
)
if external_knowledge_api is None:
if external_knowledge_api is None or external_knowledge_api.settings is None:
raise ValueError("api template not found")
settings = json.loads(external_knowledge_api.settings)
for setting in settings:
@@ -290,7 +290,7 @@ class ExternalDatasetService:
.filter_by(id=external_knowledge_binding.external_knowledge_api_id)
.first()
)
if not external_knowledge_api:
if external_knowledge_api is None or external_knowledge_api.settings is None:
raise ValueError("external api template not found")
settings = json.loads(external_knowledge_api.settings)

View File

@@ -13,13 +13,13 @@ from sqlalchemy import select
from sqlalchemy.orm import Session, sessionmaker
from configs import dify_config
from core.app.apps.workflow.app_generator import WorkflowAppGenerator
from core.app.apps.workflow.app_generator import SKIP_PREPARE_USER_INPUTS_KEY, WorkflowAppGenerator
from core.app.entities.app_invoke_entities import InvokeFrom
from core.app.layers.timeslice_layer import TimeSliceLayer
from core.app.layers.trigger_post_layer import TriggerPostLayer
from extensions.ext_database import db
from models.account import Account
from models.enums import CreatorUserRole, WorkflowTriggerStatus
from models.enums import AppTriggerType, CreatorUserRole, WorkflowTriggerStatus
from models.model import App, EndUser, Tenant
from models.trigger import WorkflowTriggerLog
from models.workflow import Workflow
@@ -81,6 +81,19 @@ def execute_workflow_sandbox(task_data_dict: dict[str, Any]):
)
def _build_generator_args(trigger_data: TriggerData) -> dict[str, Any]:
"""Build args passed into WorkflowAppGenerator.generate for Celery executions."""
args: dict[str, Any] = {
"inputs": dict(trigger_data.inputs),
"files": list(trigger_data.files),
}
if trigger_data.trigger_type == AppTriggerType.TRIGGER_WEBHOOK:
args[SKIP_PREPARE_USER_INPUTS_KEY] = True # Webhooks already provide structured inputs
return args
def _execute_workflow_common(
task_data: WorkflowTaskData,
cfs_plan_scheduler: AsyncWorkflowCFSPlanScheduler,
@@ -128,7 +141,7 @@ def _execute_workflow_common(
generator = WorkflowAppGenerator()
# Prepare args matching AppGenerateService.generate format
args: dict[str, Any] = {"inputs": dict(trigger_data.inputs), "files": list(trigger_data.files)}
args = _build_generator_args(trigger_data)
# If workflow_id was specified, add it to args
if trigger_data.workflow_id:

View File

@@ -9,7 +9,7 @@ from core.rag.index_processor.index_processor_factory import IndexProcessorFacto
from core.tools.utils.web_reader_tool import get_image_upload_file_ids
from extensions.ext_database import db
from extensions.ext_storage import storage
from models.dataset import Dataset, DocumentSegment
from models.dataset import Dataset, DatasetMetadataBinding, DocumentSegment
from models.model import UploadFile
logger = logging.getLogger(__name__)
@@ -37,6 +37,11 @@ def batch_clean_document_task(document_ids: list[str], dataset_id: str, doc_form
if not dataset:
raise Exception("Document has no dataset")
db.session.query(DatasetMetadataBinding).where(
DatasetMetadataBinding.dataset_id == dataset_id,
DatasetMetadataBinding.document_id.in_(document_ids),
).delete(synchronize_session=False)
segments = db.session.scalars(
select(DocumentSegment).where(DocumentSegment.document_id.in_(document_ids))
).all()
@@ -71,7 +76,8 @@ def batch_clean_document_task(document_ids: list[str], dataset_id: str, doc_form
except Exception:
logger.exception("Delete file failed when document deleted, file_id: %s", file.id)
db.session.delete(file)
db.session.commit()
db.session.commit()
end_at = time.perf_counter()
logger.info(

View File

@@ -0,0 +1,189 @@
"""Tests for dispatcher command checking behavior."""
from __future__ import annotations
import queue
from datetime import datetime
from unittest import mock
from core.workflow.entities.pause_reason import SchedulingPause
from core.workflow.enums import NodeType, WorkflowNodeExecutionStatus
from core.workflow.graph_engine.event_management.event_handlers import EventHandler
from core.workflow.graph_engine.orchestration.dispatcher import Dispatcher
from core.workflow.graph_engine.orchestration.execution_coordinator import ExecutionCoordinator
from core.workflow.graph_events import (
GraphNodeEventBase,
NodeRunPauseRequestedEvent,
NodeRunStartedEvent,
NodeRunSucceededEvent,
)
from core.workflow.node_events import NodeRunResult
def test_dispatcher_should_consume_remains_events_after_pause():
event_queue = queue.Queue()
event_queue.put(
GraphNodeEventBase(
id="test",
node_id="test",
node_type=NodeType.START,
)
)
event_handler = mock.Mock(spec=EventHandler)
execution_coordinator = mock.Mock(spec=ExecutionCoordinator)
execution_coordinator.paused.return_value = True
dispatcher = Dispatcher(
event_queue=event_queue,
event_handler=event_handler,
execution_coordinator=execution_coordinator,
)
dispatcher._dispatcher_loop()
assert event_queue.empty()
class _StubExecutionCoordinator:
"""Stub execution coordinator that tracks command checks."""
def __init__(self) -> None:
self.command_checks = 0
self.scaling_checks = 0
self.execution_complete = False
self.failed = False
self._paused = False
def process_commands(self) -> None:
self.command_checks += 1
def check_scaling(self) -> None:
self.scaling_checks += 1
@property
def paused(self) -> bool:
return self._paused
@property
def aborted(self) -> bool:
return False
def mark_complete(self) -> None:
self.execution_complete = True
def mark_failed(self, error: Exception) -> None: # pragma: no cover - defensive, not triggered in tests
self.failed = True
class _StubEventHandler:
"""Minimal event handler that marks execution complete after handling an event."""
def __init__(self, coordinator: _StubExecutionCoordinator) -> None:
self._coordinator = coordinator
self.events = []
def dispatch(self, event) -> None:
self.events.append(event)
self._coordinator.mark_complete()
def _run_dispatcher_for_event(event) -> int:
"""Run the dispatcher loop for a single event and return command check count."""
event_queue: queue.Queue = queue.Queue()
event_queue.put(event)
coordinator = _StubExecutionCoordinator()
event_handler = _StubEventHandler(coordinator)
dispatcher = Dispatcher(
event_queue=event_queue,
event_handler=event_handler,
execution_coordinator=coordinator,
)
dispatcher._dispatcher_loop()
return coordinator.command_checks
def _make_started_event() -> NodeRunStartedEvent:
return NodeRunStartedEvent(
id="start-event",
node_id="node-1",
node_type=NodeType.CODE,
node_title="Test Node",
start_at=datetime.utcnow(),
)
def _make_succeeded_event() -> NodeRunSucceededEvent:
return NodeRunSucceededEvent(
id="success-event",
node_id="node-1",
node_type=NodeType.CODE,
node_title="Test Node",
start_at=datetime.utcnow(),
node_run_result=NodeRunResult(status=WorkflowNodeExecutionStatus.SUCCEEDED),
)
def test_dispatcher_checks_commands_during_idle_and_on_completion() -> None:
"""Dispatcher polls commands when idle and after completion events."""
started_checks = _run_dispatcher_for_event(_make_started_event())
succeeded_checks = _run_dispatcher_for_event(_make_succeeded_event())
assert started_checks == 2
assert succeeded_checks == 3
class _PauseStubEventHandler:
"""Minimal event handler that marks execution complete after handling an event."""
def __init__(self, coordinator: _StubExecutionCoordinator) -> None:
self._coordinator = coordinator
self.events = []
def dispatch(self, event) -> None:
self.events.append(event)
if isinstance(event, NodeRunPauseRequestedEvent):
self._coordinator.mark_complete()
def test_dispatcher_drain_event_queue():
events = [
NodeRunStartedEvent(
id="start-event",
node_id="node-1",
node_type=NodeType.CODE,
node_title="Code",
start_at=datetime.utcnow(),
),
NodeRunPauseRequestedEvent(
id="pause-event",
node_id="node-1",
node_type=NodeType.CODE,
reason=SchedulingPause(message="test pause"),
),
NodeRunSucceededEvent(
id="success-event",
node_id="node-1",
node_type=NodeType.CODE,
start_at=datetime.utcnow(),
node_run_result=NodeRunResult(status=WorkflowNodeExecutionStatus.SUCCEEDED),
),
]
event_queue: queue.Queue = queue.Queue()
for e in events:
event_queue.put(e)
coordinator = _StubExecutionCoordinator()
event_handler = _PauseStubEventHandler(coordinator)
dispatcher = Dispatcher(
event_queue=event_queue,
event_handler=event_handler,
execution_coordinator=coordinator,
)
dispatcher._dispatcher_loop()
# ensure all events are drained.
assert event_queue.empty()

View File

@@ -3,13 +3,17 @@
import time
from unittest.mock import MagicMock
from core.app.entities.app_invoke_entities import InvokeFrom
from core.workflow.entities.graph_init_params import GraphInitParams
from core.workflow.entities.pause_reason import SchedulingPause
from core.workflow.graph import Graph
from core.workflow.graph_engine import GraphEngine
from core.workflow.graph_engine.command_channels import InMemoryChannel
from core.workflow.graph_engine.entities.commands import AbortCommand, CommandType, PauseCommand
from core.workflow.graph_events import GraphRunAbortedEvent, GraphRunPausedEvent, GraphRunStartedEvent
from core.workflow.nodes.start.start_node import StartNode
from core.workflow.runtime import GraphRuntimeState, VariablePool
from models.enums import UserFrom
def test_abort_command():
@@ -26,11 +30,23 @@ def test_abort_command():
mock_graph.root_node.id = "start"
# Create mock nodes with required attributes - using shared runtime state
mock_start_node = MagicMock()
mock_start_node.state = None
mock_start_node.id = "start"
mock_start_node.graph_runtime_state = shared_runtime_state # Use shared instance
mock_graph.nodes["start"] = mock_start_node
start_node = StartNode(
id="start",
config={"id": "start"},
graph_init_params=GraphInitParams(
tenant_id="test_tenant",
app_id="test_app",
workflow_id="test_workflow",
graph_config={},
user_id="test_user",
user_from=UserFrom.ACCOUNT,
invoke_from=InvokeFrom.DEBUGGER,
call_depth=0,
),
graph_runtime_state=shared_runtime_state,
)
start_node.init_node_data({"title": "start", "variables": []})
mock_graph.nodes["start"] = start_node
# Mock graph methods
mock_graph.get_outgoing_edges = MagicMock(return_value=[])
@@ -124,11 +140,23 @@ def test_pause_command():
mock_graph.root_node = MagicMock()
mock_graph.root_node.id = "start"
mock_start_node = MagicMock()
mock_start_node.state = None
mock_start_node.id = "start"
mock_start_node.graph_runtime_state = shared_runtime_state
mock_graph.nodes["start"] = mock_start_node
start_node = StartNode(
id="start",
config={"id": "start"},
graph_init_params=GraphInitParams(
tenant_id="test_tenant",
app_id="test_app",
workflow_id="test_workflow",
graph_config={},
user_id="test_user",
user_from=UserFrom.ACCOUNT,
invoke_from=InvokeFrom.DEBUGGER,
call_depth=0,
),
graph_runtime_state=shared_runtime_state,
)
start_node.init_node_data({"title": "start", "variables": []})
mock_graph.nodes["start"] = start_node
mock_graph.get_outgoing_edges = MagicMock(return_value=[])
mock_graph.get_incoming_edges = MagicMock(return_value=[])
@@ -153,5 +181,5 @@ def test_pause_command():
assert pause_events[0].reason == SchedulingPause(message="User requested pause")
graph_execution = engine.graph_runtime_state.graph_execution
assert graph_execution.is_paused
assert graph_execution.paused
assert graph_execution.pause_reason == SchedulingPause(message="User requested pause")

View File

@@ -1,109 +0,0 @@
"""Tests for dispatcher command checking behavior."""
from __future__ import annotations
import queue
from datetime import datetime
from core.workflow.enums import NodeType, WorkflowNodeExecutionStatus
from core.workflow.graph_engine.event_management.event_manager import EventManager
from core.workflow.graph_engine.orchestration.dispatcher import Dispatcher
from core.workflow.graph_events import NodeRunStartedEvent, NodeRunSucceededEvent
from core.workflow.node_events import NodeRunResult
class _StubExecutionCoordinator:
"""Stub execution coordinator that tracks command checks."""
def __init__(self) -> None:
self.command_checks = 0
self.scaling_checks = 0
self._execution_complete = False
self.mark_complete_called = False
self.failed = False
self._paused = False
def check_commands(self) -> None:
self.command_checks += 1
def check_scaling(self) -> None:
self.scaling_checks += 1
@property
def is_paused(self) -> bool:
return self._paused
def is_execution_complete(self) -> bool:
return self._execution_complete
def mark_complete(self) -> None:
self.mark_complete_called = True
def mark_failed(self, error: Exception) -> None: # pragma: no cover - defensive, not triggered in tests
self.failed = True
def set_execution_complete(self) -> None:
self._execution_complete = True
class _StubEventHandler:
"""Minimal event handler that marks execution complete after handling an event."""
def __init__(self, coordinator: _StubExecutionCoordinator) -> None:
self._coordinator = coordinator
self.events = []
def dispatch(self, event) -> None:
self.events.append(event)
self._coordinator.set_execution_complete()
def _run_dispatcher_for_event(event) -> int:
"""Run the dispatcher loop for a single event and return command check count."""
event_queue: queue.Queue = queue.Queue()
event_queue.put(event)
coordinator = _StubExecutionCoordinator()
event_handler = _StubEventHandler(coordinator)
event_manager = EventManager()
dispatcher = Dispatcher(
event_queue=event_queue,
event_handler=event_handler,
event_collector=event_manager,
execution_coordinator=coordinator,
)
dispatcher._dispatcher_loop()
return coordinator.command_checks
def _make_started_event() -> NodeRunStartedEvent:
return NodeRunStartedEvent(
id="start-event",
node_id="node-1",
node_type=NodeType.CODE,
node_title="Test Node",
start_at=datetime.utcnow(),
)
def _make_succeeded_event() -> NodeRunSucceededEvent:
return NodeRunSucceededEvent(
id="success-event",
node_id="node-1",
node_type=NodeType.CODE,
node_title="Test Node",
start_at=datetime.utcnow(),
node_run_result=NodeRunResult(status=WorkflowNodeExecutionStatus.SUCCEEDED),
)
def test_dispatcher_checks_commands_during_idle_and_on_completion() -> None:
"""Dispatcher polls commands when idle and after completion events."""
started_checks = _run_dispatcher_for_event(_make_started_event())
succeeded_checks = _run_dispatcher_for_event(_make_succeeded_event())
assert started_checks == 1
assert succeeded_checks == 2

View File

@@ -48,15 +48,3 @@ def test_handle_pause_noop_when_execution_running() -> None:
worker_pool.stop.assert_not_called()
state_manager.clear_executing.assert_not_called()
def test_is_execution_complete_when_paused() -> None:
"""Paused execution should be treated as complete."""
graph_execution = GraphExecution(workflow_id="workflow")
graph_execution.start()
graph_execution.pause("Awaiting input")
coordinator, state_manager, _worker_pool = _build_coordinator(graph_execution)
state_manager.is_execution_complete.return_value = False
assert coordinator.is_execution_complete()

View File

@@ -0,0 +1,37 @@
from core.app.apps.workflow.app_generator import SKIP_PREPARE_USER_INPUTS_KEY
from models.enums import AppTriggerType, WorkflowRunTriggeredFrom
from services.workflow.entities import TriggerData, WebhookTriggerData
from tasks import async_workflow_tasks
def test_build_generator_args_sets_skip_flag_for_webhook():
trigger_data = WebhookTriggerData(
app_id="app",
tenant_id="tenant",
workflow_id="workflow",
root_node_id="node",
inputs={"webhook_data": {"body": {"foo": "bar"}}},
)
args = async_workflow_tasks._build_generator_args(trigger_data)
assert args[SKIP_PREPARE_USER_INPUTS_KEY] is True
assert args["inputs"]["webhook_data"]["body"]["foo"] == "bar"
def test_build_generator_args_keeps_validation_for_other_triggers():
trigger_data = TriggerData(
app_id="app",
tenant_id="tenant",
workflow_id="workflow",
root_node_id="node",
inputs={"foo": "bar"},
files=[],
trigger_type=AppTriggerType.TRIGGER_SCHEDULE,
trigger_from=WorkflowRunTriggeredFrom.SCHEDULE,
)
args = async_workflow_tasks._build_generator_args(trigger_data)
assert SKIP_PREPARE_USER_INPUTS_KEY not in args
assert args["inputs"] == {"foo": "bar"}

View File

@@ -365,10 +365,9 @@ WEB_API_CORS_ALLOW_ORIGINS=*
# Specifies the allowed origins for cross-origin requests to the console API,
# e.g. https://cloud.dify.ai or * for all origins.
CONSOLE_CORS_ALLOW_ORIGINS=*
# Set COOKIE_DOMAIN when the console frontend and API are on different subdomains.
# Provide the registrable domain (e.g. example.com); leading dots are optional.
# When the frontend and backend run on different subdomains, set COOKIE_DOMAIN to the sites top-level domain (e.g., `example.com`). Leading dots are optional.
COOKIE_DOMAIN=
# The frontend reads NEXT_PUBLIC_COOKIE_DOMAIN to align cookie handling with the API.
# When the frontend and backend run on different subdomains, set NEXT_PUBLIC_COOKIE_DOMAIN=1.
NEXT_PUBLIC_COOKIE_DOMAIN=
# ------------------------------

View File

@@ -12,6 +12,9 @@ NEXT_PUBLIC_API_PREFIX=http://localhost:5001/console/api
# console or api domain.
# example: http://udify.app/api
NEXT_PUBLIC_PUBLIC_API_PREFIX=http://localhost:5001/api
# When the frontend and backend run on different subdomains, set NEXT_PUBLIC_COOKIE_DOMAIN=1.
NEXT_PUBLIC_COOKIE_DOMAIN=
# The API PREFIX for MARKETPLACE
NEXT_PUBLIC_MARKETPLACE_API_PREFIX=https://marketplace.dify.ai/api/v1
# The URL for MARKETPLACE
@@ -34,9 +37,6 @@ NEXT_PUBLIC_CSP_WHITELIST=
# Default is not allow to embed into iframe to prevent Clickjacking: https://owasp.org/www-community/attacks/Clickjacking
NEXT_PUBLIC_ALLOW_EMBED=
# Shared cookie domain when console UI and API use different subdomains (e.g. example.com)
NEXT_PUBLIC_COOKIE_DOMAIN=
# Allow rendering unsafe URLs which have "data:" scheme.
NEXT_PUBLIC_ALLOW_UNSAFE_DATA_SCHEME=false

View File

@@ -32,6 +32,7 @@ NEXT_PUBLIC_EDITION=SELF_HOSTED
# different from api or web app domain.
# example: http://cloud.dify.ai/console/api
NEXT_PUBLIC_API_PREFIX=http://localhost:5001/console/api
NEXT_PUBLIC_COOKIE_DOMAIN=
# The URL for Web APP, refers to the Web App base URL of WEB service if web app domain is different from
# console or api domain.
# example: http://udify.app/api
@@ -41,6 +42,11 @@ NEXT_PUBLIC_PUBLIC_API_PREFIX=http://localhost:5001/api
NEXT_PUBLIC_SENTRY_DSN=
```
> [!IMPORTANT]
>
> 1. When the frontend and backend run on different subdomains, set NEXT_PUBLIC_COOKIE_DOMAIN=1. The frontend and backend must be under the same top-level domain in order to share authentication cookies.
> 1. It's necessary to set NEXT_PUBLIC_API_PREFIX and NEXT_PUBLIC_PUBLIC_API_PREFIX to the correct backend API URL.
Finally, run the development server:
```bash

View File

@@ -1,6 +1,6 @@
'use client'
import type { FC } from 'react'
import React, { useMemo } from 'react'
import React, { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import AppCard from '@/app/components/app/overview/app-card'
@@ -24,6 +24,7 @@ import { useStore as useAppStore } from '@/app/components/app/store'
import { useAppWorkflow } from '@/service/use-workflow'
import type { BlockEnum } from '@/app/components/workflow/types'
import { isTriggerNode } from '@/app/components/workflow/types'
import { useDocLink } from '@/context/i18n'
export type ICardViewProps = {
appId: string
@@ -33,6 +34,7 @@ export type ICardViewProps = {
const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
const { t } = useTranslation()
const docLink = useDocLink()
const { notify } = useContext(ToastContext)
const appDetail = useAppStore(state => state.appDetail)
const setAppDetail = useAppStore(state => state.setAppDetail)
@@ -53,6 +55,35 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
})
}, [isWorkflowApp, currentWorkflow])
const shouldRenderAppCards = !isWorkflowApp || hasTriggerNode === false
const disableAppCards = !shouldRenderAppCards
const triggerDocUrl = docLink('/guides/workflow/node/start')
const buildTriggerModeMessage = useCallback((featureName: string) => (
<div className='flex flex-col gap-1'>
<div className='text-xs text-text-secondary'>
{t('appOverview.overview.disableTooltip.triggerMode', { feature: featureName })}
</div>
<div
className='cursor-pointer text-xs font-medium text-text-accent hover:underline'
onClick={(event) => {
event.stopPropagation()
window.open(triggerDocUrl, '_blank')
}}
>
{t('appOverview.overview.appInfo.enableTooltip.learnMore')}
</div>
</div>
), [t, triggerDocUrl])
const disableWebAppTooltip = disableAppCards
? buildTriggerModeMessage(t('appOverview.overview.appInfo.title'))
: null
const disableApiTooltip = disableAppCards
? buildTriggerModeMessage(t('appOverview.overview.apiInfo.title'))
: null
const disableMcpTooltip = disableAppCards
? buildTriggerModeMessage(t('tools.mcp.server.title'))
: null
const updateAppDetail = async () => {
try {
@@ -124,39 +155,48 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
if (!appDetail)
return <Loading />
return (
<div className={className || 'mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2'}>
{
shouldRenderAppCards && (
<>
<AppCard
appInfo={appDetail}
cardType="webapp"
isInPanel={isInPanel}
onChangeStatus={onChangeSiteStatus}
onGenerateCode={onGenerateCode}
onSaveSiteConfig={onSaveSiteConfig}
/>
<AppCard
cardType="api"
appInfo={appDetail}
isInPanel={isInPanel}
onChangeStatus={onChangeApiStatus}
/>
{showMCPCard && (
<MCPServiceCard
appInfo={appDetail}
/>
)}
</>
)
}
{showTriggerCard && (
<TriggerCard
const appCards = (
<>
<AppCard
appInfo={appDetail}
cardType="webapp"
isInPanel={isInPanel}
triggerModeDisabled={disableAppCards}
triggerModeMessage={disableWebAppTooltip}
onChangeStatus={onChangeSiteStatus}
onGenerateCode={onGenerateCode}
onSaveSiteConfig={onSaveSiteConfig}
/>
<AppCard
cardType="api"
appInfo={appDetail}
isInPanel={isInPanel}
triggerModeDisabled={disableAppCards}
triggerModeMessage={disableApiTooltip}
onChangeStatus={onChangeApiStatus}
/>
{showMCPCard && (
<MCPServiceCard
appInfo={appDetail}
onToggleResult={handleCallbackResult}
triggerModeDisabled={disableAppCards}
triggerModeMessage={disableMcpTooltip}
/>
)}
</>
)
const triggerCardNode = showTriggerCard ? (
<TriggerCard
appInfo={appDetail}
onToggleResult={handleCallbackResult}
/>
) : null
return (
<div className={className || 'mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2'}>
{disableAppCards && triggerCardNode}
{appCards}
{!disableAppCards && triggerCardNode}
</div>
)
}

View File

@@ -258,9 +258,9 @@ const ConfigVar: FC<IConfigVarProps> = ({ promptVariables, readonly, onPromptVar
</div>
)}
{hasVar && (
<div className={cn('mt-1 grid px-3 pb-3')}>
<div className='mt-1 px-3 pb-3'>
<ReactSortable
className={cn('grid-col-1 grid space-y-1', readonly && 'grid-cols-2 gap-1 space-y-0')}
className='space-y-1'
list={promptVariablesWithIds}
setList={(list) => { onPromptVariablesChange?.(list.map(item => item.variable)) }}
handle='.handle'

View File

@@ -38,7 +38,7 @@ const VarItem: FC<ItemProps> = ({
const [isDeleting, setIsDeleting] = useState(false)
return (
<div className={cn('group relative mb-1 flex h-[34px] w-full items-center rounded-lg border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg pl-2.5 pr-3 shadow-xs last-of-type:mb-0 hover:bg-components-panel-on-panel-item-bg-hover hover:shadow-sm', isDeleting && 'border-state-destructive-border hover:bg-state-destructive-hover', readonly && 'cursor-not-allowed', className)}>
<div className={cn('group relative mb-1 flex h-[34px] w-full items-center rounded-lg border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg pl-2.5 pr-3 shadow-xs last-of-type:mb-0 hover:bg-components-panel-on-panel-item-bg-hover hover:shadow-sm', isDeleting && 'border-state-destructive-border hover:bg-state-destructive-hover', readonly && 'cursor-not-allowed opacity-30', className)}>
<VarIcon className={cn('mr-1 h-4 w-4 shrink-0 text-text-accent', canDrag && 'group-hover:opacity-0')} />
{canDrag && (
<RiDraggable className='absolute left-3 top-3 hidden h-3 w-3 cursor-pointer text-text-tertiary group-hover:block' />

View File

@@ -13,14 +13,10 @@ import ConfigContext from '@/context/debug-configuration'
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
import Switch from '@/app/components/base/switch'
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
import OptionCard from '@/app/components/workflow/nodes/_base/components/option-card'
import { Resolution } from '@/types/app'
import { noop } from 'lodash-es'
import cn from '@/utils/classnames'
const ConfigVision: FC = () => {
const { t } = useTranslation()
const { isShowVisionConfig, isAllowVideoUpload, readonly } = useContext(ConfigContext)
const { isShowVisionConfig, isAllowVideoUpload } = useContext(ConfigContext)
const file = useFeatures(s => s.features.file)
const featuresStore = useFeaturesStore()
@@ -57,7 +53,7 @@ const ConfigVision: FC = () => {
setFeatures(newFeatures)
}, [featuresStore, isAllowVideoUpload])
if (!isShowVisionConfig || (readonly && !isImageEnabled))
if (!isShowVisionConfig)
return null
return (
@@ -78,49 +74,37 @@ const ConfigVision: FC = () => {
/>
</div>
<div className='flex shrink-0 items-center'>
{readonly ? (<>
<div className='mr-2 flex items-center gap-0.5'>
<div className='system-xs-medium-uppercase text-text-tertiary'>{t('appDebug.vision.visionSettings.resolution')}</div>
<Tooltip
popupContent={
<div className='w-[180px]' >
{t('appDebug.vision.visionSettings.resolutionTooltip').split('\n').map(item => (
<div key={item}>{item}</div>
))}
</div>
}
/>
</div>
<div className='flex items-center gap-1'>
<OptionCard
title={t('appDebug.vision.visionSettings.high')}
selected={file?.image?.detail === Resolution.high}
onSelect={noop}
className={cn(
'cursor-not-allowed rounded-lg px-3 hover:shadow-none',
file?.image?.detail !== Resolution.high && 'hover:border-components-option-card-option-border',
)}
/>
<OptionCard
title={t('appDebug.vision.visionSettings.low')}
selected={file?.image?.detail === Resolution.low}
onSelect={noop}
className={cn(
'cursor-not-allowed rounded-lg px-3 hover:shadow-none',
file?.image?.detail !== Resolution.low && 'hover:border-components-option-card-option-border',
)}
/>
</div>
</>) : <>
<ParamConfig />
<div className='ml-1 mr-3 h-3.5 w-[1px] bg-divider-regular'></div>
<Switch
defaultValue={isImageEnabled}
onChange={handleChange}
size='md'
{/* <div className='mr-2 flex items-center gap-0.5'>
<div className='text-text-tertiary system-xs-medium-uppercase'>{t('appDebug.vision.visionSettings.resolution')}</div>
<Tooltip
popupContent={
<div className='w-[180px]' >
{t('appDebug.vision.visionSettings.resolutionTooltip').split('\n').map(item => (
<div key={item}>{item}</div>
))}
</div>
}
/>
</>}
</div> */}
{/* <div className='flex items-center gap-1'>
<OptionCard
title={t('appDebug.vision.visionSettings.high')}
selected={file?.image?.detail === Resolution.high}
onSelect={() => handleChange(Resolution.high)}
/>
<OptionCard
title={t('appDebug.vision.visionSettings.low')}
selected={file?.image?.detail === Resolution.low}
onSelect={() => handleChange(Resolution.low)}
/>
</div> */}
<ParamConfig />
<div className='ml-1 mr-3 h-3.5 w-[1px] bg-divider-regular'></div>
<Switch
defaultValue={isImageEnabled}
onChange={handleChange}
size='md'
/>
</div>
</div>
)

View File

@@ -37,7 +37,7 @@ type AgentToolWithMoreInfo = AgentTool & { icon: any; collection?: Collection }
const AgentTools: FC = () => {
const { t } = useTranslation()
const [isShowChooseTool, setIsShowChooseTool] = useState(false)
const { readonly, modelConfig, setModelConfig } = useContext(ConfigContext)
const { modelConfig, setModelConfig } = useContext(ConfigContext)
const { data: buildInTools } = useAllBuiltInTools()
const { data: customTools } = useAllCustomTools()
const { data: workflowTools } = useAllWorkflowTools()
@@ -119,7 +119,7 @@ const AgentTools: FC = () => {
}
const getProviderShowName = (item: AgentTool) => {
const type = item.provider_type
if (type === CollectionType.builtIn)
if(type === CollectionType.builtIn)
return item.provider_name.split('/').pop()
return item.provider_name
}
@@ -158,7 +158,7 @@ const AgentTools: FC = () => {
headerRight={
<div className='flex items-center'>
<div className='text-xs font-normal leading-[18px] text-text-tertiary'>{tools.filter(item => !!item.enabled).length}/{tools.length}&nbsp;{t('appDebug.agent.tools.enabled')}</div>
{tools.length < MAX_TOOLS_NUM && !readonly && (
{tools.length < MAX_TOOLS_NUM && (
<>
<div className='ml-3 mr-1 h-3.5 w-px bg-divider-regular'></div>
<ToolPicker
@@ -177,7 +177,7 @@ const AgentTools: FC = () => {
</div>
}
>
<div className={cn('grid grid-cols-1 flex-wrap items-center justify-between gap-1 2xl:grid-cols-2', readonly && 'grid-cols-2')}>
<div className='grid grid-cols-1 flex-wrap items-center justify-between gap-1 2xl:grid-cols-2'>
{tools.map((item: AgentTool & { icon: any; collection?: Collection }, index) => (
<div key={index}
className={cn(
@@ -201,7 +201,7 @@ const AgentTools: FC = () => {
>
<span className='system-xs-medium pr-1.5 text-text-secondary'>{getProviderShowName(item)}</span>
<span className='text-text-tertiary'>{item.tool_label}</span>
{!item.isDeleted && !readonly && (
{!item.isDeleted && (
<Tooltip
popupContent={
<div className='w-[180px]'>
@@ -212,7 +212,7 @@ const AgentTools: FC = () => {
}
>
<div className='h-4 w-4'>
<div className={cn('ml-0.5 hidden group-hover:inline-block')}>
<div className='ml-0.5 hidden group-hover:inline-block'>
<RiInformation2Line className='h-4 w-4 text-text-tertiary' />
</div>
</div>
@@ -246,8 +246,8 @@ const AgentTools: FC = () => {
</div>
</div>
)}
{!item.isDeleted && !readonly && (
<div className={cn('mr-2 hidden items-center gap-1 group-hover:flex')}>
{!item.isDeleted && (
<div className='mr-2 hidden items-center gap-1 group-hover:flex'>
{!item.notAuthor && (
<Tooltip
popupContent={t('tools.setBuiltInTools.infoAndSetting')}
@@ -280,7 +280,7 @@ const AgentTools: FC = () => {
{!item.notAuthor && (
<Switch
defaultValue={item.isDeleted ? false : item.enabled}
disabled={item.isDeleted || readonly}
disabled={item.isDeleted}
size='md'
onChange={(enabled) => {
const newModelConfig = produce(modelConfig, (draft) => {
@@ -291,14 +291,10 @@ const AgentTools: FC = () => {
}} />
)}
{item.notAuthor && (
<Button
variant='secondary'
disabled={readonly}
size='small' onClick={() => {
setCurrentTool(item)
setIsShowSettingTool(true)
}}
>
<Button variant='secondary' size='small' onClick={() => {
setCurrentTool(item)
setIsShowSettingTool(true)
}}>
{t('tools.notAuthorized')}
<Indicator className='ml-2' color='orange' />
</Button>

View File

@@ -16,7 +16,7 @@ const ConfigAudio: FC = () => {
const { t } = useTranslation()
const file = useFeatures(s => s.features.file)
const featuresStore = useFeaturesStore()
const { isShowAudioConfig, readonly } = useContext(ConfigContext)
const { isShowAudioConfig } = useContext(ConfigContext)
const isAudioEnabled = file?.allowed_file_types?.includes(SupportUploadFileTypes.audio) ?? false
@@ -44,7 +44,7 @@ const ConfigAudio: FC = () => {
setFeatures(newFeatures)
}, [featuresStore])
if (!isShowAudioConfig || (readonly && !isAudioEnabled))
if (!isShowAudioConfig)
return null
return (
@@ -64,16 +64,14 @@ const ConfigAudio: FC = () => {
}
/>
</div>
{!readonly && (
<div className='flex shrink-0 items-center'>
<div className='ml-1 mr-3 h-3.5 w-[1px] bg-divider-subtle'></div>
<Switch
defaultValue={isAudioEnabled}
onChange={handleChange}
size='md'
/>
</div>
)}
<div className='flex shrink-0 items-center'>
<div className='ml-1 mr-3 h-3.5 w-[1px] bg-divider-subtle'></div>
<Switch
defaultValue={isAudioEnabled}
onChange={handleChange}
size='md'
/>
</div>
</div>
)
}

View File

@@ -16,7 +16,7 @@ const ConfigDocument: FC = () => {
const { t } = useTranslation()
const file = useFeatures(s => s.features.file)
const featuresStore = useFeaturesStore()
const { isShowDocumentConfig, readonly } = useContext(ConfigContext)
const { isShowDocumentConfig } = useContext(ConfigContext)
const isDocumentEnabled = file?.allowed_file_types?.includes(SupportUploadFileTypes.document) ?? false
@@ -44,7 +44,7 @@ const ConfigDocument: FC = () => {
setFeatures(newFeatures)
}, [featuresStore])
if (!isShowDocumentConfig || (readonly && !isDocumentEnabled))
if (!isShowDocumentConfig)
return null
return (
@@ -64,16 +64,14 @@ const ConfigDocument: FC = () => {
}
/>
</div>
{!readonly && (
<div className='flex shrink-0 items-center'>
<div className='ml-1 mr-3 h-3.5 w-[1px] bg-divider-subtle'></div>
<Switch
defaultValue={isDocumentEnabled}
onChange={handleChange}
size='md'
/>
</div>
)}
<div className='flex shrink-0 items-center'>
<div className='ml-1 mr-3 h-3.5 w-[1px] bg-divider-subtle'></div>
<Switch
defaultValue={isDocumentEnabled}
onChange={handleChange}
size='md'
/>
</div>
</div>
)
}

View File

@@ -18,7 +18,6 @@ import { AppModeEnum, ModelModeType } from '@/types/app'
const Config: FC = () => {
const {
readonly,
mode,
isAdvancedMode,
modelModeType,
@@ -28,7 +27,6 @@ const Config: FC = () => {
modelConfig,
setModelConfig,
setPrevPromptConfig,
dataSets,
} = useContext(ConfigContext)
const isChatApp = [AppModeEnum.ADVANCED_CHAT, AppModeEnum.AGENT_CHAT, AppModeEnum.CHAT].includes(mode)
const formattingChangedDispatcher = useFormattingChangedDispatcher()
@@ -67,28 +65,19 @@ const Config: FC = () => {
promptTemplate={promptTemplate}
promptVariables={promptVariables}
onChange={handlePromptChange}
readonly={readonly}
/>
{/* Variables */}
{!(readonly && promptVariables.length === 0) && (
<ConfigVar
promptVariables={promptVariables}
onPromptVariablesChange={handlePromptVariablesNameChange}
readonly={readonly}
/>
)}
<ConfigVar
promptVariables={promptVariables}
onPromptVariablesChange={handlePromptVariablesNameChange}
/>
{/* Dataset */}
{!(readonly && dataSets.length === 0) && (
<DatasetConfig
readonly={readonly}
hideMetadataFilter={readonly}
/>
)
}
<DatasetConfig />
{/* Tools */}
{isAgent && !(readonly && modelConfig.agentConfig.tools.length === 0) && (
{isAgent && (
<AgentTools />
)}
@@ -99,7 +88,7 @@ const Config: FC = () => {
<ConfigAudio />
{/* Chat History */}
{!readonly && isAdvancedMode && isChatApp && modelModeType === ModelModeType.completion && (
{isAdvancedMode && isChatApp && modelModeType === ModelModeType.completion && (
<HistoryPanel
showWarning={!hasSetBlockStatus.history}
onShowEditModal={showHistoryModal}

View File

@@ -29,7 +29,6 @@ const Item: FC<ItemProps> = ({
config,
onSave,
onRemove,
readonly = false,
editable = true,
}) => {
const media = useBreakpoints()
@@ -56,7 +55,6 @@ const Item: FC<ItemProps> = ({
<div className={cn(
'group relative mb-1 flex h-10 w-full cursor-pointer items-center justify-between rounded-lg border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg px-2 last-of-type:mb-0 hover:bg-components-panel-on-panel-item-bg-hover',
isDeleting && 'border-state-destructive-border hover:bg-state-destructive-hover',
readonly && 'cursor-not-allowed',
)}>
<div className='flex w-0 grow items-center space-x-1.5'>
<AppIcon
@@ -70,7 +68,7 @@ const Item: FC<ItemProps> = ({
</div>
<div className='ml-2 hidden shrink-0 items-center space-x-1 group-hover:flex'>
{
editable && !readonly && <ActionButton
editable && <ActionButton
onClick={(e) => {
e.stopPropagation()
setShowSettingsModal(true)
@@ -79,18 +77,14 @@ const Item: FC<ItemProps> = ({
<RiEditLine className='h-4 w-4 shrink-0 text-text-tertiary' />
</ActionButton>
}
{
!readonly && (
<ActionButton
onClick={() => onRemove(config.id)}
state={isDeleting ? ActionButtonState.Destructive : ActionButtonState.Default}
onMouseEnter={() => setIsDeleting(true)}
onMouseLeave={() => setIsDeleting(false)}
>
<RiDeleteBinLine className={cn('h-4 w-4 shrink-0 text-text-tertiary', isDeleting && 'text-text-destructive')} />
</ActionButton>
)
}
<ActionButton
onClick={() => onRemove(config.id)}
state={isDeleting ? ActionButtonState.Destructive : ActionButtonState.Default}
onMouseEnter={() => setIsDeleting(true)}
onMouseLeave={() => setIsDeleting(false)}
>
<RiDeleteBinLine className={cn('h-4 w-4 shrink-0 text-text-tertiary', isDeleting && 'text-text-destructive')} />
</ActionButton>
</div>
{
config.indexing_technique && <Badge
@@ -104,15 +98,13 @@ const Item: FC<ItemProps> = ({
text={t('dataset.externalTag') as string}
/>
}
{showSettingsModal && (
<Drawer isOpen={showSettingsModal} onClose={() => setShowSettingsModal(false)} footer={null} mask={isMobile} panelClassName='mt-16 mx-2 sm:mr-2 mb-3 !p-0 !max-w-[640px] rounded-xl'>
<SettingsModal
currentDataset={config}
onCancel={() => setShowSettingsModal(false)}
onSave={handleSave}
/>
</Drawer>
)}
<Drawer isOpen={showSettingsModal} onClose={() => setShowSettingsModal(false)} footer={null} mask={isMobile} panelClassName='mt-16 mx-2 sm:mr-2 mb-3 !p-0 !max-w-[640px] rounded-xl'>
<SettingsModal
currentDataset={config}
onCancel={() => setShowSettingsModal(false)}
onSave={handleSave}
/>
</Drawer>
</div >
)
}

View File

@@ -36,13 +36,8 @@ import {
LogicalOperator,
MetadataFilteringVariableType,
} from '@/app/components/workflow/nodes/knowledge-retrieval/types'
import cn from '@/utils/classnames'
type Props = {
readonly?: boolean
hideMetadataFilter?: boolean
}
const DatasetConfig: FC<Props> = ({ readonly, hideMetadataFilter }) => {
const DatasetConfig: FC = () => {
const { t } = useTranslation()
const userProfile = useAppContextSelector(s => s.userProfile)
const {
@@ -259,25 +254,24 @@ const DatasetConfig: FC<Props> = ({ readonly, hideMetadataFilter }) => {
className='mt-2'
title={t('appDebug.feature.dataSet.title')}
headerRight={
!readonly && (<div className='flex items-center gap-1'>
<div className='flex items-center gap-1'>
{!isAgent && <ParamsConfig disabled={!hasData} selectedDatasets={dataSet} />}
<OperationBtn type="add" onClick={showSelectDataSet} />
</div>)
</div>
}
hasHeaderBottomBorder={!hasData}
noBodySpacing
>
{hasData
? (
<div className={cn('mt-1 grid grid-cols-1 px-3 pb-3', readonly && 'grid-cols-2 gap-1')}>
<div className='mt-1 flex flex-wrap justify-between px-3 pb-3'>
{formattedDataset.map(item => (
<CardItem
key={item.id}
config={item}
onRemove={onRemove}
onSave={handleSave}
editable={item.editable && !readonly}
readonly={readonly}
editable={item.editable}
/>
))}
</div>
@@ -288,29 +282,27 @@ const DatasetConfig: FC<Props> = ({ readonly, hideMetadataFilter }) => {
</div>
)}
{!hideMetadataFilter && (
<div className='border-t border-t-divider-subtle py-2'>
<MetadataFilter
metadataList={metadataList}
selectedDatasetsLoaded
metadataFilterMode={datasetConfigs.metadata_filtering_mode}
metadataFilteringConditions={datasetConfigs.metadata_filtering_conditions}
handleAddCondition={handleAddCondition}
handleMetadataFilterModeChange={handleMetadataFilterModeChange}
handleRemoveCondition={handleRemoveCondition}
handleToggleConditionLogicalOperator={handleToggleConditionLogicalOperator}
handleUpdateCondition={handleUpdateCondition}
metadataModelConfig={datasetConfigs.metadata_model_config}
handleMetadataModelChange={handleMetadataModelChange}
handleMetadataCompletionParamsChange={handleMetadataCompletionParamsChange}
isCommonVariable
availableCommonStringVars={promptVariablesToSelect.filter(item => item.type === MetadataFilteringVariableType.string || item.type === MetadataFilteringVariableType.select)}
availableCommonNumberVars={promptVariablesToSelect.filter(item => item.type === MetadataFilteringVariableType.number)}
/>
</div>
)}
<div className='border-t border-t-divider-subtle py-2'>
<MetadataFilter
metadataList={metadataList}
selectedDatasetsLoaded
metadataFilterMode={datasetConfigs.metadata_filtering_mode}
metadataFilteringConditions={datasetConfigs.metadata_filtering_conditions}
handleAddCondition={handleAddCondition}
handleMetadataFilterModeChange={handleMetadataFilterModeChange}
handleRemoveCondition={handleRemoveCondition}
handleToggleConditionLogicalOperator={handleToggleConditionLogicalOperator}
handleUpdateCondition={handleUpdateCondition}
metadataModelConfig={datasetConfigs.metadata_model_config}
handleMetadataModelChange={handleMetadataModelChange}
handleMetadataCompletionParamsChange={handleMetadataCompletionParamsChange}
isCommonVariable
availableCommonStringVars={promptVariablesToSelect.filter(item => item.type === MetadataFilteringVariableType.string || item.type === MetadataFilteringVariableType.select)}
availableCommonNumberVars={promptVariablesToSelect.filter(item => item.type === MetadataFilteringVariableType.number)}
/>
</div>
{!readonly && mode === AppModeEnum.COMPLETION && dataSet.length > 0 && (
{mode === AppModeEnum.COMPLETION && dataSet.length > 0 && (
<ContextVar
value={selectedContextVar?.key}
options={promptVariablesToSelect}

View File

@@ -18,7 +18,7 @@ const ChatUserInput = ({
inputs,
}: Props) => {
const { t } = useTranslation()
const { modelConfig, setInputs, readonly } = useContext(ConfigContext)
const { modelConfig, setInputs } = useContext(ConfigContext)
const promptVariables = modelConfig.configs.prompt_variables.filter(({ key, name }) => {
return key && key?.trim() && name && name?.trim()
@@ -70,7 +70,6 @@ const ChatUserInput = ({
placeholder={name}
autoFocus={index === 0}
maxLength={max_length || DEFAULT_VALUE_MAX_LEN}
readOnly={readonly}
/>
)}
{type === 'paragraph' && (
@@ -79,7 +78,6 @@ const ChatUserInput = ({
placeholder={name}
value={inputs[key] ? `${inputs[key]}` : ''}
onChange={(e) => { handleInputValueChange(key, e.target.value) }}
readOnly={readonly}
/>
)}
{type === 'select' && (
@@ -89,7 +87,6 @@ const ChatUserInput = ({
onSelect={(i) => { handleInputValueChange(key, i.value as string) }}
items={(options || []).map(i => ({ name: i, value: i }))}
allowSearch={false}
disabled={readonly}
/>
)}
{type === 'number' && (
@@ -100,7 +97,6 @@ const ChatUserInput = ({
placeholder={name}
autoFocus={index === 0}
maxLength={max_length || DEFAULT_VALUE_MAX_LEN}
readOnly={readonly}
/>
)}
{type === 'checkbox' && (
@@ -109,7 +105,6 @@ const ChatUserInput = ({
value={!!inputs[key]}
required={required}
onChange={(value) => { handleInputValueChange(key, value) }}
readonly={readonly}
/>
)}
</div>

View File

@@ -16,7 +16,6 @@ import { useProviderContext } from '@/context/provider-context'
import { useFeatures } from '@/app/components/base/features/hooks'
import { cloneDeep, noop } from 'lodash-es'
import { DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG } from '@/config'
import { AppSourceType } from '@/service/share'
type TextGenerationItemProps = {
modelAndParameter: ModelAndParameter
@@ -130,11 +129,11 @@ const TextGenerationItem: FC<TextGenerationItemProps> = ({
return (
<TextGeneration
appSourceType={AppSourceType.webApp}
className='flex h-full flex-col overflow-y-auto border-none'
content={completion}
isLoading={!completion && isResponding}
isResponding={isResponding}
isInstalledApp={false}
siteInfo={null}
messageId={messageId}
isError={false}

View File

@@ -39,7 +39,6 @@ const DebugWithSingleModel = (
) => {
const { userProfile } = useAppContext()
const {
readonly,
modelConfig,
appId,
inputs,
@@ -155,7 +154,6 @@ const DebugWithSingleModel = (
return (
<Chat
readonly={readonly}
config={config}
chatList={chatList}
isResponding={isResponding}

View File

@@ -49,7 +49,6 @@ import PromptLogModal from '@/app/components/base/prompt-log-modal'
import { useStore as useAppStore } from '@/app/components/app/store'
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
import { noop } from 'lodash-es'
import { AppSourceType } from '@/service/share'
type IDebug = {
isAPIKeySet: boolean
@@ -72,7 +71,6 @@ const Debug: FC<IDebug> = ({
}) => {
const { t } = useTranslation()
const {
readonly,
appId,
mode,
modelModeType,
@@ -414,23 +412,19 @@ const Debug: FC<IDebug> = ({
}
{mode !== AppModeEnum.COMPLETION && (
<>
{!readonly && (
<TooltipPlus
popupContent={t('common.operation.refresh')}
>
<ActionButton onClick={clearConversation}>
<RefreshCcw01 className='h-4 w-4' />
</ActionButton>
</TooltipPlus>
)}
<TooltipPlus
popupContent={t('common.operation.refresh')}
>
<ActionButton onClick={clearConversation}>
<RefreshCcw01 className='h-4 w-4' />
</ActionButton>
</TooltipPlus>
{varList.length > 0 && (
<div className='relative ml-1 mr-2'>
<TooltipPlus
popupContent={t('workflow.panel.userInputField')}
>
<ActionButton state={expanded ? ActionButtonState.Active : undefined} onClick={() => !readonly && setExpanded(!expanded)}>
<ActionButton state={expanded ? ActionButtonState.Active : undefined} onClick={() => setExpanded(!expanded)}>
<RiEqualizer2Line className='h-4 w-4' />
</ActionButton>
</TooltipPlus>
@@ -512,12 +506,12 @@ const Debug: FC<IDebug> = ({
<div className='mx-4 mt-3'><GroupName name={t('appDebug.result')} /></div>
<div className='mx-3 mb-8'>
<TextGeneration
appSourceType={AppSourceType.webApp}
className="mt-2"
content={completionRes}
isLoading={!completionRes && isResponding}
isShowTextToSpeech={textToSpeechConfig.enabled && !!text2speechDefaultModel}
isResponding={isResponding}
isInstalledApp={false}
messageId={messageId}
isError={false}
onRetry={noop}
@@ -558,7 +552,7 @@ const Debug: FC<IDebug> = ({
onCancel={handleCancel}
/>
)}
{!isAPIKeySet && !readonly && (<HasNotSetAPIKEY isTrailFinished={!IS_CE_EDITION} onSetting={onSetting} />)}
{!isAPIKeySet && (<HasNotSetAPIKEY isTrailFinished={!IS_CE_EDITION} onSetting={onSetting} />)}
</>
)
}

View File

@@ -40,7 +40,7 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
onVisionFilesChange,
}) => {
const { t } = useTranslation()
const { readonly, modelModeType, modelConfig, setInputs, mode, isAdvancedMode, completionPromptConfig, chatPromptConfig } = useContext(ConfigContext)
const { modelModeType, modelConfig, setInputs, mode, isAdvancedMode, completionPromptConfig, chatPromptConfig } = useContext(ConfigContext)
const [userInputFieldCollapse, setUserInputFieldCollapse] = useState(false)
const promptVariables = modelConfig.configs.prompt_variables.filter(({ key, name }) => {
return key && key?.trim() && name && name?.trim()
@@ -60,12 +60,12 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
if (isAdvancedMode) {
if (modelModeType === ModelModeType.chat)
return chatPromptConfig?.prompt.every(({ text }) => !text)
return chatPromptConfig.prompt.every(({ text }) => !text)
return !completionPromptConfig.prompt?.text
}
else { return !modelConfig.configs.prompt_template }
}, [chatPromptConfig?.prompt, completionPromptConfig.prompt?.text, isAdvancedMode, mode, modelConfig.configs.prompt_template, modelModeType])
}, [chatPromptConfig.prompt, completionPromptConfig.prompt?.text, isAdvancedMode, mode, modelConfig.configs.prompt_template, modelModeType])
const handleInputValueChange = (key: string, value: string | boolean) => {
if (!(key in promptVariableObj))
@@ -95,8 +95,8 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
<div className={cn('px-4 pt-3', userInputFieldCollapse ? 'pb-3' : 'pb-1')}>
<div className='flex cursor-pointer items-center gap-0.5 py-0.5' onClick={() => setUserInputFieldCollapse(!userInputFieldCollapse)}>
<div className='system-md-semibold-uppercase text-text-secondary'>{t('appDebug.inputs.userInputField')}</div>
{userInputFieldCollapse && <RiArrowRightSLine className='h-4 w-4 text-text-secondary' />}
{!userInputFieldCollapse && <RiArrowDownSLine className='h-4 w-4 text-text-secondary' />}
{userInputFieldCollapse && <RiArrowRightSLine className='h-4 w-4 text-text-secondary'/>}
{!userInputFieldCollapse && <RiArrowDownSLine className='h-4 w-4 text-text-secondary'/>}
</div>
{!userInputFieldCollapse && (
<div className='system-xs-regular mt-1 text-text-tertiary'>{t('appDebug.inputs.completionVarTip')}</div>
@@ -124,7 +124,6 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
placeholder={name}
autoFocus={index === 0}
maxLength={max_length || DEFAULT_VALUE_MAX_LEN}
readOnly={readonly}
/>
)}
{type === 'paragraph' && (
@@ -133,7 +132,6 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
placeholder={name}
value={inputs[key] ? `${inputs[key]}` : ''}
onChange={(e) => { handleInputValueChange(key, e.target.value) }}
readOnly={readonly}
/>
)}
{type === 'select' && (
@@ -144,7 +142,6 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
items={(options || []).map(i => ({ name: i, value: i }))}
allowSearch={false}
bgClassName='bg-gray-50'
disabled={readonly}
/>
)}
{type === 'number' && (
@@ -155,7 +152,6 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
placeholder={name}
autoFocus={index === 0}
maxLength={max_length || DEFAULT_VALUE_MAX_LEN}
readOnly={readonly}
/>
)}
{type === 'checkbox' && (
@@ -164,7 +160,6 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
value={!!inputs[key]}
required={required}
onChange={(value) => { handleInputValueChange(key, value) }}
readonly={readonly}
/>
)}
</div>
@@ -183,7 +178,6 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
url: fileItem.url,
upload_file_id: fileItem.fileId,
})))}
disabled={readonly}
/>
</div>
</div>
@@ -192,12 +186,12 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
)}
{!userInputFieldCollapse && (
<div className='flex justify-between border-t border-divider-subtle p-4 pt-3'>
<Button className='w-[72px]' disabled={readonly} onClick={onClear}>{t('common.operation.clear')}</Button>
<Button className='w-[72px]' onClick={onClear}>{t('common.operation.clear')}</Button>
{canNotRun && (
<Tooltip popupContent={t('appDebug.otherError.promptNoBeEmpty')}>
<Button
variant="primary"
disabled={canNotRun || readonly}
disabled={canNotRun}
onClick={() => onSend?.()}
className="w-[96px]">
<RiPlayLargeFill className="mr-0.5 h-4 w-4 shrink-0" aria-hidden="true" />
@@ -208,7 +202,7 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
{!canNotRun && (
<Button
variant="primary"
disabled={canNotRun || readonly}
disabled={canNotRun}
onClick={() => onSend?.()}
className="w-[96px]">
<RiPlayLargeFill className="mr-0.5 h-4 w-4 shrink-0" aria-hidden="true" />
@@ -222,10 +216,7 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
<FeatureBar
showFileUpload={false}
isChatMode={appType !== AppModeEnum.COMPLETION}
onFeatureBarClick={setShowAppConfigureFeaturesModal}
disabled={readonly}
hideEditEntrance={readonly}
/>
onFeatureBarClick={setShowAppConfigureFeaturesModal} />
</div>
</>
)

View File

@@ -6,11 +6,6 @@ import Button from '@/app/components/base/button'
import cn from '@/utils/classnames'
import type { App } from '@/models/explore'
import AppIcon from '@/app/components/base/app-icon'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { RiInformation2Line } from '@remixicon/react'
import { useCallback } from 'react'
import AppListContext from '@/context/app-list-context'
import { useContextSelector } from 'use-context-selector'
export type AppCardProps = {
app: App
@@ -24,14 +19,6 @@ const AppCard = ({
}: AppCardProps) => {
const { t } = useTranslation()
const { app: appBasicInfo } = app
const { systemFeatures } = useGlobalPublicStore()
const isTrialApp = app.can_trial && systemFeatures.enable_trial_app
const setShowTryAppPanel = useContextSelector(AppListContext, ctx => ctx.setShowTryAppPanel)
const showTryAPPPanel = useCallback((appId: string) => {
return () => {
setShowTryAppPanel?.(true, { appId, app })
}
}, [setShowTryAppPanel, app.category])
return (
<div className={cn('group relative flex h-[132px] cursor-pointer flex-col overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg p-4 shadow-xs hover:shadow-lg')}>
<div className='flex shrink-0 grow-0 items-center gap-3 pb-2'>
@@ -59,17 +46,11 @@ const AppCard = ({
</div>
</div>
<div className={cn('absolute bottom-0 left-0 right-0 hidden bg-gradient-to-t from-components-panel-gradient-2 from-[60.27%] to-transparent p-4 pt-8 group-hover:flex')}>
<div className={cn('w-full', isTrialApp && 'grid grid-cols-2 gap-2')}>
<Button variant='primary' className='w-full' onClick={() => onCreate()}>
<div className={cn('flex h-8 w-full items-center space-x-2')}>
<Button variant='primary' className='grow' onClick={() => onCreate()}>
<PlusIcon className='mr-1 h-4 w-4' />
<span className='text-xs'>{t('app.newApp.useTemplate')}</span>
</Button>
{isTrialApp && (
<Button className='w-full' onClick={showTryAPPPanel(app.app_id)}>
<RiInformation2Line className='mr-1 size-4' />
<span>{t('explore.appCard.try')}</span>
</Button>
)}
</div>
</div>
</div>

View File

@@ -43,7 +43,6 @@ import cn from '@/utils/classnames'
import { noop } from 'lodash-es'
import PromptLogModal from '../../base/prompt-log-modal'
import { WorkflowContextProvider } from '@/app/components/workflow/context'
import { AppSourceType } from '@/service/share'
type AppStoreState = ReturnType<typeof useAppStore.getState>
type ConversationListItem = ChatConversationGeneralDetail | CompletionConversationGeneralDetail
@@ -690,12 +689,12 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
}}></div>
</div>
<TextGeneration
appSourceType={AppSourceType.webApp}
className='mt-2'
content={detail.message.answer}
messageId={detail.message.id}
isError={false}
onRetry={noop}
isInstalledApp={false}
supportFeedback
feedback={detail.message.feedbacks.find((item: any) => item.from_source === 'admin')}
onFeedback={feedback => onFeedback(detail.message.id, feedback)}

View File

@@ -51,6 +51,8 @@ export type IAppCardProps = {
isInPanel?: boolean
cardType?: 'api' | 'webapp'
customBgColor?: string
triggerModeDisabled?: boolean // true when Trigger Node mode needs UI locked to avoid conflicting actions
triggerModeMessage?: React.ReactNode // contextual copy explaining why the card is disabled in trigger mode
onChangeStatus: (val: boolean) => Promise<void>
onSaveSiteConfig?: (params: ConfigParams) => Promise<void>
onGenerateCode?: () => Promise<void>
@@ -61,6 +63,8 @@ function AppCard({
isInPanel,
cardType = 'webapp',
customBgColor,
triggerModeDisabled = false,
triggerModeMessage = '',
onChangeStatus,
onSaveSiteConfig,
onGenerateCode,
@@ -111,7 +115,7 @@ function AppCard({
const hasStartNode = currentWorkflow?.graph?.nodes?.some(node => node.data.type === BlockEnum.Start)
const missingStartNode = isWorkflowApp && !hasStartNode
const hasInsufficientPermissions = isApp ? !isCurrentWorkspaceEditor : !isCurrentWorkspaceManager
const toggleDisabled = hasInsufficientPermissions || appUnpublished || missingStartNode
const toggleDisabled = hasInsufficientPermissions || appUnpublished || missingStartNode || triggerModeDisabled
const runningStatus = (appUnpublished || missingStartNode) ? false : (isApp ? appInfo.enable_site : appInfo.enable_api)
const isMinimalState = appUnpublished || missingStartNode
const { app_base_url, access_token } = appInfo.site ?? {}
@@ -189,7 +193,20 @@ function AppCard({
className={
`${isInPanel ? 'border-l-[0.5px] border-t' : 'border-[0.5px] shadow-xs'} w-full max-w-full rounded-xl border-effects-highlight ${className ?? ''} ${isMinimalState ? 'h-12' : ''}`}
>
<div className={`${customBgColor ?? 'bg-background-default'} rounded-xl`}>
<div className={`${customBgColor ?? 'bg-background-default'} relative rounded-xl ${triggerModeDisabled ? 'opacity-60' : ''}`}>
{triggerModeDisabled && (
triggerModeMessage
? (
<Tooltip
popupContent={triggerModeMessage}
popupClassName="max-w-64 rounded-xl bg-components-panel-bg px-3 py-2 text-xs text-text-secondary shadow-lg"
position="right"
>
<div className='absolute inset-0 z-10 cursor-not-allowed rounded-xl' aria-hidden="true"></div>
</Tooltip>
)
: <div className='absolute inset-0 z-10 cursor-not-allowed rounded-xl' aria-hidden="true"></div>
)}
<div className={`flex w-full flex-col items-start justify-center gap-3 self-stretch p-3 ${isMinimalState ? 'border-0' : 'border-b-[0.5px] border-divider-subtle'}`}>
<div className='flex w-full items-center gap-3 self-stretch'>
<AppBasic
@@ -214,18 +231,23 @@ function AppCard({
</div>
<Tooltip
popupContent={
toggleDisabled && (appUnpublished || missingStartNode) ? (
<>
<div className="mb-1 text-xs font-normal text-text-secondary">
{t('appOverview.overview.appInfo.enableTooltip.description')}
</div>
<div
className="cursor-pointer text-xs font-normal text-text-accent hover:underline"
onClick={() => window.open(docLink('/guides/workflow/node/user-input'), '_blank')}
>
{t('appOverview.overview.appInfo.enableTooltip.learnMore')}
</div>
</>
toggleDisabled ? (
triggerModeDisabled && triggerModeMessage
? triggerModeMessage
: (appUnpublished || missingStartNode) ? (
<>
<div className="mb-1 text-xs font-normal text-text-secondary">
{t('appOverview.overview.appInfo.enableTooltip.description')}
</div>
<div
className="cursor-pointer text-xs font-normal text-text-accent hover:underline"
onClick={() => window.open(docLink('/guides/workflow/node/user-input'), '_blank')}
>
{t('appOverview.overview.appInfo.enableTooltip.learnMore')}
</div>
</>
)
: ''
) : ''
}
position="right"
@@ -329,9 +351,11 @@ function AppCard({
{!isApp && <SecretKeyButton appId={appInfo.id} />}
{OPERATIONS_MAP[cardType].map((op) => {
const disabled
= op.opName === t('appOverview.overview.appInfo.settings.entry')
? false
: !runningStatus
= triggerModeDisabled
? true
: op.opName === t('appOverview.overview.appInfo.settings.entry')
? false
: !runningStatus
return (
<Button
className="mr-1 min-w-[88px]"

View File

@@ -21,7 +21,7 @@ import { Markdown } from '@/app/components/base/markdown'
import Loading from '@/app/components/base/loading'
import Toast from '@/app/components/base/toast'
import type { FeedbackType } from '@/app/components/base/chat/chat/type'
import { AppSourceType, fetchMoreLikeThis, updateFeedback } from '@/service/share'
import { fetchMoreLikeThis, updateFeedback } from '@/service/share'
import { fetchTextGenerationMessage } from '@/service/debug'
import { useStore as useAppStore } from '@/app/components/app/store'
import WorkflowProcessItem from '@/app/components/base/chat/chat/answer/workflow-process'
@@ -52,7 +52,7 @@ export type IGenerationItemProps = {
onFeedback?: (feedback: FeedbackType) => void
onSave?: (messageId: string) => void
isMobile?: boolean
appSourceType: AppSourceType
isInstalledApp: boolean
installedAppId?: string
taskId?: string
controlClearMoreLikeThis?: number
@@ -86,7 +86,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
onSave,
depth = 1,
isMobile,
appSourceType,
isInstalledApp,
installedAppId,
taskId,
controlClearMoreLikeThis,
@@ -99,7 +99,6 @@ const GenerationItem: FC<IGenerationItemProps> = ({
const { t } = useTranslation()
const params = useParams()
const isTop = depth === 1
const isTryApp = appSourceType === AppSourceType.tryApp
const [completionRes, setCompletionRes] = useState('')
const [childMessageId, setChildMessageId] = useState<string | null>(null)
const [childFeedback, setChildFeedback] = useState<FeedbackType>({
@@ -113,7 +112,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
const setShowPromptLogModal = useAppStore(s => s.setShowPromptLogModal)
const handleFeedback = async (childFeedback: FeedbackType) => {
await updateFeedback({ url: `/messages/${childMessageId}/feedbacks`, body: { rating: childFeedback.rating } }, appSourceType, installedAppId)
await updateFeedback({ url: `/messages/${childMessageId}/feedbacks`, body: { rating: childFeedback.rating } }, isInstalledApp, installedAppId)
setChildFeedback(childFeedback)
}
@@ -131,7 +130,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
onSave,
isShowTextToSpeech,
isMobile,
appSourceType,
isInstalledApp,
installedAppId,
controlClearMoreLikeThis,
isWorkflow,
@@ -145,7 +144,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
return
}
startQuerying()
const res: any = await fetchMoreLikeThis(messageId as string, appSourceType, installedAppId)
const res: any = await fetchMoreLikeThis(messageId as string, isInstalledApp, installedAppId)
setCompletionRes(res.answer)
setChildFeedback({
rating: null,
@@ -293,7 +292,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
{!isWorkflow && <span>{content?.length} {t('common.unit.char')}</span>}
{/* action buttons */}
<div className='absolute bottom-1 right-2 flex items-center'>
{!isInWebApp && (appSourceType !== AppSourceType.installedApp) && !isResponding && (
{!isInWebApp && !isInstalledApp && !isResponding && (
<div className='ml-1 flex items-center gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-sm'>
<ActionButton disabled={isError || !messageId} onClick={handleOpenLogModal}>
<RiFileList3Line className='h-4 w-4' />
@@ -302,12 +301,12 @@ const GenerationItem: FC<IGenerationItemProps> = ({
</div>
)}
<div className='ml-1 flex items-center gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-sm'>
{moreLikeThis && !isTryApp && (
{moreLikeThis && (
<ActionButton state={depth === MAX_DEPTH ? ActionButtonState.Disabled : ActionButtonState.Default} disabled={depth === MAX_DEPTH} onClick={handleMoreLikeThis}>
<RiSparklingLine className='h-4 w-4' />
</ActionButton>
)}
{isShowTextToSpeech && !isTryApp && (
{isShowTextToSpeech && (
<NewAudioButton
id={messageId!}
voice={config?.text_to_speech?.voice}
@@ -330,13 +329,13 @@ const GenerationItem: FC<IGenerationItemProps> = ({
<RiReplay15Line className='h-4 w-4' />
</ActionButton>
)}
{isInWebApp && !isWorkflow && !isTryApp && (
{isInWebApp && !isWorkflow && (
<ActionButton disabled={isError || !messageId} onClick={() => { onSave?.(messageId as string) }}>
<RiBookmark3Line className='h-4 w-4' />
</ActionButton>
)}
</div>
{(supportFeedback || isInWebApp) && !isWorkflow && !isTryApp && !isError && messageId && (
{(supportFeedback || isInWebApp) && !isWorkflow && !isError && messageId && (
<div className='ml-1 flex items-center gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-sm'>
{!feedback?.rating && (
<>

View File

@@ -3,16 +3,6 @@ import { useEducationInit } from '@/app/education-apply/hooks'
import List from './list'
import useDocumentTitle from '@/hooks/use-document-title'
import { useTranslation } from 'react-i18next'
import AppListContext from '@/context/app-list-context'
import { useCallback, useState } from 'react'
import type { CurrentTryAppParams } from '@/context/explore-context'
import TryApp from '../explore/try-app'
import type { CreateAppModalProps } from '../explore/create-app-modal'
import CreateAppModal from '../explore/create-app-modal'
import { fetchAppDetail } from '@/service/explore'
import { DSLImportMode } from '@/models/app'
import { useImportDSL } from '@/hooks/use-import-dsl'
import DSLConfirmModal from '../app/create-from-dsl-modal/dsl-confirm-modal'
const Apps = () => {
const { t } = useTranslation()
@@ -20,122 +10,10 @@ const Apps = () => {
useDocumentTitle(t('common.menus.apps'))
useEducationInit()
const [currentTryAppParams, setCurrentTryAppParams] = useState<CurrentTryAppParams | undefined>(undefined)
const currApp = currentTryAppParams?.app
const [isShowTryAppPanel, setIsShowTryAppPanel] = useState(false)
const hideTryAppPanel = useCallback(() => {
setIsShowTryAppPanel(false)
}, [])
const setShowTryAppPanel = (showTryAppPanel: boolean, params?: CurrentTryAppParams) => {
if (showTryAppPanel)
setCurrentTryAppParams(params)
else
setCurrentTryAppParams(undefined)
setIsShowTryAppPanel(showTryAppPanel)
}
const [isShowCreateModal, setIsShowCreateModal] = useState(false)
const handleShowFromTryApp = useCallback(() => {
setIsShowCreateModal(true)
}, [])
const [controlRefreshList, setControlRefreshList] = useState(0)
const [controlHideCreateFromTemplatePanel, setControlHideCreateFromTemplatePanel] = useState(0)
const onSuccess = useCallback(() => {
setControlRefreshList(prev => prev + 1)
setControlHideCreateFromTemplatePanel(prev => prev + 1)
}, [])
const [showDSLConfirmModal, setShowDSLConfirmModal] = useState(false)
const {
handleImportDSL,
handleImportDSLConfirm,
versions,
isFetching,
} = useImportDSL()
const onConfirmDSL = useCallback(async () => {
await handleImportDSLConfirm({
onSuccess,
})
}, [handleImportDSLConfirm, onSuccess])
const onCreate: CreateAppModalProps['onConfirm'] = async ({
name,
icon_type,
icon,
icon_background,
description,
}) => {
hideTryAppPanel()
const { export_data } = await fetchAppDetail(
currApp?.app.id as string,
)
const payload = {
mode: DSLImportMode.YAML_CONTENT,
yaml_content: export_data,
name,
icon_type,
icon,
icon_background,
description,
}
await handleImportDSL(payload, {
onSuccess: () => {
setIsShowCreateModal(false)
},
onPending: () => {
setShowDSLConfirmModal(true)
},
})
}
return (
<AppListContext.Provider value={{
currentApp: currentTryAppParams,
isShowTryAppPanel,
setShowTryAppPanel,
controlHideCreateFromTemplatePanel,
}}>
<div className='relative flex h-0 shrink-0 grow flex-col overflow-y-auto bg-background-body'>
<List controlRefreshList={controlRefreshList} />
{isShowTryAppPanel && (
<TryApp appId={currentTryAppParams?.appId || ''}
category={currentTryAppParams?.app?.category}
onClose={hideTryAppPanel}
onCreate={handleShowFromTryApp}
/>
)}
{
showDSLConfirmModal && (
<DSLConfirmModal
versions={versions}
onCancel={() => setShowDSLConfirmModal(false)}
onConfirm={onConfirmDSL}
confirmDisabled={isFetching}
/>
)
}
{isShowCreateModal && (
<CreateAppModal
appIconType={currApp?.app.icon_type || 'emoji'}
appIcon={currApp?.app.icon || ''}
appIconBackground={currApp?.app.icon_background || ''}
appIconUrl={currApp?.app.icon_url}
appName={currApp?.app.name || ''}
appDescription={currApp?.app.description || ''}
show
onConfirm={onCreate}
confirmDisabled={isFetching}
onHide={() => setIsShowCreateModal(false)}
/>
)}
</div >
</AppListContext.Provider>
<div className='relative flex h-0 shrink-0 grow flex-col overflow-y-auto bg-background-body'>
<List />
</div >
)
}

View File

@@ -1,6 +1,5 @@
'use client'
import type { FC } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import {
useRouter,
@@ -68,12 +67,7 @@ const getKey = (
return null
}
type Props = {
controlRefreshList: number
}
const List: FC<Props> = ({
controlRefreshList,
}) => {
const List = () => {
const { t } = useTranslation()
const { systemFeatures } = useGlobalPublicStore()
const router = useRouter()
@@ -119,11 +113,6 @@ const List: FC<Props> = ({
},
)
useEffect(() => {
if (controlRefreshList > 0)
mutate()
}, [controlRefreshList])
const anchorRef = useRef<HTMLDivElement>(null)
const options = [
{ value: 'all', text: t('app.types.all'), icon: <RiApps2Line className='mr-1 h-[14px] w-[14px]' /> },

View File

@@ -1,6 +1,6 @@
'use client'
import React, { useEffect, useMemo, useState } from 'react'
import React, { useMemo, useState } from 'react'
import {
useRouter,
useSearchParams,
@@ -11,8 +11,6 @@ import { useProviderContext } from '@/context/provider-context'
import { FileArrow01, FilePlus01, FilePlus02 } from '@/app/components/base/icons/src/vender/line/files'
import cn from '@/utils/classnames'
import dynamic from 'next/dynamic'
import AppListContext from '@/context/app-list-context'
import { useContextSelector } from 'use-context-selector'
const CreateAppModal = dynamic(() => import('@/app/components/app/create-app-modal'), {
ssr: false,
@@ -54,12 +52,6 @@ const CreateAppCard = ({
return undefined
}, [dslUrl])
const controlHideCreateFromTemplatePanel = useContextSelector(AppListContext, ctx => ctx.controlHideCreateFromTemplatePanel)
useEffect(() => {
if (controlHideCreateFromTemplatePanel > 0)
setShowNewAppTemplateDialog(false)
}, [controlHideCreateFromTemplatePanel])
return (
<div
ref={ref}

View File

@@ -50,14 +50,13 @@ function getActionButtonState(state: ActionButtonState) {
}
}
const ActionButton = ({ className, size, state = ActionButtonState.Default, styleCss, children, ref, disabled, ...props }: ActionButtonProps) => {
const ActionButton = ({ className, size, state = ActionButtonState.Default, styleCss, children, ref, ...props }: ActionButtonProps) => {
return (
<button
type='button'
className={classNames(
actionButtonVariants({ className, size }),
getActionButtonState(state),
disabled && 'cursor-not-allowed text-text-disabled hover:bg-transparent hover:text-text-disabled',
)}
ref={ref}
style={styleCss}

View File

@@ -1,59 +0,0 @@
import {
memo,
} from 'react'
import {
RiCloseLine,
RiInformation2Fill,
} from '@remixicon/react'
import { cva } from 'class-variance-authority'
import cn from '@/utils/classnames'
type Props = {
type?: 'info'
message: string
onHide: () => void
className?: string
}
const bgVariants = cva(
'',
{
variants: {
type: {
info: 'from-components-badge-status-light-normal-halo to-background-gradient-mask-transparent',
},
},
},
)
const Alert: React.FC<Props> = ({
type = 'info',
message,
onHide,
className,
}) => {
return (
<div className={cn('pointer-events-none w-full', className)}>
<div
className='relative flex space-x-1 overflow-hidden rounded-xl border border-components-panel-border bg-components-panel-bg-blur p-3 shadow-lg'
>
<div className={cn('pointer-events-none absolute inset-0 bg-gradient-to-r opacity-[0.4]', bgVariants({ type }))}>
</div>
<div className='flex h-6 w-6 items-center justify-center'>
<RiInformation2Fill className='text-text-accent' />
</div>
<div className='p-1'>
<div className='system-xs-regular text-text-secondary'>
{message}
</div>
</div>
<div
className='pointer-events-auto flex h-6 w-6 cursor-pointer items-center justify-center'
onClick={onHide}
>
<RiCloseLine className='h-4 w-4 text-text-tertiary' />
</div>
</div>
</div>
)
}
export default memo(Alert)

View File

@@ -1,23 +0,0 @@
.appIcon {
@apply flex items-center justify-center relative w-9 h-9 text-lg rounded-lg grow-0 shrink-0;
}
.appIcon.large {
@apply w-10 h-10;
}
.appIcon.small {
@apply w-8 h-8;
}
.appIcon.tiny {
@apply w-6 h-6 text-base;
}
.appIcon.xs {
@apply w-5 h-5 text-base;
}
.appIcon.rounded {
@apply rounded-full;
}

View File

@@ -1,5 +1,5 @@
import Toast from '@/app/components/base/toast'
import { AppSourceType, textToAudioStream } from '@/service/share'
import { textToAudioStream } from '@/service/share'
declare global {
// eslint-disable-next-line ts/consistent-type-definitions
@@ -100,7 +100,7 @@ export default class AudioPlayer {
private async loadAudio() {
try {
const audioResponse: any = await textToAudioStream(this.url, this.isPublic ? AppSourceType.webApp : AppSourceType.installedApp, { content_type: 'audio/mpeg' }, {
const audioResponse: any = await textToAudioStream(this.url, this.isPublic, { content_type: 'audio/mpeg' }, {
message_id: this.msgId,
streaming: true,
voice: this.voice,

View File

@@ -1,225 +0,0 @@
import cn from '@/utils/classnames'
import Autoplay from 'embla-carousel-autoplay'
import useEmblaCarousel, { type UseEmblaCarouselType } from 'embla-carousel-react'
import * as React from 'react'
type CarouselApi = UseEmblaCarouselType[1]
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
type CarouselOptions = UseCarouselParameters[0]
type CarouselPlugin = UseCarouselParameters[1]
type CarouselProps = {
opts?: CarouselOptions;
plugins?: CarouselPlugin;
orientation?: 'horizontal' | 'vertical';
}
type CarouselContextValue = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
api: ReturnType<typeof useEmblaCarousel>[1];
scrollPrev: () => void;
scrollNext: () => void;
selectedIndex: number;
canScrollPrev: boolean;
canScrollNext: boolean;
} & CarouselProps
const CarouselContext = React.createContext<CarouselContextValue | null>(null)
function useCarousel() {
const context = React.useContext(CarouselContext)
if (!context)
throw new Error('useCarousel must be used within a <Carousel />')
return context
}
type TCarousel = {
Content: typeof CarouselContent;
Item: typeof CarouselItem;
Previous: typeof CarouselPrevious;
Next: typeof CarouselNext;
Dot: typeof CarouselDot;
Plugin: typeof CarouselPlugins;
} & React.ForwardRefExoticComponent<
React.HTMLAttributes<HTMLDivElement> & CarouselProps & React.RefAttributes<CarouselContextValue>
>
const Carousel: TCarousel = React.forwardRef(
({ orientation = 'horizontal', opts, plugins, className, children, ...props }, ref) => {
const [carouselRef, api] = useEmblaCarousel(
{ ...opts, axis: orientation === 'horizontal' ? 'x' : 'y' },
plugins,
)
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
const [canScrollNext, setCanScrollNext] = React.useState(false)
const [selectedIndex, setSelectedIndex] = React.useState(0)
const scrollPrev = React.useCallback(() => {
api?.scrollPrev()
}, [api])
const scrollNext = React.useCallback(() => {
api?.scrollNext()
}, [api])
React.useEffect(() => {
if (!api)
return
const onSelect = (api: CarouselApi) => {
if (!api)
return
setSelectedIndex(api.selectedScrollSnap())
setCanScrollPrev(api.canScrollPrev())
setCanScrollNext(api.canScrollNext())
}
onSelect(api)
api.on('reInit', onSelect)
api.on('select', onSelect)
return () => {
api?.off('select', onSelect)
}
}, [api])
React.useImperativeHandle(ref, () => ({
carouselRef,
api,
opts,
orientation,
scrollPrev,
scrollNext,
selectedIndex,
canScrollPrev,
canScrollNext,
}))
return (
<CarouselContext.Provider
value={{
carouselRef,
api,
opts,
orientation,
scrollPrev,
scrollNext,
selectedIndex,
canScrollPrev,
canScrollNext,
}}
>
<div
ref={carouselRef}
// onKeyDownCapture={handleKeyDown}
className={cn('relative overflow-hidden', className)}
role="region"
aria-roledescription="carousel"
{...props}
>
{children}
</div>
</CarouselContext.Provider>
)
},
) as TCarousel
Carousel.displayName = 'Carousel'
const CarouselContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => {
const { orientation } = useCarousel()
return (
<div
ref={ref}
className={cn('flex', orientation === 'vertical' && 'flex-col', className)}
{...props}
/>
)
},
)
CarouselContent.displayName = 'CarouselContent'
const CarouselItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => {
return (
<div
ref={ref}
role="group"
aria-roledescription="slide"
className={cn('min-w-0 shrink-0 grow-0 basis-full', className)}
{...props}
/>
)
},
)
CarouselItem.displayName = 'CarouselItem'
type CarouselActionProps = {
children?: React.ReactNode;
} & Omit<React.HTMLAttributes<HTMLButtonElement>, 'disabled' | 'onClick'>
const CarouselPrevious = React.forwardRef<HTMLButtonElement, CarouselActionProps>(
({ children, ...props }, ref) => {
const { scrollPrev, canScrollPrev } = useCarousel()
return (
<button ref={ref} {...props} disabled={!canScrollPrev} onClick={scrollPrev}>
{children}
</button>
)
},
)
CarouselPrevious.displayName = 'CarouselPrevious'
const CarouselNext = React.forwardRef<HTMLButtonElement, CarouselActionProps>(
({ children, ...props }, ref) => {
const { scrollNext, canScrollNext } = useCarousel()
return (
<button ref={ref} {...props} disabled={!canScrollNext} onClick={scrollNext}>
{children}
</button>
)
},
)
CarouselNext.displayName = 'CarouselNext'
const CarouselDot = React.forwardRef<HTMLButtonElement, CarouselActionProps>(
({ children, ...props }, ref) => {
const { api, selectedIndex } = useCarousel()
return api?.slideNodes().map((_, index) => {
return (
<button
key={index}
ref={ref}
{...props}
data-state={index === selectedIndex ? 'active' : 'inactive'}
onClick={() => {
api.scrollTo(index)
}}
>
{children}
</button>
)
})
},
)
CarouselDot.displayName = 'CarouselDot'
const CarouselPlugins = {
Autoplay,
}
Carousel.Content = CarouselContent
Carousel.Item = CarouselItem
Carousel.Previous = CarouselPrevious
Carousel.Next = CarouselNext
Carousel.Dot = CarouselDot
Carousel.Plugin = CarouselPlugins
export { Carousel, useCarousel }

View File

@@ -12,7 +12,6 @@ import { InputVarType } from '@/app/components/workflow/types'
import { TransferMethod } from '@/types/app'
import InputsForm from '@/app/components/base/chat/chat-with-history/inputs-form'
import {
AppSourceType,
fetchSuggestedQuestions,
getUrl,
stopChatMessageResponding,
@@ -53,11 +52,6 @@ const ChatWrapper = () => {
initUserVariables,
} = useChatWithHistoryContext()
const appSourceType = isInstalledApp ? AppSourceType.installedApp : AppSourceType.webApp
// Semantic variable for better code readability
const isHistoryConversation = !!currentConversationId
const appConfig = useMemo(() => {
const config = appParams || {}
@@ -85,7 +79,7 @@ const ChatWrapper = () => {
inputsForm: inputsForms,
},
appPrevChatTree,
taskId => stopChatMessageResponding('', taskId, appSourceType, appId),
taskId => stopChatMessageResponding('', taskId, isInstalledApp, appId),
clearChatList,
setClearChatList,
)
@@ -144,11 +138,11 @@ const ChatWrapper = () => {
}
handleSend(
getUrl('chat-messages', appSourceType, appId || ''),
getUrl('chat-messages', isInstalledApp, appId || ''),
data,
{
onGetSuggestedQuestions: responseItemId => fetchSuggestedQuestions(responseItemId, appSourceType, appId),
onConversationComplete: isHistoryConversation ? undefined : handleNewConversationCompleted,
onGetSuggestedQuestions: responseItemId => fetchSuggestedQuestions(responseItemId, isInstalledApp, appId),
onConversationComplete: currentConversationId ? undefined : handleNewConversationCompleted,
isPublicAPI: !isInstalledApp,
},
)
@@ -184,13 +178,13 @@ const ChatWrapper = () => {
else {
return <InputsForm collapsed={collapsed} setCollapsed={setCollapsed} />
}
}, [
},
[
inputsForms.length,
isMobile,
currentConversationId,
collapsed, allInputsHidden,
],
)
])
const welcome = useMemo(() => {
const welcomeMessage = chatList.find(item => item.isOpeningStatement)
@@ -237,7 +231,8 @@ const ChatWrapper = () => {
</div>
</div>
)
}, [
},
[
appData?.site.icon,
appData?.site.icon_background,
appData?.site.icon_type,

View File

@@ -20,7 +20,6 @@ import { buildChatItemTree, getProcessedSystemVariablesFromUrlParams, getRawInpu
import { addFileInfos, sortAgentSorts } from '../../../tools/utils'
import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
import {
AppSourceType,
delConversation,
fetchChatList,
fetchConversations,
@@ -71,7 +70,6 @@ function getFormattedChatList(messages: any[]) {
export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
const isInstalledApp = useMemo(() => !!installedAppInfo, [installedAppInfo])
const appSourceType = isInstalledApp ? AppSourceType.installedApp : AppSourceType.webApp
const appInfo = useWebAppStore(s => s.appInfo)
const appParams = useWebAppStore(s => s.appParams)
const appMeta = useWebAppStore(s => s.appMeta)
@@ -178,17 +176,17 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
const { data: appPinnedConversationData, mutate: mutateAppPinnedConversationData } = useSWR(
appId ? ['appConversationData', isInstalledApp, appId, true] : null,
() => fetchConversations(appSourceType, appId, undefined, true, 100),
() => fetchConversations(isInstalledApp, appId, undefined, true, 100),
{ revalidateOnFocus: false, revalidateOnReconnect: false },
)
const { data: appConversationData, isLoading: appConversationDataLoading, mutate: mutateAppConversationData } = useSWR(
appId ? ['appConversationData', isInstalledApp, appId, false] : null,
() => fetchConversations(appSourceType, appId, undefined, false, 100),
() => fetchConversations(isInstalledApp, appId, undefined, false, 100),
{ revalidateOnFocus: false, revalidateOnReconnect: false },
)
const { data: appChatListData, isLoading: appChatListDataLoading } = useSWR(
chatShouldReloadKey ? ['appChatList', chatShouldReloadKey, appSourceType, appId] : null,
() => fetchChatList(chatShouldReloadKey, appSourceType, appId),
chatShouldReloadKey ? ['appChatList', chatShouldReloadKey, isInstalledApp, appId] : null,
() => fetchChatList(chatShouldReloadKey, isInstalledApp, appId),
{ revalidateOnFocus: false, revalidateOnReconnect: false },
)
@@ -311,7 +309,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
handleNewConversationInputsChange(conversationInputs)
}, [handleNewConversationInputsChange, inputsForms])
const { data: newConversation } = useSWR(newConversationId ? [isInstalledApp, appId, newConversationId] : null, () => generationConversationName(appSourceType, appId, newConversationId), { revalidateOnFocus: false })
const { data: newConversation } = useSWR(newConversationId ? [isInstalledApp, appId, newConversationId] : null, () => generationConversationName(isInstalledApp, appId, newConversationId), { revalidateOnFocus: false })
const [originConversationList, setOriginConversationList] = useState<ConversationItem[]>([])
useEffect(() => {
if (appConversationData?.data && !appConversationDataLoading)
@@ -436,16 +434,16 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
}, [mutateAppConversationData, mutateAppPinnedConversationData])
const handlePinConversation = useCallback(async (conversationId: string) => {
await pinConversation(appSourceType, appId, conversationId)
await pinConversation(isInstalledApp, appId, conversationId)
notify({ type: 'success', message: t('common.api.success') })
handleUpdateConversationList()
}, [appSourceType, appId, notify, t, handleUpdateConversationList])
}, [isInstalledApp, appId, notify, t, handleUpdateConversationList])
const handleUnpinConversation = useCallback(async (conversationId: string) => {
await unpinConversation(appSourceType, appId, conversationId)
await unpinConversation(isInstalledApp, appId, conversationId)
notify({ type: 'success', message: t('common.api.success') })
handleUpdateConversationList()
}, [appSourceType, appId, notify, t, handleUpdateConversationList])
}, [isInstalledApp, appId, notify, t, handleUpdateConversationList])
const [conversationDeleting, setConversationDeleting] = useState(false)
const handleDeleteConversation = useCallback(async (
@@ -459,7 +457,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
try {
setConversationDeleting(true)
await delConversation(appSourceType, appId, conversationId)
await delConversation(isInstalledApp, appId, conversationId)
notify({ type: 'success', message: t('common.api.success') })
onSuccess()
}
@@ -494,7 +492,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
setConversationRenaming(true)
try {
await renameConversation(appSourceType, appId, conversationId, newName)
await renameConversation(isInstalledApp, appId, conversationId, newName)
notify({
type: 'success',
@@ -524,9 +522,9 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
}, [mutateAppConversationData, handleConversationIdInfoChange])
const handleFeedback = useCallback(async (messageId: string, feedback: Feedback) => {
await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating, content: feedback.content } }, appSourceType, appId)
await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating, content: feedback.content } }, isInstalledApp, appId)
notify({ type: 'success', message: t('common.api.success') })
}, [appSourceType, appId, t, notify])
}, [isInstalledApp, appId, t, notify])
return {
isInstalledApp,

View File

@@ -150,7 +150,7 @@ const Answer: FC<AnswerProps> = ({
data={workflowProcess}
item={item}
hideProcessDetail={hideProcessDetail}
readonly={hideProcessDetail && appData ? !appData.site?.show_workflow_steps : undefined}
readonly={hideProcessDetail && appData ? !appData.site.show_workflow_steps : undefined}
/>
)
}

View File

@@ -51,7 +51,6 @@ const Operation: FC<OperationProps> = ({
onAnnotationAdded,
onAnnotationEdited,
onAnnotationRemoved,
disableFeedback,
onFeedback,
onRegenerate,
} = useChatContext()
@@ -167,7 +166,7 @@ const Operation: FC<OperationProps> = ({
)}
</div>
)}
{!disableFeedback && !isOpeningStatement && config?.supportFeedback && !localFeedback?.rating && onFeedback && (
{!isOpeningStatement && config?.supportFeedback && !localFeedback?.rating && onFeedback && (
<div className='ml-1 hidden items-center gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-sm group-hover:flex'>
{!localFeedback?.rating && (
<>
@@ -181,7 +180,7 @@ const Operation: FC<OperationProps> = ({
)}
</div>
)}
{!disableFeedback && !isOpeningStatement && config?.supportFeedback && localFeedback?.rating && onFeedback && (
{!isOpeningStatement && config?.supportFeedback && localFeedback?.rating && onFeedback && (
<div className='ml-1 flex items-center gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-sm'>
{localFeedback?.rating === 'like' && (
<ActionButton state={ActionButtonState.Active} onClick={() => handleFeedback(null)}>

View File

@@ -2,7 +2,6 @@ import type { FC } from 'react'
import { memo } from 'react'
import type { ChatItem } from '../../types'
import { useChatContext } from '../context'
import cn from '@/utils/classnames'
type SuggestedQuestionsProps = {
item: ChatItem
@@ -10,7 +9,7 @@ type SuggestedQuestionsProps = {
const SuggestedQuestions: FC<SuggestedQuestionsProps> = ({
item,
}) => {
const { onSend, readonly } = useChatContext()
const { onSend } = useChatContext()
const {
isOpeningStatement,
@@ -25,11 +24,8 @@ const SuggestedQuestions: FC<SuggestedQuestionsProps> = ({
{suggestedQuestions.filter(q => !!q && q.trim()).map((question, index) => (
<div
key={index}
className={cn(
'system-sm-medium mr-1 mt-1 inline-flex max-w-full shrink-0 cursor-pointer flex-wrap rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-3.5 py-2 text-components-button-secondary-accent-text shadow-xs last:mr-0 hover:border-components-button-secondary-border-hover hover:bg-components-button-secondary-bg-hover',
readonly && 'pointer-events-none opacity-50',
)}
onClick={() => !readonly && onSend?.(question)}
className='system-sm-medium mr-1 mt-1 inline-flex max-w-full shrink-0 cursor-pointer flex-wrap rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-3.5 py-2 text-components-button-secondary-accent-text shadow-xs last:mr-0 hover:border-components-button-secondary-border-hover hover:bg-components-button-secondary-bg-hover'
onClick={() => onSend?.(question)}
>
{question}
</div>),

View File

@@ -27,10 +27,8 @@ import { useToastContext } from '@/app/components/base/toast'
import FeatureBar from '@/app/components/base/features/new-feature-panel/feature-bar'
import type { FileUpload } from '@/app/components/base/features/types'
import { TransferMethod } from '@/types/app'
import { noop } from 'lodash-es'
type ChatInputAreaProps = {
readonly?: boolean
botName?: string
showFeatureBar?: boolean
showFileUpload?: boolean
@@ -46,7 +44,6 @@ type ChatInputAreaProps = {
disabled?: boolean
}
const ChatInputArea = ({
readonly,
botName,
showFeatureBar,
showFileUpload,
@@ -171,7 +168,6 @@ const ChatInputArea = ({
const operation = (
<Operation
ref={holdSpaceRef}
readonly={readonly}
fileConfig={visionConfig}
speechToTextConfig={speechToTextConfig}
onShowVoiceInput={handleShowVoiceInput}
@@ -207,7 +203,7 @@ const ChatInputArea = ({
className={cn(
'body-lg-regular w-full resize-none bg-transparent p-1 leading-6 text-text-primary outline-none',
)}
placeholder={readonly ? t('common.chat.inputDisabledPlaceholder') : t('common.chat.inputPlaceholder', { botName }) || ''}
placeholder={t('common.chat.inputPlaceholder', { botName }) || ''}
autoFocus
minRows={1}
value={query}
@@ -220,7 +216,6 @@ const ChatInputArea = ({
onDragLeave={handleDragFileLeave}
onDragOver={handleDragFileOver}
onDrop={handleDropFile}
readOnly={readonly}
/>
</div>
{
@@ -242,12 +237,7 @@ const ChatInputArea = ({
)
}
</div>
{showFeatureBar && <FeatureBar
showFileUpload={showFileUpload}
disabled={featureBarDisabled}
onFeatureBarClick={readonly ? noop : onFeatureBarClick}
hideEditEntrance={readonly}
/>}
{showFeatureBar && <FeatureBar showFileUpload={showFileUpload} disabled={featureBarDisabled} onFeatureBarClick={onFeatureBarClick} />}
</>
)
}

View File

@@ -13,10 +13,8 @@ import ActionButton from '@/app/components/base/action-button'
import { FileUploaderInChatInput } from '@/app/components/base/file-uploader'
import type { FileUpload } from '@/app/components/base/features/types'
import cn from '@/utils/classnames'
import { noop } from 'lodash-es'
type OperationProps = {
readonly?: boolean
fileConfig?: FileUpload
speechToTextConfig?: EnableType
onShowVoiceInput?: () => void
@@ -25,7 +23,6 @@ type OperationProps = {
ref?: Ref<HTMLDivElement>;
}
const Operation: FC<OperationProps> = ({
readonly,
ref,
fileConfig,
speechToTextConfig,
@@ -44,13 +41,12 @@ const Operation: FC<OperationProps> = ({
ref={ref}
>
<div className='flex items-center space-x-1'>
{fileConfig?.enabled && <FileUploaderInChatInput readonly={readonly} fileConfig={fileConfig} />}
{fileConfig?.enabled && <FileUploaderInChatInput fileConfig={fileConfig} />}
{
speechToTextConfig?.enabled && (
<ActionButton
size='l'
disabled={readonly}
onClick={readonly ? noop : onShowVoiceInput}
onClick={onShowVoiceInput}
>
<RiMicLine className='h-5 w-5' />
</ActionButton>
@@ -60,8 +56,7 @@ const Operation: FC<OperationProps> = ({
<Button
className='ml-3 w-8 px-0'
variant='primary'
onClick={readonly ? noop : onSend}
disabled={readonly}
onClick={onSend}
style={
theme
? {

View File

@@ -15,15 +15,11 @@ export type ChatContextValue = Pick<ChatProps, 'config'
| 'onAnnotationEdited'
| 'onAnnotationAdded'
| 'onAnnotationRemoved'
| 'disableFeedback'
| 'onFeedback'
> & {
readonly?: boolean
}
>
const ChatContext = createContext<ChatContextValue>({
chatList: [],
readonly: false,
})
type ChatContextProviderProps = {
@@ -32,7 +28,6 @@ type ChatContextProviderProps = {
export const ChatContextProvider = ({
children,
readonly = false,
config,
isResponding,
chatList,
@@ -44,13 +39,11 @@ export const ChatContextProvider = ({
onAnnotationEdited,
onAnnotationAdded,
onAnnotationRemoved,
disableFeedback,
onFeedback,
}: ChatContextProviderProps) => {
return (
<ChatContext.Provider value={{
config,
readonly,
isResponding,
chatList: chatList || [],
showPromptLog,
@@ -61,7 +54,6 @@ export const ChatContextProvider = ({
onAnnotationEdited,
onAnnotationAdded,
onAnnotationRemoved,
disableFeedback,
onFeedback,
}}>
{children}

View File

@@ -36,8 +36,6 @@ import { useStore as useAppStore } from '@/app/components/app/store'
import type { AppData } from '@/models/share'
export type ChatProps = {
isTryApp?: boolean
readonly?: boolean
appData?: AppData
chatList: ChatItem[]
config?: ChatConfig
@@ -62,7 +60,6 @@ export type ChatProps = {
onAnnotationAdded?: (annotationId: string, authorName: string, question: string, answer: string, index: number) => void
onAnnotationRemoved?: (index: number) => void
chatNode?: ReactNode
disableFeedback?: boolean
onFeedback?: (messageId: string, feedback: Feedback) => void
chatAnswerContainerInner?: string
hideProcessDetail?: boolean
@@ -79,8 +76,6 @@ export type ChatProps = {
}
const Chat: FC<ChatProps> = ({
isTryApp,
readonly = false,
appData,
config,
onSend,
@@ -104,7 +99,6 @@ const Chat: FC<ChatProps> = ({
onAnnotationEdited,
onAnnotationRemoved,
chatNode,
disableFeedback,
onFeedback,
chatAnswerContainerInner,
hideProcessDetail,
@@ -225,7 +219,6 @@ const Chat: FC<ChatProps> = ({
return (
<ChatContextProvider
readonly={readonly}
config={config}
chatList={chatList}
isResponding={isResponding}
@@ -237,18 +230,17 @@ const Chat: FC<ChatProps> = ({
onAnnotationAdded={onAnnotationAdded}
onAnnotationEdited={onAnnotationEdited}
onAnnotationRemoved={onAnnotationRemoved}
disableFeedback={disableFeedback}
onFeedback={onFeedback}
>
<div className={cn('relative h-full', isTryApp && 'flex flex-col')}>
<div className='relative h-full'>
<div
ref={chatContainerRef}
className={cn('relative h-full overflow-y-auto overflow-x-hidden', isTryApp && 'h-0 grow', chatContainerClassName)}
className={cn('relative h-full overflow-y-auto overflow-x-hidden', chatContainerClassName)}
>
{chatNode}
<div
ref={chatContainerInnerRef}
className={cn('w-full', !noSpacing && 'px-8', chatContainerInnerClassName, isTryApp && 'px-0')}
className={cn('w-full', !noSpacing && 'px-8', chatContainerInnerClassName)}
>
{
chatList.map((item, index) => {
@@ -292,7 +284,7 @@ const Chat: FC<ChatProps> = ({
>
<div
ref={chatFooterInnerRef}
className={cn('relative', chatFooterInnerClassName, isTryApp && 'px-0')}
className={cn('relative', chatFooterInnerClassName)}
>
{
!noStopResponding && isResponding && (
@@ -316,7 +308,7 @@ const Chat: FC<ChatProps> = ({
{
!noChatInput && (
<ChatInputArea
botName={appData?.site?.title || 'Bot'}
botName={appData?.site.title || 'Bot'}
disabled={inputDisabled}
showFeatureBar={showFeatureBar}
showFileUpload={showFileUpload}
@@ -329,7 +321,6 @@ const Chat: FC<ChatProps> = ({
inputsForm={inputsForm}
theme={themeBuilder?.theme}
isResponding={isResponding}
readonly={readonly}
/>
)
}

View File

@@ -13,7 +13,6 @@ import { InputVarType } from '@/app/components/workflow/types'
import { TransferMethod } from '@/types/app'
import InputsForm from '@/app/components/base/chat/embedded-chatbot/inputs-form'
import {
AppSourceType,
fetchSuggestedQuestions,
getUrl,
stopChatMessageResponding,
@@ -43,7 +42,6 @@ const ChatWrapper = () => {
isInstalledApp,
appId,
appMeta,
disableFeedback,
handleFeedback,
currentChatInstanceRef,
themeBuilder,
@@ -52,9 +50,7 @@ const ChatWrapper = () => {
setIsResponding,
allInputsHidden,
initUserVariables,
appSourceType,
} = useEmbeddedChatbotContext()
const appConfig = useMemo(() => {
const config = appParams || {}
@@ -82,7 +78,7 @@ const ChatWrapper = () => {
inputsForm: inputsForms,
},
appPrevChatList,
taskId => stopChatMessageResponding('', taskId, appSourceType, appId),
taskId => stopChatMessageResponding('', taskId, isInstalledApp, appId),
clearChatList,
setClearChatList,
)
@@ -138,13 +134,14 @@ const ChatWrapper = () => {
conversation_id: currentConversationId,
parent_message_id: (isRegenerate ? parentAnswer?.id : getLastAnswer(chatList)?.id) || null,
}
handleSend(
getUrl('chat-messages', appSourceType, appId || ''),
getUrl('chat-messages', isInstalledApp, appId || ''),
data,
{
onGetSuggestedQuestions: responseItemId => fetchSuggestedQuestions(responseItemId, appSourceType, appId),
onGetSuggestedQuestions: responseItemId => fetchSuggestedQuestions(responseItemId, isInstalledApp, appId),
onConversationComplete: currentConversationId ? undefined : handleNewConversationCompleted,
isPublicAPI: appSourceType === AppSourceType.webApp,
isPublicAPI: !isInstalledApp,
},
)
}, [currentConversationId, currentConversationInputs, newConversationInputs, chatList, handleSend, isInstalledApp, appId, handleNewConversationCompleted])
@@ -166,8 +163,7 @@ const ChatWrapper = () => {
return chatList.filter(item => !item.isOpeningStatement)
}, [chatList, currentConversationId])
const isTryApp = appSourceType === AppSourceType.tryApp
const [collapsed, setCollapsed] = useState(!!currentConversationId && !isTryApp) // try app always use the new chat
const [collapsed, setCollapsed] = useState(!!currentConversationId)
const chatNode = useMemo(() => {
if (allInputsHidden || !inputsForms.length)
@@ -192,8 +188,6 @@ const ChatWrapper = () => {
return null
if (!collapsed && inputsForms.length > 0 && !allInputsHidden)
return null
if (!appData?.site)
return null
if (welcomeMessage.suggestedQuestions && welcomeMessage.suggestedQuestions?.length > 0) {
return (
<div className={cn('flex items-center justify-center px-4 py-12', isMobile ? 'min-h-[30vh] py-0' : 'h-[50vh]')}>
@@ -227,7 +221,7 @@ const ChatWrapper = () => {
</div>
</div>
)
}, [appData?.site, chatList, collapsed, currentConversationId, inputsForms.length, respondingState, allInputsHidden])
}, [appData?.site.icon, appData?.site.icon_background, appData?.site.icon_type, appData?.site.icon_url, chatList, collapsed, currentConversationId, inputsForms.length, respondingState, allInputsHidden])
const answerIcon = isDify()
? <LogoAvatar className='relative shrink-0' />
@@ -242,7 +236,6 @@ const ChatWrapper = () => {
return (
<Chat
isTryApp={isTryApp}
appData={appData || undefined}
config={appConfig}
chatList={messageList}
@@ -262,7 +255,6 @@ const ChatWrapper = () => {
</>
}
allToolIcons={appMeta?.tool_icons || {}}
disableFeedback={disableFeedback}
onFeedback={handleFeedback}
suggestedQuestions={suggestedQuestions}
answerIcon={answerIcon}

View File

@@ -15,7 +15,6 @@ import type {
ConversationItem,
} from '@/models/share'
import { noop } from 'lodash-es'
import { AppSourceType } from '@/service/share'
export type EmbeddedChatbotContextValue = {
appMeta: AppMeta | null
@@ -38,10 +37,8 @@ export type EmbeddedChatbotContextValue = {
chatShouldReloadKey: string
isMobile: boolean
isInstalledApp: boolean
appSourceType: AppSourceType
allowResetChat: boolean
appId?: string
disableFeedback?: boolean
handleFeedback: (messageId: string, feedback: Feedback) => void
currentChatInstanceRef: RefObject<{ handleStop: () => void }>
themeBuilder?: ThemeBuilder
@@ -77,7 +74,6 @@ export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>
handleNewConversationCompleted: noop,
chatShouldReloadKey: '',
isMobile: false,
appSourceType: AppSourceType.webApp,
isInstalledApp: false,
allowResetChat: true,
handleFeedback: noop,

View File

@@ -18,15 +18,12 @@ import { CONVERSATION_ID_INFO } from '../constants'
import { buildChatItemTree, getProcessedInputsFromUrlParams, getProcessedSystemVariablesFromUrlParams, getProcessedUserVariablesFromUrlParams } from '../utils'
import { getProcessedFilesFromResponse } from '../../file-uploader/utils'
import {
AppSourceType,
fetchChatList,
fetchConversations,
generationConversationName,
updateFeedback,
} from '@/service/share'
import type {
AppData,
// AppData,
ConversationItem,
} from '@/models/share'
@@ -37,7 +34,6 @@ import { TransferMethod } from '@/types/app'
import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils'
import { noop } from 'lodash-es'
import { useWebAppStore } from '@/context/web-app-context'
import { useGetTryAppInfo, useGetTryAppParams } from '@/service/use-try-app'
function getFormattedChatList(messages: any[]) {
const newChatList: ChatItem[] = []
@@ -65,35 +61,18 @@ function getFormattedChatList(messages: any[]) {
return newChatList
}
export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: string) => {
const isInstalledApp = false // just can be webapp and try app
const isTryApp = appSourceType === AppSourceType.tryApp
const { data: tryAppInfo } = useGetTryAppInfo(isTryApp ? tryAppId! : '')
const webAppInfo = useWebAppStore(s => s.appInfo)
const appInfo = isTryApp ? tryAppInfo : webAppInfo
export const useEmbeddedChatbot = () => {
const isInstalledApp = false
const appInfo = useWebAppStore(s => s.appInfo)
const appMeta = useWebAppStore(s => s.appMeta)
const { data: tryAppParams } = useGetTryAppParams(isTryApp ? tryAppId! : '')
const webAppParams = useWebAppStore(s => s.appParams)
const appParams = isTryApp ? tryAppParams : webAppParams
const appId = useMemo(() => {
return isTryApp ? tryAppId : (appInfo as any)?.app_id
}, [appInfo])
const appParams = useWebAppStore(s => s.appParams)
const embeddedConversationId = useWebAppStore(s => s.embeddedConversationId)
const embeddedUserId = useWebAppStore(s => s.embeddedUserId)
const appId = useMemo(() => appInfo?.app_id, [appInfo])
const [userId, setUserId] = useState<string>()
const [conversationId, setConversationId] = useState<string>()
useEffect(() => {
if (isTryApp) return
getProcessedSystemVariablesFromUrlParams().then(({ user_id, conversation_id }) => {
setUserId(user_id)
setConversationId(conversation_id)
})
}, [])
useEffect(() => {
setUserId(embeddedUserId || undefined)
}, [embeddedUserId])
@@ -103,7 +82,6 @@ export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: stri
}, [embeddedConversationId])
useEffect(() => {
if (isTryApp) return
const setLanguageFromParams = async () => {
// Check URL parameters for language override
const urlParams = new URLSearchParams(window.location.search)
@@ -121,9 +99,9 @@ export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: stri
// If locale is set as a system variable, use that
await changeLanguage(localeFromSysVar)
}
else if ((appInfo as unknown as AppData)?.site?.default_language) {
else if (appInfo?.site.default_language) {
// Otherwise use the default from app config
await changeLanguage((appInfo as unknown as AppData).site?.default_language)
await changeLanguage(appInfo.site.default_language)
}
}
@@ -133,13 +111,6 @@ export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: stri
const [conversationIdInfo, setConversationIdInfo] = useLocalStorageState<Record<string, Record<string, string>>>(CONVERSATION_ID_INFO, {
defaultValue: {},
})
const removeConversationIdInfo = useCallback((appId: string) => {
setConversationIdInfo((prev) => {
const newInfo = { ...prev }
delete newInfo[appId]
return newInfo
})
}, [setConversationIdInfo])
const allowResetChat = !conversationId
const currentConversationId = useMemo(() => conversationIdInfo?.[appId || '']?.[userId || 'DEFAULT'] || conversationId || '',
[appId, conversationIdInfo, userId, conversationId])
@@ -165,12 +136,10 @@ export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: stri
return currentConversationId
}, [currentConversationId, newConversationId])
// todo app params
// const { data: appParams } = useSWR(['appParams', appSourceType, appId], () => fetchAppParams(appSourceType, appId))
// const { data: appMeta } = useSWR(isTryApp ? null : ['appMeta', appSourceType, appId], () => fetchAppMeta(appSourceType, appId))
const { data: appPinnedConversationData } = useSWR(isTryApp ? null : ['appConversationData', appSourceType, appId, true], () => fetchConversations(appSourceType, appId, undefined, true, 100))
const { data: appConversationData, isLoading: appConversationDataLoading, mutate: mutateAppConversationData } = useSWR(isTryApp ? null : ['appConversationData', appSourceType, appId, false], () => fetchConversations(appSourceType, appId, undefined, false, 100))
const { data: appChatListData, isLoading: appChatListDataLoading } = useSWR((chatShouldReloadKey && !isTryApp) ? ['appChatList', chatShouldReloadKey, appSourceType, appId] : null, () => fetchChatList(chatShouldReloadKey, appSourceType, appId))
const { data: appPinnedConversationData } = useSWR(['appConversationData', isInstalledApp, appId, true], () => fetchConversations(isInstalledApp, appId, undefined, true, 100))
const { data: appConversationData, isLoading: appConversationDataLoading, mutate: mutateAppConversationData } = useSWR(['appConversationData', isInstalledApp, appId, false], () => fetchConversations(isInstalledApp, appId, undefined, false, 100))
const { data: appChatListData, isLoading: appChatListDataLoading } = useSWR(chatShouldReloadKey ? ['appChatList', chatShouldReloadKey, isInstalledApp, appId] : null, () => fetchChatList(chatShouldReloadKey, isInstalledApp, appId))
const [clearChatList, setClearChatList] = useState(false)
const [isResponding, setIsResponding] = useState(false)
@@ -275,8 +244,6 @@ export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: stri
useEffect(() => {
// init inputs from url params
(async () => {
if (isTryApp)
return
const inputs = await getProcessedInputsFromUrlParams()
const userVariables = await getProcessedUserVariablesFromUrlParams()
setInitInputs(inputs)
@@ -292,7 +259,7 @@ export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: stri
handleNewConversationInputsChange(conversationInputs)
}, [handleNewConversationInputsChange, inputsForms])
const { data: newConversation } = useSWR((!isTryApp && newConversationId) ? [isInstalledApp, appId, newConversationId] : null, () => generationConversationName(appSourceType, appId, newConversationId), { revalidateOnFocus: false })
const { data: newConversation } = useSWR(newConversationId ? [isInstalledApp, appId, newConversationId] : null, () => generationConversationName(isInstalledApp, appId, newConversationId), { revalidateOnFocus: false })
const [originConversationList, setOriginConversationList] = useState<ConversationItem[]>([])
useEffect(() => {
if (appConversationData?.data && !appConversationDataLoading)
@@ -341,7 +308,7 @@ export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: stri
}, [appChatListData, currentConversationId])
const [currentConversationInputs, setCurrentConversationInputs] = useState<Record<string, any>>(currentConversationLatestInputs || {})
useEffect(() => {
if (currentConversationItem && !isTryApp)
if (currentConversationItem)
setCurrentConversationInputs(currentConversationLatestInputs || {})
}, [currentConversationItem, currentConversationLatestInputs])
@@ -401,17 +368,12 @@ export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: stri
setClearChatList(false)
}, [handleConversationIdInfoChange, setClearChatList])
const handleNewConversation = useCallback(async () => {
if (isTryApp) {
setClearChatList(true)
return
}
currentChatInstanceRef.current.handleStop()
setShowNewConversationItemInList(true)
handleChangeConversation('')
handleNewConversationInputsChange(await getProcessedInputsFromUrlParams())
setClearChatList(true)
}, [isTryApp, setShowNewConversationItemInList, handleNewConversationInputsChange, setClearChatList])
}, [handleChangeConversation, setShowNewConversationItemInList, handleNewConversationInputsChange, setClearChatList])
const handleNewConversationCompleted = useCallback((newConversationId: string) => {
setNewConversationId(newConversationId)
@@ -421,18 +383,16 @@ export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: stri
}, [mutateAppConversationData, handleConversationIdInfoChange])
const handleFeedback = useCallback(async (messageId: string, feedback: Feedback) => {
await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating, content: feedback.content } }, appSourceType, appId)
await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating, content: feedback.content } }, isInstalledApp, appId)
notify({ type: 'success', message: t('common.api.success') })
}, [appSourceType, appId, t, notify])
}, [isInstalledApp, appId, t, notify])
return {
appSourceType,
isInstalledApp,
allowResetChat,
appId,
currentConversationId,
currentConversationItem,
removeConversationIdInfo,
handleConversationIdInfoChange,
appData: appInfo,
appParams: appParams || {} as ChatConfig,

View File

@@ -20,8 +20,6 @@ import DifyLogo from '@/app/components/base/logo/dify-logo'
import cn from '@/utils/classnames'
import useDocumentTitle from '@/hooks/use-document-title'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { AppSourceType } from '@/service/share'
import type { AppData } from '@/models/share'
const Chatbot = () => {
const {
@@ -133,11 +131,10 @@ const EmbeddedChatbotWrapper = () => {
setCurrentConversationInputs,
allInputsHidden,
initUserVariables,
} = useEmbeddedChatbot(AppSourceType.webApp)
} = useEmbeddedChatbot()
return <EmbeddedChatbotContext.Provider value={{
appSourceType: AppSourceType.webApp,
appData: appData as AppData,
appData,
appParams,
appMeta,
appChatListDataLoading,

View File

@@ -6,7 +6,6 @@ import Divider from '@/app/components/base/divider'
import InputsFormContent from '@/app/components/base/chat/embedded-chatbot/inputs-form/content'
import { useEmbeddedChatbotContext } from '../context'
import cn from '@/utils/classnames'
import { AppSourceType } from '@/service/share'
type Props = {
collapsed: boolean
@@ -19,7 +18,6 @@ const InputsFormNode = ({
}: Props) => {
const { t } = useTranslation()
const {
appSourceType,
isMobile,
currentConversationId,
themeBuilder,
@@ -27,17 +25,15 @@ const InputsFormNode = ({
allInputsHidden,
inputsForms,
} = useEmbeddedChatbotContext()
const isTryApp = appSourceType === AppSourceType.tryApp
if (allInputsHidden || inputsForms.length === 0)
return null
return (
<div className={cn('mb-6 flex flex-col items-center px-4 pt-6', isMobile && 'mb-4 pt-4', isTryApp && 'mb-0 px-0')}>
<div className={cn('mb-6 flex flex-col items-center px-4 pt-6', isMobile && 'mb-4 pt-4')}>
<div className={cn(
'w-full max-w-[672px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-md',
collapsed && 'border border-components-card-border bg-components-card-bg shadow-none',
isTryApp && 'max-w-[auto]',
)}>
<div className={cn(
'flex items-center gap-3 rounded-t-2xl px-6 py-4',

View File

@@ -33,7 +33,7 @@ const ViewFormDropdown = ({ iconColor }: Props) => {
<RiChatSettingsLine className={cn('h-[18px] w-[18px]', iconColor)} />
</ActionButton>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className="z-[99]">
<PortalToFollowElemContent className="z-50">
<div className='w-[400px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg backdrop-blur-sm'>
<div className='flex items-center gap-3 rounded-t-2xl border-b border-divider-subtle px-6 py-4'>
<Message3Fill className='h-6 w-6 shrink-0' />

View File

@@ -13,7 +13,6 @@ type Props = {
showFileUpload?: boolean
disabled?: boolean
onFeatureBarClick?: (state: boolean) => void
hideEditEntrance?: boolean
}
const FeatureBar = ({
@@ -21,7 +20,6 @@ const FeatureBar = ({
showFileUpload = true,
disabled,
onFeatureBarClick,
hideEditEntrance = false,
}: Props) => {
const { t } = useTranslation()
const features = useFeatures(s => s.features)
@@ -134,14 +132,10 @@ const FeatureBar = ({
)}
</div>
<div className='body-xs-regular grow text-text-tertiary'>{t('appDebug.feature.bar.enableText')}</div>
{
!hideEditEntrance && (
<Button className='shrink-0' variant='ghost-accent' size='small' onClick={() => onFeatureBarClick?.(true)}>
<div className='mx-1'>{t('appDebug.feature.bar.manage')}</div>
<RiArrowRightLine className='h-3.5 w-3.5 text-text-accent' />
</Button>
)
}
<Button className='shrink-0' variant='ghost-accent' size='small' onClick={() => onFeatureBarClick?.(true)}>
<div className='mx-1'>{t('appDebug.feature.bar.manage')}</div>
<RiArrowRightLine className='h-3.5 w-3.5 text-text-accent' />
</Button>
</div>
)}
</div>

View File

@@ -13,27 +13,21 @@ import { TransferMethod } from '@/types/app'
type FileUploaderInChatInputProps = {
fileConfig: FileUpload
readonly?: boolean
}
const FileUploaderInChatInput = ({
fileConfig,
readonly,
}: FileUploaderInChatInputProps) => {
const renderTrigger = useCallback((open: boolean) => {
return (
<ActionButton
size='l'
className={cn(open && 'bg-state-base-hover')}
disabled={readonly}
>
<RiAttachmentLine className='h-5 w-5' />
</ActionButton>
)
}, [])
if(readonly)
return renderTrigger(false)
return (
<FileFromLinkOrLocal
trigger={renderTrigger}

View File

@@ -69,12 +69,10 @@ const PasteImageLinkButton: FC<PasteImageLinkButtonProps> = ({
type TextGenerationImageUploaderProps = {
settings: VisionSettings
onFilesChange: (files: ImageFile[]) => void
disabled?: boolean
}
const TextGenerationImageUploader: FC<TextGenerationImageUploaderProps> = ({
settings,
onFilesChange,
disabled,
}) => {
const { t } = useTranslation()
@@ -94,7 +92,7 @@ const TextGenerationImageUploader: FC<TextGenerationImageUploaderProps> = ({
const localUpload = (
<Uploader
onUpload={onUpload}
disabled={files.length >= settings.number_limits || disabled}
disabled={files.length >= settings.number_limits}
limit={+settings.image_file_size_limit!}
>
{
@@ -115,7 +113,7 @@ const TextGenerationImageUploader: FC<TextGenerationImageUploaderProps> = ({
const urlUpload = (
<PasteImageLinkButton
onUpload={onUpload}
disabled={files.length >= settings.number_limits || disabled}
disabled={files.length >= settings.number_limits}
/>
)

View File

@@ -16,8 +16,6 @@ export type ITabHeaderProps = {
items: Item[]
value: string
itemClassName?: string
itemWrapClassName?: string
activeItemClassName?: string
onChange: (value: string) => void
}
@@ -25,8 +23,6 @@ const TabHeader: FC<ITabHeaderProps> = ({
items,
value,
itemClassName,
itemWrapClassName,
activeItemClassName,
onChange,
}) => {
const renderItem = ({ id, name, icon, extra, disabled }: Item) => (
@@ -34,9 +30,8 @@ const TabHeader: FC<ITabHeaderProps> = ({
key={id}
className={cn(
'system-md-semibold relative flex cursor-pointer items-center border-b-2 border-transparent pb-2 pt-2.5',
id === value ? cn('border-components-tab-active text-text-primary', activeItemClassName) : 'text-text-tertiary',
id === value ? 'border-components-tab-active text-text-primary' : 'text-text-tertiary',
disabled && 'cursor-not-allowed opacity-30',
itemWrapClassName,
)}
onClick={() => !disabled && onChange(id)}
>

View File

@@ -11,7 +11,7 @@ import { convertToMp3 } from './utils'
import s from './index.module.css'
import cn from '@/utils/classnames'
import { StopCircle } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
import { AppSourceType, audioToText } from '@/service/share'
import { audioToText } from '@/service/share'
type VoiceInputTypes = {
onConverted: (text: string) => void
@@ -108,7 +108,7 @@ const VoiceInput = ({
}
try {
const audioResponse = await audioToText(url, isPublic ? AppSourceType.webApp : AppSourceType.installedApp, formData)
const audioResponse = await audioToText(url, isPublic, formData)
onConverted(audioResponse.text)
onCancel()
}

View File

@@ -6,13 +6,7 @@ import cn from '@/utils/classnames'
import type { App } from '@/models/explore'
import AppIcon from '@/app/components/base/app-icon'
import { AppTypeIcon } from '../../app/type-selector'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { RiInformation2Line } from '@remixicon/react'
import { useCallback } from 'react'
import ExploreContext from '@/context/explore-context'
import { useContextSelector } from 'use-context-selector'
import { AppModeEnum } from '@/types/app'
export type AppCardProps = {
app: App
canCreate: boolean
@@ -28,17 +22,8 @@ const AppCard = ({
}: AppCardProps) => {
const { t } = useTranslation()
const { app: appBasicInfo } = app
const { systemFeatures } = useGlobalPublicStore()
const isTrialApp = app.can_trial && systemFeatures.enable_trial_app
const setShowTryAppPanel = useContextSelector(ExploreContext, ctx => ctx.setShowTryAppPanel)
const showTryAPPPanel = useCallback((appId: string) => {
return () => {
setShowTryAppPanel?.(true, { appId, app })
}
}, [setShowTryAppPanel, app.category])
return (
<div className={cn('group relative col-span-1 flex cursor-pointer flex-col overflow-hidden rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg pb-2 shadow-sm transition-all duration-200 ease-in-out hover:bg-components-panel-on-panel-item-bg-hover hover:shadow-lg')}>
<div className={cn('group relative col-span-1 flex cursor-pointer flex-col overflow-hidden rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg pb-2 shadow-sm transition-all duration-200 ease-in-out hover:shadow-lg')}>
<div className='flex h-[66px] shrink-0 grow-0 items-center gap-3 px-[14px] pb-3 pt-[14px]'>
<div className='relative shrink-0'>
<AppIcon
@@ -69,23 +54,14 @@ const AppCard = ({
{app.description}
</div>
</div>
{isExplore && (canCreate || isTrialApp) && (
<div className={cn(
'absolute bottom-0 left-0 right-0 hidden bg-gradient-to-t from-components-panel-gradient-2 from-[60.27%] to-transparent p-4 pt-8',
(canCreate && isTrialApp) && 'grid-cols-2 gap-2 group-hover:grid',
)}>
{canCreate && (
<Button variant='primary' className='h-7' onClick={() => onCreate()}>
{isExplore && canCreate && (
<div className={cn('absolute bottom-0 left-0 right-0 hidden bg-gradient-to-t from-components-panel-gradient-2 from-[60.27%] to-transparent p-4 pt-8 group-hover:flex')}>
<div className={cn('flex h-8 w-full items-center space-x-2')}>
<Button variant='primary' className='h-7 grow' onClick={() => onCreate()}>
<PlusIcon className='mr-1 h-4 w-4' />
<span className='text-xs'>{t('explore.appCard.addToWorkspace')}</span>
</Button>
)}
{isTrialApp && (
<Button className='w-full' onClick={showTryAPPPanel(app.app_id)}>
<RiInformation2Line className='mr-1 size-4' />
<span>{t('explore.appCard.try')}</span>
</Button>
)}
</div>
</div>
)}
</div>

View File

@@ -22,11 +22,6 @@ import {
} from '@/models/app'
import { useImportDSL } from '@/hooks/use-import-dsl'
import DSLConfirmModal from '@/app/components/app/create-from-dsl-modal/dsl-confirm-modal'
import Banner from '@/app/components/explore/banner/banner'
import { useGlobalPublicStore } from '@/context/global-public-context'
import Button from '@/app/components/base/button'
import { useContextSelector } from 'use-context-selector'
import TryApp from '../try-app'
type AppsProps = {
onSuccess?: () => void
@@ -41,19 +36,12 @@ const Apps = ({
onSuccess,
}: AppsProps) => {
const { t } = useTranslation()
const { systemFeatures } = useGlobalPublicStore()
const { hasEditPermission } = useContext(ExploreContext)
const allCategoriesEn = t('explore.apps.allCategories', { lng: 'en' })
const [keywords, setKeywords] = useState('')
const [searchKeywords, setSearchKeywords] = useState('')
const hasFilterCondition = !!keywords
const handleResetFilter = useCallback(() => {
setKeywords('')
setSearchKeywords('')
}, [])
const { run: handleSearch } = useDebounceFn(() => {
setSearchKeywords(keywords)
}, { wait: 500 })
@@ -108,18 +96,6 @@ const Apps = ({
isFetching,
} = useImportDSL()
const [showDSLConfirmModal, setShowDSLConfirmModal] = useState(false)
const isShowTryAppPanel = useContextSelector(ExploreContext, ctx => ctx.isShowTryAppPanel)
const setShowTryAppPanel = useContextSelector(ExploreContext, ctx => ctx.setShowTryAppPanel)
const hideTryAppPanel = useCallback(() => {
setShowTryAppPanel(false)
}, [setShowTryAppPanel])
const appParams = useContextSelector(ExploreContext, ctx => ctx.currentApp)
const handleShowFromTryApp = useCallback(() => {
setCurrApp(appParams?.app || null)
setIsShowCreateModal(true)
}, [appParams?.app])
const onCreate: CreateAppModalProps['onConfirm'] = async ({
name,
icon_type,
@@ -127,8 +103,6 @@ const Apps = ({
icon_background,
description,
}) => {
hideTryAppPanel()
const { export_data } = await fetchAppDetail(
currApp?.app.id as string,
)
@@ -167,25 +141,23 @@ const Apps = ({
return (
<div className={cn(
'flex h-full flex-col',
'flex h-full flex-col border-l-[0.5px] border-divider-regular',
)}>
{systemFeatures.enable_explore_banner && (
<div className='mt-4 px-12'>
<Banner />
</div>
)}
<div className='shrink-0 px-12 pt-6'>
<div className={`mb-1 ${s.textGradient} text-xl font-semibold`}>{t('explore.apps.title')}</div>
<div className='text-sm text-text-tertiary'>{t('explore.apps.description')}</div>
</div>
<div className={cn(
'mt-6 flex items-center justify-between px-12',
)}>
<div className='flex items-center'>
<div className={'system-xl-semibold grow truncate text-text-primary'}>{!hasFilterCondition ? t('explore.apps.title') : t('explore.apps.resultNum', { num: searchFilteredList.length })}</div>
{hasFilterCondition && (
<>
<div className='mx-3 h-4 w-px bg-divider-regular'></div>
<Button size='medium' onClick={handleResetFilter}>{t('explore.apps.resetFilter')}</Button>
</>
)}
</div>
<Category
list={categories}
value={currCategory}
onChange={setCurrCategory}
allCategoriesEn={allCategoriesEn}
/>
<Input
showLeftIcon
showClearIcon
@@ -196,15 +168,6 @@ const Apps = ({
/>
</div>
<div className='mt-2 px-12'>
<Category
list={categories}
value={currCategory}
onChange={setCurrCategory}
allCategoriesEn={allCategoriesEn}
/>
</div>
<div className={cn(
'relative mt-4 flex flex-1 shrink-0 grow flex-col overflow-auto pb-6',
)}>
@@ -251,14 +214,6 @@ const Apps = ({
/>
)
}
{isShowTryAppPanel && (
<TryApp appId={appParams?.appId || ''}
category={appParams?.app?.category}
onClose={hideTryAppPanel}
onCreate={handleShowFromTryApp}
/>
)}
</div>
)
}

View File

@@ -1,196 +0,0 @@
import type { FC } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { RiArrowRightLine } from '@remixicon/react'
import { useCarousel } from '@/app/components/base/carousel'
import { IndicatorButton } from './indicator-button'
import { useTranslation } from 'react-i18next'
import cn from '@/utils/classnames'
export type BannerData = {
id: string
content: {
'category': string
'title': string
'description': string
'img-src': string
}
status: 'enabled' | 'disabled'
link: string
created_at: number
}
type BannerItemProps = {
banner: BannerData
autoplayDelay: number
isPaused?: boolean
}
const RESPONSIVE_BREAKPOINT = 1200
const MAX_RESPONSIVE_WIDTH = 600
const INDICATOR_WIDTH = 20
const INDICATOR_GAP = 8
const MIN_VIEW_MORE_WIDTH = 480
export const BannerItem: FC<BannerItemProps> = ({ banner, autoplayDelay, isPaused = false }) => {
const { t } = useTranslation()
const { api, selectedIndex } = useCarousel()
const { category, title, description, 'img-src': imgSrc } = banner.content
const [resetKey, setResetKey] = useState(0)
const textAreaRef = useRef<HTMLDivElement>(null)
const [maxWidth, setMaxWidth] = useState<number | undefined>(undefined)
const slideInfo = useMemo(() => {
const slides = api?.slideNodes() ?? []
const totalSlides = slides.length
const nextIndex = totalSlides > 0 ? (selectedIndex + 1) % totalSlides : 0
return { slides, totalSlides, nextIndex }
}, [api, selectedIndex])
const indicatorsWidth = useMemo(() => {
const count = slideInfo.totalSlides
if (count === 0) return 0
// Calculate: indicator buttons + gaps + extra spacing (3 * 20px for divider and padding)
return (count + 2) * INDICATOR_WIDTH + (count - 1) * INDICATOR_GAP
}, [slideInfo.totalSlides])
const viewMoreStyle = useMemo(() => {
if (!maxWidth) return undefined
return {
maxWidth: `${maxWidth}px`,
minWidth: indicatorsWidth ? `${Math.min(maxWidth - indicatorsWidth, MIN_VIEW_MORE_WIDTH)}px` : undefined,
}
}, [maxWidth, indicatorsWidth])
const responsiveStyle = useMemo(
() => (maxWidth !== undefined ? { maxWidth: `${maxWidth}px` } : undefined),
[maxWidth],
)
const incrementResetKey = useCallback(() => setResetKey(prev => prev + 1), [])
useEffect(() => {
const updateMaxWidth = () => {
if (window.innerWidth < RESPONSIVE_BREAKPOINT && textAreaRef.current) {
const textAreaWidth = textAreaRef.current.offsetWidth
setMaxWidth(Math.min(textAreaWidth, MAX_RESPONSIVE_WIDTH))
}
else {
setMaxWidth(undefined)
}
}
updateMaxWidth()
const resizeObserver = new ResizeObserver(updateMaxWidth)
if (textAreaRef.current)
resizeObserver.observe(textAreaRef.current)
window.addEventListener('resize', updateMaxWidth)
return () => {
resizeObserver.disconnect()
window.removeEventListener('resize', updateMaxWidth)
}
}, [])
useEffect(() => {
incrementResetKey()
}, [selectedIndex, incrementResetKey])
const handleBannerClick = useCallback(() => {
incrementResetKey()
if (banner.link)
window.open(banner.link, '_blank', 'noopener,noreferrer')
}, [banner.link, incrementResetKey])
const handleIndicatorClick = useCallback((index: number) => {
incrementResetKey()
api?.scrollTo(index)
}, [api, incrementResetKey])
return (
<div
className="relative flex w-full min-w-[784px] cursor-pointer overflow-hidden rounded-2xl bg-components-panel-on-panel-item-bg pr-[288px] transition-shadow hover:shadow-md"
onClick={handleBannerClick}
>
{/* Left content area */}
<div className="min-w-0 flex-1">
<div className="flex h-full flex-col gap-3 py-6 pl-8 pr-0">
{/* Text section */}
<div className="flex min-h-24 flex-wrap items-end gap-1 py-1">
{/* Title area */}
<div
ref={textAreaRef}
className="flex min-w-[480px] max-w-[680px] flex-[1_0_0] flex-col pr-4"
style={responsiveStyle}
>
<p className="title-4xl-semi-bold line-clamp-1 text-dify-logo-dify-logo-blue">
{category}
</p>
<p className="title-4xl-semi-bold line-clamp-2 text-dify-logo-dify-logo-black">
{title}
</p>
</div>
{/* Description area */}
<div
className="min-w-60 max-w-[600px] flex-[1_0_0] self-end overflow-hidden py-1 pr-4"
style={responsiveStyle}
>
<p className="body-sm-regular line-clamp-4 overflow-hidden text-text-tertiary">
{description}
</p>
</div>
</div>
{/* Actions section */}
<div className="flex items-center gap-1">
{/* View more button */}
<div
className="flex min-w-[480px] max-w-[680px] flex-[1_0_0] items-center gap-[6px] py-1 pr-8"
style={viewMoreStyle}
>
<div className="flex h-4 w-4 items-center justify-center rounded-full bg-text-accent p-[2px]">
<RiArrowRightLine className="h-3 w-3 text-text-primary-on-surface" />
</div>
<span className="system-sm-semibold-uppercase text-text-accent">
{t('explore.banner.viewMore')}
</span>
</div>
<div
className={cn('flex max-w-[600px] flex-[1_0_0] items-center gap-2 py-1 pr-10', maxWidth ? '' : 'min-w-60')}
style={responsiveStyle}
>
{/* Slide navigation indicators */}
<div className="flex items-center gap-2">
{slideInfo.slides.map((_: unknown, index: number) => (
<IndicatorButton
key={index}
index={index}
selectedIndex={selectedIndex}
isNextSlide={index === slideInfo.nextIndex}
autoplayDelay={autoplayDelay}
resetKey={resetKey}
isPaused={isPaused}
onClick={() => handleIndicatorClick(index)}
/>
))}
</div>
<div className="hidden h-[1px] flex-1 bg-divider-regular min-[1380px]:block" />
</div>
</div>
</div>
</div>
{/* Right image area */}
<div className="absolute right-0 top-0 flex h-full items-center p-2">
<img
src={imgSrc}
alt={title}
className="aspect-[4/3] h-full max-w-[296px] rounded-xl"
/>
</div>
</div>
)
}

View File

@@ -1,93 +0,0 @@
import type { FC } from 'react'
import React, { useEffect, useMemo, useRef, useState } from 'react'
import { Carousel } from '@/app/components/base/carousel'
import { useGetBanners } from '@/service/use-explore'
import Loading from '../../base/loading'
import { type BannerData, BannerItem } from './banner-item'
import { useI18N } from '@/context/i18n'
const AUTOPLAY_DELAY = 5000
const MIN_LOADING_HEIGHT = 168
const RESIZE_DEBOUNCE_DELAY = 50
const LoadingState: FC = () => (
<div
className="flex items-center justify-center rounded-2xl bg-components-panel-on-panel-item-bg shadow-md"
style={{ minHeight: MIN_LOADING_HEIGHT }}
>
<Loading />
</div>
)
const Banner: FC = () => {
const { locale } = useI18N()
const { data: banners, isLoading, isError } = useGetBanners(locale)
const [isHovered, setIsHovered] = useState(false)
const [isResizing, setIsResizing] = useState(false)
const resizeTimerRef = useRef<NodeJS.Timeout | null>(null)
const enabledBanners = useMemo(
() => banners?.filter((banner: BannerData) => banner.status === 'enabled') ?? [],
[banners],
)
const isPaused = isHovered || isResizing
// Handle window resize to pause animation
useEffect(() => {
const handleResize = () => {
setIsResizing(true)
if (resizeTimerRef.current)
clearTimeout(resizeTimerRef.current)
resizeTimerRef.current = setTimeout(() => {
setIsResizing(false)
}, RESIZE_DEBOUNCE_DELAY)
}
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
if (resizeTimerRef.current)
clearTimeout(resizeTimerRef.current)
}
}, [])
if (isLoading)
return <LoadingState />
if (isError || enabledBanners.length === 0)
return null
return (
<Carousel
opts={{ loop: true }}
plugins={[
Carousel.Plugin.Autoplay({
delay: AUTOPLAY_DELAY,
stopOnInteraction: false,
stopOnMouseEnter: true,
}),
]}
className="rounded-2xl"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<Carousel.Content>
{enabledBanners.map((banner: BannerData) => (
<Carousel.Item key={banner.id}>
<BannerItem
banner={banner}
autoplayDelay={AUTOPLAY_DELAY}
isPaused={isPaused}
/>
</Carousel.Item>
))}
</Carousel.Content>
</Carousel>
)
}
export default React.memo(Banner)

View File

@@ -1,111 +0,0 @@
import type { FC } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import cn from '@/utils/classnames'
type IndicatorButtonProps = {
index: number
selectedIndex: number
isNextSlide: boolean
autoplayDelay: number
resetKey: number
isPaused?: boolean
onClick: () => void
}
const PROGRESS_MAX = 100
const DEGREES_PER_PERCENT = 3.6
export const IndicatorButton: FC<IndicatorButtonProps> = ({
index,
selectedIndex,
isNextSlide,
autoplayDelay,
resetKey,
isPaused = false,
onClick,
}) => {
const [progress, setProgress] = useState(0)
const frameIdRef = useRef<number | undefined>(undefined)
const startTimeRef = useRef(0)
const isActive = index === selectedIndex
const shouldAnimate = !document.hidden && !isPaused
useEffect(() => {
if (!isNextSlide) {
setProgress(0)
if (frameIdRef.current)
cancelAnimationFrame(frameIdRef.current)
return
}
setProgress(0)
startTimeRef.current = Date.now()
const animate = () => {
if (!document.hidden && !isPaused) {
const elapsed = Date.now() - startTimeRef.current
const newProgress = Math.min((elapsed / autoplayDelay) * PROGRESS_MAX, PROGRESS_MAX)
setProgress(newProgress)
if (newProgress < PROGRESS_MAX)
frameIdRef.current = requestAnimationFrame(animate)
}
else {
frameIdRef.current = requestAnimationFrame(animate)
}
}
if (shouldAnimate)
frameIdRef.current = requestAnimationFrame(animate)
return () => {
if (frameIdRef.current)
cancelAnimationFrame(frameIdRef.current)
}
}, [isNextSlide, autoplayDelay, resetKey, isPaused])
const handleClick = useCallback((e: React.MouseEvent) => {
e.stopPropagation()
onClick()
}, [onClick])
const progressDegrees = progress * DEGREES_PER_PERCENT
return (
<button
onClick={handleClick}
className={cn(
'system-2xs-semibold-uppercase relative flex h-[18px] w-[20px] items-center justify-center rounded-[7px] border border-divider-subtle p-[2px] text-center transition-colors',
isActive
? 'bg-text-primary text-components-panel-on-panel-item-bg'
: 'bg-components-panel-on-panel-item-bg text-text-tertiary hover:text-text-secondary',
)}
>
{/* progress border for next slide */}
{isNextSlide && !isActive && (
<span
key={resetKey}
className="absolute inset-[-1px] rounded-[7px]"
style={{
background: `conic-gradient(
from 0deg,
var(--color-text-primary) ${progressDegrees}deg,
transparent ${progressDegrees}deg
)`,
WebkitMask: 'linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)',
WebkitMaskComposite: 'xor',
mask: 'linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)',
maskComposite: 'exclude',
padding: '1px',
}}
/>
)}
{/* number content */}
<span className="relative z-10">
{String(index + 1).padStart(2, '0')}
</span>
</button>
)
}

View File

@@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next'
import cn from '@/utils/classnames'
import exploreI18n from '@/i18n/en-US/explore'
import type { AppCategory } from '@/models/explore'
import { ThumbsUp } from '@/app/components/base/icons/src/vender/line/alertsAndFeedback'
const categoryI18n = exploreI18n.category
@@ -30,7 +31,7 @@ const Category: FC<ICategoryProps> = ({
const isAllCategories = !list.includes(value as AppCategory) || value === allCategoriesEn
const itemClassName = (isSelected: boolean) => cn(
'system-sm-medium flex h-7 cursor-pointer items-center rounded-lg border border-transparent px-3 text-text-tertiary hover:bg-components-main-nav-nav-button-bg-active',
'flex h-[32px] cursor-pointer items-center rounded-lg border-[0.5px] border-transparent px-3 py-[7px] font-medium leading-[18px] text-text-tertiary hover:bg-components-main-nav-nav-button-bg-active',
isSelected && 'border-components-main-nav-nav-button-border bg-components-main-nav-nav-button-bg-active text-components-main-nav-nav-button-text-active shadow-xs',
)
@@ -40,6 +41,7 @@ const Category: FC<ICategoryProps> = ({
className={itemClassName(isAllCategories)}
onClick={() => onChange(allCategoriesEn)}
>
<ThumbsUp className='mr-1 h-3.5 w-3.5' />
{t('explore.apps.allCategories')}
</div>
{list.filter(name => name !== allCategoriesEn).map(name => (

View File

@@ -2,7 +2,6 @@
import type { FC } from 'react'
import React, { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation'
import type { CurrentTryAppParams } from '@/context/explore-context'
import ExploreContext from '@/context/explore-context'
import Sidebar from '@/app/components/explore/sidebar'
import { useAppContext } from '@/context/app-context'
@@ -43,16 +42,6 @@ const Explore: FC<IExploreProps> = ({
return router.replace('/datasets')
}, [isCurrentWorkspaceDatasetOperator])
const [currentTryAppParams, setCurrentTryAppParams] = useState<CurrentTryAppParams | undefined>(undefined)
const [isShowTryAppPanel, setIsShowTryAppPanel] = useState(false)
const setShowTryAppPanel = (showTryAppPanel: boolean, params?: CurrentTryAppParams) => {
if (showTryAppPanel)
setCurrentTryAppParams(params)
else
setCurrentTryAppParams(undefined)
setIsShowTryAppPanel(showTryAppPanel)
}
return (
<div className='flex h-full overflow-hidden border-t border-divider-regular bg-background-body'>
<ExploreContext.Provider
@@ -65,9 +54,6 @@ const Explore: FC<IExploreProps> = ({
setInstalledApps,
isFetchingInstalledApps,
setIsFetchingInstalledApps,
currentApp: currentTryAppParams,
isShowTryAppPanel,
setShowTryAppPanel,
}
}
>

View File

@@ -12,7 +12,6 @@ import AppUnavailable from '../../base/app-unavailable'
import { useGetUserCanAccessApp } from '@/service/access-control'
import { useGetInstalledAppAccessModeByAppId, useGetInstalledAppMeta, useGetInstalledAppParams } from '@/service/use-explore'
import type { AppData } from '@/models/share'
import type { AccessMode } from '@/models/access-control'
import { AppModeEnum } from '@/types/app'
export type IInstalledAppProps = {
@@ -63,8 +62,8 @@ const InstalledApp: FC<IInstalledAppProps> = ({
if (appMeta)
updateWebAppMeta(appMeta)
if (webAppAccessMode)
updateWebAppAccessMode((webAppAccessMode as { accessMode: AccessMode }).accessMode)
updateUserCanAccessApp(Boolean(userCanAccessApp && (userCanAccessApp as { result: boolean })?.result))
updateWebAppAccessMode(webAppAccessMode.accessMode)
updateUserCanAccessApp(Boolean(userCanAccessApp && userCanAccessApp?.result))
}, [installedApp, appMeta, appParams, updateAppInfo, updateAppParams, updateUserCanAccessApp, updateWebAppMeta, userCanAccessApp, webAppAccessMode, updateWebAppAccessMode])
if (appParamsError) {

View File

@@ -52,7 +52,7 @@ const ItemOperation: FC<IItemOperationProps> = ({
<PortalToFollowElemTrigger
onClick={() => setOpen(v => !v)}
>
<div className={cn(className, s.btn, 'h-6 w-6 rounded-md border-none py-1', (isItemHovering || open) && `${s.open}`)}></div>
<div className={cn(className, s.btn, 'h-6 w-6 rounded-md border-none py-1', (isItemHovering || open) && `${s.open} !bg-components-actionbar-bg !shadow-none`)}></div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent
className="z-50"

View File

@@ -57,7 +57,7 @@ export default function AppNavItem({
<>
<div className='flex w-0 grow items-center space-x-2'>
<AppIcon size='tiny' iconType={icon_type} icon={icon} background={icon_background} imageUrl={icon_url} />
<div className='system-sm-regular truncate text-components-menu-item-text' title={name}>{name}</div>
<div className='overflow-hidden text-ellipsis whitespace-nowrap' title={name}>{name}</div>
</div>
<div className='h-6 shrink-0' onClick={e => e.stopPropagation()}>
<ItemOperation

View File

@@ -13,9 +13,18 @@ import Confirm from '@/app/components/base/confirm'
import Divider from '@/app/components/base/divider'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import { useGetInstalledApps, useUninstallApp, useUpdateAppPinStatus } from '@/service/use-explore'
import { RiAppsFill, RiExpandRightLine, RiLayoutLeft2Line } from '@remixicon/react'
import { useBoolean } from 'ahooks'
import NoApps from './no-apps'
const SelectedDiscoveryIcon = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="current" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M13.4135 1.11725C13.5091 1.09983 13.6483 1.08355 13.8078 1.11745C14.0143 1.16136 14.2017 1.26953 14.343 1.42647C14.4521 1.54766 14.5076 1.67634 14.5403 1.76781C14.5685 1.84673 14.593 1.93833 14.6136 2.01504L15.5533 5.5222C15.5739 5.5989 15.5985 5.69049 15.6135 5.77296C15.6309 5.86852 15.6472 6.00771 15.6133 6.16722C15.5694 6.37378 15.4612 6.56114 15.3043 6.70245C15.1831 6.81157 15.0544 6.86706 14.9629 6.89975C14.884 6.92796 14.7924 6.95247 14.7157 6.97299L14.676 6.98364C14.3365 7.07461 14.0437 7.15309 13.7972 7.19802C13.537 7.24543 13.2715 7.26736 12.9946 7.20849C12.7513 7.15677 12.5213 7.06047 12.3156 6.92591L9.63273 7.64477C9.86399 7.97104 9.99992 8.36965 9.99992 8.80001C9.99992 9.2424 9.85628 9.65124 9.6131 9.98245L12.5508 14.291C12.7582 14.5952 12.6797 15.01 12.3755 15.2174C12.0713 15.4248 11.6566 15.3464 11.4492 15.0422L8.51171 10.7339C8.34835 10.777 8.17682 10.8 7.99992 10.8C7.82305 10.8 7.65155 10.777 7.48823 10.734L4.5508 15.0422C4.34338 15.3464 3.92863 15.4248 3.62442 15.2174C3.32021 15.01 3.24175 14.5952 3.44916 14.291L6.3868 9.98254C6.14358 9.65132 5.99992 9.24244 5.99992 8.80001C5.99992 8.73795 6.00274 8.67655 6.00827 8.61594L4.59643 8.99424C4.51973 9.01483 4.42813 9.03941 4.34567 9.05444C4.25011 9.07185 4.11092 9.08814 3.95141 9.05423C3.74485 9.01033 3.55748 8.90215 3.41618 8.74522C3.38535 8.71097 3.3588 8.67614 3.33583 8.64171L2.49206 8.8678C2.41536 8.88838 2.32376 8.91296 2.2413 8.92799C2.14574 8.94541 2.00655 8.96169 1.84704 8.92779C1.64048 8.88388 1.45311 8.77571 1.31181 8.61877C1.20269 8.49759 1.1472 8.3689 1.1145 8.27744C1.08629 8.1985 1.06177 8.10689 1.04125 8.03018L0.791701 7.09885C0.771119 7.02215 0.746538 6.93055 0.731508 6.84809C0.714092 6.75253 0.697808 6.61334 0.731712 6.45383C0.775619 6.24726 0.883793 6.0599 1.04073 5.9186C1.16191 5.80948 1.2906 5.75399 1.38206 5.72129C1.461 5.69307 1.55261 5.66856 1.62932 5.64804L2.47318 5.42193C2.47586 5.38071 2.48143 5.33735 2.49099 5.29237C2.5349 5.08581 2.64307 4.89844 2.80001 4.75714C2.92119 4.64802 3.04988 4.59253 3.14134 4.55983C3.22027 4.53162 3.31189 4.50711 3.3886 4.48658L11.1078 2.41824C11.2186 2.19888 11.3697 2.00049 11.5545 1.83406C11.7649 1.64462 12.0058 1.53085 12.2548 1.44183C12.4907 1.35749 12.7836 1.27904 13.123 1.18809L13.1628 1.17744C13.2395 1.15686 13.3311 1.13228 13.4135 1.11725ZM13.3642 2.5039C13.0648 2.58443 12.8606 2.64126 12.7036 2.69735C12.5325 2.75852 12.4742 2.80016 12.4467 2.82492C12.3421 2.91912 12.2699 3.04403 12.2407 3.18174C12.233 3.21793 12.2261 3.28928 12.2587 3.46805C12.2927 3.6545 12.3564 3.89436 12.4559 4.26563L12.5594 4.652C12.6589 5.02328 12.7236 5.26287 12.7874 5.44133C12.8486 5.61244 12.8902 5.67079 12.915 5.69829C13.0092 5.80291 13.1341 5.87503 13.2718 5.9043C13.308 5.91199 13.3793 5.91887 13.5581 5.88629C13.7221 5.85641 13.9273 5.80352 14.2269 5.72356L13.3642 2.5039Z" fill="currentColor" />
</svg>
)
const DiscoveryIcon = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="current" xmlns="http://www.w3.org/2000/svg">
<path d="M8.74786 9.89676L12.0003 14.6669M7.25269 9.89676L4.00027 14.6669M9.3336 8.80031C9.3336 9.53669 8.73665 10.1336 8.00027 10.1336C7.26389 10.1336 6.66694 9.53669 6.66694 8.80031C6.66694 8.06393 7.26389 7.46698 8.00027 7.46698C8.73665 7.46698 9.3336 8.06393 9.3336 8.80031ZM11.4326 3.02182L3.57641 5.12689C3.39609 5.1752 3.30593 5.19936 3.24646 5.25291C3.19415 5.30001 3.15809 5.36247 3.14345 5.43132C3.12681 5.5096 3.15097 5.59976 3.19929 5.78008L3.78595 7.96951C3.83426 8.14984 3.85842 8.24 3.91197 8.29947C3.95907 8.35178 4.02153 8.38784 4.09038 8.40248C4.16866 8.41911 4.25882 8.39496 4.43914 8.34664L12.2953 6.24158L11.4326 3.02182ZM14.5285 6.33338C13.8072 6.52665 13.4466 6.62328 13.1335 6.55673C12.8581 6.49819 12.6082 6.35396 12.4198 6.14471C12.2056 5.90682 12.109 5.54618 11.9157 4.82489L11.8122 4.43852C11.6189 3.71722 11.5223 3.35658 11.5889 3.04347C11.6474 2.76805 11.7916 2.51823 12.0009 2.32982C12.2388 2.11563 12.5994 2.019 13.3207 1.82573C13.501 1.77741 13.5912 1.75325 13.6695 1.76989C13.7383 1.78452 13.8008 1.82058 13.8479 1.87289C13.9014 1.93237 13.9256 2.02253 13.9739 2.20285L14.9057 5.68018C14.954 5.86051 14.9781 5.95067 14.9615 6.02894C14.9469 6.0978 14.9108 6.16025 14.8585 6.20736C14.799 6.2609 14.7088 6.28506 14.5285 6.33338ZM2.33475 8.22033L3.23628 7.97876C3.4166 7.93044 3.50676 7.90628 3.56623 7.85274C3.61854 7.80563 3.6546 7.74318 3.66924 7.67433C3.68588 7.59605 3.66172 7.50589 3.6134 7.32556L3.37184 6.42403C3.32352 6.24371 3.29936 6.15355 3.24581 6.09408C3.19871 6.04176 3.13626 6.00571 3.0674 5.99107C2.98912 5.97443 2.89896 5.99859 2.71864 6.04691L1.81711 6.28847C1.63678 6.33679 1.54662 6.36095 1.48715 6.4145C1.43484 6.4616 1.39878 6.52405 1.38415 6.59291C1.36751 6.67119 1.39167 6.76135 1.43998 6.94167L1.68155 7.8432C1.72987 8.02352 1.75402 8.11369 1.80757 8.17316C1.85467 8.22547 1.91713 8.26153 1.98598 8.27616C2.06426 8.2928 2.15442 8.26864 2.33475 8.22033Z" stroke="currentColor" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
</svg>
)
export type IExploreSideBarProps = {
controlUpdateInstalledApps: number
@@ -35,9 +44,6 @@ const SideBar: FC<IExploreSideBarProps> = ({
const media = useBreakpoints()
const isMobile = media === MediaType.mobile
const [isFold, {
toggle: toggleIsFold,
}] = useBoolean(false)
const [showConfirm, setShowConfirm] = useState(false)
const [currId, setCurrId] = useState('')
@@ -77,28 +83,22 @@ const SideBar: FC<IExploreSideBarProps> = ({
const pinnedAppsCount = installedApps.filter(({ is_pinned }) => is_pinned).length
return (
<div className={cn('relative w-fit shrink-0 cursor-pointer px-3 pt-6 sm:w-[240px]', isFold && 'sm:w-[56px]')}>
<Link
href='/explore/apps'
className={cn(isDiscoverySelected ? 'bg-state-base-active' : 'hover:bg-state-base-hover',
'flex h-8 items-center gap-2 rounded-lg px-1 mobile:w-fit mobile:justify-center pc:w-full pc:justify-start')}
>
<div className='flex size-6 shrink-0 items-center justify-center rounded-md bg-components-icon-bg-blue-solid'>
<RiAppsFill className='size-3.5 text-components-avatar-shape-fill-stop-100' />
</div>
{!isMobile && !isFold && <div className={cn('truncate', isDiscoverySelected ? 'system-sm-semibold text-components-menu-item-text-active' : 'system-sm-regular text-components-menu-item-text')}>{t('explore.sidebar.title')}</div>}
</Link>
{installedApps.length === 0 && !isMobile && !isFold
&& <div className='mt-5'>
<NoApps />
</div>
}
<div className='w-fit shrink-0 cursor-pointer border-r border-divider-burn px-4 pt-6 sm:w-[216px]'>
<div className={cn(isDiscoverySelected ? 'text-text-accent' : 'text-text-tertiary')}>
<Link
href='/explore/apps'
className={cn(isDiscoverySelected ? ' bg-components-main-nav-nav-button-bg-active' : 'font-medium hover:bg-state-base-hover',
'flex h-9 items-center gap-2 rounded-lg px-3 mobile:w-fit mobile:justify-center mobile:px-2 pc:w-full pc:justify-start')}
style={isDiscoverySelected ? { boxShadow: '0px 1px 2px rgba(16, 24, 40, 0.05)' } : {}}
>
{isDiscoverySelected ? <SelectedDiscoveryIcon /> : <DiscoveryIcon />}
{!isMobile && <div className='text-sm'>{t('explore.sidebar.discovery')}</div>}
</Link>
</div>
{installedApps.length > 0 && (
<div className='mt-5'>
{!isMobile && !isFold && <p className='system-xs-medium-uppercase mb-1.5 break-all pl-2 uppercase text-text-tertiary mobile:px-0'>{t('explore.sidebar.webApps')}</p>}
<div className='space-y-0.5 overflow-y-auto overflow-x-hidden'
<div className='mt-10'>
<p className='break-all pl-2 text-xs font-medium uppercase text-text-tertiary mobile:px-0'>{t('explore.sidebar.workspace')}</p>
<div className='mt-3 space-y-1 overflow-y-auto overflow-x-hidden'
style={{
height: 'calc(100vh - 250px)',
}}
@@ -106,7 +106,7 @@ const SideBar: FC<IExploreSideBarProps> = ({
{installedApps.map(({ id, is_pinned, uninstallable, app: { name, icon_type, icon, icon_url, icon_background } }, index) => (
<React.Fragment key={id}>
<Item
isMobile={isMobile || isFold}
isMobile={isMobile}
name={name}
icon_type={icon_type}
icon={icon}
@@ -128,15 +128,6 @@ const SideBar: FC<IExploreSideBarProps> = ({
</div>
</div>
)}
{!isMobile && (
<div className='absolute bottom-3 left-3 flex size-8 cursor-pointer items-center justify-center text-text-tertiary' onClick={toggleIsFold}>
{isFold ? <RiExpandRightLine className='size-4.5' /> : (
<RiLayoutLeft2Line className='size-4.5' />
)}
</div>
)}
{showConfirm && (
<Confirm
title={t('explore.sidebar.delete.title')}

View File

@@ -1,24 +0,0 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import s from './style.module.css'
import useTheme from '@/hooks/use-theme'
import cn from '@/utils/classnames'
import { Theme } from '@/types/app'
const i18nPrefix = 'explore.sidebar.noApps'
const NoApps: FC = () => {
const { t } = useTranslation()
const { theme } = useTheme()
return (
<div className='rounded-xl bg-background-default-subtle p-4'>
<div className={cn('h-[35px] w-[86px] bg-contain bg-center bg-no-repeat', theme === Theme.dark ? s.dark : s.light)}></div>
<div className='system-sm-semibold mt-2 text-text-secondary'>{t(`${i18nPrefix}.title`)}</div>
<div className='system-xs-regular my-1 text-text-tertiary'>{t(`${i18nPrefix}.description`)}</div>
<a className='system-xs-regular text-text-accent' target='_blank' rel='noopener noreferrer' href='https://docs.dify.ai/en/guides/application-publishing/launch-your-webapp-quickly/README'>{t(`${i18nPrefix}.learnMore`)}</a>
</div>
)
}
export default React.memo(NoApps)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -1,7 +0,0 @@
.light {
background-image: url('./no-web-apps-light.png');
}
.dark {
background-image: url('./no-web-apps-dark.png');
}

View File

@@ -1,92 +0,0 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import AppIcon from '@/app/components/base/app-icon'
import { AppTypeIcon } from '@/app/components/app/type-selector'
import { useTranslation } from 'react-i18next'
import type { TryAppInfo } from '@/service/try-app'
import cn from '@/utils/classnames'
import Button from '@/app/components/base/button'
import { RiAddLine } from '@remixicon/react'
import useGetRequirements from './use-get-requirements'
type Props = {
appId: string
appDetail: TryAppInfo
category?: string
className?: string
onCreate: () => void
}
const headerClassName = 'system-sm-semibold-uppercase text-text-secondary mb-3'
const AppInfo: FC<Props> = ({
appId,
className,
category,
appDetail,
onCreate,
}) => {
const { t } = useTranslation()
const mode = appDetail?.mode
const { requirements } = useGetRequirements({ appDetail, appId })
return (
<div className={cn('flex h-full flex-col px-4 pt-2', className)}>
{/* name and icon */}
<div className='flex shrink-0 grow-0 items-center gap-3'>
<div className='relative shrink-0'>
<AppIcon
size='large'
iconType={appDetail.site.icon_type}
icon={appDetail.site.icon}
background={appDetail.site.icon_background}
imageUrl={appDetail.site.icon_url}
/>
<AppTypeIcon wrapperClassName='absolute -bottom-0.5 -right-0.5 w-4 h-4 shadow-sm'
className='h-3 w-3' type={mode} />
</div>
<div className='w-0 grow py-[1px]'>
<div className='flex items-center text-sm font-semibold leading-5 text-text-secondary'>
<div className='truncate' title={appDetail.name}>{appDetail.name}</div>
</div>
<div className='flex items-center text-[10px] font-medium leading-[18px] text-text-tertiary'>
{mode === 'advanced-chat' && <div className='truncate'>{t('app.types.advanced').toUpperCase()}</div>}
{mode === 'chat' && <div className='truncate'>{t('app.types.chatbot').toUpperCase()}</div>}
{mode === 'agent-chat' && <div className='truncate'>{t('app.types.agent').toUpperCase()}</div>}
{mode === 'workflow' && <div className='truncate'>{t('app.types.workflow').toUpperCase()}</div>}
{mode === 'completion' && <div className='truncate'>{t('app.types.completion').toUpperCase()}</div>}
</div>
</div>
</div>
{appDetail.description && (
<div className='system-sm-regular mt-[14px] shrink-0 text-text-secondary'>{appDetail.description}</div>
)}
<Button variant='primary' className='mt-3 flex w-full max-w-full' onClick={onCreate}>
<RiAddLine className='mr-1 size-4 shrink-0' />
<span className='truncate'>{t('explore.tryApp.createFromSampleApp')}</span>
</Button>
{category && (
<div className='mt-6 shrink-0'>
<div className={headerClassName}>{t('explore.tryApp.category')}</div>
<div className='system-md-regular text-text-secondary'>{category}</div>
</div>
)}
{requirements.length > 0 && (
<div className='mt-5 grow overflow-y-auto'>
<div className={headerClassName}>{t('explore.tryApp.requirements')}</div>
<div className='space-y-0.5'>
{requirements.map(item => (
<div className='flex items-center space-x-2 py-1' key={item.name}>
<div className='size-5 rounded-md bg-cover shadow-xs' style={{ backgroundImage: `url(${item.iconUrl})` }} />
<div className='system-md-regular w-0 grow truncate text-text-secondary'>{item.name}</div>
</div>
))}
</div>
</div>
)}
</div>
)
}
export default React.memo(AppInfo)

View File

@@ -1,78 +0,0 @@
import type { LLMNodeType } from '@/app/components/workflow/nodes/llm/types'
import type { ToolNodeType } from '@/app/components/workflow/nodes/tool/types'
import { BlockEnum } from '@/app/components/workflow/types'
import { MARKETPLACE_API_PREFIX } from '@/config'
import type { TryAppInfo } from '@/service/try-app'
import { useGetTryAppFlowPreview } from '@/service/use-try-app'
import type { AgentTool } from '@/types/app'
import { uniqBy } from 'lodash-es'
type Params = {
appDetail: TryAppInfo
appId: string
}
type RequirementItem = {
name: string
iconUrl: string
}
const getIconUrl = (provider: string, tool: string) => {
return `${MARKETPLACE_API_PREFIX}/plugins/${provider}/${tool}/icon`
}
const useGetRequirements = ({ appDetail, appId }: Params) => {
const isBasic = ['chat', 'completion', 'agent-chat'].includes(appDetail.mode)
const isAgent = appDetail.mode === 'agent-chat'
const isAdvanced = !isBasic
const { data: flowData } = useGetTryAppFlowPreview(appId, isBasic)
const requirements: RequirementItem[] = []
if(isBasic) {
const modelProviderAndName = appDetail.model_config.model.provider.split('/')
const name = appDetail.model_config.model.provider.split('/').pop() || ''
requirements.push({
name,
iconUrl: getIconUrl(modelProviderAndName[0], modelProviderAndName[1]),
})
}
if(isAgent) {
requirements.push(...appDetail.model_config.agent_mode.tools.filter(data => (data as AgentTool).enabled).map((data) => {
const tool = data as AgentTool
const modelProviderAndName = tool.provider_id.split('/')
return {
name: tool.tool_label,
iconUrl: getIconUrl(modelProviderAndName[0], modelProviderAndName[1]),
}
}))
}
if(isAdvanced && flowData && flowData?.graph?.nodes?.length > 0) {
const nodes = flowData.graph.nodes
const llmNodes = nodes.filter(node => node.data.type === BlockEnum.LLM)
requirements.push(...llmNodes.map((node) => {
const data = node.data as LLMNodeType
const modelProviderAndName = data.model.provider.split('/')
return {
name: data.model.name,
iconUrl: getIconUrl(modelProviderAndName[0], modelProviderAndName[1]),
}
}))
const toolNodes = nodes.filter(node => node.data.type === BlockEnum.Tool)
requirements.push(...toolNodes.map((node) => {
const data = node.data as ToolNodeType
const toolProviderAndName = data.provider_id.split('/')
return {
name: data.tool_label,
iconUrl: getIconUrl(toolProviderAndName[0], toolProviderAndName[1]),
}
}))
}
const uniqueRequirements = uniqBy(requirements, 'name')
return {
requirements: uniqueRequirements,
}
}
export default useGetRequirements

View File

@@ -1,99 +0,0 @@
'use client'
import type { FC } from 'react'
import React, { useEffect } from 'react'
import ChatWrapper from '@/app/components/base/chat/embedded-chatbot/chat-wrapper'
import { useThemeContext } from '../../../base/chat/embedded-chatbot/theme/theme-context'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import {
EmbeddedChatbotContext,
} from '@/app/components/base/chat/embedded-chatbot/context'
import {
useEmbeddedChatbot,
} from '@/app/components/base/chat/embedded-chatbot/hooks'
import cn from '@/utils/classnames'
import { AppSourceType } from '@/service/share'
import Alert from '@/app/components/base/alert'
import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks'
import type { TryAppInfo } from '@/service/try-app'
import AppIcon from '@/app/components/base/app-icon'
import Tooltip from '@/app/components/base/tooltip'
import ActionButton from '@/app/components/base/action-button'
import { RiResetLeftLine } from '@remixicon/react'
import ViewFormDropdown from '@/app/components/base/chat/embedded-chatbot/inputs-form/view-form-dropdown'
type Props = {
appId: string
appDetail: TryAppInfo
className: string
}
const TryApp: FC<Props> = ({
appId,
appDetail,
className,
}) => {
const { t } = useTranslation()
const media = useBreakpoints()
const isMobile = media === MediaType.mobile
const themeBuilder = useThemeContext()
const { removeConversationIdInfo, ...chatData } = useEmbeddedChatbot(AppSourceType.tryApp, appId)
const currentConversationId = chatData.currentConversationId
const inputsForms = chatData.inputsForms
useEffect(() => {
if (appId)
removeConversationIdInfo(appId)
}, [appId])
const [isHideTryNotice, {
setTrue: hideTryNotice,
}] = useBoolean(false)
const handleNewConversation = () => {
removeConversationIdInfo(appId)
chatData.handleNewConversation()
}
return (
<EmbeddedChatbotContext.Provider value={{
...chatData,
disableFeedback: true,
isMobile,
themeBuilder,
} as any}>
<div className={cn('flex h-full flex-col rounded-2xl bg-background-section-burn', className)}>
<div className='flex shrink-0 justify-between p-3'>
<div className='flex grow items-center space-x-2'>
<AppIcon
size='large'
iconType={appDetail.site.icon_type}
icon={appDetail.site.icon}
background={appDetail.site.icon_background}
imageUrl={appDetail.site.icon_url}
/>
<div className='system-md-semibold grow truncate text-text-primary' title={appDetail.name}>{appDetail.name}</div>
</div>
<div className='flex items-center gap-1'>
{currentConversationId && (
<Tooltip
popupContent={t('share.chat.resetChat')}
>
<ActionButton size='l' onClick={handleNewConversation}>
<RiResetLeftLine className='h-[18px] w-[18px]' />
</ActionButton>
</Tooltip>
)}
{currentConversationId && inputsForms.length > 0 && (
<ViewFormDropdown />
)}
</div>
</div>
<div className='mx-auto mt-4 flex h-[0] w-[769px] grow flex-col'>
{!isHideTryNotice && (
<Alert className='mb-4 shrink-0' message={t('explore.tryApp.tryInfo')} onHide={hideTryNotice} />
)}
<ChatWrapper />
</div>
</div>
</EmbeddedChatbotContext.Provider>
)
}
export default React.memo(TryApp)

View File

@@ -1,44 +0,0 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import Chat from './chat'
import TextGeneration from './text-generation'
import type { AppData } from '@/models/share'
import useDocumentTitle from '@/hooks/use-document-title'
import type { TryAppInfo } from '@/service/try-app'
type Props = {
appId: string
appDetail: TryAppInfo
}
const TryApp: FC<Props> = ({
appId,
appDetail,
}) => {
const mode = appDetail?.mode
const isChat = ['chat', 'advanced-chat', 'agent-chat'].includes(mode!)
const isCompletion = !isChat
useDocumentTitle(appDetail?.site?.title || '')
return (
<div className='flex h-full w-full'>
{isChat && (
<Chat appId={appId} appDetail={appDetail} className='h-full grow' />
)}
{isCompletion && (
<TextGeneration
appId={appId}
className='h-full grow'
isWorkflow={mode === 'workflow'}
appData={{
app_id: appId,
custom_config: {},
...appDetail,
} as AppData}
/>
)}
</div>
)
}
export default React.memo(TryApp)

View File

@@ -1,252 +0,0 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import cn from '@/utils/classnames'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import AppIcon from '@/app/components/base/app-icon'
import Loading from '@/app/components/base/loading'
import { appDefaultIconBackground } from '@/config'
import RunOnce from '../../../share/text-generation/run-once'
import { useWebAppStore } from '@/context/web-app-context'
import type { AppData, SiteInfo } from '@/models/share'
import { useGetTryAppParams } from '@/service/use-try-app'
import type { MoreLikeThisConfig, PromptConfig, TextToSpeechConfig } from '@/models/debug'
import { userInputsFormToPromptVariables } from '@/utils/model-config'
import type { VisionFile, VisionSettings } from '@/types/app'
import { Resolution, TransferMethod } from '@/types/app'
import { useBoolean } from 'ahooks'
import { noop } from 'lodash-es'
import type { Task } from '../../../share/text-generation/types'
import Res from '@/app/components/share/text-generation/result'
import { AppSourceType } from '@/service/share'
import { TaskStatus } from '@/app/components/share/text-generation/types'
import Alert from '@/app/components/base/alert'
import { useTranslation } from 'react-i18next'
type Props = {
appId: string
className?: string
isWorkflow?: boolean
appData: AppData | null
}
const TextGeneration: FC<Props> = ({
appId,
className,
isWorkflow,
appData,
}) => {
const { t } = useTranslation()
const media = useBreakpoints()
const isPC = media === MediaType.pc
const [inputs, doSetInputs] = useState<Record<string, any>>({})
const inputsRef = useRef<Record<string, any>>(inputs)
const setInputs = useCallback((newInputs: Record<string, any>) => {
doSetInputs(newInputs)
inputsRef.current = newInputs
}, [])
const updateAppInfo = useWebAppStore(s => s.updateAppInfo)
const { data: tryAppParams } = useGetTryAppParams(appId)
const updateAppParams = useWebAppStore(s => s.updateAppParams)
const appParams = useWebAppStore(s => s.appParams)
const [siteInfo, setSiteInfo] = useState<SiteInfo | null>(null)
const [promptConfig, setPromptConfig] = useState<PromptConfig | null>(null)
const [customConfig, setCustomConfig] = useState<Record<string, any> | null>(null)
const [moreLikeThisConfig, setMoreLikeThisConfig] = useState<MoreLikeThisConfig | null>(null)
const [textToSpeechConfig, setTextToSpeechConfig] = useState<TextToSpeechConfig | null>(null)
const [controlSend, setControlSend] = useState(0)
const [visionConfig, setVisionConfig] = useState<VisionSettings>({
enabled: false,
number_limits: 2,
detail: Resolution.low,
transfer_methods: [TransferMethod.local_file],
})
const [completionFiles, setCompletionFiles] = useState<VisionFile[]>([])
const [isShowResultPanel, { setTrue: doShowResultPanel, setFalse: hideResultPanel }] = useBoolean(false)
const showResultPanel = () => {
// fix: useClickAway hideResSidebar will close sidebar
setTimeout(() => {
doShowResultPanel()
}, 0)
}
const handleSend = () => {
setControlSend(Date.now())
showResultPanel()
}
const [resultExisted, setResultExisted] = useState(false)
useEffect(() => {
if (!appData) return
updateAppInfo(appData)
}, [appData, updateAppInfo])
useEffect(() => {
if (!tryAppParams) return
updateAppParams(tryAppParams)
}, [tryAppParams, updateAppParams])
useEffect(() => {
(async () => {
if (!appData || !appParams)
return
const { site: siteInfo, custom_config } = appData
setSiteInfo(siteInfo as SiteInfo)
setCustomConfig(custom_config)
const { user_input_form, more_like_this, file_upload, text_to_speech }: any = appParams
setVisionConfig({
// legacy of image upload compatible
...file_upload,
transfer_methods: file_upload?.allowed_file_upload_methods || file_upload?.allowed_upload_methods,
// legacy of image upload compatible
image_file_size_limit: appParams?.system_parameters.image_file_size_limit,
fileUploadConfig: appParams?.system_parameters,
} as any)
const prompt_variables = userInputsFormToPromptVariables(user_input_form)
setPromptConfig({
prompt_template: '', // placeholder for future
prompt_variables,
} as PromptConfig)
setMoreLikeThisConfig(more_like_this)
setTextToSpeechConfig(text_to_speech)
})()
}, [appData, appParams])
const [isCompleted, setIsCompleted] = useState(false)
const handleCompleted = useCallback(() => {
setIsCompleted(true)
}, [])
const [isHideTryNotice, {
setTrue: hideTryNotice,
}] = useBoolean(false)
const renderRes = (task?: Task) => (<Res
key={task?.id}
isWorkflow={!!isWorkflow}
isCallBatchAPI={false}
isPC={isPC}
isMobile={!isPC}
appSourceType={AppSourceType.tryApp}
appId={appId}
isError={task?.status === TaskStatus.failed}
promptConfig={promptConfig}
moreLikeThisEnabled={!!moreLikeThisConfig?.enabled}
inputs={inputs}
controlSend={controlSend}
onShowRes={showResultPanel}
handleSaveMessage={noop}
taskId={task?.id}
onCompleted={handleCompleted}
visionConfig={visionConfig}
completionFiles={completionFiles}
isShowTextToSpeech={!!textToSpeechConfig?.enabled}
siteInfo={siteInfo}
onRunStart={() => setResultExisted(true)}
/>)
const renderResWrap = (
<div
className={cn(
'relative flex h-full flex-col',
'rounded-r-2xl bg-chatbot-bg',
)}
>
<div className={cn(
'flex h-0 grow flex-col overflow-y-auto p-6',
)}>
{isCompleted && !isHideTryNotice && (
<Alert className='mb-3 shrink-0' message={t('explore.tryApp.tryInfo')} onHide={hideTryNotice} />
)}
{renderRes()}
</div>
</div>
)
if (!siteInfo || !promptConfig) {
return (
<div className={cn('flex h-screen items-center', className)}>
<Loading type='app' />
</div>
)
}
return (
<div className={cn(
'rounded-2xl border border-components-panel-border bg-background-section-burn',
isPC && 'flex',
!isPC && 'flex-col',
'h-full rounded-2xl shadow-md',
className,
)}>
{/* Left */}
<div className={cn(
'relative flex h-full shrink-0 flex-col',
isPC && 'w-[600px] max-w-[50%]',
'rounded-l-2xl bg-components-panel-bg',
)}>
{/* Header */}
<div className={cn('shrink-0 space-y-4 pb-2', isPC ? ' p-8 pb-0' : 'p-4 pb-0')}>
<div className='flex items-center gap-3'>
<AppIcon
size={isPC ? 'large' : 'small'}
iconType={siteInfo.icon_type}
icon={siteInfo.icon}
background={siteInfo.icon_background || appDefaultIconBackground}
imageUrl={siteInfo.icon_url}
/>
<div className='system-md-semibold grow truncate text-text-secondary'>{siteInfo.title}</div>
</div>
{siteInfo.description && (
<div className='system-xs-regular text-text-tertiary'>{siteInfo.description}</div>
)}
</div>
{/* form */}
<div className={cn(
'h-0 grow overflow-y-auto',
isPC ? 'px-8' : 'px-4',
!isPC && resultExisted && customConfig?.remove_webapp_brand && 'rounded-b-2xl border-b-[0.5px] border-divider-regular',
)}>
<RunOnce
siteInfo={siteInfo}
inputs={inputs}
inputsRef={inputsRef}
onInputsChange={setInputs}
promptConfig={promptConfig}
onSend={handleSend}
visionConfig={visionConfig}
onVisionFilesChange={setCompletionFiles}
/>
</div>
</div>
{/* Result */}
<div className={cn('h-full w-0 grow')}>
{!isPC && (
<div
className={cn(
isShowResultPanel
? 'flex items-center justify-center p-2 pt-6'
: 'absolute left-0 top-0 z-10 flex w-full items-center justify-center px-2 pb-[57px] pt-[3px]',
)}
onClick={() => {
if (isShowResultPanel)
hideResultPanel()
else
showResultPanel()
}}
>
<div className='h-1 w-8 cursor-grab rounded bg-divider-solid' />
</div>
)}
{renderResWrap}
</div>
</div>
)
}
export default React.memo(TextGeneration)

View File

@@ -1,70 +0,0 @@
'use client'
import type { FC } from 'react'
import React, { useState } from 'react'
import Modal from '@/app/components/base/modal/index'
import Tab, { TypeEnum } from './tab'
import Button from '../../base/button'
import { RiCloseLine } from '@remixicon/react'
import AppInfo from './app-info'
import App from './app'
import Preview from './preview'
import { useGetTryAppInfo } from '@/service/use-try-app'
import Loading from '@/app/components/base/loading'
type Props = {
appId: string
category?: string
onClose: () => void
onCreate: () => void
}
const TryApp: FC<Props> = ({
appId,
category,
onClose,
onCreate,
}) => {
const [type, setType] = useState<TypeEnum>(TypeEnum.TRY)
const { data: appDetail, isLoading } = useGetTryAppInfo(appId)
return (
<Modal
isShow
onClose={onClose}
className='h-[calc(100vh-32px)] min-w-[1280px] max-w-[calc(100vw-32px)] overflow-x-auto p-2'
>
{isLoading ? (<div className='flex h-full items-center justify-center'>
<Loading type='area' />
</div>) : (
<div className='flex h-full flex-col'>
<div className='flex shrink-0 justify-between pl-4'>
<Tab
value={type}
onChange={setType}
/>
<Button
size='large'
variant='tertiary'
className='flex size-7 items-center justify-center rounded-[10px] p-0 text-components-button-tertiary-text'
onClick={onClose}
>
<RiCloseLine className='size-5' onClick={onClose} />
</Button>
</div>
{/* Main content */}
<div className='mt-2 flex h-0 grow justify-between space-x-2'>
{type === TypeEnum.TRY ? <App appId={appId} appDetail={appDetail!} /> : <Preview appId={appId} appDetail={appDetail!} />}
<AppInfo
className='w-[360px] shrink-0'
appDetail={appDetail!}
appId={appId}
category={category}
onCreate={onCreate}
/>
</div>
</div>
)}
</Modal>
)
}
export default React.memo(TryApp)

View File

@@ -1,361 +0,0 @@
'use client'
import type { FC } from 'react'
import React, { useMemo, useState } from 'react'
import { clone } from 'lodash-es'
import Loading from '@/app/components/base/loading'
import type { ModelConfig as BackendModelConfig, PromptVariable } from '@/types/app'
import ConfigContext from '@/context/debug-configuration'
import Config from '@/app/components/app/configuration/config'
import Debug from '@/app/components/app/configuration/debug'
import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { ModelModeType, Resolution, TransferMethod } from '@/types/app'
import type { ModelConfig } from '@/models/debug'
import { PromptMode } from '@/models/debug'
import { ANNOTATION_DEFAULT, DEFAULT_AGENT_SETTING, DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG } from '@/config'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import type { FormValue } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { FeaturesProvider } from '@/app/components/base/features'
import type { Features as FeaturesData, FileUpload } from '@/app/components/base/features/types'
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
import { useGetTryAppDataSets, useGetTryAppInfo } from '@/service/use-try-app'
import { noop } from 'lodash-es'
import { correctModelProvider, correctToolProvider } from '@/utils'
import { userInputsFormToPromptVariables } from '@/utils/model-config'
import { useTextGenerationCurrentProviderAndModelAndModelList } from '../../../header/account-setting/model-provider-page/hooks'
import { useAllToolProviders } from '@/service/use-tools'
import { basePath } from '@/utils/var'
type Props = {
appId: string
}
const defaultModelConfig = {
provider: 'langgenius/openai/openai',
model_id: 'gpt-3.5-turbo',
mode: ModelModeType.unset,
configs: {
prompt_template: '',
prompt_variables: [] as PromptVariable[],
},
more_like_this: null,
opening_statement: '',
suggested_questions: [],
sensitive_word_avoidance: null,
speech_to_text: null,
text_to_speech: null,
file_upload: null,
suggested_questions_after_answer: null,
retriever_resource: null,
annotation_reply: null,
dataSets: [],
agentConfig: DEFAULT_AGENT_SETTING,
}
const BasicAppPreview: FC<Props> = ({
appId,
}) => {
const media = useBreakpoints()
const isMobile = media === MediaType.mobile
const { data: appDetail, isLoading: isLoadingAppDetail } = useGetTryAppInfo(appId)
const { data: collectionListFromServer, isLoading: isLoadingToolProviders } = useAllToolProviders()
const collectionList = collectionListFromServer?.map((item) => {
return {
...item,
icon: basePath && typeof item.icon == 'string' && !item.icon.includes(basePath) ? `${basePath}${item.icon}` : item.icon,
}
})
const datasetIds = (() => {
if (isLoadingAppDetail)
return []
const modelConfig = appDetail?.model_config
if (!modelConfig)
return []
let datasets: any = null
if (modelConfig.agent_mode?.tools?.find(({ dataset }: any) => dataset?.enabled))
datasets = modelConfig.agent_mode?.tools.filter(({ dataset }: any) => dataset?.enabled)
// new dataset struct
else if (modelConfig.dataset_configs.datasets?.datasets?.length > 0)
datasets = modelConfig.dataset_configs?.datasets?.datasets
if (datasets?.length && datasets?.length > 0)
return datasets.map(({ dataset }: any) => dataset.id)
return []
})()
const { data: dataSetData, isLoading: isLoadingDatasets } = useGetTryAppDataSets(appId, datasetIds)
const dataSets = dataSetData?.data || []
const isLoading = isLoadingAppDetail || isLoadingDatasets || isLoadingToolProviders
const modelConfig: ModelConfig = ((modelConfig?: BackendModelConfig) => {
if (isLoading || !modelConfig)
return defaultModelConfig
const model = modelConfig.model
const newModelConfig = {
provider: correctModelProvider(model.provider),
model_id: model.name,
mode: model.mode,
configs: {
prompt_template: modelConfig.pre_prompt || '',
prompt_variables: userInputsFormToPromptVariables(
[
...(modelConfig.user_input_form as any),
...(
modelConfig.external_data_tools?.length
? modelConfig.external_data_tools.map((item: any) => {
return {
external_data_tool: {
variable: item.variable as string,
label: item.label as string,
enabled: item.enabled,
type: item.type as string,
config: item.config,
required: true,
icon: item.icon,
icon_background: item.icon_background,
},
}
})
: []
),
],
modelConfig.dataset_query_variable,
),
},
more_like_this: modelConfig.more_like_this,
opening_statement: modelConfig.opening_statement,
suggested_questions: modelConfig.suggested_questions,
sensitive_word_avoidance: modelConfig.sensitive_word_avoidance,
speech_to_text: modelConfig.speech_to_text,
text_to_speech: modelConfig.text_to_speech,
file_upload: modelConfig.file_upload,
suggested_questions_after_answer: modelConfig.suggested_questions_after_answer,
retriever_resource: modelConfig.retriever_resource,
annotation_reply: modelConfig.annotation_reply,
external_data_tools: modelConfig.external_data_tools,
dataSets,
agentConfig: appDetail?.mode === 'agent-chat' ? {
max_iteration: DEFAULT_AGENT_SETTING.max_iteration,
...modelConfig.agent_mode,
// remove dataset
enabled: true, // modelConfig.agent_mode?.enabled is not correct. old app: the value of app with dataset's is always true
tools: modelConfig.agent_mode?.tools.filter((tool: any) => {
return !tool.dataset
}).map((tool: any) => {
const toolInCollectionList = collectionList?.find(c => tool.provider_id === c.id)
return {
...tool,
isDeleted: appDetail?.deleted_tools?.some((deletedTool: any) => deletedTool.id === tool.id && deletedTool.tool_name === tool.tool_name),
notAuthor: toolInCollectionList?.is_team_authorization === false,
...(tool.provider_type === 'builtin' ? {
provider_id: correctToolProvider(tool.provider_name, !!toolInCollectionList),
provider_name: correctToolProvider(tool.provider_name, !!toolInCollectionList),
} : {}),
}
}),
} : DEFAULT_AGENT_SETTING,
}
return (newModelConfig as any)
})(appDetail?.model_config)
const mode = appDetail?.mode
// const isChatApp = ['chat', 'advanced-chat', 'agent-chat'].includes(mode!)
// chat configuration
const promptMode = modelConfig?.prompt_type === PromptMode.advanced ? PromptMode.advanced : PromptMode.simple
const isAdvancedMode = promptMode === PromptMode.advanced
const isAgent = mode === 'agent-chat'
const chatPromptConfig = isAdvancedMode ? (modelConfig?.chat_prompt_config || clone(DEFAULT_CHAT_PROMPT_CONFIG)) : undefined
const suggestedQuestions = modelConfig?.suggested_questions || []
const moreLikeThisConfig = modelConfig?.more_like_this || { enabled: false }
const suggestedQuestionsAfterAnswerConfig = modelConfig?.suggested_questions_after_answer || { enabled: false }
const speechToTextConfig = modelConfig?.speech_to_text || { enabled: false }
const textToSpeechConfig = modelConfig?.text_to_speech || { enabled: false, voice: '', language: '' }
const citationConfig = modelConfig?.retriever_resource || { enabled: false }
const annotationConfig = modelConfig?.annotation_reply || {
id: '',
enabled: false,
score_threshold: ANNOTATION_DEFAULT.score_threshold,
embedding_model: {
embedding_provider_name: '',
embedding_model_name: '',
},
}
const moderationConfig = modelConfig?.sensitive_word_avoidance || { enabled: false }
// completion configuration
const completionPromptConfig = modelConfig?.completion_prompt_config || clone(DEFAULT_COMPLETION_PROMPT_CONFIG) as any
// prompt & model config
const inputs = {}
const query = ''
const completionParams = useState<FormValue>({})
const {
currentModel: currModel,
} = useTextGenerationCurrentProviderAndModelAndModelList(
{
provider: modelConfig.provider,
model: modelConfig.model_id,
},
)
const isShowVisionConfig = !!currModel?.features?.includes(ModelFeatureEnum.vision)
const isShowDocumentConfig = !!currModel?.features?.includes(ModelFeatureEnum.document)
const isShowAudioConfig = !!currModel?.features?.includes(ModelFeatureEnum.audio)
const isAllowVideoUpload = !!currModel?.features?.includes(ModelFeatureEnum.video)
const visionConfig = {
enabled: false,
number_limits: 2,
detail: Resolution.low,
transfer_methods: [TransferMethod.local_file],
}
const featuresData: FeaturesData = useMemo(() => {
return {
moreLikeThis: modelConfig.more_like_this || { enabled: false },
opening: {
enabled: !!modelConfig.opening_statement,
opening_statement: modelConfig.opening_statement || '',
suggested_questions: modelConfig.suggested_questions || [],
},
moderation: modelConfig.sensitive_word_avoidance || { enabled: false },
speech2text: modelConfig.speech_to_text || { enabled: false },
text2speech: modelConfig.text_to_speech || { enabled: false },
file: {
image: {
detail: modelConfig.file_upload?.image?.detail || Resolution.high,
enabled: !!modelConfig.file_upload?.image?.enabled,
number_limits: modelConfig.file_upload?.image?.number_limits || 3,
transfer_methods: modelConfig.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'],
},
enabled: !!(modelConfig.file_upload?.enabled || modelConfig.file_upload?.image?.enabled),
allowed_file_types: modelConfig.file_upload?.allowed_file_types || [],
allowed_file_extensions: modelConfig.file_upload?.allowed_file_extensions || [...FILE_EXTS[SupportUploadFileTypes.image], ...FILE_EXTS[SupportUploadFileTypes.video]].map(ext => `.${ext}`),
allowed_file_upload_methods: modelConfig.file_upload?.allowed_file_upload_methods || modelConfig.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'],
number_limits: modelConfig.file_upload?.number_limits || modelConfig.file_upload?.image?.number_limits || 3,
fileUploadConfig: {},
} as FileUpload,
suggested: modelConfig.suggested_questions_after_answer || { enabled: false },
citation: modelConfig.retriever_resource || { enabled: false },
annotationReply: modelConfig.annotation_reply || { enabled: false },
}
}, [modelConfig])
if (isLoading) {
return <div className='flex h-full items-center justify-center'>
<Loading type='area' />
</div>
}
const value = {
readonly: true,
appId,
isAPIKeySet: true,
isTrailFinished: false,
mode,
modelModeType: '',
promptMode,
isAdvancedMode,
isAgent,
isOpenAI: false,
isFunctionCall: false,
collectionList: [],
setPromptMode: noop,
canReturnToSimpleMode: false,
setCanReturnToSimpleMode: noop,
chatPromptConfig,
completionPromptConfig,
currentAdvancedPrompt: '',
setCurrentAdvancedPrompt: noop,
conversationHistoriesRole: completionPromptConfig.conversation_histories_role,
showHistoryModal: false,
setConversationHistoriesRole: noop,
hasSetBlockStatus: true,
conversationId: '',
introduction: '',
setIntroduction: noop,
suggestedQuestions,
setSuggestedQuestions: noop,
setConversationId: noop,
controlClearChatMessage: false,
setControlClearChatMessage: noop,
prevPromptConfig: {},
setPrevPromptConfig: noop,
moreLikeThisConfig,
setMoreLikeThisConfig: noop,
suggestedQuestionsAfterAnswerConfig,
setSuggestedQuestionsAfterAnswerConfig: noop,
speechToTextConfig,
setSpeechToTextConfig: noop,
textToSpeechConfig,
setTextToSpeechConfig: noop,
citationConfig,
setCitationConfig: noop,
annotationConfig,
setAnnotationConfig: noop,
moderationConfig,
setModerationConfig: noop,
externalDataToolsConfig: {},
setExternalDataToolsConfig: noop,
formattingChanged: false,
setFormattingChanged: noop,
inputs,
setInputs: noop,
query,
setQuery: noop,
completionParams,
setCompletionParams: noop,
modelConfig,
setModelConfig: noop,
showSelectDataSet: noop,
dataSets,
setDataSets: noop,
datasetConfigs: [],
datasetConfigsRef: {},
setDatasetConfigs: noop,
hasSetContextVar: true,
isShowVisionConfig,
visionConfig,
setVisionConfig: noop,
isAllowVideoUpload,
isShowDocumentConfig,
isShowAudioConfig,
rerankSettingModalOpen: false,
setRerankSettingModalOpen: noop,
}
return (
<ConfigContext.Provider value={value as any}>
<FeaturesProvider features={featuresData}>
<div className="flex h-full w-full flex-col bg-components-panel-on-panel-item-bg">
<div className='relative flex h-[200px] grow'>
<div className={'flex h-full w-full shrink-0 flex-col sm:w-1/2'}>
<Config />
</div>
{!isMobile && <div className="relative flex h-full w-1/2 grow flex-col overflow-y-auto " style={{ borderColor: 'rgba(0, 0, 0, 0.02)' }}>
<div className='flex grow flex-col rounded-tl-2xl border-l-[0.5px] border-t-[0.5px] border-components-panel-border bg-chatbot-bg '>
<Debug
isAPIKeySet
onSetting={noop}
inputs={inputs}
modelParameterParams={{
setModel: noop,
onCompletionParamsChange: noop,
}}
debugWithMultipleModel={false}
multipleModelConfigs={[]}
onMultipleModelConfigsChange={noop}
/>
</div>
</div>}
</div>
</div>
</FeaturesProvider>
</ConfigContext.Provider>
)
}
export default React.memo(BasicAppPreview)

View File

@@ -1,37 +0,0 @@
'use client'
import Loading from '@/app/components/base/loading'
import { useGetTryAppFlowPreview } from '@/service/use-try-app'
import type { FC } from 'react'
import React from 'react'
import WorkflowPreview from '@/app/components/workflow/workflow-preview'
import cn from '@/utils/classnames'
type Props = {
appId: string
className?: string
}
const FlowAppPreview: FC<Props> = ({
appId,
className,
}) => {
const { data, isLoading } = useGetTryAppFlowPreview(appId)
if (isLoading) {
return <div className='flex h-full items-center justify-center'>
<Loading type='area' />
</div>
}
if (!data)
return null
return (
<div className='h-full w-full'>
<WorkflowPreview
{...data.graph}
className={cn(className)}
miniMapToRight
/>
</div>
)
}
export default React.memo(FlowAppPreview)

View File

@@ -1,23 +0,0 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import BasicAppPreview from './basic-app-preview'
import FlowAppPreview from './flow-app-preview'
import type { TryAppInfo } from '@/service/try-app'
type Props = {
appId: string
appDetail: TryAppInfo
}
const Preview: FC<Props> = ({
appId,
appDetail,
}) => {
const isBasicApp = ['agent-chat', 'chat', 'completion'].includes(appDetail.mode)
return <div className='h-full w-full'>
{isBasicApp ? <BasicAppPreview appId={appId} /> : <FlowAppPreview appId={appId} className='h-full' />}
</div>
}
export default React.memo(Preview)

View File

@@ -1,37 +0,0 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import TabHeader from '../../base/tab-header'
import { useTranslation } from 'react-i18next'
export enum TypeEnum {
TRY = 'try',
DETAIL = 'detail',
}
type Props = {
value: TypeEnum
onChange: (value: TypeEnum) => void
}
const Tab: FC<Props> = ({
value,
onChange,
}) => {
const { t } = useTranslation()
const tabs = [
{ id: TypeEnum.TRY, name: t('explore.tryApp.tabHeader.try') },
{ id: TypeEnum.DETAIL, name: t('explore.tryApp.tabHeader.detail') },
]
return (
<TabHeader
items={tabs}
value={value}
onChange={onChange as (value: string) => void}
itemClassName='ml-0 system-md-semibold-uppercase'
itemWrapClassName='pt-2'
activeItemClassName='border-util-colors-blue-brand-blue-brand-500'
/>
)
}
export default React.memo(Tab)

View File

@@ -24,8 +24,8 @@ import { debounce } from 'lodash-es'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import LogViewer from '../log-viewer'
import { usePluginSubscriptionStore } from '../store'
import { usePluginStore } from '../../store'
import { useSubscriptionList } from '../use-subscription-list'
type Props = {
onClose: () => void
@@ -91,7 +91,7 @@ const MultiSteps = ({ currentStep }: { currentStep: ApiKeyStep }) => {
export const CommonCreateModal = ({ onClose, createType, builder }: Props) => {
const { t } = useTranslation()
const detail = usePluginStore(state => state.detail)
const { refresh } = usePluginSubscriptionStore()
const { refetch } = useSubscriptionList()
const [currentStep, setCurrentStep] = useState<ApiKeyStep>(createType === SupportedCreationMethods.APIKEY ? ApiKeyStep.Verify : ApiKeyStep.Configuration)
@@ -295,7 +295,7 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => {
message: t('pluginTrigger.subscription.createSuccess'),
})
onClose()
refresh?.()
refetch?.()
},
onError: async (error: any) => {
const errorMessage = await parsePluginErrorMessage(error) || t('pluginTrigger.subscription.createFailed')

View File

@@ -4,7 +4,7 @@ import Toast from '@/app/components/base/toast'
import { useDeleteTriggerSubscription } from '@/service/use-triggers'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { usePluginSubscriptionStore } from './store'
import { useSubscriptionList } from './use-subscription-list'
type Props = {
onClose: (deleted: boolean) => void
@@ -18,7 +18,7 @@ const tPrefix = 'pluginTrigger.subscription.list.item.actions.deleteConfirm'
export const DeleteConfirm = (props: Props) => {
const { onClose, isShow, currentId, currentName, workflowsInUse } = props
const { refresh } = usePluginSubscriptionStore()
const { refetch } = useSubscriptionList()
const { mutate: deleteSubscription, isPending: isDeleting } = useDeleteTriggerSubscription()
const { t } = useTranslation()
const [inputName, setInputName] = useState('')
@@ -40,7 +40,7 @@ export const DeleteConfirm = (props: Props) => {
message: t(`${tPrefix}.success`, { name: currentName }),
className: 'z-[10000001]',
})
refresh?.()
refetch?.()
onClose(true)
},
onError: (error: any) => {

View File

@@ -1,11 +0,0 @@
import { create } from 'zustand'
type ShapeSubscription = {
refresh?: () => void
setRefresh: (refresh: () => void) => void
}
export const usePluginSubscriptionStore = create<ShapeSubscription>(set => ({
refresh: undefined,
setRefresh: (refresh: () => void) => set({ refresh }),
}))

View File

@@ -1,19 +1,11 @@
import { useEffect } from 'react'
import { useTriggerSubscriptions } from '@/service/use-triggers'
import { usePluginStore } from '../store'
import { usePluginSubscriptionStore } from './store'
export const useSubscriptionList = () => {
const detail = usePluginStore(state => state.detail)
const { setRefresh } = usePluginSubscriptionStore()
const { data: subscriptions, isLoading, refetch } = useTriggerSubscriptions(detail?.provider || '')
useEffect(() => {
if (refetch)
setRefresh(refetch)
}, [refetch, setRefresh])
return {
detail,
subscriptions,

View File

@@ -14,7 +14,7 @@ import RunBatch from './run-batch'
import ResDownload from './run-batch/res-download'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import RunOnce from '@/app/components/share/text-generation/run-once'
import { AppSourceType, fetchSavedMessage as doFetchSavedMessage, removeMessage, saveMessage } from '@/service/share'
import { fetchSavedMessage as doFetchSavedMessage, removeMessage, saveMessage } from '@/service/share'
import type { SiteInfo } from '@/models/share'
import type {
MoreLikeThisConfig,
@@ -41,9 +41,24 @@ import { AccessMode } from '@/models/access-control'
import { useGlobalPublicStore } from '@/context/global-public-context'
import useDocumentTitle from '@/hooks/use-document-title'
import { useWebAppStore } from '@/context/web-app-context'
import type { Task } from './types'
import { TaskStatus } from './types'
const GROUP_SIZE = 5 // to avoid RPM(Request per minute) limit. The group task finished then the next group.
enum TaskStatus {
pending = 'pending',
running = 'running',
completed = 'completed',
failed = 'failed',
}
type TaskParam = {
inputs: Record<string, any>
}
type Task = {
id: number
status: TaskStatus
params: TaskParam
}
export type IMainProps = {
isInstalledApp?: boolean
@@ -57,7 +72,6 @@ const TextGeneration: FC<IMainProps> = ({
isWorkflow = false,
}) => {
const { notify } = Toast
const appSourceType = isInstalledApp ? AppSourceType.installedApp : AppSourceType.webApp
const { t } = useTranslation()
const media = useBreakpoints()
@@ -87,18 +101,16 @@ const TextGeneration: FC<IMainProps> = ({
// save message
const [savedMessages, setSavedMessages] = useState<SavedMessage[]>([])
const fetchSavedMessage = useCallback(async () => {
if (!appId)
return
const res: any = await doFetchSavedMessage(appSourceType, appId)
const res: any = await doFetchSavedMessage(isInstalledApp, appId)
setSavedMessages(res.data)
}, [appSourceType, appId])
}, [isInstalledApp, appId])
const handleSaveMessage = async (messageId: string) => {
await saveMessage(messageId, appSourceType, appId)
await saveMessage(messageId, isInstalledApp, appId)
notify({ type: 'success', message: t('common.api.saved') })
fetchSavedMessage()
}
const handleRemoveSavedMessage = async (messageId: string) => {
await removeMessage(messageId, appSourceType, appId)
await removeMessage(messageId, isInstalledApp, appId)
notify({ type: 'success', message: t('common.api.remove') })
fetchSavedMessage()
}
@@ -404,8 +416,8 @@ const TextGeneration: FC<IMainProps> = ({
isCallBatchAPI={isCallBatchAPI}
isPC={isPC}
isMobile={!isPC}
appSourceType={isInstalledApp ? AppSourceType.installedApp : AppSourceType.webApp}
appId={installedAppInfo?.id}
isInstalledApp={isInstalledApp}
installedAppInfo={installedAppInfo}
isError={task?.status === TaskStatus.failed}
promptConfig={promptConfig}
moreLikeThisEnabled={!!moreLikeThisConfig?.enabled}

View File

@@ -7,11 +7,11 @@ import { produce } from 'immer'
import TextGenerationRes from '@/app/components/app/text-generate/item'
import NoData from '@/app/components/share/text-generation/no-data'
import Toast from '@/app/components/base/toast'
import type { AppSourceType } from '@/service/share'
import { sendCompletionMessage, sendWorkflowMessage, updateFeedback } from '@/service/share'
import type { FeedbackType } from '@/app/components/base/chat/chat/type'
import Loading from '@/app/components/base/loading'
import type { PromptConfig } from '@/models/debug'
import type { InstalledApp } from '@/models/explore'
import { TransferMethod, type VisionFile, type VisionSettings } from '@/types/app'
import { NodeRunningStatus, WorkflowRunningStatus } from '@/app/components/workflow/types'
import type { WorkflowProcess } from '@/app/components/base/chat/types'
@@ -30,8 +30,8 @@ export type IResultProps = {
isCallBatchAPI: boolean
isPC: boolean
isMobile: boolean
appSourceType: AppSourceType
appId?: string
isInstalledApp: boolean
installedAppInfo?: InstalledApp
isError: boolean
isShowTextToSpeech: boolean
promptConfig: PromptConfig | null
@@ -55,8 +55,8 @@ const Result: FC<IResultProps> = ({
isCallBatchAPI,
isPC,
isMobile,
appSourceType,
appId,
isInstalledApp,
installedAppInfo,
isError,
isShowTextToSpeech,
promptConfig,
@@ -104,7 +104,7 @@ const Result: FC<IResultProps> = ({
})
const handleFeedback = async (feedback: FeedbackType) => {
await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating, content: feedback.content } }, appSourceType, appId)
await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating, content: feedback.content } }, isInstalledApp, installedAppInfo?.id)
setFeedback(feedback)
}
@@ -128,7 +128,7 @@ const Result: FC<IResultProps> = ({
let hasEmptyInput = ''
const requiredVars = prompt_variables?.filter(({ key, name, required, type }) => {
if (type === 'boolean' || type === 'checkbox')
if(type === 'boolean' || type === 'checkbox')
return false // boolean/checkbox input is not required
const res = (!key || !key.trim()) || (!name || !name.trim()) || (required || required === undefined || required === null)
return res
@@ -374,8 +374,8 @@ const Result: FC<IResultProps> = ({
}))
},
},
appSourceType,
appId,
isInstalledApp,
installedAppInfo?.id,
)
}
else {
@@ -408,7 +408,7 @@ const Result: FC<IResultProps> = ({
onCompleted(getCompletionRes(), taskId, false)
isEnd = true
},
}, appSourceType, appId)
}, isInstalledApp, installedAppInfo?.id)
}
}
@@ -439,8 +439,8 @@ const Result: FC<IResultProps> = ({
feedback={feedback}
onSave={handleSaveMessage}
isMobile={isMobile}
appSourceType={appSourceType}
installedAppId={appId}
isInstalledApp={isInstalledApp}
installedAppId={installedAppInfo?.id}
isLoading={isCallBatchAPI ? (!completionRes && isResponding) : false}
taskId={isCallBatchAPI ? ((taskId as number) < 10 ? `0${taskId}` : `${taskId}`) : undefined}
controlClearMoreLikeThis={controlClearMoreLikeThis}

View File

@@ -1,16 +0,0 @@
type TaskParam = {
inputs: Record<string, any>
}
export type Task = {
id: number
status: TaskStatus
params: TaskParam
}
export enum TaskStatus {
pending = 'pending',
running = 'running',
completed = 'completed',
failed = 'failed',
}

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