mirror of https://github.com/langgenius/dify.git
Merge remote-tracking branch 'origin/main' into feat/migrate-es-toolkit-compat-simple
This commit is contained in:
commit
91bae1f727
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"enabledPlugins": {
|
||||
"feature-dev@claude-plugins-official": true,
|
||||
"context7@claude-plugins-official": true,
|
||||
"typescript-lsp@claude-plugins-official": true,
|
||||
"pyright-lsp@claude-plugins-official": true
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
{
|
||||
"permissions": {
|
||||
"allow": [],
|
||||
"deny": []
|
||||
},
|
||||
"env": {
|
||||
"__comment": "Environment variables for MCP servers. Override in .claude/settings.local.json with actual values.",
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
"enabledMcpjsonServers": [
|
||||
"context7",
|
||||
"sequential-thinking",
|
||||
"github",
|
||||
"fetch",
|
||||
"playwright",
|
||||
"ide"
|
||||
],
|
||||
"enableAllProjectMcpServers": true
|
||||
}
|
||||
|
|
@ -187,7 +187,7 @@ const Template = useMemo(() => {
|
|||
|
||||
**When**: Component directly handles API calls, data transformation, or complex async operations.
|
||||
|
||||
**Dify Convention**: Use `@tanstack/react-query` hooks from `web/service/use-*.ts` or create custom data hooks. Project is migrating from SWR to React Query.
|
||||
**Dify Convention**: Use `@tanstack/react-query` hooks from `web/service/use-*.ts` or create custom data hooks.
|
||||
|
||||
```typescript
|
||||
// ❌ Before: API logic in component
|
||||
|
|
|
|||
|
|
@ -22,12 +22,12 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup UV and Python
|
||||
uses: astral-sh/setup-uv@v6
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
|
@ -57,7 +57,7 @@ jobs:
|
|||
run: sh .github/workflows/expose_service_ports.sh
|
||||
|
||||
- name: Set up Sandbox
|
||||
uses: hoverkraft-tech/compose-action@v2.0.2
|
||||
uses: hoverkraft-tech/compose-action@v2
|
||||
with:
|
||||
compose-file: |
|
||||
docker/docker-compose.middleware.yaml
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ jobs:
|
|||
if: github.repository == 'langgenius/dify'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Check Docker Compose inputs
|
||||
id: docker-compose-changes
|
||||
|
|
@ -27,7 +27,7 @@ jobs:
|
|||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- uses: astral-sh/setup-uv@v6
|
||||
- uses: astral-sh/setup-uv@v7
|
||||
|
||||
- name: Generate Docker Compose
|
||||
if: steps.docker-compose-changes.outputs.any_changed == 'true'
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ jobs:
|
|||
touch "/tmp/digests/${sanitized_digest}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: digests-${{ matrix.context }}-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
|
|
|
|||
|
|
@ -13,13 +13,13 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup UV and Python
|
||||
uses: astral-sh/setup-uv@v6
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: "3.12"
|
||||
|
|
@ -63,13 +63,13 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup UV and Python
|
||||
uses: astral-sh/setup-uv@v6
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: "3.12"
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ jobs:
|
|||
vdb-changed: ${{ steps.changes.outputs.vdb }}
|
||||
migration-changed: ${{ steps.changes.outputs.migration }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: changes
|
||||
with:
|
||||
|
|
@ -38,6 +38,7 @@ jobs:
|
|||
- '.github/workflows/api-tests.yml'
|
||||
web:
|
||||
- 'web/**'
|
||||
- '.github/workflows/web-tests.yml'
|
||||
vdb:
|
||||
- 'api/core/rag/datasource/**'
|
||||
- 'docker/**'
|
||||
|
|
|
|||
|
|
@ -19,13 +19,13 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v46
|
||||
uses: tj-actions/changed-files@v47
|
||||
with:
|
||||
files: |
|
||||
api/**
|
||||
|
|
@ -33,7 +33,7 @@ jobs:
|
|||
|
||||
- name: Setup UV and Python
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: astral-sh/setup-uv@v6
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
enable-cache: false
|
||||
python-version: "3.12"
|
||||
|
|
@ -68,15 +68,17 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v46
|
||||
uses: tj-actions/changed-files@v47
|
||||
with:
|
||||
files: web/**
|
||||
files: |
|
||||
web/**
|
||||
.github/workflows/style.yml
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
|
@ -85,7 +87,7 @@ jobs:
|
|||
run_install: false
|
||||
|
||||
- name: Setup NodeJS
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
with:
|
||||
node-version: 22
|
||||
|
|
@ -114,14 +116,14 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v46
|
||||
uses: tj-actions/changed-files@v47
|
||||
with:
|
||||
files: |
|
||||
**.sh
|
||||
|
|
|
|||
|
|
@ -25,12 +25,12 @@ jobs:
|
|||
working-directory: sdks/nodejs-client
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: ''
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ on:
|
|||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'web/i18n/en-US/*.ts'
|
||||
- 'web/i18n/en-US/*.json'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
|
@ -18,7 +18,7 @@ jobs:
|
|||
run:
|
||||
working-directory: web
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
@ -28,13 +28,13 @@ jobs:
|
|||
run: |
|
||||
git fetch origin "${{ github.event.before }}" || true
|
||||
git fetch origin "${{ github.sha }}" || true
|
||||
changed_files=$(git diff --name-only "${{ github.event.before }}" "${{ github.sha }}" -- 'i18n/en-US/*.ts')
|
||||
changed_files=$(git diff --name-only "${{ github.event.before }}" "${{ github.sha }}" -- 'i18n/en-US/*.json')
|
||||
echo "Changed files: $changed_files"
|
||||
if [ -n "$changed_files" ]; then
|
||||
echo "FILES_CHANGED=true" >> $GITHUB_ENV
|
||||
file_args=""
|
||||
for file in $changed_files; do
|
||||
filename=$(basename "$file" .ts)
|
||||
filename=$(basename "$file" .json)
|
||||
file_args="$file_args --file $filename"
|
||||
done
|
||||
echo "FILE_ARGS=$file_args" >> $GITHUB_ENV
|
||||
|
|
@ -51,7 +51,7 @@ jobs:
|
|||
|
||||
- name: Set up Node.js
|
||||
if: env.FILES_CHANGED == 'true'
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
cache: pnpm
|
||||
|
|
|
|||
|
|
@ -19,19 +19,19 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Free Disk Space
|
||||
uses: endersonmenezes/free-disk-space@v2
|
||||
uses: endersonmenezes/free-disk-space@v3
|
||||
with:
|
||||
remove_dotnet: true
|
||||
remove_haskell: true
|
||||
remove_tool_cache: true
|
||||
|
||||
- name: Setup UV and Python
|
||||
uses: astral-sh/setup-uv@v6
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
|
@ -29,7 +29,7 @@ jobs:
|
|||
run_install: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
|
|
@ -360,7 +360,7 @@ jobs:
|
|||
|
||||
- name: Upload Coverage Artifact
|
||||
if: steps.coverage-summary.outputs.has_coverage == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: web-coverage-report
|
||||
path: web/coverage
|
||||
|
|
|
|||
34
.mcp.json
34
.mcp.json
|
|
@ -1,34 +0,0 @@
|
|||
{
|
||||
"mcpServers": {
|
||||
"context7": {
|
||||
"type": "http",
|
||||
"url": "https://mcp.context7.com/mcp"
|
||||
},
|
||||
"sequential-thinking": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-sequential-thinking"],
|
||||
"env": {}
|
||||
},
|
||||
"github": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-github"],
|
||||
"env": {
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_PERSONAL_ACCESS_TOKEN}"
|
||||
}
|
||||
},
|
||||
"fetch": {
|
||||
"type": "stdio",
|
||||
"command": "uvx",
|
||||
"args": ["mcp-server-fetch"],
|
||||
"env": {}
|
||||
},
|
||||
"playwright": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@playwright/mcp@latest"],
|
||||
"env": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -90,6 +90,7 @@ class AppQueueManager:
|
|||
"""
|
||||
self._clear_task_belong_cache()
|
||||
self._q.put(None)
|
||||
self._graph_runtime_state = None # Release reference to allow GC to reclaim memory
|
||||
|
||||
def _clear_task_belong_cache(self) -> None:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,9 +1,14 @@
|
|||
from collections.abc import Mapping
|
||||
from textwrap import dedent
|
||||
from typing import Any
|
||||
|
||||
from core.helper.code_executor.template_transformer import TemplateTransformer
|
||||
|
||||
|
||||
class Jinja2TemplateTransformer(TemplateTransformer):
|
||||
# Use separate placeholder for base64-encoded template to avoid confusion
|
||||
_template_b64_placeholder: str = "{{template_b64}}"
|
||||
|
||||
@classmethod
|
||||
def transform_response(cls, response: str):
|
||||
"""
|
||||
|
|
@ -13,18 +18,35 @@ class Jinja2TemplateTransformer(TemplateTransformer):
|
|||
"""
|
||||
return {"result": cls.extract_result_str_from_response(response)}
|
||||
|
||||
@classmethod
|
||||
def assemble_runner_script(cls, code: str, inputs: Mapping[str, Any]) -> str:
|
||||
"""
|
||||
Override base class to use base64 encoding for template code.
|
||||
This prevents issues with special characters (quotes, newlines) in templates
|
||||
breaking the generated Python script. Fixes #26818.
|
||||
"""
|
||||
script = cls.get_runner_script()
|
||||
# Encode template as base64 to safely embed any content including quotes
|
||||
code_b64 = cls.serialize_code(code)
|
||||
script = script.replace(cls._template_b64_placeholder, code_b64)
|
||||
inputs_str = cls.serialize_inputs(inputs)
|
||||
script = script.replace(cls._inputs_placeholder, inputs_str)
|
||||
return script
|
||||
|
||||
@classmethod
|
||||
def get_runner_script(cls) -> str:
|
||||
runner_script = dedent(f"""
|
||||
# declare main function
|
||||
def main(**inputs):
|
||||
import jinja2
|
||||
template = jinja2.Template('''{cls._code_placeholder}''')
|
||||
return template.render(**inputs)
|
||||
|
||||
import jinja2
|
||||
import json
|
||||
from base64 import b64decode
|
||||
|
||||
# declare main function
|
||||
def main(**inputs):
|
||||
# Decode base64-encoded template to handle special characters safely
|
||||
template_code = b64decode('{cls._template_b64_placeholder}').decode('utf-8')
|
||||
template = jinja2.Template(template_code)
|
||||
return template.render(**inputs)
|
||||
|
||||
# decode and prepare input dict
|
||||
inputs_obj = json.loads(b64decode('{cls._inputs_placeholder}').decode('utf-8'))
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,15 @@ class TemplateTransformer(ABC):
|
|||
_inputs_placeholder: str = "{{inputs}}"
|
||||
_result_tag: str = "<<RESULT>>"
|
||||
|
||||
@classmethod
|
||||
def serialize_code(cls, code: str) -> str:
|
||||
"""
|
||||
Serialize template code to base64 to safely embed in generated script.
|
||||
This prevents issues with special characters like quotes breaking the script.
|
||||
"""
|
||||
code_bytes = code.encode("utf-8")
|
||||
return b64encode(code_bytes).decode("utf-8")
|
||||
|
||||
@classmethod
|
||||
def transform_caller(cls, code: str, inputs: Mapping[str, Any]) -> tuple[str, str]:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -313,17 +313,20 @@ class StreamableHTTPTransport:
|
|||
if is_initialization:
|
||||
self._maybe_extract_session_id_from_response(response)
|
||||
|
||||
content_type = cast(str, response.headers.get(CONTENT_TYPE, "").lower())
|
||||
# Per https://modelcontextprotocol.io/specification/2025-06-18/basic#notifications:
|
||||
# The server MUST NOT send a response to notifications.
|
||||
if isinstance(message.root, JSONRPCRequest):
|
||||
content_type = cast(str, response.headers.get(CONTENT_TYPE, "").lower())
|
||||
|
||||
if content_type.startswith(JSON):
|
||||
self._handle_json_response(response, ctx.server_to_client_queue)
|
||||
elif content_type.startswith(SSE):
|
||||
self._handle_sse_response(response, ctx)
|
||||
else:
|
||||
self._handle_unexpected_content_type(
|
||||
content_type,
|
||||
ctx.server_to_client_queue,
|
||||
)
|
||||
if content_type.startswith(JSON):
|
||||
self._handle_json_response(response, ctx.server_to_client_queue)
|
||||
elif content_type.startswith(SSE):
|
||||
self._handle_sse_response(response, ctx)
|
||||
else:
|
||||
self._handle_unexpected_content_type(
|
||||
content_type,
|
||||
ctx.server_to_client_queue,
|
||||
)
|
||||
|
||||
def _handle_json_response(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,15 @@ from typing import Any
|
|||
|
||||
from core.mcp.auth_client import MCPClientWithAuthRetry
|
||||
from core.mcp.error import MCPConnectionError
|
||||
from core.mcp.types import AudioContent, CallToolResult, ImageContent, TextContent
|
||||
from core.mcp.types import (
|
||||
AudioContent,
|
||||
BlobResourceContents,
|
||||
CallToolResult,
|
||||
EmbeddedResource,
|
||||
ImageContent,
|
||||
TextContent,
|
||||
TextResourceContents,
|
||||
)
|
||||
from core.tools.__base.tool import Tool
|
||||
from core.tools.__base.tool_runtime import ToolRuntime
|
||||
from core.tools.entities.tool_entities import ToolEntity, ToolInvokeMessage, ToolProviderType
|
||||
|
|
@ -53,10 +61,19 @@ class MCPTool(Tool):
|
|||
for content in result.content:
|
||||
if isinstance(content, TextContent):
|
||||
yield from self._process_text_content(content)
|
||||
elif isinstance(content, ImageContent):
|
||||
yield self._process_image_content(content)
|
||||
elif isinstance(content, AudioContent):
|
||||
yield self._process_audio_content(content)
|
||||
elif isinstance(content, ImageContent | AudioContent):
|
||||
yield self.create_blob_message(
|
||||
blob=base64.b64decode(content.data), meta={"mime_type": content.mimeType}
|
||||
)
|
||||
elif isinstance(content, EmbeddedResource):
|
||||
resource = content.resource
|
||||
if isinstance(resource, TextResourceContents):
|
||||
yield self.create_text_message(resource.text)
|
||||
elif isinstance(resource, BlobResourceContents):
|
||||
mime_type = resource.mimeType or "application/octet-stream"
|
||||
yield self.create_blob_message(blob=base64.b64decode(resource.blob), meta={"mime_type": mime_type})
|
||||
else:
|
||||
raise ToolInvokeError(f"Unsupported embedded resource type: {type(resource)}")
|
||||
else:
|
||||
logger.warning("Unsupported content type=%s", type(content))
|
||||
|
||||
|
|
@ -101,14 +118,6 @@ class MCPTool(Tool):
|
|||
for item in json_list:
|
||||
yield self.create_json_message(item)
|
||||
|
||||
def _process_image_content(self, content: ImageContent) -> ToolInvokeMessage:
|
||||
"""Process image content and return a blob message."""
|
||||
return self.create_blob_message(blob=base64.b64decode(content.data), meta={"mime_type": content.mimeType})
|
||||
|
||||
def _process_audio_content(self, content: AudioContent) -> ToolInvokeMessage:
|
||||
"""Process audio content and return a blob message."""
|
||||
return self.create_blob_message(blob=base64.b64decode(content.data), meta={"mime_type": content.mimeType})
|
||||
|
||||
def fork_tool_runtime(self, runtime: ToolRuntime) -> "MCPTool":
|
||||
return MCPTool(
|
||||
entity=self.entity,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ from enums.quota_type import QuotaType, unlimited
|
|||
from extensions.otel import AppGenerateHandler, trace_span
|
||||
from models.model import Account, App, AppMode, EndUser
|
||||
from models.workflow import Workflow
|
||||
from services.errors.app import InvokeRateLimitError, QuotaExceededError, WorkflowIdFormatError, WorkflowNotFoundError
|
||||
from services.errors.app import QuotaExceededError, WorkflowIdFormatError, WorkflowNotFoundError
|
||||
from services.errors.llm import InvokeRateLimitError
|
||||
from services.workflow_service import WorkflowService
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ from models.model import App, EndUser
|
|||
from models.trigger import WorkflowTriggerLog
|
||||
from models.workflow import Workflow
|
||||
from repositories.sqlalchemy_workflow_trigger_log_repository import SQLAlchemyWorkflowTriggerLogRepository
|
||||
from services.errors.app import InvokeRateLimitError, QuotaExceededError, WorkflowNotFoundError
|
||||
from services.errors.app import QuotaExceededError, WorkflowNotFoundError, WorkflowQuotaLimitError
|
||||
from services.workflow.entities import AsyncTriggerResponse, TriggerData, WorkflowTaskData
|
||||
from services.workflow.queue_dispatcher import QueueDispatcherManager, QueuePriority
|
||||
from services.workflow_service import WorkflowService
|
||||
|
|
@ -141,7 +141,7 @@ class AsyncWorkflowService:
|
|||
trigger_log_repo.update(trigger_log)
|
||||
session.commit()
|
||||
|
||||
raise InvokeRateLimitError(
|
||||
raise WorkflowQuotaLimitError(
|
||||
f"Workflow execution quota limit reached for tenant {trigger_data.tenant_id}"
|
||||
) from e
|
||||
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ class WorkflowIdFormatError(Exception):
|
|||
pass
|
||||
|
||||
|
||||
class InvokeRateLimitError(Exception):
|
||||
"""Raised when rate limit is exceeded for workflow invocations."""
|
||||
class WorkflowQuotaLimitError(Exception):
|
||||
"""Raised when workflow execution quota is exceeded (for async/background workflows)."""
|
||||
|
||||
pass
|
||||
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ class PluginParameterService:
|
|||
provider,
|
||||
action,
|
||||
resolved_credentials,
|
||||
CredentialType.API_KEY.value,
|
||||
original_subscription.credential_type or CredentialType.UNAUTHORIZED.value,
|
||||
parameter,
|
||||
)
|
||||
.options
|
||||
|
|
|
|||
|
|
@ -868,48 +868,111 @@ class TriggerProviderService:
|
|||
if not provider_controller:
|
||||
raise ValueError(f"Provider {provider_id} not found")
|
||||
|
||||
subscription = TriggerProviderService.get_subscription_by_id(
|
||||
tenant_id=tenant_id,
|
||||
subscription_id=subscription_id,
|
||||
)
|
||||
if not subscription:
|
||||
raise ValueError(f"Subscription {subscription_id} not found")
|
||||
# Use distributed lock to prevent race conditions on the same subscription
|
||||
lock_key = f"trigger_subscription_rebuild_lock:{tenant_id}_{subscription_id}"
|
||||
with redis_client.lock(lock_key, timeout=20):
|
||||
with Session(db.engine, expire_on_commit=False) as session:
|
||||
try:
|
||||
# Get subscription within the transaction
|
||||
subscription: TriggerSubscription | None = (
|
||||
session.query(TriggerSubscription).filter_by(tenant_id=tenant_id, id=subscription_id).first()
|
||||
)
|
||||
if not subscription:
|
||||
raise ValueError(f"Subscription {subscription_id} not found")
|
||||
|
||||
credential_type = CredentialType.of(subscription.credential_type)
|
||||
if credential_type not in [CredentialType.OAUTH2, CredentialType.API_KEY]:
|
||||
raise ValueError("Credential type not supported for rebuild")
|
||||
credential_type = CredentialType.of(subscription.credential_type)
|
||||
if credential_type not in [CredentialType.OAUTH2, CredentialType.API_KEY]:
|
||||
raise ValueError("Credential type not supported for rebuild")
|
||||
|
||||
# TODO: Trying to invoke update api of the plugin trigger provider
|
||||
# Decrypt existing credentials for merging
|
||||
credential_encrypter, _ = create_trigger_provider_encrypter_for_subscription(
|
||||
tenant_id=tenant_id,
|
||||
controller=provider_controller,
|
||||
subscription=subscription,
|
||||
)
|
||||
decrypted_credentials = dict(credential_encrypter.decrypt(subscription.credentials))
|
||||
|
||||
# FALLBACK: If the update api is not implemented, delete the previous subscription and create a new one
|
||||
# Merge credentials: if caller passed HIDDEN_VALUE, retain existing decrypted value
|
||||
merged_credentials: dict[str, Any] = {
|
||||
key: value if value != HIDDEN_VALUE else decrypted_credentials.get(key, UNKNOWN_VALUE)
|
||||
for key, value in credentials.items()
|
||||
}
|
||||
|
||||
# Delete the previous subscription
|
||||
user_id = subscription.user_id
|
||||
TriggerManager.unsubscribe_trigger(
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id,
|
||||
provider_id=provider_id,
|
||||
subscription=subscription.to_entity(),
|
||||
credentials=subscription.credentials,
|
||||
credential_type=credential_type,
|
||||
)
|
||||
user_id = subscription.user_id
|
||||
|
||||
# Create a new subscription with the same subscription_id and endpoint_id
|
||||
new_subscription: TriggerSubscriptionEntity = TriggerManager.subscribe_trigger(
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id,
|
||||
provider_id=provider_id,
|
||||
endpoint=generate_plugin_trigger_endpoint_url(subscription.endpoint_id),
|
||||
parameters=parameters,
|
||||
credentials=credentials,
|
||||
credential_type=credential_type,
|
||||
)
|
||||
TriggerProviderService.update_trigger_subscription(
|
||||
tenant_id=tenant_id,
|
||||
subscription_id=subscription.id,
|
||||
name=name,
|
||||
parameters=parameters,
|
||||
credentials=credentials,
|
||||
properties=new_subscription.properties,
|
||||
expires_at=new_subscription.expires_at,
|
||||
)
|
||||
# TODO: Trying to invoke update api of the plugin trigger provider
|
||||
|
||||
# FALLBACK: If the update api is not implemented,
|
||||
# delete the previous subscription and create a new one
|
||||
|
||||
# Unsubscribe the previous subscription (external call, but we'll handle errors)
|
||||
try:
|
||||
TriggerManager.unsubscribe_trigger(
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id,
|
||||
provider_id=provider_id,
|
||||
subscription=subscription.to_entity(),
|
||||
credentials=decrypted_credentials,
|
||||
credential_type=credential_type,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception("Error unsubscribing trigger during rebuild", exc_info=e)
|
||||
# Continue anyway - the subscription might already be deleted externally
|
||||
|
||||
# Create a new subscription with the same subscription_id and endpoint_id (external call)
|
||||
new_subscription: TriggerSubscriptionEntity = TriggerManager.subscribe_trigger(
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id,
|
||||
provider_id=provider_id,
|
||||
endpoint=generate_plugin_trigger_endpoint_url(subscription.endpoint_id),
|
||||
parameters=parameters,
|
||||
credentials=merged_credentials,
|
||||
credential_type=credential_type,
|
||||
)
|
||||
|
||||
# Update the subscription in the same transaction
|
||||
# Inline update logic to reuse the same session
|
||||
if name is not None and name != subscription.name:
|
||||
existing = (
|
||||
session.query(TriggerSubscription)
|
||||
.filter_by(tenant_id=tenant_id, provider_id=str(provider_id), name=name)
|
||||
.first()
|
||||
)
|
||||
if existing and existing.id != subscription.id:
|
||||
raise ValueError(f"Subscription name '{name}' already exists for this provider")
|
||||
subscription.name = name
|
||||
|
||||
# Update parameters
|
||||
subscription.parameters = dict(parameters)
|
||||
|
||||
# Update credentials with merged (and encrypted) values
|
||||
subscription.credentials = dict(credential_encrypter.encrypt(merged_credentials))
|
||||
|
||||
# Update properties
|
||||
if new_subscription.properties:
|
||||
properties_encrypter, _ = create_provider_encrypter(
|
||||
tenant_id=tenant_id,
|
||||
config=provider_controller.get_properties_schema(),
|
||||
cache=NoOpProviderCredentialCache(),
|
||||
)
|
||||
subscription.properties = dict(properties_encrypter.encrypt(dict(new_subscription.properties)))
|
||||
|
||||
# Update expiration timestamp
|
||||
if new_subscription.expires_at is not None:
|
||||
subscription.expires_at = new_subscription.expires_at
|
||||
|
||||
# Commit the transaction
|
||||
session.commit()
|
||||
|
||||
# Clear subscription cache
|
||||
delete_cache_for_subscription(
|
||||
tenant_id=tenant_id,
|
||||
provider_id=subscription.provider_id,
|
||||
subscription_id=subscription.id,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
# Rollback on any error
|
||||
session.rollback()
|
||||
logger.exception("Failed to rebuild trigger subscription", exc_info=e)
|
||||
raise
|
||||
|
|
|
|||
|
|
@ -863,10 +863,18 @@ class WebhookService:
|
|||
not_found_in_cache.append(node_id)
|
||||
continue
|
||||
|
||||
with Session(db.engine) as session:
|
||||
try:
|
||||
# lock the concurrent webhook trigger creation
|
||||
redis_client.lock(f"{cls.__WEBHOOK_NODE_CACHE_KEY__}:apps:{app.id}:lock", timeout=10)
|
||||
lock_key = f"{cls.__WEBHOOK_NODE_CACHE_KEY__}:apps:{app.id}:lock"
|
||||
lock = redis_client.lock(lock_key, timeout=10)
|
||||
lock_acquired = False
|
||||
|
||||
try:
|
||||
# acquire the lock with blocking and timeout
|
||||
lock_acquired = lock.acquire(blocking=True, blocking_timeout=10)
|
||||
if not lock_acquired:
|
||||
logger.warning("Failed to acquire lock for webhook sync, app %s", app.id)
|
||||
raise RuntimeError("Failed to acquire lock for webhook trigger synchronization")
|
||||
|
||||
with Session(db.engine) as session:
|
||||
# fetch the non-cached nodes from DB
|
||||
all_records = session.scalars(
|
||||
select(WorkflowWebhookTrigger).where(
|
||||
|
|
@ -903,11 +911,16 @@ class WebhookService:
|
|||
session.delete(nodes_id_in_db[node_id])
|
||||
redis_client.delete(f"{cls.__WEBHOOK_NODE_CACHE_KEY__}:{app.id}:{node_id}")
|
||||
session.commit()
|
||||
except Exception:
|
||||
logger.exception("Failed to sync webhook relationships for app %s", app.id)
|
||||
raise
|
||||
finally:
|
||||
redis_client.delete(f"{cls.__WEBHOOK_NODE_CACHE_KEY__}:apps:{app.id}:lock")
|
||||
except Exception:
|
||||
logger.exception("Failed to sync webhook relationships for app %s", app.id)
|
||||
raise
|
||||
finally:
|
||||
# release the lock only if it was acquired
|
||||
if lock_acquired:
|
||||
try:
|
||||
lock.release()
|
||||
except Exception:
|
||||
logger.exception("Failed to release lock for webhook sync, app %s", app.id)
|
||||
|
||||
@classmethod
|
||||
def generate_webhook_id(cls) -> str:
|
||||
|
|
|
|||
|
|
@ -7,11 +7,14 @@ CODE_LANGUAGE = CodeLanguage.JINJA2
|
|||
|
||||
|
||||
def test_jinja2():
|
||||
"""Test basic Jinja2 template rendering."""
|
||||
template = "Hello {{template}}"
|
||||
# Template must be base64 encoded to match the new safe embedding approach
|
||||
template_b64 = base64.b64encode(template.encode("utf-8")).decode("utf-8")
|
||||
inputs = base64.b64encode(b'{"template": "World"}').decode("utf-8")
|
||||
code = (
|
||||
Jinja2TemplateTransformer.get_runner_script()
|
||||
.replace(Jinja2TemplateTransformer._code_placeholder, template)
|
||||
.replace(Jinja2TemplateTransformer._template_b64_placeholder, template_b64)
|
||||
.replace(Jinja2TemplateTransformer._inputs_placeholder, inputs)
|
||||
)
|
||||
result = CodeExecutor.execute_code(
|
||||
|
|
@ -21,6 +24,7 @@ def test_jinja2():
|
|||
|
||||
|
||||
def test_jinja2_with_code_template():
|
||||
"""Test template rendering via the high-level workflow API."""
|
||||
result = CodeExecutor.execute_workflow_code_template(
|
||||
language=CODE_LANGUAGE, code="Hello {{template}}", inputs={"template": "World"}
|
||||
)
|
||||
|
|
@ -28,7 +32,64 @@ def test_jinja2_with_code_template():
|
|||
|
||||
|
||||
def test_jinja2_get_runner_script():
|
||||
"""Test that runner script contains required placeholders."""
|
||||
runner_script = Jinja2TemplateTransformer.get_runner_script()
|
||||
assert runner_script.count(Jinja2TemplateTransformer._code_placeholder) == 1
|
||||
assert runner_script.count(Jinja2TemplateTransformer._template_b64_placeholder) == 1
|
||||
assert runner_script.count(Jinja2TemplateTransformer._inputs_placeholder) == 1
|
||||
assert runner_script.count(Jinja2TemplateTransformer._result_tag) == 2
|
||||
|
||||
|
||||
def test_jinja2_template_with_special_characters():
|
||||
"""
|
||||
Test that templates with special characters (quotes, newlines) render correctly.
|
||||
This is a regression test for issue #26818 where textarea pre-fill values
|
||||
containing special characters would break template rendering.
|
||||
"""
|
||||
# Template with triple quotes, single quotes, double quotes, and newlines
|
||||
template = """<html>
|
||||
<body>
|
||||
<input value="{{ task.get('Task ID', '') }}"/>
|
||||
<textarea>{{ task.get('Issues', 'No issues reported') }}</textarea>
|
||||
<p>Status: "{{ status }}"</p>
|
||||
<pre>'''code block'''</pre>
|
||||
</body>
|
||||
</html>"""
|
||||
inputs = {"task": {"Task ID": "TASK-123", "Issues": "Line 1\nLine 2\nLine 3"}, "status": "completed"}
|
||||
|
||||
result = CodeExecutor.execute_workflow_code_template(language=CODE_LANGUAGE, code=template, inputs=inputs)
|
||||
|
||||
# Verify the template rendered correctly with all special characters
|
||||
output = result["result"]
|
||||
assert 'value="TASK-123"' in output
|
||||
assert "<textarea>Line 1\nLine 2\nLine 3</textarea>" in output
|
||||
assert 'Status: "completed"' in output
|
||||
assert "'''code block'''" in output
|
||||
|
||||
|
||||
def test_jinja2_template_with_html_textarea_prefill():
|
||||
"""
|
||||
Specific test for HTML textarea with Jinja2 variable pre-fill.
|
||||
Verifies fix for issue #26818.
|
||||
"""
|
||||
template = "<textarea name='notes'>{{ notes }}</textarea>"
|
||||
notes_content = "This is a multi-line note.\nWith special chars: 'single' and \"double\" quotes."
|
||||
inputs = {"notes": notes_content}
|
||||
|
||||
result = CodeExecutor.execute_workflow_code_template(language=CODE_LANGUAGE, code=template, inputs=inputs)
|
||||
|
||||
expected_output = f"<textarea name='notes'>{notes_content}</textarea>"
|
||||
assert result["result"] == expected_output
|
||||
|
||||
|
||||
def test_jinja2_assemble_runner_script_encodes_template():
|
||||
"""Test that assemble_runner_script properly base64 encodes the template."""
|
||||
template = "Hello {{ name }}!"
|
||||
inputs = {"name": "World"}
|
||||
|
||||
script = Jinja2TemplateTransformer.assemble_runner_script(template, inputs)
|
||||
|
||||
# The template should be base64 encoded in the script
|
||||
template_b64 = base64.b64encode(template.encode("utf-8")).decode("utf-8")
|
||||
assert template_b64 in script
|
||||
# The raw template should NOT appear in the script (it's encoded)
|
||||
assert "Hello {{ name }}!" not in script
|
||||
|
|
|
|||
|
|
@ -0,0 +1,682 @@
|
|||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from faker import Faker
|
||||
|
||||
from constants import HIDDEN_VALUE, UNKNOWN_VALUE
|
||||
from core.plugin.entities.plugin_daemon import CredentialType
|
||||
from core.trigger.entities.entities import Subscription as TriggerSubscriptionEntity
|
||||
from extensions.ext_database import db
|
||||
from models.provider_ids import TriggerProviderID
|
||||
from models.trigger import TriggerSubscription
|
||||
from services.trigger.trigger_provider_service import TriggerProviderService
|
||||
|
||||
|
||||
class TestTriggerProviderService:
|
||||
"""Integration tests for TriggerProviderService using testcontainers."""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_external_service_dependencies(self):
|
||||
"""Mock setup for external service dependencies."""
|
||||
with (
|
||||
patch("services.trigger.trigger_provider_service.TriggerManager") as mock_trigger_manager,
|
||||
patch("services.trigger.trigger_provider_service.redis_client") as mock_redis_client,
|
||||
patch("services.trigger.trigger_provider_service.delete_cache_for_subscription") as mock_delete_cache,
|
||||
patch("services.account_service.FeatureService") as mock_account_feature_service,
|
||||
):
|
||||
# Setup default mock returns
|
||||
mock_provider_controller = MagicMock()
|
||||
mock_provider_controller.get_credential_schema_config.return_value = MagicMock()
|
||||
mock_provider_controller.get_properties_schema.return_value = MagicMock()
|
||||
mock_trigger_manager.get_trigger_provider.return_value = mock_provider_controller
|
||||
|
||||
# Mock redis lock
|
||||
mock_lock = MagicMock()
|
||||
mock_lock.__enter__ = MagicMock(return_value=None)
|
||||
mock_lock.__exit__ = MagicMock(return_value=None)
|
||||
mock_redis_client.lock.return_value = mock_lock
|
||||
|
||||
# Setup account feature service mock
|
||||
mock_account_feature_service.get_system_features.return_value.is_allow_register = True
|
||||
|
||||
yield {
|
||||
"trigger_manager": mock_trigger_manager,
|
||||
"redis_client": mock_redis_client,
|
||||
"delete_cache": mock_delete_cache,
|
||||
"provider_controller": mock_provider_controller,
|
||||
"account_feature_service": mock_account_feature_service,
|
||||
}
|
||||
|
||||
def _create_test_account_and_tenant(self, db_session_with_containers, mock_external_service_dependencies):
|
||||
"""
|
||||
Helper method to create a test account and tenant for testing.
|
||||
|
||||
Args:
|
||||
db_session_with_containers: Database session from testcontainers infrastructure
|
||||
mock_external_service_dependencies: Mock dependencies
|
||||
|
||||
Returns:
|
||||
tuple: (account, tenant) - Created account and tenant instances
|
||||
"""
|
||||
fake = Faker()
|
||||
|
||||
from services.account_service import AccountService, TenantService
|
||||
|
||||
# Setup mocks for account creation
|
||||
mock_external_service_dependencies[
|
||||
"account_feature_service"
|
||||
].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies[
|
||||
"trigger_manager"
|
||||
].get_trigger_provider.return_value = mock_external_service_dependencies["provider_controller"]
|
||||
|
||||
# Create account and tenant
|
||||
account = AccountService.create_account(
|
||||
email=fake.email(),
|
||||
name=fake.name(),
|
||||
interface_language="en-US",
|
||||
password=fake.password(length=12),
|
||||
)
|
||||
TenantService.create_owner_tenant_if_not_exist(account, name=fake.company())
|
||||
tenant = account.current_tenant
|
||||
|
||||
return account, tenant
|
||||
|
||||
def _create_test_subscription(
|
||||
self,
|
||||
db_session_with_containers,
|
||||
tenant_id,
|
||||
user_id,
|
||||
provider_id,
|
||||
credential_type,
|
||||
credentials,
|
||||
mock_external_service_dependencies,
|
||||
):
|
||||
"""
|
||||
Helper method to create a test trigger subscription.
|
||||
|
||||
Args:
|
||||
db_session_with_containers: Database session
|
||||
tenant_id: Tenant ID
|
||||
user_id: User ID
|
||||
provider_id: Provider ID
|
||||
credential_type: Credential type
|
||||
credentials: Credentials dict
|
||||
mock_external_service_dependencies: Mock dependencies
|
||||
|
||||
Returns:
|
||||
TriggerSubscription: Created subscription instance
|
||||
"""
|
||||
fake = Faker()
|
||||
from core.helper.provider_cache import NoOpProviderCredentialCache
|
||||
from core.helper.provider_encryption import create_provider_encrypter
|
||||
|
||||
# Use mock provider controller to encrypt credentials
|
||||
provider_controller = mock_external_service_dependencies["provider_controller"]
|
||||
|
||||
# Create encrypter for credentials
|
||||
credential_encrypter, _ = create_provider_encrypter(
|
||||
tenant_id=tenant_id,
|
||||
config=provider_controller.get_credential_schema_config(credential_type),
|
||||
cache=NoOpProviderCredentialCache(),
|
||||
)
|
||||
|
||||
subscription = TriggerSubscription(
|
||||
name=fake.word(),
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id,
|
||||
provider_id=str(provider_id),
|
||||
endpoint_id=fake.uuid4(),
|
||||
parameters={"param1": "value1"},
|
||||
properties={"prop1": "value1"},
|
||||
credentials=dict(credential_encrypter.encrypt(credentials)),
|
||||
credential_type=credential_type.value,
|
||||
credential_expires_at=-1,
|
||||
expires_at=-1,
|
||||
)
|
||||
|
||||
db.session.add(subscription)
|
||||
db.session.commit()
|
||||
db.session.refresh(subscription)
|
||||
|
||||
return subscription
|
||||
|
||||
def test_rebuild_trigger_subscription_success_with_merged_credentials(
|
||||
self, db_session_with_containers, mock_external_service_dependencies
|
||||
):
|
||||
"""
|
||||
Test successful rebuild with credential merging (HIDDEN_VALUE handling).
|
||||
|
||||
This test verifies:
|
||||
- Credentials are properly merged (HIDDEN_VALUE replaced with existing values)
|
||||
- Single transaction wraps all operations
|
||||
- Merged credentials are used for subscribe and update
|
||||
- Database state is correctly updated
|
||||
"""
|
||||
fake = Faker()
|
||||
account, tenant = self._create_test_account_and_tenant(
|
||||
db_session_with_containers, mock_external_service_dependencies
|
||||
)
|
||||
|
||||
provider_id = TriggerProviderID("test_org/test_plugin/test_provider")
|
||||
credential_type = CredentialType.API_KEY
|
||||
|
||||
# Create initial subscription with credentials
|
||||
original_credentials = {"api_key": "original-secret-key", "api_secret": "original-secret"}
|
||||
subscription = self._create_test_subscription(
|
||||
db_session_with_containers,
|
||||
tenant.id,
|
||||
account.id,
|
||||
provider_id,
|
||||
credential_type,
|
||||
original_credentials,
|
||||
mock_external_service_dependencies,
|
||||
)
|
||||
|
||||
# Prepare new credentials with HIDDEN_VALUE for api_key (should keep original)
|
||||
# and new value for api_secret (should update)
|
||||
new_credentials = {
|
||||
"api_key": HIDDEN_VALUE, # Should be replaced with original
|
||||
"api_secret": "new-secret-value", # Should be updated
|
||||
}
|
||||
|
||||
# Mock subscribe_trigger to return a new subscription entity
|
||||
new_subscription_entity = TriggerSubscriptionEntity(
|
||||
endpoint=subscription.endpoint_id,
|
||||
parameters={"param1": "value1"},
|
||||
properties={"prop1": "new_prop_value"},
|
||||
expires_at=1234567890,
|
||||
)
|
||||
mock_external_service_dependencies["trigger_manager"].subscribe_trigger.return_value = new_subscription_entity
|
||||
|
||||
# Mock unsubscribe_trigger
|
||||
mock_external_service_dependencies["trigger_manager"].unsubscribe_trigger.return_value = MagicMock()
|
||||
|
||||
# Execute rebuild
|
||||
TriggerProviderService.rebuild_trigger_subscription(
|
||||
tenant_id=tenant.id,
|
||||
provider_id=provider_id,
|
||||
subscription_id=subscription.id,
|
||||
credentials=new_credentials,
|
||||
parameters={"param1": "updated_value"},
|
||||
name="updated_name",
|
||||
)
|
||||
|
||||
# Verify unsubscribe was called with decrypted original credentials
|
||||
mock_external_service_dependencies["trigger_manager"].unsubscribe_trigger.assert_called_once()
|
||||
unsubscribe_call_args = mock_external_service_dependencies["trigger_manager"].unsubscribe_trigger.call_args
|
||||
assert unsubscribe_call_args.kwargs["tenant_id"] == tenant.id
|
||||
assert unsubscribe_call_args.kwargs["provider_id"] == provider_id
|
||||
assert unsubscribe_call_args.kwargs["credential_type"] == credential_type
|
||||
|
||||
# Verify subscribe was called with merged credentials (api_key from original, api_secret new)
|
||||
mock_external_service_dependencies["trigger_manager"].subscribe_trigger.assert_called_once()
|
||||
subscribe_call_args = mock_external_service_dependencies["trigger_manager"].subscribe_trigger.call_args
|
||||
subscribe_credentials = subscribe_call_args.kwargs["credentials"]
|
||||
assert subscribe_credentials["api_key"] == original_credentials["api_key"] # Merged from original
|
||||
assert subscribe_credentials["api_secret"] == "new-secret-value" # New value
|
||||
|
||||
# Verify database state was updated
|
||||
db.session.refresh(subscription)
|
||||
assert subscription.name == "updated_name"
|
||||
assert subscription.parameters == {"param1": "updated_value"}
|
||||
|
||||
# Verify credentials in DB were updated with merged values (decrypt to check)
|
||||
from core.helper.provider_cache import NoOpProviderCredentialCache
|
||||
from core.helper.provider_encryption import create_provider_encrypter
|
||||
|
||||
# Use mock provider controller to decrypt credentials
|
||||
provider_controller = mock_external_service_dependencies["provider_controller"]
|
||||
credential_encrypter, _ = create_provider_encrypter(
|
||||
tenant_id=tenant.id,
|
||||
config=provider_controller.get_credential_schema_config(credential_type),
|
||||
cache=NoOpProviderCredentialCache(),
|
||||
)
|
||||
decrypted_db_credentials = dict(credential_encrypter.decrypt(subscription.credentials))
|
||||
assert decrypted_db_credentials["api_key"] == original_credentials["api_key"]
|
||||
assert decrypted_db_credentials["api_secret"] == "new-secret-value"
|
||||
|
||||
# Verify cache was cleared
|
||||
mock_external_service_dependencies["delete_cache"].assert_called_once_with(
|
||||
tenant_id=tenant.id,
|
||||
provider_id=subscription.provider_id,
|
||||
subscription_id=subscription.id,
|
||||
)
|
||||
|
||||
def test_rebuild_trigger_subscription_with_all_new_credentials(
|
||||
self, db_session_with_containers, mock_external_service_dependencies
|
||||
):
|
||||
"""
|
||||
Test rebuild when all credentials are new (no HIDDEN_VALUE).
|
||||
|
||||
This test verifies:
|
||||
- All new credentials are used when no HIDDEN_VALUE is present
|
||||
- Merged credentials contain only new values
|
||||
"""
|
||||
fake = Faker()
|
||||
account, tenant = self._create_test_account_and_tenant(
|
||||
db_session_with_containers, mock_external_service_dependencies
|
||||
)
|
||||
|
||||
provider_id = TriggerProviderID("test_org/test_plugin/test_provider")
|
||||
credential_type = CredentialType.API_KEY
|
||||
|
||||
# Create initial subscription
|
||||
original_credentials = {"api_key": "original-key", "api_secret": "original-secret"}
|
||||
subscription = self._create_test_subscription(
|
||||
db_session_with_containers,
|
||||
tenant.id,
|
||||
account.id,
|
||||
provider_id,
|
||||
credential_type,
|
||||
original_credentials,
|
||||
mock_external_service_dependencies,
|
||||
)
|
||||
|
||||
# All new credentials (no HIDDEN_VALUE)
|
||||
new_credentials = {
|
||||
"api_key": "completely-new-key",
|
||||
"api_secret": "completely-new-secret",
|
||||
}
|
||||
|
||||
new_subscription_entity = TriggerSubscriptionEntity(
|
||||
endpoint=subscription.endpoint_id,
|
||||
parameters={},
|
||||
properties={},
|
||||
expires_at=-1,
|
||||
)
|
||||
mock_external_service_dependencies["trigger_manager"].subscribe_trigger.return_value = new_subscription_entity
|
||||
mock_external_service_dependencies["trigger_manager"].unsubscribe_trigger.return_value = MagicMock()
|
||||
|
||||
# Execute rebuild
|
||||
TriggerProviderService.rebuild_trigger_subscription(
|
||||
tenant_id=tenant.id,
|
||||
provider_id=provider_id,
|
||||
subscription_id=subscription.id,
|
||||
credentials=new_credentials,
|
||||
parameters={},
|
||||
)
|
||||
|
||||
# Verify subscribe was called with all new credentials
|
||||
subscribe_call_args = mock_external_service_dependencies["trigger_manager"].subscribe_trigger.call_args
|
||||
subscribe_credentials = subscribe_call_args.kwargs["credentials"]
|
||||
assert subscribe_credentials["api_key"] == "completely-new-key"
|
||||
assert subscribe_credentials["api_secret"] == "completely-new-secret"
|
||||
|
||||
def test_rebuild_trigger_subscription_with_all_hidden_values(
|
||||
self, db_session_with_containers, mock_external_service_dependencies
|
||||
):
|
||||
"""
|
||||
Test rebuild when all credentials are HIDDEN_VALUE (preserve all existing).
|
||||
|
||||
This test verifies:
|
||||
- All HIDDEN_VALUE credentials are replaced with existing values
|
||||
- Original credentials are preserved
|
||||
"""
|
||||
fake = Faker()
|
||||
account, tenant = self._create_test_account_and_tenant(
|
||||
db_session_with_containers, mock_external_service_dependencies
|
||||
)
|
||||
|
||||
provider_id = TriggerProviderID("test_org/test_plugin/test_provider")
|
||||
credential_type = CredentialType.API_KEY
|
||||
|
||||
original_credentials = {"api_key": "original-key", "api_secret": "original-secret"}
|
||||
subscription = self._create_test_subscription(
|
||||
db_session_with_containers,
|
||||
tenant.id,
|
||||
account.id,
|
||||
provider_id,
|
||||
credential_type,
|
||||
original_credentials,
|
||||
mock_external_service_dependencies,
|
||||
)
|
||||
|
||||
# All HIDDEN_VALUE (should preserve all original)
|
||||
new_credentials = {
|
||||
"api_key": HIDDEN_VALUE,
|
||||
"api_secret": HIDDEN_VALUE,
|
||||
}
|
||||
|
||||
new_subscription_entity = TriggerSubscriptionEntity(
|
||||
endpoint=subscription.endpoint_id,
|
||||
parameters={},
|
||||
properties={},
|
||||
expires_at=-1,
|
||||
)
|
||||
mock_external_service_dependencies["trigger_manager"].subscribe_trigger.return_value = new_subscription_entity
|
||||
mock_external_service_dependencies["trigger_manager"].unsubscribe_trigger.return_value = MagicMock()
|
||||
|
||||
# Execute rebuild
|
||||
TriggerProviderService.rebuild_trigger_subscription(
|
||||
tenant_id=tenant.id,
|
||||
provider_id=provider_id,
|
||||
subscription_id=subscription.id,
|
||||
credentials=new_credentials,
|
||||
parameters={},
|
||||
)
|
||||
|
||||
# Verify subscribe was called with all original credentials
|
||||
subscribe_call_args = mock_external_service_dependencies["trigger_manager"].subscribe_trigger.call_args
|
||||
subscribe_credentials = subscribe_call_args.kwargs["credentials"]
|
||||
assert subscribe_credentials["api_key"] == original_credentials["api_key"]
|
||||
assert subscribe_credentials["api_secret"] == original_credentials["api_secret"]
|
||||
|
||||
def test_rebuild_trigger_subscription_with_missing_key_uses_unknown_value(
|
||||
self, db_session_with_containers, mock_external_service_dependencies
|
||||
):
|
||||
"""
|
||||
Test rebuild when HIDDEN_VALUE is used for a key that doesn't exist in original.
|
||||
|
||||
This test verifies:
|
||||
- UNKNOWN_VALUE is used when HIDDEN_VALUE key doesn't exist in original credentials
|
||||
"""
|
||||
fake = Faker()
|
||||
account, tenant = self._create_test_account_and_tenant(
|
||||
db_session_with_containers, mock_external_service_dependencies
|
||||
)
|
||||
|
||||
provider_id = TriggerProviderID("test_org/test_plugin/test_provider")
|
||||
credential_type = CredentialType.API_KEY
|
||||
|
||||
# Original has only api_key
|
||||
original_credentials = {"api_key": "original-key"}
|
||||
subscription = self._create_test_subscription(
|
||||
db_session_with_containers,
|
||||
tenant.id,
|
||||
account.id,
|
||||
provider_id,
|
||||
credential_type,
|
||||
original_credentials,
|
||||
mock_external_service_dependencies,
|
||||
)
|
||||
|
||||
# HIDDEN_VALUE for non-existent key should use UNKNOWN_VALUE
|
||||
new_credentials = {
|
||||
"api_key": HIDDEN_VALUE,
|
||||
"non_existent_key": HIDDEN_VALUE, # This key doesn't exist in original
|
||||
}
|
||||
|
||||
new_subscription_entity = TriggerSubscriptionEntity(
|
||||
endpoint=subscription.endpoint_id,
|
||||
parameters={},
|
||||
properties={},
|
||||
expires_at=-1,
|
||||
)
|
||||
mock_external_service_dependencies["trigger_manager"].subscribe_trigger.return_value = new_subscription_entity
|
||||
mock_external_service_dependencies["trigger_manager"].unsubscribe_trigger.return_value = MagicMock()
|
||||
|
||||
# Execute rebuild
|
||||
TriggerProviderService.rebuild_trigger_subscription(
|
||||
tenant_id=tenant.id,
|
||||
provider_id=provider_id,
|
||||
subscription_id=subscription.id,
|
||||
credentials=new_credentials,
|
||||
parameters={},
|
||||
)
|
||||
|
||||
# Verify subscribe was called with original api_key and UNKNOWN_VALUE for missing key
|
||||
subscribe_call_args = mock_external_service_dependencies["trigger_manager"].subscribe_trigger.call_args
|
||||
subscribe_credentials = subscribe_call_args.kwargs["credentials"]
|
||||
assert subscribe_credentials["api_key"] == original_credentials["api_key"]
|
||||
assert subscribe_credentials["non_existent_key"] == UNKNOWN_VALUE
|
||||
|
||||
def test_rebuild_trigger_subscription_rollback_on_error(
|
||||
self, db_session_with_containers, mock_external_service_dependencies
|
||||
):
|
||||
"""
|
||||
Test that transaction is rolled back on error.
|
||||
|
||||
This test verifies:
|
||||
- Database transaction is rolled back when an error occurs
|
||||
- Original subscription state is preserved
|
||||
"""
|
||||
fake = Faker()
|
||||
account, tenant = self._create_test_account_and_tenant(
|
||||
db_session_with_containers, mock_external_service_dependencies
|
||||
)
|
||||
|
||||
provider_id = TriggerProviderID("test_org/test_plugin/test_provider")
|
||||
credential_type = CredentialType.API_KEY
|
||||
|
||||
original_credentials = {"api_key": "original-key"}
|
||||
subscription = self._create_test_subscription(
|
||||
db_session_with_containers,
|
||||
tenant.id,
|
||||
account.id,
|
||||
provider_id,
|
||||
credential_type,
|
||||
original_credentials,
|
||||
mock_external_service_dependencies,
|
||||
)
|
||||
|
||||
original_name = subscription.name
|
||||
original_parameters = subscription.parameters.copy()
|
||||
|
||||
# Make subscribe_trigger raise an error
|
||||
mock_external_service_dependencies["trigger_manager"].subscribe_trigger.side_effect = ValueError(
|
||||
"Subscribe failed"
|
||||
)
|
||||
mock_external_service_dependencies["trigger_manager"].unsubscribe_trigger.return_value = MagicMock()
|
||||
|
||||
# Execute rebuild and expect error
|
||||
with pytest.raises(ValueError, match="Subscribe failed"):
|
||||
TriggerProviderService.rebuild_trigger_subscription(
|
||||
tenant_id=tenant.id,
|
||||
provider_id=provider_id,
|
||||
subscription_id=subscription.id,
|
||||
credentials={"api_key": "new-key"},
|
||||
parameters={},
|
||||
)
|
||||
|
||||
# Verify subscription state was not changed (rolled back)
|
||||
db.session.refresh(subscription)
|
||||
assert subscription.name == original_name
|
||||
assert subscription.parameters == original_parameters
|
||||
|
||||
def test_rebuild_trigger_subscription_unsubscribe_error_continues(
|
||||
self, db_session_with_containers, mock_external_service_dependencies
|
||||
):
|
||||
"""
|
||||
Test that unsubscribe errors are handled gracefully and operation continues.
|
||||
|
||||
This test verifies:
|
||||
- Unsubscribe errors are caught and logged but don't stop the rebuild
|
||||
- Rebuild continues even if unsubscribe fails
|
||||
"""
|
||||
fake = Faker()
|
||||
account, tenant = self._create_test_account_and_tenant(
|
||||
db_session_with_containers, mock_external_service_dependencies
|
||||
)
|
||||
|
||||
provider_id = TriggerProviderID("test_org/test_plugin/test_provider")
|
||||
credential_type = CredentialType.API_KEY
|
||||
|
||||
original_credentials = {"api_key": "original-key"}
|
||||
subscription = self._create_test_subscription(
|
||||
db_session_with_containers,
|
||||
tenant.id,
|
||||
account.id,
|
||||
provider_id,
|
||||
credential_type,
|
||||
original_credentials,
|
||||
mock_external_service_dependencies,
|
||||
)
|
||||
|
||||
# Make unsubscribe_trigger raise an error (should be caught and continue)
|
||||
mock_external_service_dependencies["trigger_manager"].unsubscribe_trigger.side_effect = ValueError(
|
||||
"Unsubscribe failed"
|
||||
)
|
||||
|
||||
new_subscription_entity = TriggerSubscriptionEntity(
|
||||
endpoint=subscription.endpoint_id,
|
||||
parameters={},
|
||||
properties={},
|
||||
expires_at=-1,
|
||||
)
|
||||
mock_external_service_dependencies["trigger_manager"].subscribe_trigger.return_value = new_subscription_entity
|
||||
|
||||
# Execute rebuild - should succeed despite unsubscribe error
|
||||
TriggerProviderService.rebuild_trigger_subscription(
|
||||
tenant_id=tenant.id,
|
||||
provider_id=provider_id,
|
||||
subscription_id=subscription.id,
|
||||
credentials={"api_key": "new-key"},
|
||||
parameters={},
|
||||
)
|
||||
|
||||
# Verify subscribe was still called (operation continued)
|
||||
mock_external_service_dependencies["trigger_manager"].subscribe_trigger.assert_called_once()
|
||||
|
||||
# Verify subscription was updated
|
||||
db.session.refresh(subscription)
|
||||
assert subscription.parameters == {}
|
||||
|
||||
def test_rebuild_trigger_subscription_subscription_not_found(
|
||||
self, db_session_with_containers, mock_external_service_dependencies
|
||||
):
|
||||
"""
|
||||
Test error when subscription is not found.
|
||||
|
||||
This test verifies:
|
||||
- Proper error is raised when subscription doesn't exist
|
||||
"""
|
||||
fake = Faker()
|
||||
account, tenant = self._create_test_account_and_tenant(
|
||||
db_session_with_containers, mock_external_service_dependencies
|
||||
)
|
||||
|
||||
provider_id = TriggerProviderID("test_org/test_plugin/test_provider")
|
||||
fake_subscription_id = fake.uuid4()
|
||||
|
||||
with pytest.raises(ValueError, match="not found"):
|
||||
TriggerProviderService.rebuild_trigger_subscription(
|
||||
tenant_id=tenant.id,
|
||||
provider_id=provider_id,
|
||||
subscription_id=fake_subscription_id,
|
||||
credentials={},
|
||||
parameters={},
|
||||
)
|
||||
|
||||
def test_rebuild_trigger_subscription_provider_not_found(
|
||||
self, db_session_with_containers, mock_external_service_dependencies
|
||||
):
|
||||
"""
|
||||
Test error when provider is not found.
|
||||
|
||||
This test verifies:
|
||||
- Proper error is raised when provider doesn't exist
|
||||
"""
|
||||
fake = Faker()
|
||||
account, tenant = self._create_test_account_and_tenant(
|
||||
db_session_with_containers, mock_external_service_dependencies
|
||||
)
|
||||
|
||||
provider_id = TriggerProviderID("non_existent_org/non_existent_plugin/non_existent_provider")
|
||||
|
||||
# Make get_trigger_provider return None
|
||||
mock_external_service_dependencies["trigger_manager"].get_trigger_provider.return_value = None
|
||||
|
||||
with pytest.raises(ValueError, match="Provider.*not found"):
|
||||
TriggerProviderService.rebuild_trigger_subscription(
|
||||
tenant_id=tenant.id,
|
||||
provider_id=provider_id,
|
||||
subscription_id=fake.uuid4(),
|
||||
credentials={},
|
||||
parameters={},
|
||||
)
|
||||
|
||||
def test_rebuild_trigger_subscription_unsupported_credential_type(
|
||||
self, db_session_with_containers, mock_external_service_dependencies
|
||||
):
|
||||
"""
|
||||
Test error when credential type is not supported for rebuild.
|
||||
|
||||
This test verifies:
|
||||
- Proper error is raised for unsupported credential types (not OAUTH2 or API_KEY)
|
||||
"""
|
||||
fake = Faker()
|
||||
account, tenant = self._create_test_account_and_tenant(
|
||||
db_session_with_containers, mock_external_service_dependencies
|
||||
)
|
||||
|
||||
provider_id = TriggerProviderID("test_org/test_plugin/test_provider")
|
||||
credential_type = CredentialType.UNAUTHORIZED # Not supported
|
||||
|
||||
subscription = self._create_test_subscription(
|
||||
db_session_with_containers,
|
||||
tenant.id,
|
||||
account.id,
|
||||
provider_id,
|
||||
credential_type,
|
||||
{},
|
||||
mock_external_service_dependencies,
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="Credential type not supported for rebuild"):
|
||||
TriggerProviderService.rebuild_trigger_subscription(
|
||||
tenant_id=tenant.id,
|
||||
provider_id=provider_id,
|
||||
subscription_id=subscription.id,
|
||||
credentials={},
|
||||
parameters={},
|
||||
)
|
||||
|
||||
def test_rebuild_trigger_subscription_name_uniqueness_check(
|
||||
self, db_session_with_containers, mock_external_service_dependencies
|
||||
):
|
||||
"""
|
||||
Test that name uniqueness is checked when updating name.
|
||||
|
||||
This test verifies:
|
||||
- Error is raised when new name conflicts with existing subscription
|
||||
"""
|
||||
fake = Faker()
|
||||
account, tenant = self._create_test_account_and_tenant(
|
||||
db_session_with_containers, mock_external_service_dependencies
|
||||
)
|
||||
|
||||
provider_id = TriggerProviderID("test_org/test_plugin/test_provider")
|
||||
credential_type = CredentialType.API_KEY
|
||||
|
||||
# Create first subscription
|
||||
subscription1 = self._create_test_subscription(
|
||||
db_session_with_containers,
|
||||
tenant.id,
|
||||
account.id,
|
||||
provider_id,
|
||||
credential_type,
|
||||
{"api_key": "key1"},
|
||||
mock_external_service_dependencies,
|
||||
)
|
||||
|
||||
# Create second subscription with different name
|
||||
subscription2 = self._create_test_subscription(
|
||||
db_session_with_containers,
|
||||
tenant.id,
|
||||
account.id,
|
||||
provider_id,
|
||||
credential_type,
|
||||
{"api_key": "key2"},
|
||||
mock_external_service_dependencies,
|
||||
)
|
||||
|
||||
new_subscription_entity = TriggerSubscriptionEntity(
|
||||
endpoint=subscription2.endpoint_id,
|
||||
parameters={},
|
||||
properties={},
|
||||
expires_at=-1,
|
||||
)
|
||||
mock_external_service_dependencies["trigger_manager"].subscribe_trigger.return_value = new_subscription_entity
|
||||
mock_external_service_dependencies["trigger_manager"].unsubscribe_trigger.return_value = MagicMock()
|
||||
|
||||
# Try to rename subscription2 to subscription1's name (should fail)
|
||||
with pytest.raises(ValueError, match="already exists"):
|
||||
TriggerProviderService.rebuild_trigger_subscription(
|
||||
tenant_id=tenant.id,
|
||||
provider_id=provider_id,
|
||||
subscription_id=subscription2.id,
|
||||
credentials={"api_key": "new-key"},
|
||||
parameters={},
|
||||
name=subscription1.name, # Conflicting name
|
||||
)
|
||||
|
|
@ -12,10 +12,12 @@ class TestJinja2CodeExecutor(CodeExecutorTestMixin):
|
|||
_, Jinja2TemplateTransformer = self.jinja2_imports
|
||||
|
||||
template = "Hello {{template}}"
|
||||
# Template must be base64 encoded to match the new safe embedding approach
|
||||
template_b64 = base64.b64encode(template.encode("utf-8")).decode("utf-8")
|
||||
inputs = base64.b64encode(b'{"template": "World"}').decode("utf-8")
|
||||
code = (
|
||||
Jinja2TemplateTransformer.get_runner_script()
|
||||
.replace(Jinja2TemplateTransformer._code_placeholder, template)
|
||||
.replace(Jinja2TemplateTransformer._template_b64_placeholder, template_b64)
|
||||
.replace(Jinja2TemplateTransformer._inputs_placeholder, inputs)
|
||||
)
|
||||
result = CodeExecutor.execute_code(
|
||||
|
|
@ -37,6 +39,34 @@ class TestJinja2CodeExecutor(CodeExecutorTestMixin):
|
|||
_, Jinja2TemplateTransformer = self.jinja2_imports
|
||||
|
||||
runner_script = Jinja2TemplateTransformer.get_runner_script()
|
||||
assert runner_script.count(Jinja2TemplateTransformer._code_placeholder) == 1
|
||||
assert runner_script.count(Jinja2TemplateTransformer._template_b64_placeholder) == 1
|
||||
assert runner_script.count(Jinja2TemplateTransformer._inputs_placeholder) == 1
|
||||
assert runner_script.count(Jinja2TemplateTransformer._result_tag) == 2
|
||||
|
||||
def test_jinja2_template_with_special_characters(self, flask_app_with_containers):
|
||||
"""
|
||||
Test that templates with special characters (quotes, newlines) render correctly.
|
||||
This is a regression test for issue #26818 where textarea pre-fill values
|
||||
containing special characters would break template rendering.
|
||||
"""
|
||||
CodeExecutor, CodeLanguage = self.code_executor_imports
|
||||
|
||||
# Template with triple quotes, single quotes, double quotes, and newlines
|
||||
template = """<html>
|
||||
<body>
|
||||
<input value="{{ task.get('Task ID', '') }}"/>
|
||||
<textarea>{{ task.get('Issues', 'No issues reported') }}</textarea>
|
||||
<p>Status: "{{ status }}"</p>
|
||||
<pre>'''code block'''</pre>
|
||||
</body>
|
||||
</html>"""
|
||||
inputs = {"task": {"Task ID": "TASK-123", "Issues": "Line 1\nLine 2\nLine 3"}, "status": "completed"}
|
||||
|
||||
result = CodeExecutor.execute_workflow_code_template(language=CodeLanguage.JINJA2, code=template, inputs=inputs)
|
||||
|
||||
# Verify the template rendered correctly with all special characters
|
||||
output = result["result"]
|
||||
assert 'value="TASK-123"' in output
|
||||
assert "<textarea>Line 1\nLine 2\nLine 3</textarea>" in output
|
||||
assert 'Status: "completed"' in output
|
||||
assert "'''code block'''" in output
|
||||
|
|
|
|||
|
|
@ -0,0 +1,122 @@
|
|||
import base64
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from core.mcp.types import (
|
||||
AudioContent,
|
||||
BlobResourceContents,
|
||||
CallToolResult,
|
||||
EmbeddedResource,
|
||||
ImageContent,
|
||||
TextResourceContents,
|
||||
)
|
||||
from core.tools.__base.tool_runtime import ToolRuntime
|
||||
from core.tools.entities.common_entities import I18nObject
|
||||
from core.tools.entities.tool_entities import ToolEntity, ToolIdentity, ToolInvokeMessage
|
||||
from core.tools.mcp_tool.tool import MCPTool
|
||||
|
||||
|
||||
def _make_mcp_tool(output_schema: dict | None = None) -> MCPTool:
|
||||
identity = ToolIdentity(
|
||||
author="test",
|
||||
name="test_mcp_tool",
|
||||
label=I18nObject(en_US="Test MCP Tool", zh_Hans="测试MCP工具"),
|
||||
provider="test_provider",
|
||||
)
|
||||
entity = ToolEntity(identity=identity, output_schema=output_schema or {})
|
||||
runtime = Mock(spec=ToolRuntime)
|
||||
runtime.credentials = {}
|
||||
return MCPTool(
|
||||
entity=entity,
|
||||
runtime=runtime,
|
||||
tenant_id="test_tenant",
|
||||
icon="",
|
||||
server_url="https://server.invalid",
|
||||
provider_id="provider_1",
|
||||
headers={},
|
||||
)
|
||||
|
||||
|
||||
class TestMCPToolInvoke:
|
||||
@pytest.mark.parametrize(
|
||||
("content_factory", "mime_type"),
|
||||
[
|
||||
(
|
||||
lambda b64, mt: ImageContent(type="image", data=b64, mimeType=mt),
|
||||
"image/png",
|
||||
),
|
||||
(
|
||||
lambda b64, mt: AudioContent(type="audio", data=b64, mimeType=mt),
|
||||
"audio/mpeg",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_invoke_image_or_audio_yields_blob(self, content_factory, mime_type) -> None:
|
||||
tool = _make_mcp_tool()
|
||||
raw = b"\x00\x01test-bytes\x02"
|
||||
b64 = base64.b64encode(raw).decode()
|
||||
content = content_factory(b64, mime_type)
|
||||
result = CallToolResult(content=[content])
|
||||
|
||||
with patch.object(tool, "invoke_remote_mcp_tool", return_value=result):
|
||||
messages = list(tool._invoke(user_id="test_user", tool_parameters={}))
|
||||
|
||||
assert len(messages) == 1
|
||||
msg = messages[0]
|
||||
assert msg.type == ToolInvokeMessage.MessageType.BLOB
|
||||
assert isinstance(msg.message, ToolInvokeMessage.BlobMessage)
|
||||
assert msg.message.blob == raw
|
||||
assert msg.meta == {"mime_type": mime_type}
|
||||
|
||||
def test_invoke_embedded_text_resource_yields_text(self) -> None:
|
||||
tool = _make_mcp_tool()
|
||||
text_resource = TextResourceContents(uri="file://test.txt", mimeType="text/plain", text="hello world")
|
||||
content = EmbeddedResource(type="resource", resource=text_resource)
|
||||
result = CallToolResult(content=[content])
|
||||
|
||||
with patch.object(tool, "invoke_remote_mcp_tool", return_value=result):
|
||||
messages = list(tool._invoke(user_id="test_user", tool_parameters={}))
|
||||
|
||||
assert len(messages) == 1
|
||||
msg = messages[0]
|
||||
assert msg.type == ToolInvokeMessage.MessageType.TEXT
|
||||
assert isinstance(msg.message, ToolInvokeMessage.TextMessage)
|
||||
assert msg.message.text == "hello world"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("mime_type", "expected_mime"),
|
||||
[("application/pdf", "application/pdf"), (None, "application/octet-stream")],
|
||||
)
|
||||
def test_invoke_embedded_blob_resource_yields_blob(self, mime_type, expected_mime) -> None:
|
||||
tool = _make_mcp_tool()
|
||||
raw = b"binary-data"
|
||||
b64 = base64.b64encode(raw).decode()
|
||||
blob_resource = BlobResourceContents(uri="file://doc.bin", mimeType=mime_type, blob=b64)
|
||||
content = EmbeddedResource(type="resource", resource=blob_resource)
|
||||
result = CallToolResult(content=[content])
|
||||
|
||||
with patch.object(tool, "invoke_remote_mcp_tool", return_value=result):
|
||||
messages = list(tool._invoke(user_id="test_user", tool_parameters={}))
|
||||
|
||||
assert len(messages) == 1
|
||||
msg = messages[0]
|
||||
assert msg.type == ToolInvokeMessage.MessageType.BLOB
|
||||
assert isinstance(msg.message, ToolInvokeMessage.BlobMessage)
|
||||
assert msg.message.blob == raw
|
||||
assert msg.meta == {"mime_type": expected_mime}
|
||||
|
||||
def test_invoke_yields_variables_when_structured_content_and_schema(self) -> None:
|
||||
tool = _make_mcp_tool(output_schema={"type": "object"})
|
||||
result = CallToolResult(content=[], structuredContent={"a": 1, "b": "x"})
|
||||
|
||||
with patch.object(tool, "invoke_remote_mcp_tool", return_value=result):
|
||||
messages = list(tool._invoke(user_id="test_user", tool_parameters={}))
|
||||
|
||||
# Expect two variable messages corresponding to keys a and b
|
||||
assert len(messages) == 2
|
||||
var_msgs = [m for m in messages if isinstance(m.message, ToolInvokeMessage.VariableMessage)]
|
||||
assert {m.message.variable_name for m in var_msgs} == {"a", "b"}
|
||||
# Validate values
|
||||
values = {m.message.variable_name: m.message.variable_value for m in var_msgs}
|
||||
assert values == {"a": 1, "b": "x"}
|
||||
|
|
@ -3072,11 +3072,11 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "json-repair"
|
||||
version = "0.54.1"
|
||||
version = "0.54.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/00/46/d3a4d9a3dad39bb4a2ad16b8adb9fe2e8611b20b71197fe33daa6768e85d/json_repair-0.54.1.tar.gz", hash = "sha256:d010bc31f1fc66e7c36dc33bff5f8902674498ae5cb8e801ad455a53b455ad1d", size = 38555, upload-time = "2025-11-19T14:55:24.265Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b5/86/48b12ac02032f121ac7e5f11a32143edca6c1e3d19ffc54d6fb9ca0aafd0/json_repair-0.54.3.tar.gz", hash = "sha256:e50feec9725e52ac91f12184609754684ac1656119dfbd31de09bdaf9a1d8bf6", size = 38626, upload-time = "2025-12-15T09:41:58.594Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/db/96/c9aad7ee949cc1bf15df91f347fbc2d3bd10b30b80c7df689ce6fe9332b5/json_repair-0.54.1-py3-none-any.whl", hash = "sha256:016160c5db5d5fe443164927bb58d2dfbba5f43ad85719fa9bc51c713a443ab1", size = 29311, upload-time = "2025-11-19T14:55:22.886Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/08/abe317237add63c3e62f18a981bccf92112b431835b43d844aedaf61f4a0/json_repair-0.54.3-py3-none-any.whl", hash = "sha256:4cdc132ee27d4780576f71bf27a113877046224a808bfc17392e079cb344fb81", size = 29357, upload-time = "2025-12-15T09:41:57.436Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -54,17 +54,17 @@
|
|||
"publish:npm": "./scripts/publish.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.3.5"
|
||||
"axios": "^1.13.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.2.0",
|
||||
"@types/node": "^20.11.30",
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@types/node": "^25.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^8.50.1",
|
||||
"@typescript-eslint/parser": "^8.50.1",
|
||||
"@vitest/coverage-v8": "1.6.1",
|
||||
"eslint": "^9.2.0",
|
||||
"@vitest/coverage-v8": "4.0.16",
|
||||
"eslint": "^9.39.2",
|
||||
"tsup": "^8.5.1",
|
||||
"typescript": "^5.4.5",
|
||||
"vitest": "^1.5.0"
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^4.0.16"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,2 @@
|
|||
onlyBuiltDependencies:
|
||||
- esbuild
|
||||
|
|
@ -16,7 +16,7 @@ const getSupportedLocales = (): string[] => {
|
|||
|
||||
// Helper function to load translation file content
|
||||
const loadTranslationContent = (locale: string): string => {
|
||||
const filePath = path.join(I18N_DIR, locale, 'app-debug.ts')
|
||||
const filePath = path.join(I18N_DIR, locale, 'app-debug.json')
|
||||
|
||||
if (!fs.existsSync(filePath))
|
||||
throw new Error(`Translation file not found: ${filePath}`)
|
||||
|
|
@ -24,14 +24,14 @@ const loadTranslationContent = (locale: string): string => {
|
|||
return fs.readFileSync(filePath, 'utf-8')
|
||||
}
|
||||
|
||||
// Helper function to check if upload features exist
|
||||
// Helper function to check if upload features exist (supports flattened JSON)
|
||||
const hasUploadFeatures = (content: string): { [key: string]: boolean } => {
|
||||
return {
|
||||
fileUpload: /fileUpload\s*:\s*\{/.test(content),
|
||||
imageUpload: /imageUpload\s*:\s*\{/.test(content),
|
||||
documentUpload: /documentUpload\s*:\s*\{/.test(content),
|
||||
audioUpload: /audioUpload\s*:\s*\{/.test(content),
|
||||
featureBar: /bar\s*:\s*\{/.test(content),
|
||||
fileUpload: /"feature\.fileUpload\.title"/.test(content),
|
||||
imageUpload: /"feature\.imageUpload\.title"/.test(content),
|
||||
documentUpload: /"feature\.documentUpload\.title"/.test(content),
|
||||
audioUpload: /"feature\.audioUpload\.title"/.test(content),
|
||||
featureBar: /"feature\.bar\.empty"/.test(content),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -45,7 +45,7 @@ describe('Upload Features i18n Translations - Issue #23062', () => {
|
|||
|
||||
it('all locales should have translation files', () => {
|
||||
supportedLocales.forEach((locale) => {
|
||||
const filePath = path.join(I18N_DIR, locale, 'app-debug.ts')
|
||||
const filePath = path.join(I18N_DIR, locale, 'app-debug.json')
|
||||
expect(fs.existsSync(filePath)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
|
@ -76,12 +76,9 @@ describe('Upload Features i18n Translations - Issue #23062', () => {
|
|||
previouslyMissingLocales.forEach((locale) => {
|
||||
const content = loadTranslationContent(locale)
|
||||
|
||||
// Verify audioUpload exists
|
||||
expect(/audioUpload\s*:\s*\{/.test(content)).toBe(true)
|
||||
|
||||
// Verify it has title and description
|
||||
expect(/audioUpload[^}]*title\s*:/.test(content)).toBe(true)
|
||||
expect(/audioUpload[^}]*description\s*:/.test(content)).toBe(true)
|
||||
// Verify audioUpload exists with title and description (flattened JSON format)
|
||||
expect(/"feature\.audioUpload\.title"/.test(content)).toBe(true)
|
||||
expect(/"feature\.audioUpload\.description"/.test(content)).toBe(true)
|
||||
|
||||
console.log(`✅ ${locale} - Issue #23062 resolved: audioUpload feature present`)
|
||||
})
|
||||
|
|
@ -91,28 +88,28 @@ describe('Upload Features i18n Translations - Issue #23062', () => {
|
|||
supportedLocales.forEach((locale) => {
|
||||
const content = loadTranslationContent(locale)
|
||||
|
||||
// Check fileUpload has required properties
|
||||
if (/fileUpload\s*:\s*\{/.test(content)) {
|
||||
expect(/fileUpload[^}]*title\s*:/.test(content)).toBe(true)
|
||||
expect(/fileUpload[^}]*description\s*:/.test(content)).toBe(true)
|
||||
// Check fileUpload has required properties (flattened JSON format)
|
||||
if (/"feature\.fileUpload\.title"/.test(content)) {
|
||||
expect(/"feature\.fileUpload\.title"/.test(content)).toBe(true)
|
||||
expect(/"feature\.fileUpload\.description"/.test(content)).toBe(true)
|
||||
}
|
||||
|
||||
// Check imageUpload has required properties
|
||||
if (/imageUpload\s*:\s*\{/.test(content)) {
|
||||
expect(/imageUpload[^}]*title\s*:/.test(content)).toBe(true)
|
||||
expect(/imageUpload[^}]*description\s*:/.test(content)).toBe(true)
|
||||
if (/"feature\.imageUpload\.title"/.test(content)) {
|
||||
expect(/"feature\.imageUpload\.title"/.test(content)).toBe(true)
|
||||
expect(/"feature\.imageUpload\.description"/.test(content)).toBe(true)
|
||||
}
|
||||
|
||||
// Check documentUpload has required properties
|
||||
if (/documentUpload\s*:\s*\{/.test(content)) {
|
||||
expect(/documentUpload[^}]*title\s*:/.test(content)).toBe(true)
|
||||
expect(/documentUpload[^}]*description\s*:/.test(content)).toBe(true)
|
||||
if (/"feature\.documentUpload\.title"/.test(content)) {
|
||||
expect(/"feature\.documentUpload\.title"/.test(content)).toBe(true)
|
||||
expect(/"feature\.documentUpload\.description"/.test(content)).toBe(true)
|
||||
}
|
||||
|
||||
// Check audioUpload has required properties
|
||||
if (/audioUpload\s*:\s*\{/.test(content)) {
|
||||
expect(/audioUpload[^}]*title\s*:/.test(content)).toBe(true)
|
||||
expect(/audioUpload[^}]*description\s*:/.test(content)).toBe(true)
|
||||
if (/"feature\.audioUpload\.title"/.test(content)) {
|
||||
expect(/"feature\.audioUpload\.title"/.test(content)).toBe(true)
|
||||
expect(/"feature\.audioUpload\.description"/.test(content)).toBe(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
|||
const navConfig = [
|
||||
...(isCurrentWorkspaceEditor
|
||||
? [{
|
||||
name: t('common.appMenus.promptEng'),
|
||||
name: t('appMenus.promptEng', { ns: 'common' }),
|
||||
href: `/app/${appId}/${(mode === AppModeEnum.WORKFLOW || mode === AppModeEnum.ADVANCED_CHAT) ? 'workflow' : 'configuration'}`,
|
||||
icon: RiTerminalWindowLine,
|
||||
selectedIcon: RiTerminalWindowFill,
|
||||
|
|
@ -78,7 +78,7 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
|||
: []
|
||||
),
|
||||
{
|
||||
name: t('common.appMenus.apiAccess'),
|
||||
name: t('appMenus.apiAccess', { ns: 'common' }),
|
||||
href: `/app/${appId}/develop`,
|
||||
icon: RiTerminalBoxLine,
|
||||
selectedIcon: RiTerminalBoxFill,
|
||||
|
|
@ -86,8 +86,8 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
|||
...(isCurrentWorkspaceEditor
|
||||
? [{
|
||||
name: mode !== AppModeEnum.WORKFLOW
|
||||
? t('common.appMenus.logAndAnn')
|
||||
: t('common.appMenus.logs'),
|
||||
? t('appMenus.logAndAnn', { ns: 'common' })
|
||||
: t('appMenus.logs', { ns: 'common' }),
|
||||
href: `/app/${appId}/logs`,
|
||||
icon: RiFileList3Line,
|
||||
selectedIcon: RiFileList3Fill,
|
||||
|
|
@ -95,7 +95,7 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
|||
: []
|
||||
),
|
||||
{
|
||||
name: t('common.appMenus.overview'),
|
||||
name: t('appMenus.overview', { ns: 'common' }),
|
||||
href: `/app/${appId}/overview`,
|
||||
icon: RiDashboard2Line,
|
||||
selectedIcon: RiDashboard2Fill,
|
||||
|
|
@ -104,7 +104,7 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
|||
return navConfig
|
||||
}, [t])
|
||||
|
||||
useDocumentTitle(appDetail?.name || t('common.menus.appDetail'))
|
||||
useDocumentTitle(appDetail?.name || t('menus.appDetail', { ns: 'common' }))
|
||||
|
||||
useEffect(() => {
|
||||
if (appDetail) {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import type { IAppCardProps } from '@/app/components/app/overview/app-card'
|
|||
import type { BlockEnum } from '@/app/components/workflow/types'
|
||||
import type { UpdateAppSiteCodeResponse } from '@/models/app'
|
||||
import type { App } from '@/types/app'
|
||||
import type { I18nKeysByPrefix } from '@/types/i18n'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
|
@ -62,7 +63,7 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
|
|||
const buildTriggerModeMessage = useCallback((featureName: string) => (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="text-xs text-text-secondary">
|
||||
{t('appOverview.overview.disableTooltip.triggerMode', { feature: featureName })}
|
||||
{t('overview.disableTooltip.triggerMode', { ns: 'appOverview', feature: featureName })}
|
||||
</div>
|
||||
<div
|
||||
className="cursor-pointer text-xs font-medium text-text-accent hover:underline"
|
||||
|
|
@ -71,19 +72,19 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
|
|||
window.open(triggerDocUrl, '_blank')
|
||||
}}
|
||||
>
|
||||
{t('appOverview.overview.appInfo.enableTooltip.learnMore')}
|
||||
{t('overview.appInfo.enableTooltip.learnMore', { ns: 'appOverview' })}
|
||||
</div>
|
||||
</div>
|
||||
), [t, triggerDocUrl])
|
||||
|
||||
const disableWebAppTooltip = disableAppCards
|
||||
? buildTriggerModeMessage(t('appOverview.overview.appInfo.title'))
|
||||
? buildTriggerModeMessage(t('overview.appInfo.title', { ns: 'appOverview' }))
|
||||
: null
|
||||
const disableApiTooltip = disableAppCards
|
||||
? buildTriggerModeMessage(t('appOverview.overview.apiInfo.title'))
|
||||
? buildTriggerModeMessage(t('overview.apiInfo.title', { ns: 'appOverview' }))
|
||||
: null
|
||||
const disableMcpTooltip = disableAppCards
|
||||
? buildTriggerModeMessage(t('tools.mcp.server.title'))
|
||||
? buildTriggerModeMessage(t('mcp.server.title', { ns: 'tools' }))
|
||||
: null
|
||||
|
||||
const updateAppDetail = async () => {
|
||||
|
|
@ -94,7 +95,7 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
|
|||
catch (error) { console.error(error) }
|
||||
}
|
||||
|
||||
const handleCallbackResult = (err: Error | null, message?: string) => {
|
||||
const handleCallbackResult = (err: Error | null, message?: I18nKeysByPrefix<'common', 'actionMsg.'>) => {
|
||||
const type = err ? 'error' : 'success'
|
||||
|
||||
message ||= (type === 'success' ? 'modifiedSuccessfully' : 'modifiedUnsuccessfully')
|
||||
|
|
@ -104,7 +105,7 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
|
|||
|
||||
notify({
|
||||
type,
|
||||
message: t(`common.actionMsg.${message}` as any) as string,
|
||||
message: t(`actionMsg.${message}`, { ns: 'common' }) as string,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
'use client'
|
||||
import type { PeriodParams } from '@/app/components/app/overview/app-chart'
|
||||
import type { I18nKeysByPrefix } from '@/types/i18n'
|
||||
import dayjs from 'dayjs'
|
||||
import quarterOfYear from 'dayjs/plugin/quarterOfYear'
|
||||
import * as React from 'react'
|
||||
|
|
@ -16,7 +17,9 @@ dayjs.extend(quarterOfYear)
|
|||
|
||||
const today = dayjs()
|
||||
|
||||
const TIME_PERIOD_MAPPING = [
|
||||
type TimePeriodName = I18nKeysByPrefix<'appLog', 'filter.period.'>
|
||||
|
||||
const TIME_PERIOD_MAPPING: { value: number, name: TimePeriodName }[] = [
|
||||
{ value: 0, name: 'today' },
|
||||
{ value: 7, name: 'last7days' },
|
||||
{ value: 30, name: 'last30days' },
|
||||
|
|
@ -35,8 +38,8 @@ export default function ChartView({ appId, headerRight }: IChartViewProps) {
|
|||
const isChatApp = appDetail?.mode !== 'completion' && appDetail?.mode !== 'workflow'
|
||||
const isWorkflow = appDetail?.mode === 'workflow'
|
||||
const [period, setPeriod] = useState<PeriodParams>(IS_CLOUD_EDITION
|
||||
? { name: t('appLog.filter.period.today'), query: { start: today.startOf('day').format(queryDateFormat), end: today.endOf('day').format(queryDateFormat) } }
|
||||
: { name: t('appLog.filter.period.last7days'), query: { start: today.subtract(7, 'day').startOf('day').format(queryDateFormat), end: today.endOf('day').format(queryDateFormat) } },
|
||||
? { name: t('filter.period.today', { ns: 'appLog' }), query: { start: today.startOf('day').format(queryDateFormat), end: today.endOf('day').format(queryDateFormat) } }
|
||||
: { name: t('filter.period.last7days', { ns: 'appLog' }), query: { start: today.subtract(7, 'day').startOf('day').format(queryDateFormat), end: today.endOf('day').format(queryDateFormat) } },
|
||||
)
|
||||
|
||||
if (!appDetail)
|
||||
|
|
@ -45,7 +48,7 @@ export default function ChartView({ appId, headerRight }: IChartViewProps) {
|
|||
return (
|
||||
<div>
|
||||
<div className="mb-4">
|
||||
<div className="system-xl-semibold mb-2 text-text-primary">{t('common.appMenus.overview')}</div>
|
||||
<div className="system-xl-semibold mb-2 text-text-primary">{t('appMenus.overview', { ns: 'common' })}</div>
|
||||
<div className="flex items-center justify-between">
|
||||
{IS_CLOUD_EDITION
|
||||
? (
|
||||
|
|
|
|||
|
|
@ -2,13 +2,16 @@
|
|||
import type { FC } from 'react'
|
||||
import type { PeriodParams } from '@/app/components/app/overview/app-chart'
|
||||
import type { Item } from '@/app/components/base/select'
|
||||
import type { I18nKeysByPrefix } from '@/types/i18n'
|
||||
import dayjs from 'dayjs'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { SimpleSelect } from '@/app/components/base/select'
|
||||
|
||||
type TimePeriodName = I18nKeysByPrefix<'appLog', 'filter.period.'>
|
||||
|
||||
type Props = {
|
||||
periodMapping: { [key: string]: { value: number, name: string } }
|
||||
periodMapping: { [key: string]: { value: number, name: TimePeriodName } }
|
||||
onSelect: (payload: PeriodParams) => void
|
||||
queryDateFormat: string
|
||||
}
|
||||
|
|
@ -25,9 +28,9 @@ const LongTimeRangePicker: FC<Props> = ({
|
|||
const handleSelect = React.useCallback((item: Item) => {
|
||||
const id = item.value
|
||||
const value = periodMapping[id]?.value ?? '-1'
|
||||
const name = item.name || t('appLog.filter.period.allTime')
|
||||
const name = item.name || t('filter.period.allTime', { ns: 'appLog' })
|
||||
if (value === -1) {
|
||||
onSelect({ name: t('appLog.filter.period.allTime'), query: undefined })
|
||||
onSelect({ name: t('filter.period.allTime', { ns: 'appLog' }), query: undefined })
|
||||
}
|
||||
else if (value === 0) {
|
||||
const startOfToday = today.startOf('day').format(queryDateFormat)
|
||||
|
|
@ -53,7 +56,7 @@ const LongTimeRangePicker: FC<Props> = ({
|
|||
|
||||
return (
|
||||
<SimpleSelect
|
||||
items={Object.entries(periodMapping).map(([k, v]) => ({ value: k, name: t(`appLog.filter.period.${v.name}` as any) as string }))}
|
||||
items={Object.entries(periodMapping).map(([k, v]) => ({ value: k, name: t(`filter.period.${v.name}`, { ns: 'appLog' }) }))}
|
||||
className="mt-0 !w-40"
|
||||
notClearable={true}
|
||||
onSelect={handleSelect}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import type { Dayjs } from 'dayjs'
|
||||
import type { FC } from 'react'
|
||||
import type { PeriodParams, PeriodParamsWithTimeRange } from '@/app/components/app/overview/app-chart'
|
||||
import type { I18nKeysByPrefix } from '@/types/i18n'
|
||||
import dayjs from 'dayjs'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
|
|
@ -13,8 +14,10 @@ import RangeSelector from './range-selector'
|
|||
|
||||
const today = dayjs()
|
||||
|
||||
type TimePeriodName = I18nKeysByPrefix<'appLog', 'filter.period.'>
|
||||
|
||||
type Props = {
|
||||
ranges: { value: number, name: string }[]
|
||||
ranges: { value: number, name: TimePeriodName }[]
|
||||
onSelect: (payload: PeriodParams) => void
|
||||
queryDateFormat: string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import type { FC } from 'react'
|
||||
import type { PeriodParamsWithTimeRange, TimeRange } from '@/app/components/app/overview/app-chart'
|
||||
import type { Item } from '@/app/components/base/select'
|
||||
import type { I18nKeysByPrefix } from '@/types/i18n'
|
||||
import { RiArrowDownSLine, RiCheckLine } from '@remixicon/react'
|
||||
import dayjs from 'dayjs'
|
||||
import * as React from 'react'
|
||||
|
|
@ -12,9 +13,11 @@ import { cn } from '@/utils/classnames'
|
|||
|
||||
const today = dayjs()
|
||||
|
||||
type TimePeriodName = I18nKeysByPrefix<'appLog', 'filter.period.'>
|
||||
|
||||
type Props = {
|
||||
isCustomRange: boolean
|
||||
ranges: { value: number, name: string }[]
|
||||
ranges: { value: number, name: TimePeriodName }[]
|
||||
onSelect: (payload: PeriodParamsWithTimeRange) => void
|
||||
}
|
||||
|
||||
|
|
@ -42,7 +45,7 @@ const RangeSelector: FC<Props> = ({
|
|||
const renderTrigger = useCallback((item: Item | null, isOpen: boolean) => {
|
||||
return (
|
||||
<div className={cn('flex h-8 cursor-pointer items-center space-x-1.5 rounded-lg bg-components-input-bg-normal pl-3 pr-2', isOpen && 'bg-state-base-hover-alt')}>
|
||||
<div className="system-sm-regular text-components-input-text-filled">{isCustomRange ? t('appLog.filter.period.custom') : item?.name}</div>
|
||||
<div className="system-sm-regular text-components-input-text-filled">{isCustomRange ? t('filter.period.custom', { ns: 'appLog' }) : item?.name}</div>
|
||||
<RiArrowDownSLine className={cn('size-4 text-text-quaternary', isOpen && 'text-text-secondary')} />
|
||||
</div>
|
||||
)
|
||||
|
|
@ -66,7 +69,7 @@ const RangeSelector: FC<Props> = ({
|
|||
}, [])
|
||||
return (
|
||||
<SimpleSelect
|
||||
items={ranges.map(v => ({ ...v, name: t(`appLog.filter.period.${v.name}` as any) as string }))}
|
||||
items={ranges.map(v => ({ ...v, name: t(`filter.period.${v.name}`, { ns: 'appLog' }) }))}
|
||||
className="mt-0 !w-40"
|
||||
notClearable={true}
|
||||
onSelect={handleSelectRange}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import ProviderPanel from './provider-panel'
|
|||
import TracingIcon from './tracing-icon'
|
||||
import { TracingProvider } from './type'
|
||||
|
||||
const I18N_PREFIX = 'app.tracing'
|
||||
const I18N_PREFIX = 'tracing'
|
||||
|
||||
export type PopupProps = {
|
||||
appId: string
|
||||
|
|
@ -327,19 +327,19 @@ const ConfigPopup: FC<PopupProps> = ({
|
|||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<TracingIcon size="md" className="mr-2" />
|
||||
<div className="title-2xl-semi-bold text-text-primary">{t(`${I18N_PREFIX}.tracing`)}</div>
|
||||
<div className="title-2xl-semi-bold text-text-primary">{t(`${I18N_PREFIX}.tracing`, { ns: 'app' })}</div>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Indicator color={enabled ? 'green' : 'gray'} />
|
||||
<div className={cn('system-xs-semibold-uppercase ml-1 text-text-tertiary', enabled && 'text-util-colors-green-green-600')}>
|
||||
{t(`${I18N_PREFIX}.${enabled ? 'enabled' : 'disabled'}`)}
|
||||
{t(`${I18N_PREFIX}.${enabled ? 'enabled' : 'disabled'}`, { ns: 'app' })}
|
||||
</div>
|
||||
{!readOnly && (
|
||||
<>
|
||||
{providerAllNotConfigured
|
||||
? (
|
||||
<Tooltip
|
||||
popupContent={t(`${I18N_PREFIX}.disabledTip`)}
|
||||
popupContent={t(`${I18N_PREFIX}.disabledTip`, { ns: 'app' })}
|
||||
>
|
||||
{switchContent}
|
||||
</Tooltip>
|
||||
|
|
@ -351,14 +351,14 @@ const ConfigPopup: FC<PopupProps> = ({
|
|||
</div>
|
||||
|
||||
<div className="system-xs-regular mt-2 text-text-tertiary">
|
||||
{t(`${I18N_PREFIX}.tracingDescription`)}
|
||||
{t(`${I18N_PREFIX}.tracingDescription`, { ns: 'app' })}
|
||||
</div>
|
||||
<Divider className="my-3" />
|
||||
<div className="relative">
|
||||
{(providerAllConfigured || providerAllNotConfigured)
|
||||
? (
|
||||
<>
|
||||
<div className="system-xs-medium-uppercase text-text-tertiary">{t(`${I18N_PREFIX}.configProviderTitle.${providerAllConfigured ? 'configured' : 'notConfigured'}`)}</div>
|
||||
<div className="system-xs-medium-uppercase text-text-tertiary">{t(`${I18N_PREFIX}.configProviderTitle.${providerAllConfigured ? 'configured' : 'notConfigured'}`, { ns: 'app' })}</div>
|
||||
<div className="mt-2 max-h-96 space-y-2 overflow-y-auto">
|
||||
{langfusePanel}
|
||||
{langSmithPanel}
|
||||
|
|
@ -375,11 +375,11 @@ const ConfigPopup: FC<PopupProps> = ({
|
|||
)
|
||||
: (
|
||||
<>
|
||||
<div className="system-xs-medium-uppercase text-text-tertiary">{t(`${I18N_PREFIX}.configProviderTitle.configured`)}</div>
|
||||
<div className="system-xs-medium-uppercase text-text-tertiary">{t(`${I18N_PREFIX}.configProviderTitle.configured`, { ns: 'app' })}</div>
|
||||
<div className="mt-2 max-h-40 space-y-2 overflow-y-auto">
|
||||
{configuredProviderPanel()}
|
||||
</div>
|
||||
<div className="system-xs-medium-uppercase mt-3 text-text-tertiary">{t(`${I18N_PREFIX}.configProviderTitle.moreProvider`)}</div>
|
||||
<div className="system-xs-medium-uppercase mt-3 text-text-tertiary">{t(`${I18N_PREFIX}.configProviderTitle.moreProvider`, { ns: 'app' })}</div>
|
||||
<div className="mt-2 max-h-40 space-y-2 overflow-y-auto">
|
||||
{moreProviderPanel()}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import ConfigButton from './config-button'
|
|||
import TracingIcon from './tracing-icon'
|
||||
import { TracingProvider } from './type'
|
||||
|
||||
const I18N_PREFIX = 'app.tracing'
|
||||
const I18N_PREFIX = 'tracing'
|
||||
|
||||
const Panel: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
|
@ -45,7 +45,7 @@ const Panel: FC = () => {
|
|||
if (!noToast) {
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('common.api.success'),
|
||||
message: t('api.success', { ns: 'common' }),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -254,7 +254,7 @@ const Panel: FC = () => {
|
|||
)}
|
||||
>
|
||||
<TracingIcon size="md" />
|
||||
<div className="system-sm-semibold mx-2 text-text-secondary">{t(`${I18N_PREFIX}.title`)}</div>
|
||||
<div className="system-sm-semibold mx-2 text-text-secondary">{t(`${I18N_PREFIX}.title`, { ns: 'app' })}</div>
|
||||
<div className="rounded-md p-1">
|
||||
<RiEqualizer2Line className="h-4 w-4 text-text-tertiary" />
|
||||
</div>
|
||||
|
|
@ -295,7 +295,7 @@ const Panel: FC = () => {
|
|||
<div className="ml-4 mr-1 flex items-center">
|
||||
<Indicator color={enabled ? 'green' : 'gray'} />
|
||||
<div className="system-xs-semibold-uppercase ml-1.5 text-text-tertiary">
|
||||
{t(`${I18N_PREFIX}.${enabled ? 'enabled' : 'disabled'}`)}
|
||||
{t(`${I18N_PREFIX}.${enabled ? 'enabled' : 'disabled'}`, { ns: 'app' })}
|
||||
</div>
|
||||
</div>
|
||||
{InUseProviderIcon && <InUseProviderIcon className="ml-1 h-4" />}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ type Props = {
|
|||
onChosen: (provider: TracingProvider) => void
|
||||
}
|
||||
|
||||
const I18N_PREFIX = 'app.tracing.configProvider'
|
||||
const I18N_PREFIX = 'tracing.configProvider'
|
||||
|
||||
const arizeConfigTemplate = {
|
||||
api_key: '',
|
||||
|
|
@ -157,7 +157,7 @@ const ProviderConfigModal: FC<Props> = ({
|
|||
})
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('common.api.remove'),
|
||||
message: t('api.remove', { ns: 'common' }),
|
||||
})
|
||||
onRemoved()
|
||||
hideRemoveConfirm()
|
||||
|
|
@ -177,37 +177,37 @@ const ProviderConfigModal: FC<Props> = ({
|
|||
if (type === TracingProvider.arize) {
|
||||
const postData = config as ArizeConfig
|
||||
if (!postData.api_key)
|
||||
errorMessage = t('common.errorMsg.fieldRequired', { field: 'API Key' })
|
||||
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: 'API Key' })
|
||||
if (!postData.space_id)
|
||||
errorMessage = t('common.errorMsg.fieldRequired', { field: 'Space ID' })
|
||||
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: 'Space ID' })
|
||||
if (!errorMessage && !postData.project)
|
||||
errorMessage = t('common.errorMsg.fieldRequired', { field: t(`${I18N_PREFIX}.project`) })
|
||||
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: t(`${I18N_PREFIX}.project`, { ns: 'app' }) })
|
||||
}
|
||||
|
||||
if (type === TracingProvider.phoenix) {
|
||||
const postData = config as PhoenixConfig
|
||||
if (!postData.api_key)
|
||||
errorMessage = t('common.errorMsg.fieldRequired', { field: 'API Key' })
|
||||
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: 'API Key' })
|
||||
if (!errorMessage && !postData.project)
|
||||
errorMessage = t('common.errorMsg.fieldRequired', { field: t(`${I18N_PREFIX}.project`) })
|
||||
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: t(`${I18N_PREFIX}.project`, { ns: 'app' }) })
|
||||
}
|
||||
|
||||
if (type === TracingProvider.langSmith) {
|
||||
const postData = config as LangSmithConfig
|
||||
if (!postData.api_key)
|
||||
errorMessage = t('common.errorMsg.fieldRequired', { field: 'API Key' })
|
||||
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: 'API Key' })
|
||||
if (!errorMessage && !postData.project)
|
||||
errorMessage = t('common.errorMsg.fieldRequired', { field: t(`${I18N_PREFIX}.project`) })
|
||||
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: t(`${I18N_PREFIX}.project`, { ns: 'app' }) })
|
||||
}
|
||||
|
||||
if (type === TracingProvider.langfuse) {
|
||||
const postData = config as LangFuseConfig
|
||||
if (!errorMessage && !postData.secret_key)
|
||||
errorMessage = t('common.errorMsg.fieldRequired', { field: t(`${I18N_PREFIX}.secretKey`) })
|
||||
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: t(`${I18N_PREFIX}.secretKey`, { ns: 'app' }) })
|
||||
if (!errorMessage && !postData.public_key)
|
||||
errorMessage = t('common.errorMsg.fieldRequired', { field: t(`${I18N_PREFIX}.publicKey`) })
|
||||
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: t(`${I18N_PREFIX}.publicKey`, { ns: 'app' }) })
|
||||
if (!errorMessage && !postData.host)
|
||||
errorMessage = t('common.errorMsg.fieldRequired', { field: 'Host' })
|
||||
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: 'Host' })
|
||||
}
|
||||
|
||||
if (type === TracingProvider.opik) {
|
||||
|
|
@ -218,43 +218,43 @@ const ProviderConfigModal: FC<Props> = ({
|
|||
if (type === TracingProvider.weave) {
|
||||
const postData = config as WeaveConfig
|
||||
if (!errorMessage && !postData.api_key)
|
||||
errorMessage = t('common.errorMsg.fieldRequired', { field: 'API Key' })
|
||||
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: 'API Key' })
|
||||
if (!errorMessage && !postData.project)
|
||||
errorMessage = t('common.errorMsg.fieldRequired', { field: t(`${I18N_PREFIX}.project`) })
|
||||
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: t(`${I18N_PREFIX}.project`, { ns: 'app' }) })
|
||||
}
|
||||
|
||||
if (type === TracingProvider.aliyun) {
|
||||
const postData = config as AliyunConfig
|
||||
if (!errorMessage && !postData.app_name)
|
||||
errorMessage = t('common.errorMsg.fieldRequired', { field: 'App Name' })
|
||||
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: 'App Name' })
|
||||
if (!errorMessage && !postData.license_key)
|
||||
errorMessage = t('common.errorMsg.fieldRequired', { field: 'License Key' })
|
||||
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: 'License Key' })
|
||||
if (!errorMessage && !postData.endpoint)
|
||||
errorMessage = t('common.errorMsg.fieldRequired', { field: 'Endpoint' })
|
||||
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: 'Endpoint' })
|
||||
}
|
||||
|
||||
if (type === TracingProvider.mlflow) {
|
||||
const postData = config as MLflowConfig
|
||||
if (!errorMessage && !postData.tracking_uri)
|
||||
errorMessage = t('common.errorMsg.fieldRequired', { field: 'Tracking URI' })
|
||||
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: 'Tracking URI' })
|
||||
}
|
||||
|
||||
if (type === TracingProvider.databricks) {
|
||||
const postData = config as DatabricksConfig
|
||||
if (!errorMessage && !postData.experiment_id)
|
||||
errorMessage = t('common.errorMsg.fieldRequired', { field: 'Experiment ID' })
|
||||
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: 'Experiment ID' })
|
||||
if (!errorMessage && !postData.host)
|
||||
errorMessage = t('common.errorMsg.fieldRequired', { field: 'Host' })
|
||||
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: 'Host' })
|
||||
}
|
||||
|
||||
if (type === TracingProvider.tencent) {
|
||||
const postData = config as TencentConfig
|
||||
if (!errorMessage && !postData.token)
|
||||
errorMessage = t('common.errorMsg.fieldRequired', { field: 'Token' })
|
||||
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: 'Token' })
|
||||
if (!errorMessage && !postData.endpoint)
|
||||
errorMessage = t('common.errorMsg.fieldRequired', { field: 'Endpoint' })
|
||||
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: 'Endpoint' })
|
||||
if (!errorMessage && !postData.service_name)
|
||||
errorMessage = t('common.errorMsg.fieldRequired', { field: 'Service Name' })
|
||||
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: 'Service Name' })
|
||||
}
|
||||
|
||||
return errorMessage
|
||||
|
|
@ -281,7 +281,7 @@ const ProviderConfigModal: FC<Props> = ({
|
|||
})
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('common.api.success'),
|
||||
message: t('api.success', { ns: 'common' }),
|
||||
})
|
||||
onSaved(config)
|
||||
if (isAdd)
|
||||
|
|
@ -303,8 +303,8 @@ const ProviderConfigModal: FC<Props> = ({
|
|||
<div className="px-8 pt-8">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="title-2xl-semi-bold text-text-primary">
|
||||
{t(`${I18N_PREFIX}.title`)}
|
||||
{t(`app.tracing.${type}.title`)}
|
||||
{t(`${I18N_PREFIX}.title`, { ns: 'app' })}
|
||||
{t(`tracing.${type}.title`, { ns: 'app' })}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -317,7 +317,7 @@ const ProviderConfigModal: FC<Props> = ({
|
|||
isRequired
|
||||
value={(config as ArizeConfig).api_key}
|
||||
onChange={handleConfigChange('api_key')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'API Key' })!}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: 'API Key' })!}
|
||||
/>
|
||||
<Field
|
||||
label="Space ID"
|
||||
|
|
@ -325,15 +325,15 @@ const ProviderConfigModal: FC<Props> = ({
|
|||
isRequired
|
||||
value={(config as ArizeConfig).space_id}
|
||||
onChange={handleConfigChange('space_id')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'Space ID' })!}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: 'Space ID' })!}
|
||||
/>
|
||||
<Field
|
||||
label={t(`${I18N_PREFIX}.project`)!}
|
||||
label={t(`${I18N_PREFIX}.project`, { ns: 'app' })!}
|
||||
labelClassName="!text-sm"
|
||||
isRequired
|
||||
value={(config as ArizeConfig).project}
|
||||
onChange={handleConfigChange('project')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.project`) })!}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: t(`${I18N_PREFIX}.project`, { ns: 'app' }) })!}
|
||||
/>
|
||||
<Field
|
||||
label="Endpoint"
|
||||
|
|
@ -352,15 +352,15 @@ const ProviderConfigModal: FC<Props> = ({
|
|||
isRequired
|
||||
value={(config as PhoenixConfig).api_key}
|
||||
onChange={handleConfigChange('api_key')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'API Key' })!}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: 'API Key' })!}
|
||||
/>
|
||||
<Field
|
||||
label={t(`${I18N_PREFIX}.project`)!}
|
||||
label={t(`${I18N_PREFIX}.project`, { ns: 'app' })!}
|
||||
labelClassName="!text-sm"
|
||||
isRequired
|
||||
value={(config as PhoenixConfig).project}
|
||||
onChange={handleConfigChange('project')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.project`) })!}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: t(`${I18N_PREFIX}.project`, { ns: 'app' }) })!}
|
||||
/>
|
||||
<Field
|
||||
label="Endpoint"
|
||||
|
|
@ -379,7 +379,7 @@ const ProviderConfigModal: FC<Props> = ({
|
|||
isRequired
|
||||
value={(config as AliyunConfig).license_key}
|
||||
onChange={handleConfigChange('license_key')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'License Key' })!}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: 'License Key' })!}
|
||||
/>
|
||||
<Field
|
||||
label="Endpoint"
|
||||
|
|
@ -404,7 +404,7 @@ const ProviderConfigModal: FC<Props> = ({
|
|||
isRequired
|
||||
value={(config as TencentConfig).token}
|
||||
onChange={handleConfigChange('token')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'Token' })!}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: 'Token' })!}
|
||||
/>
|
||||
<Field
|
||||
label="Endpoint"
|
||||
|
|
@ -432,22 +432,22 @@ const ProviderConfigModal: FC<Props> = ({
|
|||
isRequired
|
||||
value={(config as WeaveConfig).api_key}
|
||||
onChange={handleConfigChange('api_key')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'API Key' })!}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: 'API Key' })!}
|
||||
/>
|
||||
<Field
|
||||
label={t(`${I18N_PREFIX}.project`)!}
|
||||
label={t(`${I18N_PREFIX}.project`, { ns: 'app' })!}
|
||||
labelClassName="!text-sm"
|
||||
isRequired
|
||||
value={(config as WeaveConfig).project}
|
||||
onChange={handleConfigChange('project')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.project`) })!}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: t(`${I18N_PREFIX}.project`, { ns: 'app' }) })!}
|
||||
/>
|
||||
<Field
|
||||
label="Entity"
|
||||
labelClassName="!text-sm"
|
||||
value={(config as WeaveConfig).entity}
|
||||
onChange={handleConfigChange('entity')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'Entity' })!}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: 'Entity' })!}
|
||||
/>
|
||||
<Field
|
||||
label="Endpoint"
|
||||
|
|
@ -473,15 +473,15 @@ const ProviderConfigModal: FC<Props> = ({
|
|||
isRequired
|
||||
value={(config as LangSmithConfig).api_key}
|
||||
onChange={handleConfigChange('api_key')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'API Key' })!}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: 'API Key' })!}
|
||||
/>
|
||||
<Field
|
||||
label={t(`${I18N_PREFIX}.project`)!}
|
||||
label={t(`${I18N_PREFIX}.project`, { ns: 'app' })!}
|
||||
labelClassName="!text-sm"
|
||||
isRequired
|
||||
value={(config as LangSmithConfig).project}
|
||||
onChange={handleConfigChange('project')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.project`) })!}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: t(`${I18N_PREFIX}.project`, { ns: 'app' }) })!}
|
||||
/>
|
||||
<Field
|
||||
label="Endpoint"
|
||||
|
|
@ -495,20 +495,20 @@ const ProviderConfigModal: FC<Props> = ({
|
|||
{type === TracingProvider.langfuse && (
|
||||
<>
|
||||
<Field
|
||||
label={t(`${I18N_PREFIX}.secretKey`)!}
|
||||
label={t(`${I18N_PREFIX}.secretKey`, { ns: 'app' })!}
|
||||
labelClassName="!text-sm"
|
||||
value={(config as LangFuseConfig).secret_key}
|
||||
isRequired
|
||||
onChange={handleConfigChange('secret_key')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.secretKey`) })!}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: t(`${I18N_PREFIX}.secretKey`, { ns: 'app' }) })!}
|
||||
/>
|
||||
<Field
|
||||
label={t(`${I18N_PREFIX}.publicKey`)!}
|
||||
label={t(`${I18N_PREFIX}.publicKey`, { ns: 'app' })!}
|
||||
labelClassName="!text-sm"
|
||||
isRequired
|
||||
value={(config as LangFuseConfig).public_key}
|
||||
onChange={handleConfigChange('public_key')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.publicKey`) })!}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: t(`${I18N_PREFIX}.publicKey`, { ns: 'app' }) })!}
|
||||
/>
|
||||
<Field
|
||||
label="Host"
|
||||
|
|
@ -527,14 +527,14 @@ const ProviderConfigModal: FC<Props> = ({
|
|||
labelClassName="!text-sm"
|
||||
value={(config as OpikConfig).api_key}
|
||||
onChange={handleConfigChange('api_key')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'API Key' })!}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: 'API Key' })!}
|
||||
/>
|
||||
<Field
|
||||
label={t(`${I18N_PREFIX}.project`)!}
|
||||
label={t(`${I18N_PREFIX}.project`, { ns: 'app' })!}
|
||||
labelClassName="!text-sm"
|
||||
value={(config as OpikConfig).project}
|
||||
onChange={handleConfigChange('project')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.project`) })!}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: t(`${I18N_PREFIX}.project`, { ns: 'app' }) })!}
|
||||
/>
|
||||
<Field
|
||||
label="Workspace"
|
||||
|
|
@ -555,7 +555,7 @@ const ProviderConfigModal: FC<Props> = ({
|
|||
{type === TracingProvider.mlflow && (
|
||||
<>
|
||||
<Field
|
||||
label={t(`${I18N_PREFIX}.trackingUri`)!}
|
||||
label={t(`${I18N_PREFIX}.trackingUri`, { ns: 'app' })!}
|
||||
labelClassName="!text-sm"
|
||||
value={(config as MLflowConfig).tracking_uri}
|
||||
isRequired
|
||||
|
|
@ -563,67 +563,67 @@ const ProviderConfigModal: FC<Props> = ({
|
|||
placeholder="http://localhost:5000"
|
||||
/>
|
||||
<Field
|
||||
label={t(`${I18N_PREFIX}.experimentId`)!}
|
||||
label={t(`${I18N_PREFIX}.experimentId`, { ns: 'app' })!}
|
||||
labelClassName="!text-sm"
|
||||
isRequired
|
||||
value={(config as MLflowConfig).experiment_id}
|
||||
onChange={handleConfigChange('experiment_id')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.experimentId`) })!}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: t(`${I18N_PREFIX}.experimentId`, { ns: 'app' }) })!}
|
||||
/>
|
||||
<Field
|
||||
label={t(`${I18N_PREFIX}.username`)!}
|
||||
label={t(`${I18N_PREFIX}.username`, { ns: 'app' })!}
|
||||
labelClassName="!text-sm"
|
||||
value={(config as MLflowConfig).username}
|
||||
onChange={handleConfigChange('username')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.username`) })!}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: t(`${I18N_PREFIX}.username`, { ns: 'app' }) })!}
|
||||
/>
|
||||
<Field
|
||||
label={t(`${I18N_PREFIX}.password`)!}
|
||||
label={t(`${I18N_PREFIX}.password`, { ns: 'app' })!}
|
||||
labelClassName="!text-sm"
|
||||
value={(config as MLflowConfig).password}
|
||||
onChange={handleConfigChange('password')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.password`) })!}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: t(`${I18N_PREFIX}.password`, { ns: 'app' }) })!}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{type === TracingProvider.databricks && (
|
||||
<>
|
||||
<Field
|
||||
label={t(`${I18N_PREFIX}.experimentId`)!}
|
||||
label={t(`${I18N_PREFIX}.experimentId`, { ns: 'app' })!}
|
||||
labelClassName="!text-sm"
|
||||
value={(config as DatabricksConfig).experiment_id}
|
||||
onChange={handleConfigChange('experiment_id')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.experimentId`) })!}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: t(`${I18N_PREFIX}.experimentId`, { ns: 'app' }) })!}
|
||||
isRequired
|
||||
/>
|
||||
<Field
|
||||
label={t(`${I18N_PREFIX}.databricksHost`)!}
|
||||
label={t(`${I18N_PREFIX}.databricksHost`, { ns: 'app' })!}
|
||||
labelClassName="!text-sm"
|
||||
value={(config as DatabricksConfig).host}
|
||||
onChange={handleConfigChange('host')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.databricksHost`) })!}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: t(`${I18N_PREFIX}.databricksHost`, { ns: 'app' }) })!}
|
||||
isRequired
|
||||
/>
|
||||
<Field
|
||||
label={t(`${I18N_PREFIX}.clientId`)!}
|
||||
label={t(`${I18N_PREFIX}.clientId`, { ns: 'app' })!}
|
||||
labelClassName="!text-sm"
|
||||
value={(config as DatabricksConfig).client_id}
|
||||
onChange={handleConfigChange('client_id')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.clientId`) })!}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: t(`${I18N_PREFIX}.clientId`, { ns: 'app' }) })!}
|
||||
/>
|
||||
<Field
|
||||
label={t(`${I18N_PREFIX}.clientSecret`)!}
|
||||
label={t(`${I18N_PREFIX}.clientSecret`, { ns: 'app' })!}
|
||||
labelClassName="!text-sm"
|
||||
value={(config as DatabricksConfig).client_secret}
|
||||
onChange={handleConfigChange('client_secret')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.clientSecret`) })!}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: t(`${I18N_PREFIX}.clientSecret`, { ns: 'app' }) })!}
|
||||
/>
|
||||
<Field
|
||||
label={t(`${I18N_PREFIX}.personalAccessToken`)!}
|
||||
label={t(`${I18N_PREFIX}.personalAccessToken`, { ns: 'app' })!}
|
||||
labelClassName="!text-sm"
|
||||
value={(config as DatabricksConfig).personal_access_token}
|
||||
onChange={handleConfigChange('personal_access_token')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.personalAccessToken`) })!}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: t(`${I18N_PREFIX}.personalAccessToken`, { ns: 'app' }) })!}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
|
@ -634,7 +634,7 @@ const ProviderConfigModal: FC<Props> = ({
|
|||
target="_blank"
|
||||
href={docURL[type]}
|
||||
>
|
||||
<span>{t(`${I18N_PREFIX}.viewDocsLink`, { key: t(`app.tracing.${type}.title`) })}</span>
|
||||
<span>{t(`${I18N_PREFIX}.viewDocsLink`, { ns: 'app', key: t(`tracing.${type}.title`, { ns: 'app' }) })}</span>
|
||||
<LinkExternal02 className="h-3 w-3" />
|
||||
</a>
|
||||
<div className="flex items-center">
|
||||
|
|
@ -644,7 +644,7 @@ const ProviderConfigModal: FC<Props> = ({
|
|||
className="h-9 text-sm font-medium text-text-secondary"
|
||||
onClick={showRemoveConfirm}
|
||||
>
|
||||
<span className="text-[#D92D20]">{t('common.operation.remove')}</span>
|
||||
<span className="text-[#D92D20]">{t('operation.remove', { ns: 'common' })}</span>
|
||||
</Button>
|
||||
<Divider type="vertical" className="mx-3 h-[18px]" />
|
||||
</>
|
||||
|
|
@ -653,7 +653,7 @@ const ProviderConfigModal: FC<Props> = ({
|
|||
className="mr-2 h-9 text-sm font-medium text-text-secondary"
|
||||
onClick={onCancel}
|
||||
>
|
||||
{t('common.operation.cancel')}
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
<Button
|
||||
className="h-9 text-sm font-medium"
|
||||
|
|
@ -661,7 +661,7 @@ const ProviderConfigModal: FC<Props> = ({
|
|||
onClick={handleSave}
|
||||
loading={isSaving}
|
||||
>
|
||||
{t(`common.operation.${isAdd ? 'saveAndEnable' : 'save'}`)}
|
||||
{t(`operation.${isAdd ? 'saveAndEnable' : 'save'}`, { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
|
@ -670,7 +670,7 @@ const ProviderConfigModal: FC<Props> = ({
|
|||
<div className="border-t-[0.5px] border-divider-regular">
|
||||
<div className="flex items-center justify-center bg-background-section-burn py-3 text-xs text-text-tertiary">
|
||||
<Lock01 className="mr-1 h-3 w-3 text-text-tertiary" />
|
||||
{t('common.modelProvider.encrypted.front')}
|
||||
{t('modelProvider.encrypted.front', { ns: 'common' })}
|
||||
<a
|
||||
className="mx-1 text-primary-600"
|
||||
target="_blank"
|
||||
|
|
@ -679,7 +679,7 @@ const ProviderConfigModal: FC<Props> = ({
|
|||
>
|
||||
PKCS1_OAEP
|
||||
</a>
|
||||
{t('common.modelProvider.encrypted.back')}
|
||||
{t('modelProvider.encrypted.back', { ns: 'common' })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -691,8 +691,8 @@ const ProviderConfigModal: FC<Props> = ({
|
|||
<Confirm
|
||||
isShow
|
||||
type="warning"
|
||||
title={t(`${I18N_PREFIX}.removeConfirmTitle`, { key: t(`app.tracing.${type}.title`) })!}
|
||||
content={t(`${I18N_PREFIX}.removeConfirmContent`)}
|
||||
title={t(`${I18N_PREFIX}.removeConfirmTitle`, { ns: 'app', key: t(`tracing.${type}.title`, { ns: 'app' }) })!}
|
||||
content={t(`${I18N_PREFIX}.removeConfirmContent`, { ns: 'app' })}
|
||||
onConfirm={handleRemove}
|
||||
onCancel={hideRemoveConfirm}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { Eye as View } from '@/app/components/base/icons/src/vender/solid/genera
|
|||
import { cn } from '@/utils/classnames'
|
||||
import { TracingProvider } from './type'
|
||||
|
||||
const I18N_PREFIX = 'app.tracing'
|
||||
const I18N_PREFIX = 'tracing'
|
||||
|
||||
type Props = {
|
||||
type: TracingProvider
|
||||
|
|
@ -82,14 +82,14 @@ const ProviderPanel: FC<Props> = ({
|
|||
<div className="flex items-center justify-between space-x-1">
|
||||
<div className="flex items-center">
|
||||
<Icon className="h-6" />
|
||||
{isChosen && <div className="system-2xs-medium-uppercase ml-1 flex h-4 items-center rounded-[4px] border border-text-accent-secondary px-1 text-text-accent-secondary">{t(`${I18N_PREFIX}.inUse`)}</div>}
|
||||
{isChosen && <div className="system-2xs-medium-uppercase ml-1 flex h-4 items-center rounded-[4px] border border-text-accent-secondary px-1 text-text-accent-secondary">{t(`${I18N_PREFIX}.inUse`, { ns: 'app' })}</div>}
|
||||
</div>
|
||||
{!readOnly && (
|
||||
<div className="flex items-center justify-between space-x-1">
|
||||
{hasConfigured && (
|
||||
<div className="flex h-6 cursor-pointer items-center space-x-1 rounded-md border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-2 text-text-secondary shadow-xs" onClick={viewBtnClick}>
|
||||
<View className="h-3 w-3" />
|
||||
<div className="text-xs font-medium">{t(`${I18N_PREFIX}.view`)}</div>
|
||||
<div className="text-xs font-medium">{t(`${I18N_PREFIX}.view`, { ns: 'app' })}</div>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
|
|
@ -97,13 +97,13 @@ const ProviderPanel: FC<Props> = ({
|
|||
onClick={handleConfigBtnClick}
|
||||
>
|
||||
<RiEqualizer2Line className="h-3 w-3" />
|
||||
<div className="text-xs font-medium">{t(`${I18N_PREFIX}.config`)}</div>
|
||||
<div className="text-xs font-medium">{t(`${I18N_PREFIX}.config`, { ns: 'app' })}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="system-xs-regular mt-2 text-text-tertiary">
|
||||
{t(`${I18N_PREFIX}.${type}.description`)}
|
||||
{t(`${I18N_PREFIX}.${type}.description`, { ns: 'app' })}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ const AppDetail: FC<IAppDetail> = ({ children }) => {
|
|||
const router = useRouter()
|
||||
const { isCurrentWorkspaceDatasetOperator } = useAppContext()
|
||||
const { t } = useTranslation()
|
||||
useDocumentTitle(t('common.menus.appDetail'))
|
||||
useDocumentTitle(t('menus.appDetail', { ns: 'common' }))
|
||||
|
||||
useEffect(() => {
|
||||
if (isCurrentWorkspaceDatasetOperator)
|
||||
|
|
|
|||
|
|
@ -70,14 +70,14 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
|||
const navigation = useMemo(() => {
|
||||
const baseNavigation = [
|
||||
{
|
||||
name: t('common.datasetMenus.hitTesting'),
|
||||
name: t('datasetMenus.hitTesting', { ns: 'common' }),
|
||||
href: `/datasets/${datasetId}/hitTesting`,
|
||||
icon: RiFocus2Line,
|
||||
selectedIcon: RiFocus2Fill,
|
||||
disabled: isButtonDisabledWithPipeline,
|
||||
},
|
||||
{
|
||||
name: t('common.datasetMenus.settings'),
|
||||
name: t('datasetMenus.settings', { ns: 'common' }),
|
||||
href: `/datasets/${datasetId}/settings`,
|
||||
icon: RiEqualizer2Line,
|
||||
selectedIcon: RiEqualizer2Fill,
|
||||
|
|
@ -87,14 +87,14 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
|||
|
||||
if (datasetRes?.provider !== 'external') {
|
||||
baseNavigation.unshift({
|
||||
name: t('common.datasetMenus.pipeline'),
|
||||
name: t('datasetMenus.pipeline', { ns: 'common' }),
|
||||
href: `/datasets/${datasetId}/pipeline`,
|
||||
icon: PipelineLine as RemixiconComponentType,
|
||||
selectedIcon: PipelineFill as RemixiconComponentType,
|
||||
disabled: false,
|
||||
})
|
||||
baseNavigation.unshift({
|
||||
name: t('common.datasetMenus.documents'),
|
||||
name: t('datasetMenus.documents', { ns: 'common' }),
|
||||
href: `/datasets/${datasetId}/documents`,
|
||||
icon: RiFileTextLine,
|
||||
selectedIcon: RiFileTextFill,
|
||||
|
|
@ -105,7 +105,7 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
|||
return baseNavigation
|
||||
}, [t, datasetId, isButtonDisabledWithPipeline, datasetRes?.provider])
|
||||
|
||||
useDocumentTitle(datasetRes?.name || t('common.menus.datasets'))
|
||||
useDocumentTitle(datasetRes?.name || t('menus.datasets', { ns: 'common' }))
|
||||
|
||||
const setAppSidebarExpand = useStore(state => state.setAppSidebarExpand)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable dify-i18n/require-ns-option */
|
||||
import * as React from 'react'
|
||||
import Form from '@/app/components/datasets/settings/form'
|
||||
import { getLocaleOnServer, getTranslation } from '@/i18n-config/server'
|
||||
|
|
@ -9,7 +10,7 @@ const Settings = async () => {
|
|||
return (
|
||||
<div className="h-full overflow-y-auto">
|
||||
<div className="flex flex-col gap-y-0.5 px-6 pb-2 pt-3">
|
||||
<div className="system-xl-semibold text-text-primary">{t('title') as any}</div>
|
||||
<div className="system-xl-semibold text-text-primary">{t('title')}</div>
|
||||
<div className="system-sm-regular text-text-tertiary">{t('desc')}</div>
|
||||
</div>
|
||||
<Form />
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import useDocumentTitle from '@/hooks/use-document-title'
|
|||
|
||||
const ExploreLayout: FC<PropsWithChildren> = ({ children }) => {
|
||||
const { t } = useTranslation()
|
||||
useDocumentTitle(t('common.menus.explore'))
|
||||
useDocumentTitle(t('menus.explore', { ns: 'common' }))
|
||||
return (
|
||||
<ExploreClient>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import type { ReactNode } from 'react'
|
||||
import * as React from 'react'
|
||||
import { AppInitializer } from '@/app/components/app-initializer'
|
||||
import AmplitudeProvider from '@/app/components/base/amplitude'
|
||||
import GA, { GaType } from '@/app/components/base/ga'
|
||||
import Zendesk from '@/app/components/base/zendesk'
|
||||
|
|
@ -7,7 +8,6 @@ import GotoAnything from '@/app/components/goto-anything'
|
|||
import Header from '@/app/components/header'
|
||||
import HeaderWrapper from '@/app/components/header/header-wrapper'
|
||||
import ReadmePanel from '@/app/components/plugins/readme-panel'
|
||||
import SwrInitializer from '@/app/components/swr-initializer'
|
||||
import { AppContextProvider } from '@/context/app-context'
|
||||
import { EventEmitterContextProvider } from '@/context/event-emitter'
|
||||
import { ModalContextProvider } from '@/context/modal-context'
|
||||
|
|
@ -20,7 +20,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
|
|||
<>
|
||||
<GA gaType={GaType.admin} />
|
||||
<AmplitudeProvider />
|
||||
<SwrInitializer>
|
||||
<AppInitializer>
|
||||
<AppContextProvider>
|
||||
<EventEmitterContextProvider>
|
||||
<ProviderContextProvider>
|
||||
|
|
@ -38,7 +38,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
|
|||
</EventEmitterContextProvider>
|
||||
</AppContextProvider>
|
||||
<Zendesk />
|
||||
</SwrInitializer>
|
||||
</AppInitializer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ const ToolsList: FC = () => {
|
|||
const router = useRouter()
|
||||
const { isCurrentWorkspaceDatasetOperator } = useAppContext()
|
||||
const { t } = useTranslation()
|
||||
useDocumentTitle(t('common.menus.tools'))
|
||||
useDocumentTitle(t('menus.tools', { ns: 'common' }))
|
||||
|
||||
useEffect(() => {
|
||||
if (isCurrentWorkspaceDatasetOperator)
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ const AuthenticatedLayout = ({ children }: { children: React.ReactNode }) => {
|
|||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-y-2">
|
||||
<AppUnavailable className="h-auto w-auto" code={403} unknownReason="no permission." />
|
||||
<span className="system-sm-regular cursor-pointer text-text-tertiary" onClick={backToHome}>{t('common.userProfile.logout')}</span>
|
||||
<span className="system-sm-regular cursor-pointer text-text-tertiary" onClick={backToHome}>{t('userProfile.logout', { ns: 'common' })}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,8 +94,8 @@ const Splash: FC<PropsWithChildren> = ({ children }) => {
|
|||
if (message) {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-y-4">
|
||||
<AppUnavailable className="h-auto w-auto" code={code || t('share.common.appUnavailable')} unknownReason={message} />
|
||||
<span className="system-sm-regular cursor-pointer text-text-tertiary" onClick={backToHome}>{code === '403' ? t('common.userProfile.logout') : t('share.login.backToHome')}</span>
|
||||
<AppUnavailable className="h-auto w-auto" code={code || t('common.appUnavailable', { ns: 'share' })} unknownReason={message} />
|
||||
<span className="system-sm-regular cursor-pointer text-text-tertiary" onClick={backToHome}>{code === '403' ? t('userProfile.logout', { ns: 'common' }) : t('login.backToHome', { ns: 'share' })}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,14 +26,14 @@ export default function CheckCode() {
|
|||
if (!code.trim()) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('login.checkCode.emptyCode'),
|
||||
message: t('checkCode.emptyCode', { ns: 'login' }),
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!/\d{6}/.test(code)) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('login.checkCode.invalidCode'),
|
||||
message: t('checkCode.invalidCode', { ns: 'login' }),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
|
@ -69,22 +69,22 @@ export default function CheckCode() {
|
|||
<RiMailSendFill className="h-6 w-6 text-2xl" />
|
||||
</div>
|
||||
<div className="pb-4 pt-2">
|
||||
<h2 className="title-4xl-semi-bold text-text-primary">{t('login.checkCode.checkYourEmail')}</h2>
|
||||
<h2 className="title-4xl-semi-bold text-text-primary">{t('checkCode.checkYourEmail', { ns: 'login' })}</h2>
|
||||
<p className="body-md-regular mt-2 text-text-secondary">
|
||||
<span>
|
||||
{t('login.checkCode.tipsPrefix')}
|
||||
{t('checkCode.tipsPrefix', { ns: 'login' })}
|
||||
<strong>{email}</strong>
|
||||
</span>
|
||||
<br />
|
||||
{t('login.checkCode.validTime')}
|
||||
{t('checkCode.validTime', { ns: 'login' })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form action="">
|
||||
<input type="text" className="hidden" />
|
||||
<label htmlFor="code" className="system-md-semibold mb-1 text-text-secondary">{t('login.checkCode.verificationCode')}</label>
|
||||
<Input value={code} onChange={e => setVerifyCode(e.target.value)} maxLength={6} className="mt-1" placeholder={t('login.checkCode.verificationCodePlaceholder') || ''} />
|
||||
<Button loading={loading} disabled={loading} className="my-3 w-full" variant="primary" onClick={verify}>{t('login.checkCode.verify')}</Button>
|
||||
<label htmlFor="code" className="system-md-semibold mb-1 text-text-secondary">{t('checkCode.verificationCode', { ns: 'login' })}</label>
|
||||
<Input value={code} onChange={e => setVerifyCode(e.target.value)} maxLength={6} className="mt-1" placeholder={t('checkCode.verificationCodePlaceholder', { ns: 'login' }) || ''} />
|
||||
<Button loading={loading} disabled={loading} className="my-3 w-full" variant="primary" onClick={verify}>{t('checkCode.verify', { ns: 'login' })}</Button>
|
||||
<Countdown onResend={resendCode} />
|
||||
</form>
|
||||
<div className="py-2">
|
||||
|
|
@ -94,7 +94,7 @@ export default function CheckCode() {
|
|||
<div className="bg-background-default-dimm inline-block rounded-full p-1">
|
||||
<RiArrowLeftLine size={12} />
|
||||
</div>
|
||||
<span className="system-xs-regular ml-2">{t('login.back')}</span>
|
||||
<span className="system-xs-regular ml-2">{t('back', { ns: 'login' })}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -27,14 +27,14 @@ export default function CheckCode() {
|
|||
const handleGetEMailVerificationCode = async () => {
|
||||
try {
|
||||
if (!email) {
|
||||
Toast.notify({ type: 'error', message: t('login.error.emailEmpty') })
|
||||
Toast.notify({ type: 'error', message: t('error.emailEmpty', { ns: 'login' }) })
|
||||
return
|
||||
}
|
||||
|
||||
if (!emailRegex.test(email)) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('login.error.emailInValid'),
|
||||
message: t('error.emailInValid', { ns: 'login' }),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
|
@ -50,7 +50,7 @@ export default function CheckCode() {
|
|||
else if (res.code === 'account_not_found') {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('login.error.registrationNotAllowed'),
|
||||
message: t('error.registrationNotAllowed', { ns: 'login' }),
|
||||
})
|
||||
}
|
||||
else {
|
||||
|
|
@ -74,21 +74,21 @@ export default function CheckCode() {
|
|||
<RiLockPasswordLine className="h-6 w-6 text-2xl text-text-accent-light-mode-only" />
|
||||
</div>
|
||||
<div className="pb-4 pt-2">
|
||||
<h2 className="title-4xl-semi-bold text-text-primary">{t('login.resetPassword')}</h2>
|
||||
<h2 className="title-4xl-semi-bold text-text-primary">{t('resetPassword', { ns: 'login' })}</h2>
|
||||
<p className="body-md-regular mt-2 text-text-secondary">
|
||||
{t('login.resetPasswordDesc')}
|
||||
{t('resetPasswordDesc', { ns: 'login' })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={noop}>
|
||||
<input type="text" className="hidden" />
|
||||
<div className="mb-2">
|
||||
<label htmlFor="email" className="system-md-semibold my-2 text-text-secondary">{t('login.email')}</label>
|
||||
<label htmlFor="email" className="system-md-semibold my-2 text-text-secondary">{t('email', { ns: 'login' })}</label>
|
||||
<div className="mt-1">
|
||||
<Input id="email" type="email" disabled={loading} value={email} placeholder={t('login.emailPlaceholder') as string} onChange={e => setEmail(e.target.value)} />
|
||||
<Input id="email" type="email" disabled={loading} value={email} placeholder={t('emailPlaceholder', { ns: 'login' }) as string} onChange={e => setEmail(e.target.value)} />
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<Button loading={loading} disabled={loading} variant="primary" className="w-full" onClick={handleGetEMailVerificationCode}>{t('login.sendVerificationCode')}</Button>
|
||||
<Button loading={loading} disabled={loading} variant="primary" className="w-full" onClick={handleGetEMailVerificationCode}>{t('sendVerificationCode', { ns: 'login' })}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -99,7 +99,7 @@ export default function CheckCode() {
|
|||
<div className="inline-block rounded-full bg-background-default-dimmed p-1">
|
||||
<RiArrowLeftLine size={12} />
|
||||
</div>
|
||||
<span className="system-xs-regular ml-2">{t('login.backToLogin')}</span>
|
||||
<span className="system-xs-regular ml-2">{t('backToLogin', { ns: 'login' })}</span>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -45,15 +45,15 @@ const ChangePasswordForm = () => {
|
|||
|
||||
const valid = useCallback(() => {
|
||||
if (!password.trim()) {
|
||||
showErrorMessage(t('login.error.passwordEmpty'))
|
||||
showErrorMessage(t('error.passwordEmpty', { ns: 'login' }))
|
||||
return false
|
||||
}
|
||||
if (!validPassword.test(password)) {
|
||||
showErrorMessage(t('login.error.passwordInvalid'))
|
||||
showErrorMessage(t('error.passwordInvalid', { ns: 'login' }))
|
||||
return false
|
||||
}
|
||||
if (password !== confirmPassword) {
|
||||
showErrorMessage(t('common.account.notEqual'))
|
||||
showErrorMessage(t('account.notEqual', { ns: 'common' }))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
|
@ -92,10 +92,10 @@ const ChangePasswordForm = () => {
|
|||
<div className="flex flex-col md:w-[400px]">
|
||||
<div className="mx-auto w-full">
|
||||
<h2 className="title-4xl-semi-bold text-text-primary">
|
||||
{t('login.changePassword')}
|
||||
{t('changePassword', { ns: 'login' })}
|
||||
</h2>
|
||||
<p className="body-md-regular mt-2 text-text-secondary">
|
||||
{t('login.changePasswordTip')}
|
||||
{t('changePasswordTip', { ns: 'login' })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -104,7 +104,7 @@ const ChangePasswordForm = () => {
|
|||
{/* Password */}
|
||||
<div className="mb-5">
|
||||
<label htmlFor="password" className="system-md-semibold my-2 text-text-secondary">
|
||||
{t('common.account.newPassword')}
|
||||
{t('account.newPassword', { ns: 'common' })}
|
||||
</label>
|
||||
<div className="relative mt-1">
|
||||
<Input
|
||||
|
|
@ -112,7 +112,7 @@ const ChangePasswordForm = () => {
|
|||
type={showPassword ? 'text' : 'password'}
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
placeholder={t('login.passwordPlaceholder') || ''}
|
||||
placeholder={t('passwordPlaceholder', { ns: 'login' }) || ''}
|
||||
/>
|
||||
|
||||
<div className="absolute inset-y-0 right-0 flex items-center">
|
||||
|
|
@ -125,12 +125,12 @@ const ChangePasswordForm = () => {
|
|||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="body-xs-regular mt-1 text-text-secondary">{t('login.error.passwordInvalid')}</div>
|
||||
<div className="body-xs-regular mt-1 text-text-secondary">{t('error.passwordInvalid', { ns: 'login' })}</div>
|
||||
</div>
|
||||
{/* Confirm Password */}
|
||||
<div className="mb-5">
|
||||
<label htmlFor="confirmPassword" className="system-md-semibold my-2 text-text-secondary">
|
||||
{t('common.account.confirmPassword')}
|
||||
{t('account.confirmPassword', { ns: 'common' })}
|
||||
</label>
|
||||
<div className="relative mt-1">
|
||||
<Input
|
||||
|
|
@ -138,7 +138,7 @@ const ChangePasswordForm = () => {
|
|||
type={showConfirmPassword ? 'text' : 'password'}
|
||||
value={confirmPassword}
|
||||
onChange={e => setConfirmPassword(e.target.value)}
|
||||
placeholder={t('login.confirmPasswordPlaceholder') || ''}
|
||||
placeholder={t('confirmPasswordPlaceholder', { ns: 'login' }) || ''}
|
||||
/>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center">
|
||||
<Button
|
||||
|
|
@ -157,7 +157,7 @@ const ChangePasswordForm = () => {
|
|||
className="w-full"
|
||||
onClick={handleChangePassword}
|
||||
>
|
||||
{t('login.changePasswordBtn')}
|
||||
{t('changePasswordBtn', { ns: 'login' })}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -171,7 +171,7 @@ const ChangePasswordForm = () => {
|
|||
<RiCheckboxCircleFill className="h-6 w-6 text-text-success" />
|
||||
</div>
|
||||
<h2 className="title-4xl-semi-bold text-text-primary">
|
||||
{t('login.passwordChangedTip')}
|
||||
{t('passwordChangedTip', { ns: 'login' })}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="mx-auto mt-6 w-full">
|
||||
|
|
@ -183,7 +183,7 @@ const ChangePasswordForm = () => {
|
|||
router.replace(getSignInUrl())
|
||||
}}
|
||||
>
|
||||
{t('login.passwordChanged')}
|
||||
{t('passwordChanged', { ns: 'login' })}
|
||||
{' '}
|
||||
(
|
||||
{Math.round(countdown / 1000)}
|
||||
|
|
|
|||
|
|
@ -45,21 +45,21 @@ export default function CheckCode() {
|
|||
if (!code.trim()) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('login.checkCode.emptyCode'),
|
||||
message: t('checkCode.emptyCode', { ns: 'login' }),
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!/\d{6}/.test(code)) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('login.checkCode.invalidCode'),
|
||||
message: t('checkCode.invalidCode', { ns: 'login' }),
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!redirectUrl || !appCode) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('login.error.redirectUrlMissing'),
|
||||
message: t('error.redirectUrlMissing', { ns: 'login' }),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
|
@ -108,19 +108,19 @@ export default function CheckCode() {
|
|||
<RiMailSendFill className="h-6 w-6 text-2xl text-text-accent-light-mode-only" />
|
||||
</div>
|
||||
<div className="pb-4 pt-2">
|
||||
<h2 className="title-4xl-semi-bold text-text-primary">{t('login.checkCode.checkYourEmail')}</h2>
|
||||
<h2 className="title-4xl-semi-bold text-text-primary">{t('checkCode.checkYourEmail', { ns: 'login' })}</h2>
|
||||
<p className="body-md-regular mt-2 text-text-secondary">
|
||||
<span>
|
||||
{t('login.checkCode.tipsPrefix')}
|
||||
{t('checkCode.tipsPrefix', { ns: 'login' })}
|
||||
<strong>{email}</strong>
|
||||
</span>
|
||||
<br />
|
||||
{t('login.checkCode.validTime')}
|
||||
{t('checkCode.validTime', { ns: 'login' })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<label htmlFor="code" className="system-md-semibold mb-1 text-text-secondary">{t('login.checkCode.verificationCode')}</label>
|
||||
<label htmlFor="code" className="system-md-semibold mb-1 text-text-secondary">{t('checkCode.verificationCode', { ns: 'login' })}</label>
|
||||
<Input
|
||||
ref={codeInputRef}
|
||||
id="code"
|
||||
|
|
@ -128,9 +128,9 @@ export default function CheckCode() {
|
|||
onChange={e => setVerifyCode(e.target.value)}
|
||||
maxLength={6}
|
||||
className="mt-1"
|
||||
placeholder={t('login.checkCode.verificationCodePlaceholder') || ''}
|
||||
placeholder={t('checkCode.verificationCodePlaceholder', { ns: 'login' }) || ''}
|
||||
/>
|
||||
<Button type="submit" loading={loading} disabled={loading} className="my-3 w-full" variant="primary">{t('login.checkCode.verify')}</Button>
|
||||
<Button type="submit" loading={loading} disabled={loading} className="my-3 w-full" variant="primary">{t('checkCode.verify', { ns: 'login' })}</Button>
|
||||
<Countdown onResend={resendCode} />
|
||||
</form>
|
||||
<div className="py-2">
|
||||
|
|
@ -140,7 +140,7 @@ export default function CheckCode() {
|
|||
<div className="bg-background-default-dimm inline-block rounded-full p-1">
|
||||
<RiArrowLeftLine size={12} />
|
||||
</div>
|
||||
<span className="system-xs-regular ml-2">{t('login.back')}</span>
|
||||
<span className="system-xs-regular ml-2">{t('back', { ns: 'login' })}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -23,14 +23,14 @@ export default function MailAndCodeAuth() {
|
|||
const handleGetEMailVerificationCode = async () => {
|
||||
try {
|
||||
if (!email) {
|
||||
Toast.notify({ type: 'error', message: t('login.error.emailEmpty') })
|
||||
Toast.notify({ type: 'error', message: t('error.emailEmpty', { ns: 'login' }) })
|
||||
return
|
||||
}
|
||||
|
||||
if (!emailRegex.test(email)) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('login.error.emailInValid'),
|
||||
message: t('error.emailInValid', { ns: 'login' }),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
|
@ -56,12 +56,12 @@ export default function MailAndCodeAuth() {
|
|||
<form onSubmit={noop}>
|
||||
<input type="text" className="hidden" />
|
||||
<div className="mb-2">
|
||||
<label htmlFor="email" className="system-md-semibold my-2 text-text-secondary">{t('login.email')}</label>
|
||||
<label htmlFor="email" className="system-md-semibold my-2 text-text-secondary">{t('email', { ns: 'login' })}</label>
|
||||
<div className="mt-1">
|
||||
<Input id="email" type="email" value={email} placeholder={t('login.emailPlaceholder') as string} onChange={e => setEmail(e.target.value)} />
|
||||
<Input id="email" type="email" value={email} placeholder={t('emailPlaceholder', { ns: 'login' }) as string} onChange={e => setEmail(e.target.value)} />
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<Button loading={loading} disabled={loading || !email} variant="primary" className="w-full" onClick={handleGetEMailVerificationCode}>{t('login.signup.verifyMail')}</Button>
|
||||
<Button loading={loading} disabled={loading || !email} variant="primary" className="w-full" onClick={handleGetEMailVerificationCode}>{t('signup.verifyMail', { ns: 'login' })}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -46,25 +46,25 @@ export default function MailAndPasswordAuth({ isEmailSetup }: MailAndPasswordAut
|
|||
const appCode = getAppCodeFromRedirectUrl()
|
||||
const handleEmailPasswordLogin = async () => {
|
||||
if (!email) {
|
||||
Toast.notify({ type: 'error', message: t('login.error.emailEmpty') })
|
||||
Toast.notify({ type: 'error', message: t('error.emailEmpty', { ns: 'login' }) })
|
||||
return
|
||||
}
|
||||
if (!emailRegex.test(email)) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('login.error.emailInValid'),
|
||||
message: t('error.emailInValid', { ns: 'login' }),
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!password?.trim()) {
|
||||
Toast.notify({ type: 'error', message: t('login.error.passwordEmpty') })
|
||||
Toast.notify({ type: 'error', message: t('error.passwordEmpty', { ns: 'login' }) })
|
||||
return
|
||||
}
|
||||
|
||||
if (!redirectUrl || !appCode) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('login.error.redirectUrlMissing'),
|
||||
message: t('error.redirectUrlMissing', { ns: 'login' }),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
|
@ -111,7 +111,7 @@ export default function MailAndPasswordAuth({ isEmailSetup }: MailAndPasswordAut
|
|||
<form onSubmit={noop}>
|
||||
<div className="mb-3">
|
||||
<label htmlFor="email" className="system-md-semibold my-2 text-text-secondary">
|
||||
{t('login.email')}
|
||||
{t('email', { ns: 'login' })}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<Input
|
||||
|
|
@ -120,7 +120,7 @@ export default function MailAndPasswordAuth({ isEmailSetup }: MailAndPasswordAut
|
|||
id="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
placeholder={t('login.emailPlaceholder') || ''}
|
||||
placeholder={t('emailPlaceholder', { ns: 'login' }) || ''}
|
||||
tabIndex={1}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -128,14 +128,14 @@ export default function MailAndPasswordAuth({ isEmailSetup }: MailAndPasswordAut
|
|||
|
||||
<div className="mb-3">
|
||||
<label htmlFor="password" className="my-2 flex items-center justify-between">
|
||||
<span className="system-md-semibold text-text-secondary">{t('login.password')}</span>
|
||||
<span className="system-md-semibold text-text-secondary">{t('password', { ns: 'login' })}</span>
|
||||
<Link
|
||||
href={`/webapp-reset-password?${searchParams.toString()}`}
|
||||
className={`system-xs-regular ${isEmailSetup ? 'text-components-button-secondary-accent-text' : 'pointer-events-none text-components-button-secondary-accent-text-disabled'}`}
|
||||
tabIndex={isEmailSetup ? 0 : -1}
|
||||
aria-disabled={!isEmailSetup}
|
||||
>
|
||||
{t('login.forget')}
|
||||
{t('forget', { ns: 'login' })}
|
||||
</Link>
|
||||
</label>
|
||||
<div className="relative mt-1">
|
||||
|
|
@ -149,7 +149,7 @@ export default function MailAndPasswordAuth({ isEmailSetup }: MailAndPasswordAut
|
|||
}}
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
autoComplete="current-password"
|
||||
placeholder={t('login.passwordPlaceholder') || ''}
|
||||
placeholder={t('passwordPlaceholder', { ns: 'login' }) || ''}
|
||||
tabIndex={2}
|
||||
/>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center">
|
||||
|
|
@ -172,7 +172,7 @@ export default function MailAndPasswordAuth({ isEmailSetup }: MailAndPasswordAut
|
|||
disabled={isLoading || !email || !password}
|
||||
className="w-full"
|
||||
>
|
||||
{t('login.signBtn')}
|
||||
{t('signBtn', { ns: 'login' })}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ const SSOAuth: FC<SSOAuthProps> = ({
|
|||
className="w-full"
|
||||
>
|
||||
<Lock01 className="mr-2 h-5 w-5 text-text-accent-light-mode-only" />
|
||||
<span className="truncate">{t('login.withSSO')}</span>
|
||||
<span className="truncate">{t('withSSO', { ns: 'login' })}</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { cn } from '@/utils/classnames'
|
|||
export default function SignInLayout({ children }: PropsWithChildren) {
|
||||
const { t } = useTranslation()
|
||||
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
|
||||
useDocumentTitle(t('login.webapp.login'))
|
||||
useDocumentTitle(t('webapp.login', { ns: 'login' }))
|
||||
return (
|
||||
<>
|
||||
<div className={cn('flex min-h-screen w-full justify-center bg-background-default-burn p-6')}>
|
||||
|
|
|
|||
|
|
@ -60,8 +60,8 @@ const NormalForm = () => {
|
|||
<RiContractLine className="h-5 w-5" />
|
||||
<RiErrorWarningFill className="absolute -right-1 -top-1 h-4 w-4 text-text-warning-secondary" />
|
||||
</div>
|
||||
<p className="system-sm-medium text-text-primary">{t('login.licenseLost')}</p>
|
||||
<p className="system-xs-regular mt-1 text-text-tertiary">{t('login.licenseLostTip')}</p>
|
||||
<p className="system-sm-medium text-text-primary">{t('licenseLost', { ns: 'login' })}</p>
|
||||
<p className="system-xs-regular mt-1 text-text-tertiary">{t('licenseLostTip', { ns: 'login' })}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -76,8 +76,8 @@ const NormalForm = () => {
|
|||
<RiContractLine className="h-5 w-5" />
|
||||
<RiErrorWarningFill className="absolute -right-1 -top-1 h-4 w-4 text-text-warning-secondary" />
|
||||
</div>
|
||||
<p className="system-sm-medium text-text-primary">{t('login.licenseExpired')}</p>
|
||||
<p className="system-xs-regular mt-1 text-text-tertiary">{t('login.licenseExpiredTip')}</p>
|
||||
<p className="system-sm-medium text-text-primary">{t('licenseExpired', { ns: 'login' })}</p>
|
||||
<p className="system-xs-regular mt-1 text-text-tertiary">{t('licenseExpiredTip', { ns: 'login' })}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -92,8 +92,8 @@ const NormalForm = () => {
|
|||
<RiContractLine className="h-5 w-5" />
|
||||
<RiErrorWarningFill className="absolute -right-1 -top-1 h-4 w-4 text-text-warning-secondary" />
|
||||
</div>
|
||||
<p className="system-sm-medium text-text-primary">{t('login.licenseInactive')}</p>
|
||||
<p className="system-xs-regular mt-1 text-text-tertiary">{t('login.licenseInactiveTip')}</p>
|
||||
<p className="system-sm-medium text-text-primary">{t('licenseInactive', { ns: 'login' })}</p>
|
||||
<p className="system-xs-regular mt-1 text-text-tertiary">{t('licenseInactiveTip', { ns: 'login' })}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -104,8 +104,8 @@ const NormalForm = () => {
|
|||
<>
|
||||
<div className="mx-auto mt-8 w-full">
|
||||
<div className="mx-auto w-full">
|
||||
<h2 className="title-4xl-semi-bold text-text-primary">{systemFeatures.branding.enabled ? t('login.pageTitleForE') : t('login.pageTitle')}</h2>
|
||||
<p className="body-md-regular mt-2 text-text-tertiary">{t('login.welcome')}</p>
|
||||
<h2 className="title-4xl-semi-bold text-text-primary">{systemFeatures.branding.enabled ? t('pageTitleForE', { ns: 'login' }) : t('pageTitle', { ns: 'login' })}</h2>
|
||||
<p className="body-md-regular mt-2 text-text-tertiary">{t('welcome', { ns: 'login' })}</p>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<div className="mt-6 flex flex-col gap-3">
|
||||
|
|
@ -122,7 +122,7 @@ const NormalForm = () => {
|
|||
<div className="h-px w-full bg-gradient-to-r from-background-gradient-mask-transparent via-divider-regular to-background-gradient-mask-transparent"></div>
|
||||
</div>
|
||||
<div className="relative flex justify-center">
|
||||
<span className="system-xs-medium-uppercase px-2 text-text-tertiary">{t('login.or')}</span>
|
||||
<span className="system-xs-medium-uppercase px-2 text-text-tertiary">{t('or', { ns: 'login' })}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -134,7 +134,7 @@ const NormalForm = () => {
|
|||
<MailAndCodeAuth />
|
||||
{systemFeatures.enable_email_password_login && (
|
||||
<div className="cursor-pointer py-1 text-center" onClick={() => { updateAuthType('password') }}>
|
||||
<span className="system-xs-medium text-components-button-secondary-accent-text">{t('login.usePassword')}</span>
|
||||
<span className="system-xs-medium text-components-button-secondary-accent-text">{t('usePassword', { ns: 'login' })}</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
|
@ -144,7 +144,7 @@ const NormalForm = () => {
|
|||
<MailAndPasswordAuth isEmailSetup={systemFeatures.is_email_setup} />
|
||||
{systemFeatures.enable_email_code_login && (
|
||||
<div className="cursor-pointer py-1 text-center" onClick={() => { updateAuthType('code') }}>
|
||||
<span className="system-xs-medium text-components-button-secondary-accent-text">{t('login.useVerificationCode')}</span>
|
||||
<span className="system-xs-medium text-components-button-secondary-accent-text">{t('useVerificationCode', { ns: 'login' })}</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
|
@ -158,8 +158,8 @@ const NormalForm = () => {
|
|||
<div className="shadows-shadow-lg mb-2 flex h-10 w-10 items-center justify-center rounded-xl bg-components-card-bg shadow">
|
||||
<RiDoorLockLine className="h-5 w-5" />
|
||||
</div>
|
||||
<p className="system-sm-medium text-text-primary">{t('login.noLoginMethod')}</p>
|
||||
<p className="system-xs-regular mt-1 text-text-tertiary">{t('login.noLoginMethodTip')}</p>
|
||||
<p className="system-sm-medium text-text-primary">{t('noLoginMethod', { ns: 'login' })}</p>
|
||||
<p className="system-xs-regular mt-1 text-text-tertiary">{t('noLoginMethodTip', { ns: 'login' })}</p>
|
||||
</div>
|
||||
<div className="relative my-2 py-2">
|
||||
<div className="absolute inset-0 flex items-center" aria-hidden="true">
|
||||
|
|
@ -171,7 +171,7 @@ const NormalForm = () => {
|
|||
{!systemFeatures.branding.enabled && (
|
||||
<>
|
||||
<div className="system-xs-regular mt-2 block w-full text-text-tertiary">
|
||||
{t('login.tosDesc')}
|
||||
{t('tosDesc', { ns: 'login' })}
|
||||
|
||||
<Link
|
||||
className="system-xs-medium text-text-secondary hover:underline"
|
||||
|
|
@ -179,7 +179,7 @@ const NormalForm = () => {
|
|||
rel="noopener noreferrer"
|
||||
href="https://dify.ai/terms"
|
||||
>
|
||||
{t('login.tos')}
|
||||
{t('tos', { ns: 'login' })}
|
||||
</Link>
|
||||
&
|
||||
<Link
|
||||
|
|
@ -188,18 +188,18 @@ const NormalForm = () => {
|
|||
rel="noopener noreferrer"
|
||||
href="https://dify.ai/privacy"
|
||||
>
|
||||
{t('login.pp')}
|
||||
{t('pp', { ns: 'login' })}
|
||||
</Link>
|
||||
</div>
|
||||
{IS_CE_EDITION && (
|
||||
<div className="w-hull system-xs-regular mt-2 block text-text-tertiary">
|
||||
{t('login.goToInit')}
|
||||
{t('goToInit', { ns: 'login' })}
|
||||
|
||||
<Link
|
||||
className="system-xs-medium text-text-secondary hover:underline"
|
||||
href="/install"
|
||||
>
|
||||
{t('login.setAdminAccount')}
|
||||
{t('setAdminAccount', { ns: 'login' })}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ const WebSSOForm: FC = () => {
|
|||
if (!redirectUrl) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<AppUnavailable code={t('share.common.appUnavailable')} unknownReason="redirect url is invalid." />
|
||||
<AppUnavailable code={t('common.appUnavailable', { ns: 'share' })} unknownReason="redirect url is invalid." />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -45,7 +45,7 @@ const WebSSOForm: FC = () => {
|
|||
if (!systemFeatures.webapp_auth.enabled) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<p className="system-xs-regular text-text-tertiary">{t('login.webapp.disabled')}</p>
|
||||
<p className="system-xs-regular text-text-tertiary">{t('webapp.disabled', { ns: 'login' })}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -63,7 +63,7 @@ const WebSSOForm: FC = () => {
|
|||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-y-4">
|
||||
<AppUnavailable className="h-auto w-auto" isUnknownReason={true} />
|
||||
<span className="system-sm-regular cursor-pointer text-text-tertiary" onClick={backToHome}>{t('share.login.backToHome')}</span>
|
||||
<span className="system-sm-regular cursor-pointer text-text-tertiary" onClick={backToHome}>{t('login.backToHome', { ns: 'share' })}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => {
|
|||
await updateUserProfile({ url: 'account/avatar', body: { avatar: uploadedFileId } })
|
||||
setIsShowAvatarPicker(false)
|
||||
onSave?.()
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) })
|
||||
}
|
||||
catch (e) {
|
||||
notify({ type: 'error', message: (e as Error).message })
|
||||
|
|
@ -58,7 +58,7 @@ const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => {
|
|||
const handleDeleteAvatar = useCallback(async () => {
|
||||
try {
|
||||
await updateUserProfile({ url: 'account/avatar', body: { avatar: '' } })
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) })
|
||||
setIsShowDeleteConfirm(false)
|
||||
onSave?.()
|
||||
}
|
||||
|
|
@ -145,11 +145,11 @@ const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => {
|
|||
|
||||
<div className="flex w-full items-center justify-center gap-2 p-3">
|
||||
<Button className="w-full" onClick={() => setIsShowAvatarPicker(false)}>
|
||||
{t('app.iconPicker.cancel')}
|
||||
{t('iconPicker.cancel', { ns: 'app' })}
|
||||
</Button>
|
||||
|
||||
<Button variant="primary" className="w-full" disabled={uploading || !inputImageInfo} loading={uploading} onClick={handleSelect}>
|
||||
{t('app.iconPicker.ok')}
|
||||
{t('iconPicker.ok', { ns: 'app' })}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
|
@ -160,16 +160,16 @@ const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => {
|
|||
isShow={isShowDeleteConfirm}
|
||||
onClose={() => setIsShowDeleteConfirm(false)}
|
||||
>
|
||||
<div className="title-2xl-semi-bold mb-3 text-text-primary">{t('common.avatar.deleteTitle')}</div>
|
||||
<p className="mb-8 text-text-secondary">{t('common.avatar.deleteDescription')}</p>
|
||||
<div className="title-2xl-semi-bold mb-3 text-text-primary">{t('avatar.deleteTitle', { ns: 'common' })}</div>
|
||||
<p className="mb-8 text-text-secondary">{t('avatar.deleteDescription', { ns: 'common' })}</p>
|
||||
|
||||
<div className="flex w-full items-center justify-center gap-2">
|
||||
<Button className="w-full" onClick={() => setIsShowDeleteConfirm(false)}>
|
||||
{t('common.operation.cancel')}
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
|
||||
<Button variant="warning" className="w-full" onClick={handleDeleteAvatar}>
|
||||
{t('common.operation.delete')}
|
||||
{t('operation.delete', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
|
|
|||
|
|
@ -209,9 +209,9 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
|
|||
</div>
|
||||
{step === STEP.start && (
|
||||
<>
|
||||
<div className="title-2xl-semi-bold pb-3 text-text-primary">{t('common.account.changeEmail.title')}</div>
|
||||
<div className="title-2xl-semi-bold pb-3 text-text-primary">{t('account.changeEmail.title', { ns: 'common' })}</div>
|
||||
<div className="space-y-0.5 pb-2 pt-1">
|
||||
<div className="body-md-medium text-text-warning">{t('common.account.changeEmail.authTip')}</div>
|
||||
<div className="body-md-medium text-text-warning">{t('account.changeEmail.authTip', { ns: 'common' })}</div>
|
||||
<div className="body-md-regular text-text-secondary">
|
||||
<Trans
|
||||
i18nKey="common.account.changeEmail.content1"
|
||||
|
|
@ -227,20 +227,20 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
|
|||
variant="primary"
|
||||
onClick={sendCodeToOriginEmail}
|
||||
>
|
||||
{t('common.account.changeEmail.sendVerifyCode')}
|
||||
{t('account.changeEmail.sendVerifyCode', { ns: 'common' })}
|
||||
</Button>
|
||||
<Button
|
||||
className="!w-full"
|
||||
onClick={onClose}
|
||||
>
|
||||
{t('common.operation.cancel')}
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{step === STEP.verifyOrigin && (
|
||||
<>
|
||||
<div className="title-2xl-semi-bold pb-3 text-text-primary">{t('common.account.changeEmail.verifyEmail')}</div>
|
||||
<div className="title-2xl-semi-bold pb-3 text-text-primary">{t('account.changeEmail.verifyEmail', { ns: 'common' })}</div>
|
||||
<div className="space-y-0.5 pb-2 pt-1">
|
||||
<div className="body-md-regular text-text-secondary">
|
||||
<Trans
|
||||
|
|
@ -251,10 +251,10 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
|
|||
</div>
|
||||
</div>
|
||||
<div className="pt-3">
|
||||
<div className="system-sm-medium mb-1 flex h-6 items-center text-text-secondary">{t('common.account.changeEmail.codeLabel')}</div>
|
||||
<div className="system-sm-medium mb-1 flex h-6 items-center text-text-secondary">{t('account.changeEmail.codeLabel', { ns: 'common' })}</div>
|
||||
<Input
|
||||
className="!w-full"
|
||||
placeholder={t('common.account.changeEmail.codePlaceholder')}
|
||||
placeholder={t('account.changeEmail.codePlaceholder', { ns: 'common' })}
|
||||
value={code}
|
||||
onChange={e => setCode(e.target.value)}
|
||||
maxLength={6}
|
||||
|
|
@ -267,46 +267,46 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
|
|||
variant="primary"
|
||||
onClick={handleVerifyOriginEmail}
|
||||
>
|
||||
{t('common.account.changeEmail.continue')}
|
||||
{t('account.changeEmail.continue', { ns: 'common' })}
|
||||
</Button>
|
||||
<Button
|
||||
className="!w-full"
|
||||
onClick={onClose}
|
||||
>
|
||||
{t('common.operation.cancel')}
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="system-xs-regular mt-3 flex items-center gap-1 text-text-tertiary">
|
||||
<span>{t('common.account.changeEmail.resendTip')}</span>
|
||||
<span>{t('account.changeEmail.resendTip', { ns: 'common' })}</span>
|
||||
{time > 0 && (
|
||||
<span>{t('common.account.changeEmail.resendCount', { count: time })}</span>
|
||||
<span>{t('account.changeEmail.resendCount', { ns: 'common', count: time })}</span>
|
||||
)}
|
||||
{!time && (
|
||||
<span onClick={sendCodeToOriginEmail} className="system-xs-medium cursor-pointer text-text-accent-secondary">{t('common.account.changeEmail.resend')}</span>
|
||||
<span onClick={sendCodeToOriginEmail} className="system-xs-medium cursor-pointer text-text-accent-secondary">{t('account.changeEmail.resend', { ns: 'common' })}</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{step === STEP.newEmail && (
|
||||
<>
|
||||
<div className="title-2xl-semi-bold pb-3 text-text-primary">{t('common.account.changeEmail.newEmail')}</div>
|
||||
<div className="title-2xl-semi-bold pb-3 text-text-primary">{t('account.changeEmail.newEmail', { ns: 'common' })}</div>
|
||||
<div className="space-y-0.5 pb-2 pt-1">
|
||||
<div className="body-md-regular text-text-secondary">{t('common.account.changeEmail.content3')}</div>
|
||||
<div className="body-md-regular text-text-secondary">{t('account.changeEmail.content3', { ns: 'common' })}</div>
|
||||
</div>
|
||||
<div className="pt-3">
|
||||
<div className="system-sm-medium mb-1 flex h-6 items-center text-text-secondary">{t('common.account.changeEmail.emailLabel')}</div>
|
||||
<div className="system-sm-medium mb-1 flex h-6 items-center text-text-secondary">{t('account.changeEmail.emailLabel', { ns: 'common' })}</div>
|
||||
<Input
|
||||
className="!w-full"
|
||||
placeholder={t('common.account.changeEmail.emailPlaceholder')}
|
||||
placeholder={t('account.changeEmail.emailPlaceholder', { ns: 'common' })}
|
||||
value={mail}
|
||||
onChange={e => handleNewEmailValueChange(e.target.value)}
|
||||
destructive={newEmailExited || unAvailableEmail}
|
||||
/>
|
||||
{newEmailExited && (
|
||||
<div className="body-xs-regular mt-1 py-0.5 text-text-destructive">{t('common.account.changeEmail.existingEmail')}</div>
|
||||
<div className="body-xs-regular mt-1 py-0.5 text-text-destructive">{t('account.changeEmail.existingEmail', { ns: 'common' })}</div>
|
||||
)}
|
||||
{unAvailableEmail && (
|
||||
<div className="body-xs-regular mt-1 py-0.5 text-text-destructive">{t('common.account.changeEmail.unAvailableEmail')}</div>
|
||||
<div className="body-xs-regular mt-1 py-0.5 text-text-destructive">{t('account.changeEmail.unAvailableEmail', { ns: 'common' })}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-3 space-y-2">
|
||||
|
|
@ -316,20 +316,20 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
|
|||
variant="primary"
|
||||
onClick={sendCodeToNewEmail}
|
||||
>
|
||||
{t('common.account.changeEmail.sendVerifyCode')}
|
||||
{t('account.changeEmail.sendVerifyCode', { ns: 'common' })}
|
||||
</Button>
|
||||
<Button
|
||||
className="!w-full"
|
||||
onClick={onClose}
|
||||
>
|
||||
{t('common.operation.cancel')}
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{step === STEP.verifyNew && (
|
||||
<>
|
||||
<div className="title-2xl-semi-bold pb-3 text-text-primary">{t('common.account.changeEmail.verifyNew')}</div>
|
||||
<div className="title-2xl-semi-bold pb-3 text-text-primary">{t('account.changeEmail.verifyNew', { ns: 'common' })}</div>
|
||||
<div className="space-y-0.5 pb-2 pt-1">
|
||||
<div className="body-md-regular text-text-secondary">
|
||||
<Trans
|
||||
|
|
@ -340,10 +340,10 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
|
|||
</div>
|
||||
</div>
|
||||
<div className="pt-3">
|
||||
<div className="system-sm-medium mb-1 flex h-6 items-center text-text-secondary">{t('common.account.changeEmail.codeLabel')}</div>
|
||||
<div className="system-sm-medium mb-1 flex h-6 items-center text-text-secondary">{t('account.changeEmail.codeLabel', { ns: 'common' })}</div>
|
||||
<Input
|
||||
className="!w-full"
|
||||
placeholder={t('common.account.changeEmail.codePlaceholder')}
|
||||
placeholder={t('account.changeEmail.codePlaceholder', { ns: 'common' })}
|
||||
value={code}
|
||||
onChange={e => setCode(e.target.value)}
|
||||
maxLength={6}
|
||||
|
|
@ -356,22 +356,22 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
|
|||
variant="primary"
|
||||
onClick={submitNewEmail}
|
||||
>
|
||||
{t('common.account.changeEmail.changeTo', { email: mail })}
|
||||
{t('account.changeEmail.changeTo', { ns: 'common', email: mail })}
|
||||
</Button>
|
||||
<Button
|
||||
className="!w-full"
|
||||
onClick={onClose}
|
||||
>
|
||||
{t('common.operation.cancel')}
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="system-xs-regular mt-3 flex items-center gap-1 text-text-tertiary">
|
||||
<span>{t('common.account.changeEmail.resendTip')}</span>
|
||||
<span>{t('account.changeEmail.resendTip', { ns: 'common' })}</span>
|
||||
{time > 0 && (
|
||||
<span>{t('common.account.changeEmail.resendCount', { count: time })}</span>
|
||||
<span>{t('account.changeEmail.resendCount', { ns: 'common', count: time })}</span>
|
||||
)}
|
||||
{!time && (
|
||||
<span onClick={sendCodeToNewEmail} className="system-xs-medium cursor-pointer text-text-accent-secondary">{t('common.account.changeEmail.resend')}</span>
|
||||
<span onClick={sendCodeToNewEmail} className="system-xs-medium cursor-pointer text-text-accent-secondary">{t('account.changeEmail.resend', { ns: 'common' })}</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ export default function AccountPage() {
|
|||
try {
|
||||
setEditing(true)
|
||||
await updateUserProfile({ url: 'account/name', body: { name: editName } })
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) })
|
||||
mutateUserProfile()
|
||||
setEditNameModalVisible(false)
|
||||
setEditing(false)
|
||||
|
|
@ -80,15 +80,15 @@ export default function AccountPage() {
|
|||
}
|
||||
const valid = () => {
|
||||
if (!password.trim()) {
|
||||
showErrorMessage(t('login.error.passwordEmpty'))
|
||||
showErrorMessage(t('error.passwordEmpty', { ns: 'login' }))
|
||||
return false
|
||||
}
|
||||
if (!validPassword.test(password)) {
|
||||
showErrorMessage(t('login.error.passwordInvalid'))
|
||||
showErrorMessage(t('error.passwordInvalid', { ns: 'login' }))
|
||||
return false
|
||||
}
|
||||
if (password !== confirmPassword) {
|
||||
showErrorMessage(t('common.account.notEqual'))
|
||||
showErrorMessage(t('account.notEqual', { ns: 'common' }))
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
@ -112,7 +112,7 @@ export default function AccountPage() {
|
|||
repeat_new_password: confirmPassword,
|
||||
},
|
||||
})
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) })
|
||||
mutateUserProfile()
|
||||
setEditPasswordModalVisible(false)
|
||||
resetPasswordForm()
|
||||
|
|
@ -146,7 +146,7 @@ export default function AccountPage() {
|
|||
return (
|
||||
<>
|
||||
<div className="pb-3 pt-2">
|
||||
<h4 className="title-2xl-semi-bold text-text-primary">{t('common.account.myAccount')}</h4>
|
||||
<h4 className="title-2xl-semi-bold text-text-primary">{t('account.myAccount', { ns: 'common' })}</h4>
|
||||
</div>
|
||||
<div className="mb-8 flex items-center rounded-xl bg-gradient-to-r from-background-gradient-bg-fill-chat-bg-2 to-background-gradient-bg-fill-chat-bg-1 p-6">
|
||||
<AvatarWithEdit avatar={userProfile.avatar_url} name={userProfile.name} onSave={mutateUserProfile} size={64} />
|
||||
|
|
@ -164,25 +164,25 @@ export default function AccountPage() {
|
|||
</div>
|
||||
</div>
|
||||
<div className="mb-8">
|
||||
<div className={titleClassName}>{t('common.account.name')}</div>
|
||||
<div className={titleClassName}>{t('account.name', { ns: 'common' })}</div>
|
||||
<div className="mt-2 flex w-full items-center justify-between gap-2">
|
||||
<div className="system-sm-regular flex-1 rounded-lg bg-components-input-bg-normal p-2 text-components-input-text-filled ">
|
||||
<span className="pl-1">{userProfile.name}</span>
|
||||
</div>
|
||||
<div className="system-sm-medium cursor-pointer rounded-lg bg-components-button-tertiary-bg px-3 py-2 text-components-button-tertiary-text" onClick={handleEditName}>
|
||||
{t('common.operation.edit')}
|
||||
{t('operation.edit', { ns: 'common' })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-8">
|
||||
<div className={titleClassName}>{t('common.account.email')}</div>
|
||||
<div className={titleClassName}>{t('account.email', { ns: 'common' })}</div>
|
||||
<div className="mt-2 flex w-full items-center justify-between gap-2">
|
||||
<div className="system-sm-regular flex-1 rounded-lg bg-components-input-bg-normal p-2 text-components-input-text-filled ">
|
||||
<span className="pl-1">{userProfile.email}</span>
|
||||
</div>
|
||||
{systemFeatures.enable_change_email && (
|
||||
<div className="system-sm-medium cursor-pointer rounded-lg bg-components-button-tertiary-bg px-3 py-2 text-components-button-tertiary-text" onClick={() => setShowUpdateEmail(true)}>
|
||||
{t('common.operation.change')}
|
||||
{t('operation.change', { ns: 'common' })}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -191,26 +191,26 @@ export default function AccountPage() {
|
|||
systemFeatures.enable_email_password_login && (
|
||||
<div className="mb-8 flex justify-between gap-2">
|
||||
<div>
|
||||
<div className="system-sm-semibold mb-1 text-text-secondary">{t('common.account.password')}</div>
|
||||
<div className="body-xs-regular mb-2 text-text-tertiary">{t('common.account.passwordTip')}</div>
|
||||
<div className="system-sm-semibold mb-1 text-text-secondary">{t('account.password', { ns: 'common' })}</div>
|
||||
<div className="body-xs-regular mb-2 text-text-tertiary">{t('account.passwordTip', { ns: 'common' })}</div>
|
||||
</div>
|
||||
<Button onClick={() => setEditPasswordModalVisible(true)}>{userProfile.is_password_set ? t('common.account.resetPassword') : t('common.account.setPassword')}</Button>
|
||||
<Button onClick={() => setEditPasswordModalVisible(true)}>{userProfile.is_password_set ? t('account.resetPassword', { ns: 'common' }) : t('account.setPassword', { ns: 'common' })}</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className="mb-6 border-[1px] border-divider-subtle" />
|
||||
<div className="mb-8">
|
||||
<div className={titleClassName}>{t('common.account.langGeniusAccount')}</div>
|
||||
<div className={descriptionClassName}>{t('common.account.langGeniusAccountTip')}</div>
|
||||
<div className={titleClassName}>{t('account.langGeniusAccount', { ns: 'common' })}</div>
|
||||
<div className={descriptionClassName}>{t('account.langGeniusAccountTip', { ns: 'common' })}</div>
|
||||
{!!apps.length && (
|
||||
<Collapse
|
||||
title={`${t('common.account.showAppLength', { length: apps.length })}`}
|
||||
title={`${t('account.showAppLength', { ns: 'common', length: apps.length })}`}
|
||||
items={apps.map((app: App) => ({ ...app, key: app.id, name: app.name }))}
|
||||
renderItem={renderAppItem}
|
||||
wrapperClassName="mt-2"
|
||||
/>
|
||||
)}
|
||||
{!IS_CE_EDITION && <Button className="mt-2 text-components-button-destructive-secondary-text" onClick={() => setShowDeleteAccountModal(true)}>{t('common.account.delete')}</Button>}
|
||||
{!IS_CE_EDITION && <Button className="mt-2 text-components-button-destructive-secondary-text" onClick={() => setShowDeleteAccountModal(true)}>{t('account.delete', { ns: 'common' })}</Button>}
|
||||
</div>
|
||||
{
|
||||
editNameModalVisible && (
|
||||
|
|
@ -219,21 +219,21 @@ export default function AccountPage() {
|
|||
onClose={() => setEditNameModalVisible(false)}
|
||||
className="!w-[420px] !p-6"
|
||||
>
|
||||
<div className="title-2xl-semi-bold mb-6 text-text-primary">{t('common.account.editName')}</div>
|
||||
<div className={titleClassName}>{t('common.account.name')}</div>
|
||||
<div className="title-2xl-semi-bold mb-6 text-text-primary">{t('account.editName', { ns: 'common' })}</div>
|
||||
<div className={titleClassName}>{t('account.name', { ns: 'common' })}</div>
|
||||
<Input
|
||||
className="mt-2"
|
||||
value={editName}
|
||||
onChange={e => setEditName(e.target.value)}
|
||||
/>
|
||||
<div className="mt-10 flex justify-end">
|
||||
<Button className="mr-2" onClick={() => setEditNameModalVisible(false)}>{t('common.operation.cancel')}</Button>
|
||||
<Button className="mr-2" onClick={() => setEditNameModalVisible(false)}>{t('operation.cancel', { ns: 'common' })}</Button>
|
||||
<Button
|
||||
disabled={editing || !editName}
|
||||
variant="primary"
|
||||
onClick={handleSaveName}
|
||||
>
|
||||
{t('common.operation.save')}
|
||||
{t('operation.save', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
|
@ -249,10 +249,10 @@ export default function AccountPage() {
|
|||
}}
|
||||
className="!w-[420px] !p-6"
|
||||
>
|
||||
<div className="title-2xl-semi-bold mb-6 text-text-primary">{userProfile.is_password_set ? t('common.account.resetPassword') : t('common.account.setPassword')}</div>
|
||||
<div className="title-2xl-semi-bold mb-6 text-text-primary">{userProfile.is_password_set ? t('account.resetPassword', { ns: 'common' }) : t('account.setPassword', { ns: 'common' })}</div>
|
||||
{userProfile.is_password_set && (
|
||||
<>
|
||||
<div className={titleClassName}>{t('common.account.currentPassword')}</div>
|
||||
<div className={titleClassName}>{t('account.currentPassword', { ns: 'common' })}</div>
|
||||
<div className="relative mt-2">
|
||||
<Input
|
||||
type={showCurrentPassword ? 'text' : 'password'}
|
||||
|
|
@ -273,7 +273,7 @@ export default function AccountPage() {
|
|||
</>
|
||||
)}
|
||||
<div className="system-sm-semibold mt-8 text-text-secondary">
|
||||
{userProfile.is_password_set ? t('common.account.newPassword') : t('common.account.password')}
|
||||
{userProfile.is_password_set ? t('account.newPassword', { ns: 'common' }) : t('account.password', { ns: 'common' })}
|
||||
</div>
|
||||
<div className="relative mt-2">
|
||||
<Input
|
||||
|
|
@ -291,7 +291,7 @@ export default function AccountPage() {
|
|||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="system-sm-semibold mt-8 text-text-secondary">{t('common.account.confirmPassword')}</div>
|
||||
<div className="system-sm-semibold mt-8 text-text-secondary">{t('account.confirmPassword', { ns: 'common' })}</div>
|
||||
<div className="relative mt-2">
|
||||
<Input
|
||||
type={showConfirmPassword ? 'text' : 'password'}
|
||||
|
|
@ -316,14 +316,14 @@ export default function AccountPage() {
|
|||
resetPasswordForm()
|
||||
}}
|
||||
>
|
||||
{t('common.operation.cancel')}
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={editing}
|
||||
variant="primary"
|
||||
onClick={handleSavePassword}
|
||||
>
|
||||
{userProfile.is_password_set ? t('common.operation.reset') : t('common.operation.save')}
|
||||
{userProfile.is_password_set ? t('operation.reset', { ns: 'common' }) : t('operation.save', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ export default function AppSelector() {
|
|||
className="group flex h-9 cursor-pointer items-center justify-start rounded-lg px-3 hover:bg-state-base-hover"
|
||||
>
|
||||
<LogOut01 className="mr-1 flex h-4 w-4 text-text-tertiary" />
|
||||
<div className="text-[14px] font-normal text-text-secondary">{t('common.userProfile.logout')}</div>
|
||||
<div className="text-[14px] font-normal text-text-secondary">{t('userProfile.logout', { ns: 'common' })}</div>
|
||||
</div>
|
||||
</div>
|
||||
</MenuItem>
|
||||
|
|
|
|||
|
|
@ -31,22 +31,22 @@ export default function CheckEmail(props: DeleteAccountProps) {
|
|||
return (
|
||||
<>
|
||||
<div className="body-md-medium py-1 text-text-destructive">
|
||||
{t('common.account.deleteTip')}
|
||||
{t('account.deleteTip', { ns: 'common' })}
|
||||
</div>
|
||||
<div className="body-md-regular pb-2 pt-1 text-text-secondary">
|
||||
{t('common.account.deletePrivacyLinkTip')}
|
||||
<Link href="https://dify.ai/privacy" className="text-text-accent">{t('common.account.deletePrivacyLink')}</Link>
|
||||
{t('account.deletePrivacyLinkTip', { ns: 'common' })}
|
||||
<Link href="https://dify.ai/privacy" className="text-text-accent">{t('account.deletePrivacyLink', { ns: 'common' })}</Link>
|
||||
</div>
|
||||
<label className="system-sm-semibold mb-1 mt-3 flex h-6 items-center text-text-secondary">{t('common.account.deleteLabel')}</label>
|
||||
<label className="system-sm-semibold mb-1 mt-3 flex h-6 items-center text-text-secondary">{t('account.deleteLabel', { ns: 'common' })}</label>
|
||||
<Input
|
||||
placeholder={t('common.account.deletePlaceholder') as string}
|
||||
placeholder={t('account.deletePlaceholder', { ns: 'common' }) as string}
|
||||
onChange={(e) => {
|
||||
setUserInputEmail(e.target.value)
|
||||
}}
|
||||
/>
|
||||
<div className="mt-3 flex w-full flex-col gap-2">
|
||||
<Button className="w-full" disabled={userInputEmail !== userProfile.email || isSendingEmail} loading={isSendingEmail} variant="primary" onClick={handleConfirm}>{t('common.account.sendVerificationButton')}</Button>
|
||||
<Button className="w-full" onClick={props.onCancel}>{t('common.operation.cancel')}</Button>
|
||||
<Button className="w-full" disabled={userInputEmail !== userProfile.email || isSendingEmail} loading={isSendingEmail} variant="primary" onClick={handleConfirm}>{t('account.sendVerificationButton', { ns: 'common' })}</Button>
|
||||
<Button className="w-full" onClick={props.onCancel}>{t('operation.cancel', { ns: 'common' })}</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export default function FeedBack(props: DeleteAccountProps) {
|
|||
await logout()
|
||||
// Tokens are now stored in cookies and cleared by backend
|
||||
router.push('/signin')
|
||||
Toast.notify({ type: 'info', message: t('common.account.deleteSuccessTip') })
|
||||
Toast.notify({ type: 'info', message: t('account.deleteSuccessTip', { ns: 'common' }) })
|
||||
}
|
||||
catch (error) { console.error(error) }
|
||||
}, [router, t])
|
||||
|
|
@ -50,22 +50,22 @@ export default function FeedBack(props: DeleteAccountProps) {
|
|||
<CustomDialog
|
||||
show={true}
|
||||
onClose={props.onCancel}
|
||||
title={t('common.account.feedbackTitle')}
|
||||
title={t('account.feedbackTitle', { ns: 'common' })}
|
||||
className="max-w-[480px]"
|
||||
footer={false}
|
||||
>
|
||||
<label className="system-sm-semibold mb-1 mt-3 flex items-center text-text-secondary">{t('common.account.feedbackLabel')}</label>
|
||||
<label className="system-sm-semibold mb-1 mt-3 flex items-center text-text-secondary">{t('account.feedbackLabel', { ns: 'common' })}</label>
|
||||
<Textarea
|
||||
rows={6}
|
||||
value={userFeedback}
|
||||
placeholder={t('common.account.feedbackPlaceholder') as string}
|
||||
placeholder={t('account.feedbackPlaceholder', { ns: 'common' }) as string}
|
||||
onChange={(e) => {
|
||||
setUserFeedback(e.target.value)
|
||||
}}
|
||||
/>
|
||||
<div className="mt-3 flex w-full flex-col gap-2">
|
||||
<Button className="w-full" loading={isPending} variant="primary" onClick={handleSubmit}>{t('common.operation.submit')}</Button>
|
||||
<Button className="w-full" onClick={handleSkip}>{t('common.operation.skip')}</Button>
|
||||
<Button className="w-full" loading={isPending} variant="primary" onClick={handleSubmit}>{t('operation.submit', { ns: 'common' })}</Button>
|
||||
<Button className="w-full" onClick={handleSkip}>{t('operation.skip', { ns: 'common' })}</Button>
|
||||
</div>
|
||||
</CustomDialog>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -37,24 +37,24 @@ export default function VerifyEmail(props: DeleteAccountProps) {
|
|||
return (
|
||||
<>
|
||||
<div className="body-md-medium pt-1 text-text-destructive">
|
||||
{t('common.account.deleteTip')}
|
||||
{t('account.deleteTip', { ns: 'common' })}
|
||||
</div>
|
||||
<div className="body-md-regular pb-2 pt-1 text-text-secondary">
|
||||
{t('common.account.deletePrivacyLinkTip')}
|
||||
<Link href="https://dify.ai/privacy" className="text-text-accent">{t('common.account.deletePrivacyLink')}</Link>
|
||||
{t('account.deletePrivacyLinkTip', { ns: 'common' })}
|
||||
<Link href="https://dify.ai/privacy" className="text-text-accent">{t('account.deletePrivacyLink', { ns: 'common' })}</Link>
|
||||
</div>
|
||||
<label className="system-sm-semibold mb-1 mt-3 flex h-6 items-center text-text-secondary">{t('common.account.verificationLabel')}</label>
|
||||
<label className="system-sm-semibold mb-1 mt-3 flex h-6 items-center text-text-secondary">{t('account.verificationLabel', { ns: 'common' })}</label>
|
||||
<Input
|
||||
minLength={6}
|
||||
maxLength={6}
|
||||
placeholder={t('common.account.verificationPlaceholder') as string}
|
||||
placeholder={t('account.verificationPlaceholder', { ns: 'common' }) as string}
|
||||
onChange={(e) => {
|
||||
setVerificationCode(e.target.value)
|
||||
}}
|
||||
/>
|
||||
<div className="mt-3 flex w-full flex-col gap-2">
|
||||
<Button className="w-full" disabled={shouldButtonDisabled} loading={isDeleting} variant="warning" onClick={handleConfirm}>{t('common.account.permanentlyDeleteButton')}</Button>
|
||||
<Button className="w-full" onClick={props.onCancel}>{t('common.operation.cancel')}</Button>
|
||||
<Button className="w-full" disabled={shouldButtonDisabled} loading={isDeleting} variant="warning" onClick={handleConfirm}>{t('account.permanentlyDeleteButton', { ns: 'common' })}</Button>
|
||||
<Button className="w-full" onClick={props.onCancel}>{t('operation.cancel', { ns: 'common' })}</Button>
|
||||
<Countdown onResend={sendEmail} />
|
||||
</div>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export default function DeleteAccount(props: DeleteAccountProps) {
|
|||
<CustomDialog
|
||||
show={true}
|
||||
onClose={props.onCancel}
|
||||
title={t('common.account.delete')}
|
||||
title={t('account.delete', { ns: 'common' })}
|
||||
className="max-w-[480px]"
|
||||
footer={false}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -32,12 +32,12 @@ const Header = () => {
|
|||
: <DifyLogo />}
|
||||
</div>
|
||||
<div className="h-4 w-[1px] origin-center rotate-[11.31deg] bg-divider-regular" />
|
||||
<p className="title-3xl-semi-bold relative mt-[-2px] text-text-primary">{t('common.account.account')}</p>
|
||||
<p className="title-3xl-semi-bold relative mt-[-2px] text-text-primary">{t('account.account', { ns: 'common' })}</p>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-3">
|
||||
<Button className="system-sm-medium gap-2 px-3 py-2" onClick={goToStudio}>
|
||||
<RiRobot2Line className="h-4 w-4" />
|
||||
<p>{t('common.account.studio')}</p>
|
||||
<p>{t('account.studio', { ns: 'common' })}</p>
|
||||
<RiArrowRightUpLine className="h-4 w-4" />
|
||||
</Button>
|
||||
<div className="h-4 w-[1px] bg-divider-regular" />
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import type { ReactNode } from 'react'
|
||||
import * as React from 'react'
|
||||
import { AppInitializer } from '@/app/components/app-initializer'
|
||||
import AmplitudeProvider from '@/app/components/base/amplitude'
|
||||
import GA, { GaType } from '@/app/components/base/ga'
|
||||
import HeaderWrapper from '@/app/components/header/header-wrapper'
|
||||
import SwrInitor from '@/app/components/swr-initializer'
|
||||
import { AppContextProvider } from '@/context/app-context'
|
||||
import { EventEmitterContextProvider } from '@/context/event-emitter'
|
||||
import { ModalContextProvider } from '@/context/modal-context'
|
||||
|
|
@ -15,7 +15,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
|
|||
<>
|
||||
<GA gaType={GaType.admin} />
|
||||
<AmplitudeProvider />
|
||||
<SwrInitor>
|
||||
<AppInitializer>
|
||||
<AppContextProvider>
|
||||
<EventEmitterContextProvider>
|
||||
<ProviderContextProvider>
|
||||
|
|
@ -30,7 +30,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
|
|||
</ProviderContextProvider>
|
||||
</EventEmitterContextProvider>
|
||||
</AppContextProvider>
|
||||
</SwrInitor>
|
||||
</AppInitializer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import AccountPage from './account-page'
|
|||
|
||||
export default function Account() {
|
||||
const { t } = useTranslation()
|
||||
useDocumentTitle(t('common.menus.account'))
|
||||
useDocumentTitle(t('menus.account', { ns: 'common' }))
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-[640px] px-6 pt-12">
|
||||
<AccountPage />
|
||||
|
|
|
|||
|
|
@ -50,23 +50,23 @@ export default function OAuthAuthorize() {
|
|||
const SCOPE_INFO_MAP: Record<string, { icon: React.ComponentType<{ className?: string }>, label: string }> = {
|
||||
'read:name': {
|
||||
icon: RiInfoCardLine,
|
||||
label: t('oauth.scopes.name'),
|
||||
label: t('scopes.name', { ns: 'oauth' }),
|
||||
},
|
||||
'read:email': {
|
||||
icon: RiMailLine,
|
||||
label: t('oauth.scopes.email'),
|
||||
label: t('scopes.email', { ns: 'oauth' }),
|
||||
},
|
||||
'read:avatar': {
|
||||
icon: RiAccountCircleLine,
|
||||
label: t('oauth.scopes.avatar'),
|
||||
label: t('scopes.avatar', { ns: 'oauth' }),
|
||||
},
|
||||
'read:interface_language': {
|
||||
icon: RiTranslate2,
|
||||
label: t('oauth.scopes.languagePreference'),
|
||||
label: t('scopes.languagePreference', { ns: 'oauth' }),
|
||||
},
|
||||
'read:timezone': {
|
||||
icon: RiGlobalLine,
|
||||
label: t('oauth.scopes.timezone'),
|
||||
label: t('scopes.timezone', { ns: 'oauth' }),
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -106,7 +106,7 @@ export default function OAuthAuthorize() {
|
|||
catch (err: any) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: `${t('oauth.error.authorizeFailed')}: ${err.message}`,
|
||||
message: `${t('error.authorizeFailed', { ns: 'oauth' })}: ${err.message}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -117,7 +117,7 @@ export default function OAuthAuthorize() {
|
|||
hasNotifiedRef.current = true
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: invalidParams ? t('oauth.error.invalidParams') : t('oauth.error.authAppInfoFetchFailed'),
|
||||
message: invalidParams ? t('error.invalidParams', { ns: 'oauth' }) : t('error.authAppInfoFetchFailed', { ns: 'oauth' }),
|
||||
duration: 0,
|
||||
})
|
||||
}
|
||||
|
|
@ -141,11 +141,11 @@ export default function OAuthAuthorize() {
|
|||
|
||||
<div className={`mb-4 mt-5 flex flex-col gap-2 ${isLoggedIn ? 'pb-2' : ''}`}>
|
||||
<div className="title-4xl-semi-bold">
|
||||
{isLoggedIn && <div className="text-text-primary">{t('oauth.connect')}</div>}
|
||||
<div className="text-[var(--color-saas-dify-blue-inverted)]">{authAppInfo?.app_label[language] || authAppInfo?.app_label?.en_US || t('oauth.unknownApp')}</div>
|
||||
{!isLoggedIn && <div className="text-text-primary">{t('oauth.tips.notLoggedIn')}</div>}
|
||||
{isLoggedIn && <div className="text-text-primary">{t('connect', { ns: 'oauth' })}</div>}
|
||||
<div className="text-[var(--color-saas-dify-blue-inverted)]">{authAppInfo?.app_label[language] || authAppInfo?.app_label?.en_US || t('unknownApp', { ns: 'oauth' })}</div>
|
||||
{!isLoggedIn && <div className="text-text-primary">{t('tips.notLoggedIn', { ns: 'oauth' })}</div>}
|
||||
</div>
|
||||
<div className="body-md-regular text-text-secondary">{isLoggedIn ? `${authAppInfo?.app_label[language] || authAppInfo?.app_label?.en_US || t('oauth.unknownApp')} ${t('oauth.tips.loggedIn')}` : t('oauth.tips.needLogin')}</div>
|
||||
<div className="body-md-regular text-text-secondary">{isLoggedIn ? `${authAppInfo?.app_label[language] || authAppInfo?.app_label?.en_US || t('unknownApp', { ns: 'oauth' })} ${t('tips.loggedIn', { ns: 'oauth' })}` : t('tips.needLogin', { ns: 'oauth' })}</div>
|
||||
</div>
|
||||
|
||||
{isLoggedIn && userProfile && (
|
||||
|
|
@ -157,7 +157,7 @@ export default function OAuthAuthorize() {
|
|||
<div className="system-xs-regular text-text-tertiary">{userProfile.email}</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="tertiary" size="small" onClick={onLoginSwitchClick}>{t('oauth.switchAccount')}</Button>
|
||||
<Button variant="tertiary" size="small" onClick={onLoginSwitchClick}>{t('switchAccount', { ns: 'oauth' })}</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -178,12 +178,12 @@ export default function OAuthAuthorize() {
|
|||
<div className="flex flex-col items-center gap-2 pt-4">
|
||||
{!isLoggedIn
|
||||
? (
|
||||
<Button variant="primary" size="large" className="w-full" onClick={onLoginSwitchClick}>{t('oauth.login')}</Button>
|
||||
<Button variant="primary" size="large" className="w-full" onClick={onLoginSwitchClick}>{t('login', { ns: 'oauth' })}</Button>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<Button variant="primary" size="large" className="w-full" onClick={onAuthorize} disabled={!client_id || !redirect_uri || isError || authorizing} loading={authorizing}>{t('oauth.continue')}</Button>
|
||||
<Button size="large" className="w-full" onClick={() => router.push('/apps')}>{t('common.operation.cancel')}</Button>
|
||||
<Button variant="primary" size="large" className="w-full" onClick={onAuthorize} disabled={!client_id || !redirect_uri || isError || authorizing} loading={authorizing}>{t('continue', { ns: 'oauth' })}</Button>
|
||||
<Button size="large" className="w-full" onClick={() => router.push('/apps')}>{t('operation.cancel', { ns: 'common' })}</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -199,7 +199,7 @@ export default function OAuthAuthorize() {
|
|||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="system-xs-regular mt-3 text-text-tertiary">{t('oauth.tips.common')}</div>
|
||||
<div className="system-xs-regular mt-3 text-text-tertiary">{t('tips.common', { ns: 'oauth' })}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,11 +56,11 @@ const ActivateForm = () => {
|
|||
<div className="flex flex-col md:w-[400px]">
|
||||
<div className="mx-auto w-full">
|
||||
<div className="mb-3 flex h-20 w-20 items-center justify-center rounded-[20px] border border-divider-regular bg-components-option-card-option-bg p-5 text-[40px] font-bold shadow-lg">🤷♂️</div>
|
||||
<h2 className="text-[32px] font-bold text-text-primary">{t('login.invalid')}</h2>
|
||||
<h2 className="text-[32px] font-bold text-text-primary">{t('invalid', { ns: 'login' })}</h2>
|
||||
</div>
|
||||
<div className="mx-auto mt-6 w-full">
|
||||
<Button variant="primary" className="w-full !text-sm">
|
||||
<a href="https://dify.ai">{t('login.explore')}</a>
|
||||
<a href="https://dify.ai">{t('explore', { ns: 'login' })}</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
import type { ReactNode } from 'react'
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { SWRConfig } from 'swr'
|
||||
import {
|
||||
EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION,
|
||||
EDUCATION_VERIFYING_LOCALSTORAGE_ITEM,
|
||||
|
|
@ -11,12 +10,13 @@ import {
|
|||
import { fetchSetupStatus } from '@/service/common'
|
||||
import { resolvePostLoginRedirect } from '../signin/utils/post-login-redirect'
|
||||
|
||||
type SwrInitializerProps = {
|
||||
type AppInitializerProps = {
|
||||
children: ReactNode
|
||||
}
|
||||
const SwrInitializer = ({
|
||||
|
||||
export const AppInitializer = ({
|
||||
children,
|
||||
}: SwrInitializerProps) => {
|
||||
}: AppInitializerProps) => {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
// Tokens are now stored in cookies, no need to check localStorage
|
||||
|
|
@ -69,20 +69,5 @@ const SwrInitializer = ({
|
|||
})()
|
||||
}, [isSetupFinished, router, pathname, searchParams])
|
||||
|
||||
return init
|
||||
? (
|
||||
<SWRConfig value={{
|
||||
shouldRetryOnError: false,
|
||||
revalidateOnFocus: false,
|
||||
dedupingInterval: 60000,
|
||||
focusThrottleInterval: 5000,
|
||||
provider: () => new Map(),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</SWRConfig>
|
||||
)
|
||||
: null
|
||||
return init ? children : null
|
||||
}
|
||||
|
||||
export default SwrInitializer
|
||||
|
|
@ -100,12 +100,12 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
|
|||
setShowEditModal(false)
|
||||
notify({
|
||||
type: 'success',
|
||||
message: t('app.editDone'),
|
||||
message: t('editDone', { ns: 'app' }),
|
||||
})
|
||||
setAppDetail(app)
|
||||
}
|
||||
catch {
|
||||
notify({ type: 'error', message: t('app.editFailed') })
|
||||
notify({ type: 'error', message: t('editFailed', { ns: 'app' }) })
|
||||
}
|
||||
}, [appDetail, notify, setAppDetail, t])
|
||||
|
||||
|
|
@ -124,14 +124,14 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
|
|||
setShowDuplicateModal(false)
|
||||
notify({
|
||||
type: 'success',
|
||||
message: t('app.newApp.appCreated'),
|
||||
message: t('newApp.appCreated', { ns: 'app' }),
|
||||
})
|
||||
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
|
||||
onPlanInfoChanged()
|
||||
getRedirection(true, newApp, replace)
|
||||
}
|
||||
catch {
|
||||
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
|
||||
notify({ type: 'error', message: t('newApp.appCreateFailed', { ns: 'app' }) })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -152,7 +152,7 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
|
|||
URL.revokeObjectURL(url)
|
||||
}
|
||||
catch {
|
||||
notify({ type: 'error', message: t('app.exportFailed') })
|
||||
notify({ type: 'error', message: t('exportFailed', { ns: 'app' }) })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -181,7 +181,7 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
|
|||
setSecretEnvList(list)
|
||||
}
|
||||
catch {
|
||||
notify({ type: 'error', message: t('app.exportFailed') })
|
||||
notify({ type: 'error', message: t('exportFailed', { ns: 'app' }) })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -190,7 +190,7 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
|
|||
return
|
||||
try {
|
||||
await deleteApp(appDetail.id)
|
||||
notify({ type: 'success', message: t('app.appDeleted') })
|
||||
notify({ type: 'success', message: t('appDeleted', { ns: 'app' }) })
|
||||
onPlanInfoChanged()
|
||||
setAppDetail()
|
||||
replace('/apps')
|
||||
|
|
@ -198,7 +198,7 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
|
|||
catch (e: any) {
|
||||
notify({
|
||||
type: 'error',
|
||||
message: `${t('app.appDeleteFailed')}${'message' in e ? `: ${e.message}` : ''}`,
|
||||
message: `${t('appDeleteFailed', { ns: 'app' })}${'message' in e ? `: ${e.message}` : ''}`,
|
||||
})
|
||||
}
|
||||
setShowConfirmDelete(false)
|
||||
|
|
@ -212,7 +212,7 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
|
|||
const primaryOperations = [
|
||||
{
|
||||
id: 'edit',
|
||||
title: t('app.editApp'),
|
||||
title: t('editApp', { ns: 'app' }),
|
||||
icon: <RiEditLine />,
|
||||
onClick: () => {
|
||||
setOpen(false)
|
||||
|
|
@ -222,7 +222,7 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
|
|||
},
|
||||
{
|
||||
id: 'duplicate',
|
||||
title: t('app.duplicate'),
|
||||
title: t('duplicate', { ns: 'app' }),
|
||||
icon: <RiFileCopy2Line />,
|
||||
onClick: () => {
|
||||
setOpen(false)
|
||||
|
|
@ -232,7 +232,7 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
|
|||
},
|
||||
{
|
||||
id: 'export',
|
||||
title: t('app.export'),
|
||||
title: t('export', { ns: 'app' }),
|
||||
icon: <RiFileDownloadLine />,
|
||||
onClick: exportCheck,
|
||||
},
|
||||
|
|
@ -243,7 +243,7 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
|
|||
...(appDetail.mode === AppModeEnum.ADVANCED_CHAT || appDetail.mode === AppModeEnum.WORKFLOW)
|
||||
? [{
|
||||
id: 'import',
|
||||
title: t('workflow.common.importDSL'),
|
||||
title: t('common.importDSL', { ns: 'workflow' }),
|
||||
icon: <RiFileUploadLine />,
|
||||
onClick: () => {
|
||||
setOpen(false)
|
||||
|
|
@ -263,7 +263,7 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
|
|||
// Delete operation
|
||||
{
|
||||
id: 'delete',
|
||||
title: t('common.operation.delete'),
|
||||
title: t('operation.delete', { ns: 'common' }),
|
||||
icon: <RiDeleteBinLine />,
|
||||
onClick: () => {
|
||||
setOpen(false)
|
||||
|
|
@ -277,7 +277,7 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
|
|||
const switchOperation = (appDetail.mode === AppModeEnum.COMPLETION || appDetail.mode === AppModeEnum.CHAT)
|
||||
? {
|
||||
id: 'switch',
|
||||
title: t('app.switch'),
|
||||
title: t('switch', { ns: 'app' }),
|
||||
icon: <RiExchange2Line />,
|
||||
onClick: () => {
|
||||
setOpen(false)
|
||||
|
|
@ -331,14 +331,14 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
|
|||
</div>
|
||||
<div className="system-2xs-medium-uppercase whitespace-nowrap text-text-tertiary">
|
||||
{appDetail.mode === AppModeEnum.ADVANCED_CHAT
|
||||
? t('app.types.advanced')
|
||||
? t('types.advanced', { ns: 'app' })
|
||||
: appDetail.mode === AppModeEnum.AGENT_CHAT
|
||||
? t('app.types.agent')
|
||||
? t('types.agent', { ns: 'app' })
|
||||
: appDetail.mode === AppModeEnum.CHAT
|
||||
? t('app.types.chatbot')
|
||||
? t('types.chatbot', { ns: 'app' })
|
||||
: appDetail.mode === AppModeEnum.COMPLETION
|
||||
? t('app.types.completion')
|
||||
: t('app.types.workflow')}
|
||||
? t('types.completion', { ns: 'app' })
|
||||
: t('types.workflow', { ns: 'app' })}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -364,7 +364,7 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
|
|||
/>
|
||||
<div className="flex flex-1 flex-col items-start justify-center overflow-hidden">
|
||||
<div className="system-md-semibold w-full truncate text-text-secondary">{appDetail.name}</div>
|
||||
<div className="system-2xs-medium-uppercase text-text-tertiary">{appDetail.mode === AppModeEnum.ADVANCED_CHAT ? t('app.types.advanced') : appDetail.mode === AppModeEnum.AGENT_CHAT ? t('app.types.agent') : appDetail.mode === AppModeEnum.CHAT ? t('app.types.chatbot') : appDetail.mode === AppModeEnum.COMPLETION ? t('app.types.completion') : t('app.types.workflow')}</div>
|
||||
<div className="system-2xs-medium-uppercase text-text-tertiary">{appDetail.mode === AppModeEnum.ADVANCED_CHAT ? t('types.advanced', { ns: 'app' }) : appDetail.mode === AppModeEnum.AGENT_CHAT ? t('types.agent', { ns: 'app' }) : appDetail.mode === AppModeEnum.CHAT ? t('types.chatbot', { ns: 'app' }) : appDetail.mode === AppModeEnum.COMPLETION ? t('types.completion', { ns: 'app' }) : t('types.workflow', { ns: 'app' })}</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* description */}
|
||||
|
|
@ -438,8 +438,8 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
|
|||
)}
|
||||
{showConfirmDelete && (
|
||||
<Confirm
|
||||
title={t('app.deleteAppConfirmTitle')}
|
||||
content={t('app.deleteAppConfirmContent')}
|
||||
title={t('deleteAppConfirmTitle', { ns: 'app' })}
|
||||
content={t('deleteAppConfirmContent', { ns: 'app' })}
|
||||
isShow={showConfirmDelete}
|
||||
onConfirm={onConfirmDelete}
|
||||
onCancel={() => setShowConfirmDelete(false)}
|
||||
|
|
@ -462,8 +462,8 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
|
|||
<Confirm
|
||||
type="info"
|
||||
isShow={showExportWarning}
|
||||
title={t('workflow.sidebar.exportWarning')}
|
||||
content={t('workflow.sidebar.exportWarningDesc')}
|
||||
title={t('sidebar.exportWarning', { ns: 'workflow' })}
|
||||
content={t('sidebar.exportWarningDesc', { ns: 'workflow' })}
|
||||
onConfirm={handleConfirmExport}
|
||||
onCancel={() => setShowExportWarning(false)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ const AppOperations = ({
|
|||
>
|
||||
<RiMoreLine className="h-3.5 w-3.5 text-components-button-secondary-text" />
|
||||
<span className="system-xs-medium text-components-button-secondary-text">
|
||||
{t('common.operation.more')}
|
||||
{t('operation.more', { ns: 'common' })}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -183,7 +183,7 @@ const AppOperations = ({
|
|||
>
|
||||
<RiMoreLine className="h-3.5 w-3.5 text-components-button-secondary-text" />
|
||||
<span className="system-xs-medium text-components-button-secondary-text">
|
||||
{t('common.operation.more')}
|
||||
{t('operation.more', { ns: 'common' })}
|
||||
</span>
|
||||
</Button>
|
||||
</PortalToFollowElemTrigger>
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ const AppSidebarDropdown = ({ navigation }: Props) => {
|
|||
<div className="flex w-full">
|
||||
<div className="system-md-semibold truncate text-text-secondary">{appDetail.name}</div>
|
||||
</div>
|
||||
<div className="system-2xs-medium-uppercase text-text-tertiary">{appDetail.mode === AppModeEnum.ADVANCED_CHAT ? t('app.types.advanced') : appDetail.mode === AppModeEnum.AGENT_CHAT ? t('app.types.agent') : appDetail.mode === AppModeEnum.CHAT ? t('app.types.chatbot') : appDetail.mode === AppModeEnum.COMPLETION ? t('app.types.completion') : t('app.types.workflow')}</div>
|
||||
<div className="system-2xs-medium-uppercase text-text-tertiary">{appDetail.mode === AppModeEnum.ADVANCED_CHAT ? t('types.advanced', { ns: 'app' }) : appDetail.mode === AppModeEnum.AGENT_CHAT ? t('types.agent', { ns: 'app' }) : appDetail.mode === AppModeEnum.CHAT ? t('types.chatbot', { ns: 'app' }) : appDetail.mode === AppModeEnum.COMPLETION ? t('types.completion', { ns: 'app' }) : t('types.workflow', { ns: 'app' })}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ export default function AppBasic({ icon, icon_background, name, isExternal, type
|
|||
<div className="system-2xs-medium-uppercase flex text-text-tertiary">{type}</div>
|
||||
)}
|
||||
{!hideType && !isExtraInLine && (
|
||||
<div className="system-2xs-medium-uppercase text-text-tertiary">{isExternal ? t('dataset.externalTag') : type}</div>
|
||||
<div className="system-2xs-medium-uppercase text-text-tertiary">{isExternal ? t('externalTag', { ns: 'dataset' }) : type}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -73,14 +73,14 @@ const DropDown = ({
|
|||
URL.revokeObjectURL(url)
|
||||
}
|
||||
catch {
|
||||
Toast.notify({ type: 'error', message: t('app.exportFailed') })
|
||||
Toast.notify({ type: 'error', message: t('exportFailed', { ns: 'app' }) })
|
||||
}
|
||||
}, [dataset, exportPipelineConfig, handleTrigger, t])
|
||||
|
||||
const detectIsUsedByApp = useCallback(async () => {
|
||||
try {
|
||||
const { is_using: isUsedByApp } = await checkIsUsedInApp(dataset.id)
|
||||
setConfirmMessage(isUsedByApp ? t('dataset.datasetUsedByApp')! : t('dataset.deleteDatasetConfirmContent')!)
|
||||
setConfirmMessage(isUsedByApp ? t('datasetUsedByApp', { ns: 'dataset' })! : t('deleteDatasetConfirmContent', { ns: 'dataset' })!)
|
||||
setShowConfirmDelete(true)
|
||||
}
|
||||
catch (e: any) {
|
||||
|
|
@ -95,7 +95,7 @@ const DropDown = ({
|
|||
const onConfirmDelete = useCallback(async () => {
|
||||
try {
|
||||
await deleteDataset(dataset.id)
|
||||
Toast.notify({ type: 'success', message: t('dataset.datasetDeleted') })
|
||||
Toast.notify({ type: 'success', message: t('datasetDeleted', { ns: 'dataset' }) })
|
||||
invalidDatasetList()
|
||||
replace('/datasets')
|
||||
}
|
||||
|
|
@ -141,7 +141,7 @@ const DropDown = ({
|
|||
)}
|
||||
{showConfirmDelete && (
|
||||
<Confirm
|
||||
title={t('dataset.deleteDatasetConfirmTitle')}
|
||||
title={t('deleteDatasetConfirmTitle', { ns: 'dataset' })}
|
||||
content={confirmMessage}
|
||||
isShow={showConfirmDelete}
|
||||
onConfirm={onConfirmDelete}
|
||||
|
|
|
|||
|
|
@ -70,10 +70,10 @@ const DatasetInfo: FC<DatasetInfoProps> = ({
|
|||
{dataset.name}
|
||||
</div>
|
||||
<div className="system-2xs-medium-uppercase text-text-tertiary">
|
||||
{isExternalProvider && t('dataset.externalTag')}
|
||||
{isExternalProvider && t('externalTag', { ns: 'dataset' })}
|
||||
{!isExternalProvider && isPipelinePublished && dataset.doc_form && dataset.indexing_technique && (
|
||||
<div className="flex items-center gap-x-2">
|
||||
<span>{t(`dataset.chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}` as any) as string}</span>
|
||||
<span>{t(`chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}`, { ns: 'dataset' })}</span>
|
||||
<span>{formatIndexingTechniqueAndMethod(dataset.indexing_technique, dataset.retrieval_model_dict?.search_method)}</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -26,13 +26,13 @@ const Menu = ({
|
|||
<div className="flex flex-col p-1">
|
||||
<MenuItem
|
||||
Icon={RiEditLine}
|
||||
name={t('common.operation.edit')}
|
||||
name={t('operation.edit', { ns: 'common' })}
|
||||
handleClick={openRenameModal}
|
||||
/>
|
||||
{runtimeMode === 'rag_pipeline' && (
|
||||
<MenuItem
|
||||
Icon={RiFileDownloadLine}
|
||||
name={t('datasetPipeline.operations.exportPipeline')}
|
||||
name={t('operations.exportPipeline', { ns: 'datasetPipeline' })}
|
||||
handleClick={handleExportPipeline}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -43,7 +43,7 @@ const Menu = ({
|
|||
<div className="flex flex-col p-1">
|
||||
<MenuItem
|
||||
Icon={RiDeleteBinLine}
|
||||
name={t('common.operation.delete')}
|
||||
name={t('operation.delete', { ns: 'common' })}
|
||||
handleClick={detectIsUsedByApp}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -113,10 +113,10 @@ const DatasetSidebarDropdown = ({
|
|||
{dataset.name}
|
||||
</div>
|
||||
<div className="system-2xs-medium-uppercase text-text-tertiary">
|
||||
{isExternalProvider && t('dataset.externalTag')}
|
||||
{isExternalProvider && t('externalTag', { ns: 'dataset' })}
|
||||
{!isExternalProvider && dataset.doc_form && dataset.indexing_technique && (
|
||||
<div className="flex items-center gap-x-2">
|
||||
<span>{t(`dataset.chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}` as any) as string}</span>
|
||||
<span>{t(`chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}`, { ns: 'dataset' })}</span>
|
||||
<span>{formatIndexingTechniqueAndMethod(dataset.indexing_technique, dataset.retrieval_model_dict?.search_method)}</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ const TooltipContent = ({
|
|||
|
||||
return (
|
||||
<div className="flex items-center gap-x-1">
|
||||
<span className="system-xs-medium px-0.5 text-text-secondary">{expand ? t('layout.sidebar.collapseSidebar') : t('layout.sidebar.expandSidebar')}</span>
|
||||
<span className="system-xs-medium px-0.5 text-text-secondary">{expand ? t('sidebar.collapseSidebar', { ns: 'layout' }) : t('sidebar.expandSidebar', { ns: 'layout' })}</span>
|
||||
<div className="flex items-center gap-x-0.5">
|
||||
{
|
||||
TOGGLE_SHORTCUT.map(key => (
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@ const EditItem: FC<Props> = ({
|
|||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const avatar = type === EditItemType.Query ? <User className="h-6 w-6" /> : <Robot className="h-6 w-6" />
|
||||
const name = type === EditItemType.Query ? t('appAnnotation.addModal.queryName') : t('appAnnotation.addModal.answerName')
|
||||
const placeholder = type === EditItemType.Query ? t('appAnnotation.addModal.queryPlaceholder') : t('appAnnotation.addModal.answerPlaceholder')
|
||||
const name = type === EditItemType.Query ? t('addModal.queryName', { ns: 'appAnnotation' }) : t('addModal.answerName', { ns: 'appAnnotation' })
|
||||
const placeholder = type === EditItemType.Query ? t('addModal.queryPlaceholder', { ns: 'appAnnotation' }) : t('addModal.answerPlaceholder', { ns: 'appAnnotation' })
|
||||
|
||||
return (
|
||||
<div className="flex" onClick={e => e.stopPropagation()}>
|
||||
|
|
|
|||
|
|
@ -33,10 +33,10 @@ const AddAnnotationModal: FC<Props> = ({
|
|||
|
||||
const isValid = (payload: AnnotationItemBasic) => {
|
||||
if (!payload.question)
|
||||
return t('appAnnotation.errorMessage.queryRequired')
|
||||
return t('errorMessage.queryRequired', { ns: 'appAnnotation' })
|
||||
|
||||
if (!payload.answer)
|
||||
return t('appAnnotation.errorMessage.answerRequired')
|
||||
return t('errorMessage.answerRequired', { ns: 'appAnnotation' })
|
||||
|
||||
return true
|
||||
}
|
||||
|
|
@ -76,7 +76,7 @@ const AddAnnotationModal: FC<Props> = ({
|
|||
isShow={isShow}
|
||||
onHide={onHide}
|
||||
maxWidthClassName="!max-w-[480px]"
|
||||
title={t('appAnnotation.addModal.title') as string}
|
||||
title={t('addModal.title', { ns: 'appAnnotation' }) as string}
|
||||
body={(
|
||||
<div className="space-y-6 p-6 pb-4">
|
||||
<EditItem
|
||||
|
|
@ -104,11 +104,11 @@ const AddAnnotationModal: FC<Props> = ({
|
|||
className="flex items-center space-x-2"
|
||||
>
|
||||
<Checkbox id="create-next-checkbox" checked={isCreateNext} onCheck={() => setIsCreateNext(!isCreateNext)} />
|
||||
<div>{t('appAnnotation.addModal.createNext')}</div>
|
||||
<div>{t('addModal.createNext', { ns: 'appAnnotation' })}</div>
|
||||
</div>
|
||||
<div className="mt-2 flex space-x-2">
|
||||
<Button className="h-7 text-xs" onClick={onHide}>{t('common.operation.cancel')}</Button>
|
||||
<Button className="h-7 text-xs" variant="primary" onClick={handleSave} loading={isSaving} disabled={isAnnotationFull}>{t('common.operation.add')}</Button>
|
||||
<Button className="h-7 text-xs" onClick={onHide}>{t('operation.cancel', { ns: 'common' })}</Button>
|
||||
<Button className="h-7 text-xs" variant="primary" onClick={handleSave} loading={isSaving} disabled={isAnnotationFull}>{t('operation.add', { ns: 'common' })}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import Confirm from '@/app/components/base/confirm'
|
|||
import Divider from '@/app/components/base/divider'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
const i18nPrefix = 'appAnnotation.batchAction'
|
||||
const i18nPrefix = 'batchAction'
|
||||
|
||||
type IBatchActionProps = {
|
||||
className?: string
|
||||
|
|
@ -45,27 +45,27 @@ const BatchAction: FC<IBatchActionProps> = ({
|
|||
<span className="flex h-5 w-5 items-center justify-center rounded-md bg-text-accent px-1 py-0.5 text-xs font-medium text-text-primary-on-surface">
|
||||
{selectedIds.length}
|
||||
</span>
|
||||
<span className="text-[13px] font-semibold leading-[16px] text-text-accent">{t(`${i18nPrefix}.selected`)}</span>
|
||||
<span className="text-[13px] font-semibold leading-[16px] text-text-accent">{t(`${i18nPrefix}.selected`, { ns: 'appAnnotation' })}</span>
|
||||
</div>
|
||||
<Divider type="vertical" className="mx-0.5 h-3.5 bg-divider-regular" />
|
||||
<div className="flex cursor-pointer items-center gap-x-0.5 px-3 py-2" onClick={showDeleteConfirm}>
|
||||
<RiDeleteBinLine className="h-4 w-4 text-components-button-destructive-ghost-text" />
|
||||
<button type="button" className="px-0.5 text-[13px] font-medium leading-[16px] text-components-button-destructive-ghost-text">
|
||||
{t('common.operation.delete')}
|
||||
{t('operation.delete', { ns: 'common' })}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Divider type="vertical" className="mx-0.5 h-3.5 bg-divider-regular" />
|
||||
<button type="button" className="px-3.5 py-2 text-[13px] font-medium leading-[16px] text-components-button-ghost-text" onClick={onCancel}>
|
||||
{t('common.operation.cancel')}
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</button>
|
||||
</div>
|
||||
{
|
||||
isShowDeleteConfirm && (
|
||||
<Confirm
|
||||
isShow
|
||||
title={t('appAnnotation.list.delete.title')}
|
||||
confirmText={t('common.operation.delete')}
|
||||
title={t('list.delete.title', { ns: 'appAnnotation' })}
|
||||
confirmText={t('operation.delete', { ns: 'common' })}
|
||||
onConfirm={handleBatchDelete}
|
||||
onCancel={hideDeleteConfirm}
|
||||
isLoading={isDeleting}
|
||||
|
|
|
|||
|
|
@ -33,36 +33,36 @@ const CSVDownload: FC = () => {
|
|||
|
||||
return (
|
||||
<div className="mt-6">
|
||||
<div className="system-sm-medium text-text-primary">{t('share.generation.csvStructureTitle')}</div>
|
||||
<div className="system-sm-medium text-text-primary">{t('generation.csvStructureTitle', { ns: 'share' })}</div>
|
||||
<div className="mt-2 max-h-[500px] overflow-auto">
|
||||
<table className="w-full table-fixed border-separate border-spacing-0 rounded-lg border border-divider-regular text-xs">
|
||||
<thead className="text-text-tertiary">
|
||||
<tr>
|
||||
<td className="h-9 border-b border-divider-regular pl-3 pr-2">{t('appAnnotation.batchModal.question')}</td>
|
||||
<td className="h-9 border-b border-divider-regular pl-3 pr-2">{t('appAnnotation.batchModal.answer')}</td>
|
||||
<td className="h-9 border-b border-divider-regular pl-3 pr-2">{t('batchModal.question', { ns: 'appAnnotation' })}</td>
|
||||
<td className="h-9 border-b border-divider-regular pl-3 pr-2">{t('batchModal.answer', { ns: 'appAnnotation' })}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-text-secondary">
|
||||
<tr>
|
||||
<td className="h-9 border-b border-divider-subtle pl-3 pr-2 text-[13px]">
|
||||
{t('appAnnotation.batchModal.question')}
|
||||
{t('batchModal.question', { ns: 'appAnnotation' })}
|
||||
{' '}
|
||||
1
|
||||
</td>
|
||||
<td className="h-9 border-b border-divider-subtle pl-3 pr-2 text-[13px]">
|
||||
{t('appAnnotation.batchModal.answer')}
|
||||
{t('batchModal.answer', { ns: 'appAnnotation' })}
|
||||
{' '}
|
||||
1
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="h-9 pl-3 pr-2 text-[13px]">
|
||||
{t('appAnnotation.batchModal.question')}
|
||||
{t('batchModal.question', { ns: 'appAnnotation' })}
|
||||
{' '}
|
||||
2
|
||||
</td>
|
||||
<td className="h-9 pl-3 pr-2 text-[13px]">
|
||||
{t('appAnnotation.batchModal.answer')}
|
||||
{t('batchModal.answer', { ns: 'appAnnotation' })}
|
||||
{' '}
|
||||
2
|
||||
</td>
|
||||
|
|
@ -79,7 +79,7 @@ const CSVDownload: FC = () => {
|
|||
>
|
||||
<div className="system-xs-medium flex h-[18px] items-center space-x-1 text-text-accent">
|
||||
<DownloadIcon className="mr-1 h-3 w-3" />
|
||||
{t('appAnnotation.batchModal.template')}
|
||||
{t('batchModal.template', { ns: 'appAnnotation' })}
|
||||
</div>
|
||||
</CSVDownloader>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ const CSVUploader: FC<Props> = ({
|
|||
return
|
||||
const files = [...e.dataTransfer.files]
|
||||
if (files.length > 1) {
|
||||
notify({ type: 'error', message: t('datasetCreation.stepOne.uploader.validation.count') })
|
||||
notify({ type: 'error', message: t('stepOne.uploader.validation.count', { ns: 'datasetCreation' }) })
|
||||
return
|
||||
}
|
||||
updateFile(files[0])
|
||||
|
|
@ -98,8 +98,8 @@ const CSVUploader: FC<Props> = ({
|
|||
<div className="flex w-full items-center justify-center space-x-2">
|
||||
<CSVIcon className="shrink-0" />
|
||||
<div className="text-text-tertiary">
|
||||
{t('appAnnotation.batchModal.csvUploadTitle')}
|
||||
<span className="cursor-pointer text-text-accent" onClick={selectHandle}>{t('appAnnotation.batchModal.browse')}</span>
|
||||
{t('batchModal.csvUploadTitle', { ns: 'appAnnotation' })}
|
||||
<span className="cursor-pointer text-text-accent" onClick={selectHandle}>{t('batchModal.browse', { ns: 'appAnnotation' })}</span>
|
||||
</div>
|
||||
</div>
|
||||
{dragging && <div ref={dragRef} className="absolute left-0 top-0 h-full w-full" />}
|
||||
|
|
@ -113,7 +113,7 @@ const CSVUploader: FC<Props> = ({
|
|||
<span className="shrink-0 text-text-tertiary">.csv</span>
|
||||
</div>
|
||||
<div className="hidden items-center group-hover:flex">
|
||||
<Button variant="secondary" onClick={selectHandle}>{t('datasetCreation.stepOne.uploader.change')}</Button>
|
||||
<Button variant="secondary" onClick={selectHandle}>{t('stepOne.uploader.change', { ns: 'datasetCreation' })}</Button>
|
||||
<div className="mx-2 h-4 w-px bg-divider-regular" />
|
||||
<div className="cursor-pointer p-2" onClick={removeFile} data-testid="remove-file-button">
|
||||
<RiDeleteBinLine className="h-4 w-4 text-text-tertiary" />
|
||||
|
|
|
|||
|
|
@ -54,15 +54,15 @@ const BatchModal: FC<IBatchModalProps> = ({
|
|||
if (res.job_status === ProcessStatus.WAITING || res.job_status === ProcessStatus.PROCESSING)
|
||||
setTimeout(() => checkProcess(res.job_id), 2500)
|
||||
if (res.job_status === ProcessStatus.ERROR)
|
||||
notify({ type: 'error', message: `${t('appAnnotation.batchModal.runError')}` })
|
||||
notify({ type: 'error', message: `${t('batchModal.runError', { ns: 'appAnnotation' })}` })
|
||||
if (res.job_status === ProcessStatus.COMPLETED) {
|
||||
notify({ type: 'success', message: `${t('appAnnotation.batchModal.completed')}` })
|
||||
notify({ type: 'success', message: `${t('batchModal.completed', { ns: 'appAnnotation' })}` })
|
||||
onAdded()
|
||||
onCancel()
|
||||
}
|
||||
}
|
||||
catch (e: any) {
|
||||
notify({ type: 'error', message: `${t('appAnnotation.batchModal.runError')}${'message' in e ? `: ${e.message}` : ''}` })
|
||||
notify({ type: 'error', message: `${t('batchModal.runError', { ns: 'appAnnotation' })}${'message' in e ? `: ${e.message}` : ''}` })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -78,7 +78,7 @@ const BatchModal: FC<IBatchModalProps> = ({
|
|||
checkProcess(res.job_id)
|
||||
}
|
||||
catch (e: any) {
|
||||
notify({ type: 'error', message: `${t('appAnnotation.batchModal.runError')}${'message' in e ? `: ${e.message}` : ''}` })
|
||||
notify({ type: 'error', message: `${t('batchModal.runError', { ns: 'appAnnotation' })}${'message' in e ? `: ${e.message}` : ''}` })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -90,7 +90,7 @@ const BatchModal: FC<IBatchModalProps> = ({
|
|||
|
||||
return (
|
||||
<Modal isShow={isShow} onClose={noop} className="!max-w-[520px] !rounded-xl px-8 py-6">
|
||||
<div className="system-xl-medium relative pb-1 text-text-primary">{t('appAnnotation.batchModal.title')}</div>
|
||||
<div className="system-xl-medium relative pb-1 text-text-primary">{t('batchModal.title', { ns: 'appAnnotation' })}</div>
|
||||
<div className="absolute right-4 top-4 cursor-pointer p-2" onClick={onCancel}>
|
||||
<RiCloseLine className="h-4 w-4 text-text-tertiary" />
|
||||
</div>
|
||||
|
|
@ -108,7 +108,7 @@ const BatchModal: FC<IBatchModalProps> = ({
|
|||
|
||||
<div className="mt-[28px] flex justify-end pt-6">
|
||||
<Button className="system-sm-medium mr-2 text-text-tertiary" onClick={onCancel}>
|
||||
{t('appAnnotation.batchModal.cancel')}
|
||||
{t('batchModal.cancel', { ns: 'appAnnotation' })}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
|
|
@ -116,7 +116,7 @@ const BatchModal: FC<IBatchModalProps> = ({
|
|||
disabled={isAnnotationFull || !currentCSV}
|
||||
loading={importStatus === ProcessStatus.PROCESSING || importStatus === ProcessStatus.WAITING}
|
||||
>
|
||||
{t('appAnnotation.batchModal.run')}
|
||||
{t('batchModal.run', { ns: 'appAnnotation' })}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
|
|
|||
|
|
@ -4,13 +4,16 @@ import ClearAllAnnotationsConfirmModal from './index'
|
|||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => {
|
||||
t: (key: string, options?: { ns?: string }) => {
|
||||
const translations: Record<string, string> = {
|
||||
'appAnnotation.table.header.clearAllConfirm': 'Clear all annotations?',
|
||||
'common.operation.confirm': 'Confirm',
|
||||
'common.operation.cancel': 'Cancel',
|
||||
'table.header.clearAllConfirm': 'Clear all annotations?',
|
||||
'operation.confirm': 'Confirm',
|
||||
'operation.cancel': 'Cancel',
|
||||
}
|
||||
return translations[key] || key
|
||||
if (translations[key])
|
||||
return translations[key]
|
||||
const prefix = options?.ns ? `${options.ns}.` : ''
|
||||
return `${prefix}${key}`
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ const ClearAllAnnotationsConfirmModal: FC<Props> = ({
|
|||
onCancel={onHide}
|
||||
onConfirm={onConfirm}
|
||||
type="danger"
|
||||
title={t('appAnnotation.table.header.clearAllConfirm')}
|
||||
title={t('table.header.clearAllConfirm', { ns: 'appAnnotation' })}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,9 +43,9 @@ const EditItem: FC<Props> = ({
|
|||
const [newContent, setNewContent] = useState('')
|
||||
const showNewContent = newContent && newContent !== content
|
||||
const avatar = type === EditItemType.Query ? <User className="h-6 w-6" /> : <Robot className="h-6 w-6" />
|
||||
const name = type === EditItemType.Query ? t('appAnnotation.editModal.queryName') : t('appAnnotation.editModal.answerName')
|
||||
const editTitle = type === EditItemType.Query ? t('appAnnotation.editModal.yourQuery') : t('appAnnotation.editModal.yourAnswer')
|
||||
const placeholder = type === EditItemType.Query ? t('appAnnotation.editModal.queryPlaceholder') : t('appAnnotation.editModal.answerPlaceholder')
|
||||
const name = type === EditItemType.Query ? t('editModal.queryName', { ns: 'appAnnotation' }) : t('editModal.answerName', { ns: 'appAnnotation' })
|
||||
const editTitle = type === EditItemType.Query ? t('editModal.yourQuery', { ns: 'appAnnotation' }) : t('editModal.yourAnswer', { ns: 'appAnnotation' })
|
||||
const placeholder = type === EditItemType.Query ? t('editModal.queryPlaceholder', { ns: 'appAnnotation' }) : t('editModal.answerPlaceholder', { ns: 'appAnnotation' })
|
||||
const [isEdit, setIsEdit] = useState(false)
|
||||
|
||||
// Reset newContent when content prop changes
|
||||
|
|
@ -95,7 +95,7 @@ const EditItem: FC<Props> = ({
|
|||
}}
|
||||
>
|
||||
<RiEditLine className="mr-1 h-3.5 w-3.5" />
|
||||
<div>{t('common.operation.edit')}</div>
|
||||
<div>{t('operation.edit', { ns: 'common' })}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -119,7 +119,7 @@ const EditItem: FC<Props> = ({
|
|||
<div className="h-3.5 w-3.5">
|
||||
<RiDeleteBinLine className="h-3.5 w-3.5" />
|
||||
</div>
|
||||
<div>{t('common.operation.delete')}</div>
|
||||
<div>{t('operation.delete', { ns: 'common' })}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -136,8 +136,8 @@ const EditItem: FC<Props> = ({
|
|||
autoFocus
|
||||
/>
|
||||
<div className="mt-2 flex space-x-2">
|
||||
<Button size="small" variant="primary" onClick={handleSave}>{t('common.operation.save')}</Button>
|
||||
<Button size="small" onClick={handleCancel}>{t('common.operation.cancel')}</Button>
|
||||
<Button size="small" variant="primary" onClick={handleSave}>{t('operation.save', { ns: 'common' })}</Button>
|
||||
<Button size="small" onClick={handleCancel}>{t('operation.cancel', { ns: 'common' })}</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -73,12 +73,12 @@ const EditAnnotationModal: FC<Props> = ({
|
|||
}
|
||||
|
||||
Toast.notify({
|
||||
message: t('common.api.actionSuccess') as string,
|
||||
message: t('api.actionSuccess', { ns: 'common' }) as string,
|
||||
type: 'success',
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
const fallbackMessage = t('common.api.actionFailed') as string
|
||||
const fallbackMessage = t('api.actionFailed', { ns: 'common' }) as string
|
||||
const message = error instanceof Error && error.message ? error.message : fallbackMessage
|
||||
Toast.notify({
|
||||
message,
|
||||
|
|
@ -96,7 +96,7 @@ const EditAnnotationModal: FC<Props> = ({
|
|||
isShow={isShow}
|
||||
onHide={onHide}
|
||||
maxWidthClassName="!max-w-[480px]"
|
||||
title={t('appAnnotation.editModal.title') as string}
|
||||
title={t('editModal.title', { ns: 'appAnnotation' }) as string}
|
||||
body={(
|
||||
<div>
|
||||
<div className="space-y-6 p-6 pb-4">
|
||||
|
|
@ -120,7 +120,7 @@ const EditAnnotationModal: FC<Props> = ({
|
|||
setShowModal(false)
|
||||
onHide()
|
||||
}}
|
||||
title={t('appDebug.feature.annotation.removeConfirm')}
|
||||
title={t('feature.annotation.removeConfirm', { ns: 'appDebug' })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -142,13 +142,13 @@ const EditAnnotationModal: FC<Props> = ({
|
|||
onClick={() => setShowModal(true)}
|
||||
>
|
||||
<MessageCheckRemove />
|
||||
<div>{t('appAnnotation.editModal.removeThisCache')}</div>
|
||||
<div>{t('editModal.removeThisCache', { ns: 'appAnnotation' })}</div>
|
||||
</div>
|
||||
{createdAt && (
|
||||
<div>
|
||||
{t('appAnnotation.editModal.createdAt')}
|
||||
{t('editModal.createdAt', { ns: 'appAnnotation' })}
|
||||
|
||||
{formatTime(createdAt, t('appLog.dateTimeFormat') as string)}
|
||||
{formatTime(createdAt, t('dateTimeFormat', { ns: 'appLog' }) as string)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -18,11 +18,11 @@ const EmptyElement: FC = () => {
|
|||
<div className="flex h-full items-center justify-center">
|
||||
<div className="box-border h-fit w-[560px] rounded-2xl bg-background-section-burn px-5 py-4">
|
||||
<span className="system-md-semibold text-text-secondary">
|
||||
{t('appAnnotation.noData.title')}
|
||||
{t('noData.title', { ns: 'appAnnotation' })}
|
||||
<ThreeDotsIcon className="relative -left-1.5 -top-3 inline" />
|
||||
</span>
|
||||
<div className="system-sm-regular mt-2 text-text-tertiary">
|
||||
{t('appAnnotation.noData.description')}
|
||||
{t('noData.description', { ns: 'appAnnotation' })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ const Filter: FC<IFilterProps> = ({
|
|||
showLeftIcon
|
||||
showClearIcon
|
||||
value={queryParams.keyword}
|
||||
placeholder={t('common.operation.search')!}
|
||||
placeholder={t('operation.search', { ns: 'common' })!}
|
||||
onChange={(e) => {
|
||||
setQueryParams({ ...queryParams, keyword: e.target.value })
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -108,12 +108,12 @@ const HeaderOptions: FC<Props> = ({
|
|||
}}
|
||||
>
|
||||
<FilePlus02 className="h-4 w-4 text-text-tertiary" />
|
||||
<span className="system-sm-regular grow text-left text-text-secondary">{t('appAnnotation.table.header.bulkImport')}</span>
|
||||
<span className="system-sm-regular grow text-left text-text-secondary">{t('table.header.bulkImport', { ns: 'appAnnotation' })}</span>
|
||||
</button>
|
||||
<Menu as="div" className="relative h-full w-full">
|
||||
<MenuButton className="mx-1 flex h-9 w-[calc(100%_-_8px)] cursor-pointer items-center space-x-2 rounded-lg px-3 py-2 hover:bg-components-panel-on-panel-item-bg-hover disabled:opacity-50">
|
||||
<FileDownload02 className="h-4 w-4 text-text-tertiary" />
|
||||
<span className="system-sm-regular grow text-left text-text-secondary">{t('appAnnotation.table.header.bulkExport')}</span>
|
||||
<span className="system-sm-regular grow text-left text-text-secondary">{t('table.header.bulkExport', { ns: 'appAnnotation' })}</span>
|
||||
<ChevronRight className="h-[14px] w-[14px] shrink-0 text-text-tertiary" />
|
||||
</MenuButton>
|
||||
<Transition
|
||||
|
|
@ -156,7 +156,7 @@ const HeaderOptions: FC<Props> = ({
|
|||
>
|
||||
<RiDeleteBinLine className="h-4 w-4" />
|
||||
<span className="system-sm-regular grow text-left">
|
||||
{t('appAnnotation.table.header.clearAll')}
|
||||
{t('table.header.clearAll', { ns: 'appAnnotation' })}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -169,7 +169,7 @@ const HeaderOptions: FC<Props> = ({
|
|||
<div className="flex space-x-2">
|
||||
<Button variant="primary" onClick={() => setShowAddModal(true)}>
|
||||
<RiAddLine className="mr-0.5 h-4 w-4" />
|
||||
<div>{t('appAnnotation.table.header.addAnnotation')}</div>
|
||||
<div>{t('table.header.addAnnotation', { ns: 'appAnnotation' })}</div>
|
||||
</Button>
|
||||
<CustomPopover
|
||||
htmlContent={<Operations />}
|
||||
|
|
|
|||
|
|
@ -98,14 +98,14 @@ const Annotation: FC<Props> = (props) => {
|
|||
|
||||
const handleAdd = async (payload: AnnotationItemBasic) => {
|
||||
await addAnnotation(appDetail.id, payload)
|
||||
Toast.notify({ message: t('common.api.actionSuccess'), type: 'success' })
|
||||
Toast.notify({ message: t('api.actionSuccess', { ns: 'common' }), type: 'success' })
|
||||
fetchList()
|
||||
setControlUpdateList(Date.now())
|
||||
}
|
||||
|
||||
const handleRemove = async (id: string) => {
|
||||
await delAnnotation(appDetail.id, id)
|
||||
Toast.notify({ message: t('common.api.actionSuccess'), type: 'success' })
|
||||
Toast.notify({ message: t('api.actionSuccess', { ns: 'common' }), type: 'success' })
|
||||
fetchList()
|
||||
setControlUpdateList(Date.now())
|
||||
}
|
||||
|
|
@ -113,13 +113,13 @@ const Annotation: FC<Props> = (props) => {
|
|||
const handleBatchDelete = async () => {
|
||||
try {
|
||||
await delAnnotations(appDetail.id, selectedIds)
|
||||
Toast.notify({ message: t('common.api.actionSuccess'), type: 'success' })
|
||||
Toast.notify({ message: t('api.actionSuccess', { ns: 'common' }), type: 'success' })
|
||||
fetchList()
|
||||
setControlUpdateList(Date.now())
|
||||
setSelectedIds([])
|
||||
}
|
||||
catch (e: any) {
|
||||
Toast.notify({ type: 'error', message: e.message || t('common.api.actionFailed') })
|
||||
Toast.notify({ type: 'error', message: e.message || t('api.actionFailed', { ns: 'common' }) })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -132,7 +132,7 @@ const Annotation: FC<Props> = (props) => {
|
|||
if (!currItem)
|
||||
return
|
||||
await editAnnotation(appDetail.id, currItem.id, { question, answer })
|
||||
Toast.notify({ message: t('common.api.actionSuccess'), type: 'success' })
|
||||
Toast.notify({ message: t('api.actionSuccess', { ns: 'common' }), type: 'success' })
|
||||
fetchList()
|
||||
setControlUpdateList(Date.now())
|
||||
}
|
||||
|
|
@ -144,7 +144,7 @@ const Annotation: FC<Props> = (props) => {
|
|||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<p className="system-sm-regular text-text-tertiary">{t('appLog.description')}</p>
|
||||
<p className="system-sm-regular text-text-tertiary">{t('description', { ns: 'appLog' })}</p>
|
||||
<div className="relative flex h-full flex-1 flex-col py-4">
|
||||
<Filter appId={appDetail.id} queryParams={queryParams} setQueryParams={setQueryParams}>
|
||||
<div className="flex items-center space-x-2">
|
||||
|
|
@ -152,7 +152,7 @@ const Annotation: FC<Props> = (props) => {
|
|||
<>
|
||||
<div className={cn(!annotationConfig?.enabled && 'pr-2', 'flex h-7 items-center space-x-1 rounded-lg border border-components-panel-border bg-components-panel-bg-blur pl-2')}>
|
||||
<MessageFast className="h-4 w-4 text-util-colors-indigo-indigo-600" />
|
||||
<div className="system-sm-medium text-text-primary">{t('appAnnotation.name')}</div>
|
||||
<div className="system-sm-medium text-text-primary">{t('name', { ns: 'appAnnotation' })}</div>
|
||||
<Switch
|
||||
key={controlRefreshSwitch}
|
||||
defaultValue={annotationConfig?.enabled}
|
||||
|
|
@ -171,7 +171,7 @@ const Annotation: FC<Props> = (props) => {
|
|||
await ensureJobCompleted(jobId, AnnotationEnableStatus.disable)
|
||||
await fetchAnnotationConfig()
|
||||
Toast.notify({
|
||||
message: t('common.api.actionSuccess'),
|
||||
message: t('api.actionSuccess', { ns: 'common' }),
|
||||
type: 'success',
|
||||
})
|
||||
}
|
||||
|
|
@ -264,7 +264,7 @@ const Annotation: FC<Props> = (props) => {
|
|||
|
||||
await fetchAnnotationConfig()
|
||||
Toast.notify({
|
||||
message: t('common.api.actionSuccess'),
|
||||
message: t('api.actionSuccess', { ns: 'common' }),
|
||||
type: 'success',
|
||||
})
|
||||
setIsShowEdit(false)
|
||||
|
|
|
|||
|
|
@ -68,11 +68,11 @@ const List: FC<Props> = ({
|
|||
onCheck={handleSelectAll}
|
||||
/>
|
||||
</td>
|
||||
<td className="w-5 whitespace-nowrap bg-background-section-burn pl-2 pr-1">{t('appAnnotation.table.header.question')}</td>
|
||||
<td className="whitespace-nowrap bg-background-section-burn py-1.5 pl-3">{t('appAnnotation.table.header.answer')}</td>
|
||||
<td className="whitespace-nowrap bg-background-section-burn py-1.5 pl-3">{t('appAnnotation.table.header.createdAt')}</td>
|
||||
<td className="whitespace-nowrap bg-background-section-burn py-1.5 pl-3">{t('appAnnotation.table.header.hits')}</td>
|
||||
<td className="w-[96px] whitespace-nowrap rounded-r-lg bg-background-section-burn py-1.5 pl-3">{t('appAnnotation.table.header.actions')}</td>
|
||||
<td className="w-5 whitespace-nowrap bg-background-section-burn pl-2 pr-1">{t('table.header.question', { ns: 'appAnnotation' })}</td>
|
||||
<td className="whitespace-nowrap bg-background-section-burn py-1.5 pl-3">{t('table.header.answer', { ns: 'appAnnotation' })}</td>
|
||||
<td className="whitespace-nowrap bg-background-section-burn py-1.5 pl-3">{t('table.header.createdAt', { ns: 'appAnnotation' })}</td>
|
||||
<td className="whitespace-nowrap bg-background-section-burn py-1.5 pl-3">{t('table.header.hits', { ns: 'appAnnotation' })}</td>
|
||||
<td className="w-[96px] whitespace-nowrap rounded-r-lg bg-background-section-burn py-1.5 pl-3">{t('table.header.actions', { ns: 'appAnnotation' })}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="system-sm-regular text-text-secondary">
|
||||
|
|
@ -110,7 +110,7 @@ const List: FC<Props> = ({
|
|||
>
|
||||
{item.answer}
|
||||
</td>
|
||||
<td className="p-3 pr-2">{formatTime(item.created_at, t('appLog.dateTimeFormat') as string)}</td>
|
||||
<td className="p-3 pr-2">{formatTime(item.created_at, t('dateTimeFormat', { ns: 'appLog' }) as string)}</td>
|
||||
<td className="p-3 pr-2">{item.hit_count}</td>
|
||||
<td className="w-[96px] p-3 pr-2" onClick={e => e.stopPropagation()}>
|
||||
{/* Actions */}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue