mirror of https://github.com/langgenius/dify.git
Merge remote-tracking branch 'origin/main' into feat/trigger
This commit is contained in:
commit
29353bd7c2
|
|
@ -25,7 +25,7 @@ class FirecrawlApp:
|
|||
}
|
||||
if params:
|
||||
json_data.update(params)
|
||||
response = self._post_request(f"{self.base_url}/v1/scrape", json_data, headers)
|
||||
response = self._post_request(f"{self.base_url}/v2/scrape", json_data, headers)
|
||||
if response.status_code == 200:
|
||||
response_data = response.json()
|
||||
data = response_data["data"]
|
||||
|
|
@ -42,7 +42,7 @@ class FirecrawlApp:
|
|||
json_data = {"url": url}
|
||||
if params:
|
||||
json_data.update(params)
|
||||
response = self._post_request(f"{self.base_url}/v1/crawl", json_data, headers)
|
||||
response = self._post_request(f"{self.base_url}/v2/crawl", json_data, headers)
|
||||
if response.status_code == 200:
|
||||
# There's also another two fields in the response: "success" (bool) and "url" (str)
|
||||
job_id = response.json().get("id")
|
||||
|
|
@ -51,9 +51,25 @@ class FirecrawlApp:
|
|||
self._handle_error(response, "start crawl job")
|
||||
return "" # unreachable
|
||||
|
||||
def map(self, url: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
# Documentation: https://docs.firecrawl.dev/api-reference/endpoint/map
|
||||
headers = self._prepare_headers()
|
||||
json_data: dict[str, Any] = {"url": url, "integration": "dify"}
|
||||
if params:
|
||||
# Pass through provided params, including optional "sitemap": "only" | "include" | "skip"
|
||||
json_data.update(params)
|
||||
response = self._post_request(f"{self.base_url}/v2/map", json_data, headers)
|
||||
if response.status_code == 200:
|
||||
return cast(dict[str, Any], response.json())
|
||||
elif response.status_code in {402, 409, 500, 429, 408}:
|
||||
self._handle_error(response, "start map job")
|
||||
return {}
|
||||
else:
|
||||
raise Exception(f"Failed to start map job. Status code: {response.status_code}")
|
||||
|
||||
def check_crawl_status(self, job_id) -> dict[str, Any]:
|
||||
headers = self._prepare_headers()
|
||||
response = self._get_request(f"{self.base_url}/v1/crawl/{job_id}", headers)
|
||||
response = self._get_request(f"{self.base_url}/v2/crawl/{job_id}", headers)
|
||||
if response.status_code == 200:
|
||||
crawl_status_response = response.json()
|
||||
if crawl_status_response.get("status") == "completed":
|
||||
|
|
@ -135,12 +151,16 @@ class FirecrawlApp:
|
|||
"lang": "en",
|
||||
"country": "us",
|
||||
"timeout": 60000,
|
||||
"ignoreInvalidURLs": False,
|
||||
"ignoreInvalidURLs": True,
|
||||
"scrapeOptions": {},
|
||||
"sources": [
|
||||
{"type": "web"},
|
||||
],
|
||||
"integration": "dify",
|
||||
}
|
||||
if params:
|
||||
json_data.update(params)
|
||||
response = self._post_request(f"{self.base_url}/v1/search", json_data, headers)
|
||||
response = self._post_request(f"{self.base_url}/v2/search", json_data, headers)
|
||||
if response.status_code == 200:
|
||||
response_data = response.json()
|
||||
if not response_data.get("success"):
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ class RedisChannel:
|
|||
self._redis = redis_client
|
||||
self._key = channel_key
|
||||
self._command_ttl = command_ttl
|
||||
self._pending_key = f"{channel_key}:pending"
|
||||
|
||||
def fetch_commands(self) -> list[GraphEngineCommand]:
|
||||
"""
|
||||
|
|
@ -49,6 +50,9 @@ class RedisChannel:
|
|||
Returns:
|
||||
List of pending commands (drains the Redis list)
|
||||
"""
|
||||
if not self._has_pending_commands():
|
||||
return []
|
||||
|
||||
commands: list[GraphEngineCommand] = []
|
||||
|
||||
# Use pipeline for atomic operations
|
||||
|
|
@ -85,6 +89,7 @@ class RedisChannel:
|
|||
with self._redis.pipeline() as pipe:
|
||||
pipe.rpush(self._key, command_json)
|
||||
pipe.expire(self._key, self._command_ttl)
|
||||
pipe.set(self._pending_key, "1", ex=self._command_ttl)
|
||||
pipe.execute()
|
||||
|
||||
def _deserialize_command(self, data: dict[str, Any]) -> GraphEngineCommand | None:
|
||||
|
|
@ -112,3 +117,17 @@ class RedisChannel:
|
|||
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
def _has_pending_commands(self) -> bool:
|
||||
"""
|
||||
Check and consume the pending marker to avoid unnecessary list reads.
|
||||
|
||||
Returns:
|
||||
True if commands should be fetched from Redis.
|
||||
"""
|
||||
with self._redis.pipeline() as pipe:
|
||||
pipe.get(self._pending_key)
|
||||
pipe.delete(self._pending_key)
|
||||
pending_value, _ = pipe.execute()
|
||||
|
||||
return pending_value is not None
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from collections.abc import Mapping
|
|||
from functools import singledispatchmethod
|
||||
from typing import TYPE_CHECKING, final
|
||||
|
||||
from core.model_runtime.entities.llm_entities import LLMUsage
|
||||
from core.workflow.entities import GraphRuntimeState
|
||||
from core.workflow.enums import ErrorStrategy, NodeExecutionType
|
||||
from core.workflow.graph import Graph
|
||||
|
|
@ -125,6 +126,7 @@ class EventHandler:
|
|||
node_execution = self._graph_execution.get_or_create_node_execution(event.node_id)
|
||||
is_initial_attempt = node_execution.retry_count == 0
|
||||
node_execution.mark_started(event.id)
|
||||
self._graph_runtime_state.increment_node_run_steps()
|
||||
|
||||
# Track in response coordinator for stream ordering
|
||||
self._response_coordinator.track_node_execution(event.node_id, event.id)
|
||||
|
|
@ -163,6 +165,8 @@ class EventHandler:
|
|||
node_execution = self._graph_execution.get_or_create_node_execution(event.node_id)
|
||||
node_execution.mark_taken()
|
||||
|
||||
self._accumulate_node_usage(event.node_run_result.llm_usage)
|
||||
|
||||
# Store outputs in variable pool
|
||||
self._store_node_outputs(event.node_id, event.node_run_result.outputs)
|
||||
|
||||
|
|
@ -212,6 +216,8 @@ class EventHandler:
|
|||
node_execution.mark_failed(event.error)
|
||||
self._graph_execution.record_node_failure()
|
||||
|
||||
self._accumulate_node_usage(event.node_run_result.llm_usage)
|
||||
|
||||
result = self._error_handler.handle_node_failure(event)
|
||||
|
||||
if result:
|
||||
|
|
@ -235,6 +241,8 @@ class EventHandler:
|
|||
node_execution = self._graph_execution.get_or_create_node_execution(event.node_id)
|
||||
node_execution.mark_taken()
|
||||
|
||||
self._accumulate_node_usage(event.node_run_result.llm_usage)
|
||||
|
||||
# Persist outputs produced by the exception strategy (e.g. default values)
|
||||
self._store_node_outputs(event.node_id, event.node_run_result.outputs)
|
||||
|
||||
|
|
@ -286,6 +294,19 @@ class EventHandler:
|
|||
self._state_manager.enqueue_node(event.node_id)
|
||||
self._state_manager.start_execution(event.node_id)
|
||||
|
||||
def _accumulate_node_usage(self, usage: LLMUsage) -> None:
|
||||
"""Accumulate token usage into the shared runtime state."""
|
||||
if usage.total_tokens <= 0:
|
||||
return
|
||||
|
||||
self._graph_runtime_state.add_tokens(usage.total_tokens)
|
||||
|
||||
current_usage = self._graph_runtime_state.llm_usage
|
||||
if current_usage.total_tokens == 0:
|
||||
self._graph_runtime_state.llm_usage = usage
|
||||
else:
|
||||
self._graph_runtime_state.llm_usage = current_usage.plus(usage)
|
||||
|
||||
def _store_node_outputs(self, node_id: str, outputs: Mapping[str, object]) -> None:
|
||||
"""
|
||||
Store node outputs in the variable pool.
|
||||
|
|
|
|||
|
|
@ -8,7 +8,12 @@ import threading
|
|||
import time
|
||||
from typing import TYPE_CHECKING, final
|
||||
|
||||
from core.workflow.graph_events.base import GraphNodeEventBase
|
||||
from core.workflow.graph_events import (
|
||||
GraphNodeEventBase,
|
||||
NodeRunExceptionEvent,
|
||||
NodeRunFailedEvent,
|
||||
NodeRunSucceededEvent,
|
||||
)
|
||||
|
||||
from ..event_management import EventManager
|
||||
from .execution_coordinator import ExecutionCoordinator
|
||||
|
|
@ -72,13 +77,16 @@ class Dispatcher:
|
|||
if self._thread and self._thread.is_alive():
|
||||
self._thread.join(timeout=10.0)
|
||||
|
||||
_COMMAND_TRIGGER_EVENTS = (
|
||||
NodeRunSucceededEvent,
|
||||
NodeRunFailedEvent,
|
||||
NodeRunExceptionEvent,
|
||||
)
|
||||
|
||||
def _dispatcher_loop(self) -> None:
|
||||
"""Main dispatcher loop."""
|
||||
try:
|
||||
while not self._stop_event.is_set():
|
||||
# Check for commands
|
||||
self._execution_coordinator.check_commands()
|
||||
|
||||
# Check for scaling
|
||||
self._execution_coordinator.check_scaling()
|
||||
|
||||
|
|
@ -87,6 +95,8 @@ class Dispatcher:
|
|||
event = self._event_queue.get(timeout=0.1)
|
||||
# Route to the event handler
|
||||
self._event_handler.dispatch(event)
|
||||
if self._should_check_commands(event):
|
||||
self._execution_coordinator.check_commands()
|
||||
self._event_queue.task_done()
|
||||
except queue.Empty:
|
||||
# Check if execution is complete
|
||||
|
|
@ -102,3 +112,7 @@ class Dispatcher:
|
|||
# Signal the event emitter that execution is complete
|
||||
if self._event_emitter:
|
||||
self._event_emitter.mark_complete()
|
||||
|
||||
def _should_check_commands(self, event: GraphNodeEventBase) -> bool:
|
||||
"""Return True if the event represents a node completion."""
|
||||
return isinstance(event, self._COMMAND_TRIGGER_EVENTS)
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ class CrawlOptions:
|
|||
only_main_content: bool = False
|
||||
includes: str | None = None
|
||||
excludes: str | None = None
|
||||
prompt: str | None = None
|
||||
max_depth: int | None = None
|
||||
use_sitemap: bool = True
|
||||
|
||||
|
|
@ -70,6 +71,7 @@ class WebsiteCrawlApiRequest:
|
|||
only_main_content=self.options.get("only_main_content", False),
|
||||
includes=self.options.get("includes"),
|
||||
excludes=self.options.get("excludes"),
|
||||
prompt=self.options.get("prompt"),
|
||||
max_depth=self.options.get("max_depth"),
|
||||
use_sitemap=self.options.get("use_sitemap", True),
|
||||
)
|
||||
|
|
@ -174,6 +176,7 @@ class WebsiteService:
|
|||
def _crawl_with_firecrawl(cls, request: CrawlRequest, api_key: str, config: dict) -> dict[str, Any]:
|
||||
firecrawl_app = FirecrawlApp(api_key=api_key, base_url=config.get("base_url"))
|
||||
|
||||
params: dict[str, Any]
|
||||
if not request.options.crawl_sub_pages:
|
||||
params = {
|
||||
"includePaths": [],
|
||||
|
|
@ -188,8 +191,10 @@ class WebsiteService:
|
|||
"limit": request.options.limit,
|
||||
"scrapeOptions": {"onlyMainContent": request.options.only_main_content},
|
||||
}
|
||||
if request.options.max_depth:
|
||||
params["maxDepth"] = request.options.max_depth
|
||||
|
||||
# Add optional prompt for Firecrawl v2 crawl-params compatibility
|
||||
if request.options.prompt:
|
||||
params["prompt"] = request.options.prompt
|
||||
|
||||
job_id = firecrawl_app.crawl_url(request.url, params)
|
||||
website_crawl_time_cache_key = f"website_crawl_{job_id}"
|
||||
|
|
|
|||
|
|
@ -35,11 +35,15 @@ class TestRedisChannel:
|
|||
"""Test sending a command to Redis."""
|
||||
mock_redis = MagicMock()
|
||||
mock_pipe = MagicMock()
|
||||
mock_redis.pipeline.return_value.__enter__ = MagicMock(return_value=mock_pipe)
|
||||
mock_redis.pipeline.return_value.__exit__ = MagicMock(return_value=None)
|
||||
context = MagicMock()
|
||||
context.__enter__.return_value = mock_pipe
|
||||
context.__exit__.return_value = None
|
||||
mock_redis.pipeline.return_value = context
|
||||
|
||||
channel = RedisChannel(mock_redis, "test:key", 3600)
|
||||
|
||||
pending_key = "test:key:pending"
|
||||
|
||||
# Create a test command
|
||||
command = GraphEngineCommand(command_type=CommandType.ABORT)
|
||||
|
||||
|
|
@ -55,6 +59,7 @@ class TestRedisChannel:
|
|||
|
||||
# Verify expire was set
|
||||
mock_pipe.expire.assert_called_once_with("test:key", 3600)
|
||||
mock_pipe.set.assert_called_once_with(pending_key, "1", ex=3600)
|
||||
|
||||
# Verify execute was called
|
||||
mock_pipe.execute.assert_called_once()
|
||||
|
|
@ -62,33 +67,48 @@ class TestRedisChannel:
|
|||
def test_fetch_commands_empty(self):
|
||||
"""Test fetching commands when Redis list is empty."""
|
||||
mock_redis = MagicMock()
|
||||
mock_pipe = MagicMock()
|
||||
mock_redis.pipeline.return_value.__enter__ = MagicMock(return_value=mock_pipe)
|
||||
mock_redis.pipeline.return_value.__exit__ = MagicMock(return_value=None)
|
||||
pending_pipe = MagicMock()
|
||||
fetch_pipe = MagicMock()
|
||||
pending_context = MagicMock()
|
||||
fetch_context = MagicMock()
|
||||
pending_context.__enter__.return_value = pending_pipe
|
||||
pending_context.__exit__.return_value = None
|
||||
fetch_context.__enter__.return_value = fetch_pipe
|
||||
fetch_context.__exit__.return_value = None
|
||||
mock_redis.pipeline.side_effect = [pending_context]
|
||||
|
||||
# Simulate empty list
|
||||
mock_pipe.execute.return_value = [[], 1] # Empty list, delete successful
|
||||
# No pending marker
|
||||
pending_pipe.execute.return_value = [None, 0]
|
||||
mock_redis.llen.return_value = 0
|
||||
|
||||
channel = RedisChannel(mock_redis, "test:key")
|
||||
commands = channel.fetch_commands()
|
||||
|
||||
assert commands == []
|
||||
mock_pipe.lrange.assert_called_once_with("test:key", 0, -1)
|
||||
mock_pipe.delete.assert_called_once_with("test:key")
|
||||
mock_redis.pipeline.assert_called_once()
|
||||
fetch_pipe.lrange.assert_not_called()
|
||||
fetch_pipe.delete.assert_not_called()
|
||||
|
||||
def test_fetch_commands_with_abort_command(self):
|
||||
"""Test fetching abort commands from Redis."""
|
||||
mock_redis = MagicMock()
|
||||
mock_pipe = MagicMock()
|
||||
mock_redis.pipeline.return_value.__enter__ = MagicMock(return_value=mock_pipe)
|
||||
mock_redis.pipeline.return_value.__exit__ = MagicMock(return_value=None)
|
||||
pending_pipe = MagicMock()
|
||||
fetch_pipe = MagicMock()
|
||||
pending_context = MagicMock()
|
||||
fetch_context = MagicMock()
|
||||
pending_context.__enter__.return_value = pending_pipe
|
||||
pending_context.__exit__.return_value = None
|
||||
fetch_context.__enter__.return_value = fetch_pipe
|
||||
fetch_context.__exit__.return_value = None
|
||||
mock_redis.pipeline.side_effect = [pending_context, fetch_context]
|
||||
|
||||
# Create abort command data
|
||||
abort_command = AbortCommand()
|
||||
command_json = json.dumps(abort_command.model_dump())
|
||||
|
||||
# Simulate Redis returning one command
|
||||
mock_pipe.execute.return_value = [[command_json.encode()], 1]
|
||||
pending_pipe.execute.return_value = [b"1", 1]
|
||||
fetch_pipe.execute.return_value = [[command_json.encode()], 1]
|
||||
|
||||
channel = RedisChannel(mock_redis, "test:key")
|
||||
commands = channel.fetch_commands()
|
||||
|
|
@ -100,9 +120,15 @@ class TestRedisChannel:
|
|||
def test_fetch_commands_multiple(self):
|
||||
"""Test fetching multiple commands from Redis."""
|
||||
mock_redis = MagicMock()
|
||||
mock_pipe = MagicMock()
|
||||
mock_redis.pipeline.return_value.__enter__ = MagicMock(return_value=mock_pipe)
|
||||
mock_redis.pipeline.return_value.__exit__ = MagicMock(return_value=None)
|
||||
pending_pipe = MagicMock()
|
||||
fetch_pipe = MagicMock()
|
||||
pending_context = MagicMock()
|
||||
fetch_context = MagicMock()
|
||||
pending_context.__enter__.return_value = pending_pipe
|
||||
pending_context.__exit__.return_value = None
|
||||
fetch_context.__enter__.return_value = fetch_pipe
|
||||
fetch_context.__exit__.return_value = None
|
||||
mock_redis.pipeline.side_effect = [pending_context, fetch_context]
|
||||
|
||||
# Create multiple commands
|
||||
command1 = GraphEngineCommand(command_type=CommandType.ABORT)
|
||||
|
|
@ -112,7 +138,8 @@ class TestRedisChannel:
|
|||
command2_json = json.dumps(command2.model_dump())
|
||||
|
||||
# Simulate Redis returning multiple commands
|
||||
mock_pipe.execute.return_value = [[command1_json.encode(), command2_json.encode()], 1]
|
||||
pending_pipe.execute.return_value = [b"1", 1]
|
||||
fetch_pipe.execute.return_value = [[command1_json.encode(), command2_json.encode()], 1]
|
||||
|
||||
channel = RedisChannel(mock_redis, "test:key")
|
||||
commands = channel.fetch_commands()
|
||||
|
|
@ -124,9 +151,15 @@ class TestRedisChannel:
|
|||
def test_fetch_commands_skips_invalid_json(self):
|
||||
"""Test that invalid JSON commands are skipped."""
|
||||
mock_redis = MagicMock()
|
||||
mock_pipe = MagicMock()
|
||||
mock_redis.pipeline.return_value.__enter__ = MagicMock(return_value=mock_pipe)
|
||||
mock_redis.pipeline.return_value.__exit__ = MagicMock(return_value=None)
|
||||
pending_pipe = MagicMock()
|
||||
fetch_pipe = MagicMock()
|
||||
pending_context = MagicMock()
|
||||
fetch_context = MagicMock()
|
||||
pending_context.__enter__.return_value = pending_pipe
|
||||
pending_context.__exit__.return_value = None
|
||||
fetch_context.__enter__.return_value = fetch_pipe
|
||||
fetch_context.__exit__.return_value = None
|
||||
mock_redis.pipeline.side_effect = [pending_context, fetch_context]
|
||||
|
||||
# Mix valid and invalid JSON
|
||||
valid_command = AbortCommand()
|
||||
|
|
@ -134,7 +167,8 @@ class TestRedisChannel:
|
|||
invalid_json = b"invalid json {"
|
||||
|
||||
# Simulate Redis returning mixed valid/invalid commands
|
||||
mock_pipe.execute.return_value = [[invalid_json, valid_json.encode()], 1]
|
||||
pending_pipe.execute.return_value = [b"1", 1]
|
||||
fetch_pipe.execute.return_value = [[invalid_json, valid_json.encode()], 1]
|
||||
|
||||
channel = RedisChannel(mock_redis, "test:key")
|
||||
commands = channel.fetch_commands()
|
||||
|
|
@ -187,13 +221,20 @@ class TestRedisChannel:
|
|||
def test_atomic_fetch_and_clear(self):
|
||||
"""Test that fetch_commands atomically fetches and clears the list."""
|
||||
mock_redis = MagicMock()
|
||||
mock_pipe = MagicMock()
|
||||
mock_redis.pipeline.return_value.__enter__ = MagicMock(return_value=mock_pipe)
|
||||
mock_redis.pipeline.return_value.__exit__ = MagicMock(return_value=None)
|
||||
pending_pipe = MagicMock()
|
||||
fetch_pipe = MagicMock()
|
||||
pending_context = MagicMock()
|
||||
fetch_context = MagicMock()
|
||||
pending_context.__enter__.return_value = pending_pipe
|
||||
pending_context.__exit__.return_value = None
|
||||
fetch_context.__enter__.return_value = fetch_pipe
|
||||
fetch_context.__exit__.return_value = None
|
||||
mock_redis.pipeline.side_effect = [pending_context, fetch_context]
|
||||
|
||||
command = AbortCommand()
|
||||
command_json = json.dumps(command.model_dump())
|
||||
mock_pipe.execute.return_value = [[command_json.encode()], 1]
|
||||
pending_pipe.execute.return_value = [b"1", 1]
|
||||
fetch_pipe.execute.return_value = [[command_json.encode()], 1]
|
||||
|
||||
channel = RedisChannel(mock_redis, "test:key")
|
||||
|
||||
|
|
@ -202,7 +243,29 @@ class TestRedisChannel:
|
|||
assert len(commands) == 1
|
||||
|
||||
# Verify both lrange and delete were called in the pipeline
|
||||
assert mock_pipe.lrange.call_count == 1
|
||||
assert mock_pipe.delete.call_count == 1
|
||||
mock_pipe.lrange.assert_called_with("test:key", 0, -1)
|
||||
mock_pipe.delete.assert_called_with("test:key")
|
||||
assert fetch_pipe.lrange.call_count == 1
|
||||
assert fetch_pipe.delete.call_count == 1
|
||||
fetch_pipe.lrange.assert_called_with("test:key", 0, -1)
|
||||
fetch_pipe.delete.assert_called_with("test:key")
|
||||
|
||||
def test_fetch_commands_without_pending_marker_returns_empty(self):
|
||||
"""Ensure we avoid unnecessary list reads when pending flag is missing."""
|
||||
mock_redis = MagicMock()
|
||||
pending_pipe = MagicMock()
|
||||
fetch_pipe = MagicMock()
|
||||
pending_context = MagicMock()
|
||||
fetch_context = MagicMock()
|
||||
pending_context.__enter__.return_value = pending_pipe
|
||||
pending_context.__exit__.return_value = None
|
||||
fetch_context.__enter__.return_value = fetch_pipe
|
||||
fetch_context.__exit__.return_value = None
|
||||
mock_redis.pipeline.side_effect = [pending_context, fetch_context]
|
||||
|
||||
# Pending flag absent
|
||||
pending_pipe.execute.return_value = [None, 0]
|
||||
channel = RedisChannel(mock_redis, "test:key")
|
||||
commands = channel.fetch_commands()
|
||||
|
||||
assert commands == []
|
||||
mock_redis.llen.assert_not_called()
|
||||
assert mock_redis.pipeline.call_count == 1
|
||||
|
|
|
|||
|
|
@ -0,0 +1,104 @@
|
|||
"""Tests for dispatcher command checking behavior."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import queue
|
||||
from datetime import datetime
|
||||
|
||||
from core.workflow.enums import NodeType, WorkflowNodeExecutionStatus
|
||||
from core.workflow.graph_engine.event_management.event_manager import EventManager
|
||||
from core.workflow.graph_engine.orchestration.dispatcher import Dispatcher
|
||||
from core.workflow.graph_events import NodeRunStartedEvent, NodeRunSucceededEvent
|
||||
from core.workflow.node_events import NodeRunResult
|
||||
|
||||
|
||||
class _StubExecutionCoordinator:
|
||||
"""Stub execution coordinator that tracks command checks."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.command_checks = 0
|
||||
self.scaling_checks = 0
|
||||
self._execution_complete = False
|
||||
self.mark_complete_called = False
|
||||
self.failed = False
|
||||
|
||||
def check_commands(self) -> None:
|
||||
self.command_checks += 1
|
||||
|
||||
def check_scaling(self) -> None:
|
||||
self.scaling_checks += 1
|
||||
|
||||
def is_execution_complete(self) -> bool:
|
||||
return self._execution_complete
|
||||
|
||||
def mark_complete(self) -> None:
|
||||
self.mark_complete_called = True
|
||||
|
||||
def mark_failed(self, error: Exception) -> None: # pragma: no cover - defensive, not triggered in tests
|
||||
self.failed = True
|
||||
|
||||
def set_execution_complete(self) -> None:
|
||||
self._execution_complete = True
|
||||
|
||||
|
||||
class _StubEventHandler:
|
||||
"""Minimal event handler that marks execution complete after handling an event."""
|
||||
|
||||
def __init__(self, coordinator: _StubExecutionCoordinator) -> None:
|
||||
self._coordinator = coordinator
|
||||
self.events = []
|
||||
|
||||
def dispatch(self, event) -> None:
|
||||
self.events.append(event)
|
||||
self._coordinator.set_execution_complete()
|
||||
|
||||
|
||||
def _run_dispatcher_for_event(event) -> int:
|
||||
"""Run the dispatcher loop for a single event and return command check count."""
|
||||
event_queue: queue.Queue = queue.Queue()
|
||||
event_queue.put(event)
|
||||
|
||||
coordinator = _StubExecutionCoordinator()
|
||||
event_handler = _StubEventHandler(coordinator)
|
||||
event_manager = EventManager()
|
||||
|
||||
dispatcher = Dispatcher(
|
||||
event_queue=event_queue,
|
||||
event_handler=event_handler,
|
||||
event_collector=event_manager,
|
||||
execution_coordinator=coordinator,
|
||||
)
|
||||
|
||||
dispatcher._dispatcher_loop()
|
||||
|
||||
return coordinator.command_checks
|
||||
|
||||
|
||||
def _make_started_event() -> NodeRunStartedEvent:
|
||||
return NodeRunStartedEvent(
|
||||
id="start-event",
|
||||
node_id="node-1",
|
||||
node_type=NodeType.CODE,
|
||||
node_title="Test Node",
|
||||
start_at=datetime.utcnow(),
|
||||
)
|
||||
|
||||
|
||||
def _make_succeeded_event() -> NodeRunSucceededEvent:
|
||||
return NodeRunSucceededEvent(
|
||||
id="success-event",
|
||||
node_id="node-1",
|
||||
node_type=NodeType.CODE,
|
||||
node_title="Test Node",
|
||||
start_at=datetime.utcnow(),
|
||||
node_run_result=NodeRunResult(status=WorkflowNodeExecutionStatus.SUCCEEDED),
|
||||
)
|
||||
|
||||
|
||||
def test_dispatcher_checks_commands_after_node_completion() -> None:
|
||||
"""Dispatcher should only check commands after node completion events."""
|
||||
started_checks = _run_dispatcher_for_event(_make_started_event())
|
||||
succeeded_checks = _run_dispatcher_for_event(_make_succeeded_event())
|
||||
|
||||
assert started_checks == 0
|
||||
assert succeeded_checks == 1
|
||||
|
|
@ -132,15 +132,22 @@ class TestRedisStopIntegration:
|
|||
"""Test RedisChannel correctly fetches and deserializes commands."""
|
||||
# Setup
|
||||
mock_redis = MagicMock()
|
||||
mock_pipeline = MagicMock()
|
||||
mock_redis.pipeline.return_value.__enter__ = Mock(return_value=mock_pipeline)
|
||||
mock_redis.pipeline.return_value.__exit__ = Mock(return_value=None)
|
||||
pending_pipe = MagicMock()
|
||||
fetch_pipe = MagicMock()
|
||||
pending_context = MagicMock()
|
||||
fetch_context = MagicMock()
|
||||
pending_context.__enter__.return_value = pending_pipe
|
||||
pending_context.__exit__.return_value = None
|
||||
fetch_context.__enter__.return_value = fetch_pipe
|
||||
fetch_context.__exit__.return_value = None
|
||||
mock_redis.pipeline.side_effect = [pending_context, fetch_context]
|
||||
|
||||
# Mock command data
|
||||
abort_command_json = json.dumps({"command_type": CommandType.ABORT, "reason": "Test abort", "payload": None})
|
||||
|
||||
# Mock pipeline execute to return commands
|
||||
mock_pipeline.execute.return_value = [
|
||||
pending_pipe.execute.return_value = [b"1", 1]
|
||||
fetch_pipe.execute.return_value = [
|
||||
[abort_command_json.encode()], # lrange result
|
||||
True, # delete result
|
||||
]
|
||||
|
|
@ -158,19 +165,29 @@ class TestRedisStopIntegration:
|
|||
assert commands[0].reason == "Test abort"
|
||||
|
||||
# Verify Redis operations
|
||||
mock_pipeline.lrange.assert_called_once_with(channel_key, 0, -1)
|
||||
mock_pipeline.delete.assert_called_once_with(channel_key)
|
||||
pending_pipe.get.assert_called_once_with(f"{channel_key}:pending")
|
||||
pending_pipe.delete.assert_called_once_with(f"{channel_key}:pending")
|
||||
fetch_pipe.lrange.assert_called_once_with(channel_key, 0, -1)
|
||||
fetch_pipe.delete.assert_called_once_with(channel_key)
|
||||
assert mock_redis.pipeline.call_count == 2
|
||||
|
||||
def test_redis_channel_fetch_commands_handles_invalid_json(self):
|
||||
"""Test RedisChannel gracefully handles invalid JSON in commands."""
|
||||
# Setup
|
||||
mock_redis = MagicMock()
|
||||
mock_pipeline = MagicMock()
|
||||
mock_redis.pipeline.return_value.__enter__ = Mock(return_value=mock_pipeline)
|
||||
mock_redis.pipeline.return_value.__exit__ = Mock(return_value=None)
|
||||
pending_pipe = MagicMock()
|
||||
fetch_pipe = MagicMock()
|
||||
pending_context = MagicMock()
|
||||
fetch_context = MagicMock()
|
||||
pending_context.__enter__.return_value = pending_pipe
|
||||
pending_context.__exit__.return_value = None
|
||||
fetch_context.__enter__.return_value = fetch_pipe
|
||||
fetch_context.__exit__.return_value = None
|
||||
mock_redis.pipeline.side_effect = [pending_context, fetch_context]
|
||||
|
||||
# Mock invalid command data
|
||||
mock_pipeline.execute.return_value = [
|
||||
pending_pipe.execute.return_value = [b"1", 1]
|
||||
fetch_pipe.execute.return_value = [
|
||||
[b"invalid json", b'{"command_type": "invalid_type"}'], # lrange result
|
||||
True, # delete result
|
||||
]
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next'
|
|||
import MenuItem from './menu-item'
|
||||
import { RiDeleteBinLine, RiEditLine, RiFileDownloadLine } from '@remixicon/react'
|
||||
import Divider from '../../base/divider'
|
||||
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
|
||||
|
||||
type MenuProps = {
|
||||
showDelete: boolean
|
||||
|
|
@ -18,6 +19,7 @@ const Menu = ({
|
|||
detectIsUsedByApp,
|
||||
}: MenuProps) => {
|
||||
const { t } = useTranslation()
|
||||
const runtimeMode = useDatasetDetailContextWithSelector(state => state.dataset?.runtime_mode)
|
||||
|
||||
return (
|
||||
<div className='flex w-[200px] flex-col rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg shadow-shadow-shadow-5 backdrop-blur-[5px]'>
|
||||
|
|
@ -27,11 +29,13 @@ const Menu = ({
|
|||
name={t('common.operation.edit')}
|
||||
handleClick={openRenameModal}
|
||||
/>
|
||||
<MenuItem
|
||||
Icon={RiFileDownloadLine}
|
||||
name={t('datasetPipeline.operations.exportPipeline')}
|
||||
handleClick={handleExportPipeline}
|
||||
/>
|
||||
{runtimeMode === 'rag_pipeline' && (
|
||||
<MenuItem
|
||||
Icon={RiFileDownloadLine}
|
||||
name={t('datasetPipeline.operations.exportPipeline')}
|
||||
handleClick={handleExportPipeline}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{showDelete && (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
import type { SVGProps } from 'react'
|
||||
|
||||
const CitationIcon = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M7 6h10M7 12h6M7 18h10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M5 6c0-1.105.895-2 2-2h10c1.105 0 2 .895 2 2v12c0 1.105-.895 2-2 2H9l-4 3v-3H7"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
fill="none"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default CitationIcon
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
|
||||
const MoreLikeThisIcon: FC = () => {
|
||||
return (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M5.83914 0.666748H10.1609C10.6975 0.666741 11.1404 0.666734 11.5012 0.696212C11.8759 0.726829 12.2204 0.792538 12.544 0.957399C13.0457 1.21306 13.4537 1.62101 13.7093 2.12277C13.8742 2.44633 13.9399 2.7908 13.9705 3.16553C14 3.52633 14 3.96923 14 4.50587V7.41171C14 7.62908 14 7.73776 13.9652 7.80784C13.9303 7.87806 13.8939 7.91566 13.8249 7.95288C13.756 7.99003 13.6262 7.99438 13.3665 8.00307C12.8879 8.01909 12.4204 8.14633 11.997 8.36429C10.9478 7.82388 9.62021 7.82912 8.53296 8.73228C7.15064 9.88056 6.92784 11.8645 8.0466 13.2641C8.36602 13.6637 8.91519 14.1949 9.40533 14.6492C9.49781 14.7349 9.54405 14.7777 9.5632 14.8041C9.70784 15.003 9.5994 15.2795 9.35808 15.3271C9.32614 15.3334 9.26453 15.3334 9.14129 15.3334H5.83912C5.30248 15.3334 4.85958 15.3334 4.49878 15.304C4.12405 15.2733 3.77958 15.2076 3.45603 15.0428C2.95426 14.7871 2.54631 14.3792 2.29065 13.8774C2.12579 13.5538 2.06008 13.2094 2.02946 12.8346C1.99999 12.4738 1.99999 12.0309 2 11.4943V4.50587C1.99999 3.96924 1.99999 3.52632 2.02946 3.16553C2.06008 2.7908 2.12579 2.44633 2.29065 2.12277C2.54631 1.62101 2.95426 1.21306 3.45603 0.957399C3.77958 0.792538 4.12405 0.726829 4.49878 0.696212C4.85957 0.666734 5.3025 0.666741 5.83914 0.666748ZM4.66667 5.33342C4.29848 5.33342 4 5.63189 4 6.00008C4 6.36827 4.29848 6.66675 4.66667 6.66675H8.66667C9.03486 6.66675 9.33333 6.36827 9.33333 6.00008C9.33333 5.63189 9.03486 5.33342 8.66667 5.33342H4.66667ZM4 8.66675C4 8.29856 4.29848 8.00008 4.66667 8.00008H6C6.36819 8.00008 6.66667 8.29856 6.66667 8.66675C6.66667 9.03494 6.36819 9.33342 6 9.33342H4.66667C4.29848 9.33342 4 9.03494 4 8.66675ZM4.66667 2.66675C4.29848 2.66675 4 2.96523 4 3.33342C4 3.7016 4.29848 4.00008 4.66667 4.00008H10.6667C11.0349 4.00008 11.3333 3.7016 11.3333 3.33342C11.3333 2.96523 11.0349 2.66675 10.6667 2.66675H4.66667Z" fill="#DD2590" />
|
||||
<path d="M11.9977 10.0256C11.3313 9.26808 10.2199 9.06432 9.3849 9.75796C8.54988 10.4516 8.43232 11.6113 9.08807 12.4317C9.58479 13.0531 10.9986 14.3025 11.655 14.8719C11.7744 14.9754 11.8341 15.0272 11.9037 15.0477C11.9642 15.0654 12.0312 15.0654 12.0917 15.0477C12.1613 15.0272 12.221 14.9754 12.3404 14.8719C12.9968 14.3025 14.4106 13.0531 14.9074 12.4317C15.5631 11.6113 15.4599 10.4443 14.6105 9.75796C13.7612 9.07161 12.6642 9.26808 11.9977 10.0256Z" fill="#DD2590" />
|
||||
</svg>
|
||||
|
||||
)
|
||||
}
|
||||
export default React.memo(MoreLikeThisIcon)
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
|
||||
const SuggestedQuestionsAfterAnswerIcon: FC = () => {
|
||||
return (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M10.8275 1.33325H5.17245C4.63581 1.33324 4.19289 1.33324 3.8321 1.36272C3.45737 1.39333 3.1129 1.45904 2.78934 1.6239C2.28758 1.87956 1.87963 2.28751 1.62397 2.78928C1.45911 3.11284 1.3934 3.4573 1.36278 3.83204C1.3333 4.19283 1.33331 4.63574 1.33332 5.17239L1.33328 9.42497C1.333 9.95523 1.33278 10.349 1.42418 10.6901C1.67076 11.6103 2.38955 12.3291 3.3098 12.5757C3.51478 12.6306 3.73878 12.6525 3.99998 12.6611L3.99998 13.5806C3.99995 13.7374 3.99992 13.8973 4.01182 14.0283C4.0232 14.1536 4.05333 14.3901 4.21844 14.5969C4.40843 14.8349 4.69652 14.9734 5.00106 14.973C5.26572 14.9728 5.46921 14.8486 5.57416 14.7792C5.6839 14.7066 5.80872 14.6067 5.93117 14.5087L7.53992 13.2217C7.88564 12.9451 7.98829 12.8671 8.09494 12.8126C8.20192 12.7579 8.3158 12.718 8.43349 12.6938C8.55081 12.6697 8.67974 12.6666 9.12248 12.6666H10.8275C11.3642 12.6666 11.8071 12.6666 12.1679 12.6371C12.5426 12.6065 12.8871 12.5408 13.2106 12.3759C13.7124 12.1203 14.1203 11.7123 14.376 11.2106C14.5409 10.887 14.6066 10.5425 14.6372 10.1678C14.6667 9.80701 14.6667 9.36411 14.6667 8.82747V5.17237C14.6667 4.63573 14.6667 4.19283 14.6372 3.83204C14.6066 3.4573 14.5409 3.11284 14.376 2.78928C14.1203 2.28751 13.7124 1.87956 13.2106 1.6239C12.8871 1.45904 12.5426 1.39333 12.1679 1.36272C11.8071 1.33324 11.3642 1.33324 10.8275 1.33325ZM8.99504 4.99992C8.99504 4.44763 9.44275 3.99992 9.99504 3.99992C10.5473 3.99992 10.995 4.44763 10.995 4.99992C10.995 5.5522 10.5473 5.99992 9.99504 5.99992C9.44275 5.99992 8.99504 5.5522 8.99504 4.99992ZM4.92837 7.79996C5.222 7.57974 5.63816 7.63837 5.85961 7.93051C5.90071 7.98295 5.94593 8.03229 5.99199 8.08035C6.09019 8.18282 6.23775 8.32184 6.42882 8.4608C6.81353 8.74059 7.3454 8.99996 7.99504 8.99996C8.64469 8.99996 9.17655 8.74059 9.56126 8.4608C9.75233 8.32184 9.89989 8.18282 9.99809 8.08035C10.0441 8.0323 10.0894 7.98294 10.1305 7.93051C10.3519 7.63837 10.7681 7.57974 11.0617 7.79996C11.3563 8.02087 11.416 8.43874 11.195 8.73329C11.1967 8.73112 11.1928 8.7361 11.186 8.74466C11.1697 8.7651 11.1372 8.80597 11.1261 8.81916C11.087 8.86575 11.0317 8.92884 10.9607 9.00289C10.8194 9.15043 10.6128 9.34474 10.3455 9.53912C9.81353 9.92599 9.01206 10.3333 7.99504 10.3333C6.97802 10.3333 6.17655 9.92599 5.64459 9.53912C5.37733 9.34474 5.17072 9.15043 5.02934 9.00289C4.95837 8.92884 4.90305 8.86575 4.86395 8.81916C4.84438 8.79585 4.82881 8.77659 4.81731 8.76207C4.58702 8.46455 4.61798 8.03275 4.92837 7.79996ZM5.99504 3.99992C5.44275 3.99992 4.99504 4.44763 4.99504 4.99992C4.99504 5.5522 5.44275 5.99992 5.99504 5.99992C6.54732 5.99992 6.99504 5.5522 6.99504 4.99992C6.99504 4.44763 6.54732 3.99992 5.99504 3.99992Z" fill="#06AED4" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
export default React.memo(SuggestedQuestionsAfterAnswerIcon)
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import produce from 'immer'
|
||||
import { ReactSortable } from 'react-sortablejs'
|
||||
import Panel from '../base/feature-panel'
|
||||
import EditModal from './config-modal'
|
||||
import VarItem from './var-item'
|
||||
|
|
@ -22,6 +23,7 @@ import { useModalContext } from '@/context/modal-context'
|
|||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import type { InputVar } from '@/app/components/workflow/types'
|
||||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
export const ADD_EXTERNAL_DATA_TOOL = 'ADD_EXTERNAL_DATA_TOOL'
|
||||
|
||||
|
|
@ -218,6 +220,16 @@ const ConfigVar: FC<IConfigVarProps> = ({ promptVariables, readonly, onPromptVar
|
|||
|
||||
showEditModal()
|
||||
}
|
||||
|
||||
const promptVariablesWithIds = useMemo(() => promptVariables.map((item) => {
|
||||
return {
|
||||
id: item.key,
|
||||
variable: { ...item },
|
||||
}
|
||||
}), [promptVariables])
|
||||
|
||||
const canDrag = !readonly && promptVariables.length > 1
|
||||
|
||||
return (
|
||||
<Panel
|
||||
className="mt-2"
|
||||
|
|
@ -245,18 +257,32 @@ const ConfigVar: FC<IConfigVarProps> = ({ promptVariables, readonly, onPromptVar
|
|||
)}
|
||||
{hasVar && (
|
||||
<div className='mt-1 px-3 pb-3'>
|
||||
{promptVariables.map(({ key, name, type, required, config, icon, icon_background }, index) => (
|
||||
<VarItem
|
||||
key={index}
|
||||
readonly={readonly}
|
||||
name={key}
|
||||
label={name}
|
||||
required={!!required}
|
||||
type={type}
|
||||
onEdit={() => handleConfig({ type, key, index, name, config, icon, icon_background })}
|
||||
onRemove={() => handleRemoveVar(index)}
|
||||
/>
|
||||
))}
|
||||
<ReactSortable
|
||||
className='space-y-1'
|
||||
list={promptVariablesWithIds}
|
||||
setList={(list) => { onPromptVariablesChange?.(list.map(item => item.variable)) }}
|
||||
handle='.handle'
|
||||
ghostClass='opacity-50'
|
||||
animation={150}
|
||||
>
|
||||
{promptVariablesWithIds.map((item, index) => {
|
||||
const { key, name, type, required, config, icon, icon_background } = item.variable
|
||||
return (
|
||||
<VarItem
|
||||
className={cn(canDrag && 'handle')}
|
||||
key={key}
|
||||
readonly={readonly}
|
||||
name={key}
|
||||
label={name}
|
||||
required={!!required}
|
||||
type={type}
|
||||
onEdit={() => handleConfig({ type, key, index, name, config, icon, icon_background })}
|
||||
onRemove={() => handleRemoveVar(index)}
|
||||
canDrag={canDrag}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</ReactSortable>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import type { FC } from 'react'
|
|||
import React, { useState } from 'react'
|
||||
import {
|
||||
RiDeleteBinLine,
|
||||
RiDraggable,
|
||||
RiEditLine,
|
||||
} from '@remixicon/react'
|
||||
import type { IInputTypeIconProps } from './input-type-icon'
|
||||
|
|
@ -12,6 +13,7 @@ import Badge from '@/app/components/base/badge'
|
|||
import cn from '@/utils/classnames'
|
||||
|
||||
type ItemProps = {
|
||||
className?: string
|
||||
readonly?: boolean
|
||||
name: string
|
||||
label: string
|
||||
|
|
@ -19,9 +21,11 @@ type ItemProps = {
|
|||
type: string
|
||||
onEdit: () => void
|
||||
onRemove: () => void
|
||||
canDrag?: boolean
|
||||
}
|
||||
|
||||
const VarItem: FC<ItemProps> = ({
|
||||
className,
|
||||
readonly,
|
||||
name,
|
||||
label,
|
||||
|
|
@ -29,12 +33,16 @@ const VarItem: FC<ItemProps> = ({
|
|||
type,
|
||||
onEdit,
|
||||
onRemove,
|
||||
canDrag,
|
||||
}) => {
|
||||
const [isDeleting, setIsDeleting] = useState(false)
|
||||
|
||||
return (
|
||||
<div className={cn('group relative mb-1 flex h-[34px] w-full items-center rounded-lg border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg pl-2.5 pr-3 shadow-xs last-of-type:mb-0 hover:bg-components-panel-on-panel-item-bg-hover hover:shadow-sm', isDeleting && 'border-state-destructive-border hover:bg-state-destructive-hover', readonly && 'cursor-not-allowed opacity-30')}>
|
||||
<VarIcon className='mr-1 h-4 w-4 shrink-0 text-text-accent' />
|
||||
<div className={cn('group relative mb-1 flex h-[34px] w-full items-center rounded-lg border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg pl-2.5 pr-3 shadow-xs last-of-type:mb-0 hover:bg-components-panel-on-panel-item-bg-hover hover:shadow-sm', isDeleting && 'border-state-destructive-border hover:bg-state-destructive-hover', readonly && 'cursor-not-allowed opacity-30', className)}>
|
||||
<VarIcon className={cn('mr-1 h-4 w-4 shrink-0 text-text-accent', canDrag && 'group-hover:opacity-0')} />
|
||||
{canDrag && (
|
||||
<RiDraggable className='absolute left-3 top-3 hidden h-3 w-3 cursor-pointer text-text-tertiary group-hover:block' />
|
||||
)}
|
||||
<div className='flex w-0 grow items-center'>
|
||||
<div className='truncate' title={`${name} · ${label}`}>
|
||||
<span className='system-sm-medium text-text-secondary'>{name}</span>
|
||||
|
|
|
|||
|
|
@ -1,96 +0,0 @@
|
|||
import React, { useEffect } from 'react'
|
||||
|
||||
function useFeature({
|
||||
introduction,
|
||||
setIntroduction,
|
||||
moreLikeThis,
|
||||
setMoreLikeThis,
|
||||
suggestedQuestionsAfterAnswer,
|
||||
setSuggestedQuestionsAfterAnswer,
|
||||
speechToText,
|
||||
setSpeechToText,
|
||||
textToSpeech,
|
||||
setTextToSpeech,
|
||||
citation,
|
||||
setCitation,
|
||||
annotation,
|
||||
setAnnotation,
|
||||
moderation,
|
||||
setModeration,
|
||||
}: {
|
||||
introduction: string
|
||||
setIntroduction: (introduction: string) => void
|
||||
moreLikeThis: boolean
|
||||
setMoreLikeThis: (moreLikeThis: boolean) => void
|
||||
suggestedQuestionsAfterAnswer: boolean
|
||||
setSuggestedQuestionsAfterAnswer: (suggestedQuestionsAfterAnswer: boolean) => void
|
||||
speechToText: boolean
|
||||
setSpeechToText: (speechToText: boolean) => void
|
||||
textToSpeech: boolean
|
||||
setTextToSpeech: (textToSpeech: boolean) => void
|
||||
citation: boolean
|
||||
setCitation: (citation: boolean) => void
|
||||
annotation: boolean
|
||||
setAnnotation: (annotation: boolean) => void
|
||||
moderation: boolean
|
||||
setModeration: (moderation: boolean) => void
|
||||
}) {
|
||||
const [tempShowOpeningStatement, setTempShowOpeningStatement] = React.useState(!!introduction)
|
||||
useEffect(() => {
|
||||
// wait to api data back
|
||||
if (introduction)
|
||||
setTempShowOpeningStatement(true)
|
||||
}, [introduction])
|
||||
|
||||
// const [tempMoreLikeThis, setTempMoreLikeThis] = React.useState(moreLikeThis)
|
||||
// useEffect(() => {
|
||||
// setTempMoreLikeThis(moreLikeThis)
|
||||
// }, [moreLikeThis])
|
||||
|
||||
const featureConfig = {
|
||||
openingStatement: tempShowOpeningStatement,
|
||||
moreLikeThis,
|
||||
suggestedQuestionsAfterAnswer,
|
||||
speechToText,
|
||||
textToSpeech,
|
||||
citation,
|
||||
annotation,
|
||||
moderation,
|
||||
}
|
||||
const handleFeatureChange = (key: string, value: boolean) => {
|
||||
switch (key) {
|
||||
case 'openingStatement':
|
||||
if (!value)
|
||||
setIntroduction('')
|
||||
|
||||
setTempShowOpeningStatement(value)
|
||||
break
|
||||
case 'moreLikeThis':
|
||||
setMoreLikeThis(value)
|
||||
break
|
||||
case 'suggestedQuestionsAfterAnswer':
|
||||
setSuggestedQuestionsAfterAnswer(value)
|
||||
break
|
||||
case 'speechToText':
|
||||
setSpeechToText(value)
|
||||
break
|
||||
case 'textToSpeech':
|
||||
setTextToSpeech(value)
|
||||
break
|
||||
case 'citation':
|
||||
setCitation(value)
|
||||
break
|
||||
case 'annotation':
|
||||
setAnnotation(value)
|
||||
break
|
||||
case 'moderation':
|
||||
setModeration(value)
|
||||
}
|
||||
}
|
||||
return {
|
||||
featureConfig,
|
||||
handleFeatureChange,
|
||||
}
|
||||
}
|
||||
|
||||
export default useFeature
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
type Props = {
|
||||
onReturnToSimpleMode: () => void
|
||||
}
|
||||
|
||||
const AdvancedModeWarning: FC<Props> = ({
|
||||
onReturnToSimpleMode,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
const [show, setShow] = React.useState(true)
|
||||
if (!show)
|
||||
return null
|
||||
return (
|
||||
<div className='mb-3 rounded-xl border border-[#FEF0C7] bg-[#FFFAEB] px-4 py-3' >
|
||||
<div className='mb-2 text-xs font-bold leading-[18px] text-[#DC6803]'>{t('appDebug.promptMode.advancedWarning.title')}</div>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='text-xs leading-[18px] '>
|
||||
<span className='text-gray-700'>{t('appDebug.promptMode.advancedWarning.description')}</span>
|
||||
<a
|
||||
className='font-medium text-[#155EEF]'
|
||||
href={docLink('/guides/features/prompt-engineering')}
|
||||
target='_blank' rel='noopener noreferrer'
|
||||
>
|
||||
{t('appDebug.promptMode.advancedWarning.learnMore')}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center space-x-1'>
|
||||
<div
|
||||
onClick={onReturnToSimpleMode}
|
||||
className='flex h-6 shrink-0 cursor-pointer items-center space-x-1 rounded-lg border border-gray-200 bg-indigo-600 px-2 text-xs font-semibold text-white shadow-xs'
|
||||
>
|
||||
<div className='text-xs font-semibold uppercase'>{t('appDebug.promptMode.switchBack')}</div>
|
||||
</div>
|
||||
<div
|
||||
className='flex h-6 cursor-pointer items-center rounded-md border border-gray-200 bg-[#fff] px-2 text-xs font-medium text-primary-600 shadow-xs'
|
||||
onClick={() => setShow(false)}
|
||||
>{t('appDebug.promptMode.advancedWarning.ok')}</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(AdvancedModeWarning)
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
import { useEffect, useRef } from 'react'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type AutoHeightTextareaProps
|
||||
= & React.DetailedHTMLProps<React.TextareaHTMLAttributes<HTMLTextAreaElement>, HTMLTextAreaElement>
|
||||
& { outerClassName?: string }
|
||||
|
||||
const AutoHeightTextarea = (
|
||||
{
|
||||
ref: outRef,
|
||||
outerClassName,
|
||||
value,
|
||||
className,
|
||||
placeholder,
|
||||
autoFocus,
|
||||
disabled,
|
||||
...rest
|
||||
}: AutoHeightTextareaProps & {
|
||||
ref: React.RefObject<HTMLTextAreaElement>;
|
||||
},
|
||||
) => {
|
||||
const innerRef = useRef<HTMLTextAreaElement>(null)
|
||||
const ref = outRef || innerRef
|
||||
|
||||
useEffect(() => {
|
||||
if (autoFocus && !disabled && value) {
|
||||
if (typeof ref !== 'function') {
|
||||
ref.current?.setSelectionRange(`${value}`.length, `${value}`.length)
|
||||
ref.current?.focus()
|
||||
}
|
||||
}
|
||||
}, [autoFocus, disabled, ref])
|
||||
return (
|
||||
(<div className={outerClassName}>
|
||||
<div className='relative'>
|
||||
<div className={cn(className, 'invisible whitespace-pre-wrap break-all')}>
|
||||
{!value ? placeholder : `${value}`.replace(/\n$/, '\n ')}
|
||||
</div>
|
||||
<textarea
|
||||
ref={ref}
|
||||
placeholder={placeholder}
|
||||
className={cn(className, 'absolute inset-0 h-full w-full resize-none appearance-none border-none outline-none disabled:bg-transparent')}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
{...rest}
|
||||
/>
|
||||
</div>
|
||||
</div>)
|
||||
)
|
||||
}
|
||||
|
||||
AutoHeightTextarea.displayName = 'AutoHeightTextarea'
|
||||
|
||||
export default AutoHeightTextarea
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type Props = {
|
||||
isRequest: boolean
|
||||
toolName: string
|
||||
content: string
|
||||
}
|
||||
|
||||
const Panel: FC<Props> = ({
|
||||
isRequest,
|
||||
toolName,
|
||||
content,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='overflow-hidden rounded-md border border-black/5 bg-gray-100'>
|
||||
<div className='flex items-center bg-gray-50 px-2 py-1 text-xs font-medium uppercase leading-[18px] text-gray-500'>
|
||||
{t(`tools.thought.${isRequest ? 'requestTitle' : 'responseTitle'}`)} {toolName}
|
||||
</div>
|
||||
<div className='border-t border-black/5 p-2 text-xs leading-4 text-gray-700'>{content}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Panel)
|
||||
|
|
@ -1,106 +0,0 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import {
|
||||
RiArrowDownSLine,
|
||||
RiLoader2Line,
|
||||
} from '@remixicon/react'
|
||||
import type { ToolInfoInThought } from '../type'
|
||||
import Panel from './panel'
|
||||
import cn from '@/utils/classnames'
|
||||
import { CheckCircle } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import { DataSet as DataSetIcon } from '@/app/components/base/icons/src/public/thought'
|
||||
import type { Emoji } from '@/app/components/tools/types'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
|
||||
type Props = {
|
||||
payload: ToolInfoInThought
|
||||
allToolIcons?: Record<string, string | Emoji>
|
||||
}
|
||||
|
||||
const getIcon = (toolName: string, allToolIcons: Record<string, string | Emoji>) => {
|
||||
if (toolName.startsWith('dataset_'))
|
||||
return <DataSetIcon className='shrink-0'></DataSetIcon>
|
||||
const icon = allToolIcons[toolName]
|
||||
if (!icon)
|
||||
return null
|
||||
return (
|
||||
typeof icon === 'string'
|
||||
? (
|
||||
<div
|
||||
className='h-3 w-3 shrink-0 rounded-[3px] bg-cover bg-center'
|
||||
style={{
|
||||
backgroundImage: `url(${icon})`,
|
||||
}}
|
||||
></div>
|
||||
)
|
||||
: (
|
||||
<AppIcon
|
||||
className='shrink-0 rounded-[3px]'
|
||||
size='xs'
|
||||
icon={icon?.content}
|
||||
background={icon?.background}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
||||
const Tool: FC<Props> = ({
|
||||
payload,
|
||||
allToolIcons = {},
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { name, label, input, isFinished, output } = payload
|
||||
const toolName = name.startsWith('dataset_') ? t('dataset.knowledge') : name
|
||||
const toolLabel = name.startsWith('dataset_') ? t('dataset.knowledge') : label
|
||||
const [isShowDetail, setIsShowDetail] = useState(false)
|
||||
const icon = getIcon(name, allToolIcons) as any
|
||||
return (
|
||||
<div>
|
||||
<div className={cn(!isShowDetail && 'shadow-sm', !isShowDetail && 'inline-block', 'max-w-full overflow-x-auto rounded-md bg-white')}>
|
||||
<div
|
||||
className={cn('flex h-7 cursor-pointer items-center px-2')}
|
||||
onClick={() => setIsShowDetail(!isShowDetail)}
|
||||
>
|
||||
{!isFinished && (
|
||||
<RiLoader2Line className='h-3 w-3 shrink-0 animate-spin text-gray-500' />
|
||||
)}
|
||||
{isFinished && !isShowDetail && (
|
||||
<CheckCircle className='h-3 w-3 shrink-0 text-[#12B76A]' />
|
||||
)}
|
||||
{isFinished && isShowDetail && (
|
||||
icon
|
||||
)}
|
||||
<span className='mx-1 shrink-0 text-xs font-medium text-gray-500'>
|
||||
{t(`tools.thought.${isFinished ? 'used' : 'using'}`)}
|
||||
</span>
|
||||
<span
|
||||
className='truncate text-xs font-medium text-gray-700'
|
||||
title={toolLabel}
|
||||
>
|
||||
{toolLabel}
|
||||
</span>
|
||||
<RiArrowDownSLine
|
||||
className={cn(isShowDetail && 'rotate-180', 'ml-1 h-3 w-3 shrink-0 cursor-pointer select-none text-gray-500')}
|
||||
/>
|
||||
</div>
|
||||
{isShowDetail && (
|
||||
<div className='space-y-2 border-t border-black/5 p-2 '>
|
||||
<Panel
|
||||
isRequest={true}
|
||||
toolName={toolName}
|
||||
content={input} />
|
||||
{output && (
|
||||
<Panel
|
||||
isRequest={false}
|
||||
toolName={toolName}
|
||||
content={output as string} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Tool)
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
'use client'
|
||||
import { useState } from 'react'
|
||||
import { t } from 'i18next'
|
||||
import { debounce } from 'lodash-es'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import s from './style.module.css'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
type ICopyBtnProps = {
|
||||
value: string
|
||||
className?: string
|
||||
isPlain?: boolean
|
||||
}
|
||||
|
||||
const CopyBtn = ({
|
||||
value,
|
||||
className,
|
||||
isPlain,
|
||||
}: ICopyBtnProps) => {
|
||||
const [isCopied, setIsCopied] = useState(false)
|
||||
|
||||
const onClickCopy = debounce(() => {
|
||||
copy(value)
|
||||
setIsCopied(true)
|
||||
}, 100)
|
||||
|
||||
const onMouseLeave = debounce(() => {
|
||||
setIsCopied(false)
|
||||
}, 100)
|
||||
|
||||
return (
|
||||
<div className={`${className}`}>
|
||||
<Tooltip
|
||||
popupContent={(isCopied ? t('appApi.copied') : t('appApi.copy'))}
|
||||
asChild={false}
|
||||
>
|
||||
<div
|
||||
onMouseLeave={onMouseLeave}
|
||||
className={'box-border flex cursor-pointer items-center justify-center rounded-md bg-components-button-secondary-bg p-0.5'}
|
||||
style={!isPlain
|
||||
? {
|
||||
boxShadow: '0px 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06)',
|
||||
}
|
||||
: {}}
|
||||
onClick={onClickCopy}
|
||||
>
|
||||
<div className={`h-6 w-6 rounded-md hover:bg-components-button-secondary-bg-hover ${s.copyIcon} ${isCopied ? s.copied : ''}`}></div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CopyBtn
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
.copyIcon {
|
||||
background-image: url(~@/app/components/develop/secret-key/assets/copy.svg);
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.copyIcon:hover {
|
||||
background-image: url(~@/app/components/develop/secret-key/assets/copy-hover.svg);
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.copyIcon.copied {
|
||||
background-image: url(~@/app/components/develop/secret-key/assets/copied.svg);
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
|
||||
type IconProps = {
|
||||
icon: any
|
||||
className?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
const Icon: FC<IconProps> = ({ icon, className, ...other }) => {
|
||||
return (
|
||||
<img src={icon} className={`h-3 w-3 ${className}`} {...other} alt="icon" />
|
||||
)
|
||||
}
|
||||
|
||||
export default Icon
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
import type { FC } from 'react'
|
||||
import type { DividerProps } from '.'
|
||||
import Divider from '.'
|
||||
import classNames from '@/utils/classnames'
|
||||
|
||||
export type DividerWithLabelProps = DividerProps & {
|
||||
label: string
|
||||
}
|
||||
|
||||
export const DividerWithLabel: FC<DividerWithLabelProps> = (props) => {
|
||||
const { label, className, ...rest } = props
|
||||
return <div
|
||||
className="my-2 flex items-center gap-2"
|
||||
>
|
||||
<Divider {...rest} className={classNames('flex-1', className)} />
|
||||
<span className="text-xs text-text-tertiary">
|
||||
{label}
|
||||
</span>
|
||||
<Divider {...rest} className={classNames('flex-1', className)} />
|
||||
</div>
|
||||
}
|
||||
|
||||
export default DividerWithLabel
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
'use client'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import type { PortalToFollowElemOptions } from '@/app/components/base/portal-to-follow-elem'
|
||||
|
||||
type IFloatRightContainerProps = {
|
||||
isMobile: boolean
|
||||
open: boolean
|
||||
toggle: () => void
|
||||
triggerElement?: React.ReactNode
|
||||
children?: React.ReactNode
|
||||
} & PortalToFollowElemOptions
|
||||
|
||||
const FloatRightContainer = ({ open, toggle, triggerElement, isMobile, children, ...portalProps }: IFloatRightContainerProps) => {
|
||||
return (
|
||||
<>
|
||||
{isMobile && (
|
||||
<PortalToFollowElem open={open} {...portalProps}>
|
||||
<PortalToFollowElemTrigger onClick={toggle}>
|
||||
{triggerElement}
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent>
|
||||
{children}
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)}
|
||||
{!isMobile && open && (
|
||||
<>{children}</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default FloatRightContainer
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
import Button from '../button'
|
||||
import { RiInstallLine, RiLoader2Line } from '@remixicon/react'
|
||||
|
||||
type InstallButtonProps = {
|
||||
loading: boolean
|
||||
onInstall: (e: React.MouseEvent) => void
|
||||
t: any
|
||||
}
|
||||
|
||||
const InstallButton = ({ loading, onInstall, t }: InstallButtonProps) => {
|
||||
return (
|
||||
<Button size='small' className='z-[100]' onClick={onInstall}>
|
||||
<div className={`flex items-center justify-center gap-1 px-[3px]
|
||||
${loading ? 'text-components-button-secondary-text-disabled' : 'text-components-button-secondary-text'}
|
||||
system-xs-medium`}
|
||||
>
|
||||
{loading ? t('workflow.nodes.agent.pluginInstaller.installing') : t('workflow.nodes.agent.pluginInstaller.install')}
|
||||
</div>
|
||||
{loading
|
||||
? <RiLoader2Line className='h-3.5 w-3.5 animate-spin text-text-quaternary' />
|
||||
: <RiInstallLine className='h-3.5 w-3.5 text-text-secondary' />
|
||||
}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export default InstallButton
|
||||
|
|
@ -118,8 +118,20 @@ const FormItem: FC<Props> = ({
|
|||
<div className={cn(className)}>
|
||||
{!isArrayLikeType && !isBooleanType && (
|
||||
<div className='system-sm-semibold mb-1 flex h-6 items-center gap-1 text-text-secondary'>
|
||||
<div className='truncate'>{typeof payload.label === 'object' ? nodeKey : payload.label}</div>
|
||||
{!payload.required && <span className='system-xs-regular text-text-tertiary'>{t('workflow.panel.optional')}</span>}
|
||||
<div className='truncate'>
|
||||
{typeof payload.label === 'object' ? nodeKey : payload.label}
|
||||
</div>
|
||||
{payload.hide === true ? (
|
||||
<span className='system-xs-regular text-text-tertiary'>
|
||||
{t('workflow.panel.optional_and_hidden')}
|
||||
</span>
|
||||
) : (
|
||||
!payload.required && (
|
||||
<span className='system-xs-regular text-text-tertiary'>
|
||||
{t('workflow.panel.optional')}
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className='grow'>
|
||||
|
|
|
|||
|
|
@ -61,7 +61,6 @@ const VarList: FC<Props> = ({
|
|||
return
|
||||
}
|
||||
if (list.some(item => item.variable?.trim() === newKey.trim())) {
|
||||
console.log('new key', newKey.trim())
|
||||
setToastHandle(Toast.notify({
|
||||
type: 'error',
|
||||
message: t('appDebug.varKeyError.keyAlreadyExists', { key: newKey }),
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import produce from 'immer'
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import VarItem from './var-item'
|
||||
import { ChangeType, type InputVar, type MoreInfo } from '@/app/components/workflow/types'
|
||||
import { v4 as uuid4 } from 'uuid'
|
||||
import { ReactSortable } from 'react-sortablejs'
|
||||
import { RiDraggable } from '@remixicon/react'
|
||||
import cn from '@/utils/classnames'
|
||||
|
|
@ -71,9 +70,8 @@ const VarList: FC<Props> = ({
|
|||
}, [list, onChange])
|
||||
|
||||
const listWithIds = useMemo(() => list.map((item) => {
|
||||
const id = uuid4()
|
||||
return {
|
||||
id,
|
||||
id: item.variable,
|
||||
variable: { ...item },
|
||||
}
|
||||
}), [list])
|
||||
|
|
@ -88,6 +86,8 @@ const VarList: FC<Props> = ({
|
|||
)
|
||||
}
|
||||
|
||||
const canDrag = !readonly && varCount > 1
|
||||
|
||||
return (
|
||||
<ReactSortable
|
||||
className='space-y-1'
|
||||
|
|
@ -97,30 +97,23 @@ const VarList: FC<Props> = ({
|
|||
ghostClass='opacity-50'
|
||||
animation={150}
|
||||
>
|
||||
{list.map((item, index) => {
|
||||
const canDrag = (() => {
|
||||
if (readonly)
|
||||
return false
|
||||
return varCount > 1
|
||||
})()
|
||||
return (
|
||||
<div key={index} className='group relative'>
|
||||
<VarItem
|
||||
className={cn(canDrag && 'handle')}
|
||||
readonly={readonly}
|
||||
payload={item}
|
||||
onChange={handleVarChange(index)}
|
||||
onRemove={handleVarRemove(index)}
|
||||
varKeys={list.map(item => item.variable)}
|
||||
canDrag={canDrag}
|
||||
/>
|
||||
{canDrag && <RiDraggable className={cn(
|
||||
'handle absolute left-3 top-2.5 hidden h-3 w-3 cursor-pointer text-text-tertiary',
|
||||
'group-hover:block',
|
||||
)} />}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{listWithIds.map((itemWithId, index) => (
|
||||
<div key={itemWithId.id} className='group relative'>
|
||||
<VarItem
|
||||
className={cn(canDrag && 'handle')}
|
||||
readonly={readonly}
|
||||
payload={itemWithId.variable}
|
||||
onChange={handleVarChange(index)}
|
||||
onRemove={handleVarRemove(index)}
|
||||
varKeys={list.map(item => item.variable)}
|
||||
canDrag={canDrag}
|
||||
/>
|
||||
{canDrag && <RiDraggable className={cn(
|
||||
'handle absolute left-3 top-2.5 hidden h-3 w-3 cursor-pointer text-text-tertiary',
|
||||
'group-hover:block',
|
||||
)} />}
|
||||
</div>
|
||||
))}
|
||||
</ReactSortable>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ const DebugAndPreview = () => {
|
|||
const selectedNode = nodes.find(node => node.data.selected)
|
||||
const startNode = nodes.find(node => node.data.type === BlockEnum.Start)
|
||||
const variables = startNode?.data.variables || []
|
||||
const visibleVariables = variables.filter(v => v.hide !== true)
|
||||
const visibleVariables = variables
|
||||
|
||||
const [showConversationVariableModal, setShowConversationVariableModal] = useState(false)
|
||||
|
||||
|
|
|
|||
|
|
@ -14,10 +14,11 @@ import cn from '@/utils/classnames'
|
|||
const UserInput = () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const inputs = useStore(s => s.inputs)
|
||||
const showDebugAndPreviewPanel = useStore(s => s.showDebugAndPreviewPanel)
|
||||
const nodes = useNodes<StartNodeType>()
|
||||
const startNode = nodes.find(node => node.data.type === BlockEnum.Start)
|
||||
const variables = startNode?.data.variables || []
|
||||
const visibleVariables = variables.filter(v => v.hide !== true)
|
||||
const visibleVariables = showDebugAndPreviewPanel ? variables : variables.filter(v => v.hide !== true)
|
||||
|
||||
const handleValueChange = (variable: string, v: string) => {
|
||||
const {
|
||||
|
|
|
|||
|
|
@ -226,6 +226,17 @@ const translation = {
|
|||
updated: 'Aktualisierte',
|
||||
externalKnowledgeBase: 'Externe Wissensdatenbank',
|
||||
createFromPipeline: 'Aus Wissenspipeline erstellen',
|
||||
serviceApi: {
|
||||
card: {
|
||||
title: 'Backend-Dienst-API',
|
||||
apiReference: 'API Referenz',
|
||||
apiKey: 'API-Schlüssel',
|
||||
endpoint: 'Service-API-Endpunkt',
|
||||
},
|
||||
title: 'Service-API',
|
||||
enabled: 'Im Dienst',
|
||||
disabled: 'Behindert',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
|
|||
|
|
@ -326,6 +326,7 @@ const translation = {
|
|||
maximize: 'Maximiere die Leinwand',
|
||||
minimize: 'Vollbildmodus beenden',
|
||||
scrollToSelectedNode: 'Zum ausgewählten Knoten scrollen',
|
||||
optional_and_hidden: '(optional & hidden)',
|
||||
},
|
||||
nodes: {
|
||||
common: {
|
||||
|
|
|
|||
|
|
@ -90,8 +90,8 @@ const translation = {
|
|||
intro2: 'as a context',
|
||||
intro3: ',',
|
||||
intro4: 'or it ',
|
||||
intro5: 'can be created',
|
||||
intro6: ' as a standalone ChatGPT index plug-in to publish',
|
||||
intro5: 'can be published',
|
||||
intro6: ' as an independent service.',
|
||||
unavailable: 'Unavailable',
|
||||
unavailableTip: 'Embedding model is not available, the default embedding model needs to be configured',
|
||||
datasets: 'KNOWLEDGE',
|
||||
|
|
|
|||
|
|
@ -371,6 +371,7 @@ const translation = {
|
|||
maximize: 'Maximize Canvas',
|
||||
minimize: 'Exit Full Screen',
|
||||
scrollToSelectedNode: 'Scroll to selected node',
|
||||
optional_and_hidden: '(optional & hidden)',
|
||||
},
|
||||
nodes: {
|
||||
common: {
|
||||
|
|
|
|||
|
|
@ -226,6 +226,17 @@ const translation = {
|
|||
externalKnowledgeBase: 'Base de conocimientos externa',
|
||||
createFromPipeline: 'Crear desde Knowledge Pipeline',
|
||||
updated: 'Actualizado',
|
||||
serviceApi: {
|
||||
card: {
|
||||
apiReference: 'Referencia de la API',
|
||||
apiKey: 'Clave API',
|
||||
endpoint: 'Punto de enlace de la API de servicio',
|
||||
title: 'API del servicio de backend',
|
||||
},
|
||||
enabled: 'En servicio',
|
||||
title: 'API de servicios',
|
||||
disabled: 'Discapacitado',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
|
|||
|
|
@ -326,6 +326,7 @@ const translation = {
|
|||
maximize: 'Maximizar Canvas',
|
||||
minimize: 'Salir de pantalla completa',
|
||||
scrollToSelectedNode: 'Desplácese hasta el nodo seleccionado',
|
||||
optional_and_hidden: '(opcional y oculto)',
|
||||
},
|
||||
nodes: {
|
||||
common: {
|
||||
|
|
|
|||
|
|
@ -226,6 +226,17 @@ const translation = {
|
|||
updated: 'بروز رسانی',
|
||||
createFromPipeline: 'ایجاد از پایپ لاین دانش',
|
||||
externalKnowledgeBase: 'پایگاه دانش خارجی',
|
||||
serviceApi: {
|
||||
card: {
|
||||
apiKey: 'کلید API',
|
||||
title: 'رابط برنامهنویسی سرویس پشتیبان',
|
||||
apiReference: 'مرجع API',
|
||||
endpoint: 'نقطه انتهایی رابط برنامهنویسی سرویس',
|
||||
},
|
||||
disabled: 'معلول',
|
||||
enabled: 'در حال خدمت',
|
||||
title: 'رابط برنامهنویسی سرویس',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
|
|||
|
|
@ -326,6 +326,7 @@ const translation = {
|
|||
minimize: 'خروج از حالت تمام صفحه',
|
||||
maximize: 'بیشینهسازی بوم',
|
||||
scrollToSelectedNode: 'به گره انتخاب شده بروید',
|
||||
optional_and_hidden: '(اختیاری و پنهان)',
|
||||
},
|
||||
nodes: {
|
||||
common: {
|
||||
|
|
|
|||
|
|
@ -226,6 +226,17 @@ const translation = {
|
|||
updated: 'Actualisé',
|
||||
createFromPipeline: 'Créer à partir du pipeline de connaissances',
|
||||
externalKnowledgeBase: 'Base de connaissances externe',
|
||||
serviceApi: {
|
||||
card: {
|
||||
apiKey: 'Clé API',
|
||||
apiReference: 'Référence API',
|
||||
title: 'API du service backend',
|
||||
endpoint: 'Point de terminaison de l\'API',
|
||||
},
|
||||
enabled: 'En service',
|
||||
title: 'API de service',
|
||||
disabled: 'désactivé',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
|
|||
|
|
@ -326,6 +326,7 @@ const translation = {
|
|||
maximize: 'Maximiser le Canvas',
|
||||
minimize: 'Sortir du mode plein écran',
|
||||
scrollToSelectedNode: 'Faites défiler jusqu’au nœud sélectionné',
|
||||
optional_and_hidden: '(optionnel et caché)',
|
||||
},
|
||||
nodes: {
|
||||
common: {
|
||||
|
|
|
|||
|
|
@ -233,6 +233,17 @@ const translation = {
|
|||
updated: 'अपडेट किया गया',
|
||||
externalKnowledgeBase: 'बाहरी ज्ञान आधार',
|
||||
createFromPipeline: 'ज्ञान पाइपलाइन से बनाएं',
|
||||
serviceApi: {
|
||||
card: {
|
||||
apiReference: 'एपीआई संदर्भ',
|
||||
apiKey: 'एपीआई कुंजी',
|
||||
title: 'बैकएंड सेवा एपीआई',
|
||||
endpoint: 'सेवा एपीआई एंडपॉइंट',
|
||||
},
|
||||
enabled: 'सेवा में',
|
||||
disabled: 'अक्षम',
|
||||
title: 'सेवा एपीआई',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
|
|||
|
|
@ -338,6 +338,7 @@ const translation = {
|
|||
minimize: 'पूर्ण स्क्रीन से बाहर निकलें',
|
||||
maximize: 'कैनवास का अधिकतम लाभ उठाएँ',
|
||||
scrollToSelectedNode: 'चुने गए नोड पर स्क्रॉल करें',
|
||||
optional_and_hidden: '(वैकल्पिक और छिपा हुआ)',
|
||||
},
|
||||
nodes: {
|
||||
common: {
|
||||
|
|
|
|||
|
|
@ -219,6 +219,17 @@ const translation = {
|
|||
updated: 'Diperbarui',
|
||||
createFromPipeline: 'Membuat dari Knowledge Pipeline',
|
||||
externalKnowledgeBase: 'Basis Pengetahuan Eksternal',
|
||||
serviceApi: {
|
||||
card: {
|
||||
apiKey: 'Kunci API',
|
||||
apiReference: 'Referensi API',
|
||||
title: 'API layanan backend',
|
||||
endpoint: 'Titik Akhir API Layanan',
|
||||
},
|
||||
title: 'API Layanan',
|
||||
enabled: 'Sedang Beroperasi',
|
||||
disabled: 'Dinonaktifkan',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
|
|||
|
|
@ -325,6 +325,7 @@ const translation = {
|
|||
changeBlock: 'Ubah Node',
|
||||
runThisStep: 'Jalankan langkah ini',
|
||||
maximize: 'Maksimalkan Kanvas',
|
||||
optional_and_hidden: '(opsional & tersembunyi)',
|
||||
},
|
||||
nodes: {
|
||||
common: {
|
||||
|
|
|
|||
|
|
@ -233,6 +233,17 @@ const translation = {
|
|||
updated: 'Aggiornato',
|
||||
externalKnowledgeBase: 'Base di conoscenza esterna',
|
||||
createFromPipeline: 'Creazione da pipeline di conoscenza',
|
||||
serviceApi: {
|
||||
card: {
|
||||
endpoint: 'Endpoint dell\'API di servizio',
|
||||
apiKey: 'Chiave API',
|
||||
title: 'API del servizio backend',
|
||||
apiReference: 'Riferimento API',
|
||||
},
|
||||
disabled: 'Disabilitato',
|
||||
title: 'API di servizio',
|
||||
enabled: 'In servizio',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
|
|||
|
|
@ -341,6 +341,7 @@ const translation = {
|
|||
minimize: 'Esci dalla modalità schermo intero',
|
||||
maximize: 'Massimizza Canvas',
|
||||
scrollToSelectedNode: 'Scorri fino al nodo selezionato',
|
||||
optional_and_hidden: '(opzionale e nascosto)',
|
||||
},
|
||||
nodes: {
|
||||
common: {
|
||||
|
|
|
|||
|
|
@ -87,8 +87,8 @@ const translation = {
|
|||
intro2: 'コンテキストとして',
|
||||
intro3: '、',
|
||||
intro4: 'または',
|
||||
intro5: '作成することができます',
|
||||
intro6: '単体の ChatGPT インデックスプラグインとして公開するために',
|
||||
intro5: '公開することができます',
|
||||
intro6: '独立したサービスとして',
|
||||
unavailable: '利用不可',
|
||||
unavailableTip: '埋め込みモデルが利用できません。デフォルトの埋め込みモデルを設定する必要があります',
|
||||
datasets: 'ナレッジベース',
|
||||
|
|
|
|||
|
|
@ -367,6 +367,7 @@ const translation = {
|
|||
maximize: 'キャンバスを最大化する',
|
||||
minimize: '全画面を終了する',
|
||||
scrollToSelectedNode: '選択したノードまでスクロール',
|
||||
optional_and_hidden: '(オプションおよび非表示)',
|
||||
},
|
||||
nodes: {
|
||||
common: {
|
||||
|
|
|
|||
|
|
@ -225,6 +225,17 @@ const translation = {
|
|||
updated: '업데이트',
|
||||
externalKnowledgeBase: '외부 기술 자료',
|
||||
createFromPipeline: '지식 파이프라인에서 만들기',
|
||||
serviceApi: {
|
||||
card: {
|
||||
apiReference: 'API 참고',
|
||||
endpoint: '서비스 API 엔드포인트',
|
||||
apiKey: 'API 키',
|
||||
title: '백엔드 서비스 API',
|
||||
},
|
||||
enabled: '서비스 중',
|
||||
title: '서비스 API',
|
||||
disabled: '장애인',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
|
|||
|
|
@ -347,6 +347,7 @@ const translation = {
|
|||
minimize: '전체 화면 종료',
|
||||
maximize: '캔버스 전체 화면',
|
||||
scrollToSelectedNode: '선택한 노드로 스크롤',
|
||||
optional_and_hidden: '(선택 사항 및 숨김)',
|
||||
},
|
||||
nodes: {
|
||||
common: {
|
||||
|
|
|
|||
|
|
@ -232,6 +232,17 @@ const translation = {
|
|||
updated: 'Aktualizowano',
|
||||
createFromPipeline: 'Tworzenie na podstawie potoku wiedzy',
|
||||
externalKnowledgeBase: 'Zewnętrzna baza wiedzy',
|
||||
serviceApi: {
|
||||
card: {
|
||||
apiKey: 'Klucz API',
|
||||
title: 'Usługa backendowa API',
|
||||
apiReference: 'Dokumentacja API',
|
||||
endpoint: 'Punkt końcowy API usługi',
|
||||
},
|
||||
title: 'Interfejs API usługi',
|
||||
disabled: 'Niepełnosprawny',
|
||||
enabled: 'W serwisie',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
|
|||
|
|
@ -326,6 +326,7 @@ const translation = {
|
|||
minimize: 'Wyjdź z trybu pełnoekranowego',
|
||||
maximize: 'Maksymalizuj płótno',
|
||||
scrollToSelectedNode: 'Przewiń do wybranego węzła',
|
||||
optional_and_hidden: '(opcjonalne i ukryte)',
|
||||
},
|
||||
nodes: {
|
||||
common: {
|
||||
|
|
|
|||
|
|
@ -226,6 +226,17 @@ const translation = {
|
|||
updated: 'Atualizado',
|
||||
externalKnowledgeBase: 'Base de conhecimento externa',
|
||||
createFromPipeline: 'Criar a partir do pipeline de conhecimento',
|
||||
serviceApi: {
|
||||
card: {
|
||||
apiKey: 'Chave de API',
|
||||
apiReference: 'Referência da API',
|
||||
title: 'API de serviço de backend',
|
||||
endpoint: 'Endpoint da API de Serviço',
|
||||
},
|
||||
enabled: 'Em serviço',
|
||||
title: 'API de Serviço',
|
||||
disabled: 'Desativado',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
|
|||
|
|
@ -326,6 +326,7 @@ const translation = {
|
|||
maximize: 'Maximize Canvas',
|
||||
minimize: 'Sair do Modo Tela Cheia',
|
||||
scrollToSelectedNode: 'Role até o nó selecionado',
|
||||
optional_and_hidden: '(opcional & oculto)',
|
||||
},
|
||||
nodes: {
|
||||
common: {
|
||||
|
|
|
|||
|
|
@ -226,6 +226,17 @@ const translation = {
|
|||
updated: 'Actualizat',
|
||||
externalKnowledgeBase: 'Baza de cunoștințe externă',
|
||||
createFromPipeline: 'Crearea din Knowledge Pipeline',
|
||||
serviceApi: {
|
||||
card: {
|
||||
title: 'API pentru serviciul backend',
|
||||
apiReference: 'Referință API',
|
||||
endpoint: 'Punct final API de servicii',
|
||||
apiKey: 'Cheie API',
|
||||
},
|
||||
disabled: 'Dezactivat',
|
||||
enabled: 'În serviciu',
|
||||
title: 'API de servicii',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
|
|||
|
|
@ -326,6 +326,7 @@ const translation = {
|
|||
maximize: 'Maximize Canvas',
|
||||
minimize: 'Iesi din modul pe tot ecranul',
|
||||
scrollToSelectedNode: 'Derulați la nodul selectat',
|
||||
optional_and_hidden: '(opțional și ascuns)',
|
||||
},
|
||||
nodes: {
|
||||
common: {
|
||||
|
|
|
|||
|
|
@ -226,6 +226,17 @@ const translation = {
|
|||
updated: 'Обновлено',
|
||||
externalKnowledgeBase: 'Внешняя база знаний',
|
||||
createFromPipeline: 'Создание из конвейера знаний',
|
||||
serviceApi: {
|
||||
card: {
|
||||
apiReference: 'Справочник API',
|
||||
title: 'API бэкенд-сервиса',
|
||||
apiKey: 'API ключ',
|
||||
endpoint: 'Конечная точка API сервиса',
|
||||
},
|
||||
enabled: 'На службе',
|
||||
title: 'Сервисный API',
|
||||
disabled: 'Отключено',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
|
|||
|
|
@ -326,6 +326,7 @@ const translation = {
|
|||
minimize: 'Выйти из полноэкранного режима',
|
||||
maximize: 'Максимизировать холст',
|
||||
scrollToSelectedNode: 'Прокрутите до выбранного узла',
|
||||
optional_and_hidden: '(необязательно и скрыто)',
|
||||
},
|
||||
nodes: {
|
||||
common: {
|
||||
|
|
|
|||
|
|
@ -226,6 +226,17 @@ const translation = {
|
|||
createFromPipeline: 'Ustvarjanje iz cevovoda znanja',
|
||||
updated: 'Posodobljene',
|
||||
externalKnowledgeBase: 'Zunanja baza znanja',
|
||||
serviceApi: {
|
||||
card: {
|
||||
apiKey: 'API ključ',
|
||||
endpoint: 'Vhodna točka API storitve',
|
||||
title: 'API storitev za zaledje',
|
||||
apiReference: 'API Referenca',
|
||||
},
|
||||
title: 'Storitveni API',
|
||||
disabled: 'Onemogočeno',
|
||||
enabled: 'V storitvi',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
|
|||
|
|
@ -333,6 +333,7 @@ const translation = {
|
|||
maximize: 'Maksimiziraj platno',
|
||||
optional: '(neobvezno)',
|
||||
scrollToSelectedNode: 'Pomaknite se do izbranega vozlišča',
|
||||
optional_and_hidden: '(neobvezno in skrito)',
|
||||
},
|
||||
nodes: {
|
||||
common: {
|
||||
|
|
|
|||
|
|
@ -225,6 +225,17 @@ const translation = {
|
|||
updated: 'ปรับ ปรุง',
|
||||
externalKnowledgeBase: 'ฐานความรู้ภายนอก',
|
||||
createFromPipeline: 'สร้างจากไปป์ไลน์ความรู้',
|
||||
serviceApi: {
|
||||
card: {
|
||||
title: 'บริการแบ็กเอนด์ API',
|
||||
apiReference: 'เอกสารอ้างอิง API',
|
||||
apiKey: 'กุญแจ API',
|
||||
endpoint: 'จุดเชื่อมต่อ API บริการ',
|
||||
},
|
||||
enabled: 'ให้บริการ',
|
||||
disabled: 'ถูกปิดใช้งาน',
|
||||
title: 'บริการ API',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
|
|||
|
|
@ -326,6 +326,7 @@ const translation = {
|
|||
minimize: 'ออกจากโหมดเต็มหน้าจอ',
|
||||
maximize: 'เพิ่มประสิทธิภาพผ้าใบ',
|
||||
scrollToSelectedNode: 'เลื่อนไปยังโหนดที่เลือก',
|
||||
optional_and_hidden: '(ตัวเลือก & ซ่อน)',
|
||||
},
|
||||
nodes: {
|
||||
common: {
|
||||
|
|
|
|||
|
|
@ -226,6 +226,17 @@ const translation = {
|
|||
updated: 'Güncel -leştirilmiş',
|
||||
createFromPipeline: 'Bilgi İşlem Hattından Oluşturun',
|
||||
externalKnowledgeBase: 'Harici Bilgi Bankası',
|
||||
serviceApi: {
|
||||
card: {
|
||||
apiReference: 'API Referansı',
|
||||
title: 'Backend servis api',
|
||||
apiKey: 'API Anahtarı',
|
||||
endpoint: 'Hizmet API Uç Noktası',
|
||||
},
|
||||
disabled: 'Engelli',
|
||||
enabled: 'Hizmette',
|
||||
title: 'Servis API\'si',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
|
|||
|
|
@ -326,6 +326,7 @@ const translation = {
|
|||
minimize: 'Tam Ekrandan Çık',
|
||||
maximize: 'Kanvası Maksimize Et',
|
||||
scrollToSelectedNode: 'Seçili düğüme kaydırma',
|
||||
optional_and_hidden: '(isteğe bağlı ve gizli)',
|
||||
},
|
||||
nodes: {
|
||||
common: {
|
||||
|
|
|
|||
|
|
@ -227,6 +227,17 @@ const translation = {
|
|||
updated: 'Оновлено',
|
||||
createFromPipeline: 'Створюйте на основі Knowledge Pipeline',
|
||||
externalKnowledgeBase: 'Зовнішня база знань',
|
||||
serviceApi: {
|
||||
card: {
|
||||
title: 'API бекенд-сервіс',
|
||||
apiReference: 'Посилання на API',
|
||||
apiKey: 'Ключ API',
|
||||
endpoint: 'Кінцева точка API сервісу',
|
||||
},
|
||||
disabled: 'Вимкнено',
|
||||
enabled: 'У службі',
|
||||
title: 'Сервісний API',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
|
|||
|
|
@ -326,6 +326,7 @@ const translation = {
|
|||
minimize: 'Вийти з повноекранного режиму',
|
||||
maximize: 'Максимізувати полотно',
|
||||
scrollToSelectedNode: 'Прокрутіть до вибраного вузла',
|
||||
optional_and_hidden: '(необов\'язково & приховано)',
|
||||
},
|
||||
nodes: {
|
||||
common: {
|
||||
|
|
|
|||
|
|
@ -226,6 +226,17 @@ const translation = {
|
|||
updated: 'Cập nhật',
|
||||
createFromPipeline: 'Tạo từ quy trình kiến thức',
|
||||
externalKnowledgeBase: 'Cơ sở kiến thức bên ngoài',
|
||||
serviceApi: {
|
||||
card: {
|
||||
title: 'API dịch vụ backend',
|
||||
endpoint: 'Điểm cuối API dịch vụ',
|
||||
apiKey: 'Khóa API',
|
||||
apiReference: 'Tham chiếu API',
|
||||
},
|
||||
enabled: 'Đang phục vụ',
|
||||
disabled: 'Vô hiệu hóa',
|
||||
title: 'Giao diện lập trình dịch vụ',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
|
|||
|
|
@ -326,6 +326,7 @@ const translation = {
|
|||
maximize: 'Tối đa hóa Canvas',
|
||||
minimize: 'Thoát chế độ toàn màn hình',
|
||||
scrollToSelectedNode: 'Cuộn đến nút đã chọn',
|
||||
optional_and_hidden: '(tùy chọn & ẩn)',
|
||||
},
|
||||
nodes: {
|
||||
common: {
|
||||
|
|
|
|||
|
|
@ -90,8 +90,8 @@ const translation = {
|
|||
intro2: '作为上下文',
|
||||
intro3: ',',
|
||||
intro4: '或可以',
|
||||
intro5: '创建',
|
||||
intro6: '为独立的 ChatGPT 插件发布使用',
|
||||
intro5: '发布',
|
||||
intro6: '为独立的服务',
|
||||
unavailable: '不可用',
|
||||
unavailableTip: '由于 embedding 模型不可用,需要配置默认 embedding 模型',
|
||||
datasets: '知识库',
|
||||
|
|
|
|||
|
|
@ -370,6 +370,7 @@ const translation = {
|
|||
maximize: '最大化画布',
|
||||
minimize: '退出最大化',
|
||||
scrollToSelectedNode: '滚动至选中节点',
|
||||
optional_and_hidden: '(选填 & 隐藏)',
|
||||
},
|
||||
nodes: {
|
||||
common: {
|
||||
|
|
|
|||
|
|
@ -226,6 +226,17 @@ const translation = {
|
|||
externalKnowledgeBase: '外部知識庫',
|
||||
createFromPipeline: '從知識管線建立',
|
||||
updated: '更新時間',
|
||||
serviceApi: {
|
||||
card: {
|
||||
title: '後端服務 API',
|
||||
apiReference: 'API 參考',
|
||||
endpoint: '服務 API 端點',
|
||||
apiKey: 'API 金鑰',
|
||||
},
|
||||
enabled: '使用中',
|
||||
title: '服務 API',
|
||||
disabled: '已停用',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
|
|||
|
|
@ -326,6 +326,7 @@ const translation = {
|
|||
minimize: '退出全螢幕',
|
||||
maximize: '最大化畫布',
|
||||
scrollToSelectedNode: '捲動至選取的節點',
|
||||
optional_and_hidden: '(可選且隱藏)',
|
||||
},
|
||||
nodes: {
|
||||
common: {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"name": "dify-web",
|
||||
"version": "1.9.1",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.18.2",
|
||||
"packageManager": "pnpm@10.18.3+sha512.bbd16e6d7286fd7e01f6b3c0b3c932cda2965c06a908328f74663f10a9aea51f1129eea615134bf992831b009eabe167ecb7008b597f40ff9bc75946aadfb08d",
|
||||
"engines": {
|
||||
"node": ">=v22.11.0"
|
||||
},
|
||||
|
|
@ -27,6 +27,7 @@
|
|||
"lint:fix": "eslint --cache --cache-location node_modules/.cache/eslint/.eslint-cache --fix",
|
||||
"lint:quiet": "eslint --cache --cache-location node_modules/.cache/eslint/.eslint-cache --quiet",
|
||||
"lint:complexity": "eslint --cache --cache-location node_modules/.cache/eslint/.eslint-cache --rule 'complexity: [error, {max: 15}]' --quiet",
|
||||
"type-check": "tsc --noEmit",
|
||||
"prepare": "cd ../ && node -e \"if (process.env.NODE_ENV !== 'production'){process.exit(1)} \" || husky ./web/.husky",
|
||||
"gen-icons": "node ./app/components/base/icons/script.mjs",
|
||||
"uglify-embed": "node ./bin/uglify-embed",
|
||||
|
|
|
|||
Loading…
Reference in New Issue