mirror of
https://github.com/langgenius/dify.git
synced 2026-04-29 20:48:01 +08:00
Merge branch 'deploy/dev' of https://github.com/langgenius/dify into deploy/dev
This commit is contained in:
commit
7d19ca6a03
@ -32,6 +32,7 @@ from libs.token import (
|
|||||||
clear_csrf_token_from_cookie,
|
clear_csrf_token_from_cookie,
|
||||||
clear_refresh_token_from_cookie,
|
clear_refresh_token_from_cookie,
|
||||||
extract_access_token,
|
extract_access_token,
|
||||||
|
extract_refresh_token,
|
||||||
set_access_token_to_cookie,
|
set_access_token_to_cookie,
|
||||||
set_csrf_token_to_cookie,
|
set_csrf_token_to_cookie,
|
||||||
set_refresh_token_to_cookie,
|
set_refresh_token_to_cookie,
|
||||||
@ -273,7 +274,7 @@ class EmailCodeLoginApi(Resource):
|
|||||||
class RefreshTokenApi(Resource):
|
class RefreshTokenApi(Resource):
|
||||||
def post(self):
|
def post(self):
|
||||||
# Get refresh token from cookie instead of request body
|
# Get refresh token from cookie instead of request body
|
||||||
refresh_token = request.cookies.get("refresh_token")
|
refresh_token = extract_refresh_token(request)
|
||||||
|
|
||||||
if not refresh_token:
|
if not refresh_token:
|
||||||
return {"result": "fail", "message": "No refresh token provided"}, 401
|
return {"result": "fail", "message": "No refresh token provided"}, 401
|
||||||
|
|||||||
@ -39,6 +39,7 @@ class FileApi(Resource):
|
|||||||
return {
|
return {
|
||||||
"file_size_limit": dify_config.UPLOAD_FILE_SIZE_LIMIT,
|
"file_size_limit": dify_config.UPLOAD_FILE_SIZE_LIMIT,
|
||||||
"batch_count_limit": dify_config.UPLOAD_FILE_BATCH_LIMIT,
|
"batch_count_limit": dify_config.UPLOAD_FILE_BATCH_LIMIT,
|
||||||
|
"file_upload_limit": dify_config.BATCH_UPLOAD_LIMIT,
|
||||||
"image_file_size_limit": dify_config.UPLOAD_IMAGE_FILE_SIZE_LIMIT,
|
"image_file_size_limit": dify_config.UPLOAD_IMAGE_FILE_SIZE_LIMIT,
|
||||||
"video_file_size_limit": dify_config.UPLOAD_VIDEO_FILE_SIZE_LIMIT,
|
"video_file_size_limit": dify_config.UPLOAD_VIDEO_FILE_SIZE_LIMIT,
|
||||||
"audio_file_size_limit": dify_config.UPLOAD_AUDIO_FILE_SIZE_LIMIT,
|
"audio_file_size_limit": dify_config.UPLOAD_AUDIO_FILE_SIZE_LIMIT,
|
||||||
|
|||||||
@ -193,15 +193,19 @@ class QuestionClassifierNode(Node):
|
|||||||
finish_reason = event.finish_reason
|
finish_reason = event.finish_reason
|
||||||
break
|
break
|
||||||
|
|
||||||
category_name = node_data.classes[0].name
|
rendered_classes = [
|
||||||
category_id = node_data.classes[0].id
|
c.model_copy(update={"name": variable_pool.convert_template(c.name).text}) for c in node_data.classes
|
||||||
|
]
|
||||||
|
|
||||||
|
category_name = rendered_classes[0].name
|
||||||
|
category_id = rendered_classes[0].id
|
||||||
if "<think>" in result_text:
|
if "<think>" in result_text:
|
||||||
result_text = re.sub(r"<think[^>]*>[\s\S]*?</think>", "", result_text, flags=re.IGNORECASE)
|
result_text = re.sub(r"<think[^>]*>[\s\S]*?</think>", "", result_text, flags=re.IGNORECASE)
|
||||||
result_text_json = parse_and_check_json_markdown(result_text, [])
|
result_text_json = parse_and_check_json_markdown(result_text, [])
|
||||||
# result_text_json = json.loads(result_text.strip('```JSON\n'))
|
# result_text_json = json.loads(result_text.strip('```JSON\n'))
|
||||||
if "category_name" in result_text_json and "category_id" in result_text_json:
|
if "category_name" in result_text_json and "category_id" in result_text_json:
|
||||||
category_id_result = result_text_json["category_id"]
|
category_id_result = result_text_json["category_id"]
|
||||||
classes = node_data.classes
|
classes = rendered_classes
|
||||||
classes_map = {class_.id: class_.name for class_ in classes}
|
classes_map = {class_.id: class_.name for class_ in classes}
|
||||||
category_ids = [_class.id for _class in classes]
|
category_ids = [_class.id for _class in classes]
|
||||||
if category_id_result in category_ids:
|
if category_id_result in category_ids:
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import json
|
|||||||
from collections.abc import Mapping, Sequence
|
from collections.abc import Mapping, Sequence
|
||||||
from collections.abc import Mapping as TypingMapping
|
from collections.abc import Mapping as TypingMapping
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
from dataclasses import dataclass
|
||||||
from typing import Any, Protocol
|
from typing import Any, Protocol
|
||||||
|
|
||||||
from pydantic.json import pydantic_encoder
|
from pydantic.json import pydantic_encoder
|
||||||
@ -106,6 +107,23 @@ class GraphProtocol(Protocol):
|
|||||||
def get_outgoing_edges(self, node_id: str) -> Sequence[object]: ...
|
def get_outgoing_edges(self, node_id: str) -> Sequence[object]: ...
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class _GraphRuntimeStateSnapshot:
|
||||||
|
"""Immutable view of a serialized runtime state snapshot."""
|
||||||
|
|
||||||
|
start_at: float
|
||||||
|
total_tokens: int
|
||||||
|
node_run_steps: int
|
||||||
|
llm_usage: LLMUsage
|
||||||
|
outputs: dict[str, Any]
|
||||||
|
variable_pool: VariablePool
|
||||||
|
has_variable_pool: bool
|
||||||
|
ready_queue_dump: str | None
|
||||||
|
graph_execution_dump: str | None
|
||||||
|
response_coordinator_dump: str | None
|
||||||
|
paused_nodes: tuple[str, ...]
|
||||||
|
|
||||||
|
|
||||||
class GraphRuntimeState:
|
class GraphRuntimeState:
|
||||||
"""Mutable runtime state shared across graph execution components."""
|
"""Mutable runtime state shared across graph execution components."""
|
||||||
|
|
||||||
@ -293,69 +311,28 @@ class GraphRuntimeState:
|
|||||||
|
|
||||||
return json.dumps(snapshot, default=pydantic_encoder)
|
return json.dumps(snapshot, default=pydantic_encoder)
|
||||||
|
|
||||||
def loads(self, data: str | Mapping[str, Any]) -> None:
|
@classmethod
|
||||||
|
def from_snapshot(cls, data: str | Mapping[str, Any]) -> GraphRuntimeState:
|
||||||
"""Restore runtime state from a serialized snapshot."""
|
"""Restore runtime state from a serialized snapshot."""
|
||||||
|
|
||||||
payload: dict[str, Any]
|
snapshot = cls._parse_snapshot_payload(data)
|
||||||
if isinstance(data, str):
|
|
||||||
payload = json.loads(data)
|
|
||||||
else:
|
|
||||||
payload = dict(data)
|
|
||||||
|
|
||||||
version = payload.get("version")
|
state = cls(
|
||||||
if version != "1.0":
|
variable_pool=snapshot.variable_pool,
|
||||||
raise ValueError(f"Unsupported GraphRuntimeState snapshot version: {version}")
|
start_at=snapshot.start_at,
|
||||||
|
total_tokens=snapshot.total_tokens,
|
||||||
|
llm_usage=snapshot.llm_usage,
|
||||||
|
outputs=snapshot.outputs,
|
||||||
|
node_run_steps=snapshot.node_run_steps,
|
||||||
|
)
|
||||||
|
state._apply_snapshot(snapshot)
|
||||||
|
return state
|
||||||
|
|
||||||
self._start_at = float(payload.get("start_at", 0.0))
|
def loads(self, data: str | Mapping[str, Any]) -> None:
|
||||||
total_tokens = int(payload.get("total_tokens", 0))
|
"""Restore runtime state from a serialized snapshot (legacy API)."""
|
||||||
if total_tokens < 0:
|
|
||||||
raise ValueError("total_tokens must be non-negative")
|
|
||||||
self._total_tokens = total_tokens
|
|
||||||
|
|
||||||
node_run_steps = int(payload.get("node_run_steps", 0))
|
snapshot = self._parse_snapshot_payload(data)
|
||||||
if node_run_steps < 0:
|
self._apply_snapshot(snapshot)
|
||||||
raise ValueError("node_run_steps must be non-negative")
|
|
||||||
self._node_run_steps = node_run_steps
|
|
||||||
|
|
||||||
llm_usage_payload = payload.get("llm_usage", {})
|
|
||||||
self._llm_usage = LLMUsage.model_validate(llm_usage_payload)
|
|
||||||
|
|
||||||
self._outputs = deepcopy(payload.get("outputs", {}))
|
|
||||||
|
|
||||||
variable_pool_payload = payload.get("variable_pool")
|
|
||||||
if variable_pool_payload is not None:
|
|
||||||
self._variable_pool = VariablePool.model_validate(variable_pool_payload)
|
|
||||||
|
|
||||||
ready_queue_payload = payload.get("ready_queue")
|
|
||||||
if ready_queue_payload is not None:
|
|
||||||
self._ready_queue = self._build_ready_queue()
|
|
||||||
self._ready_queue.loads(ready_queue_payload)
|
|
||||||
else:
|
|
||||||
self._ready_queue = None
|
|
||||||
|
|
||||||
graph_execution_payload = payload.get("graph_execution")
|
|
||||||
self._graph_execution = None
|
|
||||||
self._pending_graph_execution_workflow_id = None
|
|
||||||
if graph_execution_payload is not None:
|
|
||||||
try:
|
|
||||||
execution_payload = json.loads(graph_execution_payload)
|
|
||||||
self._pending_graph_execution_workflow_id = execution_payload.get("workflow_id")
|
|
||||||
except (json.JSONDecodeError, TypeError, AttributeError):
|
|
||||||
self._pending_graph_execution_workflow_id = None
|
|
||||||
self.graph_execution.loads(graph_execution_payload)
|
|
||||||
|
|
||||||
response_payload = payload.get("response_coordinator")
|
|
||||||
if response_payload is not None:
|
|
||||||
if self._graph is not None:
|
|
||||||
self.response_coordinator.loads(response_payload)
|
|
||||||
else:
|
|
||||||
self._pending_response_coordinator_dump = response_payload
|
|
||||||
else:
|
|
||||||
self._pending_response_coordinator_dump = None
|
|
||||||
self._response_coordinator = None
|
|
||||||
|
|
||||||
paused_nodes_payload = payload.get("paused_nodes", [])
|
|
||||||
self._paused_nodes = set(map(str, paused_nodes_payload))
|
|
||||||
|
|
||||||
def register_paused_node(self, node_id: str) -> None:
|
def register_paused_node(self, node_id: str) -> None:
|
||||||
"""Record a node that should resume when execution is continued."""
|
"""Record a node that should resume when execution is continued."""
|
||||||
@ -391,3 +368,106 @@ class GraphRuntimeState:
|
|||||||
module = importlib.import_module("core.workflow.graph_engine.response_coordinator")
|
module = importlib.import_module("core.workflow.graph_engine.response_coordinator")
|
||||||
coordinator_cls = module.ResponseStreamCoordinator
|
coordinator_cls = module.ResponseStreamCoordinator
|
||||||
return coordinator_cls(variable_pool=self.variable_pool, graph=graph)
|
return coordinator_cls(variable_pool=self.variable_pool, graph=graph)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Snapshot helpers
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@classmethod
|
||||||
|
def _parse_snapshot_payload(cls, data: str | Mapping[str, Any]) -> _GraphRuntimeStateSnapshot:
|
||||||
|
payload: dict[str, Any]
|
||||||
|
if isinstance(data, str):
|
||||||
|
payload = json.loads(data)
|
||||||
|
else:
|
||||||
|
payload = dict(data)
|
||||||
|
|
||||||
|
version = payload.get("version")
|
||||||
|
if version != "1.0":
|
||||||
|
raise ValueError(f"Unsupported GraphRuntimeState snapshot version: {version}")
|
||||||
|
|
||||||
|
start_at = float(payload.get("start_at", 0.0))
|
||||||
|
|
||||||
|
total_tokens = int(payload.get("total_tokens", 0))
|
||||||
|
if total_tokens < 0:
|
||||||
|
raise ValueError("total_tokens must be non-negative")
|
||||||
|
|
||||||
|
node_run_steps = int(payload.get("node_run_steps", 0))
|
||||||
|
if node_run_steps < 0:
|
||||||
|
raise ValueError("node_run_steps must be non-negative")
|
||||||
|
|
||||||
|
llm_usage_payload = payload.get("llm_usage", {})
|
||||||
|
llm_usage = LLMUsage.model_validate(llm_usage_payload)
|
||||||
|
|
||||||
|
outputs_payload = deepcopy(payload.get("outputs", {}))
|
||||||
|
|
||||||
|
variable_pool_payload = payload.get("variable_pool")
|
||||||
|
has_variable_pool = variable_pool_payload is not None
|
||||||
|
variable_pool = VariablePool.model_validate(variable_pool_payload) if has_variable_pool else VariablePool()
|
||||||
|
|
||||||
|
ready_queue_payload = payload.get("ready_queue")
|
||||||
|
graph_execution_payload = payload.get("graph_execution")
|
||||||
|
response_payload = payload.get("response_coordinator")
|
||||||
|
paused_nodes_payload = payload.get("paused_nodes", [])
|
||||||
|
|
||||||
|
return _GraphRuntimeStateSnapshot(
|
||||||
|
start_at=start_at,
|
||||||
|
total_tokens=total_tokens,
|
||||||
|
node_run_steps=node_run_steps,
|
||||||
|
llm_usage=llm_usage,
|
||||||
|
outputs=outputs_payload,
|
||||||
|
variable_pool=variable_pool,
|
||||||
|
has_variable_pool=has_variable_pool,
|
||||||
|
ready_queue_dump=ready_queue_payload,
|
||||||
|
graph_execution_dump=graph_execution_payload,
|
||||||
|
response_coordinator_dump=response_payload,
|
||||||
|
paused_nodes=tuple(map(str, paused_nodes_payload)),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _apply_snapshot(self, snapshot: _GraphRuntimeStateSnapshot) -> None:
|
||||||
|
self._start_at = snapshot.start_at
|
||||||
|
self._total_tokens = snapshot.total_tokens
|
||||||
|
self._node_run_steps = snapshot.node_run_steps
|
||||||
|
self._llm_usage = snapshot.llm_usage.model_copy()
|
||||||
|
self._outputs = deepcopy(snapshot.outputs)
|
||||||
|
if snapshot.has_variable_pool or self._variable_pool is None:
|
||||||
|
self._variable_pool = snapshot.variable_pool
|
||||||
|
|
||||||
|
self._restore_ready_queue(snapshot.ready_queue_dump)
|
||||||
|
self._restore_graph_execution(snapshot.graph_execution_dump)
|
||||||
|
self._restore_response_coordinator(snapshot.response_coordinator_dump)
|
||||||
|
self._paused_nodes = set(snapshot.paused_nodes)
|
||||||
|
|
||||||
|
def _restore_ready_queue(self, payload: str | None) -> None:
|
||||||
|
if payload is not None:
|
||||||
|
self._ready_queue = self._build_ready_queue()
|
||||||
|
self._ready_queue.loads(payload)
|
||||||
|
else:
|
||||||
|
self._ready_queue = None
|
||||||
|
|
||||||
|
def _restore_graph_execution(self, payload: str | None) -> None:
|
||||||
|
self._graph_execution = None
|
||||||
|
self._pending_graph_execution_workflow_id = None
|
||||||
|
|
||||||
|
if payload is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
execution_payload = json.loads(payload)
|
||||||
|
self._pending_graph_execution_workflow_id = execution_payload.get("workflow_id")
|
||||||
|
except (json.JSONDecodeError, TypeError, AttributeError):
|
||||||
|
self._pending_graph_execution_workflow_id = None
|
||||||
|
|
||||||
|
self.graph_execution.loads(payload)
|
||||||
|
|
||||||
|
def _restore_response_coordinator(self, payload: str | None) -> None:
|
||||||
|
if payload is None:
|
||||||
|
self._pending_response_coordinator_dump = None
|
||||||
|
self._response_coordinator = None
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._graph is not None:
|
||||||
|
self.response_coordinator.loads(payload)
|
||||||
|
self._pending_response_coordinator_dump = None
|
||||||
|
return
|
||||||
|
|
||||||
|
self._pending_response_coordinator_dump = payload
|
||||||
|
self._response_coordinator = None
|
||||||
|
|||||||
@ -6,10 +6,11 @@ from flask_login import user_loaded_from_request, user_logged_in
|
|||||||
from werkzeug.exceptions import NotFound, Unauthorized
|
from werkzeug.exceptions import NotFound, Unauthorized
|
||||||
|
|
||||||
from configs import dify_config
|
from configs import dify_config
|
||||||
|
from constants import HEADER_NAME_APP_CODE
|
||||||
from dify_app import DifyApp
|
from dify_app import DifyApp
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
from libs.passport import PassportService
|
from libs.passport import PassportService
|
||||||
from libs.token import extract_access_token
|
from libs.token import extract_access_token, extract_webapp_passport
|
||||||
from models import Account, Tenant, TenantAccountJoin
|
from models import Account, Tenant, TenantAccountJoin
|
||||||
from models.model import AppMCPServer, EndUser
|
from models.model import AppMCPServer, EndUser
|
||||||
from services.account_service import AccountService
|
from services.account_service import AccountService
|
||||||
@ -61,14 +62,30 @@ def load_user_from_request(request_from_flask_login):
|
|||||||
logged_in_account = AccountService.load_logged_in_account(account_id=user_id)
|
logged_in_account = AccountService.load_logged_in_account(account_id=user_id)
|
||||||
return logged_in_account
|
return logged_in_account
|
||||||
elif request.blueprint == "web":
|
elif request.blueprint == "web":
|
||||||
decoded = PassportService().verify(auth_token)
|
app_code = request.headers.get(HEADER_NAME_APP_CODE)
|
||||||
end_user_id = decoded.get("end_user_id")
|
webapp_token = extract_webapp_passport(app_code, request) if app_code else None
|
||||||
if not end_user_id:
|
|
||||||
raise Unauthorized("Invalid Authorization token.")
|
if webapp_token:
|
||||||
end_user = db.session.query(EndUser).where(EndUser.id == decoded["end_user_id"]).first()
|
decoded = PassportService().verify(webapp_token)
|
||||||
if not end_user:
|
end_user_id = decoded.get("end_user_id")
|
||||||
raise NotFound("End user not found.")
|
if not end_user_id:
|
||||||
return end_user
|
raise Unauthorized("Invalid Authorization token.")
|
||||||
|
end_user = db.session.query(EndUser).where(EndUser.id == end_user_id).first()
|
||||||
|
if not end_user:
|
||||||
|
raise NotFound("End user not found.")
|
||||||
|
return end_user
|
||||||
|
else:
|
||||||
|
if not auth_token:
|
||||||
|
raise Unauthorized("Invalid Authorization token.")
|
||||||
|
decoded = PassportService().verify(auth_token)
|
||||||
|
end_user_id = decoded.get("end_user_id")
|
||||||
|
if end_user_id:
|
||||||
|
end_user = db.session.query(EndUser).where(EndUser.id == end_user_id).first()
|
||||||
|
if not end_user:
|
||||||
|
raise NotFound("End user not found.")
|
||||||
|
return end_user
|
||||||
|
else:
|
||||||
|
raise Unauthorized("Invalid Authorization token for web API.")
|
||||||
elif request.blueprint == "mcp":
|
elif request.blueprint == "mcp":
|
||||||
server_code = request.view_args.get("server_code") if request.view_args else None
|
server_code = request.view_args.get("server_code") if request.view_args else None
|
||||||
if not server_code:
|
if not server_code:
|
||||||
|
|||||||
@ -38,9 +38,6 @@ def _real_cookie_name(cookie_name: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _try_extract_from_header(request: Request) -> str | None:
|
def _try_extract_from_header(request: Request) -> str | None:
|
||||||
"""
|
|
||||||
Try to extract access token from header
|
|
||||||
"""
|
|
||||||
auth_header = request.headers.get("Authorization")
|
auth_header = request.headers.get("Authorization")
|
||||||
if auth_header:
|
if auth_header:
|
||||||
if " " not in auth_header:
|
if " " not in auth_header:
|
||||||
@ -55,27 +52,19 @@ def _try_extract_from_header(request: Request) -> str | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def extract_refresh_token(request: Request) -> str | None:
|
||||||
|
return request.cookies.get(_real_cookie_name(COOKIE_NAME_REFRESH_TOKEN))
|
||||||
|
|
||||||
|
|
||||||
def extract_csrf_token(request: Request) -> str | None:
|
def extract_csrf_token(request: Request) -> str | None:
|
||||||
"""
|
|
||||||
Try to extract CSRF token from header or cookie.
|
|
||||||
"""
|
|
||||||
return request.headers.get(HEADER_NAME_CSRF_TOKEN)
|
return request.headers.get(HEADER_NAME_CSRF_TOKEN)
|
||||||
|
|
||||||
|
|
||||||
def extract_csrf_token_from_cookie(request: Request) -> str | None:
|
def extract_csrf_token_from_cookie(request: Request) -> str | None:
|
||||||
"""
|
|
||||||
Try to extract CSRF token from cookie.
|
|
||||||
"""
|
|
||||||
return request.cookies.get(_real_cookie_name(COOKIE_NAME_CSRF_TOKEN))
|
return request.cookies.get(_real_cookie_name(COOKIE_NAME_CSRF_TOKEN))
|
||||||
|
|
||||||
|
|
||||||
def extract_access_token(request: Request) -> str | None:
|
def extract_access_token(request: Request) -> str | None:
|
||||||
"""
|
|
||||||
Try to extract access token from cookie, header or params.
|
|
||||||
|
|
||||||
Access token is either for console session or webapp passport exchange.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _try_extract_from_cookie(request: Request) -> str | None:
|
def _try_extract_from_cookie(request: Request) -> str | None:
|
||||||
return request.cookies.get(_real_cookie_name(COOKIE_NAME_ACCESS_TOKEN))
|
return request.cookies.get(_real_cookie_name(COOKIE_NAME_ACCESS_TOKEN))
|
||||||
|
|
||||||
@ -83,20 +72,10 @@ def extract_access_token(request: Request) -> str | None:
|
|||||||
|
|
||||||
|
|
||||||
def extract_webapp_access_token(request: Request) -> str | None:
|
def extract_webapp_access_token(request: Request) -> str | None:
|
||||||
"""
|
|
||||||
Try to extract webapp access token from cookie, then header.
|
|
||||||
"""
|
|
||||||
|
|
||||||
return request.cookies.get(_real_cookie_name(COOKIE_NAME_WEBAPP_ACCESS_TOKEN)) or _try_extract_from_header(request)
|
return request.cookies.get(_real_cookie_name(COOKIE_NAME_WEBAPP_ACCESS_TOKEN)) or _try_extract_from_header(request)
|
||||||
|
|
||||||
|
|
||||||
def extract_webapp_passport(app_code: str, request: Request) -> str | None:
|
def extract_webapp_passport(app_code: str, request: Request) -> str | None:
|
||||||
"""
|
|
||||||
Try to extract app token from header or params.
|
|
||||||
|
|
||||||
Webapp access token (part of passport) is only used for webapp session.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _try_extract_passport_token_from_cookie(request: Request) -> str | None:
|
def _try_extract_passport_token_from_cookie(request: Request) -> str | None:
|
||||||
return request.cookies.get(_real_cookie_name(COOKIE_NAME_PASSPORT + "-" + app_code))
|
return request.cookies.get(_real_cookie_name(COOKIE_NAME_PASSPORT + "-" + app_code))
|
||||||
|
|
||||||
|
|||||||
@ -82,54 +82,51 @@ class AudioService:
|
|||||||
message_id: str | None = None,
|
message_id: str | None = None,
|
||||||
is_draft: bool = False,
|
is_draft: bool = False,
|
||||||
):
|
):
|
||||||
from app import app
|
|
||||||
|
|
||||||
def invoke_tts(text_content: str, app_model: App, voice: str | None = None, is_draft: bool = False):
|
def invoke_tts(text_content: str, app_model: App, voice: str | None = None, is_draft: bool = False):
|
||||||
with app.app_context():
|
if voice is None:
|
||||||
if voice is None:
|
if app_model.mode in {AppMode.ADVANCED_CHAT, AppMode.WORKFLOW}:
|
||||||
if app_model.mode in {AppMode.ADVANCED_CHAT, AppMode.WORKFLOW}:
|
if is_draft:
|
||||||
if is_draft:
|
workflow = WorkflowService().get_draft_workflow(app_model=app_model)
|
||||||
workflow = WorkflowService().get_draft_workflow(app_model=app_model)
|
else:
|
||||||
else:
|
workflow = app_model.workflow
|
||||||
workflow = app_model.workflow
|
if (
|
||||||
if (
|
workflow is None
|
||||||
workflow is None
|
or "text_to_speech" not in workflow.features_dict
|
||||||
or "text_to_speech" not in workflow.features_dict
|
or not workflow.features_dict["text_to_speech"].get("enabled")
|
||||||
or not workflow.features_dict["text_to_speech"].get("enabled")
|
):
|
||||||
):
|
raise ValueError("TTS is not enabled")
|
||||||
|
|
||||||
|
voice = workflow.features_dict["text_to_speech"].get("voice")
|
||||||
|
else:
|
||||||
|
if not is_draft:
|
||||||
|
if app_model.app_model_config is None:
|
||||||
|
raise ValueError("AppModelConfig not found")
|
||||||
|
text_to_speech_dict = app_model.app_model_config.text_to_speech_dict
|
||||||
|
|
||||||
|
if not text_to_speech_dict.get("enabled"):
|
||||||
raise ValueError("TTS is not enabled")
|
raise ValueError("TTS is not enabled")
|
||||||
|
|
||||||
voice = workflow.features_dict["text_to_speech"].get("voice")
|
voice = text_to_speech_dict.get("voice")
|
||||||
else:
|
|
||||||
if not is_draft:
|
|
||||||
if app_model.app_model_config is None:
|
|
||||||
raise ValueError("AppModelConfig not found")
|
|
||||||
text_to_speech_dict = app_model.app_model_config.text_to_speech_dict
|
|
||||||
|
|
||||||
if not text_to_speech_dict.get("enabled"):
|
model_manager = ModelManager()
|
||||||
raise ValueError("TTS is not enabled")
|
model_instance = model_manager.get_default_model_instance(
|
||||||
|
tenant_id=app_model.tenant_id, model_type=ModelType.TTS
|
||||||
voice = text_to_speech_dict.get("voice")
|
)
|
||||||
|
try:
|
||||||
model_manager = ModelManager()
|
if not voice:
|
||||||
model_instance = model_manager.get_default_model_instance(
|
voices = model_instance.get_tts_voices()
|
||||||
tenant_id=app_model.tenant_id, model_type=ModelType.TTS
|
if voices:
|
||||||
)
|
voice = voices[0].get("value")
|
||||||
try:
|
if not voice:
|
||||||
if not voice:
|
|
||||||
voices = model_instance.get_tts_voices()
|
|
||||||
if voices:
|
|
||||||
voice = voices[0].get("value")
|
|
||||||
if not voice:
|
|
||||||
raise ValueError("Sorry, no voice available.")
|
|
||||||
else:
|
|
||||||
raise ValueError("Sorry, no voice available.")
|
raise ValueError("Sorry, no voice available.")
|
||||||
|
else:
|
||||||
|
raise ValueError("Sorry, no voice available.")
|
||||||
|
|
||||||
return model_instance.invoke_tts(
|
return model_instance.invoke_tts(
|
||||||
content_text=text_content.strip(), user=end_user, tenant_id=app_model.tenant_id, voice=voice
|
content_text=text_content.strip(), user=end_user, tenant_id=app_model.tenant_id, voice=voice
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
if message_id:
|
if message_id:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -283,7 +283,7 @@ class VariableTruncator:
|
|||||||
break
|
break
|
||||||
|
|
||||||
remaining_budget = target_size - used_size
|
remaining_budget = target_size - used_size
|
||||||
if item is None or isinstance(item, (str, list, dict, bool, int, float)):
|
if item is None or isinstance(item, (str, list, dict, bool, int, float, UpdatedVariable)):
|
||||||
part_result = self._truncate_json_primitives(item, remaining_budget)
|
part_result = self._truncate_json_primitives(item, remaining_budget)
|
||||||
else:
|
else:
|
||||||
raise UnknownTypeError(f"got unknown type {type(item)} in array truncation")
|
raise UnknownTypeError(f"got unknown type {type(item)} in array truncation")
|
||||||
@ -373,6 +373,11 @@ class VariableTruncator:
|
|||||||
|
|
||||||
return _PartResult(truncated_obj, used_size, truncated)
|
return _PartResult(truncated_obj, used_size, truncated)
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def _truncate_json_primitives(
|
||||||
|
self, val: UpdatedVariable, target_size: int
|
||||||
|
) -> _PartResult[Mapping[str, object]]: ...
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def _truncate_json_primitives(self, val: str, target_size: int) -> _PartResult[str]: ...
|
def _truncate_json_primitives(self, val: str, target_size: int) -> _PartResult[str]: ...
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,18 @@ from core.model_runtime.entities.llm_entities import LLMUsage
|
|||||||
from core.workflow.runtime import GraphRuntimeState, ReadOnlyGraphRuntimeStateWrapper, VariablePool
|
from core.workflow.runtime import GraphRuntimeState, ReadOnlyGraphRuntimeStateWrapper, VariablePool
|
||||||
|
|
||||||
|
|
||||||
|
class StubCoordinator:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.state = "initial"
|
||||||
|
|
||||||
|
def dumps(self) -> str:
|
||||||
|
return json.dumps({"state": self.state})
|
||||||
|
|
||||||
|
def loads(self, data: str) -> None:
|
||||||
|
payload = json.loads(data)
|
||||||
|
self.state = payload["state"]
|
||||||
|
|
||||||
|
|
||||||
class TestGraphRuntimeState:
|
class TestGraphRuntimeState:
|
||||||
def test_property_getters_and_setters(self):
|
def test_property_getters_and_setters(self):
|
||||||
# FIXME(-LAN-): Mock VariablePool if needed
|
# FIXME(-LAN-): Mock VariablePool if needed
|
||||||
@ -191,17 +203,6 @@ class TestGraphRuntimeState:
|
|||||||
graph_execution.exceptions_count = 4
|
graph_execution.exceptions_count = 4
|
||||||
graph_execution.started = True
|
graph_execution.started = True
|
||||||
|
|
||||||
class StubCoordinator:
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.state = "initial"
|
|
||||||
|
|
||||||
def dumps(self) -> str:
|
|
||||||
return json.dumps({"state": self.state})
|
|
||||||
|
|
||||||
def loads(self, data: str) -> None:
|
|
||||||
payload = json.loads(data)
|
|
||||||
self.state = payload["state"]
|
|
||||||
|
|
||||||
mock_graph = MagicMock()
|
mock_graph = MagicMock()
|
||||||
stub = StubCoordinator()
|
stub = StubCoordinator()
|
||||||
with patch.object(GraphRuntimeState, "_build_response_coordinator", return_value=stub):
|
with patch.object(GraphRuntimeState, "_build_response_coordinator", return_value=stub):
|
||||||
@ -211,8 +212,7 @@ class TestGraphRuntimeState:
|
|||||||
|
|
||||||
snapshot = state.dumps()
|
snapshot = state.dumps()
|
||||||
|
|
||||||
restored = GraphRuntimeState(variable_pool=VariablePool(), start_at=0.0)
|
restored = GraphRuntimeState.from_snapshot(snapshot)
|
||||||
restored.loads(snapshot)
|
|
||||||
|
|
||||||
assert restored.total_tokens == 10
|
assert restored.total_tokens == 10
|
||||||
assert restored.node_run_steps == 3
|
assert restored.node_run_steps == 3
|
||||||
@ -235,3 +235,47 @@ class TestGraphRuntimeState:
|
|||||||
restored.attach_graph(mock_graph)
|
restored.attach_graph(mock_graph)
|
||||||
|
|
||||||
assert new_stub.state == "configured"
|
assert new_stub.state == "configured"
|
||||||
|
|
||||||
|
def test_loads_rehydrates_existing_instance(self):
|
||||||
|
variable_pool = VariablePool()
|
||||||
|
variable_pool.add(("node", "key"), "value")
|
||||||
|
|
||||||
|
state = GraphRuntimeState(variable_pool=variable_pool, start_at=time())
|
||||||
|
state.total_tokens = 7
|
||||||
|
state.node_run_steps = 2
|
||||||
|
state.set_output("foo", "bar")
|
||||||
|
state.ready_queue.put("node-1")
|
||||||
|
|
||||||
|
execution = state.graph_execution
|
||||||
|
execution.workflow_id = "wf-456"
|
||||||
|
execution.started = True
|
||||||
|
|
||||||
|
mock_graph = MagicMock()
|
||||||
|
original_stub = StubCoordinator()
|
||||||
|
with patch.object(GraphRuntimeState, "_build_response_coordinator", return_value=original_stub):
|
||||||
|
state.attach_graph(mock_graph)
|
||||||
|
|
||||||
|
original_stub.state = "configured"
|
||||||
|
snapshot = state.dumps()
|
||||||
|
|
||||||
|
new_stub = StubCoordinator()
|
||||||
|
with patch.object(GraphRuntimeState, "_build_response_coordinator", return_value=new_stub):
|
||||||
|
restored = GraphRuntimeState(variable_pool=VariablePool(), start_at=0.0)
|
||||||
|
restored.attach_graph(mock_graph)
|
||||||
|
restored.loads(snapshot)
|
||||||
|
|
||||||
|
assert restored.total_tokens == 7
|
||||||
|
assert restored.node_run_steps == 2
|
||||||
|
assert restored.get_output("foo") == "bar"
|
||||||
|
assert restored.ready_queue.qsize() == 1
|
||||||
|
assert restored.ready_queue.get(timeout=0.01) == "node-1"
|
||||||
|
|
||||||
|
restored_segment = restored.variable_pool.get(("node", "key"))
|
||||||
|
assert restored_segment is not None
|
||||||
|
assert restored_segment.value == "value"
|
||||||
|
|
||||||
|
restored_execution = restored.graph_execution
|
||||||
|
assert restored_execution.workflow_id == "wf-456"
|
||||||
|
assert restored_execution.started is True
|
||||||
|
|
||||||
|
assert new_stub.state == "configured"
|
||||||
|
|||||||
@ -265,16 +265,18 @@ POSTGRES_MAINTENANCE_WORK_MEM=64MB
|
|||||||
POSTGRES_EFFECTIVE_CACHE_SIZE=4096MB
|
POSTGRES_EFFECTIVE_CACHE_SIZE=4096MB
|
||||||
|
|
||||||
# Sets the maximum allowed duration of any statement before termination.
|
# Sets the maximum allowed duration of any statement before termination.
|
||||||
# Default is 60000 milliseconds.
|
# Default is 0 (no timeout).
|
||||||
#
|
#
|
||||||
# Reference: https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-STATEMENT-TIMEOUT
|
# Reference: https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-STATEMENT-TIMEOUT
|
||||||
POSTGRES_STATEMENT_TIMEOUT=60000
|
# A value of 0 prevents the server from timing out statements.
|
||||||
|
POSTGRES_STATEMENT_TIMEOUT=0
|
||||||
|
|
||||||
# Sets the maximum allowed duration of any idle in-transaction session before termination.
|
# Sets the maximum allowed duration of any idle in-transaction session before termination.
|
||||||
# Default is 60000 milliseconds.
|
# Default is 0 (no timeout).
|
||||||
#
|
#
|
||||||
# Reference: https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-IDLE-IN-TRANSACTION-SESSION-TIMEOUT
|
# Reference: https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-IDLE-IN-TRANSACTION-SESSION-TIMEOUT
|
||||||
POSTGRES_IDLE_IN_TRANSACTION_SESSION_TIMEOUT=60000
|
# A value of 0 prevents the server from terminating idle sessions.
|
||||||
|
POSTGRES_IDLE_IN_TRANSACTION_SESSION_TIMEOUT=0
|
||||||
|
|
||||||
# ------------------------------
|
# ------------------------------
|
||||||
# Redis Configuration
|
# Redis Configuration
|
||||||
|
|||||||
@ -115,8 +115,8 @@ services:
|
|||||||
-c 'work_mem=${POSTGRES_WORK_MEM:-4MB}'
|
-c 'work_mem=${POSTGRES_WORK_MEM:-4MB}'
|
||||||
-c 'maintenance_work_mem=${POSTGRES_MAINTENANCE_WORK_MEM:-64MB}'
|
-c 'maintenance_work_mem=${POSTGRES_MAINTENANCE_WORK_MEM:-64MB}'
|
||||||
-c 'effective_cache_size=${POSTGRES_EFFECTIVE_CACHE_SIZE:-4096MB}'
|
-c 'effective_cache_size=${POSTGRES_EFFECTIVE_CACHE_SIZE:-4096MB}'
|
||||||
-c 'statement_timeout=${POSTGRES_STATEMENT_TIMEOUT:-60000}'
|
-c 'statement_timeout=${POSTGRES_STATEMENT_TIMEOUT:-0}'
|
||||||
-c 'idle_in_transaction_session_timeout=${POSTGRES_IDLE_IN_TRANSACTION_SESSION_TIMEOUT:-60000}'
|
-c 'idle_in_transaction_session_timeout=${POSTGRES_IDLE_IN_TRANSACTION_SESSION_TIMEOUT:-0}'
|
||||||
volumes:
|
volumes:
|
||||||
- ./volumes/db/data:/var/lib/postgresql/data
|
- ./volumes/db/data:/var/lib/postgresql/data
|
||||||
healthcheck:
|
healthcheck:
|
||||||
|
|||||||
@ -15,8 +15,8 @@ services:
|
|||||||
-c 'work_mem=${POSTGRES_WORK_MEM:-4MB}'
|
-c 'work_mem=${POSTGRES_WORK_MEM:-4MB}'
|
||||||
-c 'maintenance_work_mem=${POSTGRES_MAINTENANCE_WORK_MEM:-64MB}'
|
-c 'maintenance_work_mem=${POSTGRES_MAINTENANCE_WORK_MEM:-64MB}'
|
||||||
-c 'effective_cache_size=${POSTGRES_EFFECTIVE_CACHE_SIZE:-4096MB}'
|
-c 'effective_cache_size=${POSTGRES_EFFECTIVE_CACHE_SIZE:-4096MB}'
|
||||||
-c 'statement_timeout=${POSTGRES_STATEMENT_TIMEOUT:-60000}'
|
-c 'statement_timeout=${POSTGRES_STATEMENT_TIMEOUT:-0}'
|
||||||
-c 'idle_in_transaction_session_timeout=${POSTGRES_IDLE_IN_TRANSACTION_SESSION_TIMEOUT:-60000}'
|
-c 'idle_in_transaction_session_timeout=${POSTGRES_IDLE_IN_TRANSACTION_SESSION_TIMEOUT:-0}'
|
||||||
volumes:
|
volumes:
|
||||||
- ${PGDATA_HOST_VOLUME:-./volumes/db/data}:/var/lib/postgresql/data
|
- ${PGDATA_HOST_VOLUME:-./volumes/db/data}:/var/lib/postgresql/data
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@ -68,8 +68,8 @@ x-shared-env: &shared-api-worker-env
|
|||||||
POSTGRES_WORK_MEM: ${POSTGRES_WORK_MEM:-4MB}
|
POSTGRES_WORK_MEM: ${POSTGRES_WORK_MEM:-4MB}
|
||||||
POSTGRES_MAINTENANCE_WORK_MEM: ${POSTGRES_MAINTENANCE_WORK_MEM:-64MB}
|
POSTGRES_MAINTENANCE_WORK_MEM: ${POSTGRES_MAINTENANCE_WORK_MEM:-64MB}
|
||||||
POSTGRES_EFFECTIVE_CACHE_SIZE: ${POSTGRES_EFFECTIVE_CACHE_SIZE:-4096MB}
|
POSTGRES_EFFECTIVE_CACHE_SIZE: ${POSTGRES_EFFECTIVE_CACHE_SIZE:-4096MB}
|
||||||
POSTGRES_STATEMENT_TIMEOUT: ${POSTGRES_STATEMENT_TIMEOUT:-60000}
|
POSTGRES_STATEMENT_TIMEOUT: ${POSTGRES_STATEMENT_TIMEOUT:-0}
|
||||||
POSTGRES_IDLE_IN_TRANSACTION_SESSION_TIMEOUT: ${POSTGRES_IDLE_IN_TRANSACTION_SESSION_TIMEOUT:-60000}
|
POSTGRES_IDLE_IN_TRANSACTION_SESSION_TIMEOUT: ${POSTGRES_IDLE_IN_TRANSACTION_SESSION_TIMEOUT:-0}
|
||||||
REDIS_HOST: ${REDIS_HOST:-redis}
|
REDIS_HOST: ${REDIS_HOST:-redis}
|
||||||
REDIS_PORT: ${REDIS_PORT:-6379}
|
REDIS_PORT: ${REDIS_PORT:-6379}
|
||||||
REDIS_USERNAME: ${REDIS_USERNAME:-}
|
REDIS_USERNAME: ${REDIS_USERNAME:-}
|
||||||
@ -724,8 +724,8 @@ services:
|
|||||||
-c 'work_mem=${POSTGRES_WORK_MEM:-4MB}'
|
-c 'work_mem=${POSTGRES_WORK_MEM:-4MB}'
|
||||||
-c 'maintenance_work_mem=${POSTGRES_MAINTENANCE_WORK_MEM:-64MB}'
|
-c 'maintenance_work_mem=${POSTGRES_MAINTENANCE_WORK_MEM:-64MB}'
|
||||||
-c 'effective_cache_size=${POSTGRES_EFFECTIVE_CACHE_SIZE:-4096MB}'
|
-c 'effective_cache_size=${POSTGRES_EFFECTIVE_CACHE_SIZE:-4096MB}'
|
||||||
-c 'statement_timeout=${POSTGRES_STATEMENT_TIMEOUT:-60000}'
|
-c 'statement_timeout=${POSTGRES_STATEMENT_TIMEOUT:-0}'
|
||||||
-c 'idle_in_transaction_session_timeout=${POSTGRES_IDLE_IN_TRANSACTION_SESSION_TIMEOUT:-60000}'
|
-c 'idle_in_transaction_session_timeout=${POSTGRES_IDLE_IN_TRANSACTION_SESSION_TIMEOUT:-0}'
|
||||||
volumes:
|
volumes:
|
||||||
- ./volumes/db/data:/var/lib/postgresql/data
|
- ./volumes/db/data:/var/lib/postgresql/data
|
||||||
healthcheck:
|
healthcheck:
|
||||||
|
|||||||
@ -41,16 +41,18 @@ POSTGRES_MAINTENANCE_WORK_MEM=64MB
|
|||||||
POSTGRES_EFFECTIVE_CACHE_SIZE=4096MB
|
POSTGRES_EFFECTIVE_CACHE_SIZE=4096MB
|
||||||
|
|
||||||
# Sets the maximum allowed duration of any statement before termination.
|
# Sets the maximum allowed duration of any statement before termination.
|
||||||
# Default is 60000 milliseconds.
|
# Default is 0 (no timeout).
|
||||||
#
|
#
|
||||||
# Reference: https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-STATEMENT-TIMEOUT
|
# Reference: https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-STATEMENT-TIMEOUT
|
||||||
POSTGRES_STATEMENT_TIMEOUT=60000
|
# A value of 0 prevents the server from timing out statements.
|
||||||
|
POSTGRES_STATEMENT_TIMEOUT=0
|
||||||
|
|
||||||
# Sets the maximum allowed duration of any idle in-transaction session before termination.
|
# Sets the maximum allowed duration of any idle in-transaction session before termination.
|
||||||
# Default is 60000 milliseconds.
|
# Default is 0 (no timeout).
|
||||||
#
|
#
|
||||||
# Reference: https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-IDLE-IN-TRANSACTION-SESSION-TIMEOUT
|
# Reference: https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-IDLE-IN-TRANSACTION-SESSION-TIMEOUT
|
||||||
POSTGRES_IDLE_IN_TRANSACTION_SESSION_TIMEOUT=60000
|
# A value of 0 prevents the server from terminating idle sessions.
|
||||||
|
POSTGRES_IDLE_IN_TRANSACTION_SESSION_TIMEOUT=0
|
||||||
|
|
||||||
# -----------------------------
|
# -----------------------------
|
||||||
# Environment Variables for redis Service
|
# Environment Variables for redis Service
|
||||||
|
|||||||
@ -258,9 +258,9 @@ const ConfigVar: FC<IConfigVarProps> = ({ promptVariables, readonly, onPromptVar
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{hasVar && (
|
{hasVar && (
|
||||||
<div className='mt-1 px-3 pb-3'>
|
<div className={cn('mt-1 grid px-3 pb-3')}>
|
||||||
<ReactSortable
|
<ReactSortable
|
||||||
className='space-y-1'
|
className={cn('grid-col-1 grid space-y-1', readonly && 'grid-cols-2 gap-1 space-y-0')}
|
||||||
list={promptVariablesWithIds}
|
list={promptVariablesWithIds}
|
||||||
setList={(list) => { onPromptVariablesChange?.(list.map(item => item.variable)) }}
|
setList={(list) => { onPromptVariablesChange?.(list.map(item => item.variable)) }}
|
||||||
handle='.handle'
|
handle='.handle'
|
||||||
|
|||||||
@ -38,7 +38,7 @@ const VarItem: FC<ItemProps> = ({
|
|||||||
const [isDeleting, setIsDeleting] = useState(false)
|
const [isDeleting, setIsDeleting] = useState(false)
|
||||||
|
|
||||||
return (
|
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 opacity-30', 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', className)}>
|
||||||
<VarIcon className={cn('mr-1 h-4 w-4 shrink-0 text-text-accent', canDrag && 'group-hover:opacity-0')} />
|
<VarIcon className={cn('mr-1 h-4 w-4 shrink-0 text-text-accent', canDrag && 'group-hover:opacity-0')} />
|
||||||
{canDrag && (
|
{canDrag && (
|
||||||
<RiDraggable className='absolute left-3 top-3 hidden h-3 w-3 cursor-pointer text-text-tertiary group-hover:block' />
|
<RiDraggable className='absolute left-3 top-3 hidden h-3 w-3 cursor-pointer text-text-tertiary group-hover:block' />
|
||||||
|
|||||||
@ -13,10 +13,14 @@ import ConfigContext from '@/context/debug-configuration'
|
|||||||
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
|
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
|
||||||
import Switch from '@/app/components/base/switch'
|
import Switch from '@/app/components/base/switch'
|
||||||
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
|
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 ConfigVision: FC = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { isShowVisionConfig, isAllowVideoUpload } = useContext(ConfigContext)
|
const { isShowVisionConfig, isAllowVideoUpload, readonly } = useContext(ConfigContext)
|
||||||
const file = useFeatures(s => s.features.file)
|
const file = useFeatures(s => s.features.file)
|
||||||
const featuresStore = useFeaturesStore()
|
const featuresStore = useFeaturesStore()
|
||||||
|
|
||||||
@ -53,7 +57,7 @@ const ConfigVision: FC = () => {
|
|||||||
setFeatures(newFeatures)
|
setFeatures(newFeatures)
|
||||||
}, [featuresStore, isAllowVideoUpload])
|
}, [featuresStore, isAllowVideoUpload])
|
||||||
|
|
||||||
if (!isShowVisionConfig)
|
if (!isShowVisionConfig || (readonly && !isImageEnabled))
|
||||||
return null
|
return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -74,37 +78,49 @@ const ConfigVision: FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex shrink-0 items-center'>
|
<div className='flex shrink-0 items-center'>
|
||||||
{/* <div className='mr-2 flex items-center gap-0.5'>
|
{readonly ? (<>
|
||||||
<div className='text-text-tertiary system-xs-medium-uppercase'>{t('appDebug.vision.visionSettings.resolution')}</div>
|
<div className='mr-2 flex items-center gap-0.5'>
|
||||||
<Tooltip
|
<div className='system-xs-medium-uppercase text-text-tertiary'>{t('appDebug.vision.visionSettings.resolution')}</div>
|
||||||
popupContent={
|
<Tooltip
|
||||||
<div className='w-[180px]' >
|
popupContent={
|
||||||
{t('appDebug.vision.visionSettings.resolutionTooltip').split('\n').map(item => (
|
<div className='w-[180px]' >
|
||||||
<div key={item}>{item}</div>
|
{t('appDebug.vision.visionSettings.resolutionTooltip').split('\n').map(item => (
|
||||||
))}
|
<div key={item}>{item}</div>
|
||||||
</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> */}
|
</>}
|
||||||
{/* <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>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -37,7 +37,7 @@ type AgentToolWithMoreInfo = AgentTool & { icon: any; collection?: Collection }
|
|||||||
const AgentTools: FC = () => {
|
const AgentTools: FC = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [isShowChooseTool, setIsShowChooseTool] = useState(false)
|
const [isShowChooseTool, setIsShowChooseTool] = useState(false)
|
||||||
const { modelConfig, setModelConfig } = useContext(ConfigContext)
|
const { readonly, modelConfig, setModelConfig } = useContext(ConfigContext)
|
||||||
const { data: buildInTools } = useAllBuiltInTools()
|
const { data: buildInTools } = useAllBuiltInTools()
|
||||||
const { data: customTools } = useAllCustomTools()
|
const { data: customTools } = useAllCustomTools()
|
||||||
const { data: workflowTools } = useAllWorkflowTools()
|
const { data: workflowTools } = useAllWorkflowTools()
|
||||||
@ -158,7 +158,7 @@ const AgentTools: FC = () => {
|
|||||||
headerRight={
|
headerRight={
|
||||||
<div className='flex items-center'>
|
<div className='flex items-center'>
|
||||||
<div className='text-xs font-normal leading-[18px] text-text-tertiary'>{tools.filter(item => !!item.enabled).length}/{tools.length} {t('appDebug.agent.tools.enabled')}</div>
|
<div className='text-xs font-normal leading-[18px] text-text-tertiary'>{tools.filter(item => !!item.enabled).length}/{tools.length} {t('appDebug.agent.tools.enabled')}</div>
|
||||||
{tools.length < MAX_TOOLS_NUM && (
|
{tools.length < MAX_TOOLS_NUM && !readonly && (
|
||||||
<>
|
<>
|
||||||
<div className='ml-3 mr-1 h-3.5 w-px bg-divider-regular'></div>
|
<div className='ml-3 mr-1 h-3.5 w-px bg-divider-regular'></div>
|
||||||
<ToolPicker
|
<ToolPicker
|
||||||
@ -177,7 +177,7 @@ const AgentTools: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className='grid grid-cols-1 flex-wrap items-center justify-between gap-1 2xl:grid-cols-2'>
|
<div className={cn('grid grid-cols-1 flex-wrap items-center justify-between gap-1 2xl:grid-cols-2', readonly && 'grid-cols-2')}>
|
||||||
{tools.map((item: AgentTool & { icon: any; collection?: Collection }, index) => (
|
{tools.map((item: AgentTool & { icon: any; collection?: Collection }, index) => (
|
||||||
<div key={index}
|
<div key={index}
|
||||||
className={cn(
|
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='system-xs-medium pr-1.5 text-text-secondary'>{getProviderShowName(item)}</span>
|
||||||
<span className='text-text-tertiary'>{item.tool_label}</span>
|
<span className='text-text-tertiary'>{item.tool_label}</span>
|
||||||
{!item.isDeleted && (
|
{!item.isDeleted && !readonly && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
popupContent={
|
popupContent={
|
||||||
<div className='w-[180px]'>
|
<div className='w-[180px]'>
|
||||||
@ -212,7 +212,7 @@ const AgentTools: FC = () => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className='h-4 w-4'>
|
<div className='h-4 w-4'>
|
||||||
<div className='ml-0.5 hidden group-hover:inline-block'>
|
<div className={cn('ml-0.5 hidden group-hover:inline-block')}>
|
||||||
<RiInformation2Line className='h-4 w-4 text-text-tertiary' />
|
<RiInformation2Line className='h-4 w-4 text-text-tertiary' />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -246,8 +246,8 @@ const AgentTools: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!item.isDeleted && (
|
{!item.isDeleted && !readonly && (
|
||||||
<div className='mr-2 hidden items-center gap-1 group-hover:flex'>
|
<div className={cn('mr-2 hidden items-center gap-1 group-hover:flex')}>
|
||||||
{!item.notAuthor && (
|
{!item.notAuthor && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
popupContent={t('tools.setBuiltInTools.infoAndSetting')}
|
popupContent={t('tools.setBuiltInTools.infoAndSetting')}
|
||||||
@ -280,7 +280,7 @@ const AgentTools: FC = () => {
|
|||||||
{!item.notAuthor && (
|
{!item.notAuthor && (
|
||||||
<Switch
|
<Switch
|
||||||
defaultValue={item.isDeleted ? false : item.enabled}
|
defaultValue={item.isDeleted ? false : item.enabled}
|
||||||
disabled={item.isDeleted}
|
disabled={item.isDeleted || readonly}
|
||||||
size='md'
|
size='md'
|
||||||
onChange={(enabled) => {
|
onChange={(enabled) => {
|
||||||
const newModelConfig = produce(modelConfig, (draft) => {
|
const newModelConfig = produce(modelConfig, (draft) => {
|
||||||
|
|||||||
@ -16,7 +16,7 @@ const ConfigAudio: FC = () => {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const file = useFeatures(s => s.features.file)
|
const file = useFeatures(s => s.features.file)
|
||||||
const featuresStore = useFeaturesStore()
|
const featuresStore = useFeaturesStore()
|
||||||
const { isShowAudioConfig } = useContext(ConfigContext)
|
const { isShowAudioConfig, readonly } = useContext(ConfigContext)
|
||||||
|
|
||||||
const isAudioEnabled = file?.allowed_file_types?.includes(SupportUploadFileTypes.audio) ?? false
|
const isAudioEnabled = file?.allowed_file_types?.includes(SupportUploadFileTypes.audio) ?? false
|
||||||
|
|
||||||
@ -44,7 +44,7 @@ const ConfigAudio: FC = () => {
|
|||||||
setFeatures(newFeatures)
|
setFeatures(newFeatures)
|
||||||
}, [featuresStore])
|
}, [featuresStore])
|
||||||
|
|
||||||
if (!isShowAudioConfig)
|
if (!isShowAudioConfig || (readonly && !isAudioEnabled))
|
||||||
return null
|
return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -64,14 +64,16 @@ const ConfigAudio: FC = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex shrink-0 items-center'>
|
{!readonly && (
|
||||||
<div className='ml-1 mr-3 h-3.5 w-[1px] bg-divider-subtle'></div>
|
<div className='flex shrink-0 items-center'>
|
||||||
<Switch
|
<div className='ml-1 mr-3 h-3.5 w-[1px] bg-divider-subtle'></div>
|
||||||
defaultValue={isAudioEnabled}
|
<Switch
|
||||||
onChange={handleChange}
|
defaultValue={isAudioEnabled}
|
||||||
size='md'
|
onChange={handleChange}
|
||||||
/>
|
size='md'
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,7 +16,7 @@ const ConfigDocument: FC = () => {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const file = useFeatures(s => s.features.file)
|
const file = useFeatures(s => s.features.file)
|
||||||
const featuresStore = useFeaturesStore()
|
const featuresStore = useFeaturesStore()
|
||||||
const { isShowDocumentConfig } = useContext(ConfigContext)
|
const { isShowDocumentConfig, readonly } = useContext(ConfigContext)
|
||||||
|
|
||||||
const isDocumentEnabled = file?.allowed_file_types?.includes(SupportUploadFileTypes.document) ?? false
|
const isDocumentEnabled = file?.allowed_file_types?.includes(SupportUploadFileTypes.document) ?? false
|
||||||
|
|
||||||
@ -44,7 +44,7 @@ const ConfigDocument: FC = () => {
|
|||||||
setFeatures(newFeatures)
|
setFeatures(newFeatures)
|
||||||
}, [featuresStore])
|
}, [featuresStore])
|
||||||
|
|
||||||
if (!isShowDocumentConfig)
|
if (!isShowDocumentConfig || (readonly && !isDocumentEnabled))
|
||||||
return null
|
return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -64,14 +64,16 @@ const ConfigDocument: FC = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex shrink-0 items-center'>
|
{!readonly && (
|
||||||
<div className='ml-1 mr-3 h-3.5 w-[1px] bg-divider-subtle'></div>
|
<div className='flex shrink-0 items-center'>
|
||||||
<Switch
|
<div className='ml-1 mr-3 h-3.5 w-[1px] bg-divider-subtle'></div>
|
||||||
defaultValue={isDocumentEnabled}
|
<Switch
|
||||||
onChange={handleChange}
|
defaultValue={isDocumentEnabled}
|
||||||
size='md'
|
onChange={handleChange}
|
||||||
/>
|
size='md'
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import { ModelModeType } from '@/types/app'
|
|||||||
|
|
||||||
const Config: FC = () => {
|
const Config: FC = () => {
|
||||||
const {
|
const {
|
||||||
|
readonly,
|
||||||
mode,
|
mode,
|
||||||
isAdvancedMode,
|
isAdvancedMode,
|
||||||
modelModeType,
|
modelModeType,
|
||||||
@ -28,6 +29,7 @@ const Config: FC = () => {
|
|||||||
modelConfig,
|
modelConfig,
|
||||||
setModelConfig,
|
setModelConfig,
|
||||||
setPrevPromptConfig,
|
setPrevPromptConfig,
|
||||||
|
dataSets,
|
||||||
} = useContext(ConfigContext)
|
} = useContext(ConfigContext)
|
||||||
const isChatApp = ['advanced-chat', 'agent-chat', 'chat'].includes(mode)
|
const isChatApp = ['advanced-chat', 'agent-chat', 'chat'].includes(mode)
|
||||||
const formattingChangedDispatcher = useFormattingChangedDispatcher()
|
const formattingChangedDispatcher = useFormattingChangedDispatcher()
|
||||||
@ -66,19 +68,28 @@ const Config: FC = () => {
|
|||||||
promptTemplate={promptTemplate}
|
promptTemplate={promptTemplate}
|
||||||
promptVariables={promptVariables}
|
promptVariables={promptVariables}
|
||||||
onChange={handlePromptChange}
|
onChange={handlePromptChange}
|
||||||
|
readonly={readonly}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Variables */}
|
{/* Variables */}
|
||||||
<ConfigVar
|
{!(readonly && promptVariables.length === 0) && (
|
||||||
promptVariables={promptVariables}
|
<ConfigVar
|
||||||
onPromptVariablesChange={handlePromptVariablesNameChange}
|
promptVariables={promptVariables}
|
||||||
/>
|
onPromptVariablesChange={handlePromptVariablesNameChange}
|
||||||
|
readonly={readonly}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Dataset */}
|
{/* Dataset */}
|
||||||
<DatasetConfig />
|
{!(readonly && dataSets.length === 0) && (
|
||||||
|
<DatasetConfig
|
||||||
|
readonly={readonly}
|
||||||
|
hideMetadataFilter={readonly}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
{/* Tools */}
|
{/* Tools */}
|
||||||
{isAgent && (
|
{isAgent && !(readonly && modelConfig.agentConfig.tools.length === 0) && (
|
||||||
<AgentTools />
|
<AgentTools />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -89,7 +100,7 @@ const Config: FC = () => {
|
|||||||
<ConfigAudio />
|
<ConfigAudio />
|
||||||
|
|
||||||
{/* Chat History */}
|
{/* Chat History */}
|
||||||
{isAdvancedMode && isChatApp && modelModeType === ModelModeType.completion && (
|
{!readonly && isAdvancedMode && isChatApp && modelModeType === ModelModeType.completion && (
|
||||||
<HistoryPanel
|
<HistoryPanel
|
||||||
showWarning={!hasSetBlockStatus.history}
|
showWarning={!hasSetBlockStatus.history}
|
||||||
onShowEditModal={showHistoryModal}
|
onShowEditModal={showHistoryModal}
|
||||||
|
|||||||
@ -29,6 +29,7 @@ const Item: FC<ItemProps> = ({
|
|||||||
config,
|
config,
|
||||||
onSave,
|
onSave,
|
||||||
onRemove,
|
onRemove,
|
||||||
|
readonly = false,
|
||||||
editable = true,
|
editable = true,
|
||||||
}) => {
|
}) => {
|
||||||
const media = useBreakpoints()
|
const media = useBreakpoints()
|
||||||
@ -55,6 +56,7 @@ const Item: FC<ItemProps> = ({
|
|||||||
<div className={cn(
|
<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',
|
'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',
|
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'>
|
<div className='flex w-0 grow items-center space-x-1.5'>
|
||||||
<AppIcon
|
<AppIcon
|
||||||
@ -68,7 +70,7 @@ const Item: FC<ItemProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className='ml-2 hidden shrink-0 items-center space-x-1 group-hover:flex'>
|
<div className='ml-2 hidden shrink-0 items-center space-x-1 group-hover:flex'>
|
||||||
{
|
{
|
||||||
editable && <ActionButton
|
editable && !readonly && <ActionButton
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
setShowSettingsModal(true)
|
setShowSettingsModal(true)
|
||||||
@ -77,14 +79,18 @@ const Item: FC<ItemProps> = ({
|
|||||||
<RiEditLine className='h-4 w-4 shrink-0 text-text-tertiary' />
|
<RiEditLine className='h-4 w-4 shrink-0 text-text-tertiary' />
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
}
|
}
|
||||||
<ActionButton
|
{
|
||||||
onClick={() => onRemove(config.id)}
|
!readonly && (
|
||||||
state={isDeleting ? ActionButtonState.Destructive : ActionButtonState.Default}
|
<ActionButton
|
||||||
onMouseEnter={() => setIsDeleting(true)}
|
onClick={() => onRemove(config.id)}
|
||||||
onMouseLeave={() => setIsDeleting(false)}
|
state={isDeleting ? ActionButtonState.Destructive : ActionButtonState.Default}
|
||||||
>
|
onMouseEnter={() => setIsDeleting(true)}
|
||||||
<RiDeleteBinLine className={cn('h-4 w-4 shrink-0 text-text-tertiary', isDeleting && 'text-text-destructive')} />
|
onMouseLeave={() => setIsDeleting(false)}
|
||||||
</ActionButton>
|
>
|
||||||
|
<RiDeleteBinLine className={cn('h-4 w-4 shrink-0 text-text-tertiary', isDeleting && 'text-text-destructive')} />
|
||||||
|
</ActionButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
{
|
{
|
||||||
config.indexing_technique && <Badge
|
config.indexing_technique && <Badge
|
||||||
@ -98,13 +104,15 @@ const Item: FC<ItemProps> = ({
|
|||||||
text={t('dataset.externalTag') as string}
|
text={t('dataset.externalTag') as string}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
<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'>
|
{showSettingsModal && (
|
||||||
<SettingsModal
|
<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'>
|
||||||
currentDataset={config}
|
<SettingsModal
|
||||||
onCancel={() => setShowSettingsModal(false)}
|
currentDataset={config}
|
||||||
onSave={handleSave}
|
onCancel={() => setShowSettingsModal(false)}
|
||||||
/>
|
onSave={handleSave}
|
||||||
</Drawer>
|
/>
|
||||||
|
</Drawer>
|
||||||
|
)}
|
||||||
</div >
|
</div >
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,8 +36,13 @@ import {
|
|||||||
LogicalOperator,
|
LogicalOperator,
|
||||||
MetadataFilteringVariableType,
|
MetadataFilteringVariableType,
|
||||||
} from '@/app/components/workflow/nodes/knowledge-retrieval/types'
|
} from '@/app/components/workflow/nodes/knowledge-retrieval/types'
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
|
||||||
const DatasetConfig: FC = () => {
|
type Props = {
|
||||||
|
readonly?: boolean
|
||||||
|
hideMetadataFilter?: boolean
|
||||||
|
}
|
||||||
|
const DatasetConfig: FC<Props> = ({ readonly, hideMetadataFilter }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const userProfile = useAppContextSelector(s => s.userProfile)
|
const userProfile = useAppContextSelector(s => s.userProfile)
|
||||||
const {
|
const {
|
||||||
@ -254,24 +259,25 @@ const DatasetConfig: FC = () => {
|
|||||||
className='mt-2'
|
className='mt-2'
|
||||||
title={t('appDebug.feature.dataSet.title')}
|
title={t('appDebug.feature.dataSet.title')}
|
||||||
headerRight={
|
headerRight={
|
||||||
<div className='flex items-center gap-1'>
|
!readonly && (<div className='flex items-center gap-1'>
|
||||||
{!isAgent && <ParamsConfig disabled={!hasData} selectedDatasets={dataSet} />}
|
{!isAgent && <ParamsConfig disabled={!hasData} selectedDatasets={dataSet} />}
|
||||||
<OperationBtn type="add" onClick={showSelectDataSet} />
|
<OperationBtn type="add" onClick={showSelectDataSet} />
|
||||||
</div>
|
</div>)
|
||||||
}
|
}
|
||||||
hasHeaderBottomBorder={!hasData}
|
hasHeaderBottomBorder={!hasData}
|
||||||
noBodySpacing
|
noBodySpacing
|
||||||
>
|
>
|
||||||
{hasData
|
{hasData
|
||||||
? (
|
? (
|
||||||
<div className='mt-1 flex flex-wrap justify-between px-3 pb-3'>
|
<div className={cn('mt-1 grid grid-cols-1 px-3 pb-3', readonly && 'grid-cols-2 gap-1')}>
|
||||||
{formattedDataset.map(item => (
|
{formattedDataset.map(item => (
|
||||||
<CardItem
|
<CardItem
|
||||||
key={item.id}
|
key={item.id}
|
||||||
config={item}
|
config={item}
|
||||||
onRemove={onRemove}
|
onRemove={onRemove}
|
||||||
onSave={handleSave}
|
onSave={handleSave}
|
||||||
editable={item.editable}
|
editable={item.editable && !readonly}
|
||||||
|
readonly={readonly}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -282,27 +288,29 @@ const DatasetConfig: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className='border-t border-t-divider-subtle py-2'>
|
{!hideMetadataFilter && (
|
||||||
<MetadataFilter
|
<div className='border-t border-t-divider-subtle py-2'>
|
||||||
metadataList={metadataList}
|
<MetadataFilter
|
||||||
selectedDatasetsLoaded
|
metadataList={metadataList}
|
||||||
metadataFilterMode={datasetConfigs.metadata_filtering_mode}
|
selectedDatasetsLoaded
|
||||||
metadataFilteringConditions={datasetConfigs.metadata_filtering_conditions}
|
metadataFilterMode={datasetConfigs.metadata_filtering_mode}
|
||||||
handleAddCondition={handleAddCondition}
|
metadataFilteringConditions={datasetConfigs.metadata_filtering_conditions}
|
||||||
handleMetadataFilterModeChange={handleMetadataFilterModeChange}
|
handleAddCondition={handleAddCondition}
|
||||||
handleRemoveCondition={handleRemoveCondition}
|
handleMetadataFilterModeChange={handleMetadataFilterModeChange}
|
||||||
handleToggleConditionLogicalOperator={handleToggleConditionLogicalOperator}
|
handleRemoveCondition={handleRemoveCondition}
|
||||||
handleUpdateCondition={handleUpdateCondition}
|
handleToggleConditionLogicalOperator={handleToggleConditionLogicalOperator}
|
||||||
metadataModelConfig={datasetConfigs.metadata_model_config}
|
handleUpdateCondition={handleUpdateCondition}
|
||||||
handleMetadataModelChange={handleMetadataModelChange}
|
metadataModelConfig={datasetConfigs.metadata_model_config}
|
||||||
handleMetadataCompletionParamsChange={handleMetadataCompletionParamsChange}
|
handleMetadataModelChange={handleMetadataModelChange}
|
||||||
isCommonVariable
|
handleMetadataCompletionParamsChange={handleMetadataCompletionParamsChange}
|
||||||
availableCommonStringVars={promptVariablesToSelect.filter(item => item.type === MetadataFilteringVariableType.string || item.type === MetadataFilteringVariableType.select)}
|
isCommonVariable
|
||||||
availableCommonNumberVars={promptVariablesToSelect.filter(item => item.type === MetadataFilteringVariableType.number)}
|
availableCommonStringVars={promptVariablesToSelect.filter(item => item.type === MetadataFilteringVariableType.string || item.type === MetadataFilteringVariableType.select)}
|
||||||
/>
|
availableCommonNumberVars={promptVariablesToSelect.filter(item => item.type === MetadataFilteringVariableType.number)}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{mode === AppType.completion && dataSet.length > 0 && (
|
{!readonly && mode === AppType.completion && dataSet.length > 0 && (
|
||||||
<ContextVar
|
<ContextVar
|
||||||
value={selectedContextVar?.key}
|
value={selectedContextVar?.key}
|
||||||
options={promptVariablesToSelect}
|
options={promptVariablesToSelect}
|
||||||
|
|||||||
@ -18,7 +18,7 @@ const ChatUserInput = ({
|
|||||||
inputs,
|
inputs,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { modelConfig, setInputs } = useContext(ConfigContext)
|
const { modelConfig, setInputs, readonly } = useContext(ConfigContext)
|
||||||
|
|
||||||
const promptVariables = modelConfig.configs.prompt_variables.filter(({ key, name }) => {
|
const promptVariables = modelConfig.configs.prompt_variables.filter(({ key, name }) => {
|
||||||
return key && key?.trim() && name && name?.trim()
|
return key && key?.trim() && name && name?.trim()
|
||||||
@ -70,6 +70,7 @@ const ChatUserInput = ({
|
|||||||
placeholder={name}
|
placeholder={name}
|
||||||
autoFocus={index === 0}
|
autoFocus={index === 0}
|
||||||
maxLength={max_length || DEFAULT_VALUE_MAX_LEN}
|
maxLength={max_length || DEFAULT_VALUE_MAX_LEN}
|
||||||
|
readOnly={readonly}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{type === 'paragraph' && (
|
{type === 'paragraph' && (
|
||||||
@ -78,6 +79,7 @@ const ChatUserInput = ({
|
|||||||
placeholder={name}
|
placeholder={name}
|
||||||
value={inputs[key] ? `${inputs[key]}` : ''}
|
value={inputs[key] ? `${inputs[key]}` : ''}
|
||||||
onChange={(e) => { handleInputValueChange(key, e.target.value) }}
|
onChange={(e) => { handleInputValueChange(key, e.target.value) }}
|
||||||
|
readOnly={readonly}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{type === 'select' && (
|
{type === 'select' && (
|
||||||
@ -87,6 +89,7 @@ const ChatUserInput = ({
|
|||||||
onSelect={(i) => { handleInputValueChange(key, i.value as string) }}
|
onSelect={(i) => { handleInputValueChange(key, i.value as string) }}
|
||||||
items={(options || []).map(i => ({ name: i, value: i }))}
|
items={(options || []).map(i => ({ name: i, value: i }))}
|
||||||
allowSearch={false}
|
allowSearch={false}
|
||||||
|
disabled={readonly}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{type === 'number' && (
|
{type === 'number' && (
|
||||||
@ -97,6 +100,7 @@ const ChatUserInput = ({
|
|||||||
placeholder={name}
|
placeholder={name}
|
||||||
autoFocus={index === 0}
|
autoFocus={index === 0}
|
||||||
maxLength={max_length || DEFAULT_VALUE_MAX_LEN}
|
maxLength={max_length || DEFAULT_VALUE_MAX_LEN}
|
||||||
|
readOnly={readonly}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{type === 'checkbox' && (
|
{type === 'checkbox' && (
|
||||||
@ -105,6 +109,7 @@ const ChatUserInput = ({
|
|||||||
value={!!inputs[key]}
|
value={!!inputs[key]}
|
||||||
required={required}
|
required={required}
|
||||||
onChange={(value) => { handleInputValueChange(key, value) }}
|
onChange={(value) => { handleInputValueChange(key, value) }}
|
||||||
|
readonly={readonly}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -39,6 +39,7 @@ const DebugWithSingleModel = (
|
|||||||
) => {
|
) => {
|
||||||
const { userProfile } = useAppContext()
|
const { userProfile } = useAppContext()
|
||||||
const {
|
const {
|
||||||
|
readonly,
|
||||||
modelConfig,
|
modelConfig,
|
||||||
appId,
|
appId,
|
||||||
inputs,
|
inputs,
|
||||||
@ -154,6 +155,7 @@ const DebugWithSingleModel = (
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Chat
|
<Chat
|
||||||
|
readonly={readonly}
|
||||||
config={config}
|
config={config}
|
||||||
chatList={chatList}
|
chatList={chatList}
|
||||||
isResponding={isResponding}
|
isResponding={isResponding}
|
||||||
|
|||||||
@ -70,6 +70,7 @@ const Debug: FC<IDebug> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const {
|
const {
|
||||||
|
readonly,
|
||||||
appId,
|
appId,
|
||||||
mode,
|
mode,
|
||||||
modelModeType,
|
modelModeType,
|
||||||
@ -413,19 +414,23 @@ const Debug: FC<IDebug> = ({
|
|||||||
}
|
}
|
||||||
{mode !== AppType.completion && (
|
{mode !== AppType.completion && (
|
||||||
<>
|
<>
|
||||||
<TooltipPlus
|
{!readonly && (
|
||||||
popupContent={t('common.operation.refresh')}
|
<TooltipPlus
|
||||||
>
|
popupContent={t('common.operation.refresh')}
|
||||||
<ActionButton onClick={clearConversation}>
|
>
|
||||||
<RefreshCcw01 className='h-4 w-4' />
|
<ActionButton onClick={clearConversation}>
|
||||||
</ActionButton>
|
<RefreshCcw01 className='h-4 w-4' />
|
||||||
</TooltipPlus>
|
</ActionButton>
|
||||||
|
|
||||||
|
</TooltipPlus>
|
||||||
|
)}
|
||||||
|
|
||||||
{varList.length > 0 && (
|
{varList.length > 0 && (
|
||||||
<div className='relative ml-1 mr-2'>
|
<div className='relative ml-1 mr-2'>
|
||||||
<TooltipPlus
|
<TooltipPlus
|
||||||
popupContent={t('workflow.panel.userInputField')}
|
popupContent={t('workflow.panel.userInputField')}
|
||||||
>
|
>
|
||||||
<ActionButton state={expanded ? ActionButtonState.Active : undefined} onClick={() => setExpanded(!expanded)}>
|
<ActionButton state={expanded ? ActionButtonState.Active : undefined} onClick={() => !readonly && setExpanded(!expanded)}>
|
||||||
<RiEqualizer2Line className='h-4 w-4' />
|
<RiEqualizer2Line className='h-4 w-4' />
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</TooltipPlus>
|
</TooltipPlus>
|
||||||
@ -553,7 +558,7 @@ const Debug: FC<IDebug> = ({
|
|||||||
onCancel={handleCancel}
|
onCancel={handleCancel}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!isAPIKeySet && (<HasNotSetAPIKEY isTrailFinished={!IS_CE_EDITION} onSetting={onSetting} />)}
|
{!isAPIKeySet && !readonly && (<HasNotSetAPIKEY isTrailFinished={!IS_CE_EDITION} onSetting={onSetting} />)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,7 +40,7 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
|||||||
onVisionFilesChange,
|
onVisionFilesChange,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { modelModeType, modelConfig, setInputs, mode, isAdvancedMode, completionPromptConfig, chatPromptConfig } = useContext(ConfigContext)
|
const { readonly, modelModeType, modelConfig, setInputs, mode, isAdvancedMode, completionPromptConfig, chatPromptConfig } = useContext(ConfigContext)
|
||||||
const [userInputFieldCollapse, setUserInputFieldCollapse] = useState(false)
|
const [userInputFieldCollapse, setUserInputFieldCollapse] = useState(false)
|
||||||
const promptVariables = modelConfig.configs.prompt_variables.filter(({ key, name }) => {
|
const promptVariables = modelConfig.configs.prompt_variables.filter(({ key, name }) => {
|
||||||
return key && key?.trim() && name && name?.trim()
|
return key && key?.trim() && name && name?.trim()
|
||||||
@ -60,12 +60,12 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
|||||||
|
|
||||||
if (isAdvancedMode) {
|
if (isAdvancedMode) {
|
||||||
if (modelModeType === ModelModeType.chat)
|
if (modelModeType === ModelModeType.chat)
|
||||||
return chatPromptConfig.prompt.every(({ text }) => !text)
|
return chatPromptConfig?.prompt.every(({ text }) => !text)
|
||||||
return !completionPromptConfig.prompt?.text
|
return !completionPromptConfig.prompt?.text
|
||||||
}
|
}
|
||||||
|
|
||||||
else { return !modelConfig.configs.prompt_template }
|
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) => {
|
const handleInputValueChange = (key: string, value: string | boolean) => {
|
||||||
if (!(key in promptVariableObj))
|
if (!(key in promptVariableObj))
|
||||||
@ -124,6 +124,7 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
|||||||
placeholder={name}
|
placeholder={name}
|
||||||
autoFocus={index === 0}
|
autoFocus={index === 0}
|
||||||
maxLength={max_length || DEFAULT_VALUE_MAX_LEN}
|
maxLength={max_length || DEFAULT_VALUE_MAX_LEN}
|
||||||
|
readOnly={readonly}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{type === 'paragraph' && (
|
{type === 'paragraph' && (
|
||||||
@ -132,6 +133,7 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
|||||||
placeholder={name}
|
placeholder={name}
|
||||||
value={inputs[key] ? `${inputs[key]}` : ''}
|
value={inputs[key] ? `${inputs[key]}` : ''}
|
||||||
onChange={(e) => { handleInputValueChange(key, e.target.value) }}
|
onChange={(e) => { handleInputValueChange(key, e.target.value) }}
|
||||||
|
readOnly={readonly}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{type === 'select' && (
|
{type === 'select' && (
|
||||||
@ -142,6 +144,7 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
|||||||
items={(options || []).map(i => ({ name: i, value: i }))}
|
items={(options || []).map(i => ({ name: i, value: i }))}
|
||||||
allowSearch={false}
|
allowSearch={false}
|
||||||
bgClassName='bg-gray-50'
|
bgClassName='bg-gray-50'
|
||||||
|
disabled={readonly}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{type === 'number' && (
|
{type === 'number' && (
|
||||||
@ -152,6 +155,7 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
|||||||
placeholder={name}
|
placeholder={name}
|
||||||
autoFocus={index === 0}
|
autoFocus={index === 0}
|
||||||
maxLength={max_length || DEFAULT_VALUE_MAX_LEN}
|
maxLength={max_length || DEFAULT_VALUE_MAX_LEN}
|
||||||
|
readOnly={readonly}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{type === 'checkbox' && (
|
{type === 'checkbox' && (
|
||||||
@ -160,6 +164,7 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
|||||||
value={!!inputs[key]}
|
value={!!inputs[key]}
|
||||||
required={required}
|
required={required}
|
||||||
onChange={(value) => { handleInputValueChange(key, value) }}
|
onChange={(value) => { handleInputValueChange(key, value) }}
|
||||||
|
readonly={readonly}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -178,6 +183,7 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
|||||||
url: fileItem.url,
|
url: fileItem.url,
|
||||||
upload_file_id: fileItem.fileId,
|
upload_file_id: fileItem.fileId,
|
||||||
})))}
|
})))}
|
||||||
|
disabled={readonly}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -186,12 +192,12 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
|||||||
)}
|
)}
|
||||||
{!userInputFieldCollapse && (
|
{!userInputFieldCollapse && (
|
||||||
<div className='flex justify-between border-t border-divider-subtle p-4 pt-3'>
|
<div className='flex justify-between border-t border-divider-subtle p-4 pt-3'>
|
||||||
<Button className='w-[72px]' onClick={onClear}>{t('common.operation.clear')}</Button>
|
<Button className='w-[72px]' disabled={readonly} onClick={onClear}>{t('common.operation.clear')}</Button>
|
||||||
{canNotRun && (
|
{canNotRun && (
|
||||||
<Tooltip popupContent={t('appDebug.otherError.promptNoBeEmpty')}>
|
<Tooltip popupContent={t('appDebug.otherError.promptNoBeEmpty')}>
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
disabled={canNotRun}
|
disabled={canNotRun || readonly}
|
||||||
onClick={() => onSend?.()}
|
onClick={() => onSend?.()}
|
||||||
className="w-[96px]">
|
className="w-[96px]">
|
||||||
<RiPlayLargeFill className="mr-0.5 h-4 w-4 shrink-0" aria-hidden="true" />
|
<RiPlayLargeFill className="mr-0.5 h-4 w-4 shrink-0" aria-hidden="true" />
|
||||||
@ -202,7 +208,7 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
|||||||
{!canNotRun && (
|
{!canNotRun && (
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
disabled={canNotRun}
|
disabled={canNotRun || readonly}
|
||||||
onClick={() => onSend?.()}
|
onClick={() => onSend?.()}
|
||||||
className="w-[96px]">
|
className="w-[96px]">
|
||||||
<RiPlayLargeFill className="mr-0.5 h-4 w-4 shrink-0" aria-hidden="true" />
|
<RiPlayLargeFill className="mr-0.5 h-4 w-4 shrink-0" aria-hidden="true" />
|
||||||
@ -216,7 +222,10 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
|||||||
<FeatureBar
|
<FeatureBar
|
||||||
showFileUpload={false}
|
showFileUpload={false}
|
||||||
isChatMode={appType !== AppType.completion}
|
isChatMode={appType !== AppType.completion}
|
||||||
onFeatureBarClick={setShowAppConfigureFeaturesModal} />
|
onFeatureBarClick={setShowAppConfigureFeaturesModal}
|
||||||
|
disabled={readonly}
|
||||||
|
hideEditEntrance={readonly}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -132,8 +132,6 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
|
|||||||
importedVersion: imported_dsl_version ?? '',
|
importedVersion: imported_dsl_version ?? '',
|
||||||
systemVersion: current_dsl_version ?? '',
|
systemVersion: current_dsl_version ?? '',
|
||||||
})
|
})
|
||||||
if (onClose)
|
|
||||||
onClose()
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setShowErrorModal(true)
|
setShowErrorModal(true)
|
||||||
}, 300)
|
}, 300)
|
||||||
|
|||||||
@ -14,7 +14,6 @@ import timezone from 'dayjs/plugin/timezone'
|
|||||||
import { createContext, useContext } from 'use-context-selector'
|
import { createContext, useContext } from 'use-context-selector'
|
||||||
import { useShallow } from 'zustand/react/shallow'
|
import { useShallow } from 'zustand/react/shallow'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
|
|
||||||
import type { ChatItemInTree } from '../../base/chat/types'
|
import type { ChatItemInTree } from '../../base/chat/types'
|
||||||
import Indicator from '../../header/indicator'
|
import Indicator from '../../header/indicator'
|
||||||
import VarPanel from './var-panel'
|
import VarPanel from './var-panel'
|
||||||
@ -43,10 +42,6 @@ import cn from '@/utils/classnames'
|
|||||||
import { noop } from 'lodash-es'
|
import { noop } from 'lodash-es'
|
||||||
import PromptLogModal from '../../base/prompt-log-modal'
|
import PromptLogModal from '../../base/prompt-log-modal'
|
||||||
|
|
||||||
type AppStoreState = ReturnType<typeof useAppStore.getState>
|
|
||||||
type ConversationListItem = ChatConversationGeneralDetail | CompletionConversationGeneralDetail
|
|
||||||
type ConversationSelection = ConversationListItem | { id: string; isPlaceholder?: true }
|
|
||||||
|
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc)
|
||||||
dayjs.extend(timezone)
|
dayjs.extend(timezone)
|
||||||
|
|
||||||
@ -206,7 +201,7 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
|||||||
const { formatTime } = useTimestamp()
|
const { formatTime } = useTimestamp()
|
||||||
const { onClose, appDetail } = useContext(DrawerContext)
|
const { onClose, appDetail } = useContext(DrawerContext)
|
||||||
const { notify } = useContext(ToastContext)
|
const { notify } = useContext(ToastContext)
|
||||||
const { currentLogItem, setCurrentLogItem, showMessageLogModal, setShowMessageLogModal, showPromptLogModal, setShowPromptLogModal, currentLogModalActiveTab } = useAppStore(useShallow((state: AppStoreState) => ({
|
const { currentLogItem, setCurrentLogItem, showMessageLogModal, setShowMessageLogModal, showPromptLogModal, setShowPromptLogModal, currentLogModalActiveTab } = useAppStore(useShallow(state => ({
|
||||||
currentLogItem: state.currentLogItem,
|
currentLogItem: state.currentLogItem,
|
||||||
setCurrentLogItem: state.setCurrentLogItem,
|
setCurrentLogItem: state.setCurrentLogItem,
|
||||||
showMessageLogModal: state.showMessageLogModal,
|
showMessageLogModal: state.showMessageLogModal,
|
||||||
@ -898,113 +893,20 @@ const ChatConversationDetailComp: FC<{ appId?: string; conversationId?: string }
|
|||||||
const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh }) => {
|
const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { formatTime } = useTimestamp()
|
const { formatTime } = useTimestamp()
|
||||||
const router = useRouter()
|
|
||||||
const pathname = usePathname()
|
|
||||||
const searchParams = useSearchParams()
|
|
||||||
const conversationIdInUrl = searchParams.get('conversation_id') ?? undefined
|
|
||||||
|
|
||||||
const media = useBreakpoints()
|
const media = useBreakpoints()
|
||||||
const isMobile = media === MediaType.mobile
|
const isMobile = media === MediaType.mobile
|
||||||
|
|
||||||
const [showDrawer, setShowDrawer] = useState<boolean>(false) // Whether to display the chat details drawer
|
const [showDrawer, setShowDrawer] = useState<boolean>(false) // Whether to display the chat details drawer
|
||||||
const [currentConversation, setCurrentConversation] = useState<ConversationSelection | undefined>() // Currently selected conversation
|
const [currentConversation, setCurrentConversation] = useState<ChatConversationGeneralDetail | CompletionConversationGeneralDetail | undefined>() // Currently selected conversation
|
||||||
const closingConversationIdRef = useRef<string | null>(null)
|
|
||||||
const pendingConversationIdRef = useRef<string | null>(null)
|
|
||||||
const pendingConversationCacheRef = useRef<ConversationSelection | undefined>(undefined)
|
|
||||||
const isChatMode = appDetail.mode !== 'completion' // Whether the app is a chat app
|
const isChatMode = appDetail.mode !== 'completion' // Whether the app is a chat app
|
||||||
const isChatflow = appDetail.mode === 'advanced-chat' // Whether the app is a chatflow app
|
const isChatflow = appDetail.mode === 'advanced-chat' // Whether the app is a chatflow app
|
||||||
const { setShowPromptLogModal, setShowAgentLogModal, setShowMessageLogModal } = useAppStore(useShallow((state: AppStoreState) => ({
|
const { setShowPromptLogModal, setShowAgentLogModal, setShowMessageLogModal } = useAppStore(useShallow(state => ({
|
||||||
setShowPromptLogModal: state.setShowPromptLogModal,
|
setShowPromptLogModal: state.setShowPromptLogModal,
|
||||||
setShowAgentLogModal: state.setShowAgentLogModal,
|
setShowAgentLogModal: state.setShowAgentLogModal,
|
||||||
setShowMessageLogModal: state.setShowMessageLogModal,
|
setShowMessageLogModal: state.setShowMessageLogModal,
|
||||||
})))
|
})))
|
||||||
|
|
||||||
const activeConversationId = conversationIdInUrl ?? pendingConversationIdRef.current ?? currentConversation?.id
|
|
||||||
|
|
||||||
const buildUrlWithConversation = useCallback((conversationId?: string) => {
|
|
||||||
const params = new URLSearchParams(searchParams.toString())
|
|
||||||
if (conversationId)
|
|
||||||
params.set('conversation_id', conversationId)
|
|
||||||
else
|
|
||||||
params.delete('conversation_id')
|
|
||||||
|
|
||||||
const queryString = params.toString()
|
|
||||||
return queryString ? `${pathname}?${queryString}` : pathname
|
|
||||||
}, [pathname, searchParams])
|
|
||||||
|
|
||||||
const handleRowClick = useCallback((log: ConversationListItem) => {
|
|
||||||
if (conversationIdInUrl === log.id) {
|
|
||||||
if (!showDrawer)
|
|
||||||
setShowDrawer(true)
|
|
||||||
|
|
||||||
if (!currentConversation || currentConversation.id !== log.id)
|
|
||||||
setCurrentConversation(log)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
pendingConversationIdRef.current = log.id
|
|
||||||
pendingConversationCacheRef.current = log
|
|
||||||
if (!showDrawer)
|
|
||||||
setShowDrawer(true)
|
|
||||||
|
|
||||||
if (currentConversation?.id !== log.id)
|
|
||||||
setCurrentConversation(undefined)
|
|
||||||
|
|
||||||
router.push(buildUrlWithConversation(log.id), { scroll: false })
|
|
||||||
}, [buildUrlWithConversation, conversationIdInUrl, currentConversation, router, showDrawer])
|
|
||||||
|
|
||||||
const currentConversationId = currentConversation?.id
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!conversationIdInUrl) {
|
|
||||||
if (pendingConversationIdRef.current)
|
|
||||||
return
|
|
||||||
|
|
||||||
if (showDrawer || currentConversationId) {
|
|
||||||
setShowDrawer(false)
|
|
||||||
setCurrentConversation(undefined)
|
|
||||||
}
|
|
||||||
closingConversationIdRef.current = null
|
|
||||||
pendingConversationCacheRef.current = undefined
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (closingConversationIdRef.current === conversationIdInUrl)
|
|
||||||
return
|
|
||||||
|
|
||||||
if (pendingConversationIdRef.current === conversationIdInUrl)
|
|
||||||
pendingConversationIdRef.current = null
|
|
||||||
|
|
||||||
const matchedConversation = logs?.data?.find((item: ConversationListItem) => item.id === conversationIdInUrl)
|
|
||||||
const nextConversation: ConversationSelection = matchedConversation
|
|
||||||
?? pendingConversationCacheRef.current
|
|
||||||
?? { id: conversationIdInUrl, isPlaceholder: true }
|
|
||||||
|
|
||||||
if (!showDrawer)
|
|
||||||
setShowDrawer(true)
|
|
||||||
|
|
||||||
if (!currentConversation || currentConversation.id !== conversationIdInUrl || (matchedConversation && currentConversation !== matchedConversation))
|
|
||||||
setCurrentConversation(nextConversation)
|
|
||||||
|
|
||||||
if (pendingConversationCacheRef.current?.id === conversationIdInUrl || matchedConversation)
|
|
||||||
pendingConversationCacheRef.current = undefined
|
|
||||||
}, [conversationIdInUrl, currentConversation, isChatMode, logs?.data, showDrawer])
|
|
||||||
|
|
||||||
const onCloseDrawer = useCallback(() => {
|
|
||||||
onRefresh()
|
|
||||||
setShowDrawer(false)
|
|
||||||
setCurrentConversation(undefined)
|
|
||||||
setShowPromptLogModal(false)
|
|
||||||
setShowAgentLogModal(false)
|
|
||||||
setShowMessageLogModal(false)
|
|
||||||
pendingConversationIdRef.current = null
|
|
||||||
pendingConversationCacheRef.current = undefined
|
|
||||||
closingConversationIdRef.current = conversationIdInUrl ?? null
|
|
||||||
|
|
||||||
if (conversationIdInUrl)
|
|
||||||
router.replace(buildUrlWithConversation(), { scroll: false })
|
|
||||||
}, [buildUrlWithConversation, conversationIdInUrl, onRefresh, router, setShowAgentLogModal, setShowMessageLogModal, setShowPromptLogModal])
|
|
||||||
|
|
||||||
// Annotated data needs to be highlighted
|
// Annotated data needs to be highlighted
|
||||||
const renderTdValue = (value: string | number | null, isEmptyStyle: boolean, isHighlight = false, annotation?: LogAnnotation) => {
|
const renderTdValue = (value: string | number | null, isEmptyStyle: boolean, isHighlight = false, annotation?: LogAnnotation) => {
|
||||||
return (
|
return (
|
||||||
@ -1023,6 +925,15 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh })
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onCloseDrawer = () => {
|
||||||
|
onRefresh()
|
||||||
|
setShowDrawer(false)
|
||||||
|
setCurrentConversation(undefined)
|
||||||
|
setShowPromptLogModal(false)
|
||||||
|
setShowAgentLogModal(false)
|
||||||
|
setShowMessageLogModal(false)
|
||||||
|
}
|
||||||
|
|
||||||
if (!logs)
|
if (!logs)
|
||||||
return <Loading />
|
return <Loading />
|
||||||
|
|
||||||
@ -1049,8 +960,11 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh })
|
|||||||
const rightValue = get(log, isChatMode ? 'message_count' : 'message.answer')
|
const rightValue = get(log, isChatMode ? 'message_count' : 'message.answer')
|
||||||
return <tr
|
return <tr
|
||||||
key={log.id}
|
key={log.id}
|
||||||
className={cn('cursor-pointer border-b border-divider-subtle hover:bg-background-default-hover', activeConversationId !== log.id ? '' : 'bg-background-default-hover')}
|
className={cn('cursor-pointer border-b border-divider-subtle hover:bg-background-default-hover', currentConversation?.id !== log.id ? '' : 'bg-background-default-hover')}
|
||||||
onClick={() => handleRowClick(log)}>
|
onClick={() => {
|
||||||
|
setShowDrawer(true)
|
||||||
|
setCurrentConversation(log)
|
||||||
|
}}>
|
||||||
<td className='h-4'>
|
<td className='h-4'>
|
||||||
{!log.read_at && (
|
{!log.read_at && (
|
||||||
<div className='flex items-center p-3 pr-0.5'>
|
<div className='flex items-center p-3 pr-0.5'>
|
||||||
|
|||||||
@ -21,7 +21,7 @@ import { Markdown } from '@/app/components/base/markdown'
|
|||||||
import Loading from '@/app/components/base/loading'
|
import Loading from '@/app/components/base/loading'
|
||||||
import Toast from '@/app/components/base/toast'
|
import Toast from '@/app/components/base/toast'
|
||||||
import type { FeedbackType } from '@/app/components/base/chat/chat/type'
|
import type { FeedbackType } from '@/app/components/base/chat/chat/type'
|
||||||
import { fetchMoreLikeThis, updateFeedback } from '@/service/share'
|
import { AppSourceType, fetchMoreLikeThis, updateFeedback } from '@/service/share'
|
||||||
import { fetchTextGenerationMessage } from '@/service/debug'
|
import { fetchTextGenerationMessage } from '@/service/debug'
|
||||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||||
import WorkflowProcessItem from '@/app/components/base/chat/chat/answer/workflow-process'
|
import WorkflowProcessItem from '@/app/components/base/chat/chat/answer/workflow-process'
|
||||||
@ -52,7 +52,7 @@ export type IGenerationItemProps = {
|
|||||||
onFeedback?: (feedback: FeedbackType) => void
|
onFeedback?: (feedback: FeedbackType) => void
|
||||||
onSave?: (messageId: string) => void
|
onSave?: (messageId: string) => void
|
||||||
isMobile?: boolean
|
isMobile?: boolean
|
||||||
isInstalledApp: boolean
|
appSourceType: AppSourceType
|
||||||
installedAppId?: string
|
installedAppId?: string
|
||||||
taskId?: string
|
taskId?: string
|
||||||
controlClearMoreLikeThis?: number
|
controlClearMoreLikeThis?: number
|
||||||
@ -86,7 +86,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
|||||||
onSave,
|
onSave,
|
||||||
depth = 1,
|
depth = 1,
|
||||||
isMobile,
|
isMobile,
|
||||||
isInstalledApp,
|
appSourceType,
|
||||||
installedAppId,
|
installedAppId,
|
||||||
taskId,
|
taskId,
|
||||||
controlClearMoreLikeThis,
|
controlClearMoreLikeThis,
|
||||||
@ -99,6 +99,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const isTop = depth === 1
|
const isTop = depth === 1
|
||||||
|
const isTryApp = appSourceType === AppSourceType.tryApp
|
||||||
const [completionRes, setCompletionRes] = useState('')
|
const [completionRes, setCompletionRes] = useState('')
|
||||||
const [childMessageId, setChildMessageId] = useState<string | null>(null)
|
const [childMessageId, setChildMessageId] = useState<string | null>(null)
|
||||||
const [childFeedback, setChildFeedback] = useState<FeedbackType>({
|
const [childFeedback, setChildFeedback] = useState<FeedbackType>({
|
||||||
@ -112,7 +113,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
|||||||
const setShowPromptLogModal = useAppStore(s => s.setShowPromptLogModal)
|
const setShowPromptLogModal = useAppStore(s => s.setShowPromptLogModal)
|
||||||
|
|
||||||
const handleFeedback = async (childFeedback: FeedbackType) => {
|
const handleFeedback = async (childFeedback: FeedbackType) => {
|
||||||
await updateFeedback({ url: `/messages/${childMessageId}/feedbacks`, body: { rating: childFeedback.rating } }, isInstalledApp, installedAppId)
|
await updateFeedback({ url: `/messages/${childMessageId}/feedbacks`, body: { rating: childFeedback.rating } }, appSourceType, installedAppId)
|
||||||
setChildFeedback(childFeedback)
|
setChildFeedback(childFeedback)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,7 +131,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
|||||||
onSave,
|
onSave,
|
||||||
isShowTextToSpeech,
|
isShowTextToSpeech,
|
||||||
isMobile,
|
isMobile,
|
||||||
isInstalledApp,
|
appSourceType,
|
||||||
installedAppId,
|
installedAppId,
|
||||||
controlClearMoreLikeThis,
|
controlClearMoreLikeThis,
|
||||||
isWorkflow,
|
isWorkflow,
|
||||||
@ -144,7 +145,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
startQuerying()
|
startQuerying()
|
||||||
const res: any = await fetchMoreLikeThis(messageId as string, isInstalledApp, installedAppId)
|
const res: any = await fetchMoreLikeThis(messageId as string, appSourceType, installedAppId)
|
||||||
setCompletionRes(res.answer)
|
setCompletionRes(res.answer)
|
||||||
setChildFeedback({
|
setChildFeedback({
|
||||||
rating: null,
|
rating: null,
|
||||||
@ -292,7 +293,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
|||||||
{!isWorkflow && <span>{content?.length} {t('common.unit.char')}</span>}
|
{!isWorkflow && <span>{content?.length} {t('common.unit.char')}</span>}
|
||||||
{/* action buttons */}
|
{/* action buttons */}
|
||||||
<div className='absolute bottom-1 right-2 flex items-center'>
|
<div className='absolute bottom-1 right-2 flex items-center'>
|
||||||
{!isInWebApp && !isInstalledApp && !isResponding && (
|
{!isInWebApp && (appSourceType !== AppSourceType.installedApp) && !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'>
|
<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}>
|
<ActionButton disabled={isError || !messageId} onClick={handleOpenLogModal}>
|
||||||
<RiFileList3Line className='h-4 w-4' />
|
<RiFileList3Line className='h-4 w-4' />
|
||||||
@ -329,13 +330,13 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
|||||||
<RiReplay15Line className='h-4 w-4' />
|
<RiReplay15Line className='h-4 w-4' />
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
)}
|
)}
|
||||||
{isInWebApp && !isWorkflow && (
|
{isInWebApp && !isWorkflow && !isTryApp && (
|
||||||
<ActionButton disabled={isError || !messageId} onClick={() => { onSave?.(messageId as string) }}>
|
<ActionButton disabled={isError || !messageId} onClick={() => { onSave?.(messageId as string) }}>
|
||||||
<RiBookmark3Line className='h-4 w-4' />
|
<RiBookmark3Line className='h-4 w-4' />
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{(supportFeedback || isInWebApp) && !isWorkflow && !isError && messageId && (
|
{(supportFeedback || isInWebApp) && !isWorkflow && !isTryApp && !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'>
|
<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 && (
|
{!feedback?.rating && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -50,13 +50,14 @@ function getActionButtonState(state: ActionButtonState) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ActionButton = ({ className, size, state = ActionButtonState.Default, styleCss, children, ref, ...props }: ActionButtonProps) => {
|
const ActionButton = ({ className, size, state = ActionButtonState.Default, styleCss, children, ref, disabled, ...props }: ActionButtonProps) => {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
className={classNames(
|
className={classNames(
|
||||||
actionButtonVariants({ className, size }),
|
actionButtonVariants({ className, size }),
|
||||||
getActionButtonState(state),
|
getActionButtonState(state),
|
||||||
|
disabled && 'cursor-not-allowed text-text-disabled hover:bg-transparent hover:text-text-disabled',
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
style={styleCss}
|
style={styleCss}
|
||||||
|
|||||||
59
web/app/components/base/alert.tsx
Normal file
59
web/app/components/base/alert.tsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
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)
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import Toast from '@/app/components/base/toast'
|
import Toast from '@/app/components/base/toast'
|
||||||
import { textToAudioStream } from '@/service/share'
|
import { AppSourceType, textToAudioStream } from '@/service/share'
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
// eslint-disable-next-line ts/consistent-type-definitions
|
// eslint-disable-next-line ts/consistent-type-definitions
|
||||||
@ -100,7 +100,7 @@ export default class AudioPlayer {
|
|||||||
|
|
||||||
private async loadAudio() {
|
private async loadAudio() {
|
||||||
try {
|
try {
|
||||||
const audioResponse: any = await textToAudioStream(this.url, this.isPublic, { content_type: 'audio/mpeg' }, {
|
const audioResponse: any = await textToAudioStream(this.url, this.isPublic ? AppSourceType.webApp : AppSourceType.installedApp, { content_type: 'audio/mpeg' }, {
|
||||||
message_id: this.msgId,
|
message_id: this.msgId,
|
||||||
streaming: true,
|
streaming: true,
|
||||||
voice: this.voice,
|
voice: this.voice,
|
||||||
|
|||||||
225
web/app/components/base/carousel/index.tsx
Normal file
225
web/app/components/base/carousel/index.tsx
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
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 }
|
||||||
@ -13,6 +13,7 @@ import { InputVarType } from '@/app/components/workflow/types'
|
|||||||
import { TransferMethod } from '@/types/app'
|
import { TransferMethod } from '@/types/app'
|
||||||
import InputsForm from '@/app/components/base/chat/chat-with-history/inputs-form'
|
import InputsForm from '@/app/components/base/chat/chat-with-history/inputs-form'
|
||||||
import {
|
import {
|
||||||
|
AppSourceType,
|
||||||
fetchSuggestedQuestions,
|
fetchSuggestedQuestions,
|
||||||
getUrl,
|
getUrl,
|
||||||
stopChatMessageResponding,
|
stopChatMessageResponding,
|
||||||
@ -53,6 +54,11 @@ const ChatWrapper = () => {
|
|||||||
initUserVariables,
|
initUserVariables,
|
||||||
} = useChatWithHistoryContext()
|
} = useChatWithHistoryContext()
|
||||||
|
|
||||||
|
const appSourceType = isInstalledApp ? AppSourceType.installedApp : AppSourceType.webApp
|
||||||
|
|
||||||
|
// Semantic variable for better code readability
|
||||||
|
const isHistoryConversation = !!currentConversationId
|
||||||
|
|
||||||
const appConfig = useMemo(() => {
|
const appConfig = useMemo(() => {
|
||||||
const config = appParams || {}
|
const config = appParams || {}
|
||||||
|
|
||||||
@ -80,7 +86,7 @@ const ChatWrapper = () => {
|
|||||||
inputsForm: inputsForms,
|
inputsForm: inputsForms,
|
||||||
},
|
},
|
||||||
appPrevChatTree,
|
appPrevChatTree,
|
||||||
taskId => stopChatMessageResponding('', taskId, isInstalledApp, appId),
|
taskId => stopChatMessageResponding('', taskId, appSourceType, appId),
|
||||||
clearChatList,
|
clearChatList,
|
||||||
setClearChatList,
|
setClearChatList,
|
||||||
)
|
)
|
||||||
@ -139,11 +145,11 @@ const ChatWrapper = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleSend(
|
handleSend(
|
||||||
getUrl('chat-messages', isInstalledApp, appId || ''),
|
getUrl('chat-messages', appSourceType, appId || ''),
|
||||||
data,
|
data,
|
||||||
{
|
{
|
||||||
onGetSuggestedQuestions: responseItemId => fetchSuggestedQuestions(responseItemId, isInstalledApp, appId),
|
onGetSuggestedQuestions: responseItemId => fetchSuggestedQuestions(responseItemId, appSourceType, appId),
|
||||||
onConversationComplete: currentConversationId ? undefined : handleNewConversationCompleted,
|
onConversationComplete: isHistoryConversation ? undefined : handleNewConversationCompleted,
|
||||||
isPublicAPI: !isInstalledApp,
|
isPublicAPI: !isInstalledApp,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -179,13 +185,13 @@ const ChatWrapper = () => {
|
|||||||
else {
|
else {
|
||||||
return <InputsForm collapsed={collapsed} setCollapsed={setCollapsed} />
|
return <InputsForm collapsed={collapsed} setCollapsed={setCollapsed} />
|
||||||
}
|
}
|
||||||
},
|
}, [
|
||||||
[
|
|
||||||
inputsForms.length,
|
inputsForms.length,
|
||||||
isMobile,
|
isMobile,
|
||||||
currentConversationId,
|
currentConversationId,
|
||||||
collapsed, allInputsHidden,
|
collapsed, allInputsHidden,
|
||||||
])
|
],
|
||||||
|
)
|
||||||
|
|
||||||
const welcome = useMemo(() => {
|
const welcome = useMemo(() => {
|
||||||
const welcomeMessage = chatList.find(item => item.isOpeningStatement)
|
const welcomeMessage = chatList.find(item => item.isOpeningStatement)
|
||||||
@ -232,8 +238,7 @@ const ChatWrapper = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
},
|
}, [
|
||||||
[
|
|
||||||
appData?.site.icon,
|
appData?.site.icon,
|
||||||
appData?.site.icon_background,
|
appData?.site.icon_background,
|
||||||
appData?.site.icon_type,
|
appData?.site.icon_type,
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import { buildChatItemTree, getProcessedSystemVariablesFromUrlParams, getRawInpu
|
|||||||
import { addFileInfos, sortAgentSorts } from '../../../tools/utils'
|
import { addFileInfos, sortAgentSorts } from '../../../tools/utils'
|
||||||
import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
|
import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
|
||||||
import {
|
import {
|
||||||
|
AppSourceType,
|
||||||
delConversation,
|
delConversation,
|
||||||
fetchChatList,
|
fetchChatList,
|
||||||
fetchConversations,
|
fetchConversations,
|
||||||
@ -70,6 +71,7 @@ function getFormattedChatList(messages: any[]) {
|
|||||||
|
|
||||||
export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||||
const isInstalledApp = useMemo(() => !!installedAppInfo, [installedAppInfo])
|
const isInstalledApp = useMemo(() => !!installedAppInfo, [installedAppInfo])
|
||||||
|
const appSourceType = isInstalledApp ? AppSourceType.installedApp : AppSourceType.webApp
|
||||||
const appInfo = useWebAppStore(s => s.appInfo)
|
const appInfo = useWebAppStore(s => s.appInfo)
|
||||||
const appParams = useWebAppStore(s => s.appParams)
|
const appParams = useWebAppStore(s => s.appParams)
|
||||||
const appMeta = useWebAppStore(s => s.appMeta)
|
const appMeta = useWebAppStore(s => s.appMeta)
|
||||||
@ -176,17 +178,17 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
|||||||
|
|
||||||
const { data: appPinnedConversationData, mutate: mutateAppPinnedConversationData } = useSWR(
|
const { data: appPinnedConversationData, mutate: mutateAppPinnedConversationData } = useSWR(
|
||||||
appId ? ['appConversationData', isInstalledApp, appId, true] : null,
|
appId ? ['appConversationData', isInstalledApp, appId, true] : null,
|
||||||
() => fetchConversations(isInstalledApp, appId, undefined, true, 100),
|
() => fetchConversations(appSourceType, appId, undefined, true, 100),
|
||||||
{ revalidateOnFocus: false, revalidateOnReconnect: false },
|
{ revalidateOnFocus: false, revalidateOnReconnect: false },
|
||||||
)
|
)
|
||||||
const { data: appConversationData, isLoading: appConversationDataLoading, mutate: mutateAppConversationData } = useSWR(
|
const { data: appConversationData, isLoading: appConversationDataLoading, mutate: mutateAppConversationData } = useSWR(
|
||||||
appId ? ['appConversationData', isInstalledApp, appId, false] : null,
|
appId ? ['appConversationData', isInstalledApp, appId, false] : null,
|
||||||
() => fetchConversations(isInstalledApp, appId, undefined, false, 100),
|
() => fetchConversations(appSourceType, appId, undefined, false, 100),
|
||||||
{ revalidateOnFocus: false, revalidateOnReconnect: false },
|
{ revalidateOnFocus: false, revalidateOnReconnect: false },
|
||||||
)
|
)
|
||||||
const { data: appChatListData, isLoading: appChatListDataLoading } = useSWR(
|
const { data: appChatListData, isLoading: appChatListDataLoading } = useSWR(
|
||||||
chatShouldReloadKey ? ['appChatList', chatShouldReloadKey, isInstalledApp, appId] : null,
|
chatShouldReloadKey ? ['appChatList', chatShouldReloadKey, appSourceType, appId] : null,
|
||||||
() => fetchChatList(chatShouldReloadKey, isInstalledApp, appId),
|
() => fetchChatList(chatShouldReloadKey, appSourceType, appId),
|
||||||
{ revalidateOnFocus: false, revalidateOnReconnect: false },
|
{ revalidateOnFocus: false, revalidateOnReconnect: false },
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -309,7 +311,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
|||||||
handleNewConversationInputsChange(conversationInputs)
|
handleNewConversationInputsChange(conversationInputs)
|
||||||
}, [handleNewConversationInputsChange, inputsForms])
|
}, [handleNewConversationInputsChange, inputsForms])
|
||||||
|
|
||||||
const { data: newConversation } = useSWR(newConversationId ? [isInstalledApp, appId, newConversationId] : null, () => generationConversationName(isInstalledApp, appId, newConversationId), { revalidateOnFocus: false })
|
const { data: newConversation } = useSWR(newConversationId ? [isInstalledApp, appId, newConversationId] : null, () => generationConversationName(appSourceType, appId, newConversationId), { revalidateOnFocus: false })
|
||||||
const [originConversationList, setOriginConversationList] = useState<ConversationItem[]>([])
|
const [originConversationList, setOriginConversationList] = useState<ConversationItem[]>([])
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (appConversationData?.data && !appConversationDataLoading)
|
if (appConversationData?.data && !appConversationDataLoading)
|
||||||
@ -434,16 +436,16 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
|||||||
}, [mutateAppConversationData, mutateAppPinnedConversationData])
|
}, [mutateAppConversationData, mutateAppPinnedConversationData])
|
||||||
|
|
||||||
const handlePinConversation = useCallback(async (conversationId: string) => {
|
const handlePinConversation = useCallback(async (conversationId: string) => {
|
||||||
await pinConversation(isInstalledApp, appId, conversationId)
|
await pinConversation(appSourceType, appId, conversationId)
|
||||||
notify({ type: 'success', message: t('common.api.success') })
|
notify({ type: 'success', message: t('common.api.success') })
|
||||||
handleUpdateConversationList()
|
handleUpdateConversationList()
|
||||||
}, [isInstalledApp, appId, notify, t, handleUpdateConversationList])
|
}, [appSourceType, appId, notify, t, handleUpdateConversationList])
|
||||||
|
|
||||||
const handleUnpinConversation = useCallback(async (conversationId: string) => {
|
const handleUnpinConversation = useCallback(async (conversationId: string) => {
|
||||||
await unpinConversation(isInstalledApp, appId, conversationId)
|
await unpinConversation(appSourceType, appId, conversationId)
|
||||||
notify({ type: 'success', message: t('common.api.success') })
|
notify({ type: 'success', message: t('common.api.success') })
|
||||||
handleUpdateConversationList()
|
handleUpdateConversationList()
|
||||||
}, [isInstalledApp, appId, notify, t, handleUpdateConversationList])
|
}, [appSourceType, appId, notify, t, handleUpdateConversationList])
|
||||||
|
|
||||||
const [conversationDeleting, setConversationDeleting] = useState(false)
|
const [conversationDeleting, setConversationDeleting] = useState(false)
|
||||||
const handleDeleteConversation = useCallback(async (
|
const handleDeleteConversation = useCallback(async (
|
||||||
@ -457,7 +459,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
setConversationDeleting(true)
|
setConversationDeleting(true)
|
||||||
await delConversation(isInstalledApp, appId, conversationId)
|
await delConversation(appSourceType, appId, conversationId)
|
||||||
notify({ type: 'success', message: t('common.api.success') })
|
notify({ type: 'success', message: t('common.api.success') })
|
||||||
onSuccess()
|
onSuccess()
|
||||||
}
|
}
|
||||||
@ -492,7 +494,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
|||||||
|
|
||||||
setConversationRenaming(true)
|
setConversationRenaming(true)
|
||||||
try {
|
try {
|
||||||
await renameConversation(isInstalledApp, appId, conversationId, newName)
|
await renameConversation(appSourceType, appId, conversationId, newName)
|
||||||
|
|
||||||
notify({
|
notify({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
@ -522,9 +524,9 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
|||||||
}, [mutateAppConversationData, handleConversationIdInfoChange])
|
}, [mutateAppConversationData, handleConversationIdInfoChange])
|
||||||
|
|
||||||
const handleFeedback = useCallback(async (messageId: string, feedback: Feedback) => {
|
const handleFeedback = useCallback(async (messageId: string, feedback: Feedback) => {
|
||||||
await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating, content: feedback.content } }, isInstalledApp, appId)
|
await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating, content: feedback.content } }, appSourceType, appId)
|
||||||
notify({ type: 'success', message: t('common.api.success') })
|
notify({ type: 'success', message: t('common.api.success') })
|
||||||
}, [isInstalledApp, appId, t, notify])
|
}, [appSourceType, appId, t, notify])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isInstalledApp,
|
isInstalledApp,
|
||||||
|
|||||||
@ -51,6 +51,7 @@ const Operation: FC<OperationProps> = ({
|
|||||||
onAnnotationAdded,
|
onAnnotationAdded,
|
||||||
onAnnotationEdited,
|
onAnnotationEdited,
|
||||||
onAnnotationRemoved,
|
onAnnotationRemoved,
|
||||||
|
disableFeedback,
|
||||||
onFeedback,
|
onFeedback,
|
||||||
onRegenerate,
|
onRegenerate,
|
||||||
} = useChatContext()
|
} = useChatContext()
|
||||||
@ -166,7 +167,7 @@ const Operation: FC<OperationProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!isOpeningStatement && config?.supportFeedback && !localFeedback?.rating && onFeedback && (
|
{!disableFeedback && !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'>
|
<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 && (
|
{!localFeedback?.rating && (
|
||||||
<>
|
<>
|
||||||
@ -180,7 +181,7 @@ const Operation: FC<OperationProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!isOpeningStatement && config?.supportFeedback && localFeedback?.rating && onFeedback && (
|
{!disableFeedback && !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'>
|
<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' && (
|
{localFeedback?.rating === 'like' && (
|
||||||
<ActionButton state={ActionButtonState.Active} onClick={() => handleFeedback(null)}>
|
<ActionButton state={ActionButtonState.Active} onClick={() => handleFeedback(null)}>
|
||||||
|
|||||||
@ -27,8 +27,10 @@ import { useToastContext } from '@/app/components/base/toast'
|
|||||||
import FeatureBar from '@/app/components/base/features/new-feature-panel/feature-bar'
|
import FeatureBar from '@/app/components/base/features/new-feature-panel/feature-bar'
|
||||||
import type { FileUpload } from '@/app/components/base/features/types'
|
import type { FileUpload } from '@/app/components/base/features/types'
|
||||||
import { TransferMethod } from '@/types/app'
|
import { TransferMethod } from '@/types/app'
|
||||||
|
import { noop } from 'lodash-es'
|
||||||
|
|
||||||
type ChatInputAreaProps = {
|
type ChatInputAreaProps = {
|
||||||
|
readonly?: boolean
|
||||||
botName?: string
|
botName?: string
|
||||||
showFeatureBar?: boolean
|
showFeatureBar?: boolean
|
||||||
showFileUpload?: boolean
|
showFileUpload?: boolean
|
||||||
@ -44,6 +46,7 @@ type ChatInputAreaProps = {
|
|||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
}
|
}
|
||||||
const ChatInputArea = ({
|
const ChatInputArea = ({
|
||||||
|
readonly,
|
||||||
botName,
|
botName,
|
||||||
showFeatureBar,
|
showFeatureBar,
|
||||||
showFileUpload,
|
showFileUpload,
|
||||||
@ -168,6 +171,7 @@ const ChatInputArea = ({
|
|||||||
const operation = (
|
const operation = (
|
||||||
<Operation
|
<Operation
|
||||||
ref={holdSpaceRef}
|
ref={holdSpaceRef}
|
||||||
|
readonly={readonly}
|
||||||
fileConfig={visionConfig}
|
fileConfig={visionConfig}
|
||||||
speechToTextConfig={speechToTextConfig}
|
speechToTextConfig={speechToTextConfig}
|
||||||
onShowVoiceInput={handleShowVoiceInput}
|
onShowVoiceInput={handleShowVoiceInput}
|
||||||
@ -203,7 +207,7 @@ const ChatInputArea = ({
|
|||||||
className={cn(
|
className={cn(
|
||||||
'body-lg-regular w-full resize-none bg-transparent p-1 leading-6 text-text-primary outline-none',
|
'body-lg-regular w-full resize-none bg-transparent p-1 leading-6 text-text-primary outline-none',
|
||||||
)}
|
)}
|
||||||
placeholder={t('common.chat.inputPlaceholder', { botName }) || ''}
|
placeholder={readonly ? t('common.chat.inputDisabledPlaceholder') : t('common.chat.inputPlaceholder', { botName }) || ''}
|
||||||
autoFocus
|
autoFocus
|
||||||
minRows={1}
|
minRows={1}
|
||||||
value={query}
|
value={query}
|
||||||
@ -216,6 +220,7 @@ const ChatInputArea = ({
|
|||||||
onDragLeave={handleDragFileLeave}
|
onDragLeave={handleDragFileLeave}
|
||||||
onDragOver={handleDragFileOver}
|
onDragOver={handleDragFileOver}
|
||||||
onDrop={handleDropFile}
|
onDrop={handleDropFile}
|
||||||
|
readOnly={readonly}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{
|
{
|
||||||
@ -237,7 +242,12 @@ const ChatInputArea = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
{showFeatureBar && <FeatureBar showFileUpload={showFileUpload} disabled={featureBarDisabled} onFeatureBarClick={onFeatureBarClick} />}
|
{showFeatureBar && <FeatureBar
|
||||||
|
showFileUpload={showFileUpload}
|
||||||
|
disabled={featureBarDisabled}
|
||||||
|
onFeatureBarClick={readonly ? noop : onFeatureBarClick}
|
||||||
|
hideEditEntrance={readonly}
|
||||||
|
/>}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,8 +13,10 @@ import ActionButton from '@/app/components/base/action-button'
|
|||||||
import { FileUploaderInChatInput } from '@/app/components/base/file-uploader'
|
import { FileUploaderInChatInput } from '@/app/components/base/file-uploader'
|
||||||
import type { FileUpload } from '@/app/components/base/features/types'
|
import type { FileUpload } from '@/app/components/base/features/types'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
|
import { noop } from 'lodash-es'
|
||||||
|
|
||||||
type OperationProps = {
|
type OperationProps = {
|
||||||
|
readonly?: boolean
|
||||||
fileConfig?: FileUpload
|
fileConfig?: FileUpload
|
||||||
speechToTextConfig?: EnableType
|
speechToTextConfig?: EnableType
|
||||||
onShowVoiceInput?: () => void
|
onShowVoiceInput?: () => void
|
||||||
@ -23,6 +25,7 @@ type OperationProps = {
|
|||||||
ref?: Ref<HTMLDivElement>;
|
ref?: Ref<HTMLDivElement>;
|
||||||
}
|
}
|
||||||
const Operation: FC<OperationProps> = ({
|
const Operation: FC<OperationProps> = ({
|
||||||
|
readonly,
|
||||||
ref,
|
ref,
|
||||||
fileConfig,
|
fileConfig,
|
||||||
speechToTextConfig,
|
speechToTextConfig,
|
||||||
@ -41,12 +44,13 @@ const Operation: FC<OperationProps> = ({
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
>
|
>
|
||||||
<div className='flex items-center space-x-1'>
|
<div className='flex items-center space-x-1'>
|
||||||
{fileConfig?.enabled && <FileUploaderInChatInput fileConfig={fileConfig} />}
|
{fileConfig?.enabled && <FileUploaderInChatInput readonly={readonly} fileConfig={fileConfig} />}
|
||||||
{
|
{
|
||||||
speechToTextConfig?.enabled && (
|
speechToTextConfig?.enabled && (
|
||||||
<ActionButton
|
<ActionButton
|
||||||
size='l'
|
size='l'
|
||||||
onClick={onShowVoiceInput}
|
disabled={readonly}
|
||||||
|
onClick={readonly ? noop : onShowVoiceInput}
|
||||||
>
|
>
|
||||||
<RiMicLine className='h-5 w-5' />
|
<RiMicLine className='h-5 w-5' />
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
@ -56,7 +60,8 @@ const Operation: FC<OperationProps> = ({
|
|||||||
<Button
|
<Button
|
||||||
className='ml-3 w-8 px-0'
|
className='ml-3 w-8 px-0'
|
||||||
variant='primary'
|
variant='primary'
|
||||||
onClick={onSend}
|
onClick={readonly ? noop : onSend}
|
||||||
|
disabled={readonly}
|
||||||
style={
|
style={
|
||||||
theme
|
theme
|
||||||
? {
|
? {
|
||||||
|
|||||||
@ -15,11 +15,15 @@ export type ChatContextValue = Pick<ChatProps, 'config'
|
|||||||
| 'onAnnotationEdited'
|
| 'onAnnotationEdited'
|
||||||
| 'onAnnotationAdded'
|
| 'onAnnotationAdded'
|
||||||
| 'onAnnotationRemoved'
|
| 'onAnnotationRemoved'
|
||||||
|
| 'disableFeedback'
|
||||||
| 'onFeedback'
|
| 'onFeedback'
|
||||||
>
|
> & {
|
||||||
|
readonly?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
const ChatContext = createContext<ChatContextValue>({
|
const ChatContext = createContext<ChatContextValue>({
|
||||||
chatList: [],
|
chatList: [],
|
||||||
|
readonly: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
type ChatContextProviderProps = {
|
type ChatContextProviderProps = {
|
||||||
@ -28,6 +32,7 @@ type ChatContextProviderProps = {
|
|||||||
|
|
||||||
export const ChatContextProvider = ({
|
export const ChatContextProvider = ({
|
||||||
children,
|
children,
|
||||||
|
readonly = false,
|
||||||
config,
|
config,
|
||||||
isResponding,
|
isResponding,
|
||||||
chatList,
|
chatList,
|
||||||
@ -39,11 +44,13 @@ export const ChatContextProvider = ({
|
|||||||
onAnnotationEdited,
|
onAnnotationEdited,
|
||||||
onAnnotationAdded,
|
onAnnotationAdded,
|
||||||
onAnnotationRemoved,
|
onAnnotationRemoved,
|
||||||
|
disableFeedback,
|
||||||
onFeedback,
|
onFeedback,
|
||||||
}: ChatContextProviderProps) => {
|
}: ChatContextProviderProps) => {
|
||||||
return (
|
return (
|
||||||
<ChatContext.Provider value={{
|
<ChatContext.Provider value={{
|
||||||
config,
|
config,
|
||||||
|
readonly,
|
||||||
isResponding,
|
isResponding,
|
||||||
chatList: chatList || [],
|
chatList: chatList || [],
|
||||||
showPromptLog,
|
showPromptLog,
|
||||||
@ -54,6 +61,7 @@ export const ChatContextProvider = ({
|
|||||||
onAnnotationEdited,
|
onAnnotationEdited,
|
||||||
onAnnotationAdded,
|
onAnnotationAdded,
|
||||||
onAnnotationRemoved,
|
onAnnotationRemoved,
|
||||||
|
disableFeedback,
|
||||||
onFeedback,
|
onFeedback,
|
||||||
}}>
|
}}>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@ -36,6 +36,8 @@ import { useStore as useAppStore } from '@/app/components/app/store'
|
|||||||
import type { AppData } from '@/models/share'
|
import type { AppData } from '@/models/share'
|
||||||
|
|
||||||
export type ChatProps = {
|
export type ChatProps = {
|
||||||
|
isTryApp?: boolean
|
||||||
|
readonly?: boolean
|
||||||
appData?: AppData
|
appData?: AppData
|
||||||
chatList: ChatItem[]
|
chatList: ChatItem[]
|
||||||
config?: ChatConfig
|
config?: ChatConfig
|
||||||
@ -60,6 +62,7 @@ export type ChatProps = {
|
|||||||
onAnnotationAdded?: (annotationId: string, authorName: string, question: string, answer: string, index: number) => void
|
onAnnotationAdded?: (annotationId: string, authorName: string, question: string, answer: string, index: number) => void
|
||||||
onAnnotationRemoved?: (index: number) => void
|
onAnnotationRemoved?: (index: number) => void
|
||||||
chatNode?: ReactNode
|
chatNode?: ReactNode
|
||||||
|
disableFeedback?: boolean
|
||||||
onFeedback?: (messageId: string, feedback: Feedback) => void
|
onFeedback?: (messageId: string, feedback: Feedback) => void
|
||||||
chatAnswerContainerInner?: string
|
chatAnswerContainerInner?: string
|
||||||
hideProcessDetail?: boolean
|
hideProcessDetail?: boolean
|
||||||
@ -76,6 +79,8 @@ export type ChatProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Chat: FC<ChatProps> = ({
|
const Chat: FC<ChatProps> = ({
|
||||||
|
isTryApp,
|
||||||
|
readonly = false,
|
||||||
appData,
|
appData,
|
||||||
config,
|
config,
|
||||||
onSend,
|
onSend,
|
||||||
@ -99,6 +104,7 @@ const Chat: FC<ChatProps> = ({
|
|||||||
onAnnotationEdited,
|
onAnnotationEdited,
|
||||||
onAnnotationRemoved,
|
onAnnotationRemoved,
|
||||||
chatNode,
|
chatNode,
|
||||||
|
disableFeedback,
|
||||||
onFeedback,
|
onFeedback,
|
||||||
chatAnswerContainerInner,
|
chatAnswerContainerInner,
|
||||||
hideProcessDetail,
|
hideProcessDetail,
|
||||||
@ -219,6 +225,7 @@ const Chat: FC<ChatProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ChatContextProvider
|
<ChatContextProvider
|
||||||
|
readonly={readonly}
|
||||||
config={config}
|
config={config}
|
||||||
chatList={chatList}
|
chatList={chatList}
|
||||||
isResponding={isResponding}
|
isResponding={isResponding}
|
||||||
@ -230,17 +237,18 @@ const Chat: FC<ChatProps> = ({
|
|||||||
onAnnotationAdded={onAnnotationAdded}
|
onAnnotationAdded={onAnnotationAdded}
|
||||||
onAnnotationEdited={onAnnotationEdited}
|
onAnnotationEdited={onAnnotationEdited}
|
||||||
onAnnotationRemoved={onAnnotationRemoved}
|
onAnnotationRemoved={onAnnotationRemoved}
|
||||||
|
disableFeedback={disableFeedback}
|
||||||
onFeedback={onFeedback}
|
onFeedback={onFeedback}
|
||||||
>
|
>
|
||||||
<div className='relative h-full'>
|
<div className={cn('relative h-full', isTryApp && 'flex flex-col')}>
|
||||||
<div
|
<div
|
||||||
ref={chatContainerRef}
|
ref={chatContainerRef}
|
||||||
className={cn('relative h-full overflow-y-auto overflow-x-hidden', chatContainerClassName)}
|
className={cn('relative h-full overflow-y-auto overflow-x-hidden', isTryApp && 'h-0 grow', chatContainerClassName)}
|
||||||
>
|
>
|
||||||
{chatNode}
|
{chatNode}
|
||||||
<div
|
<div
|
||||||
ref={chatContainerInnerRef}
|
ref={chatContainerInnerRef}
|
||||||
className={cn('w-full', !noSpacing && 'px-8', chatContainerInnerClassName)}
|
className={cn('w-full', !noSpacing && 'px-8', chatContainerInnerClassName, isTryApp && 'px-0')}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
chatList.map((item, index) => {
|
chatList.map((item, index) => {
|
||||||
@ -284,7 +292,7 @@ const Chat: FC<ChatProps> = ({
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
ref={chatFooterInnerRef}
|
ref={chatFooterInnerRef}
|
||||||
className={cn('relative', chatFooterInnerClassName)}
|
className={cn('relative', chatFooterInnerClassName, isTryApp && 'px-0')}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
!noStopResponding && isResponding && (
|
!noStopResponding && isResponding && (
|
||||||
@ -321,6 +329,7 @@ const Chat: FC<ChatProps> = ({
|
|||||||
inputsForm={inputsForm}
|
inputsForm={inputsForm}
|
||||||
theme={themeBuilder?.theme}
|
theme={themeBuilder?.theme}
|
||||||
isResponding={isResponding}
|
isResponding={isResponding}
|
||||||
|
readonly={readonly}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import { InputVarType } from '@/app/components/workflow/types'
|
|||||||
import { TransferMethod } from '@/types/app'
|
import { TransferMethod } from '@/types/app'
|
||||||
import InputsForm from '@/app/components/base/chat/embedded-chatbot/inputs-form'
|
import InputsForm from '@/app/components/base/chat/embedded-chatbot/inputs-form'
|
||||||
import {
|
import {
|
||||||
|
AppSourceType,
|
||||||
fetchSuggestedQuestions,
|
fetchSuggestedQuestions,
|
||||||
getUrl,
|
getUrl,
|
||||||
stopChatMessageResponding,
|
stopChatMessageResponding,
|
||||||
@ -43,6 +44,7 @@ const ChatWrapper = () => {
|
|||||||
isInstalledApp,
|
isInstalledApp,
|
||||||
appId,
|
appId,
|
||||||
appMeta,
|
appMeta,
|
||||||
|
disableFeedback,
|
||||||
handleFeedback,
|
handleFeedback,
|
||||||
currentChatInstanceRef,
|
currentChatInstanceRef,
|
||||||
themeBuilder,
|
themeBuilder,
|
||||||
@ -51,7 +53,9 @@ const ChatWrapper = () => {
|
|||||||
setIsResponding,
|
setIsResponding,
|
||||||
allInputsHidden,
|
allInputsHidden,
|
||||||
initUserVariables,
|
initUserVariables,
|
||||||
|
appSourceType,
|
||||||
} = useEmbeddedChatbotContext()
|
} = useEmbeddedChatbotContext()
|
||||||
|
|
||||||
const appConfig = useMemo(() => {
|
const appConfig = useMemo(() => {
|
||||||
const config = appParams || {}
|
const config = appParams || {}
|
||||||
|
|
||||||
@ -79,7 +83,7 @@ const ChatWrapper = () => {
|
|||||||
inputsForm: inputsForms,
|
inputsForm: inputsForms,
|
||||||
},
|
},
|
||||||
appPrevChatList,
|
appPrevChatList,
|
||||||
taskId => stopChatMessageResponding('', taskId, isInstalledApp, appId),
|
taskId => stopChatMessageResponding('', taskId, appSourceType, appId),
|
||||||
clearChatList,
|
clearChatList,
|
||||||
setClearChatList,
|
setClearChatList,
|
||||||
)
|
)
|
||||||
@ -135,14 +139,13 @@ const ChatWrapper = () => {
|
|||||||
conversation_id: currentConversationId,
|
conversation_id: currentConversationId,
|
||||||
parent_message_id: (isRegenerate ? parentAnswer?.id : getLastAnswer(chatList)?.id) || null,
|
parent_message_id: (isRegenerate ? parentAnswer?.id : getLastAnswer(chatList)?.id) || null,
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSend(
|
handleSend(
|
||||||
getUrl('chat-messages', isInstalledApp, appId || ''),
|
getUrl('chat-messages', appSourceType, appId || ''),
|
||||||
data,
|
data,
|
||||||
{
|
{
|
||||||
onGetSuggestedQuestions: responseItemId => fetchSuggestedQuestions(responseItemId, isInstalledApp, appId),
|
onGetSuggestedQuestions: responseItemId => fetchSuggestedQuestions(responseItemId, appSourceType, appId),
|
||||||
onConversationComplete: currentConversationId ? undefined : handleNewConversationCompleted,
|
onConversationComplete: currentConversationId ? undefined : handleNewConversationCompleted,
|
||||||
isPublicAPI: !isInstalledApp,
|
isPublicAPI: appSourceType === AppSourceType.webApp,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}, [currentConversationId, currentConversationInputs, newConversationInputs, chatList, handleSend, isInstalledApp, appId, handleNewConversationCompleted])
|
}, [currentConversationId, currentConversationInputs, newConversationInputs, chatList, handleSend, isInstalledApp, appId, handleNewConversationCompleted])
|
||||||
@ -164,7 +167,8 @@ const ChatWrapper = () => {
|
|||||||
return chatList.filter(item => !item.isOpeningStatement)
|
return chatList.filter(item => !item.isOpeningStatement)
|
||||||
}, [chatList, currentConversationId])
|
}, [chatList, currentConversationId])
|
||||||
|
|
||||||
const [collapsed, setCollapsed] = useState(!!currentConversationId)
|
const isTryApp = appSourceType === AppSourceType.tryApp
|
||||||
|
const [collapsed, setCollapsed] = useState(!!currentConversationId && !isTryApp) // try app always use the new chat
|
||||||
|
|
||||||
const chatNode = useMemo(() => {
|
const chatNode = useMemo(() => {
|
||||||
if (allInputsHidden || !inputsForms.length)
|
if (allInputsHidden || !inputsForms.length)
|
||||||
@ -237,6 +241,7 @@ const ChatWrapper = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Chat
|
<Chat
|
||||||
|
isTryApp={isTryApp}
|
||||||
appData={appData || undefined}
|
appData={appData || undefined}
|
||||||
config={appConfig}
|
config={appConfig}
|
||||||
chatList={messageList}
|
chatList={messageList}
|
||||||
@ -256,6 +261,7 @@ const ChatWrapper = () => {
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
allToolIcons={appMeta?.tool_icons || {}}
|
allToolIcons={appMeta?.tool_icons || {}}
|
||||||
|
disableFeedback={disableFeedback}
|
||||||
onFeedback={handleFeedback}
|
onFeedback={handleFeedback}
|
||||||
suggestedQuestions={suggestedQuestions}
|
suggestedQuestions={suggestedQuestions}
|
||||||
answerIcon={answerIcon}
|
answerIcon={answerIcon}
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import type {
|
|||||||
ConversationItem,
|
ConversationItem,
|
||||||
} from '@/models/share'
|
} from '@/models/share'
|
||||||
import { noop } from 'lodash-es'
|
import { noop } from 'lodash-es'
|
||||||
|
import { AppSourceType } from '@/service/share'
|
||||||
|
|
||||||
export type EmbeddedChatbotContextValue = {
|
export type EmbeddedChatbotContextValue = {
|
||||||
appMeta: AppMeta | null
|
appMeta: AppMeta | null
|
||||||
@ -37,8 +38,10 @@ export type EmbeddedChatbotContextValue = {
|
|||||||
chatShouldReloadKey: string
|
chatShouldReloadKey: string
|
||||||
isMobile: boolean
|
isMobile: boolean
|
||||||
isInstalledApp: boolean
|
isInstalledApp: boolean
|
||||||
|
appSourceType: AppSourceType
|
||||||
allowResetChat: boolean
|
allowResetChat: boolean
|
||||||
appId?: string
|
appId?: string
|
||||||
|
disableFeedback?: boolean
|
||||||
handleFeedback: (messageId: string, feedback: Feedback) => void
|
handleFeedback: (messageId: string, feedback: Feedback) => void
|
||||||
currentChatInstanceRef: RefObject<{ handleStop: () => void }>
|
currentChatInstanceRef: RefObject<{ handleStop: () => void }>
|
||||||
themeBuilder?: ThemeBuilder
|
themeBuilder?: ThemeBuilder
|
||||||
@ -74,6 +77,7 @@ export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>
|
|||||||
handleNewConversationCompleted: noop,
|
handleNewConversationCompleted: noop,
|
||||||
chatShouldReloadKey: '',
|
chatShouldReloadKey: '',
|
||||||
isMobile: false,
|
isMobile: false,
|
||||||
|
appSourceType: AppSourceType.webApp,
|
||||||
isInstalledApp: false,
|
isInstalledApp: false,
|
||||||
allowResetChat: true,
|
allowResetChat: true,
|
||||||
handleFeedback: noop,
|
handleFeedback: noop,
|
||||||
|
|||||||
@ -18,11 +18,18 @@ import { CONVERSATION_ID_INFO } from '../constants'
|
|||||||
import { buildChatItemTree, getProcessedInputsFromUrlParams, getProcessedSystemVariablesFromUrlParams, getProcessedUserVariablesFromUrlParams } from '../utils'
|
import { buildChatItemTree, getProcessedInputsFromUrlParams, getProcessedSystemVariablesFromUrlParams, getProcessedUserVariablesFromUrlParams } from '../utils'
|
||||||
import { getProcessedFilesFromResponse } from '../../file-uploader/utils'
|
import { getProcessedFilesFromResponse } from '../../file-uploader/utils'
|
||||||
import {
|
import {
|
||||||
|
AppSourceType,
|
||||||
|
fetchAppInfo,
|
||||||
|
fetchAppMeta,
|
||||||
|
fetchAppParams,
|
||||||
fetchChatList,
|
fetchChatList,
|
||||||
fetchConversations,
|
fetchConversations,
|
||||||
generationConversationName,
|
generationConversationName,
|
||||||
updateFeedback,
|
updateFeedback,
|
||||||
} from '@/service/share'
|
} from '@/service/share'
|
||||||
|
import {
|
||||||
|
fetchTryAppInfo,
|
||||||
|
} from '@/service/try-app'
|
||||||
import type {
|
import type {
|
||||||
// AppData,
|
// AppData,
|
||||||
ConversationItem,
|
ConversationItem,
|
||||||
@ -33,7 +40,7 @@ import { InputVarType } from '@/app/components/workflow/types'
|
|||||||
import { TransferMethod } from '@/types/app'
|
import { TransferMethod } from '@/types/app'
|
||||||
import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils'
|
import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils'
|
||||||
import { noop } from 'lodash-es'
|
import { noop } from 'lodash-es'
|
||||||
import { useWebAppStore } from '@/context/web-app-context'
|
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||||
|
|
||||||
function getFormattedChatList(messages: any[]) {
|
function getFormattedChatList(messages: any[]) {
|
||||||
const newChatList: ChatItem[] = []
|
const newChatList: ChatItem[] = []
|
||||||
@ -61,16 +68,17 @@ function getFormattedChatList(messages: any[]) {
|
|||||||
return newChatList
|
return newChatList
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useEmbeddedChatbot = () => {
|
export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: string) => {
|
||||||
const isInstalledApp = false
|
const isInstalledApp = false // just can be webapp and try app
|
||||||
const appInfo = useWebAppStore(s => s.appInfo)
|
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
|
||||||
const appMeta = useWebAppStore(s => s.appMeta)
|
const isTryApp = appSourceType === AppSourceType.tryApp
|
||||||
const appParams = useWebAppStore(s => s.appParams)
|
const { data: appInfo, isLoading: appInfoLoading, error: appInfoError } = useSWR('appInfo', isTryApp ? () => fetchTryAppInfo(tryAppId) : fetchAppInfo)
|
||||||
const appId = useMemo(() => appInfo?.app_id, [appInfo])
|
const appId = useMemo(() => isTryApp ? tryAppId : appInfo?.app_id, [appInfo])
|
||||||
|
|
||||||
const [userId, setUserId] = useState<string>()
|
const [userId, setUserId] = useState<string>()
|
||||||
const [conversationId, setConversationId] = useState<string>()
|
const [conversationId, setConversationId] = useState<string>()
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (isTryApp) return
|
||||||
getProcessedSystemVariablesFromUrlParams().then(({ user_id, conversation_id }) => {
|
getProcessedSystemVariablesFromUrlParams().then(({ user_id, conversation_id }) => {
|
||||||
setUserId(user_id)
|
setUserId(user_id)
|
||||||
setConversationId(conversation_id)
|
setConversationId(conversation_id)
|
||||||
@ -78,6 +86,7 @@ export const useEmbeddedChatbot = () => {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (isTryApp) return
|
||||||
const setLanguageFromParams = async () => {
|
const setLanguageFromParams = async () => {
|
||||||
// Check URL parameters for language override
|
// Check URL parameters for language override
|
||||||
const urlParams = new URLSearchParams(window.location.search)
|
const urlParams = new URLSearchParams(window.location.search)
|
||||||
@ -108,8 +117,8 @@ export const useEmbeddedChatbot = () => {
|
|||||||
defaultValue: {},
|
defaultValue: {},
|
||||||
})
|
})
|
||||||
const allowResetChat = !conversationId
|
const allowResetChat = !conversationId
|
||||||
const currentConversationId = useMemo(() => conversationIdInfo?.[appId || '']?.[userId || 'DEFAULT'] || conversationId || '',
|
const currentConversationId = useMemo(() => isTryApp ? '' : conversationIdInfo?.[appId || '']?.[userId || 'DEFAULT'] || conversationId || '',
|
||||||
[appId, conversationIdInfo, userId, conversationId])
|
[isTryApp, appId, conversationIdInfo, userId, conversationId])
|
||||||
const handleConversationIdInfoChange = useCallback((changeConversationId: string) => {
|
const handleConversationIdInfoChange = useCallback((changeConversationId: string) => {
|
||||||
if (appId) {
|
if (appId) {
|
||||||
let prevValue = conversationIdInfo?.[appId || '']
|
let prevValue = conversationIdInfo?.[appId || '']
|
||||||
@ -133,9 +142,11 @@ export const useEmbeddedChatbot = () => {
|
|||||||
return currentConversationId
|
return currentConversationId
|
||||||
}, [currentConversationId, newConversationId])
|
}, [currentConversationId, newConversationId])
|
||||||
|
|
||||||
const { data: appPinnedConversationData } = useSWR(['appConversationData', isInstalledApp, appId, true], () => fetchConversations(isInstalledApp, appId, undefined, true, 100))
|
const { data: appParams } = useSWR(['appParams', appSourceType, appId], () => fetchAppParams(appSourceType, appId))
|
||||||
const { data: appConversationData, isLoading: appConversationDataLoading, mutate: mutateAppConversationData } = useSWR(['appConversationData', isInstalledApp, appId, false], () => fetchConversations(isInstalledApp, appId, undefined, false, 100))
|
const { data: appMeta } = useSWR(isTryApp ? null : ['appMeta', appSourceType, appId], () => fetchAppMeta(appSourceType, appId))
|
||||||
const { data: appChatListData, isLoading: appChatListDataLoading } = useSWR(chatShouldReloadKey ? ['appChatList', chatShouldReloadKey, isInstalledApp, appId] : null, () => fetchChatList(chatShouldReloadKey, isInstalledApp, 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 ? ['appChatList', chatShouldReloadKey, appSourceType, appId] : null, () => fetchChatList(chatShouldReloadKey, appSourceType, appId))
|
||||||
|
|
||||||
const [clearChatList, setClearChatList] = useState(false)
|
const [clearChatList, setClearChatList] = useState(false)
|
||||||
const [isResponding, setIsResponding] = useState(false)
|
const [isResponding, setIsResponding] = useState(false)
|
||||||
@ -240,6 +251,8 @@ export const useEmbeddedChatbot = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// init inputs from url params
|
// init inputs from url params
|
||||||
(async () => {
|
(async () => {
|
||||||
|
if (isTryApp)
|
||||||
|
return
|
||||||
const inputs = await getProcessedInputsFromUrlParams()
|
const inputs = await getProcessedInputsFromUrlParams()
|
||||||
const userVariables = await getProcessedUserVariablesFromUrlParams()
|
const userVariables = await getProcessedUserVariablesFromUrlParams()
|
||||||
setInitInputs(inputs)
|
setInitInputs(inputs)
|
||||||
@ -255,7 +268,7 @@ export const useEmbeddedChatbot = () => {
|
|||||||
handleNewConversationInputsChange(conversationInputs)
|
handleNewConversationInputsChange(conversationInputs)
|
||||||
}, [handleNewConversationInputsChange, inputsForms])
|
}, [handleNewConversationInputsChange, inputsForms])
|
||||||
|
|
||||||
const { data: newConversation } = useSWR(newConversationId ? [isInstalledApp, appId, newConversationId] : null, () => generationConversationName(isInstalledApp, appId, newConversationId), { revalidateOnFocus: false })
|
const { data: newConversation } = useSWR((!isTryApp && newConversationId) ? [isInstalledApp, appId, newConversationId] : null, () => generationConversationName(appSourceType, appId, newConversationId), { revalidateOnFocus: false })
|
||||||
const [originConversationList, setOriginConversationList] = useState<ConversationItem[]>([])
|
const [originConversationList, setOriginConversationList] = useState<ConversationItem[]>([])
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (appConversationData?.data && !appConversationDataLoading)
|
if (appConversationData?.data && !appConversationDataLoading)
|
||||||
@ -379,11 +392,15 @@ export const useEmbeddedChatbot = () => {
|
|||||||
}, [mutateAppConversationData, handleConversationIdInfoChange])
|
}, [mutateAppConversationData, handleConversationIdInfoChange])
|
||||||
|
|
||||||
const handleFeedback = useCallback(async (messageId: string, feedback: Feedback) => {
|
const handleFeedback = useCallback(async (messageId: string, feedback: Feedback) => {
|
||||||
await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating, content: feedback.content } }, isInstalledApp, appId)
|
await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating, content: feedback.content } }, appSourceType, appId)
|
||||||
notify({ type: 'success', message: t('common.api.success') })
|
notify({ type: 'success', message: t('common.api.success') })
|
||||||
}, [isInstalledApp, appId, t, notify])
|
}, [appSourceType, appId, t, notify])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
appInfoError,
|
||||||
|
appInfoLoading: appInfoLoading || (systemFeatures.webapp_auth.enabled && isCheckingPermission),
|
||||||
|
userCanAccess: isTryApp || (systemFeatures.webapp_auth.enabled ? (userCanAccessResult as { result: boolean })?.result : true),
|
||||||
|
appSourceType,
|
||||||
isInstalledApp,
|
isInstalledApp,
|
||||||
allowResetChat,
|
allowResetChat,
|
||||||
appId,
|
appId,
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import Divider from '@/app/components/base/divider'
|
|||||||
import InputsFormContent from '@/app/components/base/chat/embedded-chatbot/inputs-form/content'
|
import InputsFormContent from '@/app/components/base/chat/embedded-chatbot/inputs-form/content'
|
||||||
import { useEmbeddedChatbotContext } from '../context'
|
import { useEmbeddedChatbotContext } from '../context'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
|
import { AppSourceType } from '@/service/share'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
collapsed: boolean
|
collapsed: boolean
|
||||||
@ -18,6 +19,7 @@ const InputsFormNode = ({
|
|||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const {
|
const {
|
||||||
|
appSourceType,
|
||||||
isMobile,
|
isMobile,
|
||||||
currentConversationId,
|
currentConversationId,
|
||||||
themeBuilder,
|
themeBuilder,
|
||||||
@ -25,15 +27,17 @@ const InputsFormNode = ({
|
|||||||
allInputsHidden,
|
allInputsHidden,
|
||||||
inputsForms,
|
inputsForms,
|
||||||
} = useEmbeddedChatbotContext()
|
} = useEmbeddedChatbotContext()
|
||||||
|
const isTryApp = appSourceType === AppSourceType.tryApp
|
||||||
|
|
||||||
if (allInputsHidden || inputsForms.length === 0)
|
if (allInputsHidden || inputsForms.length === 0)
|
||||||
return null
|
return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('mb-6 flex flex-col items-center px-4 pt-6', isMobile && 'mb-4 pt-4')}>
|
<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(
|
<div className={cn(
|
||||||
'w-full max-w-[672px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-md',
|
'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',
|
collapsed && 'border border-components-card-border bg-components-card-bg shadow-none',
|
||||||
|
isTryApp && 'max-w-[auto]',
|
||||||
)}>
|
)}>
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
'flex items-center gap-3 rounded-t-2xl px-6 py-4',
|
'flex items-center gap-3 rounded-t-2xl px-6 py-4',
|
||||||
|
|||||||
@ -13,6 +13,7 @@ type Props = {
|
|||||||
showFileUpload?: boolean
|
showFileUpload?: boolean
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
onFeatureBarClick?: (state: boolean) => void
|
onFeatureBarClick?: (state: boolean) => void
|
||||||
|
hideEditEntrance?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const FeatureBar = ({
|
const FeatureBar = ({
|
||||||
@ -20,6 +21,7 @@ const FeatureBar = ({
|
|||||||
showFileUpload = true,
|
showFileUpload = true,
|
||||||
disabled,
|
disabled,
|
||||||
onFeatureBarClick,
|
onFeatureBarClick,
|
||||||
|
hideEditEntrance = false,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const features = useFeatures(s => s.features)
|
const features = useFeatures(s => s.features)
|
||||||
@ -132,10 +134,14 @@ const FeatureBar = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className='body-xs-regular grow text-text-tertiary'>{t('appDebug.feature.bar.enableText')}</div>
|
<div className='body-xs-regular grow text-text-tertiary'>{t('appDebug.feature.bar.enableText')}</div>
|
||||||
<Button className='shrink-0' variant='ghost-accent' size='small' onClick={() => onFeatureBarClick?.(true)}>
|
{
|
||||||
<div className='mx-1'>{t('appDebug.feature.bar.manage')}</div>
|
!hideEditEntrance && (
|
||||||
<RiArrowRightLine className='h-3.5 w-3.5 text-text-accent' />
|
<Button className='shrink-0' variant='ghost-accent' size='small' onClick={() => onFeatureBarClick?.(true)}>
|
||||||
</Button>
|
<div className='mx-1'>{t('appDebug.feature.bar.manage')}</div>
|
||||||
|
<RiArrowRightLine className='h-3.5 w-3.5 text-text-accent' />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -13,21 +13,27 @@ import { TransferMethod } from '@/types/app'
|
|||||||
|
|
||||||
type FileUploaderInChatInputProps = {
|
type FileUploaderInChatInputProps = {
|
||||||
fileConfig: FileUpload
|
fileConfig: FileUpload
|
||||||
|
readonly?: boolean
|
||||||
}
|
}
|
||||||
const FileUploaderInChatInput = ({
|
const FileUploaderInChatInput = ({
|
||||||
fileConfig,
|
fileConfig,
|
||||||
|
readonly,
|
||||||
}: FileUploaderInChatInputProps) => {
|
}: FileUploaderInChatInputProps) => {
|
||||||
const renderTrigger = useCallback((open: boolean) => {
|
const renderTrigger = useCallback((open: boolean) => {
|
||||||
return (
|
return (
|
||||||
<ActionButton
|
<ActionButton
|
||||||
size='l'
|
size='l'
|
||||||
className={cn(open && 'bg-state-base-hover')}
|
className={cn(open && 'bg-state-base-hover')}
|
||||||
|
disabled={readonly}
|
||||||
>
|
>
|
||||||
<RiAttachmentLine className='h-5 w-5' />
|
<RiAttachmentLine className='h-5 w-5' />
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
)
|
)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
if(readonly)
|
||||||
|
return renderTrigger(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FileFromLinkOrLocal
|
<FileFromLinkOrLocal
|
||||||
trigger={renderTrigger}
|
trigger={renderTrigger}
|
||||||
|
|||||||
@ -69,10 +69,12 @@ const PasteImageLinkButton: FC<PasteImageLinkButtonProps> = ({
|
|||||||
type TextGenerationImageUploaderProps = {
|
type TextGenerationImageUploaderProps = {
|
||||||
settings: VisionSettings
|
settings: VisionSettings
|
||||||
onFilesChange: (files: ImageFile[]) => void
|
onFilesChange: (files: ImageFile[]) => void
|
||||||
|
disabled?: boolean
|
||||||
}
|
}
|
||||||
const TextGenerationImageUploader: FC<TextGenerationImageUploaderProps> = ({
|
const TextGenerationImageUploader: FC<TextGenerationImageUploaderProps> = ({
|
||||||
settings,
|
settings,
|
||||||
onFilesChange,
|
onFilesChange,
|
||||||
|
disabled,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
@ -92,7 +94,7 @@ const TextGenerationImageUploader: FC<TextGenerationImageUploaderProps> = ({
|
|||||||
const localUpload = (
|
const localUpload = (
|
||||||
<Uploader
|
<Uploader
|
||||||
onUpload={onUpload}
|
onUpload={onUpload}
|
||||||
disabled={files.length >= settings.number_limits}
|
disabled={files.length >= settings.number_limits || disabled}
|
||||||
limit={+settings.image_file_size_limit!}
|
limit={+settings.image_file_size_limit!}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
@ -113,7 +115,7 @@ const TextGenerationImageUploader: FC<TextGenerationImageUploaderProps> = ({
|
|||||||
const urlUpload = (
|
const urlUpload = (
|
||||||
<PasteImageLinkButton
|
<PasteImageLinkButton
|
||||||
onUpload={onUpload}
|
onUpload={onUpload}
|
||||||
disabled={files.length >= settings.number_limits}
|
disabled={files.length >= settings.number_limits || disabled}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -16,6 +16,8 @@ export type ITabHeaderProps = {
|
|||||||
items: Item[]
|
items: Item[]
|
||||||
value: string
|
value: string
|
||||||
itemClassName?: string
|
itemClassName?: string
|
||||||
|
itemWrapClassName?: string
|
||||||
|
activeItemClassName?: string
|
||||||
onChange: (value: string) => void
|
onChange: (value: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,6 +25,8 @@ const TabHeader: FC<ITabHeaderProps> = ({
|
|||||||
items,
|
items,
|
||||||
value,
|
value,
|
||||||
itemClassName,
|
itemClassName,
|
||||||
|
itemWrapClassName,
|
||||||
|
activeItemClassName,
|
||||||
onChange,
|
onChange,
|
||||||
}) => {
|
}) => {
|
||||||
const renderItem = ({ id, name, icon, extra, disabled }: Item) => (
|
const renderItem = ({ id, name, icon, extra, disabled }: Item) => (
|
||||||
@ -30,8 +34,9 @@ const TabHeader: FC<ITabHeaderProps> = ({
|
|||||||
key={id}
|
key={id}
|
||||||
className={cn(
|
className={cn(
|
||||||
'system-md-semibold relative flex cursor-pointer items-center border-b-2 border-transparent pb-2 pt-2.5',
|
'system-md-semibold relative flex cursor-pointer items-center border-b-2 border-transparent pb-2 pt-2.5',
|
||||||
id === value ? 'border-components-tab-active text-text-primary' : 'text-text-tertiary',
|
id === value ? cn('border-components-tab-active text-text-primary', activeItemClassName) : 'text-text-tertiary',
|
||||||
disabled && 'cursor-not-allowed opacity-30',
|
disabled && 'cursor-not-allowed opacity-30',
|
||||||
|
itemWrapClassName,
|
||||||
)}
|
)}
|
||||||
onClick={() => !disabled && onChange(id)}
|
onClick={() => !disabled && onChange(id)}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import { convertToMp3 } from './utils'
|
|||||||
import s from './index.module.css'
|
import s from './index.module.css'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
import { StopCircle } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
|
import { StopCircle } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
|
||||||
import { audioToText } from '@/service/share'
|
import { AppSourceType, audioToText } from '@/service/share'
|
||||||
|
|
||||||
type VoiceInputTypes = {
|
type VoiceInputTypes = {
|
||||||
onConverted: (text: string) => void
|
onConverted: (text: string) => void
|
||||||
@ -108,7 +108,7 @@ const VoiceInput = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const audioResponse = await audioToText(url, isPublic, formData)
|
const audioResponse = await audioToText(url, isPublic ? AppSourceType.webApp : AppSourceType.installedApp, formData)
|
||||||
onConverted(audioResponse.text)
|
onConverted(audioResponse.text)
|
||||||
onCancel()
|
onCancel()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,8 +19,6 @@ import { IS_CE_EDITION } from '@/config'
|
|||||||
import { Theme } from '@/types/app'
|
import { Theme } from '@/types/app'
|
||||||
import useTheme from '@/hooks/use-theme'
|
import useTheme from '@/hooks/use-theme'
|
||||||
|
|
||||||
const FILES_NUMBER_LIMIT = 20
|
|
||||||
|
|
||||||
type IFileUploaderProps = {
|
type IFileUploaderProps = {
|
||||||
fileList: FileItem[]
|
fileList: FileItem[]
|
||||||
titleClassName?: string
|
titleClassName?: string
|
||||||
@ -72,6 +70,7 @@ const FileUploader = ({
|
|||||||
const fileUploadConfig = useMemo(() => fileUploadConfigResponse ?? {
|
const fileUploadConfig = useMemo(() => fileUploadConfigResponse ?? {
|
||||||
file_size_limit: 15,
|
file_size_limit: 15,
|
||||||
batch_count_limit: 5,
|
batch_count_limit: 5,
|
||||||
|
file_upload_limit: 5,
|
||||||
}, [fileUploadConfigResponse])
|
}, [fileUploadConfigResponse])
|
||||||
|
|
||||||
const fileListRef = useRef<FileItem[]>([])
|
const fileListRef = useRef<FileItem[]>([])
|
||||||
@ -121,10 +120,10 @@ const FileUploader = ({
|
|||||||
data: formData,
|
data: formData,
|
||||||
onprogress: onProgress,
|
onprogress: onProgress,
|
||||||
}, false, undefined, '?source=datasets')
|
}, false, undefined, '?source=datasets')
|
||||||
.then((res: File) => {
|
.then((res) => {
|
||||||
const completeFile = {
|
const completeFile = {
|
||||||
fileID: fileItem.fileID,
|
fileID: fileItem.fileID,
|
||||||
file: res,
|
file: res as unknown as File,
|
||||||
progress: -1,
|
progress: -1,
|
||||||
}
|
}
|
||||||
const index = fileListRef.current.findIndex(item => item.fileID === fileItem.fileID)
|
const index = fileListRef.current.findIndex(item => item.fileID === fileItem.fileID)
|
||||||
@ -163,11 +162,12 @@ const FileUploader = ({
|
|||||||
}, [fileUploadConfig, uploadBatchFiles])
|
}, [fileUploadConfig, uploadBatchFiles])
|
||||||
|
|
||||||
const initialUpload = useCallback((files: File[]) => {
|
const initialUpload = useCallback((files: File[]) => {
|
||||||
|
const filesCountLimit = fileUploadConfig.file_upload_limit
|
||||||
if (!files.length)
|
if (!files.length)
|
||||||
return false
|
return false
|
||||||
|
|
||||||
if (files.length + fileList.length > FILES_NUMBER_LIMIT && !IS_CE_EDITION) {
|
if (files.length + fileList.length > filesCountLimit && !IS_CE_EDITION) {
|
||||||
notify({ type: 'error', message: t('datasetCreation.stepOne.uploader.validation.filesNumber', { filesNumber: FILES_NUMBER_LIMIT }) })
|
notify({ type: 'error', message: t('datasetCreation.stepOne.uploader.validation.filesNumber', { filesNumber: filesCountLimit }) })
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -255,10 +255,11 @@ const FileUploader = ({
|
|||||||
)
|
)
|
||||||
let files = nested.flat()
|
let files = nested.flat()
|
||||||
if (notSupportBatchUpload) files = files.slice(0, 1)
|
if (notSupportBatchUpload) files = files.slice(0, 1)
|
||||||
|
files = files.slice(0, fileUploadConfig.batch_count_limit)
|
||||||
const valid = files.filter(isValid)
|
const valid = files.filter(isValid)
|
||||||
initialUpload(valid)
|
initialUpload(valid)
|
||||||
},
|
},
|
||||||
[initialUpload, isValid, notSupportBatchUpload, traverseFileEntry],
|
[initialUpload, isValid, notSupportBatchUpload, traverseFileEntry, fileUploadConfig],
|
||||||
)
|
)
|
||||||
const selectHandle = () => {
|
const selectHandle = () => {
|
||||||
if (fileUploader.current)
|
if (fileUploader.current)
|
||||||
@ -273,7 +274,8 @@ const FileUploader = ({
|
|||||||
onFileListUpdate?.([...fileListRef.current])
|
onFileListUpdate?.([...fileListRef.current])
|
||||||
}
|
}
|
||||||
const fileChangeHandle = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
const fileChangeHandle = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const files = [...(e.target.files ?? [])] as File[]
|
let files = [...(e.target.files ?? [])] as File[]
|
||||||
|
files = files.slice(0, fileUploadConfig.batch_count_limit)
|
||||||
initialUpload(files.filter(isValid))
|
initialUpload(files.filter(isValid))
|
||||||
}, [isValid, initialUpload])
|
}, [isValid, initialUpload])
|
||||||
|
|
||||||
@ -325,6 +327,7 @@ const FileUploader = ({
|
|||||||
size: fileUploadConfig.file_size_limit,
|
size: fileUploadConfig.file_size_limit,
|
||||||
supportTypes: supportTypesShowNames,
|
supportTypes: supportTypesShowNames,
|
||||||
batchCount: fileUploadConfig.batch_count_limit,
|
batchCount: fileUploadConfig.batch_count_limit,
|
||||||
|
totalCount: fileUploadConfig.file_upload_limit,
|
||||||
})}</div>
|
})}</div>
|
||||||
{dragging && <div ref={dragRef} className='absolute left-0 top-0 h-full w-full' />}
|
{dragging && <div ref={dragRef} className='absolute left-0 top-0 h-full w-full' />}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -6,6 +6,12 @@ import cn from '@/utils/classnames'
|
|||||||
import type { App } from '@/models/explore'
|
import type { App } from '@/models/explore'
|
||||||
import AppIcon from '@/app/components/base/app-icon'
|
import AppIcon from '@/app/components/base/app-icon'
|
||||||
import { AppTypeIcon } from '../../app/type-selector'
|
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'
|
||||||
|
|
||||||
export type AppCardProps = {
|
export type AppCardProps = {
|
||||||
app: App
|
app: App
|
||||||
canCreate: boolean
|
canCreate: boolean
|
||||||
@ -21,8 +27,17 @@ const AppCard = ({
|
|||||||
}: AppCardProps) => {
|
}: AppCardProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { app: appBasicInfo } = app
|
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 (
|
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: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:bg-components-panel-on-panel-item-bg-hover hover:shadow-lg')}>
|
||||||
<div className='flex h-[66px] shrink-0 grow-0 items-center gap-3 px-[14px] pb-3 pt-[14px]'>
|
<div className='flex h-[66px] shrink-0 grow-0 items-center gap-3 px-[14px] pb-3 pt-[14px]'>
|
||||||
<div className='relative shrink-0'>
|
<div className='relative shrink-0'>
|
||||||
<AppIcon
|
<AppIcon
|
||||||
@ -53,14 +68,23 @@ const AppCard = ({
|
|||||||
{app.description}
|
{app.description}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{isExplore && canCreate && (
|
{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 group-hover:flex')}>
|
<div className={cn(
|
||||||
<div className={cn('flex h-8 w-full items-center space-x-2')}>
|
'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',
|
||||||
<Button variant='primary' className='h-7 grow' onClick={() => onCreate()}>
|
(canCreate && isTrialApp) && 'grid-cols-2 gap-2 group-hover:grid ',
|
||||||
|
)}>
|
||||||
|
{canCreate && (
|
||||||
|
<Button variant='primary' className='h-7' onClick={() => onCreate()}>
|
||||||
<PlusIcon className='mr-1 h-4 w-4' />
|
<PlusIcon className='mr-1 h-4 w-4' />
|
||||||
<span className='text-xs'>{t('explore.appCard.addToWorkspace')}</span>
|
<span className='text-xs'>{t('explore.appCard.addToWorkspace')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
)}
|
||||||
|
{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>
|
</div>
|
||||||
|
|||||||
@ -22,6 +22,11 @@ import {
|
|||||||
} from '@/models/app'
|
} from '@/models/app'
|
||||||
import { useImportDSL } from '@/hooks/use-import-dsl'
|
import { useImportDSL } from '@/hooks/use-import-dsl'
|
||||||
import DSLConfirmModal from '@/app/components/app/create-from-dsl-modal/dsl-confirm-modal'
|
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 = {
|
type AppsProps = {
|
||||||
onSuccess?: () => void
|
onSuccess?: () => void
|
||||||
@ -36,12 +41,19 @@ const Apps = ({
|
|||||||
onSuccess,
|
onSuccess,
|
||||||
}: AppsProps) => {
|
}: AppsProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { systemFeatures } = useGlobalPublicStore()
|
||||||
const { hasEditPermission } = useContext(ExploreContext)
|
const { hasEditPermission } = useContext(ExploreContext)
|
||||||
const allCategoriesEn = t('explore.apps.allCategories', { lng: 'en' })
|
const allCategoriesEn = t('explore.apps.allCategories', { lng: 'en' })
|
||||||
|
|
||||||
const [keywords, setKeywords] = useState('')
|
const [keywords, setKeywords] = useState('')
|
||||||
const [searchKeywords, setSearchKeywords] = useState('')
|
const [searchKeywords, setSearchKeywords] = useState('')
|
||||||
|
|
||||||
|
const hasFilterCondition = !!keywords
|
||||||
|
const handleResetFilter = useCallback(() => {
|
||||||
|
setKeywords('')
|
||||||
|
setSearchKeywords('')
|
||||||
|
}, [])
|
||||||
|
|
||||||
const { run: handleSearch } = useDebounceFn(() => {
|
const { run: handleSearch } = useDebounceFn(() => {
|
||||||
setSearchKeywords(keywords)
|
setSearchKeywords(keywords)
|
||||||
}, { wait: 500 })
|
}, { wait: 500 })
|
||||||
@ -96,6 +108,18 @@ const Apps = ({
|
|||||||
isFetching,
|
isFetching,
|
||||||
} = useImportDSL()
|
} = useImportDSL()
|
||||||
const [showDSLConfirmModal, setShowDSLConfirmModal] = useState(false)
|
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 ({
|
const onCreate: CreateAppModalProps['onConfirm'] = async ({
|
||||||
name,
|
name,
|
||||||
icon_type,
|
icon_type,
|
||||||
@ -103,6 +127,8 @@ const Apps = ({
|
|||||||
icon_background,
|
icon_background,
|
||||||
description,
|
description,
|
||||||
}) => {
|
}) => {
|
||||||
|
hideTryAppPanel()
|
||||||
|
|
||||||
const { export_data } = await fetchAppDetail(
|
const { export_data } = await fetchAppDetail(
|
||||||
currApp?.app.id as string,
|
currApp?.app.id as string,
|
||||||
)
|
)
|
||||||
@ -141,23 +167,25 @@ const Apps = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
'flex h-full flex-col border-l-[0.5px] border-divider-regular',
|
'flex h-full flex-col',
|
||||||
)}>
|
)}>
|
||||||
|
{systemFeatures.enable_explore_banner && (
|
||||||
<div className='shrink-0 px-12 pt-6'>
|
<div className='mt-4 px-12'>
|
||||||
<div className={`mb-1 ${s.textGradient} text-xl font-semibold`}>{t('explore.apps.title')}</div>
|
<Banner />
|
||||||
<div className='text-sm text-text-tertiary'>{t('explore.apps.description')}</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
'mt-6 flex items-center justify-between px-12',
|
'mt-6 flex items-center justify-between px-12',
|
||||||
)}>
|
)}>
|
||||||
<Category
|
<div className='flex items-center'>
|
||||||
list={categories}
|
<div className={'system-xl-semibold grow truncate text-text-primary'}>{!hasFilterCondition ? t('explore.apps.title') : t('explore.apps.resultNum', { num: searchFilteredList.length })}</div>
|
||||||
value={currCategory}
|
{hasFilterCondition && (
|
||||||
onChange={setCurrCategory}
|
<>
|
||||||
allCategoriesEn={allCategoriesEn}
|
<div className='mx-3 h-4 w-px bg-divider-regular'></div>
|
||||||
/>
|
<Button size='medium' onClick={handleResetFilter}>{t('explore.apps.resetFilter')}</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<Input
|
<Input
|
||||||
showLeftIcon
|
showLeftIcon
|
||||||
showClearIcon
|
showClearIcon
|
||||||
@ -168,6 +196,15 @@ const Apps = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-2 px-12'>
|
||||||
|
<Category
|
||||||
|
list={categories}
|
||||||
|
value={currCategory}
|
||||||
|
onChange={setCurrCategory}
|
||||||
|
allCategoriesEn={allCategoriesEn}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
'relative mt-4 flex flex-1 shrink-0 grow flex-col overflow-auto pb-6',
|
'relative mt-4 flex flex-1 shrink-0 grow flex-col overflow-auto pb-6',
|
||||||
)}>
|
)}>
|
||||||
@ -214,6 +251,14 @@ const Apps = ({
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{isShowTryAppPanel && (
|
||||||
|
<TryApp appId={appParams?.appId || ''}
|
||||||
|
category={appParams?.app?.category}
|
||||||
|
onClose={hideTryAppPanel}
|
||||||
|
onCreate={handleShowFromTryApp}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
196
web/app/components/explore/banner/banner-item.tsx
Normal file
196
web/app/components/explore/banner/banner-item.tsx
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
93
web/app/components/explore/banner/banner.tsx
Normal file
93
web/app/components/explore/banner/banner.tsx
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
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)
|
||||||
111
web/app/components/explore/banner/indicator-button.tsx
Normal file
111
web/app/components/explore/banner/indicator-button.tsx
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -5,7 +5,6 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
import exploreI18n from '@/i18n/en-US/explore'
|
import exploreI18n from '@/i18n/en-US/explore'
|
||||||
import type { AppCategory } from '@/models/explore'
|
import type { AppCategory } from '@/models/explore'
|
||||||
import { ThumbsUp } from '@/app/components/base/icons/src/vender/line/alertsAndFeedback'
|
|
||||||
|
|
||||||
const categoryI18n = exploreI18n.category
|
const categoryI18n = exploreI18n.category
|
||||||
|
|
||||||
@ -31,7 +30,7 @@ const Category: FC<ICategoryProps> = ({
|
|||||||
const isAllCategories = !list.includes(value as AppCategory) || value === allCategoriesEn
|
const isAllCategories = !list.includes(value as AppCategory) || value === allCategoriesEn
|
||||||
|
|
||||||
const itemClassName = (isSelected: boolean) => cn(
|
const itemClassName = (isSelected: boolean) => cn(
|
||||||
'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',
|
'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',
|
||||||
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',
|
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',
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -41,7 +40,6 @@ const Category: FC<ICategoryProps> = ({
|
|||||||
className={itemClassName(isAllCategories)}
|
className={itemClassName(isAllCategories)}
|
||||||
onClick={() => onChange(allCategoriesEn)}
|
onClick={() => onChange(allCategoriesEn)}
|
||||||
>
|
>
|
||||||
<ThumbsUp className='mr-1 h-3.5 w-3.5' />
|
|
||||||
{t('explore.apps.allCategories')}
|
{t('explore.apps.allCategories')}
|
||||||
</div>
|
</div>
|
||||||
{list.filter(name => name !== allCategoriesEn).map(name => (
|
{list.filter(name => name !== allCategoriesEn).map(name => (
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
|
import type { CurrentTryAppParams } from '@/context/explore-context'
|
||||||
import ExploreContext from '@/context/explore-context'
|
import ExploreContext from '@/context/explore-context'
|
||||||
import Sidebar from '@/app/components/explore/sidebar'
|
import Sidebar from '@/app/components/explore/sidebar'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
@ -42,6 +43,16 @@ const Explore: FC<IExploreProps> = ({
|
|||||||
return router.replace('/datasets')
|
return router.replace('/datasets')
|
||||||
}, [isCurrentWorkspaceDatasetOperator])
|
}, [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 (
|
return (
|
||||||
<div className='flex h-full overflow-hidden border-t border-divider-regular bg-background-body'>
|
<div className='flex h-full overflow-hidden border-t border-divider-regular bg-background-body'>
|
||||||
<ExploreContext.Provider
|
<ExploreContext.Provider
|
||||||
@ -54,6 +65,9 @@ const Explore: FC<IExploreProps> = ({
|
|||||||
setInstalledApps,
|
setInstalledApps,
|
||||||
isFetchingInstalledApps,
|
isFetchingInstalledApps,
|
||||||
setIsFetchingInstalledApps,
|
setIsFetchingInstalledApps,
|
||||||
|
currentApp: currentTryAppParams,
|
||||||
|
isShowTryAppPanel,
|
||||||
|
setShowTryAppPanel,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import AppUnavailable from '../../base/app-unavailable'
|
|||||||
import { useGetUserCanAccessApp } from '@/service/access-control'
|
import { useGetUserCanAccessApp } from '@/service/access-control'
|
||||||
import { useGetInstalledAppAccessModeByAppId, useGetInstalledAppMeta, useGetInstalledAppParams } from '@/service/use-explore'
|
import { useGetInstalledAppAccessModeByAppId, useGetInstalledAppMeta, useGetInstalledAppParams } from '@/service/use-explore'
|
||||||
import type { AppData } from '@/models/share'
|
import type { AppData } from '@/models/share'
|
||||||
|
import type { AccessMode } from '@/models/access-control'
|
||||||
|
|
||||||
export type IInstalledAppProps = {
|
export type IInstalledAppProps = {
|
||||||
id: string
|
id: string
|
||||||
@ -61,8 +62,8 @@ const InstalledApp: FC<IInstalledAppProps> = ({
|
|||||||
if (appMeta)
|
if (appMeta)
|
||||||
updateWebAppMeta(appMeta)
|
updateWebAppMeta(appMeta)
|
||||||
if (webAppAccessMode)
|
if (webAppAccessMode)
|
||||||
updateWebAppAccessMode(webAppAccessMode.accessMode)
|
updateWebAppAccessMode((webAppAccessMode as { accessMode: AccessMode }).accessMode)
|
||||||
updateUserCanAccessApp(Boolean(userCanAccessApp && userCanAccessApp?.result))
|
updateUserCanAccessApp(Boolean(userCanAccessApp && (userCanAccessApp as { result: boolean })?.result))
|
||||||
}, [installedApp, appMeta, appParams, updateAppInfo, updateAppParams, updateUserCanAccessApp, updateWebAppMeta, userCanAccessApp, webAppAccessMode, updateWebAppAccessMode])
|
}, [installedApp, appMeta, appParams, updateAppInfo, updateAppParams, updateUserCanAccessApp, updateWebAppMeta, userCanAccessApp, webAppAccessMode, updateWebAppAccessMode])
|
||||||
|
|
||||||
if (appParamsError) {
|
if (appParamsError) {
|
||||||
|
|||||||
@ -52,7 +52,7 @@ const ItemOperation: FC<IItemOperationProps> = ({
|
|||||||
<PortalToFollowElemTrigger
|
<PortalToFollowElemTrigger
|
||||||
onClick={() => setOpen(v => !v)}
|
onClick={() => setOpen(v => !v)}
|
||||||
>
|
>
|
||||||
<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>
|
<div className={cn(className, s.btn, 'h-6 w-6 rounded-md border-none py-1', (isItemHovering || open) && `${s.open}`)}></div>
|
||||||
</PortalToFollowElemTrigger>
|
</PortalToFollowElemTrigger>
|
||||||
<PortalToFollowElemContent
|
<PortalToFollowElemContent
|
||||||
className="z-50"
|
className="z-50"
|
||||||
|
|||||||
@ -57,7 +57,7 @@ export default function AppNavItem({
|
|||||||
<>
|
<>
|
||||||
<div className='flex w-0 grow items-center space-x-2'>
|
<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} />
|
<AppIcon size='tiny' iconType={icon_type} icon={icon} background={icon_background} imageUrl={icon_url} />
|
||||||
<div className='overflow-hidden text-ellipsis whitespace-nowrap' title={name}>{name}</div>
|
<div className='system-sm-regular truncate text-components-menu-item-text' title={name}>{name}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='h-6 shrink-0' onClick={e => e.stopPropagation()}>
|
<div className='h-6 shrink-0' onClick={e => e.stopPropagation()}>
|
||||||
<ItemOperation
|
<ItemOperation
|
||||||
|
|||||||
@ -13,18 +13,9 @@ import Confirm from '@/app/components/base/confirm'
|
|||||||
import Divider from '@/app/components/base/divider'
|
import Divider from '@/app/components/base/divider'
|
||||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||||
import { useGetInstalledApps, useUninstallApp, useUpdateAppPinStatus } from '@/service/use-explore'
|
import { useGetInstalledApps, useUninstallApp, useUpdateAppPinStatus } from '@/service/use-explore'
|
||||||
|
import { RiAppsFill, RiExpandRightLine, RiLayoutLeft2Line } from '@remixicon/react'
|
||||||
const SelectedDiscoveryIcon = () => (
|
import { useBoolean } from 'ahooks'
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="current" xmlns="http://www.w3.org/2000/svg">
|
import NoApps from './no-apps'
|
||||||
<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 = {
|
export type IExploreSideBarProps = {
|
||||||
controlUpdateInstalledApps: number
|
controlUpdateInstalledApps: number
|
||||||
@ -44,6 +35,9 @@ const SideBar: FC<IExploreSideBarProps> = ({
|
|||||||
|
|
||||||
const media = useBreakpoints()
|
const media = useBreakpoints()
|
||||||
const isMobile = media === MediaType.mobile
|
const isMobile = media === MediaType.mobile
|
||||||
|
const [isFold, {
|
||||||
|
toggle: toggleIsFold,
|
||||||
|
}] = useBoolean(false)
|
||||||
|
|
||||||
const [showConfirm, setShowConfirm] = useState(false)
|
const [showConfirm, setShowConfirm] = useState(false)
|
||||||
const [currId, setCurrId] = useState('')
|
const [currId, setCurrId] = useState('')
|
||||||
@ -83,22 +77,22 @@ const SideBar: FC<IExploreSideBarProps> = ({
|
|||||||
|
|
||||||
const pinnedAppsCount = installedApps.filter(({ is_pinned }) => is_pinned).length
|
const pinnedAppsCount = installedApps.filter(({ is_pinned }) => is_pinned).length
|
||||||
return (
|
return (
|
||||||
<div className='w-fit shrink-0 cursor-pointer border-r border-divider-burn px-4 pt-6 sm:w-[216px]'>
|
<div className={cn('relative w-fit shrink-0 cursor-pointer px-3 pt-6 sm:w-[240px]', isFold && 'sm:w-[56px]')}>
|
||||||
<div className={cn(isDiscoverySelected ? 'text-text-accent' : 'text-text-tertiary')}>
|
<Link
|
||||||
<Link
|
href='/explore/apps'
|
||||||
href='/explore/apps'
|
className={cn(isDiscoverySelected ? 'bg-state-base-active' : 'hover:bg-state-base-hover',
|
||||||
className={cn(isDiscoverySelected ? ' bg-components-main-nav-nav-button-bg-active' : 'font-medium 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')}
|
||||||
'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)' } : {}}
|
<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' />
|
||||||
{isDiscoverySelected ? <SelectedDiscoveryIcon /> : <DiscoveryIcon />}
|
</div>
|
||||||
{!isMobile && <div className='text-sm'>{t('explore.sidebar.discovery')}</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>
|
</Link>
|
||||||
</div>
|
|
||||||
{installedApps.length > 0 && (
|
{installedApps.length > 0 && (
|
||||||
<div className='mt-10'>
|
<div className='mt-5'>
|
||||||
<p className='break-all pl-2 text-xs font-medium uppercase text-text-tertiary mobile:px-0'>{t('explore.sidebar.workspace')}</p>
|
{!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='mt-3 space-y-1 overflow-y-auto overflow-x-hidden'
|
{installedApps.length === 0 && !isMobile && !isFold && <NoApps />}
|
||||||
|
<div className='space-y-0.5 overflow-y-auto overflow-x-hidden'
|
||||||
style={{
|
style={{
|
||||||
height: 'calc(100vh - 250px)',
|
height: 'calc(100vh - 250px)',
|
||||||
}}
|
}}
|
||||||
@ -106,7 +100,7 @@ const SideBar: FC<IExploreSideBarProps> = ({
|
|||||||
{installedApps.map(({ id, is_pinned, uninstallable, app: { name, icon_type, icon, icon_url, icon_background } }, index) => (
|
{installedApps.map(({ id, is_pinned, uninstallable, app: { name, icon_type, icon, icon_url, icon_background } }, index) => (
|
||||||
<React.Fragment key={id}>
|
<React.Fragment key={id}>
|
||||||
<Item
|
<Item
|
||||||
isMobile={isMobile}
|
isMobile={isMobile || isFold}
|
||||||
name={name}
|
name={name}
|
||||||
icon_type={icon_type}
|
icon_type={icon_type}
|
||||||
icon={icon}
|
icon={icon}
|
||||||
@ -128,6 +122,15 @@ const SideBar: FC<IExploreSideBarProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</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 && (
|
{showConfirm && (
|
||||||
<Confirm
|
<Confirm
|
||||||
title={t('explore.sidebar.delete.title')}
|
title={t('explore.sidebar.delete.title')}
|
||||||
|
|||||||
24
web/app/components/explore/sidebar/no-apps/index.tsx
Normal file
24
web/app/components/explore/sidebar/no-apps/index.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
'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)
|
||||||
BIN
web/app/components/explore/sidebar/no-apps/no-web-apps-dark.png
Normal file
BIN
web/app/components/explore/sidebar/no-apps/no-web-apps-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
BIN
web/app/components/explore/sidebar/no-apps/no-web-apps-light.png
Normal file
BIN
web/app/components/explore/sidebar/no-apps/no-web-apps-light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
@ -0,0 +1,7 @@
|
|||||||
|
.light {
|
||||||
|
background-image: url('./no-web-apps-light.png');
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
background-image: url('./no-web-apps-dark.png');
|
||||||
|
}
|
||||||
92
web/app/components/explore/try-app/app-info/index.tsx
Normal file
92
web/app/components/explore/try-app/app-info/index.tsx
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
'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)
|
||||||
@ -0,0 +1,78 @@
|
|||||||
|
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
|
||||||
70
web/app/components/explore/try-app/app/chat.tsx
Normal file
70
web/app/components/explore/try-app/app/chat.tsx
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
'use client'
|
||||||
|
import type { FC } from 'react'
|
||||||
|
import React 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'
|
||||||
|
|
||||||
|
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 chatData = useEmbeddedChatbot(AppSourceType.tryApp, appId)
|
||||||
|
const [isHideTryNotice, {
|
||||||
|
setTrue: hideTryNotice,
|
||||||
|
}] = useBoolean(false)
|
||||||
|
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>
|
||||||
|
<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)
|
||||||
44
web/app/components/explore/try-app/app/index.tsx
Normal file
44
web/app/components/explore/try-app/app/index.tsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
'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)
|
||||||
252
web/app/components/explore/try-app/app/text-generation.tsx
Normal file
252
web/app/components/explore/try-app/app/text-generation.tsx
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
'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)
|
||||||
70
web/app/components/explore/try-app/index.tsx
Normal file
70
web/app/components/explore/try-app/index.tsx
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
'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)
|
||||||
361
web/app/components/explore/try-app/preview/basic-app-preview.tsx
Normal file
361
web/app/components/explore/try-app/preview/basic-app-preview.tsx
Normal file
@ -0,0 +1,361 @@
|
|||||||
|
'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)
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
'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)
|
||||||
23
web/app/components/explore/try-app/preview/index.tsx
Normal file
23
web/app/components/explore/try-app/preview/index.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
'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)
|
||||||
37
web/app/components/explore/try-app/tab.tsx
Normal file
37
web/app/components/explore/try-app/tab.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
'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)
|
||||||
@ -14,7 +14,7 @@ import RunBatch from './run-batch'
|
|||||||
import ResDownload from './run-batch/res-download'
|
import ResDownload from './run-batch/res-download'
|
||||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||||
import RunOnce from '@/app/components/share/text-generation/run-once'
|
import RunOnce from '@/app/components/share/text-generation/run-once'
|
||||||
import { fetchSavedMessage as doFetchSavedMessage, removeMessage, saveMessage } from '@/service/share'
|
import { AppSourceType, fetchSavedMessage as doFetchSavedMessage, removeMessage, saveMessage } from '@/service/share'
|
||||||
import type { SiteInfo } from '@/models/share'
|
import type { SiteInfo } from '@/models/share'
|
||||||
import type {
|
import type {
|
||||||
MoreLikeThisConfig,
|
MoreLikeThisConfig,
|
||||||
@ -41,24 +41,9 @@ import { AccessMode } from '@/models/access-control'
|
|||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||||
import useDocumentTitle from '@/hooks/use-document-title'
|
import useDocumentTitle from '@/hooks/use-document-title'
|
||||||
import { useWebAppStore } from '@/context/web-app-context'
|
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.
|
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 = {
|
export type IMainProps = {
|
||||||
isInstalledApp?: boolean
|
isInstalledApp?: boolean
|
||||||
@ -72,6 +57,7 @@ const TextGeneration: FC<IMainProps> = ({
|
|||||||
isWorkflow = false,
|
isWorkflow = false,
|
||||||
}) => {
|
}) => {
|
||||||
const { notify } = Toast
|
const { notify } = Toast
|
||||||
|
const appSourceType = isInstalledApp ? AppSourceType.installedApp : AppSourceType.webApp
|
||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const media = useBreakpoints()
|
const media = useBreakpoints()
|
||||||
@ -101,16 +87,16 @@ const TextGeneration: FC<IMainProps> = ({
|
|||||||
// save message
|
// save message
|
||||||
const [savedMessages, setSavedMessages] = useState<SavedMessage[]>([])
|
const [savedMessages, setSavedMessages] = useState<SavedMessage[]>([])
|
||||||
const fetchSavedMessage = useCallback(async () => {
|
const fetchSavedMessage = useCallback(async () => {
|
||||||
const res: any = await doFetchSavedMessage(isInstalledApp, appId)
|
const res: any = await doFetchSavedMessage(appSourceType, appId)
|
||||||
setSavedMessages(res.data)
|
setSavedMessages(res.data)
|
||||||
}, [isInstalledApp, appId])
|
}, [appSourceType, appId])
|
||||||
const handleSaveMessage = async (messageId: string) => {
|
const handleSaveMessage = async (messageId: string) => {
|
||||||
await saveMessage(messageId, isInstalledApp, appId)
|
await saveMessage(messageId, appSourceType, appId)
|
||||||
notify({ type: 'success', message: t('common.api.saved') })
|
notify({ type: 'success', message: t('common.api.saved') })
|
||||||
fetchSavedMessage()
|
fetchSavedMessage()
|
||||||
}
|
}
|
||||||
const handleRemoveSavedMessage = async (messageId: string) => {
|
const handleRemoveSavedMessage = async (messageId: string) => {
|
||||||
await removeMessage(messageId, isInstalledApp, appId)
|
await removeMessage(messageId, appSourceType, appId)
|
||||||
notify({ type: 'success', message: t('common.api.remove') })
|
notify({ type: 'success', message: t('common.api.remove') })
|
||||||
fetchSavedMessage()
|
fetchSavedMessage()
|
||||||
}
|
}
|
||||||
@ -416,8 +402,8 @@ const TextGeneration: FC<IMainProps> = ({
|
|||||||
isCallBatchAPI={isCallBatchAPI}
|
isCallBatchAPI={isCallBatchAPI}
|
||||||
isPC={isPC}
|
isPC={isPC}
|
||||||
isMobile={!isPC}
|
isMobile={!isPC}
|
||||||
isInstalledApp={isInstalledApp}
|
appSourceType={isInstalledApp ? AppSourceType.installedApp : AppSourceType.webApp}
|
||||||
installedAppInfo={installedAppInfo}
|
appId={installedAppInfo?.id}
|
||||||
isError={task?.status === TaskStatus.failed}
|
isError={task?.status === TaskStatus.failed}
|
||||||
promptConfig={promptConfig}
|
promptConfig={promptConfig}
|
||||||
moreLikeThisEnabled={!!moreLikeThisConfig?.enabled}
|
moreLikeThisEnabled={!!moreLikeThisConfig?.enabled}
|
||||||
|
|||||||
@ -7,11 +7,11 @@ import { produce } from 'immer'
|
|||||||
import TextGenerationRes from '@/app/components/app/text-generate/item'
|
import TextGenerationRes from '@/app/components/app/text-generate/item'
|
||||||
import NoData from '@/app/components/share/text-generation/no-data'
|
import NoData from '@/app/components/share/text-generation/no-data'
|
||||||
import Toast from '@/app/components/base/toast'
|
import Toast from '@/app/components/base/toast'
|
||||||
|
import type { AppSourceType } from '@/service/share'
|
||||||
import { sendCompletionMessage, sendWorkflowMessage, updateFeedback } from '@/service/share'
|
import { sendCompletionMessage, sendWorkflowMessage, updateFeedback } from '@/service/share'
|
||||||
import type { FeedbackType } from '@/app/components/base/chat/chat/type'
|
import type { FeedbackType } from '@/app/components/base/chat/chat/type'
|
||||||
import Loading from '@/app/components/base/loading'
|
import Loading from '@/app/components/base/loading'
|
||||||
import type { PromptConfig } from '@/models/debug'
|
import type { PromptConfig } from '@/models/debug'
|
||||||
import type { InstalledApp } from '@/models/explore'
|
|
||||||
import { TransferMethod, type VisionFile, type VisionSettings } from '@/types/app'
|
import { TransferMethod, type VisionFile, type VisionSettings } from '@/types/app'
|
||||||
import { NodeRunningStatus, WorkflowRunningStatus } from '@/app/components/workflow/types'
|
import { NodeRunningStatus, WorkflowRunningStatus } from '@/app/components/workflow/types'
|
||||||
import type { WorkflowProcess } from '@/app/components/base/chat/types'
|
import type { WorkflowProcess } from '@/app/components/base/chat/types'
|
||||||
@ -30,8 +30,8 @@ export type IResultProps = {
|
|||||||
isCallBatchAPI: boolean
|
isCallBatchAPI: boolean
|
||||||
isPC: boolean
|
isPC: boolean
|
||||||
isMobile: boolean
|
isMobile: boolean
|
||||||
isInstalledApp: boolean
|
appSourceType: AppSourceType
|
||||||
installedAppInfo?: InstalledApp
|
appId?: string
|
||||||
isError: boolean
|
isError: boolean
|
||||||
isShowTextToSpeech: boolean
|
isShowTextToSpeech: boolean
|
||||||
promptConfig: PromptConfig | null
|
promptConfig: PromptConfig | null
|
||||||
@ -55,8 +55,8 @@ const Result: FC<IResultProps> = ({
|
|||||||
isCallBatchAPI,
|
isCallBatchAPI,
|
||||||
isPC,
|
isPC,
|
||||||
isMobile,
|
isMobile,
|
||||||
isInstalledApp,
|
appSourceType,
|
||||||
installedAppInfo,
|
appId,
|
||||||
isError,
|
isError,
|
||||||
isShowTextToSpeech,
|
isShowTextToSpeech,
|
||||||
promptConfig,
|
promptConfig,
|
||||||
@ -104,7 +104,7 @@ const Result: FC<IResultProps> = ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const handleFeedback = async (feedback: FeedbackType) => {
|
const handleFeedback = async (feedback: FeedbackType) => {
|
||||||
await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating, content: feedback.content } }, isInstalledApp, installedAppInfo?.id)
|
await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating, content: feedback.content } }, appSourceType, appId)
|
||||||
setFeedback(feedback)
|
setFeedback(feedback)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -374,8 +374,8 @@ const Result: FC<IResultProps> = ({
|
|||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
isInstalledApp,
|
appSourceType,
|
||||||
installedAppInfo?.id,
|
appId,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@ -408,7 +408,7 @@ const Result: FC<IResultProps> = ({
|
|||||||
onCompleted(getCompletionRes(), taskId, false)
|
onCompleted(getCompletionRes(), taskId, false)
|
||||||
isEnd = true
|
isEnd = true
|
||||||
},
|
},
|
||||||
}, isInstalledApp, installedAppInfo?.id)
|
}, appSourceType, appId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -439,8 +439,8 @@ const Result: FC<IResultProps> = ({
|
|||||||
feedback={feedback}
|
feedback={feedback}
|
||||||
onSave={handleSaveMessage}
|
onSave={handleSaveMessage}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
isInstalledApp={isInstalledApp}
|
appSourceType={appSourceType}
|
||||||
installedAppId={installedAppInfo?.id}
|
installedAppId={appId}
|
||||||
isLoading={isCallBatchAPI ? (!completionRes && isResponding) : false}
|
isLoading={isCallBatchAPI ? (!completionRes && isResponding) : false}
|
||||||
taskId={isCallBatchAPI ? ((taskId as number) < 10 ? `0${taskId}` : `${taskId}`) : undefined}
|
taskId={isCallBatchAPI ? ((taskId as number) < 10 ? `0${taskId}` : `${taskId}`) : undefined}
|
||||||
controlClearMoreLikeThis={controlClearMoreLikeThis}
|
controlClearMoreLikeThis={controlClearMoreLikeThis}
|
||||||
|
|||||||
16
web/app/components/share/text-generation/types.ts
Normal file
16
web/app/components/share/text-generation/types.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
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',
|
||||||
|
}
|
||||||
@ -4,6 +4,7 @@ import {
|
|||||||
import { produce } from 'immer'
|
import { produce } from 'immer'
|
||||||
import {
|
import {
|
||||||
useReactFlow,
|
useReactFlow,
|
||||||
|
useStoreApi,
|
||||||
useViewport,
|
useViewport,
|
||||||
} from 'reactflow'
|
} from 'reactflow'
|
||||||
import { useEventListener } from 'ahooks'
|
import { useEventListener } from 'ahooks'
|
||||||
@ -12,15 +13,15 @@ import {
|
|||||||
useWorkflowStore,
|
useWorkflowStore,
|
||||||
} from './store'
|
} from './store'
|
||||||
import { WorkflowHistoryEvent, useNodesInteractions, useWorkflowHistory } from './hooks'
|
import { WorkflowHistoryEvent, useNodesInteractions, useWorkflowHistory } from './hooks'
|
||||||
import { CUSTOM_NODE } from './constants'
|
import { CUSTOM_NODE, ITERATION_PADDING } from './constants'
|
||||||
import { getIterationStartNode, getLoopStartNode } from './utils'
|
import { getIterationStartNode, getLoopStartNode } from './utils'
|
||||||
import CustomNode from './nodes'
|
import CustomNode from './nodes'
|
||||||
import CustomNoteNode from './note-node'
|
import CustomNoteNode from './note-node'
|
||||||
import { CUSTOM_NOTE_NODE } from './note-node/constants'
|
import { CUSTOM_NOTE_NODE } from './note-node/constants'
|
||||||
import { BlockEnum } from './types'
|
import { BlockEnum } from './types'
|
||||||
import { useCollaborativeWorkflow } from '@/app/components/workflow/hooks/use-collaborative-workflow'
|
|
||||||
|
|
||||||
const CandidateNode = () => {
|
const CandidateNode = () => {
|
||||||
|
const store = useStoreApi()
|
||||||
const reactflow = useReactFlow()
|
const reactflow = useReactFlow()
|
||||||
const workflowStore = useWorkflowStore()
|
const workflowStore = useWorkflowStore()
|
||||||
const candidateNode = useStore(s => s.candidateNode)
|
const candidateNode = useStore(s => s.candidateNode)
|
||||||
@ -28,16 +29,45 @@ const CandidateNode = () => {
|
|||||||
const { zoom } = useViewport()
|
const { zoom } = useViewport()
|
||||||
const { handleNodeSelect } = useNodesInteractions()
|
const { handleNodeSelect } = useNodesInteractions()
|
||||||
const { saveStateToHistory } = useWorkflowHistory()
|
const { saveStateToHistory } = useWorkflowHistory()
|
||||||
const collaborativeWorkflow = useCollaborativeWorkflow()
|
|
||||||
|
|
||||||
useEventListener('click', (e) => {
|
useEventListener('click', (e) => {
|
||||||
const { candidateNode, mousePosition } = workflowStore.getState()
|
const { candidateNode, mousePosition } = workflowStore.getState()
|
||||||
|
|
||||||
if (candidateNode) {
|
if (candidateNode) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const { nodes, setNodes } = collaborativeWorkflow.getState()
|
const {
|
||||||
|
getNodes,
|
||||||
|
setNodes,
|
||||||
|
} = store.getState()
|
||||||
const { screenToFlowPosition } = reactflow
|
const { screenToFlowPosition } = reactflow
|
||||||
const { x, y } = screenToFlowPosition({ x: mousePosition.pageX, y: mousePosition.pageY })
|
const nodes = getNodes()
|
||||||
|
// Get mouse position in flow coordinates (this is where the top-left corner should be)
|
||||||
|
let { x, y } = screenToFlowPosition({ x: mousePosition.pageX, y: mousePosition.pageY })
|
||||||
|
|
||||||
|
// If the node has a parent (e.g., inside iteration), apply constraints and convert to relative position
|
||||||
|
if (candidateNode.parentId) {
|
||||||
|
const parentNode = nodes.find(node => node.id === candidateNode.parentId)
|
||||||
|
if (parentNode && parentNode.position) {
|
||||||
|
// Apply boundary constraints for iteration nodes
|
||||||
|
if (candidateNode.data.isInIteration) {
|
||||||
|
const nodeWidth = candidateNode.width || 0
|
||||||
|
const nodeHeight = candidateNode.height || 0
|
||||||
|
const minX = parentNode.position.x + ITERATION_PADDING.left
|
||||||
|
const maxX = parentNode.position.x + (parentNode.width || 0) - ITERATION_PADDING.right - nodeWidth
|
||||||
|
const minY = parentNode.position.y + ITERATION_PADDING.top
|
||||||
|
const maxY = parentNode.position.y + (parentNode.height || 0) - ITERATION_PADDING.bottom - nodeHeight
|
||||||
|
|
||||||
|
// Constrain position
|
||||||
|
x = Math.max(minX, Math.min(maxX, x))
|
||||||
|
y = Math.max(minY, Math.min(maxY, y))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to relative position
|
||||||
|
x = x - parentNode.position.x
|
||||||
|
y = y - parentNode.position.y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const newNodes = produce(nodes, (draft) => {
|
const newNodes = produce(nodes, (draft) => {
|
||||||
draft.push({
|
draft.push({
|
||||||
...candidateNode,
|
...candidateNode,
|
||||||
@ -55,6 +85,20 @@ const CandidateNode = () => {
|
|||||||
|
|
||||||
if (candidateNode.data.type === BlockEnum.Loop)
|
if (candidateNode.data.type === BlockEnum.Loop)
|
||||||
draft.push(getLoopStartNode(candidateNode.id))
|
draft.push(getLoopStartNode(candidateNode.id))
|
||||||
|
|
||||||
|
// Update parent iteration node's _children array
|
||||||
|
if (candidateNode.parentId && candidateNode.data.isInIteration) {
|
||||||
|
const parentNode = draft.find(node => node.id === candidateNode.parentId)
|
||||||
|
if (parentNode && parentNode.data.type === BlockEnum.Iteration) {
|
||||||
|
if (!parentNode.data._children)
|
||||||
|
parentNode.data._children = []
|
||||||
|
|
||||||
|
parentNode.data._children.push({
|
||||||
|
nodeId: candidateNode.id,
|
||||||
|
nodeType: candidateNode.data.type,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
setNodes(newNodes)
|
setNodes(newNodes)
|
||||||
if (candidateNode.type === CUSTOM_NOTE_NODE)
|
if (candidateNode.type === CUSTOM_NOTE_NODE)
|
||||||
@ -80,6 +124,34 @@ const CandidateNode = () => {
|
|||||||
if (!candidateNode)
|
if (!candidateNode)
|
||||||
return null
|
return null
|
||||||
|
|
||||||
|
// Apply boundary constraints if node is inside iteration
|
||||||
|
if (candidateNode.parentId && candidateNode.data.isInIteration) {
|
||||||
|
const { getNodes } = store.getState()
|
||||||
|
const nodes = getNodes()
|
||||||
|
const parentNode = nodes.find(node => node.id === candidateNode.parentId)
|
||||||
|
|
||||||
|
if (parentNode && parentNode.position) {
|
||||||
|
const { screenToFlowPosition, flowToScreenPosition } = reactflow
|
||||||
|
// Get mouse position in flow coordinates
|
||||||
|
const flowPosition = screenToFlowPosition({ x: mousePosition.pageX, y: mousePosition.pageY })
|
||||||
|
|
||||||
|
// Calculate boundaries in flow coordinates
|
||||||
|
const nodeWidth = candidateNode.width || 0
|
||||||
|
const nodeHeight = candidateNode.height || 0
|
||||||
|
const minX = parentNode.position.x + ITERATION_PADDING.left
|
||||||
|
const maxX = parentNode.position.x + (parentNode.width || 0) - ITERATION_PADDING.right - nodeWidth
|
||||||
|
const minY = parentNode.position.y + ITERATION_PADDING.top
|
||||||
|
const maxY = parentNode.position.y + (parentNode.height || 0) - ITERATION_PADDING.bottom - nodeHeight
|
||||||
|
|
||||||
|
// Constrain position
|
||||||
|
const constrainedX = Math.max(minX, Math.min(maxX, flowPosition.x))
|
||||||
|
const constrainedY = Math.max(minY, Math.min(maxY, flowPosition.y))
|
||||||
|
|
||||||
|
// Convert back to screen coordinates
|
||||||
|
flowToScreenPosition({ x: constrainedX, y: constrainedY })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className='absolute z-10'
|
className='absolute z-10'
|
||||||
|
|||||||
@ -125,12 +125,12 @@ export const useNodesInteractions = () => {
|
|||||||
|
|
||||||
const { restrictPosition } = handleNodeIterationChildDrag(node)
|
const { restrictPosition } = handleNodeIterationChildDrag(node)
|
||||||
const { restrictPosition: restrictLoopPosition }
|
const { restrictPosition: restrictLoopPosition }
|
||||||
= handleNodeLoopChildDrag(node)
|
= handleNodeLoopChildDrag(node)
|
||||||
|
|
||||||
const { showHorizontalHelpLineNodes, showVerticalHelpLineNodes }
|
const { showHorizontalHelpLineNodes, showVerticalHelpLineNodes }
|
||||||
= handleSetHelpline(node)
|
= handleSetHelpline(node)
|
||||||
const showHorizontalHelpLineNodesLength
|
const showHorizontalHelpLineNodesLength
|
||||||
= showHorizontalHelpLineNodes.length
|
= showHorizontalHelpLineNodes.length
|
||||||
const showVerticalHelpLineNodesLength = showVerticalHelpLineNodes.length
|
const showVerticalHelpLineNodesLength = showVerticalHelpLineNodes.length
|
||||||
|
|
||||||
const newNodes = produce(nodes, (draft) => {
|
const newNodes = produce(nodes, (draft) => {
|
||||||
@ -716,7 +716,7 @@ export const useNodesInteractions = () => {
|
|||||||
targetHandle = 'target',
|
targetHandle = 'target',
|
||||||
toolDefaultValue,
|
toolDefaultValue,
|
||||||
},
|
},
|
||||||
{ prevNodeId, prevNodeSourceHandle, nextNodeId, nextNodeTargetHandle },
|
{ prevNodeId, prevNodeSourceHandle, nextNodeId, nextNodeTargetHandle, skipAutoConnect },
|
||||||
) => {
|
) => {
|
||||||
if (getNodesReadOnly()) return
|
if (getNodesReadOnly()) return
|
||||||
|
|
||||||
@ -808,7 +808,7 @@ export const useNodesInteractions = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let newEdge = null
|
let newEdge = null
|
||||||
if (nodeType !== BlockEnum.DataSource) {
|
if (nodeType !== BlockEnum.DataSource && !skipAutoConnect) {
|
||||||
newEdge = {
|
newEdge = {
|
||||||
id: `${prevNodeId}-${prevNodeSourceHandle}-${newNode.id}-${targetHandle}`,
|
id: `${prevNodeId}-${prevNodeSourceHandle}-${newNode.id}-${targetHandle}`,
|
||||||
type: CUSTOM_EDGE,
|
type: CUSTOM_EDGE,
|
||||||
@ -948,6 +948,7 @@ export const useNodesInteractions = () => {
|
|||||||
nodeType !== BlockEnum.IfElse
|
nodeType !== BlockEnum.IfElse
|
||||||
&& nodeType !== BlockEnum.QuestionClassifier
|
&& nodeType !== BlockEnum.QuestionClassifier
|
||||||
&& nodeType !== BlockEnum.LoopEnd
|
&& nodeType !== BlockEnum.LoopEnd
|
||||||
|
&& !skipAutoConnect
|
||||||
) {
|
) {
|
||||||
newEdge = {
|
newEdge = {
|
||||||
id: `${newNode.id}-${sourceHandle}-${nextNodeId}-${nextNodeTargetHandle}`,
|
id: `${newNode.id}-${sourceHandle}-${nextNodeId}-${nextNodeTargetHandle}`,
|
||||||
@ -1097,7 +1098,7 @@ export const useNodesInteractions = () => {
|
|||||||
)
|
)
|
||||||
let newPrevEdge = null
|
let newPrevEdge = null
|
||||||
|
|
||||||
if (nodeType !== BlockEnum.DataSource) {
|
if (nodeType !== BlockEnum.DataSource && !skipAutoConnect) {
|
||||||
newPrevEdge = {
|
newPrevEdge = {
|
||||||
id: `${prevNodeId}-${prevNodeSourceHandle}-${newNode.id}-${targetHandle}`,
|
id: `${prevNodeId}-${prevNodeSourceHandle}-${newNode.id}-${targetHandle}`,
|
||||||
type: CUSTOM_EDGE,
|
type: CUSTOM_EDGE,
|
||||||
@ -1137,6 +1138,7 @@ export const useNodesInteractions = () => {
|
|||||||
nodeType !== BlockEnum.IfElse
|
nodeType !== BlockEnum.IfElse
|
||||||
&& nodeType !== BlockEnum.QuestionClassifier
|
&& nodeType !== BlockEnum.QuestionClassifier
|
||||||
&& nodeType !== BlockEnum.LoopEnd
|
&& nodeType !== BlockEnum.LoopEnd
|
||||||
|
&& !skipAutoConnect
|
||||||
) {
|
) {
|
||||||
newNextEdge = {
|
newNextEdge = {
|
||||||
id: `${newNode.id}-${sourceHandle}-${nextNodeId}-${nextNodeTargetHandle}`,
|
id: `${newNode.id}-${sourceHandle}-${nextNodeId}-${nextNodeTargetHandle}`,
|
||||||
|
|||||||
@ -9,6 +9,7 @@ type Props = {
|
|||||||
value: boolean
|
value: boolean
|
||||||
required?: boolean
|
required?: boolean
|
||||||
onChange: (value: boolean) => void
|
onChange: (value: boolean) => void
|
||||||
|
readonly?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const BoolInput: FC<Props> = ({
|
const BoolInput: FC<Props> = ({
|
||||||
@ -16,6 +17,7 @@ const BoolInput: FC<Props> = ({
|
|||||||
onChange,
|
onChange,
|
||||||
name,
|
name,
|
||||||
required,
|
required,
|
||||||
|
readonly,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const handleChange = useCallback(() => {
|
const handleChange = useCallback(() => {
|
||||||
@ -27,6 +29,7 @@ const BoolInput: FC<Props> = ({
|
|||||||
className='!h-4 !w-4'
|
className='!h-4 !w-4'
|
||||||
checked={!!value}
|
checked={!!value}
|
||||||
onCheck={handleChange}
|
onCheck={handleChange}
|
||||||
|
disabled={readonly}
|
||||||
/>
|
/>
|
||||||
<div className='system-sm-medium flex items-center gap-1 text-text-secondary'>
|
<div className='system-sm-medium flex items-center gap-1 text-text-secondary'>
|
||||||
{name}
|
{name}
|
||||||
|
|||||||
@ -15,7 +15,9 @@ import {
|
|||||||
useNodesSyncDraft,
|
useNodesSyncDraft,
|
||||||
} from '@/app/components/workflow/hooks'
|
} from '@/app/components/workflow/hooks'
|
||||||
import ShortcutsName from '@/app/components/workflow/shortcuts-name'
|
import ShortcutsName from '@/app/components/workflow/shortcuts-name'
|
||||||
import type { Node } from '@/app/components/workflow/types'
|
import { BlockEnum, type Node } from '@/app/components/workflow/types'
|
||||||
|
import PanelAddBlock from '@/app/components/workflow/nodes/iteration/panel-add-block'
|
||||||
|
import type { IterationNodeType } from '@/app/components/workflow/nodes/iteration/types'
|
||||||
|
|
||||||
type PanelOperatorPopupProps = {
|
type PanelOperatorPopupProps = {
|
||||||
id: string
|
id: string
|
||||||
@ -51,6 +53,9 @@ const PanelOperatorPopup = ({
|
|||||||
(showChangeBlock || canRunBySingle(data.type, isChildNode)) && (
|
(showChangeBlock || canRunBySingle(data.type, isChildNode)) && (
|
||||||
<>
|
<>
|
||||||
<div className='p-1'>
|
<div className='p-1'>
|
||||||
|
{data.type === BlockEnum.Iteration && (
|
||||||
|
<PanelAddBlock iterationNodeData={data as IterationNodeType} onClosePopup={onClosePopup}/>
|
||||||
|
)}
|
||||||
{
|
{
|
||||||
canRunBySingle(data.type, isChildNode) && (
|
canRunBySingle(data.type, isChildNode) && (
|
||||||
<div
|
<div
|
||||||
|
|||||||
116
web/app/components/workflow/nodes/iteration/panel-add-block.tsx
Normal file
116
web/app/components/workflow/nodes/iteration/panel-add-block.tsx
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import {
|
||||||
|
memo,
|
||||||
|
useCallback,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import type { OffsetOptions } from '@floating-ui/react'
|
||||||
|
import { useStoreApi } from 'reactflow'
|
||||||
|
|
||||||
|
import BlockSelector from '@/app/components/workflow/block-selector'
|
||||||
|
import type {
|
||||||
|
OnSelectBlock,
|
||||||
|
} from '@/app/components/workflow/types'
|
||||||
|
import {
|
||||||
|
BlockEnum,
|
||||||
|
} from '@/app/components/workflow/types'
|
||||||
|
import { useAvailableBlocks, useNodesMetaData, useNodesReadOnly, usePanelInteractions } from '../../hooks'
|
||||||
|
import type { IterationNodeType } from './types'
|
||||||
|
import { useWorkflowStore } from '../../store'
|
||||||
|
import { generateNewNode, getNodeCustomTypeByNodeDataType } from '../../utils'
|
||||||
|
import { ITERATION_CHILDREN_Z_INDEX } from '../../constants'
|
||||||
|
|
||||||
|
type AddBlockProps = {
|
||||||
|
renderTrigger?: (open: boolean) => React.ReactNode
|
||||||
|
offset?: OffsetOptions
|
||||||
|
iterationNodeData: IterationNodeType
|
||||||
|
onClosePopup: () => void
|
||||||
|
}
|
||||||
|
const AddBlock = ({
|
||||||
|
offset,
|
||||||
|
iterationNodeData,
|
||||||
|
onClosePopup,
|
||||||
|
}: AddBlockProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const store = useStoreApi()
|
||||||
|
const workflowStore = useWorkflowStore()
|
||||||
|
const { nodesReadOnly } = useNodesReadOnly()
|
||||||
|
const { handlePaneContextmenuCancel } = usePanelInteractions()
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const { availableNextBlocks } = useAvailableBlocks(BlockEnum.Start, false)
|
||||||
|
const { nodesMap: nodesMetaDataMap } = useNodesMetaData()
|
||||||
|
|
||||||
|
const handleOpenChange = useCallback((open: boolean) => {
|
||||||
|
setOpen(open)
|
||||||
|
if (!open)
|
||||||
|
handlePaneContextmenuCancel()
|
||||||
|
}, [handlePaneContextmenuCancel])
|
||||||
|
|
||||||
|
const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
|
||||||
|
const { getNodes } = store.getState()
|
||||||
|
const nodes = getNodes()
|
||||||
|
const nodesWithSameType = nodes.filter(node => node.data.type === type)
|
||||||
|
const { defaultValue } = nodesMetaDataMap![type]
|
||||||
|
|
||||||
|
// Find the parent iteration node
|
||||||
|
const parentIterationNode = nodes.find(node => node.data.start_node_id === iterationNodeData.start_node_id)
|
||||||
|
|
||||||
|
const { newNode } = generateNewNode({
|
||||||
|
type: getNodeCustomTypeByNodeDataType(type),
|
||||||
|
data: {
|
||||||
|
...(defaultValue as any),
|
||||||
|
title: nodesWithSameType.length > 0 ? `${defaultValue.title} ${nodesWithSameType.length + 1}` : defaultValue.title,
|
||||||
|
...toolDefaultValue,
|
||||||
|
_isCandidate: true,
|
||||||
|
// Set iteration-specific properties
|
||||||
|
isInIteration: true,
|
||||||
|
iteration_id: parentIterationNode?.id,
|
||||||
|
},
|
||||||
|
position: {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Set parent and z-index for iteration child
|
||||||
|
if (parentIterationNode) {
|
||||||
|
newNode.parentId = parentIterationNode.id
|
||||||
|
newNode.extent = 'parent' as any
|
||||||
|
newNode.zIndex = ITERATION_CHILDREN_Z_INDEX
|
||||||
|
}
|
||||||
|
|
||||||
|
workflowStore.setState({
|
||||||
|
candidateNode: newNode,
|
||||||
|
})
|
||||||
|
onClosePopup()
|
||||||
|
}, [store, workflowStore, nodesMetaDataMap, iterationNodeData.start_node_id, onClosePopup])
|
||||||
|
|
||||||
|
const renderTrigger = () => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className='flex h-8 cursor-pointer items-center justify-between rounded-lg px-3 text-sm text-text-secondary hover:bg-state-base-hover'
|
||||||
|
>
|
||||||
|
{t('workflow.common.addBlock')}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BlockSelector
|
||||||
|
open={open}
|
||||||
|
onOpenChange={handleOpenChange}
|
||||||
|
disabled={nodesReadOnly}
|
||||||
|
onSelect={handleSelect}
|
||||||
|
placement='right-start'
|
||||||
|
offset={offset ?? {
|
||||||
|
mainAxis: 4,
|
||||||
|
crossAxis: -8,
|
||||||
|
}}
|
||||||
|
trigger={renderTrigger}
|
||||||
|
popupClassName='!min-w-[256px]'
|
||||||
|
availableBlocksTypes={availableNextBlocks}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(AddBlock)
|
||||||
@ -380,6 +380,7 @@ export type OnNodeAdd = (
|
|||||||
prevNodeSourceHandle?: string
|
prevNodeSourceHandle?: string
|
||||||
nextNodeId?: string
|
nextNodeId?: string
|
||||||
nextNodeTargetHandle?: string
|
nextNodeTargetHandle?: string
|
||||||
|
skipAutoConnect?: boolean
|
||||||
},
|
},
|
||||||
) => void
|
) => void
|
||||||
|
|
||||||
|
|||||||
@ -61,12 +61,14 @@ type WorkflowPreviewProps = {
|
|||||||
edges: Edge[]
|
edges: Edge[]
|
||||||
viewport: Viewport
|
viewport: Viewport
|
||||||
className?: string
|
className?: string
|
||||||
|
miniMapToRight?: boolean
|
||||||
}
|
}
|
||||||
const WorkflowPreview = ({
|
const WorkflowPreview = ({
|
||||||
nodes,
|
nodes,
|
||||||
edges,
|
edges,
|
||||||
viewport,
|
viewport,
|
||||||
className,
|
className,
|
||||||
|
miniMapToRight,
|
||||||
}: WorkflowPreviewProps) => {
|
}: WorkflowPreviewProps) => {
|
||||||
const [nodesData, setNodesData] = useState(() => initialNodes(nodes, edges))
|
const [nodesData, setNodesData] = useState(() => initialNodes(nodes, edges))
|
||||||
const [edgesData, setEdgesData] = useState(() => initialEdges(edges, nodes))
|
const [edgesData, setEdgesData] = useState(() => initialEdges(edges, nodes))
|
||||||
@ -97,8 +99,9 @@ const WorkflowPreview = ({
|
|||||||
height: 72,
|
height: 72,
|
||||||
}}
|
}}
|
||||||
maskColor='var(--color-workflow-minimap-bg)'
|
maskColor='var(--color-workflow-minimap-bg)'
|
||||||
className='!absolute !bottom-14 !left-4 z-[9] !m-0 !h-[72px] !w-[102px] !rounded-lg !border-[0.5px]
|
className={cn('!absolute !bottom-14 z-[9] !m-0 !h-[72px] !w-[102px] !rounded-lg !border-[0.5px] !border-divider-subtle !bg-background-default-subtle !shadow-md !shadow-shadow-shadow-5',
|
||||||
!border-divider-subtle !bg-background-default-subtle !shadow-md !shadow-shadow-shadow-5'
|
miniMapToRight ? '!right-4' : '!left-4',
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<div className='absolute bottom-4 left-4 z-[9] mt-1 flex items-center gap-2'>
|
<div className='absolute bottom-4 left-4 z-[9] mt-1 flex items-center gap-2'>
|
||||||
<ZoomInOut />
|
<ZoomInOut />
|
||||||
|
|||||||
@ -29,6 +29,7 @@ import type { Collection } from '@/app/components/tools/types'
|
|||||||
import { noop } from 'lodash-es'
|
import { noop } from 'lodash-es'
|
||||||
|
|
||||||
type IDebugConfiguration = {
|
type IDebugConfiguration = {
|
||||||
|
readonly?: boolean
|
||||||
appId: string
|
appId: string
|
||||||
isAPIKeySet: boolean
|
isAPIKeySet: boolean
|
||||||
isTrailFinished: boolean
|
isTrailFinished: boolean
|
||||||
@ -108,6 +109,7 @@ type IDebugConfiguration = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const DebugConfigurationContext = createContext<IDebugConfiguration>({
|
const DebugConfigurationContext = createContext<IDebugConfiguration>({
|
||||||
|
readonly: false,
|
||||||
appId: '',
|
appId: '',
|
||||||
isAPIKeySet: false,
|
isAPIKeySet: false,
|
||||||
isTrailFinished: false,
|
isTrailFinished: false,
|
||||||
|
|||||||
@ -1,7 +1,12 @@
|
|||||||
import { createContext } from 'use-context-selector'
|
import { createContext } from 'use-context-selector'
|
||||||
import type { InstalledApp } from '@/models/explore'
|
import type { App, InstalledApp } from '@/models/explore'
|
||||||
import { noop } from 'lodash-es'
|
import { noop } from 'lodash-es'
|
||||||
|
|
||||||
|
export type CurrentTryAppParams = {
|
||||||
|
appId: string
|
||||||
|
app: App
|
||||||
|
}
|
||||||
|
|
||||||
type IExplore = {
|
type IExplore = {
|
||||||
controlUpdateInstalledApps: number
|
controlUpdateInstalledApps: number
|
||||||
setControlUpdateInstalledApps: (controlUpdateInstalledApps: number) => void
|
setControlUpdateInstalledApps: (controlUpdateInstalledApps: number) => void
|
||||||
@ -10,6 +15,9 @@ type IExplore = {
|
|||||||
setInstalledApps: (installedApps: InstalledApp[]) => void
|
setInstalledApps: (installedApps: InstalledApp[]) => void
|
||||||
isFetchingInstalledApps: boolean
|
isFetchingInstalledApps: boolean
|
||||||
setIsFetchingInstalledApps: (isFetchingInstalledApps: boolean) => void
|
setIsFetchingInstalledApps: (isFetchingInstalledApps: boolean) => void
|
||||||
|
currentApp?: CurrentTryAppParams
|
||||||
|
isShowTryAppPanel: boolean
|
||||||
|
setShowTryAppPanel: (showTryAppPanel: boolean, params?: CurrentTryAppParams) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const ExploreContext = createContext<IExplore>({
|
const ExploreContext = createContext<IExplore>({
|
||||||
@ -20,6 +28,9 @@ const ExploreContext = createContext<IExplore>({
|
|||||||
setInstalledApps: noop,
|
setInstalledApps: noop,
|
||||||
isFetchingInstalledApps: false,
|
isFetchingInstalledApps: false,
|
||||||
setIsFetchingInstalledApps: noop,
|
setIsFetchingInstalledApps: noop,
|
||||||
|
isShowTryAppPanel: false,
|
||||||
|
setShowTryAppPanel: noop,
|
||||||
|
currentApp: undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
export default ExploreContext
|
export default ExploreContext
|
||||||
|
|||||||
@ -2,19 +2,19 @@
|
|||||||
|
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const { camelCase } = require('lodash')
|
const { camelCase } = require('lodash-es')
|
||||||
|
|
||||||
// Import the NAMESPACES array from i18next-config.ts
|
// Import the NAMESPACES array from i18next-config.ts
|
||||||
function getNamespacesFromConfig() {
|
function getNamespacesFromConfig() {
|
||||||
const configPath = path.join(__dirname, 'i18next-config.ts')
|
const configPath = path.join(__dirname, 'i18next-config.ts')
|
||||||
const configContent = fs.readFileSync(configPath, 'utf8')
|
const configContent = fs.readFileSync(configPath, 'utf8')
|
||||||
|
|
||||||
// Extract NAMESPACES array using regex
|
// Extract NAMESPACES array using regex
|
||||||
const namespacesMatch = configContent.match(/const NAMESPACES = \[([\s\S]*?)\]/)
|
const namespacesMatch = configContent.match(/const NAMESPACES = \[([\s\S]*?)\]/)
|
||||||
if (!namespacesMatch) {
|
if (!namespacesMatch) {
|
||||||
throw new Error('Could not find NAMESPACES array in i18next-config.ts')
|
throw new Error('Could not find NAMESPACES array in i18next-config.ts')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the namespaces
|
// Parse the namespaces
|
||||||
const namespacesStr = namespacesMatch[1]
|
const namespacesStr = namespacesMatch[1]
|
||||||
const namespaces = namespacesStr
|
const namespaces = namespacesStr
|
||||||
@ -22,25 +22,25 @@ function getNamespacesFromConfig() {
|
|||||||
.map(line => line.trim())
|
.map(line => line.trim())
|
||||||
.filter(line => line.startsWith("'") || line.startsWith('"'))
|
.filter(line => line.startsWith("'") || line.startsWith('"'))
|
||||||
.map(line => line.slice(1, -1)) // Remove quotes
|
.map(line => line.slice(1, -1)) // Remove quotes
|
||||||
|
|
||||||
return namespaces
|
return namespaces
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNamespacesFromTypes() {
|
function getNamespacesFromTypes() {
|
||||||
const typesPath = path.join(__dirname, '../types/i18n.d.ts')
|
const typesPath = path.join(__dirname, '../types/i18n.d.ts')
|
||||||
|
|
||||||
if (!fs.existsSync(typesPath)) {
|
if (!fs.existsSync(typesPath)) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const typesContent = fs.readFileSync(typesPath, 'utf8')
|
const typesContent = fs.readFileSync(typesPath, 'utf8')
|
||||||
|
|
||||||
// Extract namespaces from Messages type
|
// Extract namespaces from Messages type
|
||||||
const messagesMatch = typesContent.match(/export type Messages = \{([\s\S]*?)\}/)
|
const messagesMatch = typesContent.match(/export type Messages = \{([\s\S]*?)\}/)
|
||||||
if (!messagesMatch) {
|
if (!messagesMatch) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the properties
|
// Parse the properties
|
||||||
const propertiesStr = messagesMatch[1]
|
const propertiesStr = messagesMatch[1]
|
||||||
const properties = propertiesStr
|
const properties = propertiesStr
|
||||||
@ -49,66 +49,66 @@ function getNamespacesFromTypes() {
|
|||||||
.filter(line => line.includes(':'))
|
.filter(line => line.includes(':'))
|
||||||
.map(line => line.split(':')[0].trim())
|
.map(line => line.split(':')[0].trim())
|
||||||
.filter(prop => prop.length > 0)
|
.filter(prop => prop.length > 0)
|
||||||
|
|
||||||
return properties
|
return properties
|
||||||
}
|
}
|
||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
try {
|
try {
|
||||||
console.log('🔍 Checking i18n types synchronization...')
|
console.log('🔍 Checking i18n types synchronization...')
|
||||||
|
|
||||||
// Get namespaces from config
|
// Get namespaces from config
|
||||||
const configNamespaces = getNamespacesFromConfig()
|
const configNamespaces = getNamespacesFromConfig()
|
||||||
console.log(`📦 Found ${configNamespaces.length} namespaces in config`)
|
console.log(`📦 Found ${configNamespaces.length} namespaces in config`)
|
||||||
|
|
||||||
// Convert to camelCase for comparison
|
// Convert to camelCase for comparison
|
||||||
const configCamelCase = configNamespaces.map(ns => camelCase(ns)).sort()
|
const configCamelCase = configNamespaces.map(ns => camelCase(ns)).sort()
|
||||||
|
|
||||||
// Get namespaces from type definitions
|
// Get namespaces from type definitions
|
||||||
const typeNamespaces = getNamespacesFromTypes()
|
const typeNamespaces = getNamespacesFromTypes()
|
||||||
|
|
||||||
if (!typeNamespaces) {
|
if (!typeNamespaces) {
|
||||||
console.error('❌ Type definitions file not found or invalid')
|
console.error('❌ Type definitions file not found or invalid')
|
||||||
console.error(' Run: pnpm run gen:i18n-types')
|
console.error(' Run: pnpm run gen:i18n-types')
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`🔧 Found ${typeNamespaces.length} namespaces in types`)
|
console.log(`🔧 Found ${typeNamespaces.length} namespaces in types`)
|
||||||
|
|
||||||
const typeCamelCase = typeNamespaces.sort()
|
const typeCamelCase = typeNamespaces.sort()
|
||||||
|
|
||||||
// Compare arrays
|
// Compare arrays
|
||||||
const configSet = new Set(configCamelCase)
|
const configSet = new Set(configCamelCase)
|
||||||
const typeSet = new Set(typeCamelCase)
|
const typeSet = new Set(typeCamelCase)
|
||||||
|
|
||||||
// Find missing in types
|
// Find missing in types
|
||||||
const missingInTypes = configCamelCase.filter(ns => !typeSet.has(ns))
|
const missingInTypes = configCamelCase.filter(ns => !typeSet.has(ns))
|
||||||
|
|
||||||
// Find extra in types
|
// Find extra in types
|
||||||
const extraInTypes = typeCamelCase.filter(ns => !configSet.has(ns))
|
const extraInTypes = typeCamelCase.filter(ns => !configSet.has(ns))
|
||||||
|
|
||||||
let hasErrors = false
|
let hasErrors = false
|
||||||
|
|
||||||
if (missingInTypes.length > 0) {
|
if (missingInTypes.length > 0) {
|
||||||
hasErrors = true
|
hasErrors = true
|
||||||
console.error('❌ Missing in type definitions:')
|
console.error('❌ Missing in type definitions:')
|
||||||
missingInTypes.forEach(ns => console.error(` - ${ns}`))
|
missingInTypes.forEach(ns => console.error(` - ${ns}`))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (extraInTypes.length > 0) {
|
if (extraInTypes.length > 0) {
|
||||||
hasErrors = true
|
hasErrors = true
|
||||||
console.error('❌ Extra in type definitions:')
|
console.error('❌ Extra in type definitions:')
|
||||||
extraInTypes.forEach(ns => console.error(` - ${ns}`))
|
extraInTypes.forEach(ns => console.error(` - ${ns}`))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasErrors) {
|
if (hasErrors) {
|
||||||
console.error('\n💡 To fix synchronization issues:')
|
console.error('\n💡 To fix synchronization issues:')
|
||||||
console.error(' Run: pnpm run gen:i18n-types')
|
console.error(' Run: pnpm run gen:i18n-types')
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('✅ i18n types are synchronized')
|
console.log('✅ i18n types are synchronized')
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Error:', error.message)
|
console.error('❌ Error:', error.message)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
@ -117,4 +117,4 @@ function main() {
|
|||||||
|
|
||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
main()
|
main()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,19 +2,19 @@
|
|||||||
|
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const { camelCase } = require('lodash')
|
const { camelCase } = require('lodash-es')
|
||||||
|
|
||||||
// Import the NAMESPACES array from i18next-config.ts
|
// Import the NAMESPACES array from i18next-config.ts
|
||||||
function getNamespacesFromConfig() {
|
function getNamespacesFromConfig() {
|
||||||
const configPath = path.join(__dirname, 'i18next-config.ts')
|
const configPath = path.join(__dirname, 'i18next-config.ts')
|
||||||
const configContent = fs.readFileSync(configPath, 'utf8')
|
const configContent = fs.readFileSync(configPath, 'utf8')
|
||||||
|
|
||||||
// Extract NAMESPACES array using regex
|
// Extract NAMESPACES array using regex
|
||||||
const namespacesMatch = configContent.match(/const NAMESPACES = \[([\s\S]*?)\]/)
|
const namespacesMatch = configContent.match(/const NAMESPACES = \[([\s\S]*?)\]/)
|
||||||
if (!namespacesMatch) {
|
if (!namespacesMatch) {
|
||||||
throw new Error('Could not find NAMESPACES array in i18next-config.ts')
|
throw new Error('Could not find NAMESPACES array in i18next-config.ts')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the namespaces
|
// Parse the namespaces
|
||||||
const namespacesStr = namespacesMatch[1]
|
const namespacesStr = namespacesMatch[1]
|
||||||
const namespaces = namespacesStr
|
const namespaces = namespacesStr
|
||||||
@ -22,7 +22,7 @@ function getNamespacesFromConfig() {
|
|||||||
.map(line => line.trim())
|
.map(line => line.trim())
|
||||||
.filter(line => line.startsWith("'") || line.startsWith('"'))
|
.filter(line => line.startsWith("'") || line.startsWith('"'))
|
||||||
.map(line => line.slice(1, -1)) // Remove quotes
|
.map(line => line.slice(1, -1)) // Remove quotes
|
||||||
|
|
||||||
return namespaces
|
return namespaces
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,40 +90,40 @@ declare module 'i18next' {
|
|||||||
function main() {
|
function main() {
|
||||||
const args = process.argv.slice(2)
|
const args = process.argv.slice(2)
|
||||||
const checkMode = args.includes('--check')
|
const checkMode = args.includes('--check')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('📦 Generating i18n type definitions...')
|
console.log('📦 Generating i18n type definitions...')
|
||||||
|
|
||||||
// Get namespaces from config
|
// Get namespaces from config
|
||||||
const namespaces = getNamespacesFromConfig()
|
const namespaces = getNamespacesFromConfig()
|
||||||
console.log(`✅ Found ${namespaces.length} namespaces`)
|
console.log(`✅ Found ${namespaces.length} namespaces`)
|
||||||
|
|
||||||
// Generate type definitions
|
// Generate type definitions
|
||||||
const typeDefinitions = generateTypeDefinitions(namespaces)
|
const typeDefinitions = generateTypeDefinitions(namespaces)
|
||||||
|
|
||||||
const outputPath = path.join(__dirname, '../types/i18n.d.ts')
|
const outputPath = path.join(__dirname, '../types/i18n.d.ts')
|
||||||
|
|
||||||
if (checkMode) {
|
if (checkMode) {
|
||||||
// Check mode: compare with existing file
|
// Check mode: compare with existing file
|
||||||
if (!fs.existsSync(outputPath)) {
|
if (!fs.existsSync(outputPath)) {
|
||||||
console.error('❌ Type definitions file does not exist')
|
console.error('❌ Type definitions file does not exist')
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingContent = fs.readFileSync(outputPath, 'utf8')
|
const existingContent = fs.readFileSync(outputPath, 'utf8')
|
||||||
if (existingContent.trim() !== typeDefinitions.trim()) {
|
if (existingContent.trim() !== typeDefinitions.trim()) {
|
||||||
console.error('❌ Type definitions are out of sync')
|
console.error('❌ Type definitions are out of sync')
|
||||||
console.error(' Run: pnpm run gen:i18n-types')
|
console.error(' Run: pnpm run gen:i18n-types')
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('✅ Type definitions are in sync')
|
console.log('✅ Type definitions are in sync')
|
||||||
} else {
|
} else {
|
||||||
// Generate mode: write file
|
// Generate mode: write file
|
||||||
fs.writeFileSync(outputPath, typeDefinitions)
|
fs.writeFileSync(outputPath, typeDefinitions)
|
||||||
console.log(`✅ Generated type definitions: ${outputPath}`)
|
console.log(`✅ Generated type definitions: ${outputPath}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Error:', error.message)
|
console.error('❌ Error:', error.message)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
@ -132,4 +132,4 @@ function main() {
|
|||||||
|
|
||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
main()
|
main()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
const translation = {
|
const translation = {
|
||||||
title: 'Entdecken',
|
title: 'Entdecken',
|
||||||
sidebar: {
|
sidebar: {
|
||||||
discovery: 'Entdeckung',
|
|
||||||
chat: 'Chat',
|
chat: 'Chat',
|
||||||
workspace: 'Arbeitsbereich',
|
|
||||||
action: {
|
action: {
|
||||||
pin: 'Anheften',
|
pin: 'Anheften',
|
||||||
unpin: 'Lösen',
|
unpin: 'Lösen',
|
||||||
@ -16,12 +14,8 @@ const translation = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
apps: {
|
apps: {
|
||||||
title: 'Apps von Dify erkunden',
|
|
||||||
description: 'Nutzen Sie diese Vorlagen-Apps sofort oder passen Sie Ihre eigenen Apps basierend auf den Vorlagen an.',
|
|
||||||
allCategories: 'Alle Kategorien',
|
|
||||||
},
|
},
|
||||||
appCard: {
|
appCard: {
|
||||||
addToWorkspace: 'Zum Arbeitsbereich hinzufügen',
|
|
||||||
customize: 'Anpassen',
|
customize: 'Anpassen',
|
||||||
},
|
},
|
||||||
appCustomize: {
|
appCustomize: {
|
||||||
|
|||||||
@ -666,6 +666,7 @@ const translation = {
|
|||||||
hitScore: 'Retrieval Score:',
|
hitScore: 'Retrieval Score:',
|
||||||
},
|
},
|
||||||
inputPlaceholder: 'Talk to {{botName}}',
|
inputPlaceholder: 'Talk to {{botName}}',
|
||||||
|
inputDisabledPlaceholder: 'Preview Only',
|
||||||
thinking: 'Thinking...',
|
thinking: 'Thinking...',
|
||||||
thought: 'Thought',
|
thought: 'Thought',
|
||||||
resend: 'Resend',
|
resend: 'Resend',
|
||||||
|
|||||||
@ -38,7 +38,7 @@ const translation = {
|
|||||||
button: 'Drag and drop file or folder, or',
|
button: 'Drag and drop file or folder, or',
|
||||||
buttonSingleFile: 'Drag and drop file, or',
|
buttonSingleFile: 'Drag and drop file, or',
|
||||||
browse: 'Browse',
|
browse: 'Browse',
|
||||||
tip: 'Supports {{supportTypes}}. Max {{batchCount}} in a batch and {{size}} MB each.',
|
tip: 'Supports {{supportTypes}}. Max {{batchCount}} in a batch and {{size}} MB each. Max total {{totalCount}} files.',
|
||||||
validation: {
|
validation: {
|
||||||
typeError: 'File type not supported',
|
typeError: 'File type not supported',
|
||||||
size: 'File too large. Maximum is {{size}}MB',
|
size: 'File too large. Maximum is {{size}}MB',
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
const translation = {
|
const translation = {
|
||||||
title: 'Explore',
|
title: 'Explore',
|
||||||
sidebar: {
|
sidebar: {
|
||||||
discovery: 'Discovery',
|
title: 'App gallery',
|
||||||
chat: 'Chat',
|
chat: 'Chat',
|
||||||
workspace: 'Workspace',
|
webApps: 'Web apps',
|
||||||
action: {
|
action: {
|
||||||
pin: 'Pin',
|
pin: 'Pin',
|
||||||
unpin: 'Unpin',
|
unpin: 'Unpin',
|
||||||
@ -14,15 +14,31 @@ const translation = {
|
|||||||
title: 'Delete app',
|
title: 'Delete app',
|
||||||
content: 'Are you sure you want to delete this app?',
|
content: 'Are you sure you want to delete this app?',
|
||||||
},
|
},
|
||||||
|
noApps: {
|
||||||
|
title: 'No web apps',
|
||||||
|
description: 'Published web apps will appear here',
|
||||||
|
learnMore: 'Learn more',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
apps: {
|
apps: {
|
||||||
title: 'Explore Apps',
|
title: 'Try Dify\'s curated apps to find AI solutions for your business',
|
||||||
description: 'Use these template apps instantly or customize your own apps based on the templates.',
|
allCategories: 'All',
|
||||||
allCategories: 'Recommended',
|
resultNum: '{{num}} results',
|
||||||
|
resetFilter: 'Clear filter',
|
||||||
},
|
},
|
||||||
appCard: {
|
appCard: {
|
||||||
addToWorkspace: 'Add to Workspace',
|
addToWorkspace: 'Use template',
|
||||||
customize: 'Customize',
|
try: 'Details',
|
||||||
|
},
|
||||||
|
tryApp: {
|
||||||
|
tabHeader: {
|
||||||
|
try: 'Try it',
|
||||||
|
detail: 'Orchestration Details',
|
||||||
|
},
|
||||||
|
createFromSampleApp: 'Create from this sample app',
|
||||||
|
category: 'Category',
|
||||||
|
requirements: 'Requirements',
|
||||||
|
tryInfo: 'This is a sample app. You can try up to 5 messages. To keep using it, click "Create form this sample app" and set it up!',
|
||||||
},
|
},
|
||||||
appCustomize: {
|
appCustomize: {
|
||||||
title: 'Create app from {{name}}',
|
title: 'Create app from {{name}}',
|
||||||
@ -39,6 +55,9 @@ const translation = {
|
|||||||
Workflow: 'Workflow',
|
Workflow: 'Workflow',
|
||||||
Entertainment: 'Entertainment',
|
Entertainment: 'Entertainment',
|
||||||
},
|
},
|
||||||
|
banner: {
|
||||||
|
viewMore: 'VIEW MORE',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default translation
|
export default translation
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
const translation = {
|
const translation = {
|
||||||
title: 'Explorar',
|
title: 'Explorar',
|
||||||
sidebar: {
|
sidebar: {
|
||||||
discovery: 'Descubrimiento',
|
|
||||||
chat: 'Chat',
|
chat: 'Chat',
|
||||||
workspace: 'Espacio de trabajo',
|
|
||||||
action: {
|
action: {
|
||||||
pin: 'Anclar',
|
pin: 'Anclar',
|
||||||
unpin: 'Desanclar',
|
unpin: 'Desanclar',
|
||||||
@ -16,12 +14,8 @@ const translation = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
apps: {
|
apps: {
|
||||||
title: 'Explorar aplicaciones de Dify',
|
|
||||||
description: 'Utiliza estas aplicaciones de plantilla al instante o personaliza tus propias aplicaciones basadas en las plantillas.',
|
|
||||||
allCategories: 'Recomendado',
|
|
||||||
},
|
},
|
||||||
appCard: {
|
appCard: {
|
||||||
addToWorkspace: 'Agregar al espacio de trabajo',
|
|
||||||
customize: 'Personalizar',
|
customize: 'Personalizar',
|
||||||
},
|
},
|
||||||
appCustomize: {
|
appCustomize: {
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
const translation = {
|
const translation = {
|
||||||
title: 'کاوش',
|
title: 'کاوش',
|
||||||
sidebar: {
|
sidebar: {
|
||||||
discovery: 'کشف',
|
|
||||||
chat: 'چت',
|
chat: 'چت',
|
||||||
workspace: 'فضای کاری',
|
|
||||||
action: {
|
action: {
|
||||||
pin: 'سنجاق کردن',
|
pin: 'سنجاق کردن',
|
||||||
unpin: 'برداشتن سنجاق',
|
unpin: 'برداشتن سنجاق',
|
||||||
@ -16,12 +14,8 @@ const translation = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
apps: {
|
apps: {
|
||||||
title: 'کاوش برنامهها توسط دیفی',
|
|
||||||
description: 'از این برنامههای قالبی بلافاصله استفاده کنید یا برنامههای خود را بر اساس این قالبها سفارشی کنید.',
|
|
||||||
allCategories: 'پیشنهاد شده',
|
|
||||||
},
|
},
|
||||||
appCard: {
|
appCard: {
|
||||||
addToWorkspace: 'افزودن به فضای کاری',
|
|
||||||
customize: 'سفارشی کردن',
|
customize: 'سفارشی کردن',
|
||||||
},
|
},
|
||||||
appCustomize: {
|
appCustomize: {
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
const translation = {
|
const translation = {
|
||||||
title: 'Explorer',
|
title: 'Explorer',
|
||||||
sidebar: {
|
sidebar: {
|
||||||
discovery: 'Découverte',
|
|
||||||
chat: 'Discussion',
|
chat: 'Discussion',
|
||||||
workspace: 'Espace de travail',
|
|
||||||
action: {
|
action: {
|
||||||
pin: 'Épingle',
|
pin: 'Épingle',
|
||||||
unpin: 'Détacher',
|
unpin: 'Détacher',
|
||||||
@ -16,12 +14,8 @@ const translation = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
apps: {
|
apps: {
|
||||||
title: 'Explorez les applications par Dify',
|
|
||||||
description: 'Utilisez ces applications modèles instantanément ou personnalisez vos propres applications basées sur les modèles.',
|
|
||||||
allCategories: 'Recommandé',
|
|
||||||
},
|
},
|
||||||
appCard: {
|
appCard: {
|
||||||
addToWorkspace: 'Ajouter à l\'espace de travail',
|
|
||||||
customize: 'Personnaliser',
|
customize: 'Personnaliser',
|
||||||
},
|
},
|
||||||
appCustomize: {
|
appCustomize: {
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
const translation = {
|
const translation = {
|
||||||
title: 'अन्वेषण करें',
|
title: 'अन्वेषण करें',
|
||||||
sidebar: {
|
sidebar: {
|
||||||
discovery: 'खोज',
|
|
||||||
chat: 'चैट',
|
chat: 'चैट',
|
||||||
workspace: 'कार्यक्षेत्र',
|
|
||||||
action: {
|
action: {
|
||||||
pin: 'पिन करें',
|
pin: 'पिन करें',
|
||||||
unpin: 'पिन हटाएँ',
|
unpin: 'पिन हटाएँ',
|
||||||
@ -16,13 +14,8 @@ const translation = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
apps: {
|
apps: {
|
||||||
title: 'डिफ़ी द्वारा ऐप्स का अन्वेषण करें',
|
|
||||||
description:
|
|
||||||
'इन टेम्प्लेट ऐप्स का तुरंत उपयोग करें या टेम्प्लेट्स के आधार पर अपने स्वयं के ऐप्स को कस्टमाइज़ करें।',
|
|
||||||
allCategories: 'अनुशंसित',
|
|
||||||
},
|
},
|
||||||
appCard: {
|
appCard: {
|
||||||
addToWorkspace: 'कार्यक्षेत्र में जोड़ें',
|
|
||||||
customize: 'अनुकूलित करें',
|
customize: 'अनुकूलित करें',
|
||||||
},
|
},
|
||||||
appCustomize: {
|
appCustomize: {
|
||||||
|
|||||||
@ -10,18 +10,12 @@ const translation = {
|
|||||||
content: 'Apakah Anda yakin ingin menghapus aplikasi ini?',
|
content: 'Apakah Anda yakin ingin menghapus aplikasi ini?',
|
||||||
title: 'Hapus aplikasi',
|
title: 'Hapus aplikasi',
|
||||||
},
|
},
|
||||||
workspace: 'Workspace',
|
|
||||||
discovery: 'Penemuan',
|
|
||||||
chat: 'Mengobrol',
|
chat: 'Mengobrol',
|
||||||
},
|
},
|
||||||
apps: {
|
apps: {
|
||||||
allCategories: 'Direkomendasikan',
|
|
||||||
description: 'Gunakan aplikasi templat ini secara instan atau sesuaikan aplikasi Anda sendiri berdasarkan templat.',
|
|
||||||
title: 'Jelajahi Aplikasi',
|
|
||||||
},
|
},
|
||||||
appCard: {
|
appCard: {
|
||||||
customize: 'Menyesuaikan',
|
customize: 'Menyesuaikan',
|
||||||
addToWorkspace: 'Tambahkan ke Ruang Kerja',
|
|
||||||
},
|
},
|
||||||
appCustomize: {
|
appCustomize: {
|
||||||
subTitle: 'Ikon & nama aplikasi',
|
subTitle: 'Ikon & nama aplikasi',
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
const translation = {
|
const translation = {
|
||||||
title: 'Esplora',
|
title: 'Esplora',
|
||||||
sidebar: {
|
sidebar: {
|
||||||
discovery: 'Scoperta',
|
|
||||||
chat: 'Chat',
|
chat: 'Chat',
|
||||||
workspace: 'Workspace',
|
|
||||||
action: {
|
action: {
|
||||||
pin: 'Fissa',
|
pin: 'Fissa',
|
||||||
unpin: 'Sblocca',
|
unpin: 'Sblocca',
|
||||||
@ -16,13 +14,8 @@ const translation = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
apps: {
|
apps: {
|
||||||
title: 'Esplora App di Dify',
|
|
||||||
description:
|
|
||||||
'Usa queste app modello istantaneamente o personalizza le tue app basate sui modelli.',
|
|
||||||
allCategories: 'Consigliato',
|
|
||||||
},
|
},
|
||||||
appCard: {
|
appCard: {
|
||||||
addToWorkspace: 'Aggiungi a Workspace',
|
|
||||||
customize: 'Personalizza',
|
customize: 'Personalizza',
|
||||||
},
|
},
|
||||||
appCustomize: {
|
appCustomize: {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user