Merge remote-tracking branch 'origin/main' into feat/trigger

This commit is contained in:
lyzno1 2025-10-15 20:39:17 +08:00
commit 29353bd7c2
No known key found for this signature in database
73 changed files with 635 additions and 670 deletions

View File

@ -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"):

View File

@ -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

View File

@ -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.

View File

@ -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)

View File

@ -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}"

View File

@ -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

View File

@ -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

View File

@ -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
]

View File

@ -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 && (
<>

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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>
)}

View File

@ -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>

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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);
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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'>

View File

@ -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 }),

View File

@ -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>
)
}

View File

@ -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)

View File

@ -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 {

View File

@ -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

View File

@ -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: {

View File

@ -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',

View File

@ -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: {

View File

@ -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

View File

@ -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: {

View File

@ -226,6 +226,17 @@ const translation = {
updated: 'بروز رسانی',
createFromPipeline: 'ایجاد از پایپ لاین دانش',
externalKnowledgeBase: 'پایگاه دانش خارجی',
serviceApi: {
card: {
apiKey: 'کلید API',
title: 'رابط برنامه‌نویسی سرویس پشتیبان',
apiReference: 'مرجع API',
endpoint: 'نقطه انتهایی رابط برنامه‌نویسی سرویس',
},
disabled: 'معلول',
enabled: 'در حال خدمت',
title: 'رابط برنامه‌نویسی سرویس',
},
}
export default translation

View File

@ -326,6 +326,7 @@ const translation = {
minimize: 'خروج از حالت تمام صفحه',
maximize: 'بیشینه‌سازی بوم',
scrollToSelectedNode: 'به گره انتخاب شده بروید',
optional_and_hidden: '(اختیاری و پنهان)',
},
nodes: {
common: {

View File

@ -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

View File

@ -326,6 +326,7 @@ const translation = {
maximize: 'Maximiser le Canvas',
minimize: 'Sortir du mode plein écran',
scrollToSelectedNode: 'Faites défiler jusquau nœud sélectionné',
optional_and_hidden: '(optionnel et caché)',
},
nodes: {
common: {

View File

@ -233,6 +233,17 @@ const translation = {
updated: 'अपडेट किया गया',
externalKnowledgeBase: 'बाहरी ज्ञान आधार',
createFromPipeline: 'ज्ञान पाइपलाइन से बनाएं',
serviceApi: {
card: {
apiReference: 'एपीआई संदर्भ',
apiKey: 'एपीआई कुंजी',
title: 'बैकएंड सेवा एपीआई',
endpoint: 'सेवा एपीआई एंडपॉइंट',
},
enabled: 'सेवा में',
disabled: 'अक्षम',
title: 'सेवा एपीआई',
},
}
export default translation

View File

@ -338,6 +338,7 @@ const translation = {
minimize: 'पूर्ण स्क्रीन से बाहर निकलें',
maximize: 'कैनवास का अधिकतम लाभ उठाएँ',
scrollToSelectedNode: 'चुने गए नोड पर स्क्रॉल करें',
optional_and_hidden: '(वैकल्पिक और छिपा हुआ)',
},
nodes: {
common: {

View File

@ -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

View File

@ -325,6 +325,7 @@ const translation = {
changeBlock: 'Ubah Node',
runThisStep: 'Jalankan langkah ini',
maximize: 'Maksimalkan Kanvas',
optional_and_hidden: '(opsional & tersembunyi)',
},
nodes: {
common: {

View File

@ -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

View File

@ -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: {

View File

@ -87,8 +87,8 @@ const translation = {
intro2: 'コンテキストとして',
intro3: '、',
intro4: 'または',
intro5: '作成することができます',
intro6: '単体の ChatGPT インデックスプラグインとして公開するために',
intro5: '公開することができます',
intro6: '独立したサービスとして',
unavailable: '利用不可',
unavailableTip: '埋め込みモデルが利用できません。デフォルトの埋め込みモデルを設定する必要があります',
datasets: 'ナレッジベース',

View File

@ -367,6 +367,7 @@ const translation = {
maximize: 'キャンバスを最大化する',
minimize: '全画面を終了する',
scrollToSelectedNode: '選択したノードまでスクロール',
optional_and_hidden: '(オプションおよび非表示)',
},
nodes: {
common: {

View File

@ -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

View File

@ -347,6 +347,7 @@ const translation = {
minimize: '전체 화면 종료',
maximize: '캔버스 전체 화면',
scrollToSelectedNode: '선택한 노드로 스크롤',
optional_and_hidden: '(선택 사항 및 숨김)',
},
nodes: {
common: {

View File

@ -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

View File

@ -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: {

View File

@ -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

View File

@ -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: {

View File

@ -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

View File

@ -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: {

View File

@ -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

View File

@ -326,6 +326,7 @@ const translation = {
minimize: 'Выйти из полноэкранного режима',
maximize: 'Максимизировать холст',
scrollToSelectedNode: 'Прокрутите до выбранного узла',
optional_and_hidden: '(необязательно и скрыто)',
},
nodes: {
common: {

View File

@ -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

View File

@ -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: {

View File

@ -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

View File

@ -326,6 +326,7 @@ const translation = {
minimize: 'ออกจากโหมดเต็มหน้าจอ',
maximize: 'เพิ่มประสิทธิภาพผ้าใบ',
scrollToSelectedNode: 'เลื่อนไปยังโหนดที่เลือก',
optional_and_hidden: '(ตัวเลือก & ซ่อน)',
},
nodes: {
common: {

View File

@ -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

View File

@ -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: {

View File

@ -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

View File

@ -326,6 +326,7 @@ const translation = {
minimize: 'Вийти з повноекранного режиму',
maximize: 'Максимізувати полотно',
scrollToSelectedNode: 'Прокрутіть до вибраного вузла',
optional_and_hidden: '(необов\'язково & приховано)',
},
nodes: {
common: {

View File

@ -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

View File

@ -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: {

View File

@ -90,8 +90,8 @@ const translation = {
intro2: '作为上下文',
intro3: ',',
intro4: '或可以',
intro5: '创建',
intro6: '为独立的 ChatGPT 插件发布使用',
intro5: '发布',
intro6: '为独立的服务',
unavailable: '不可用',
unavailableTip: '由于 embedding 模型不可用,需要配置默认 embedding 模型',
datasets: '知识库',

View File

@ -370,6 +370,7 @@ const translation = {
maximize: '最大化画布',
minimize: '退出最大化',
scrollToSelectedNode: '滚动至选中节点',
optional_and_hidden: '(选填 & 隐藏)',
},
nodes: {
common: {

View File

@ -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

View File

@ -326,6 +326,7 @@ const translation = {
minimize: '退出全螢幕',
maximize: '最大化畫布',
scrollToSelectedNode: '捲動至選取的節點',
optional_and_hidden: '(可選且隱藏)',
},
nodes: {
common: {

View File

@ -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",