Merge branch 'main' into jzh

This commit is contained in:
JzoNg
2026-04-07 14:41:42 +08:00
54 changed files with 387 additions and 342 deletions

View File

@@ -2,6 +2,7 @@ import csv
import io
from collections.abc import Callable
from functools import wraps
from typing import cast
from flask import request
from flask_restx import Resource
@@ -17,7 +18,7 @@ from core.db.session_factory import session_factory
from extensions.ext_database import db
from libs.token import extract_access_token
from models.model import App, ExporleBanner, InstalledApp, RecommendedApp, TrialApp
from services.billing_service import BillingService
from services.billing_service import BillingService, LangContentDict
DEFAULT_REF_TEMPLATE_SWAGGER_2_0 = "#/definitions/{model}"
@@ -328,7 +329,7 @@ class UpsertNotificationApi(Resource):
def post(self):
payload = UpsertNotificationPayload.model_validate(console_ns.payload)
result = BillingService.upsert_notification(
contents=[c.model_dump() for c in payload.contents],
contents=[cast(LangContentDict, c.model_dump()) for c in payload.contents],
frequency=payload.frequency,
status=payload.status,
notification_id=payload.notification_id,

View File

@@ -5,6 +5,10 @@ from configs import dify_config
from constants import DEFAULT_FILE_NUMBER_LIMITS
class FeatureToggleDict(TypedDict):
enabled: bool
class SystemParametersDict(TypedDict):
image_file_size_limit: int
video_file_size_limit: int
@@ -16,12 +20,12 @@ class SystemParametersDict(TypedDict):
class AppParametersDict(TypedDict):
opening_statement: str | None
suggested_questions: list[str]
suggested_questions_after_answer: dict[str, Any]
speech_to_text: dict[str, Any]
text_to_speech: dict[str, Any]
retriever_resource: dict[str, Any]
annotation_reply: dict[str, Any]
more_like_this: dict[str, Any]
suggested_questions_after_answer: FeatureToggleDict
speech_to_text: FeatureToggleDict
text_to_speech: FeatureToggleDict
retriever_resource: FeatureToggleDict
annotation_reply: FeatureToggleDict
more_like_this: FeatureToggleDict
user_input_form: list[dict[str, Any]]
sensitive_word_avoidance: dict[str, Any]
file_upload: dict[str, Any]

View File

@@ -107,13 +107,13 @@ class AppGenerateResponseConverter(ABC):
return metadata
@classmethod
def _error_to_stream_response(cls, e: Exception):
def _error_to_stream_response(cls, e: Exception) -> dict[str, Any]:
"""
Error to stream response.
:param e: exception
:return:
"""
error_responses = {
error_responses: dict[type[Exception], dict[str, Any]] = {
ValueError: {"code": "invalid_param", "status": 400},
ProviderTokenNotInitError: {"code": "provider_not_initialize", "status": 400},
QuotaExceededError: {
@@ -127,7 +127,7 @@ class AppGenerateResponseConverter(ABC):
}
# Determine the response based on the type of exception
data = None
data: dict[str, Any] | None = None
for k, v in error_responses.items():
if isinstance(e, k):
data = v

View File

@@ -1,6 +1,6 @@
import json
from collections.abc import Mapping
from typing import Any
from typing import Any, TypedDict
from graphon.entities import WorkflowNodeExecution
from graphon.enums import WorkflowNodeExecutionStatus
@@ -56,10 +56,22 @@ def create_links_from_trace_id(trace_id: str | None) -> list[Link]:
return links
def extract_retrieval_documents(documents: list[Document]) -> list[dict[str, Any]]:
documents_data = []
class RetrievalDocumentMetadataDict(TypedDict):
dataset_id: Any
doc_id: Any
document_id: Any
class RetrievalDocumentDict(TypedDict):
content: str
metadata: RetrievalDocumentMetadataDict
score: Any
def extract_retrieval_documents(documents: list[Document]) -> list[RetrievalDocumentDict]:
documents_data: list[RetrievalDocumentDict] = []
for document in documents:
document_data = {
document_data: RetrievalDocumentDict = {
"content": document.page_content,
"metadata": {
"dataset_id": document.metadata.get("dataset_id"),
@@ -83,7 +95,7 @@ def create_common_span_attributes(
framework: str = DEFAULT_FRAMEWORK_NAME,
inputs: str = "",
outputs: str = "",
) -> dict[str, Any]:
) -> dict[str, str]:
return {
GEN_AI_SESSION_ID: session_id,
GEN_AI_USER_ID: user_id,

View File

@@ -240,7 +240,7 @@ class RetrievalService:
@classmethod
def _get_dataset(cls, dataset_id: str) -> Dataset | None:
with Session(db.engine) as session:
return session.query(Dataset).where(Dataset.id == dataset_id).first()
return session.scalar(select(Dataset).where(Dataset.id == dataset_id).limit(1))
@classmethod
def keyword_search(
@@ -573,15 +573,13 @@ class RetrievalService:
# Batch query summaries for segments retrieved via summary (only enabled summaries)
if summary_segment_ids:
summaries = (
session.query(DocumentSegmentSummary)
.filter(
summaries = session.scalars(
select(DocumentSegmentSummary).where(
DocumentSegmentSummary.chunk_id.in_(list(summary_segment_ids)),
DocumentSegmentSummary.status == "completed",
DocumentSegmentSummary.enabled == True, # Only retrieve enabled summaries
DocumentSegmentSummary.enabled.is_(True), # Only retrieve enabled summaries
)
.all()
)
).all()
for summary in summaries:
if summary.summary_content:
segment_summary_map[summary.chunk_id] = summary.summary_content
@@ -851,12 +849,12 @@ class RetrievalService:
def get_segment_attachment_info(
cls, dataset_id: str, tenant_id: str, attachment_id: str, session: Session
) -> SegmentAttachmentResult | None:
upload_file = session.query(UploadFile).where(UploadFile.id == attachment_id).first()
upload_file = session.scalar(select(UploadFile).where(UploadFile.id == attachment_id).limit(1))
if upload_file:
attachment_binding = (
session.query(SegmentAttachmentBinding)
attachment_binding = session.scalar(
select(SegmentAttachmentBinding)
.where(SegmentAttachmentBinding.attachment_id == upload_file.id)
.first()
.limit(1)
)
if attachment_binding:
attachment_info: AttachmentInfoDict = {
@@ -875,14 +873,12 @@ class RetrievalService:
cls, attachment_ids: list[str], session: Session
) -> list[SegmentAttachmentInfoResult]:
attachment_infos: list[SegmentAttachmentInfoResult] = []
upload_files = session.query(UploadFile).where(UploadFile.id.in_(attachment_ids)).all()
upload_files = session.scalars(select(UploadFile).where(UploadFile.id.in_(attachment_ids))).all()
if upload_files:
upload_file_ids = [upload_file.id for upload_file in upload_files]
attachment_bindings = (
session.query(SegmentAttachmentBinding)
.where(SegmentAttachmentBinding.attachment_id.in_(upload_file_ids))
.all()
)
attachment_bindings = session.scalars(
select(SegmentAttachmentBinding).where(SegmentAttachmentBinding.attachment_id.in_(upload_file_ids))
).all()
attachment_binding_map = {binding.attachment_id: binding for binding in attachment_bindings}
if attachment_bindings:

View File

@@ -37,6 +37,44 @@ class KnowledgeRateLimitDict(TypedDict):
subscription_plan: str
class TenantFeaturePlanUsageDict(TypedDict):
result: str
history_id: str
class LangContentDict(TypedDict):
lang: str
title: str
subtitle: str
body: str
title_pic_url: str
class NotificationDict(TypedDict):
notification_id: str
contents: dict[str, LangContentDict]
frequency: Literal["once", "every_page_load"]
class AccountNotificationDict(TypedDict, total=False):
should_show: bool
notification: NotificationDict
shouldShow: bool
notifications: list[dict]
class UpsertNotificationDict(TypedDict):
notification_id: str
class BatchAddNotificationAccountsDict(TypedDict):
count: int
class DismissNotificationDict(TypedDict):
success: bool
class BillingService:
base_url = os.environ.get("BILLING_API_URL", "BILLING_API_URL")
secret_key = os.environ.get("BILLING_API_SECRET_KEY", "BILLING_API_SECRET_KEY")
@@ -94,7 +132,9 @@ class BillingService:
return cls._send_request("GET", "/invoices", params=params)
@classmethod
def update_tenant_feature_plan_usage(cls, tenant_id: str, feature_key: str, delta: int) -> dict:
def update_tenant_feature_plan_usage(
cls, tenant_id: str, feature_key: str, delta: int
) -> TenantFeaturePlanUsageDict:
"""
Update tenant feature plan usage.
@@ -114,7 +154,7 @@ class BillingService:
)
@classmethod
def refund_tenant_feature_plan_usage(cls, history_id: str) -> dict:
def refund_tenant_feature_plan_usage(cls, history_id: str) -> TenantFeaturePlanUsageDict:
"""
Refund a previous usage charge.
@@ -410,7 +450,7 @@ class BillingService:
return tenant_whitelist
@classmethod
def get_account_notification(cls, account_id: str) -> dict:
def get_account_notification(cls, account_id: str) -> AccountNotificationDict:
"""Return the active in-product notification for account_id, if any.
Calling this endpoint also marks the notification as seen; subsequent
@@ -434,13 +474,13 @@ class BillingService:
@classmethod
def upsert_notification(
cls,
contents: list[dict],
contents: list[LangContentDict],
frequency: str = "once",
status: str = "active",
notification_id: str | None = None,
start_time: str | None = None,
end_time: str | None = None,
) -> dict:
) -> UpsertNotificationDict:
"""Create or update a notification.
contents: list of {"lang": str, "title": str, "subtitle": str, "body": str, "title_pic_url": str}
@@ -461,7 +501,9 @@ class BillingService:
return cls._send_request("POST", "/notifications", json=payload)
@classmethod
def batch_add_notification_accounts(cls, notification_id: str, account_ids: list[str]) -> dict:
def batch_add_notification_accounts(
cls, notification_id: str, account_ids: list[str]
) -> BatchAddNotificationAccountsDict:
"""Register target account IDs for a notification (max 1000 per call).
Returns {"count": int}.
@@ -473,7 +515,7 @@ class BillingService:
)
@classmethod
def dismiss_notification(cls, notification_id: str, account_id: str) -> dict:
def dismiss_notification(cls, notification_id: str, account_id: str) -> DismissNotificationDict:
"""Mark a notification as dismissed for an account.
Returns {"success": bool}.

View File

@@ -1,6 +1,6 @@
import json
import logging
from typing import Any, Union
from typing import Any, TypedDict, Union
from graphon.model_runtime.entities.model_entities import ModelType
from graphon.model_runtime.entities.provider_entities import (
@@ -25,6 +25,23 @@ from models.provider import LoadBalancingModelConfig, ProviderCredential, Provid
logger = logging.getLogger(__name__)
class LoadBalancingConfigDetailDict(TypedDict):
id: str
name: str
credentials: dict[str, Any]
enabled: bool
class LoadBalancingConfigSummaryDict(TypedDict):
id: str
name: str
credentials: dict[str, Any]
credential_id: str | None
enabled: bool
in_cooldown: bool
ttl: int
class ModelLoadBalancingService:
@staticmethod
def _get_provider_manager(tenant_id: str) -> ProviderManager:
@@ -74,7 +91,7 @@ class ModelLoadBalancingService:
def get_load_balancing_configs(
self, tenant_id: str, provider: str, model: str, model_type: str, config_from: str = ""
) -> tuple[bool, list[dict]]:
) -> tuple[bool, list[LoadBalancingConfigSummaryDict]]:
"""
Get load balancing configurations.
:param tenant_id: workspace id
@@ -156,7 +173,7 @@ class ModelLoadBalancingService:
decoding_rsa_key, decoding_cipher_rsa = encrypter.get_decrypt_decoding(tenant_id)
# fetch status and ttl for each config
datas = []
datas: list[LoadBalancingConfigSummaryDict] = []
for load_balancing_config in load_balancing_configs:
in_cooldown, ttl = LBModelManager.get_config_in_cooldown_and_ttl(
tenant_id=tenant_id,
@@ -214,7 +231,7 @@ class ModelLoadBalancingService:
def get_load_balancing_config(
self, tenant_id: str, provider: str, model: str, model_type: str, config_id: str
) -> dict | None:
) -> LoadBalancingConfigDetailDict | None:
"""
Get load balancing configuration.
:param tenant_id: workspace id
@@ -267,12 +284,13 @@ class ModelLoadBalancingService:
credentials=credentials, credential_form_schemas=credential_schemas.credential_form_schemas
)
return {
result: LoadBalancingConfigDetailDict = {
"id": load_balancing_model_config.id,
"name": load_balancing_model_config.name,
"credentials": credentials,
"enabled": load_balancing_model_config.enabled,
}
return result
def _init_inherit_config(
self, tenant_id: str, provider: str, model: str, model_type: ModelType

View File

@@ -3,7 +3,7 @@ import logging
import mimetypes
import secrets
from collections.abc import Callable, Mapping, Sequence
from typing import Any, TypedDict
from typing import Any, NotRequired, TypedDict
import orjson
from flask import request
@@ -58,6 +58,18 @@ class RawWebhookDataDict(TypedDict):
files: dict[str, Any]
class ValidationResultDict(TypedDict):
valid: bool
error: NotRequired[str]
class WorkflowInputsDict(TypedDict):
webhook_data: RawWebhookDataDict
webhook_headers: dict[str, str]
webhook_query_params: dict[str, str]
webhook_body: dict[str, Any]
class WebhookService:
"""Service for handling webhook operations."""
@@ -173,7 +185,7 @@ class WebhookService:
node_data = WebhookData.model_validate(node_config["data"], from_attributes=True)
validation_result = cls._validate_http_metadata(raw_data, node_data)
if not validation_result["valid"]:
raise ValueError(validation_result["error"])
raise ValueError(validation_result.get("error", "Validation failed"))
# Process and validate data according to configuration
processed_data = cls._process_and_validate_data(raw_data, node_data)
@@ -672,7 +684,7 @@ class WebhookService:
raise ValueError(f"Required header missing: {header_name}")
@classmethod
def _validate_http_metadata(cls, webhook_data: RawWebhookDataDict, node_data: WebhookData) -> dict[str, Any]:
def _validate_http_metadata(cls, webhook_data: RawWebhookDataDict, node_data: WebhookData) -> ValidationResultDict:
"""Validate HTTP method and content-type.
Args:
@@ -716,7 +728,7 @@ class WebhookService:
return content_type.split(";")[0].strip()
@classmethod
def _validation_error(cls, error_message: str) -> dict[str, Any]:
def _validation_error(cls, error_message: str) -> ValidationResultDict:
"""Create a standard validation error response.
Args:
@@ -737,7 +749,7 @@ class WebhookService:
return False
@classmethod
def build_workflow_inputs(cls, webhook_data: RawWebhookDataDict) -> dict[str, Any]:
def build_workflow_inputs(cls, webhook_data: RawWebhookDataDict) -> WorkflowInputsDict:
"""Construct workflow inputs payload from webhook data.
Args:

View File

@@ -635,7 +635,7 @@ class WorkflowService:
# If we can't determine the status, assume load balancing is not enabled
return False
def _get_load_balancing_configs(self, tenant_id: str, provider: str, model_name: str) -> list[dict]:
def _get_load_balancing_configs(self, tenant_id: str, provider: str, model_name: str) -> list[dict[str, Any]]:
"""
Get all load balancing configurations for a model.
@@ -659,7 +659,7 @@ class WorkflowService:
_, custom_configs = model_load_balancing_service.get_load_balancing_configs(
tenant_id=tenant_id, provider=provider, model=model_name, model_type="llm", config_from="custom-model"
)
all_configs = configs + custom_configs
all_configs = cast(list[dict[str, Any]], configs) + cast(list[dict[str, Any]], custom_configs)
return [config for config in all_configs if config.get("credential_id")]

View File

@@ -26,7 +26,7 @@ def clean_notion_document_task(document_ids: list[str], dataset_id: str):
total_index_node_ids = []
with session_factory.create_session() as session:
dataset = session.query(Dataset).where(Dataset.id == dataset_id).first()
dataset = session.scalar(select(Dataset).where(Dataset.id == dataset_id).limit(1))
if not dataset:
raise Exception("Document has no dataset")
@@ -41,7 +41,7 @@ def clean_notion_document_task(document_ids: list[str], dataset_id: str):
total_index_node_ids.extend([segment.index_node_id for segment in segments])
with session_factory.create_session() as session:
dataset = session.query(Dataset).where(Dataset.id == dataset_id).first()
dataset = session.scalar(select(Dataset).where(Dataset.id == dataset_id).limit(1))
if dataset:
index_processor.clean(
dataset, total_index_node_ids, with_keywords=True, delete_child_chunks=True, delete_summaries=True

View File

@@ -28,7 +28,9 @@ def document_indexing_update_task(dataset_id: str, document_id: str):
start_at = time.perf_counter()
with session_factory.create_session() as session, session.begin():
document = session.query(Document).where(Document.id == document_id, Document.dataset_id == dataset_id).first()
document = session.scalar(
select(Document).where(Document.id == document_id, Document.dataset_id == dataset_id).limit(1)
)
if not document:
logger.info(click.style(f"Document not found: {document_id}", fg="red"))
@@ -37,7 +39,7 @@ def document_indexing_update_task(dataset_id: str, document_id: str):
document.indexing_status = IndexingStatus.PARSING
document.processing_started_at = naive_utc_now()
dataset = session.query(Dataset).where(Dataset.id == dataset_id).first()
dataset = session.scalar(select(Dataset).where(Dataset.id == dataset_id).limit(1))
if not dataset:
return

View File

@@ -5,6 +5,7 @@ import time
import click
from celery import shared_task
from sqlalchemy import select, update
from core.db.session_factory import session_factory
from core.rag.index_processor.constant.index_type import IndexTechniqueType
@@ -39,12 +40,12 @@ def generate_summary_index_task(dataset_id: str, document_id: str, segment_ids:
try:
with session_factory.create_session() as session:
dataset = session.query(Dataset).where(Dataset.id == dataset_id).first()
dataset = session.scalar(select(Dataset).where(Dataset.id == dataset_id).limit(1))
if not dataset:
logger.error(click.style(f"Dataset not found: {dataset_id}", fg="red"))
return
document = session.query(DatasetDocument).where(DatasetDocument.id == document_id).first()
document = session.scalar(select(DatasetDocument).where(DatasetDocument.id == document_id).limit(1))
if not document:
logger.error(click.style(f"Document not found: {document_id}", fg="red"))
return
@@ -108,13 +109,12 @@ def generate_summary_index_task(dataset_id: str, document_id: str, segment_ids:
if segment_ids:
error_message = f"Summary generation failed: {str(e)}"
with session_factory.create_session() as session:
session.query(DocumentSegment).filter(
DocumentSegment.id.in_(segment_ids),
DocumentSegment.dataset_id == dataset_id,
).update(
{
DocumentSegment.error: error_message,
},
synchronize_session=False,
session.execute(
update(DocumentSegment)
.where(
DocumentSegment.id.in_(segment_ids),
DocumentSegment.dataset_id == dataset_id,
)
.values(error=error_message)
)
session.commit()

View File

@@ -10,6 +10,7 @@ from typing import Any
import click
from celery import shared_task # type: ignore
from flask import current_app, g
from sqlalchemy import select
from sqlalchemy.orm import Session, sessionmaker
from configs import dify_config
@@ -118,20 +119,20 @@ def run_single_rag_pipeline_task(rag_pipeline_invoke_entity: Mapping[str, Any],
with Session(db.engine, expire_on_commit=False) as session:
# Load required entities
account = session.query(Account).where(Account.id == user_id).first()
account = session.scalar(select(Account).where(Account.id == user_id).limit(1))
if not account:
raise ValueError(f"Account {user_id} not found")
tenant = session.query(Tenant).where(Tenant.id == tenant_id).first()
tenant = session.scalar(select(Tenant).where(Tenant.id == tenant_id).limit(1))
if not tenant:
raise ValueError(f"Tenant {tenant_id} not found")
account.current_tenant = tenant
pipeline = session.query(Pipeline).where(Pipeline.id == pipeline_id).first()
pipeline = session.scalar(select(Pipeline).where(Pipeline.id == pipeline_id).limit(1))
if not pipeline:
raise ValueError(f"Pipeline {pipeline_id} not found")
workflow = session.query(Workflow).where(Workflow.id == pipeline.workflow_id).first()
workflow = session.scalar(select(Workflow).where(Workflow.id == pipeline.workflow_id).limit(1))
if not workflow:
raise ValueError(f"Workflow {pipeline.workflow_id} not found")

View File

@@ -11,6 +11,7 @@ from typing import Any
import click
from celery import group, shared_task
from flask import current_app, g
from sqlalchemy import select
from sqlalchemy.orm import Session, sessionmaker
from configs import dify_config
@@ -132,20 +133,20 @@ def run_single_rag_pipeline_task(rag_pipeline_invoke_entity: Mapping[str, Any],
with Session(db.engine) as session:
# Load required entities
account = session.query(Account).where(Account.id == user_id).first()
account = session.scalar(select(Account).where(Account.id == user_id).limit(1))
if not account:
raise ValueError(f"Account {user_id} not found")
tenant = session.query(Tenant).where(Tenant.id == tenant_id).first()
tenant = session.scalar(select(Tenant).where(Tenant.id == tenant_id).limit(1))
if not tenant:
raise ValueError(f"Tenant {tenant_id} not found")
account.current_tenant = tenant
pipeline = session.query(Pipeline).where(Pipeline.id == pipeline_id).first()
pipeline = session.scalar(select(Pipeline).where(Pipeline.id == pipeline_id).limit(1))
if not pipeline:
raise ValueError(f"Pipeline {pipeline_id} not found")
workflow = session.query(Workflow).where(Workflow.id == pipeline.workflow_id).first()
workflow = session.scalar(select(Workflow).where(Workflow.id == pipeline.workflow_id).limit(1))
if not workflow:
raise ValueError(f"Workflow {pipeline.workflow_id} not found")

View File

@@ -3,7 +3,7 @@ import time
import click
from celery import shared_task
from sqlalchemy import select
from sqlalchemy import select, update
from core.db.session_factory import session_factory
from core.rag.index_processor.index_processor_factory import IndexProcessorFactory
@@ -26,7 +26,7 @@ def remove_document_from_index_task(document_id: str):
start_at = time.perf_counter()
with session_factory.create_session() as session:
document = session.query(Document).where(Document.id == document_id).first()
document = session.scalar(select(Document).where(Document.id == document_id).limit(1))
if not document:
logger.info(click.style(f"Document not found: {document_id}", fg="red"))
return
@@ -68,13 +68,15 @@ def remove_document_from_index_task(document_id: str):
except Exception:
logger.exception("clean dataset %s from index failed", dataset.id)
# update segment to disable
session.query(DocumentSegment).where(DocumentSegment.document_id == document.id).update(
{
DocumentSegment.enabled: False,
DocumentSegment.disabled_at: naive_utc_now(),
DocumentSegment.disabled_by: document.disabled_by,
DocumentSegment.updated_at: naive_utc_now(),
}
session.execute(
update(DocumentSegment)
.where(DocumentSegment.document_id == document.id)
.values(
enabled=False,
disabled_at=naive_utc_now(),
disabled_by=document.disabled_by,
updated_at=naive_utc_now(),
)
)
session.commit()

View File

@@ -32,15 +32,15 @@ def retry_document_indexing_task(dataset_id: str, document_ids: list[str], user_
start_at = time.perf_counter()
with session_factory.create_session() as session:
try:
dataset = session.query(Dataset).where(Dataset.id == dataset_id).first()
dataset = session.scalar(select(Dataset).where(Dataset.id == dataset_id).limit(1))
if not dataset:
logger.info(click.style(f"Dataset not found: {dataset_id}", fg="red"))
return
user = session.query(Account).where(Account.id == user_id).first()
user = session.scalar(select(Account).where(Account.id == user_id).limit(1))
if not user:
logger.info(click.style(f"User not found: {user_id}", fg="red"))
return
tenant = session.query(Tenant).where(Tenant.id == dataset.tenant_id).first()
tenant = session.scalar(select(Tenant).where(Tenant.id == dataset.tenant_id).limit(1))
if not tenant:
raise ValueError("Tenant not found")
user.current_tenant = tenant
@@ -58,10 +58,8 @@ def retry_document_indexing_task(dataset_id: str, document_ids: list[str], user_
"your subscription."
)
except Exception as e:
document = (
session.query(Document)
.where(Document.id == document_id, Document.dataset_id == dataset_id)
.first()
document = session.scalar(
select(Document).where(Document.id == document_id, Document.dataset_id == dataset_id).limit(1)
)
if document:
document.indexing_status = IndexingStatus.ERROR
@@ -73,8 +71,8 @@ def retry_document_indexing_task(dataset_id: str, document_ids: list[str], user_
return
logger.info(click.style(f"Start retry document: {document_id}", fg="green"))
document = (
session.query(Document).where(Document.id == document_id, Document.dataset_id == dataset_id).first()
document = session.scalar(
select(Document).where(Document.id == document_id, Document.dataset_id == dataset_id).limit(1)
)
if not document:
logger.info(click.style(f"Document not found: {document_id}", fg="yellow"))

View File

@@ -119,6 +119,14 @@ class _FakeSummaryQuery:
return self._summaries
class _FakeScalarsResult:
def __init__(self, data: list) -> None:
self._data = data
def all(self) -> list:
return self._data
class _FakeSession:
def __init__(self, execute_payloads: list[list], summaries: list) -> None:
self._payloads = list(execute_payloads)
@@ -128,8 +136,8 @@ class _FakeSession:
data = self._payloads.pop(0) if self._payloads else []
return _FakeExecuteResult(data)
def query(self, model):
return _FakeSummaryQuery(self._summaries)
def scalars(self, stmt):
return _FakeScalarsResult(self._summaries)
class _FakeSessionContext:
@@ -265,14 +273,14 @@ class TestRetrievalServiceInternals:
def test_get_dataset_queries_by_id(self, mock_session_class):
expected_dataset = Mock(spec=Dataset)
mock_session = Mock()
mock_session.query.return_value.where.return_value.first.return_value = expected_dataset
mock_session.scalar.return_value = expected_dataset
mock_session_class.return_value.__enter__.return_value = mock_session
with patch.object(retrieval_service_module, "db", SimpleNamespace(engine=Mock())):
result = RetrievalService._get_dataset("dataset-123")
assert result == expected_dataset
mock_session.query.assert_called_once()
mock_session.scalar.assert_called_once()
@patch("core.rag.datasource.retrieval_service.Keyword")
@patch("core.rag.datasource.retrieval_service.RetrievalService._get_dataset")
@@ -1046,12 +1054,8 @@ class TestRetrievalServiceInternals:
size=42,
)
binding = SimpleNamespace(segment_id="segment-1", attachment_id="upload-1")
upload_query = Mock()
upload_query.where.return_value.first.return_value = upload_file
binding_query = Mock()
binding_query.where.return_value.first.return_value = binding
session = Mock()
session.query.side_effect = [upload_query, binding_query]
session.scalar.side_effect = [upload_file, binding]
result = RetrievalService.get_segment_attachment_info("dataset-id", "tenant-id", "upload-1", session)
@@ -1076,32 +1080,26 @@ class TestRetrievalServiceInternals:
mime_type="image/png",
size=42,
)
upload_query = Mock()
upload_query.where.return_value.first.return_value = upload_file
binding_query = Mock()
binding_query.where.return_value.first.return_value = None
session = Mock()
session.query.side_effect = [upload_query, binding_query]
session.scalar.side_effect = [upload_file, None]
result = RetrievalService.get_segment_attachment_info("dataset-id", "tenant-id", "upload-1", session)
assert result is None
def test_get_segment_attachment_info_returns_none_when_upload_file_missing(self):
upload_query = Mock()
upload_query.where.return_value.first.return_value = None
session = Mock()
session.query.return_value = upload_query
session.scalar.return_value = None
result = RetrievalService.get_segment_attachment_info("dataset-id", "tenant-id", "upload-1", session)
assert result is None
def test_get_segment_attachment_infos_returns_empty_when_upload_files_missing(self):
upload_query = Mock()
upload_query.where.return_value.all.return_value = []
scalars_result = Mock()
scalars_result.all.return_value = []
session = Mock()
session.query.return_value = upload_query
session.scalars.return_value = scalars_result
result = RetrievalService.get_segment_attachment_infos(["upload-1"], session)
@@ -1115,12 +1113,12 @@ class TestRetrievalServiceInternals:
mime_type="image/png",
size=42,
)
upload_query = Mock()
upload_query.where.return_value.all.return_value = [upload_file]
binding_query = Mock()
binding_query.where.return_value.all.return_value = []
upload_scalars = Mock()
upload_scalars.all.return_value = [upload_file]
binding_scalars = Mock()
binding_scalars.all.return_value = []
session = Mock()
session.query.side_effect = [upload_query, binding_query]
session.scalars.side_effect = [upload_scalars, binding_scalars]
result = RetrievalService.get_segment_attachment_infos(["upload-1"], session)
@@ -1144,12 +1142,12 @@ class TestRetrievalServiceInternals:
)
binding = SimpleNamespace(attachment_id="upload-1", segment_id="segment-1")
upload_query = Mock()
upload_query.where.return_value.all.return_value = [upload_file_1, upload_file_2]
binding_query = Mock()
binding_query.where.return_value.all.return_value = [binding]
upload_scalars = Mock()
upload_scalars.all.return_value = [upload_file_1, upload_file_2]
binding_scalars = Mock()
binding_scalars.all.return_value = [binding]
session = Mock()
session.query.side_effect = [upload_query, binding_query]
session.scalars.side_effect = [upload_scalars, binding_scalars]
result = RetrievalService.get_segment_attachment_infos(["upload-1", "upload-2"], session)

View File

@@ -8,11 +8,12 @@ import type { EnvironmentVariable } from '@/app/components/workflow/types'
import type { App } from '@/types/app'
import { RiBuildingLine, RiGlobalLine, RiLockLine, RiMoreFill, RiVerifiedBadgeLine } from '@remixicon/react'
import * as React from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useCallback, useEffect, useId, useMemo, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { AppTypeIcon } from '@/app/components/app/type-selector'
import AppIcon from '@/app/components/base/app-icon'
import Divider from '@/app/components/base/divider'
import Input from '@/app/components/base/input'
import CustomPopover from '@/app/components/base/popover'
import TagSelector from '@/app/components/base/tag-management/selector'
import Tooltip from '@/app/components/base/tooltip'
@@ -69,6 +70,7 @@ type AppCardProps = {
const AppCard = ({ app, onRefresh }: AppCardProps) => {
const { t } = useTranslation()
const deleteAppNameInputId = useId()
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const { isCurrentWorkspaceEditor } = useAppContext()
const { onPlanInfoChanged } = useProviderContext()
@@ -89,14 +91,12 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
await mutateDeleteApp(app.id)
toast.success(t('appDeleted', { ns: 'app' }))
onPlanInfoChanged()
setShowConfirmDelete(false)
setConfirmDeleteInput('')
}
catch (e: any) {
toast.error(`${t('appDeleteFailed', { ns: 'app' })}${'message' in e ? `: ${e.message}` : ''}`)
}
finally {
setShowConfirmDelete(false)
setConfirmDeleteInput('')
}
}, [app.id, mutateDeleteApp, onPlanInfoChanged, t])
const onDeleteDialogOpenChange = useCallback((open: boolean) => {
@@ -108,6 +108,16 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
setConfirmDeleteInput('')
}, [isDeleting])
const isDeleteConfirmDisabled = isDeleting || confirmDeleteInput !== app.name
const onDeleteDialogSubmit: React.FormEventHandler<HTMLFormElement> = useCallback((e) => {
e.preventDefault()
if (isDeleteConfirmDisabled)
return
void onConfirmDelete()
}, [isDeleteConfirmDisabled, onConfirmDelete])
const onEdit: CreateAppModalProps['onConfirm'] = useCallback(async ({
name,
icon_type,
@@ -503,38 +513,51 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
)}
<AlertDialog open={showConfirmDelete} onOpenChange={onDeleteDialogOpenChange}>
<AlertDialogContent>
<div className="flex flex-col gap-2 px-6 pb-4 pt-6">
<AlertDialogTitle className="text-text-primary title-2xl-semi-bold">
{t('deleteAppConfirmTitle', { ns: 'app' })}
</AlertDialogTitle>
<AlertDialogDescription className="w-full whitespace-pre-wrap wrap-break-word text-text-tertiary system-md-regular">
{t('deleteAppConfirmContent', { ns: 'app' })}
</AlertDialogDescription>
<div className="mt-2">
<label className="mb-1 block text-text-secondary system-sm-regular">
{t('deleteAppConfirmInputLabel', { ns: 'app', appName: app.name })}
</label>
<input
type="text"
className="border-components-input-border bg-components-input-bg focus:border-components-input-border-focus focus:ring-components-input-border-focus h-9 w-full rounded-lg border px-3 text-sm text-text-primary placeholder:text-text-quaternary focus:outline-hidden focus:ring-1"
placeholder={t('deleteAppConfirmInputPlaceholder', { ns: 'app' })}
value={confirmDeleteInput}
onChange={e => setConfirmDeleteInput(e.target.value)}
/>
<form className="flex flex-col" onSubmit={onDeleteDialogSubmit}>
<div className="flex flex-col gap-2 px-6 pt-6 pb-4">
<AlertDialogTitle className="title-2xl-semi-bold text-text-primary">
{t('deleteAppConfirmTitle', { ns: 'app' })}
</AlertDialogTitle>
<AlertDialogDescription className="w-full system-md-regular wrap-break-word whitespace-pre-wrap text-text-tertiary">
{t('deleteAppConfirmContent', { ns: 'app' })}
</AlertDialogDescription>
<div className="mt-2">
<label htmlFor={deleteAppNameInputId} className="mb-1 block system-sm-regular text-text-secondary">
<Trans
i18nKey="deleteAppConfirmInputLabel"
ns="app"
values={{ appName: app.name }}
components={{
appName: <span className="system-sm-semibold text-text-primary" translate="no" />,
}}
/>
</label>
<Input
id={deleteAppNameInputId}
name="confirm-app-name"
type="text"
autoComplete="off"
spellCheck={false}
placeholder={t('deleteAppConfirmInputPlaceholder', { ns: 'app' })}
value={confirmDeleteInput}
onChange={e => setConfirmDeleteInput(e.target.value)}
className="border-components-input-border-hover bg-components-input-bg-normal focus:border-components-input-border-active focus:bg-components-input-bg-active"
/>
</div>
</div>
</div>
<AlertDialogActions>
<AlertDialogCancelButton disabled={isDeleting}>
{t('operation.cancel', { ns: 'common' })}
</AlertDialogCancelButton>
<AlertDialogConfirmButton
loading={isDeleting}
disabled={isDeleting || confirmDeleteInput !== app.name}
onClick={onConfirmDelete}
>
{t('operation.confirm', { ns: 'common' })}
</AlertDialogConfirmButton>
</AlertDialogActions>
<AlertDialogActions>
<AlertDialogCancelButton type="button" disabled={isDeleting}>
{t('operation.cancel', { ns: 'common' })}
</AlertDialogCancelButton>
<AlertDialogConfirmButton
type="submit"
loading={isDeleting}
disabled={isDeleteConfirmDisabled}
>
{t('operation.confirm', { ns: 'common' })}
</AlertDialogConfirmButton>
</AlertDialogActions>
</form>
</AlertDialogContent>
</AlertDialog>
{secretEnvList.length > 0 && (

View File

@@ -32,7 +32,7 @@ export function AlertDialogContent({
<BaseAlertDialog.Backdrop
{...backdropProps}
className={cn(
'fixed inset-0 z-1002 bg-background-overlay',
'inset-0 fixed z-1002 bg-background-overlay',
'transition-opacity duration-150 data-ending-style:opacity-0 data-starting-style:opacity-0 motion-reduce:transition-none',
overlayClassName,
)}
@@ -40,8 +40,8 @@ export function AlertDialogContent({
<BaseAlertDialog.Popup
{...popupProps}
className={cn(
'fixed left-1/2 top-1/2 z-1002 max-h-[calc(100vh-2rem)] w-[480px] max-w-[calc(100vw-2rem)] -translate-x-1/2 -translate-y-1/2 overflow-y-auto overscroll-contain rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg',
'transition-[transform,scale,opacity] duration-150 data-ending-style:scale-95 data-starting-style:scale-95 data-ending-style:opacity-0 data-starting-style:opacity-0 motion-reduce:transition-none',
'fixed top-1/2 left-1/2 z-1002 max-h-[calc(100vh-2rem)] w-[480px] max-w-[calc(100vw-2rem)] -translate-x-1/2 -translate-y-1/2 overflow-y-auto overscroll-contain rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg',
'transition-[transform,scale,opacity] duration-150 data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0 motion-reduce:transition-none',
className,
)}
>

View File

@@ -22,7 +22,7 @@ import {
const TriggerArea = ({ label = 'Right-click inside this area' }: { label?: string }) => (
<ContextMenuTrigger
aria-label="context menu trigger area"
render={<button type="button" className="flex h-44 w-80 select-none items-center justify-center rounded-xl border border-divider-subtle bg-background-default-subtle px-6 text-center text-sm text-text-tertiary" />}
render={<button type="button" className="flex h-44 w-80 items-center justify-center rounded-xl border border-divider-subtle bg-background-default-subtle px-6 text-center text-sm text-text-tertiary select-none" />}
>
{label}
</ContextMenuTrigger>

View File

@@ -33,7 +33,7 @@ export function DialogCloseButton({
aria-label={ariaLabel}
{...props}
className={cn(
'absolute right-6 top-6 z-10 flex h-5 w-5 cursor-pointer items-center justify-center rounded-2xl hover:bg-state-base-hover focus-visible:bg-state-base-hover focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-components-input-border-hover disabled:cursor-not-allowed disabled:opacity-50',
'absolute top-6 right-6 z-10 flex h-5 w-5 cursor-pointer items-center justify-center rounded-2xl hover:bg-state-base-hover focus-visible:bg-state-base-hover focus-visible:ring-1 focus-visible:ring-components-input-border-hover focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
>
@@ -60,7 +60,7 @@ export function DialogContent({
<BaseDialog.Backdrop
{...backdropProps}
className={cn(
'fixed inset-0 z-1002 bg-background-overlay',
'inset-0 fixed z-1002 bg-background-overlay',
'transition-opacity duration-150 data-ending-style:opacity-0 data-starting-style:opacity-0 motion-reduce:transition-none',
overlayClassName,
backdropProps?.className,
@@ -68,8 +68,8 @@ export function DialogContent({
/>
<BaseDialog.Popup
className={cn(
'fixed left-1/2 top-1/2 z-1002 max-h-[80dvh] w-[480px] max-w-[calc(100vw-2rem)] -translate-x-1/2 -translate-y-1/2 overflow-y-auto overscroll-contain rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-6 shadow-xl',
'transition-[transform,scale,opacity] duration-150 data-ending-style:scale-95 data-starting-style:scale-95 data-ending-style:opacity-0 data-starting-style:opacity-0 motion-reduce:transition-none',
'fixed top-1/2 left-1/2 z-1002 max-h-[80dvh] w-[480px] max-w-[calc(100vw-2rem)] -translate-x-1/2 -translate-y-1/2 overflow-y-auto overscroll-contain rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-6 shadow-xl',
'transition-[transform,scale,opacity] duration-150 data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0 motion-reduce:transition-none',
className,
)}
>

View File

@@ -186,7 +186,7 @@ export function DropdownMenuSubTrigger({
{...props}
>
{children}
<span aria-hidden className="i-ri-arrow-right-s-line ml-auto size-4 shrink-0 text-text-tertiary" />
<span aria-hidden className="ml-auto i-ri-arrow-right-s-line size-4 shrink-0 text-text-tertiary" />
</Menu.SubmenuTrigger>
)
}

View File

@@ -44,10 +44,10 @@ const FieldLabel = ({
helperText,
}: Pick<DemoFieldProps, 'label' | 'helperText'> & { inputId: string }) => (
<div className="space-y-1">
<label htmlFor={inputId} className="text-text-secondary system-sm-medium">
<label htmlFor={inputId} className="system-sm-medium text-text-secondary">
{label}
</label>
<p className="text-text-tertiary system-xs-regular">{helperText}</p>
<p className="system-xs-regular text-text-tertiary">{helperText}</p>
</div>
)
@@ -97,7 +97,7 @@ const DemoField = ({
</NumberFieldGroup>
</NumberField>
{showCurrentValue && (
<p className="text-text-quaternary system-xs-regular">
<p className="system-xs-regular text-text-quaternary">
Current value:
{' '}
{formatValue ? formatValue(value) : formatNumericValue(value, unit)}

View File

@@ -84,7 +84,7 @@ export function NumberFieldInput({
}
export const numberFieldUnitVariants = cva(
'flex shrink-0 items-center self-stretch text-text-tertiary system-sm-regular',
'flex shrink-0 items-center self-stretch system-sm-regular text-text-tertiary',
{
variants: {
size: {
@@ -133,9 +133,9 @@ export function NumberFieldControls({
const numberFieldControlButtonVariants = cva(
[
'flex touch-manipulation select-none items-center justify-center px-1.5 text-text-tertiary outline-hidden transition-colors',
'flex touch-manipulation items-center justify-center px-1.5 text-text-tertiary outline-hidden transition-colors select-none',
'hover:bg-components-input-bg-hover focus-visible:bg-components-input-bg-hover',
'focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-components-input-border-active',
'focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:ring-inset',
'disabled:cursor-not-allowed disabled:hover:bg-transparent disabled:focus-visible:bg-transparent disabled:focus-visible:ring-0',
'group-data-disabled/number-field:cursor-not-allowed hover:group-data-disabled/number-field:bg-transparent focus-visible:group-data-disabled/number-field:bg-transparent focus-visible:group-data-disabled/number-field:ring-0',
'group-data-readonly/number-field:cursor-default hover:group-data-readonly/number-field:bg-transparent focus-visible:group-data-readonly/number-field:bg-transparent focus-visible:group-data-readonly/number-field:ring-0',

View File

@@ -54,7 +54,7 @@ export function PopoverContent({
<BasePopover.Popup
className={cn(
'rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg',
'origin-(--transform-origin) transition-[transform,scale,opacity] data-ending-style:scale-95 data-starting-style:scale-95 data-ending-style:opacity-0 data-starting-style:opacity-0 motion-reduce:transition-none',
'origin-(--transform-origin) transition-[transform,scale,opacity] data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0 motion-reduce:transition-none',
popupClassName,
)}
{...popupProps}

View File

@@ -32,8 +32,8 @@ type Story = StoryObj<typeof meta>
const panelClassName = 'overflow-hidden rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg shadow-shadow-shadow-5'
const blurPanelClassName = 'overflow-hidden rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-xl shadow-shadow-shadow-7 backdrop-blur-[6px]'
const labelClassName = 'text-text-tertiary system-xs-medium-uppercase tracking-[0.14em]'
const titleClassName = 'text-text-primary system-sm-semibold'
const bodyClassName = 'text-text-secondary system-sm-regular'
const titleClassName = 'system-sm-semibold text-text-primary'
const bodyClassName = 'system-sm-regular text-text-secondary'
const insetScrollAreaClassName = 'h-full p-1'
const insetViewportClassName = 'radius-3xl bg-components-panel-bg'
const insetScrollbarClassName = 'data-[orientation=vertical]:my-1 data-[orientation=vertical]:me-1 data-[orientation=horizontal]:mx-1 data-[orientation=horizontal]:mb-1'
@@ -126,8 +126,8 @@ const StoryCard = ({
<section className={cn('min-w-0 radius-6xl border border-divider-subtle bg-background-body p-5', className)}>
<div className="space-y-1">
<div className={labelClassName}>{eyebrow}</div>
<h3 className="text-pretty text-text-primary system-md-semibold">{title}</h3>
<p className="max-w-[72ch] text-pretty text-text-secondary system-sm-regular">{description}</p>
<h3 className="system-md-semibold text-pretty text-text-primary">{title}</h3>
<p className="max-w-[72ch] system-sm-regular text-pretty text-text-secondary">{description}</p>
</div>
{children}
</section>
@@ -140,7 +140,7 @@ const VerticalPanelPane = () => (
<ScrollAreaContent className="space-y-3 p-4 pr-6">
<div className="space-y-1">
<div className={labelClassName}>Release board</div>
<div className="text-text-primary system-md-semibold">Weekly checkpoints</div>
<div className="system-md-semibold text-text-primary">Weekly checkpoints</div>
<p className={bodyClassName}>A simple vertical panel with the default scrollbar skin and no business-specific overrides.</p>
</div>
{releaseRows.map(item => (
@@ -148,9 +148,9 @@ const VerticalPanelPane = () => (
<div className="flex items-start justify-between gap-3">
<div className="min-w-0 space-y-1">
<h4 className={cn(titleClassName, 'truncate')}>{item.title}</h4>
<p className="text-text-tertiary system-xs-regular">{item.meta}</p>
<p className="system-xs-regular text-text-tertiary">{item.meta}</p>
</div>
<span className="rounded-full bg-state-base-hover px-2 py-1 text-text-secondary system-xs-medium">
<span className="rounded-full bg-state-base-hover px-2 py-1 system-xs-medium text-text-secondary">
{item.status}
</span>
</div>
@@ -170,14 +170,14 @@ const StickyListPane = () => (
<ScrollAreaRoot className={insetScrollAreaClassName}>
<ScrollAreaViewport className={cn(insetViewportClassName, 'mask-[linear-gradient(to_bottom,transparent_0px,black_10px,black_calc(100%-14px),transparent_100%)]')}>
<ScrollAreaContent className="min-h-full">
<div className="sticky top-0 z-10 border-b border-divider-subtle bg-components-panel-bg px-4 pb-3 pt-4">
<div className="sticky top-0 z-10 border-b border-divider-subtle bg-components-panel-bg px-4 pt-4 pb-3">
<div className={labelClassName}>Sticky header</div>
<div className="mt-1 flex items-center justify-between gap-3">
<div>
<div className="text-text-primary system-md-semibold">Operational queue</div>
<p className="mt-1 text-text-secondary system-xs-regular">The scrollbar is still the shared base/ui primitive, while the pane adds sticky structure and a viewport mask.</p>
<div className="system-md-semibold text-text-primary">Operational queue</div>
<p className="mt-1 system-xs-regular text-text-secondary">The scrollbar is still the shared base/ui primitive, while the pane adds sticky structure and a viewport mask.</p>
</div>
<span className="rounded-lg border border-divider-subtle bg-components-panel-bg-alt px-2.5 py-1 text-text-secondary system-xs-medium">
<span className="rounded-lg border border-divider-subtle bg-components-panel-bg-alt px-2.5 py-1 system-xs-medium text-text-secondary">
24 items
</span>
</div>
@@ -187,10 +187,10 @@ const StickyListPane = () => (
<article key={item.id} className="rounded-xl border border-divider-subtle bg-components-panel-bg-alt px-3 py-3">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0 space-y-1">
<div className="truncate text-text-primary system-sm-semibold">{item.title}</div>
<div className="line-clamp-2 wrap-break-word text-text-tertiary system-xs-regular">{item.note}</div>
<div className="truncate system-sm-semibold text-text-primary">{item.title}</div>
<div className="line-clamp-2 system-xs-regular wrap-break-word text-text-tertiary">{item.note}</div>
</div>
<span className="text-text-quaternary system-xs-medium">{item.id}</span>
<span className="system-xs-medium text-text-quaternary">{item.id}</span>
</div>
</article>
))}
@@ -221,7 +221,7 @@ const WorkbenchPane = ({
<ScrollAreaContent className="space-y-3 p-4 pr-6">
<div className="space-y-1">
<div className={labelClassName}>{eyebrow}</div>
<div className="text-text-primary system-md-semibold">{title}</div>
<div className="system-md-semibold text-text-primary">{title}</div>
</div>
{children}
</ScrollAreaContent>
@@ -234,13 +234,13 @@ const WorkbenchPane = ({
)
const HorizontalRailPane = () => (
<div className={cn(panelClassName, 'h-[272px] min-w-0 max-w-full')}>
<div className={cn(panelClassName, 'h-[272px] max-w-full min-w-0')}>
<ScrollAreaRoot className={insetScrollAreaClassName}>
<ScrollAreaViewport className={insetViewportClassName}>
<ScrollAreaContent className="min-h-full min-w-max space-y-4 p-4 pb-6">
<div className="space-y-1">
<div className={labelClassName}>Horizontal rail</div>
<div className="text-text-primary system-md-semibold">Model lanes</div>
<div className="system-md-semibold text-text-primary">Model lanes</div>
<p className={bodyClassName}>This pane keeps the default track behavior and only changes the surface layout around it.</p>
</div>
<div className="flex gap-3">
@@ -250,10 +250,10 @@ const HorizontalRailPane = () => (
<span className="inline-flex h-10 w-10 items-center justify-center rounded-xl bg-state-base-hover text-text-secondary">
<span aria-hidden className="i-ri-stack-line size-5" />
</span>
<div className="text-text-primary system-sm-semibold">{card.title}</div>
<div className="text-text-secondary system-sm-regular">{card.detail}</div>
<div className="system-sm-semibold text-text-primary">{card.title}</div>
<div className="system-sm-regular text-text-secondary">{card.detail}</div>
</div>
<div className="text-text-tertiary system-xs-regular">Drag cards into orchestration groups.</div>
<div className="system-xs-regular text-text-tertiary">Drag cards into orchestration groups.</div>
</article>
))}
</div>
@@ -315,8 +315,8 @@ const ScrollbarStatePane = ({
<div className="min-w-0 radius-6xl border border-divider-subtle bg-background-body p-5">
<div className="space-y-1">
<div className={labelClassName}>{eyebrow}</div>
<div className="text-text-primary system-md-semibold">{title}</div>
<p className="text-text-secondary system-sm-regular">{description}</p>
<div className="system-md-semibold text-text-primary">{title}</div>
<p className="system-sm-regular text-text-secondary">{description}</p>
</div>
<div className="mt-4 min-w-0 rounded-[24px] border border-divider-subtle bg-components-panel-bg p-3">
<ScrollAreaRoot className="h-[320px] p-1">
@@ -324,8 +324,8 @@ const ScrollbarStatePane = ({
<ScrollAreaContent className="min-w-0 space-y-2 p-4 pr-6">
{scrollbarShowcaseRows.map(item => (
<article key={item.title} className="min-w-0 rounded-xl border border-divider-subtle bg-components-panel-bg-alt p-3">
<div className="truncate text-text-primary system-sm-semibold">{item.title}</div>
<div className="mt-1 wrap-break-word text-text-secondary system-sm-regular">{item.body}</div>
<div className="truncate system-sm-semibold text-text-primary">{item.title}</div>
<div className="mt-1 system-sm-regular wrap-break-word text-text-secondary">{item.body}</div>
</article>
))}
</ScrollAreaContent>
@@ -343,22 +343,22 @@ const HorizontalScrollbarShowcasePane = () => (
<div className="min-w-0 radius-6xl border border-divider-subtle bg-background-body p-5">
<div className="space-y-1">
<div className={labelClassName}>Horizontal</div>
<div className="text-text-primary system-md-semibold">Horizontal track reference</div>
<p className="text-text-secondary system-sm-regular">Current design delivery defines the horizontal scrollbar body, but not a horizontal edge hint.</p>
<div className="system-md-semibold text-text-primary">Horizontal track reference</div>
<p className="system-sm-regular text-text-secondary">Current design delivery defines the horizontal scrollbar body, but not a horizontal edge hint.</p>
</div>
<div className="mt-4 min-w-0 rounded-[24px] border border-divider-subtle bg-components-panel-bg p-3">
<ScrollAreaRoot className="h-[240px] p-1">
<ScrollAreaViewport className="radius-3xl bg-components-panel-bg">
<ScrollAreaContent className="min-h-full min-w-max space-y-4 p-4 pb-6">
<div className="space-y-1">
<div className="text-text-primary system-sm-semibold">Horizontal scrollbar</div>
<div className="text-text-secondary system-sm-regular">A clean horizontal pane to inspect thickness, padding, and thumb behavior without extra masks.</div>
<div className="system-sm-semibold text-text-primary">Horizontal scrollbar</div>
<div className="system-sm-regular text-text-secondary">A clean horizontal pane to inspect thickness, padding, and thumb behavior without extra masks.</div>
</div>
<div className="flex gap-3">
{horizontalShowcaseCards.map(card => (
<article key={card.title} className="flex h-[120px] w-[220px] shrink-0 flex-col justify-between rounded-2xl border border-divider-subtle bg-components-panel-bg-alt p-4">
<div className="text-text-primary system-sm-semibold">{card.title}</div>
<div className="text-text-secondary system-sm-regular">{card.body}</div>
<div className="system-sm-semibold text-text-primary">{card.title}</div>
<div className="system-sm-regular text-text-secondary">{card.body}</div>
</article>
))}
</div>
@@ -380,7 +380,7 @@ const OverlayPane = () => (
<ScrollAreaContent className="space-y-2 p-3 pr-6">
<div className="sticky top-0 z-10 rounded-xl border border-divider-subtle bg-components-panel-bg-blur px-3 py-3 backdrop-blur-[6px]">
<div className={labelClassName}>Overlay palette</div>
<div className="mt-1 text-text-primary system-md-semibold">Quick actions</div>
<div className="mt-1 system-md-semibold text-text-primary">Quick actions</div>
</div>
{activityRows.map(item => (
<article key={item.title} className="rounded-xl border border-divider-subtle bg-components-panel-bg px-3 py-3 shadow-sm shadow-shadow-shadow-2">
@@ -389,8 +389,8 @@ const OverlayPane = () => (
<span aria-hidden className="i-ri-flashlight-line size-4" />
</span>
<div className="space-y-1">
<div className="text-text-primary system-sm-semibold">{item.title}</div>
<div className="text-text-secondary system-xs-regular">{item.body}</div>
<div className="system-sm-semibold text-text-primary">{item.title}</div>
<div className="system-xs-regular text-text-secondary">{item.body}</div>
</div>
</div>
</article>
@@ -413,22 +413,22 @@ const CornerPane = () => (
<div className="flex items-start justify-between gap-6">
<div className="space-y-1">
<div className={labelClassName}>Corner surface</div>
<div className="text-text-primary system-md-semibold">Bi-directional inspector canvas</div>
<div className="system-md-semibold text-text-primary">Bi-directional inspector canvas</div>
<p className={bodyClassName}>Both axes overflow here so the corner becomes visible as a deliberate seam between the two tracks.</p>
</div>
<span className="rounded-full bg-state-base-hover px-2 py-1 text-text-secondary system-xs-medium">
<span className="rounded-full bg-state-base-hover px-2 py-1 system-xs-medium text-text-secondary">
Always visible
</span>
</div>
<div className="grid min-w-[560px] grid-cols-[220px_repeat(3,180px)] gap-3">
{Array.from({ length: 12 }, (_, index) => (
<article key={index} className="rounded-2xl border border-divider-subtle bg-components-panel-bg-alt p-4">
<div className="text-text-primary system-sm-semibold">
<div className="system-sm-semibold text-text-primary">
Cell
{' '}
{index + 1}
</div>
<p className="mt-2 text-text-secondary system-sm-regular">
<p className="mt-2 system-sm-regular text-text-secondary">
Wide-and-tall content to force both scrollbars and show the corner treatment clearly.
</p>
</article>
@@ -458,7 +458,7 @@ const ExploreSidebarWebAppsPane = () => {
<div className="flex size-6 shrink-0 items-center justify-center rounded-md bg-components-icon-bg-blue-solid">
<span className="i-ri-apps-fill size-3.5 text-components-avatar-shape-fill-stop-100" />
</div>
<div className="min-w-0 truncate text-components-menu-item-text-active system-sm-semibold">
<div className="min-w-0 truncate system-sm-semibold text-components-menu-item-text-active">
Explore
</div>
</div>
@@ -466,10 +466,10 @@ const ExploreSidebarWebAppsPane = () => {
<div className="space-y-1.5">
<div className="flex items-center justify-between gap-3 px-2">
<p className="min-w-0 uppercase text-text-tertiary system-xs-medium-uppercase">
<p className="min-w-0 system-xs-medium-uppercase text-text-tertiary uppercase">
Web Apps
</p>
<span className="shrink-0 text-text-quaternary system-xs-medium">
<span className="shrink-0 system-xs-medium text-text-quaternary">
{webAppsRows.length}
</span>
</div>
@@ -555,7 +555,7 @@ export const ThreePaneWorkbench: Story = {
{releaseRows.map(item => (
<button key={item.title} type="button" className={storyButtonClassName}>
<span className="min-w-0 truncate system-sm-medium">{item.title}</span>
<span className="text-text-quaternary system-xs-medium">{item.status}</span>
<span className="system-xs-medium text-text-quaternary">{item.status}</span>
</button>
))}
</div>
@@ -565,16 +565,16 @@ export const ThreePaneWorkbench: Story = {
{Array.from({ length: 7 }, (_, index) => (
<section key={index} className="rounded-2xl border border-divider-subtle bg-components-panel-bg-alt p-4">
<div className="flex items-center justify-between gap-3">
<div className="text-text-primary system-sm-semibold">
<div className="system-sm-semibold text-text-primary">
Section
{' '}
{index + 1}
</div>
<span className="rounded-full bg-state-base-hover px-2 py-1 text-text-secondary system-xs-medium">
<span className="rounded-full bg-state-base-hover px-2 py-1 system-xs-medium text-text-secondary">
Active
</span>
</div>
<p className="mt-2 text-text-secondary system-sm-regular">
<p className="mt-2 system-sm-regular text-text-secondary">
This pane is intentionally long so the default vertical scrollbar sits over a larger editorial surface.
</p>
</section>
@@ -585,9 +585,9 @@ export const ThreePaneWorkbench: Story = {
<div className="space-y-3">
{queueRows.map(item => (
<article key={item.id} className="rounded-xl border border-divider-subtle bg-components-panel-bg-alt p-3">
<div className="text-text-primary system-sm-semibold">{item.id}</div>
<div className="mt-1 text-text-secondary system-sm-regular">{item.title}</div>
<div className="mt-2 text-text-tertiary system-xs-regular">{item.note}</div>
<div className="system-sm-semibold text-text-primary">{item.id}</div>
<div className="mt-1 system-sm-regular text-text-secondary">{item.title}</div>
<div className="mt-2 system-xs-regular text-text-tertiary">{item.note}</div>
</article>
))}
</div>
@@ -658,7 +658,7 @@ export const PrimitiveComposition: Story = {
<ScrollAreaViewport className={insetViewportClassName}>
<ScrollAreaContent className="min-w-[560px] space-y-3 p-4 pr-6">
{Array.from({ length: 8 }, (_, index) => (
<div key={index} className="rounded-xl border border-divider-subtle bg-components-panel-bg-alt px-3 py-3 text-text-secondary system-sm-regular">
<div key={index} className="rounded-xl border border-divider-subtle bg-components-panel-bg-alt px-3 py-3 system-sm-regular text-text-secondary">
Primitive row
{' '}
{index + 1}

View File

@@ -26,7 +26,7 @@ type ScrollAreaProps = Omit<ScrollAreaRootProps, 'children'> & {
const scrollAreaScrollbarClassName = cn(
styles.scrollbar,
'flex touch-none select-none overflow-clip p-1 opacity-100 transition-opacity motion-reduce:transition-none',
'flex touch-none overflow-clip p-1 opacity-100 transition-opacity select-none motion-reduce:transition-none',
'pointer-events-none data-hovering:pointer-events-auto',
'data-scrolling:pointer-events-auto',
'data-[orientation=vertical]:absolute data-[orientation=vertical]:inset-y-0 data-[orientation=vertical]:w-3 data-[orientation=vertical]:justify-center',
@@ -41,7 +41,7 @@ const scrollAreaThumbClassName = cn(
const scrollAreaViewportClassName = cn(
'size-full min-h-0 min-w-0 outline-hidden',
'focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-components-input-border-hover',
'focus-visible:ring-1 focus-visible:ring-components-input-border-hover focus-visible:ring-inset',
)
const scrollAreaCornerClassName = 'bg-transparent'

View File

@@ -53,7 +53,7 @@ function SliderDemo({
onValueChange={setValue}
aria-label="Demo slider"
/>
<div className="text-center text-text-secondary system-sm-medium">
<div className="text-center system-sm-medium text-text-secondary">
{value}
</div>
</div>

View File

@@ -28,7 +28,7 @@ type SliderProps = ControlledSliderProps | UncontrolledSliderProps
const sliderRootClassName = 'group/slider relative inline-flex w-full data-disabled:opacity-30'
const sliderControlClassName = cn(
'relative flex h-5 w-full touch-none select-none items-center',
'relative flex h-5 w-full touch-none items-center select-none',
'data-disabled:cursor-not-allowed',
)
const sliderTrackClassName = cn(
@@ -45,7 +45,7 @@ const sliderThumbClassName = cn(
'bg-(--slider-knob,var(--color-components-slider-knob)) shadow-sm',
'transition-[background-color,border-color,box-shadow,opacity] motion-reduce:transition-none',
'hover:bg-(--slider-knob-hover,var(--color-components-slider-knob-hover))',
'focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-components-slider-knob-border-hover focus-visible:ring-offset-0',
'focus-visible:ring-2 focus-visible:ring-components-slider-knob-border-hover focus-visible:ring-offset-0 focus-visible:outline-hidden',
'active:shadow-md',
'group-data-disabled/slider:bg-(--slider-knob-disabled,var(--color-components-slider-knob-disabled))',
'group-data-disabled/slider:border-(--slider-knob-border,var(--color-components-slider-knob-border))',

View File

@@ -41,8 +41,8 @@ export function TooltipContent({
>
<BaseTooltip.Popup
className={cn(
variant === 'default' && 'max-w-[300px] wrap-break-word rounded-md bg-components-panel-bg px-3 py-2 text-left text-text-tertiary shadow-lg system-xs-regular',
'origin-(--transform-origin) transition-opacity data-ending-style:opacity-0 data-starting-style:opacity-0 data-instant:transition-none motion-reduce:transition-none',
variant === 'default' && 'max-w-[300px] rounded-md bg-components-panel-bg px-3 py-2 text-left system-xs-regular wrap-break-word text-text-tertiary shadow-lg',
'origin-(--transform-origin) transition-opacity data-ending-style:opacity-0 data-instant:transition-none data-starting-style:opacity-0 motion-reduce:transition-none',
popupClassName,
)}
{...props}

View File

@@ -1388,7 +1388,7 @@
"count": 1
},
"tailwindcss/enforce-consistent-class-order": {
"count": 15
"count": 10
},
"ts/no-explicit-any": {
"count": 2
@@ -3813,71 +3813,6 @@
"count": 1
}
},
"app/components/base/ui/alert-dialog/index.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 3
}
},
"app/components/base/ui/context-menu/index.stories.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 1
}
},
"app/components/base/ui/dialog/index.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 4
}
},
"app/components/base/ui/dropdown-menu/index.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 1
}
},
"app/components/base/ui/number-field/index.stories.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 3
}
},
"app/components/base/ui/number-field/index.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 3
}
},
"app/components/base/ui/popover/index.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 1
}
},
"app/components/base/ui/scroll-area/index.stories.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 46
}
},
"app/components/base/ui/scroll-area/index.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 2
}
},
"app/components/base/ui/select/index.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 5
}
},
"app/components/base/ui/slider/index.stories.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 1
}
},
"app/components/base/ui/slider/index.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 2
}
},
"app/components/base/ui/tooltip/index.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 2
}
},
"app/components/base/video-gallery/VideoPlayer.tsx": {
"react/set-state-in-effect": {
"count": 1

View File

@@ -36,8 +36,8 @@
"createApp": "إنشاء تطبيق",
"createFromConfigFile": "إنشاء من ملف DSL",
"deleteAppConfirmContent": "حذف التطبيق لا رجعة فيه. لن يتمكن المستخدمون من الوصول إلى تطبيقك بعد الآن، وسيتم حذف جميع تكوينات المطالبة والسجلات بشكل دائم.",
"deleteAppConfirmInputLabel": "للتأكيد، اكتب \"{{appName}}\" في المربع أدناه:",
"deleteAppConfirmInputPlaceholder": "أدخل اسم التطبيق",
"deleteAppConfirmInputLabel": "للتأكيد، اكتب <appName>{{appName}}</appName> في المربع أدناه:",
"deleteAppConfirmInputPlaceholder": "أدخل اسم التطبيق",
"deleteAppConfirmTitle": "حذف هذا التطبيق؟",
"dslUploader.browse": "تصفح",
"dslUploader.button": "اسحب وأفلت الملف، أو",

View File

@@ -36,8 +36,8 @@
"createApp": "Neue App erstellen",
"createFromConfigFile": "App aus Konfigurationsdatei erstellen",
"deleteAppConfirmContent": "Das Löschen der App ist unwiderruflich. Nutzer werden keinen Zugang mehr zu Ihrer App haben, und alle Prompt-Konfigurationen und Logs werden dauerhaft gelöscht.",
"deleteAppConfirmInputLabel": "Geben Sie zur Bestätigung \"{{appName}}\" in das Feld unten ein:",
"deleteAppConfirmInputPlaceholder": "App-Namen eingeben",
"deleteAppConfirmInputLabel": "Geben Sie zur Bestätigung <appName>{{appName}}</appName> in das Feld unten ein:",
"deleteAppConfirmInputPlaceholder": "App-Namen eingeben",
"deleteAppConfirmTitle": "Diese App löschen?",
"dslUploader.browse": "Durchsuchen",
"dslUploader.button": "Datei per Drag & Drop ablegen oder",

View File

@@ -36,8 +36,8 @@
"createApp": "CREATE APP",
"createFromConfigFile": "Create from DSL file",
"deleteAppConfirmContent": "Deleting the app is irreversible. Users will no longer be able to access your app, and all prompt configurations and logs will be permanently deleted.",
"deleteAppConfirmInputLabel": "To confirm, type \"{{appName}}\" in the box below:",
"deleteAppConfirmInputPlaceholder": "Enter app name",
"deleteAppConfirmInputLabel": "To confirm, type <appName>{{appName}}</appName> in the box below:",
"deleteAppConfirmInputPlaceholder": "Enter app name",
"deleteAppConfirmTitle": "Delete this app?",
"dslUploader.browse": "Browse",
"dslUploader.button": "Drag and drop file, or",

View File

@@ -36,8 +36,8 @@
"createApp": "CREAR APP",
"createFromConfigFile": "Crear desde archivo DSL",
"deleteAppConfirmContent": "Eliminar la app es irreversible. Los usuarios ya no podrán acceder a tu app y todas las configuraciones y registros de prompts se eliminarán permanentemente.",
"deleteAppConfirmInputLabel": "Para confirmar, escriba \"{{appName}}\" en el cuadro a continuación:",
"deleteAppConfirmInputPlaceholder": "Ingrese el nombre de la app",
"deleteAppConfirmInputLabel": "Para confirmar, escriba <appName>{{appName}}</appName> en el cuadro a continuación:",
"deleteAppConfirmInputPlaceholder": "Ingrese el nombre de la app",
"deleteAppConfirmTitle": "¿Eliminar esta app?",
"dslUploader.browse": "Examinar",
"dslUploader.button": "Arrastrar y soltar archivo, o",

View File

@@ -36,8 +36,8 @@
"createApp": "ایجاد برنامه",
"createFromConfigFile": "ایجاد از فایل DSL",
"deleteAppConfirmContent": "حذف برنامه غیرقابل برگشت است. کاربران دیگر قادر به دسترسی به برنامه شما نخواهند بود و تمام تنظیمات و گزارشات درخواست‌ها به صورت دائم حذف خواهند شد.",
"deleteAppConfirmInputLabel": "برای تأیید، \"{{appName}}\" را در کادر زیر تایپ کنید:",
"deleteAppConfirmInputPlaceholder": "نام برنامه را وارد کنید",
"deleteAppConfirmInputLabel": "برای تأیید، <appName>{{appName}}</appName> را در کادر زیر تایپ کنید:",
"deleteAppConfirmInputPlaceholder": "نام برنامه را وارد کنید",
"deleteAppConfirmTitle": "آیا این برنامه حذف شود؟",
"dslUploader.browse": "مرور",
"dslUploader.button": "فایل را بکشید و رها کنید، یا",

View File

@@ -36,8 +36,8 @@
"createApp": "CRÉER UNE APPLICATION",
"createFromConfigFile": "Créer à partir du fichier DSL",
"deleteAppConfirmContent": "La suppression de l'application est irréversible. Les utilisateurs ne pourront plus accéder à votre application et toutes les configurations de prompt et les journaux seront définitivement supprimés.",
"deleteAppConfirmInputLabel": "Pour confirmer, tapez \"{{appName}}\" dans la case ci-dessous :",
"deleteAppConfirmInputPlaceholder": "Entrez le nom de l'application",
"deleteAppConfirmInputLabel": "Pour confirmer, tapez <appName>{{appName}}</appName> dans la case ci-dessous :",
"deleteAppConfirmInputPlaceholder": "Entrez le nom de l'application",
"deleteAppConfirmTitle": "Supprimer cette application ?",
"dslUploader.browse": "Parcourir",
"dslUploader.button": "Glisser-déposer un fichier, ou",

View File

@@ -36,8 +36,8 @@
"createApp": "ऐप बनाएँ",
"createFromConfigFile": "डीएसएल फ़ाइल से बनाएँ",
"deleteAppConfirmContent": "ऐप को हटाना अपरिवर्तनीय है। उपयोगकर्ता अब आपके ऐप तक पहुँचने में सक्षम नहीं होंगे, और सभी प्रॉम्प्ट कॉन्फ़िगरेशन और लॉग स्थायी रूप से हटा दिए जाएंगे।",
"deleteAppConfirmInputLabel": "पुष्टि करने के लिए, नीचे दिए गए बॉक्स में \"{{appName}}\" टाइप करें:",
"deleteAppConfirmInputPlaceholder": "ऐप का नाम दर्ज करें",
"deleteAppConfirmInputLabel": "पुष्टि करने के लिए, नीचे दिए गए बॉक्स में <appName>{{appName}}</appName> टाइप करें:",
"deleteAppConfirmInputPlaceholder": "ऐप का नाम दर्ज करें",
"deleteAppConfirmTitle": "इस ऐप को हटाएँ?",
"dslUploader.browse": "ब्राउज़ करें",
"dslUploader.button": "फ़ाइल खींचकर छोड़ें, या",

View File

@@ -36,8 +36,8 @@
"createApp": "BUAT APLIKASI",
"createFromConfigFile": "Buat dari file DSL",
"deleteAppConfirmContent": "Menghapus aplikasi tidak dapat diubah. Pengguna tidak akan dapat lagi mengakses aplikasi Anda, dan semua konfigurasi prompt serta log akan dihapus secara permanen.",
"deleteAppConfirmInputLabel": "Untuk konfirmasi, ketik \"{{appName}}\" di kotak di bawah ini:",
"deleteAppConfirmInputPlaceholder": "Masukkan nama aplikasi",
"deleteAppConfirmInputLabel": "Untuk konfirmasi, ketik <appName>{{appName}}</appName> di kotak di bawah ini:",
"deleteAppConfirmInputPlaceholder": "Masukkan nama aplikasi",
"deleteAppConfirmTitle": "Hapus aplikasi ini?",
"dslUploader.browse": "Ramban",
"dslUploader.button": "Seret dan lepas file, atau",

View File

@@ -36,8 +36,8 @@
"createApp": "CREA APP",
"createFromConfigFile": "Crea da file DSL",
"deleteAppConfirmContent": "Eliminare l'app è irreversibile. Gli utenti non potranno più accedere alla tua app e tutte le configurazioni e i log dei prompt verranno eliminati permanentemente.",
"deleteAppConfirmInputLabel": "Per confermare, digita \"{{appName}}\" nel campo sottostante:",
"deleteAppConfirmInputPlaceholder": "Inserisci il nome dell'app",
"deleteAppConfirmInputLabel": "Per confermare, digita <appName>{{appName}}</appName> nel campo sottostante:",
"deleteAppConfirmInputPlaceholder": "Inserisci il nome dell'app",
"deleteAppConfirmTitle": "Eliminare questa app?",
"dslUploader.browse": "Sfoglia",
"dslUploader.button": "Trascina e rilascia il file, o",

View File

@@ -36,8 +36,8 @@
"createApp": "アプリを作成する",
"createFromConfigFile": "DSL ファイルから作成する",
"deleteAppConfirmContent": "アプリを削除すると、元に戻すことはできません。他のユーザーはもはやこのアプリにアクセスできず、すべてのプロンプトの設定とログが永久に削除されます。",
"deleteAppConfirmInputLabel": "確認するには、下のボックスに{{appName}}と入力してください:",
"deleteAppConfirmInputPlaceholder": "アプリ名を入力",
"deleteAppConfirmInputLabel": "確認するには、下のボックスに<appName>{{appName}}</appName>と入力してください:",
"deleteAppConfirmInputPlaceholder": "アプリ名を入力",
"deleteAppConfirmTitle": "このアプリを削除しますか?",
"dslUploader.browse": "参照",
"dslUploader.button": "ファイルをドラッグ&ドロップするか、",

View File

@@ -36,8 +36,8 @@
"createApp": "앱 만들기",
"createFromConfigFile": "DSL 파일에서 생성하기",
"deleteAppConfirmContent": "앱을 삭제하면 복구할 수 없습니다. 사용자는 더 이상 앱에 액세스할 수 없으며 모든 프롬프트 설정 및 로그가 영구적으로 삭제됩니다.",
"deleteAppConfirmInputLabel": "확인하려면 아래 상자에 \"{{appName}}\"을 입력하세요:",
"deleteAppConfirmInputPlaceholder": "앱 이름 입력",
"deleteAppConfirmInputLabel": "확인하려면 아래 상자에 <appName>{{appName}}</appName>을 입력하세요:",
"deleteAppConfirmInputPlaceholder": "앱 이름 입력",
"deleteAppConfirmTitle": "이 앱을 삭제하시겠습니까?",
"dslUploader.browse": "찾아보기",
"dslUploader.button": "파일을 드래그 앤 드롭하거나",

View File

@@ -36,8 +36,8 @@
"createApp": "CREATE APP",
"createFromConfigFile": "Create from DSL file",
"deleteAppConfirmContent": "Deleting the app is irreversible. Users will no longer be able to access your app, and all prompt configurations and logs will be permanently deleted.",
"deleteAppConfirmInputLabel": "To confirm, type \"{{appName}}\" in the box below:",
"deleteAppConfirmInputPlaceholder": "Enter app name",
"deleteAppConfirmInputLabel": "To confirm, type <appName>{{appName}}</appName> in the box below:",
"deleteAppConfirmInputPlaceholder": "Enter app name",
"deleteAppConfirmTitle": "Delete this app?",
"dslUploader.browse": "Browse",
"dslUploader.button": "Drag and drop file, or",

View File

@@ -36,8 +36,8 @@
"createApp": "UTWÓRZ APLIKACJĘ",
"createFromConfigFile": "Utwórz z pliku DSL",
"deleteAppConfirmContent": "Usunięcie aplikacji jest nieodwracalne. Użytkownicy nie będą mieli już dostępu do twojej aplikacji, a wszystkie konfiguracje monitów i dzienniki zostaną trwale usunięte.",
"deleteAppConfirmInputLabel": "Aby potwierdzić, wpisz \"{{appName}}\" w polu poniżej:",
"deleteAppConfirmInputPlaceholder": "Wpisz nazwę aplikacji",
"deleteAppConfirmInputLabel": "Aby potwierdzić, wpisz <appName>{{appName}}</appName> w polu poniżej:",
"deleteAppConfirmInputPlaceholder": "Wpisz nazwę aplikacji",
"deleteAppConfirmTitle": "Usunąć tę aplikację?",
"dslUploader.browse": "Przeglądaj",
"dslUploader.button": "Przeciągnij i upuść plik, lub",

View File

@@ -36,8 +36,8 @@
"createApp": "CRIAR APLICATIVO",
"createFromConfigFile": "Criar a partir do arquivo DSL",
"deleteAppConfirmContent": "A exclusão do aplicativo é irreversível. Os usuários não poderão mais acessar seu aplicativo e todas as configurações de prompt e logs serão permanentemente excluídas.",
"deleteAppConfirmInputLabel": "Para confirmar, digite \"{{appName}}\" na caixa abaixo:",
"deleteAppConfirmInputPlaceholder": "Digite o nome do aplicativo",
"deleteAppConfirmInputLabel": "Para confirmar, digite <appName>{{appName}}</appName> na caixa abaixo:",
"deleteAppConfirmInputPlaceholder": "Digite o nome do aplicativo",
"deleteAppConfirmTitle": "Excluir este aplicativo?",
"dslUploader.browse": "Navegar",
"dslUploader.button": "Arraste e solte o arquivo, ou",

View File

@@ -36,8 +36,8 @@
"createApp": "CREEAZĂ APLICAȚIE",
"createFromConfigFile": "Creează din fișier DSL",
"deleteAppConfirmContent": "Ștergerea aplicației este ireversibilă. Utilizatorii nu vor mai putea accesa aplicația ta, iar toate configurațiile promptului și jurnalele vor fi șterse permanent.",
"deleteAppConfirmInputLabel": "Pentru confirmare, tastați \"{{appName}}\" în caseta de mai jos:",
"deleteAppConfirmInputPlaceholder": "Introduceți numele aplicației",
"deleteAppConfirmInputLabel": "Pentru confirmare, tastați <appName>{{appName}}</appName> în caseta de mai jos:",
"deleteAppConfirmInputPlaceholder": "Introduceți numele aplicației",
"deleteAppConfirmTitle": "Ștergi această aplicație?",
"dslUploader.browse": "Răsfoiți",
"dslUploader.button": "Trageți și plasați fișierul, sau",

View File

@@ -36,8 +36,8 @@
"createApp": "СОЗДАТЬ ПРИЛОЖЕНИЕ",
"createFromConfigFile": "Создать из файла DSL",
"deleteAppConfirmContent": "Удаление приложения необратимо. Пользователи больше не смогут получить доступ к вашему приложению, и все настройки подсказок и журналы будут безвозвратно удалены.",
"deleteAppConfirmInputLabel": "Для подтверждения введите \"{{appName}}\" в поле ниже:",
"deleteAppConfirmInputPlaceholder": "Введите название приложения",
"deleteAppConfirmInputLabel": "Для подтверждения введите <appName>{{appName}}</appName> в поле ниже:",
"deleteAppConfirmInputPlaceholder": "Введите название приложения",
"deleteAppConfirmTitle": "Удалить это приложение?",
"dslUploader.browse": "Обзор",
"dslUploader.button": "Перетащите файл, или",

View File

@@ -36,8 +36,8 @@
"createApp": "USTVARI APLIKACIJO",
"createFromConfigFile": "Ustvari iz datoteke DSL",
"deleteAppConfirmContent": "Brisanje aplikacije je nepopravljivo. Uporabniki ne bodo več imeli dostopa do vaše aplikacije, vse konfiguracije in dnevniki pa bodo trajno izbrisani.",
"deleteAppConfirmInputLabel": "Za potrditev vnesite \"{{appName}}\" v polje spodaj:",
"deleteAppConfirmInputPlaceholder": "Vnesite ime aplikacije",
"deleteAppConfirmInputLabel": "Za potrditev vnesite <appName>{{appName}}</appName> v polje spodaj:",
"deleteAppConfirmInputPlaceholder": "Vnesite ime aplikacije",
"deleteAppConfirmTitle": "Izbrišem to aplikacijo?",
"dslUploader.browse": "Prebrskaj",
"dslUploader.button": "Povlecite in spustite datoteko, ali",

View File

@@ -36,8 +36,8 @@
"createApp": "สร้างโปรเจกต์ใหม่",
"createFromConfigFile": "สร้างจากไฟล์ DSL",
"deleteAppConfirmContent": "การลบโปรเจกนั้นไม่สามารถย้อนกลับได้ ผู้ใช้จะไม่สามารถเข้าถึงโปรเจกต์ของคุณอีกต่อไป และการกําหนดค่าต่างๆและบันทึกทั้งหมดจะถูกลบอย่างถาวร",
"deleteAppConfirmInputLabel": "หากต้องการยืนยัน พิมพ์ \"{{appName}}\" ในช่องด้านล่าง:",
"deleteAppConfirmInputPlaceholder": "ใส่ชื่อแอป",
"deleteAppConfirmInputLabel": "หากต้องการยืนยัน พิมพ์ <appName>{{appName}}</appName> ในช่องด้านล่าง:",
"deleteAppConfirmInputPlaceholder": "ใส่ชื่อแอป",
"deleteAppConfirmTitle": "ลบโปรเจกต์นี้?",
"dslUploader.browse": "เรียกดู",
"dslUploader.button": "ลากและวางไฟล์ หรือ",

View File

@@ -36,8 +36,8 @@
"createApp": "UYGULAMA OLUŞTUR",
"createFromConfigFile": "DSL dosyasından oluştur",
"deleteAppConfirmContent": "Uygulamanın silinmesi geri alınamaz. Kullanıcılar artık uygulamanıza erişemeyecek ve tüm prompt yapılandırmaları ile loglar kalıcı olarak silinecektir.",
"deleteAppConfirmInputLabel": "Onaylamak için aşağıdaki kutuya \"{{appName}}\" yazın:",
"deleteAppConfirmInputPlaceholder": "Uygulama adını girin",
"deleteAppConfirmInputLabel": "Onaylamak için aşağıdaki kutuya <appName>{{appName}}</appName> yazın:",
"deleteAppConfirmInputPlaceholder": "Uygulama adını girin",
"deleteAppConfirmTitle": "Bu uygulamayı silmek istiyor musunuz?",
"dslUploader.browse": "Gözat",
"dslUploader.button": "Dosyayı sürükleyip bırakın veya",

View File

@@ -36,8 +36,8 @@
"createApp": "Створити додаток",
"createFromConfigFile": "Створити з файлу DSL",
"deleteAppConfirmContent": "Видалення додатка незворотнє. Користувачі більше не зможуть отримати доступ до вашого додатка, і всі налаштування запитів та журнали будуть остаточно видалені.",
"deleteAppConfirmInputLabel": "Для підтвердження введіть \"{{appName}}\" у поле нижче:",
"deleteAppConfirmInputPlaceholder": "Введіть назву додатка",
"deleteAppConfirmInputLabel": "Для підтвердження введіть <appName>{{appName}}</appName> у поле нижче:",
"deleteAppConfirmInputPlaceholder": "Введіть назву додатка",
"deleteAppConfirmTitle": "Видалити цей додаток?",
"dslUploader.browse": "Огляд",
"dslUploader.button": "Перетягніть файл, або",

View File

@@ -36,8 +36,8 @@
"createApp": "TẠO ỨNG DỤNG",
"createFromConfigFile": "Tạo từ tệp DSL",
"deleteAppConfirmContent": "Việc xóa ứng dụng là không thể hoàn tác. Người dùng sẽ không thể truy cập vào ứng dụng của bạn nữa và tất cả cấu hình cũng như nhật ký nhắc sẽ bị xóa vĩnh viễn.",
"deleteAppConfirmInputLabel": "Để xác nhận, hãy nhập \"{{appName}}\" vào ô bên dưới:",
"deleteAppConfirmInputPlaceholder": "Nhập tên ứng dụng",
"deleteAppConfirmInputLabel": "Để xác nhận, hãy nhập <appName>{{appName}}</appName> vào ô bên dưới:",
"deleteAppConfirmInputPlaceholder": "Nhập tên ứng dụng",
"deleteAppConfirmTitle": "Xóa ứng dụng này?",
"dslUploader.browse": "Duyệt",
"dslUploader.button": "Kéo và thả tệp, hoặc",

View File

@@ -36,8 +36,8 @@
"createApp": "创建应用",
"createFromConfigFile": "通过 DSL 文件创建",
"deleteAppConfirmContent": "删除应用将无法撤销。用户将不能访问你的应用,所有 Prompt 编排配置和日志均将一并被删除。",
"deleteAppConfirmInputLabel": "请在下方输入框中输入\"{{appName}}\"以确认:",
"deleteAppConfirmInputPlaceholder": "输入应用名称",
"deleteAppConfirmInputLabel": "请在下方输入框中输入<appName>{{appName}}</appName>以确认:",
"deleteAppConfirmInputPlaceholder": "输入应用名称",
"deleteAppConfirmTitle": "确认删除应用?",
"dslUploader.browse": "选择文件",
"dslUploader.button": "拖拽文件至此,或者",

View File

@@ -36,8 +36,8 @@
"createApp": "建立應用",
"createFromConfigFile": "透過 DSL 檔案建立",
"deleteAppConfirmContent": "刪除應用將無法復原。使用者將無法存取你的應用,所有 Prompt 設定和日誌都將一併被刪除。",
"deleteAppConfirmInputLabel": "請在下方輸入框中輸入{{appName}}以確認:",
"deleteAppConfirmInputPlaceholder": "輸入應用程式名稱",
"deleteAppConfirmInputLabel": "請在下方輸入框中輸入<appName>{{appName}}</appName>以確認:",
"deleteAppConfirmInputPlaceholder": "輸入應用程式名稱",
"deleteAppConfirmTitle": "確認刪除應用?",
"dslUploader.browse": "選擇檔案",
"dslUploader.button": "拖拽檔案至此,或者",