Compare commits

..

40 Commits
1.1.1 ... 1.1.3

Author SHA1 Message Date
1be0d26c1f fix metadata filter not affect in keyword-search and fulltext-search (#16644) 2025-03-24 18:35:16 +08:00
c167a1f4f4 chore: bump the package version to 1.1.3 (#16612)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2025-03-24 17:59:54 +08:00
5eb0ca9b9d fix: fix inner API workspace Account.query error. (#16630) 2025-03-24 17:52:50 +08:00
6e26ed2bb7 fix: update retrieval configuration to correctly handle reranking mod… (#16641) 2025-03-24 17:47:56 +08:00
058d9c3525 chore: update release trigger to include all tags in build-push workflow (#16631)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2025-03-24 17:17:55 +08:00
b247fbb2ef Fix: style of sidebar with in mobile (#16629) 2025-03-24 16:30:23 +08:00
bc6f122364 Fix: style issue of app detail panel in jp (#16620) 2025-03-24 15:54:00 +08:00
815d77856d Fix: show feedback status in conversation (#16615) 2025-03-24 15:20:18 +08:00
05eaef84bb fix: cancel marketplace debounced search when clear search keywords (#16614) 2025-03-24 15:17:12 +08:00
770c461a8f feat: add openGauss PQ acceleration feature (#16432)
Co-authored-by: chenhuan <huan.chen0728@foxmail>
2025-03-24 15:16:40 +08:00
16b6ffd915 fix: sanitizer svg to avoid xss (#16606) 2025-03-24 14:36:07 +08:00
9701b573e0 feat: add datasets detail context and provider for improved data vali… (#16451) 2025-03-24 14:30:26 +08:00
83cd14104d feat: datasets openapi list segements support paged resp (#16603) 2025-03-24 14:27:31 +08:00
e2988acc2f Fix: web app sidebar cannot close when long title conversation existed (#16593) 2025-03-24 12:51:21 +08:00
cea4669b76 fix: transition in simple select causes page crash (#16587) 2025-03-24 11:07:16 +08:00
17b4d4c7b2 fix: workflow if-else node variable tag style (#16583) 2025-03-24 11:06:10 +08:00
e95f0fcceb chore: enable eslint cache (#16570) 2025-03-24 10:17:54 +08:00
6d5d6f0f24 fix: update app mode display text for advanced-chat type (#16578) 2025-03-24 10:17:04 +08:00
7ce8faf176 fix: fix variable-aggregator cannot pass node check in group mode (#16439)
Co-authored-by: crazywoola <427733928@qq.com>
2025-03-24 09:49:33 +08:00
f31e3313b0 feat: Make the logic of APP filtering and creation the same (#16079)
Co-authored-by: 刘江波 <jiangbo721@163.com>
2025-03-23 09:40:15 +08:00
f6ac98a37d fix: fix the app max_active_requests been overwritten bug (#16513) 2025-03-23 09:34:23 +08:00
f8e7e301cd fix: update chatbot doc link at create app page (#16479) 2025-03-23 09:27:16 +08:00
35bafb3235 fix: the error occurring when passing inputs on iOS devices and some … (#16534) 2025-03-23 09:26:35 +08:00
ae5d2ecf48 fix:weight_type missing when create document in dataset (#16503) 2025-03-22 20:21:57 +08:00
1907d2a90a fix: optimize query for expired workflow runs by adding date filter and limiting results (#16491) 2025-03-22 11:17:21 +08:00
4448a54cc1 use REDIS_PORT to replace 6379 in celery config (#16182) 2025-03-21 21:34:07 +08:00
bfc0d606dc feat: cleanup free tenants expired data like messages/conversations/workflow_runs/workflow_node_executions (#16490) 2025-03-21 21:30:35 +08:00
3306228840 fix: workflow file add related-id in iteration node (#16255)
Signed-off-by: kenwoodjw <blackxin55+@gmail.com>
2025-03-21 20:57:02 +08:00
d7e00ae691 feat: Skip Redis operations if RateLimit is disabled (#12226) 2025-03-21 19:55:27 +08:00
0e2e2db3fa refactor: add OpikDataTrace instance builder. (#16444)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2025-03-21 18:09:39 +08:00
bf90d34c2f chore: update version to 1.1.2 in configuration and Docker files (#16457)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2025-03-21 18:03:48 +08:00
383af7bf76 chore(api): enhance ruff rules to disallow dangerous functions and modules (#16461) 2025-03-21 17:49:35 +08:00
ac910ed200 feat: replace file content type to avoid load script in svg. (#16454)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2025-03-21 17:44:13 +08:00
7709d9df20 Chore: frontend infrastructure upgrade (#16420)
Co-authored-by: NFish <douxc512@gmail.com>
Co-authored-by: zxhlyh <jasonapring2015@outlook.com>
Co-authored-by: twwu <twwu@dify.ai>
Co-authored-by: jZonG <jzongcode@gmail.com>
2025-03-21 17:41:03 +08:00
e61415223b fix: xss in render svg (#16433) 2025-03-21 15:14:08 +08:00
a30945312a fix: typos (#16385) 2025-03-21 11:14:40 +08:00
Ron
bf682302ee fix error with literal_eval (#16297)
Co-authored-by: Novice <novice12185727@gmail.com>
2025-03-21 09:30:24 +08:00
72191f5b13 add built-in field check when doing old metadata migrate (#16371) 2025-03-20 21:53:49 +08:00
e324e59930 fix import DSL install Github plugin failed (#16362) 2025-03-20 21:37:45 +08:00
727caccfc9 fix: knowledge base openapi cannot delete metadata (#16365) 2025-03-20 21:36:09 +08:00
1495 changed files with 14263 additions and 11816 deletions

View File

@ -9,7 +9,7 @@ body:
required: true
- label: I confirm that I am using English to submit this report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)).
required: true
- label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue否则会被关闭。谢谢:"
- label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue否则会被关闭。谢谢:)"
required: true
- label: "Please do not modify this template :) and fill in all the required fields."
required: true

View File

@ -9,7 +9,7 @@ body:
required: true
- label: I confirm that I am using English to submit this report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)).
required: true
- label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue否则会被关闭。谢谢:"
- label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue否则会被关闭。谢谢:)"
required: true
- label: "Please do not modify this template :) and fill in all the required fields."
required: true

View File

@ -9,7 +9,7 @@ body:
required: true
- label: I confirm that I am using English to submit this report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)).
required: true
- label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue否则会被关闭。谢谢:"
- label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue否则会被关闭。谢谢:)"
required: true
- label: "Please do not modify this template :) and fill in all the required fields."
required: true

View File

@ -14,7 +14,7 @@ body:
required: true
- label: I confirm that I am using English to submit this report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)).
required: true
- label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue否则会被关闭。谢谢:"
- label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue否则会被关闭。谢谢:)"
required: true
- label: "Please do not modify this template :) and fill in all the required fields."
required: true

View File

@ -12,7 +12,7 @@ body:
required: true
- label: I confirm that I am using English to submit report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)).
required: true
- label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue否则会被关闭。谢谢:"
- label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue否则会被关闭。谢谢:)"
required: true
- label: "Please do not modify this template :) and fill in all the required fields."
required: true

View File

@ -12,7 +12,7 @@ body:
required: true
- label: I confirm that I am using English to submit this report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)).
required: true
- label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue否则会被关闭。谢谢:"
- label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue否则会被关闭。谢谢:)"
required: true
- label: "Please do not modify this template :) and fill in all the required fields."
required: true

View File

@ -1,5 +1,5 @@
name: "👾 Tracker"
description: For inner usages, please donot use this template.
description: For inner usages, please do not use this template.
title: "[Tracker] "
labels:
- tracker

View File

@ -1,5 +1,5 @@
name: "🌐 Localization/Translation issue"
description: Report incorrect translations. [please use English :]
description: Report incorrect translations. [please use English :)]
labels:
- translation
body:
@ -12,7 +12,7 @@ body:
required: true
- label: I confirm that I am using English to submit this report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)).
required: true
- label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue否则会被关闭。谢谢:"
- label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue否则会被关闭。谢谢:)"
required: true
- label: "Please do not modify this template :) and fill in all the required fields."
required: true

View File

@ -6,8 +6,8 @@ on:
- "main"
- "deploy/dev"
- "deploy/enterprise"
release:
types: [published]
tags:
- "*"
concurrency:
group: build-push-${{ github.head_ref || github.run_id }}

View File

@ -26,9 +26,6 @@ ACCESS_TOKEN_EXPIRE_MINUTES=60
# Refresh token expiration time in days
REFRESH_TOKEN_EXPIRE_DAYS=30
# celery configuration
CELERY_BROKER_URL=redis://:difyai123456@localhost:6379/1
# redis configuration
REDIS_HOST=localhost
REDIS_PORT=6379
@ -50,6 +47,9 @@ REDIS_USE_CLUSTERS=false
REDIS_CLUSTERS=
REDIS_CLUSTERS_PASSWORD=
# celery configuration
CELERY_BROKER_URL=redis://:difyai123456@localhost:${REDIS_PORT}/1
# PostgreSQL database configuration
DB_USERNAME=postgres
DB_PASSWORD=difyai123456

View File

@ -37,6 +37,12 @@ select = [
"UP", # pyupgrade rules
"W191", # tab-indentation
"W605", # invalid-escape-sequence
# security related linting rules
# RCE proctection (sort of)
"S102", # exec-builtin, disallow use of `exec`
"S307", # suspicious-eval-usage, disallow use of `eval` and `ast.literal_eval`
"S301", # suspicious-pickle-usage, disallow use of `pickle` and its wrappers.
"S302", # suspicious-marshal-usage, disallow use of `marshal` module
]
ignore = [

View File

@ -12,6 +12,7 @@ from configs import dify_config
from constants.languages import languages
from core.rag.datasource.vdb.vector_factory import Vector
from core.rag.datasource.vdb.vector_type import VectorType
from core.rag.index_processor.constant.built_in_field import BuiltInField
from core.rag.models.document import Document
from events.app_event import app_was_created
from extensions.ext_database import db
@ -25,6 +26,7 @@ from models.dataset import Document as DatasetDocument
from models.model import Account, App, AppAnnotationSetting, AppMode, Conversation, MessageAnnotation
from models.provider import Provider, ProviderModel
from services.account_service import RegisterService, TenantService
from services.clear_free_plan_tenant_expired_logs import ClearFreePlanTenantExpiredLogs
from services.plugin.data_migration import PluginDataMigration
from services.plugin.plugin_migration import PluginMigration
@ -559,36 +561,25 @@ def old_metadata_migration():
if document.doc_metadata:
doc_metadata = document.doc_metadata
for key, value in doc_metadata.items():
dataset_metadata = (
db.session.query(DatasetMetadata)
.filter(DatasetMetadata.dataset_id == document.dataset_id, DatasetMetadata.name == key)
.first()
)
if not dataset_metadata:
dataset_metadata = DatasetMetadata(
tenant_id=document.tenant_id,
dataset_id=document.dataset_id,
name=key,
type="string",
created_by=document.created_by,
)
db.session.add(dataset_metadata)
db.session.flush()
dataset_metadata_binding = DatasetMetadataBinding(
tenant_id=document.tenant_id,
dataset_id=document.dataset_id,
metadata_id=dataset_metadata.id,
document_id=document.id,
created_by=document.created_by,
)
db.session.add(dataset_metadata_binding)
for field in BuiltInField:
if field.value == key:
break
else:
dataset_metadata_binding = DatasetMetadataBinding.query.filter(
DatasetMetadataBinding.dataset_id == document.dataset_id,
DatasetMetadataBinding.document_id == document.id,
DatasetMetadataBinding.metadata_id == dataset_metadata.id,
).first()
if not dataset_metadata_binding:
dataset_metadata = (
db.session.query(DatasetMetadata)
.filter(DatasetMetadata.dataset_id == document.dataset_id, DatasetMetadata.name == key)
.first()
)
if not dataset_metadata:
dataset_metadata = DatasetMetadata(
tenant_id=document.tenant_id,
dataset_id=document.dataset_id,
name=key,
type="string",
created_by=document.created_by,
)
db.session.add(dataset_metadata)
db.session.flush()
dataset_metadata_binding = DatasetMetadataBinding(
tenant_id=document.tenant_id,
dataset_id=document.dataset_id,
@ -597,7 +588,22 @@ def old_metadata_migration():
created_by=document.created_by,
)
db.session.add(dataset_metadata_binding)
db.session.commit()
else:
dataset_metadata_binding = DatasetMetadataBinding.query.filter(
DatasetMetadataBinding.dataset_id == document.dataset_id,
DatasetMetadataBinding.document_id == document.id,
DatasetMetadataBinding.metadata_id == dataset_metadata.id,
).first()
if not dataset_metadata_binding:
dataset_metadata_binding = DatasetMetadataBinding(
tenant_id=document.tenant_id,
dataset_id=document.dataset_id,
metadata_id=dataset_metadata.id,
document_id=document.id,
created_by=document.created_by,
)
db.session.add(dataset_metadata_binding)
db.session.commit()
page += 1
click.echo(click.style("Old metadata migration completed.", fg="green"))
@ -787,3 +793,23 @@ def install_plugins(input_file: str, output_file: str, workers: int):
PluginMigration.install_plugins(input_file, output_file, workers)
click.echo(click.style("Install plugins completed.", fg="green"))
@click.command("clear-free-plan-tenant-expired-logs", help="Clear free plan tenant expired logs.")
@click.option("--days", prompt=True, help="The days to clear free plan tenant expired logs.", default=30)
@click.option("--batch", prompt=True, help="The batch size to clear free plan tenant expired logs.", default=100)
@click.option(
"--tenant_ids",
prompt=True,
multiple=True,
help="The tenant ids to clear free plan tenant expired logs.",
)
def clear_free_plan_tenant_expired_logs(days: int, batch: int, tenant_ids: list[str]):
"""
Clear free plan tenant expired logs.
"""
click.echo(click.style("Starting clear free plan tenant expired logs.", fg="white"))
ClearFreePlanTenantExpiredLogs.process(days, batch, tenant_ids)
click.echo(click.style("Clear free plan tenant expired logs completed.", fg="green"))

View File

@ -43,3 +43,8 @@ class OpenGaussConfig(BaseSettings):
description="Max connection of the OpenGauss database",
default=5,
)
OPENGAUSS_ENABLE_PQ: bool = Field(
description="Enable openGauss PQ acceleration feature",
default=False,
)

View File

@ -9,7 +9,7 @@ class PackagingInfo(BaseSettings):
CURRENT_VERSION: str = Field(
description="Dify version",
default="1.1.1",
default="1.1.3",
)
COMMIT_SHA: str = Field(

View File

@ -50,7 +50,15 @@ class AppListApi(Resource):
parser.add_argument(
"mode",
type=str,
choices=["chat", "workflow", "agent-chat", "channel", "all"],
choices=[
"completion",
"chat",
"advanced-chat",
"workflow",
"agent-chat",
"channel",
"all",
],
default="all",
location="args",
required=False,
@ -130,7 +138,6 @@ class AppApi(Resource):
parser.add_argument("icon_type", type=str, location="json")
parser.add_argument("icon", type=str, location="json")
parser.add_argument("icon_background", type=str, location="json")
parser.add_argument("max_active_requests", type=int, location="json")
parser.add_argument("use_icon_as_answer_icon", type=bool, location="json")
args = parser.parse_args()

View File

@ -75,6 +75,7 @@ class FilePreviewApi(Resource):
if args["as_attachment"]:
encoded_filename = quote(upload_file.name)
response.headers["Content-Disposition"] = f"attachment; filename*=UTF-8''{encoded_filename}"
response.headers["Content-Type"] = "application/octet-stream"
return response

View File

@ -6,6 +6,7 @@ from controllers.console.wraps import setup_required
from controllers.inner_api import api
from controllers.inner_api.wraps import enterprise_inner_api_only
from events.tenant_event import tenant_was_created
from extensions.ext_database import db
from models.account import Account
from services.account_service import TenantService
@ -19,7 +20,7 @@ class EnterpriseWorkspace(Resource):
parser.add_argument("owner_email", type=str, required=True, location="json")
args = parser.parse_args()
account = Account.query.filter_by(email=args["owner_email"]).first()
account = db.session.query(Account).filter_by(email=args["owner_email"]).first()
if account is None:
return {"message": "owner account not found."}, 404

View File

@ -66,7 +66,7 @@ class DatasetMetadataServiceApi(DatasetApiResource):
metadata = MetadataService.update_metadata_name(dataset_id_str, metadata_id_str, args.get("name"))
return marshal(metadata, dataset_metadata_fields), 200
def delete(self, dataset_id, metadata_id):
def delete(self, tenant_id, dataset_id, metadata_id):
dataset_id_str = str(dataset_id)
metadata_id_str = str(metadata_id)
dataset = DatasetService.get_dataset(dataset_id_str)

View File

@ -1,3 +1,4 @@
from flask import request
from flask_login import current_user # type: ignore
from flask_restful import marshal, reqparse # type: ignore
from werkzeug.exceptions import NotFound
@ -74,6 +75,8 @@ class SegmentApi(DatasetApiResource):
# check dataset
dataset_id = str(dataset_id)
tenant_id = str(tenant_id)
page = request.args.get("page", default=1, type=int)
limit = request.args.get("limit", default=20, type=int)
dataset = db.session.query(Dataset).filter(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id).first()
if not dataset:
raise NotFound("Dataset not found.")
@ -118,8 +121,25 @@ class SegmentApi(DatasetApiResource):
query = query.where(DocumentSegment.content.ilike(f"%{keyword}%"))
total = query.count()
segments = query.order_by(DocumentSegment.position).all()
return {"data": marshal(segments, segment_fields), "doc_form": document.doc_form, "total": total}, 200
query = query.order_by(DocumentSegment.position)
paginated_segments = query.paginate(
page=page,
per_page=limit,
max_per_page=100,
error_out=False,
)
segments = paginated_segments.items
response = {
"data": marshal(segments, segment_fields),
"doc_form": document.doc_form,
"total": total,
"has_more": len(segments) == limit,
"limit": limit,
"page": page,
}
return response, 200
class DatasetSegmentApi(DatasetApiResource):

View File

@ -27,6 +27,9 @@ class RateLimit:
def __init__(self, client_id: str, max_active_requests: int):
self.max_active_requests = max_active_requests
# must be called after max_active_requests is set
if self.disabled():
return
if hasattr(self, "initialized"):
return
self.initialized = True
@ -37,6 +40,8 @@ class RateLimit:
self.flush_cache(use_local_value=True)
def flush_cache(self, use_local_value=False):
if self.disabled():
return
self.last_recalculate_time = time.time()
# flush max active requests
if use_local_value or not redis_client.exists(self.max_active_requests_key):
@ -59,18 +64,18 @@ class RateLimit:
redis_client.hdel(self.active_requests_key, *timeout_requests)
def enter(self, request_id: Optional[str] = None) -> str:
if self.disabled():
return RateLimit._UNLIMITED_REQUEST_ID
if time.time() - self.last_recalculate_time > RateLimit._ACTIVE_REQUESTS_COUNT_FLUSH_INTERVAL:
self.flush_cache()
if self.max_active_requests <= 0:
return RateLimit._UNLIMITED_REQUEST_ID
if not request_id:
request_id = RateLimit.gen_request_key()
active_requests_count = redis_client.hlen(self.active_requests_key)
if active_requests_count >= self.max_active_requests:
raise AppInvokeQuotaExceededError(
"Too many requests. Please try again later. The current maximum "
"concurrent requests allowed is {}.".format(self.max_active_requests)
f"Too many requests. Please try again later. The current maximum concurrent requests allowed "
f"for {self.client_id} is {self.max_active_requests}."
)
redis_client.hset(self.active_requests_key, request_id, str(time.time()))
return request_id
@ -80,6 +85,9 @@ class RateLimit:
return
redis_client.hdel(self.active_requests_key, request_id)
def disabled(self):
return self.max_active_requests <= 0
@staticmethod
def gen_request_key() -> str:
return str(uuid.uuid4())

View File

@ -49,6 +49,7 @@ class FileAttribute(StrEnum):
TRANSFER_METHOD = "transfer_method"
URL = "url"
EXTENSION = "extension"
RELATED_ID = "related_id"
class ArrayFileAttribute(StrEnum):

View File

@ -34,6 +34,8 @@ def get_attr(*, file: File, attr: FileAttribute):
return file.remote_url
case FileAttribute.EXTENSION:
return file.extension
case FileAttribute.RELATED_ID:
return file.related_id
def to_prompt_message_content(

View File

@ -10,7 +10,7 @@
- 支持 5 种模型类型的能力调用
- `LLM` - LLM 文本补全、对话,预计算 tokens 能力
- `Text Embedidng Model` - 文本 Embedding ,预计算 tokens 能力
- `Text Embedding Model` - 文本 Embedding ,预计算 tokens 能力
- `Rerank Model` - 分段 Rerank 能力
- `Speech-to-text Model` - 语音转文本能力
- `Text-to-speech Model` - 文本转语音能力

View File

@ -33,7 +33,6 @@ from core.ops.entities.trace_entity import (
)
from core.ops.langfuse_trace.langfuse_trace import LangFuseDataTrace
from core.ops.langsmith_trace.langsmith_trace import LangSmithDataTrace
from core.ops.opik_trace.opik_trace import OpikDataTrace
from core.ops.utils import get_message_data
from extensions.ext_database import db
from extensions.ext_storage import storage
@ -41,6 +40,13 @@ from models.model import App, AppModelConfig, Conversation, Message, MessageFile
from models.workflow import WorkflowAppLog, WorkflowRun
from tasks.ops_trace_task import process_trace_tasks
def build_opik_trace_instance(config: OpikConfig):
from core.ops.opik_trace.opik_trace import OpikDataTrace
return OpikDataTrace(config)
provider_config_map: dict[str, dict[str, Any]] = {
TracingProviderEnum.LANGFUSE.value: {
"config_class": LangfuseConfig,
@ -58,7 +64,7 @@ provider_config_map: dict[str, dict[str, Any]] = {
"config_class": OpikConfig,
"secret_keys": ["api_key"],
"other_keys": ["project", "url", "workspace"],
"trace_instance": OpikDataTrace,
"trace_instance": lambda config: build_opik_trace_instance(config),
},
}

View File

@ -97,6 +97,7 @@ class RetrievalService:
all_documents=all_documents,
retrieval_method=retrieval_method,
exceptions=exceptions,
document_ids_filter=document_ids_filter,
)
)
concurrent.futures.wait(futures, timeout=30, return_when=concurrent.futures.ALL_COMPLETED)
@ -222,6 +223,7 @@ class RetrievalService:
all_documents: list,
retrieval_method: str,
exceptions: list,
document_ids_filter: Optional[list[str]] = None,
):
with flask_app.app_context():
try:
@ -231,7 +233,9 @@ class RetrievalService:
vector_processor = Vector(dataset=dataset)
documents = vector_processor.search_by_full_text(cls.escape_query_for_search(query), top_k=top_k)
documents = vector_processor.search_by_full_text(
cls.escape_query_for_search(query), top_k=top_k, document_ids_filter=document_ids_filter
)
if documents:
if (
reranking_model

View File

@ -25,6 +25,7 @@ class OpenGaussConfig(BaseModel):
database: str
min_connection: int
max_connection: int
enable_pq: bool = False # Enable PQ acceleration
@model_validator(mode="before")
@classmethod
@ -57,6 +58,11 @@ CREATE TABLE IF NOT EXISTS {table_name} (
);
"""
SQL_CREATE_INDEX_PQ = """
CREATE INDEX IF NOT EXISTS embedding_{table_name}_pq_idx ON {table_name}
USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64, enable_pq=on, pq_m={pq_m});
"""
SQL_CREATE_INDEX = """
CREATE INDEX IF NOT EXISTS embedding_cosine_{table_name}_idx ON {table_name}
USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64);
@ -68,6 +74,7 @@ class OpenGauss(BaseVector):
super().__init__(collection_name)
self.pool = self._create_connection_pool(config)
self.table_name = f"embedding_{collection_name}"
self.pq_enabled = config.enable_pq
def get_type(self) -> str:
return VectorType.OPENGAUSS
@ -97,7 +104,26 @@ class OpenGauss(BaseVector):
def create(self, texts: list[Document], embeddings: list[list[float]], **kwargs):
dimension = len(embeddings[0])
self._create_collection(dimension)
return self.add_texts(texts, embeddings)
self.add_texts(texts, embeddings)
self._create_index(dimension)
def _create_index(self, dimension: int):
index_cache_key = f"vector_index_{self._collection_name}"
lock_name = f"{index_cache_key}_lock"
with redis_client.lock(lock_name, timeout=60):
index_exist_cache_key = f"vector_index_{self._collection_name}"
if redis_client.get(index_exist_cache_key):
return
with self._get_cursor() as cur:
if dimension <= 2000:
if self.pq_enabled:
cur.execute(SQL_CREATE_INDEX_PQ.format(table_name=self.table_name, pq_m=int(dimension / 4)))
cur.execute("SET hnsw_earlystop_threshold = 320")
if not self.pq_enabled:
cur.execute(SQL_CREATE_INDEX.format(table_name=self.table_name))
redis_client.set(index_exist_cache_key, 1, ex=3600)
def add_texts(self, documents: list[Document], embeddings: list[list[float]], **kwargs):
values = []
@ -211,8 +237,6 @@ class OpenGauss(BaseVector):
with self._get_cursor() as cur:
cur.execute(SQL_CREATE_TABLE.format(table_name=self.table_name, dimension=dimension))
if dimension <= 2000:
cur.execute(SQL_CREATE_INDEX.format(table_name=self.table_name))
redis_client.set(collection_exist_cache_key, 1, ex=3600)
@ -236,5 +260,6 @@ class OpenGaussFactory(AbstractVectorFactory):
database=dify_config.OPENGAUSS_DATABASE or "dify",
min_connection=dify_config.OPENGAUSS_MIN_CONNECTION,
max_connection=dify_config.OPENGAUSS_MAX_CONNECTION,
enable_pq=dify_config.OPENGAUSS_ENABLE_PQ or False,
),
)

View File

@ -610,7 +610,11 @@ class DatasetRetrieval:
if dataset.indexing_technique == "economy":
# use keyword table query
documents = RetrievalService.retrieve(
retrieval_method="keyword_search", dataset_id=dataset.id, query=query, top_k=top_k
retrieval_method="keyword_search",
dataset_id=dataset.id,
query=query,
top_k=top_k,
document_ids_filter=document_ids_filter,
)
if documents:
all_documents.extend(documents)

View File

@ -1,4 +1,4 @@
from ast import literal_eval
import json
from collections.abc import Generator, Mapping, Sequence
from typing import Any, cast
@ -143,15 +143,23 @@ class AgentNode(ToolNode):
raise ValueError(f"Variable {agent_input.value} does not exist")
parameter_value = variable.value
elif agent_input.type in {"mixed", "constant"}:
segment_group = variable_pool.convert_template(str(agent_input.value))
# variable_pool.convert_template expects a string template,
# but if passing a dict, convert to JSON string first before rendering
try:
parameter_value = json.dumps(agent_input.value, ensure_ascii=False)
except TypeError:
parameter_value = str(agent_input.value)
segment_group = variable_pool.convert_template(parameter_value)
parameter_value = segment_group.log if for_log else segment_group.text
# variable_pool.convert_template returns a string,
# so we need to convert it back to a dictionary
try:
parameter_value = json.loads(parameter_value)
except json.JSONDecodeError:
parameter_value = parameter_value
else:
raise ValueError(f"Unknown agent input type '{agent_input.type}'")
value = parameter_value.strip()
if (parameter_value.startswith("{") and parameter_value.endswith("}")) or (
parameter_value.startswith("[") and parameter_value.endswith("]")
):
value = literal_eval(parameter_value) # transform string to python object
value = parameter_value
if parameter.type == "array[tools]":
value = cast(list[dict[str, Any]], value)
value = [tool for tool in value if tool.get("enabled", False)]

View File

@ -4,6 +4,7 @@ from dify_app import DifyApp
def init_app(app: DifyApp):
from commands import (
add_qdrant_index,
clear_free_plan_tenant_expired_logs,
convert_to_agent_apps,
create_tenant,
extract_plugins,
@ -34,6 +35,7 @@ def init_app(app: DifyApp):
extract_unique_plugins,
install_plugins,
old_metadata_migration,
clear_free_plan_tenant_expired_logs,
]
for cmd in cmds_to_register:
app.cli.add_command(cmd)

View File

@ -24,6 +24,7 @@ vector_setting_fields = {
}
weighted_score_fields = {
"weight_type": fields.String,
"keyword_setting": fields.Nested(keyword_setting_fields),
"vector_setting": fields.Nested(vector_setting_fields),
}

View File

@ -910,7 +910,7 @@ class Embedding(db.Model): # type: ignore[name-defined]
self.embedding = pickle.dumps(embedding_data, protocol=pickle.HIGHEST_PROTOCOL)
def get_embedding(self) -> list[float]:
return cast(list[float], pickle.loads(self.embedding))
return cast(list[float], pickle.loads(self.embedding)) # noqa: S301
class DatasetCollectionBinding(db.Model): # type: ignore[name-defined]

View File

@ -838,6 +838,33 @@ class Conversation(db.Model): # type: ignore[name-defined]
def in_debug_mode(self):
return self.override_model_configs is not None
def to_dict(self):
return {
"id": self.id,
"app_id": self.app_id,
"app_model_config_id": self.app_model_config_id,
"model_provider": self.model_provider,
"override_model_configs": self.override_model_configs,
"model_id": self.model_id,
"mode": self.mode,
"name": self.name,
"summary": self.summary,
"inputs": self.inputs,
"introduction": self.introduction,
"system_instruction": self.system_instruction,
"system_instruction_tokens": self.system_instruction_tokens,
"status": self.status,
"invoke_from": self.invoke_from,
"from_source": self.from_source,
"from_end_user_id": self.from_end_user_id,
"from_account_id": self.from_account_id,
"read_at": self.read_at,
"read_account_id": self.read_account_id,
"dialogue_count": self.dialogue_count,
"created_at": self.created_at,
"updated_at": self.updated_at,
}
class Message(db.Model): # type: ignore[name-defined]
__tablename__ = "messages"

View File

@ -9,7 +9,6 @@ from flask_sqlalchemy.pagination import Pagination
from configs import dify_config
from constants.model_template import default_app_templates
from core.agent.entities import AgentToolEntity
from core.app.features.rate_limiting import RateLimit
from core.errors.error import LLMBadRequestError, ProviderTokenNotInitError
from core.model_manager import ModelManager
from core.model_runtime.entities.model_entities import ModelPropertyKey, ModelType
@ -37,9 +36,13 @@ class AppService:
filters = [App.tenant_id == tenant_id, App.is_universal == False]
if args["mode"] == "workflow":
filters.append(App.mode.in_([AppMode.WORKFLOW.value, AppMode.COMPLETION.value]))
filters.append(App.mode == AppMode.WORKFLOW.value)
elif args["mode"] == "completion":
filters.append(App.mode == AppMode.COMPLETION.value)
elif args["mode"] == "chat":
filters.append(App.mode.in_([AppMode.CHAT.value, AppMode.ADVANCED_CHAT.value]))
filters.append(App.mode == AppMode.CHAT.value)
elif args["mode"] == "advanced-chat":
filters.append(App.mode == AppMode.ADVANCED_CHAT.value)
elif args["mode"] == "agent-chat":
filters.append(App.mode == AppMode.AGENT_CHAT.value)
elif args["mode"] == "channel":
@ -222,7 +225,6 @@ class AppService:
"""
app.name = args.get("name")
app.description = args.get("description", "")
app.max_active_requests = args.get("max_active_requests")
app.icon_type = args.get("icon_type", "emoji")
app.icon = args.get("icon")
app.icon_background = args.get("icon_background")
@ -231,9 +233,6 @@ class AppService:
app.updated_at = datetime.now(UTC).replace(tzinfo=None)
db.session.commit()
if app.max_active_requests is not None:
rate_limit = RateLimit(app.id, app.max_active_requests)
rate_limit.flush_cache(use_local_value=True)
return app
def update_app_name(self, app: App, name: str) -> App:

View File

@ -0,0 +1,313 @@
import datetime
import json
import logging
import time
from concurrent.futures import ThreadPoolExecutor
import click
from flask import Flask, current_app
from sqlalchemy.orm import Session
from configs import dify_config
from core.model_runtime.utils.encoders import jsonable_encoder
from extensions.ext_database import db
from extensions.ext_storage import storage
from models.account import Tenant
from models.model import App, Conversation, Message
from models.workflow import WorkflowNodeExecution, WorkflowRun
from services.billing_service import BillingService
logger = logging.getLogger(__name__)
class ClearFreePlanTenantExpiredLogs:
@classmethod
def process_tenant(cls, flask_app: Flask, tenant_id: str, days: int, batch: int):
with flask_app.app_context():
apps = db.session.query(App).filter(App.tenant_id == tenant_id).all()
app_ids = [app.id for app in apps]
while True:
with Session(db.engine).no_autoflush as session:
messages = (
session.query(Message)
.filter(
Message.app_id.in_(app_ids),
Message.created_at < datetime.datetime.now() - datetime.timedelta(days=days),
)
.limit(batch)
.all()
)
if len(messages) == 0:
break
storage.save(
f"free_plan_tenant_expired_logs/"
f"{tenant_id}/messages/{datetime.datetime.now().strftime('%Y-%m-%d')}"
f"-{time.time()}.json",
json.dumps(
jsonable_encoder(
[message.to_dict() for message in messages],
),
).encode("utf-8"),
)
message_ids = [message.id for message in messages]
# delete messages
session.query(Message).filter(
Message.id.in_(message_ids),
).delete(synchronize_session=False)
session.commit()
click.echo(
click.style(
f"[{datetime.datetime.now()}] Processed {len(message_ids)} messages for tenant {tenant_id} "
)
)
while True:
with Session(db.engine).no_autoflush as session:
conversations = (
session.query(Conversation)
.filter(
Conversation.app_id.in_(app_ids),
Conversation.updated_at < datetime.datetime.now() - datetime.timedelta(days=days),
)
.limit(batch)
.all()
)
if len(conversations) == 0:
break
storage.save(
f"free_plan_tenant_expired_logs/"
f"{tenant_id}/conversations/{datetime.datetime.now().strftime('%Y-%m-%d')}"
f"-{time.time()}.json",
json.dumps(
jsonable_encoder(
[conversation.to_dict() for conversation in conversations],
),
).encode("utf-8"),
)
conversation_ids = [conversation.id for conversation in conversations]
session.query(Conversation).filter(
Conversation.id.in_(conversation_ids),
).delete(synchronize_session=False)
session.commit()
click.echo(
click.style(
f"[{datetime.datetime.now()}] Processed {len(conversation_ids)}"
f" conversations for tenant {tenant_id}"
)
)
while True:
with Session(db.engine).no_autoflush as session:
workflow_node_executions = (
session.query(WorkflowNodeExecution)
.filter(
WorkflowNodeExecution.tenant_id == tenant_id,
WorkflowNodeExecution.created_at < datetime.datetime.now() - datetime.timedelta(days=days),
)
.limit(batch)
.all()
)
if len(workflow_node_executions) == 0:
break
# save workflow node executions
storage.save(
f"free_plan_tenant_expired_logs/"
f"{tenant_id}/workflow_node_executions/{datetime.datetime.now().strftime('%Y-%m-%d')}"
f"-{time.time()}.json",
json.dumps(
jsonable_encoder(workflow_node_executions),
).encode("utf-8"),
)
workflow_node_execution_ids = [
workflow_node_execution.id for workflow_node_execution in workflow_node_executions
]
# delete workflow node executions
session.query(WorkflowNodeExecution).filter(
WorkflowNodeExecution.id.in_(workflow_node_execution_ids),
).delete(synchronize_session=False)
session.commit()
click.echo(
click.style(
f"[{datetime.datetime.now()}] Processed {len(workflow_node_execution_ids)}"
f" workflow node executions for tenant {tenant_id}"
)
)
while True:
with Session(db.engine).no_autoflush as session:
workflow_runs = (
session.query(WorkflowRun)
.filter(
WorkflowRun.tenant_id == tenant_id,
WorkflowRun.created_at < datetime.datetime.now() - datetime.timedelta(days=days),
)
.limit(batch)
.all()
)
if len(workflow_runs) == 0:
break
# save workflow runs
storage.save(
f"free_plan_tenant_expired_logs/"
f"{tenant_id}/workflow_runs/{datetime.datetime.now().strftime('%Y-%m-%d')}"
f"-{time.time()}.json",
json.dumps(
jsonable_encoder(
[workflow_run.to_dict() for workflow_run in workflow_runs],
),
).encode("utf-8"),
)
workflow_run_ids = [workflow_run.id for workflow_run in workflow_runs]
# delete workflow runs
session.query(WorkflowRun).filter(
WorkflowRun.id.in_(workflow_run_ids),
).delete(synchronize_session=False)
session.commit()
@classmethod
def process(cls, days: int, batch: int, tenant_ids: list[str]):
"""
Clear free plan tenant expired logs.
"""
click.echo(click.style("Clearing free plan tenant expired logs", fg="white"))
ended_at = datetime.datetime.now()
started_at = datetime.datetime(2023, 4, 3, 8, 59, 24)
current_time = started_at
with Session(db.engine) as session:
total_tenant_count = session.query(Tenant.id).count()
click.echo(click.style(f"Total tenant count: {total_tenant_count}", fg="white"))
handled_tenant_count = 0
thread_pool = ThreadPoolExecutor(max_workers=10)
def process_tenant(flask_app: Flask, tenant_id: str) -> None:
try:
if (
not dify_config.BILLING_ENABLED
or BillingService.get_info(tenant_id)["subscription"]["plan"] == "sandbox"
):
# only process sandbox tenant
cls.process_tenant(flask_app, tenant_id, days, batch)
except Exception:
logger.exception(f"Failed to process tenant {tenant_id}")
finally:
nonlocal handled_tenant_count
handled_tenant_count += 1
if handled_tenant_count % 100 == 0:
click.echo(
click.style(
f"[{datetime.datetime.now()}] "
f"Processed {handled_tenant_count} tenants "
f"({(handled_tenant_count / total_tenant_count) * 100:.1f}%), "
f"{handled_tenant_count}/{total_tenant_count}",
fg="green",
)
)
futures = []
if tenant_ids:
for tenant_id in tenant_ids:
futures.append(
thread_pool.submit(
process_tenant,
current_app._get_current_object(), # type: ignore[attr-defined]
tenant_id,
)
)
else:
while current_time < ended_at:
click.echo(
click.style(f"Current time: {current_time}, Started at: {datetime.datetime.now()}", fg="white")
)
# Initial interval of 1 day, will be dynamically adjusted based on tenant count
interval = datetime.timedelta(days=1)
# Process tenants in this batch
with Session(db.engine) as session:
# Calculate tenant count in next batch with current interval
# Try different intervals until we find one with a reasonable tenant count
test_intervals = [
datetime.timedelta(days=1),
datetime.timedelta(hours=12),
datetime.timedelta(hours=6),
datetime.timedelta(hours=3),
datetime.timedelta(hours=1),
]
for test_interval in test_intervals:
tenant_count = (
session.query(Tenant.id)
.filter(Tenant.created_at.between(current_time, current_time + test_interval))
.count()
)
if tenant_count <= 100:
interval = test_interval
break
else:
# If all intervals have too many tenants, use minimum interval
interval = datetime.timedelta(hours=1)
# Adjust interval to target ~100 tenants per batch
if tenant_count > 0:
# Scale interval based on ratio to target count
interval = min(
datetime.timedelta(days=1), # Max 1 day
max(
datetime.timedelta(hours=1), # Min 1 hour
interval * (100 / tenant_count), # Scale to target 100
),
)
batch_end = min(current_time + interval, ended_at)
rs = (
session.query(Tenant.id)
.filter(Tenant.created_at.between(current_time, batch_end))
.order_by(Tenant.created_at)
)
tenants = []
for row in rs:
tenant_id = str(row.id)
try:
tenants.append(tenant_id)
except Exception:
logger.exception(f"Failed to process tenant {tenant_id}")
continue
futures.append(
thread_pool.submit(
process_tenant,
current_app._get_current_object(), # type: ignore[attr-defined]
tenant_id,
)
)
current_time = batch_end
# wait for all threads to finish
for future in futures:
future.result()

View File

@ -68,7 +68,7 @@ DEBUG=false
# which is convenient for debugging.
FLASK_DEBUG=false
# A secretkey that is used for securely signing the session cookie
# A secret key that is used for securely signing the session cookie
# and encrypting sensitive information on the database.
# You can generate a strong key using `openssl rand -base64 42`.
SECRET_KEY=sk-9f73s3ljTXVcMT3Blb3ljTqtsKiGHXVcMT3BlbkFJLK7U
@ -76,7 +76,7 @@ SECRET_KEY=sk-9f73s3ljTXVcMT3Blb3ljTqtsKiGHXVcMT3BlbkFJLK7U
# Password for admin user initialization.
# If left unset, admin user will not be prompted for a password
# when creating the initial admin account.
# The length of the password cannot exceed 30 charactors.
# The length of the password cannot exceed 30 characters.
INIT_PASSWORD=
# Deployment environment.
@ -563,6 +563,7 @@ OPENGAUSS_PASSWORD=Dify@123
OPENGAUSS_DATABASE=dify
OPENGAUSS_MIN_CONNECTION=1
OPENGAUSS_MAX_CONNECTION=5
OPENGAUSS_ENABLE_PQ=false
# Upstash Vector configuration, only available when VECTOR_STORE is `upstash`
UPSTASH_VECTOR_URL=https://xxx-vector.upstash.io

View File

@ -27,7 +27,7 @@ Welcome to the new `docker` directory for deploying Dify using Docker Compose. T
- Execute `docker compose up` from the `docker` directory to start the services.
- To specify a vector database, set the `VECTOR_STORE` variable in your `.env` file to your desired vector database service, such as `milvus`, `weaviate`, or `opensearch`.
4. **SSL Certificate Setup**:
- Rrefer `docker/certbot/README.md` to set up SSL certificates using Certbot.
- Refer `docker/certbot/README.md` to set up SSL certificates using Certbot.
### How to Deploy Middleware for Developing Dify
@ -54,7 +54,7 @@ For users migrating from the `docker-legacy` setup:
- **Vector Database Services**: Depending on the type of vector database used (`VECTOR_STORE`), users can set specific endpoints, ports, and authentication details.
- **Storage Services**: Depending on the storage type (`STORAGE_TYPE`), users can configure specific settings for S3, Azure Blob, Google Storage, etc.
- **API and Web Services**: Users can define URLs and other settings that affect how the API and web frontends operate.
- **API and Web Services**: Users can define URLs and other settings that affect how the API and web frontend operate.
#### Other notable variables

View File

@ -2,7 +2,7 @@ x-shared-env: &shared-api-worker-env
services:
# API service
api:
image: langgenius/dify-api:1.1.1
image: langgenius/dify-api:1.1.3
restart: always
environment:
# Use the shared environment variables.
@ -29,7 +29,7 @@ services:
# worker service
# The Celery worker for processing the queue.
worker:
image: langgenius/dify-api:1.1.1
image: langgenius/dify-api:1.1.3
restart: always
environment:
# Use the shared environment variables.
@ -53,7 +53,7 @@ services:
# Frontend web application.
web:
image: langgenius/dify-web:1.1.1
image: langgenius/dify-web:1.1.3
restart: always
environment:
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
@ -110,7 +110,7 @@ services:
# The DifySandbox
sandbox:
image: langgenius/dify-sandbox:0.2.10
image: langgenius/dify-sandbox:0.2.11
restart: always
environment:
# The DifySandbox configurations

View File

@ -43,7 +43,7 @@ services:
# The DifySandbox
sandbox:
image: langgenius/dify-sandbox:0.2.10
image: langgenius/dify-sandbox:0.2.11
restart: always
environment:
# The DifySandbox configurations

View File

@ -259,6 +259,7 @@ x-shared-env: &shared-api-worker-env
OPENGAUSS_DATABASE: ${OPENGAUSS_DATABASE:-dify}
OPENGAUSS_MIN_CONNECTION: ${OPENGAUSS_MIN_CONNECTION:-1}
OPENGAUSS_MAX_CONNECTION: ${OPENGAUSS_MAX_CONNECTION:-5}
OPENGAUSS_ENABLE_PQ: ${OPENGAUSS_ENABLE_PQ:-false}
UPSTASH_VECTOR_URL: ${UPSTASH_VECTOR_URL:-https://xxx-vector.upstash.io}
UPSTASH_VECTOR_TOKEN: ${UPSTASH_VECTOR_TOKEN:-dify}
UPLOAD_FILE_SIZE_LIMIT: ${UPLOAD_FILE_SIZE_LIMIT:-15}
@ -432,7 +433,7 @@ x-shared-env: &shared-api-worker-env
services:
# API service
api:
image: langgenius/dify-api:1.1.1
image: langgenius/dify-api:1.1.3
restart: always
environment:
# Use the shared environment variables.
@ -459,7 +460,7 @@ services:
# worker service
# The Celery worker for processing the queue.
worker:
image: langgenius/dify-api:1.1.1
image: langgenius/dify-api:1.1.3
restart: always
environment:
# Use the shared environment variables.
@ -483,7 +484,7 @@ services:
# Frontend web application.
web:
image: langgenius/dify-web:1.1.1
image: langgenius/dify-web:1.1.3
restart: always
environment:
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
@ -540,7 +541,7 @@ services:
# The DifySandbox
sandbox:
image: langgenius/dify-sandbox:0.2.10
image: langgenius/dify-sandbox:0.2.11
restart: always
environment:
# The DifySandbox configurations

View File

@ -3,7 +3,7 @@ import Main from '@/app/components/app/log-annotation'
import { PageType } from '@/app/components/base/features/new-feature-panel/annotation-reply/type'
export type IProps = {
params: { appId: string }
params: Promise<{ appId: string }>
}
const Logs = async () => {

View File

@ -3,12 +3,16 @@ import type { Locale } from '@/i18n'
import DevelopMain from '@/app/components/develop'
export type IDevelopProps = {
params: { locale: Locale; appId: string }
params: Promise<{ locale: Locale; appId: string }>
}
const Develop = async ({
params: { appId },
}: IDevelopProps) => {
const Develop = async (props: IDevelopProps) => {
const params = await props.params
const {
appId,
} = params
return <DevelopMain appId={appId} />
}

View File

@ -0,0 +1,177 @@
'use client'
import type { FC } from 'react'
import { useUnmount } from 'ahooks'
import React, { useCallback, useEffect, useState } from 'react'
import { usePathname, useRouter } from 'next/navigation'
import {
RiDashboard2Fill,
RiDashboard2Line,
RiFileList3Fill,
RiFileList3Line,
RiTerminalBoxFill,
RiTerminalBoxLine,
RiTerminalWindowFill,
RiTerminalWindowLine,
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useShallow } from 'zustand/react/shallow'
import { useContextSelector } from 'use-context-selector'
import s from './style.module.css'
import cn from '@/utils/classnames'
import { useStore } from '@/app/components/app/store'
import AppSideBar from '@/app/components/app-sidebar'
import type { NavIcon } from '@/app/components/app-sidebar/navLink'
import { fetchAppDetail, fetchAppSSO } from '@/service/apps'
import AppContext, { useAppContext } from '@/context/app-context'
import Loading from '@/app/components/base/loading'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import type { App } from '@/types/app'
export type IAppDetailLayoutProps = {
children: React.ReactNode
appId: string
}
const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
const {
children,
appId, // get appId in path
} = props
const { t } = useTranslation()
const router = useRouter()
const pathname = usePathname()
const media = useBreakpoints()
const isMobile = media === MediaType.mobile
const { isCurrentWorkspaceEditor, isLoadingCurrentWorkspace } = useAppContext()
const { appDetail, setAppDetail, setAppSiderbarExpand } = useStore(useShallow(state => ({
appDetail: state.appDetail,
setAppDetail: state.setAppDetail,
setAppSiderbarExpand: state.setAppSiderbarExpand,
})))
const [isLoadingAppDetail, setIsLoadingAppDetail] = useState(false)
const [appDetailRes, setAppDetailRes] = useState<App | null>(null)
const [navigation, setNavigation] = useState<Array<{
name: string
href: string
icon: NavIcon
selectedIcon: NavIcon
}>>([])
const systemFeatures = useContextSelector(AppContext, state => state.systemFeatures)
const getNavigations = useCallback((appId: string, isCurrentWorkspaceEditor: boolean, mode: string) => {
const navs = [
...(isCurrentWorkspaceEditor
? [{
name: t('common.appMenus.promptEng'),
href: `/app/${appId}/${(mode === 'workflow' || mode === 'advanced-chat') ? 'workflow' : 'configuration'}`,
icon: RiTerminalWindowLine,
selectedIcon: RiTerminalWindowFill,
}]
: []
),
{
name: t('common.appMenus.apiAccess'),
href: `/app/${appId}/develop`,
icon: RiTerminalBoxLine,
selectedIcon: RiTerminalBoxFill,
},
...(isCurrentWorkspaceEditor
? [{
name: mode !== 'workflow'
? t('common.appMenus.logAndAnn')
: t('common.appMenus.logs'),
href: `/app/${appId}/logs`,
icon: RiFileList3Line,
selectedIcon: RiFileList3Fill,
}]
: []
),
{
name: t('common.appMenus.overview'),
href: `/app/${appId}/overview`,
icon: RiDashboard2Line,
selectedIcon: RiDashboard2Fill,
},
]
return navs
}, [])
useEffect(() => {
if (appDetail) {
document.title = `${(appDetail.name || 'App')} - Dify`
const localeMode = localStorage.getItem('app-detail-collapse-or-expand') || 'expand'
const mode = isMobile ? 'collapse' : 'expand'
setAppSiderbarExpand(isMobile ? mode : localeMode)
// TODO: consider screen size and mode
// if ((appDetail.mode === 'advanced-chat' || appDetail.mode === 'workflow') && (pathname).endsWith('workflow'))
// setAppSiderbarExpand('collapse')
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [appDetail, isMobile])
useEffect(() => {
setAppDetail()
setIsLoadingAppDetail(true)
fetchAppDetail({ url: '/apps', id: appId }).then((res) => {
setAppDetailRes(res)
}).catch((e: any) => {
if (e.status === 404)
router.replace('/apps')
}).finally(() => {
setIsLoadingAppDetail(false)
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [appId, pathname])
useEffect(() => {
if (!appDetailRes || isLoadingCurrentWorkspace || isLoadingAppDetail)
return
const res = appDetailRes
// redirection
const canIEditApp = isCurrentWorkspaceEditor
if (!canIEditApp && (pathname.endsWith('configuration') || pathname.endsWith('workflow') || pathname.endsWith('logs'))) {
router.replace(`/app/${appId}/overview`)
return
}
if ((res.mode === 'workflow' || res.mode === 'advanced-chat') && (pathname).endsWith('configuration')) {
router.replace(`/app/${appId}/workflow`)
}
else if ((res.mode !== 'workflow' && res.mode !== 'advanced-chat') && (pathname).endsWith('workflow')) {
router.replace(`/app/${appId}/configuration`)
}
else {
setAppDetail({ ...res, enable_sso: false })
setNavigation(getNavigations(appId, isCurrentWorkspaceEditor, res.mode))
if (systemFeatures.enable_web_sso_switch_component && canIEditApp) {
fetchAppSSO({ appId }).then((ssoRes) => {
setAppDetail({ ...res, enable_sso: ssoRes.enabled })
})
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [appDetailRes, isCurrentWorkspaceEditor, isLoadingAppDetail, isLoadingCurrentWorkspace, systemFeatures.enable_web_sso_switch_component])
useUnmount(() => {
setAppDetail()
})
if (!appDetail) {
return (
<div className='flex h-full items-center justify-center bg-background-body'>
<Loading />
</div>
)
}
return (
<div className={cn(s.app, 'relative flex', 'overflow-hidden')}>
{appDetail && (
<AppSideBar title={appDetail.name} icon={appDetail.icon} icon_background={appDetail.icon_background as string} desc={appDetail.mode} navigation={navigation} />
)}
<div className="grow overflow-hidden bg-components-panel-bg">
{children}
</div>
</div>
)
}
export default React.memo(AppDetailLayout)

View File

@ -1,177 +1,14 @@
'use client'
import type { FC } from 'react'
import { useUnmount } from 'ahooks'
import React, { useCallback, useEffect, useState } from 'react'
import { usePathname, useRouter } from 'next/navigation'
import {
RiDashboard2Fill,
RiDashboard2Line,
RiFileList3Fill,
RiFileList3Line,
RiTerminalBoxFill,
RiTerminalBoxLine,
RiTerminalWindowFill,
RiTerminalWindowLine,
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useShallow } from 'zustand/react/shallow'
import { useContextSelector } from 'use-context-selector'
import s from './style.module.css'
import cn from '@/utils/classnames'
import { useStore } from '@/app/components/app/store'
import AppSideBar from '@/app/components/app-sidebar'
import type { NavIcon } from '@/app/components/app-sidebar/navLink'
import { fetchAppDetail, fetchAppSSO } from '@/service/apps'
import AppContext, { useAppContext } from '@/context/app-context'
import Loading from '@/app/components/base/loading'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import type { App } from '@/types/app'
import Main from './layout-main'
export type IAppDetailLayoutProps = {
const AppDetailLayout = async (props: {
children: React.ReactNode
params: { appId: string }
}
const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
params: Promise<{ appId: string }>
}) => {
const {
children,
params: { appId }, // get appId in path
params,
} = props
const { t } = useTranslation()
const router = useRouter()
const pathname = usePathname()
const media = useBreakpoints()
const isMobile = media === MediaType.mobile
const { isCurrentWorkspaceEditor, isLoadingCurrentWorkspace } = useAppContext()
const { appDetail, setAppDetail, setAppSiderbarExpand } = useStore(useShallow(state => ({
appDetail: state.appDetail,
setAppDetail: state.setAppDetail,
setAppSiderbarExpand: state.setAppSiderbarExpand,
})))
const [isLoadingAppDetail, setIsLoadingAppDetail] = useState(false)
const [appDetailRes, setAppDetailRes] = useState<App | null>(null)
const [navigation, setNavigation] = useState<Array<{
name: string
href: string
icon: NavIcon
selectedIcon: NavIcon
}>>([])
const systemFeatures = useContextSelector(AppContext, state => state.systemFeatures)
const getNavigations = useCallback((appId: string, isCurrentWorkspaceEditor: boolean, mode: string) => {
const navs = [
...(isCurrentWorkspaceEditor
? [{
name: t('common.appMenus.promptEng'),
href: `/app/${appId}/${(mode === 'workflow' || mode === 'advanced-chat') ? 'workflow' : 'configuration'}`,
icon: RiTerminalWindowLine,
selectedIcon: RiTerminalWindowFill,
}]
: []
),
{
name: t('common.appMenus.apiAccess'),
href: `/app/${appId}/develop`,
icon: RiTerminalBoxLine,
selectedIcon: RiTerminalBoxFill,
},
...(isCurrentWorkspaceEditor
? [{
name: mode !== 'workflow'
? t('common.appMenus.logAndAnn')
: t('common.appMenus.logs'),
href: `/app/${appId}/logs`,
icon: RiFileList3Line,
selectedIcon: RiFileList3Fill,
}]
: []
),
{
name: t('common.appMenus.overview'),
href: `/app/${appId}/overview`,
icon: RiDashboard2Line,
selectedIcon: RiDashboard2Fill,
},
]
return navs
}, [])
useEffect(() => {
if (appDetail) {
document.title = `${(appDetail.name || 'App')} - Dify`
const localeMode = localStorage.getItem('app-detail-collapse-or-expand') || 'expand'
const mode = isMobile ? 'collapse' : 'expand'
setAppSiderbarExpand(isMobile ? mode : localeMode)
// TODO: consider screen size and mode
// if ((appDetail.mode === 'advanced-chat' || appDetail.mode === 'workflow') && (pathname).endsWith('workflow'))
// setAppSiderbarExpand('collapse')
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [appDetail, isMobile])
useEffect(() => {
setAppDetail()
setIsLoadingAppDetail(true)
fetchAppDetail({ url: '/apps', id: appId }).then((res) => {
setAppDetailRes(res)
}).catch((e: any) => {
if (e.status === 404)
router.replace('/apps')
}).finally(() => {
setIsLoadingAppDetail(false)
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [appId, pathname])
useEffect(() => {
if (!appDetailRes || isLoadingCurrentWorkspace || isLoadingAppDetail)
return
const res = appDetailRes
// redirection
const canIEditApp = isCurrentWorkspaceEditor
if (!canIEditApp && (pathname.endsWith('configuration') || pathname.endsWith('workflow') || pathname.endsWith('logs'))) {
router.replace(`/app/${appId}/overview`)
return
}
if ((res.mode === 'workflow' || res.mode === 'advanced-chat') && (pathname).endsWith('configuration')) {
router.replace(`/app/${appId}/workflow`)
}
else if ((res.mode !== 'workflow' && res.mode !== 'advanced-chat') && (pathname).endsWith('workflow')) {
router.replace(`/app/${appId}/configuration`)
}
else {
setAppDetail({ ...res, enable_sso: false })
setNavigation(getNavigations(appId, isCurrentWorkspaceEditor, res.mode))
if (systemFeatures.enable_web_sso_switch_component && canIEditApp) {
fetchAppSSO({ appId }).then((ssoRes) => {
setAppDetail({ ...res, enable_sso: ssoRes.enabled })
})
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [appDetailRes, isCurrentWorkspaceEditor, isLoadingAppDetail, isLoadingCurrentWorkspace, systemFeatures.enable_web_sso_switch_component])
useUnmount(() => {
setAppDetail()
})
if (!appDetail) {
return (
<div className='flex h-full items-center justify-center bg-background-body'>
<Loading />
</div>
)
}
return (
<div className={cn(s.app, 'flex relative', 'overflow-hidden')}>
{appDetail && (
<AppSideBar title={appDetail.name} icon={appDetail.icon} icon_background={appDetail.icon_background as string} desc={appDetail.mode} navigation={navigation} />
)}
<div className="bg-components-panel-bg grow overflow-hidden">
{children}
</div>
</div>
)
return <Main appId={(await params).appId}>{children}</Main>
}
export default React.memo(AppDetailLayout)
export default AppDetailLayout

View File

@ -122,7 +122,7 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
return <Loading />
return (
<div className={className || 'grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6'}>
<div className={className || 'mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2'}>
<AppCard
appInfo={appDetail}
cardType="webapp"

View File

@ -46,7 +46,7 @@ export default function ChartView({ appId }: IChartViewProps) {
return (
<div>
<div className='flex flex-row items-center mt-8 mb-4 system-xl-semibold text-text-primary'>
<div className='system-xl-semibold mb-4 mt-8 flex flex-row items-center text-text-primary'>
<span className='mr-3'>{t('appOverview.analysis.title')}</span>
<SimpleSelect
items={Object.entries(TIME_PERIOD_MAPPING).map(([k, v]) => ({ value: k, name: t(`appLog.filter.period.${v.name}`) }))}
@ -61,13 +61,13 @@ export default function ChartView({ appId }: IChartViewProps) {
/>
</div>
{!isWorkflow && (
<div className='grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6'>
<div className='mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2'>
<ConversationsChart period={period} id={appId} />
<EndUsersChart period={period} id={appId} />
</div>
)}
{!isWorkflow && (
<div className='grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6'>
<div className='mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2'>
{isChatApp
? (
<AvgSessionInteractions period={period} id={appId} />
@ -79,24 +79,24 @@ export default function ChartView({ appId }: IChartViewProps) {
</div>
)}
{!isWorkflow && (
<div className='grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6'>
<div className='mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2'>
<UserSatisfactionRate period={period} id={appId} />
<CostChart period={period} id={appId} />
</div>
)}
{!isWorkflow && isChatApp && (
<div className='grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6'>
<div className='mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2'>
<MessagesChart period={period} id={appId} />
</div>
)}
{isWorkflow && (
<div className='grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6'>
<div className='mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2'>
<WorkflowMessagesChart period={period} id={appId} />
<WorkflowDailyTerminalsChart period={period} id={appId} />
</div>
)}
{isWorkflow && (
<div className='grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6'>
<div className='mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2'>
<WorkflowCostChart period={period} id={appId} />
<AvgUserInteractions period={period} id={appId} />
</div>

View File

@ -5,14 +5,18 @@ import TracingPanel from './tracing/panel'
import ApikeyInfoPanel from '@/app/components/app/overview/apikey-info-panel'
export type IDevelopProps = {
params: { appId: string }
params: Promise<{ appId: string }>
}
const Overview = async ({
params: { appId },
}: IDevelopProps) => {
const Overview = async (props: IDevelopProps) => {
const params = await props.params
const {
appId,
} = params
return (
<div className="h-full px-4 sm:px-12 py-6 overflow-scroll bg-chatbot-bg">
<div className="h-full overflow-scroll bg-chatbot-bg px-4 py-6 sm:px-12">
<ApikeyInfoPanel />
<TracingPanel />
<CardView appId={appId} />

View File

@ -58,8 +58,8 @@ const ConfigBtn: FC<Props> = ({
}}
>
<PortalToFollowElemTrigger onClick={handleTrigger}>
<div className={cn(className, 'p-1 rounded-md')}>
<RiEqualizer2Line className='w-4 h-4 text-text-tertiary' />
<div className={cn(className, 'rounded-md p-1')}>
<RiEqualizer2Line className='h-4 w-4 text-text-tertiary' />
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[11]'>

View File

@ -162,15 +162,15 @@ const ConfigPopup: FC<PopupProps> = ({
}
return (
<div className='w-[420px] p-4 rounded-2xl bg-components-panel-bg border-[0.5px] border-components-panel-border shadow-xl'>
<div className='flex justify-between items-center'>
<div className='w-[420px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-4 shadow-xl'>
<div className='flex items-center justify-between'>
<div className='flex items-center'>
<TracingIcon size='md' className='mr-2' />
<div className='text-text-primary title-2xl-semi-bold'>{t(`${I18N_PREFIX}.tracing`)}</div>
<div className='title-2xl-semi-bold text-text-primary'>{t(`${I18N_PREFIX}.tracing`)}</div>
</div>
<div className='flex items-center'>
<Indicator color={enabled ? 'green' : 'gray'} />
<div className={cn('ml-1 system-xs-semibold-uppercase text-text-tertiary', enabled && 'text-util-colors-green-green-600')}>
<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'}`)}
</div>
{!readOnly && (
@ -189,7 +189,7 @@ const ConfigPopup: FC<PopupProps> = ({
</div>
</div>
<div className='mt-2 system-xs-regular text-text-tertiary'>
<div className='system-xs-regular mt-2 text-text-tertiary'>
{t(`${I18N_PREFIX}.tracingDescription`)}
</div>
<Divider className='my-3' />
@ -211,7 +211,7 @@ const ConfigPopup: FC<PopupProps> = ({
<div className='mt-2 space-y-2'>
{configuredProviderPanel()}
</div>
<div className='mt-3 system-xs-medium-uppercase 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`)}</div>
<div className='mt-2 space-y-2'>
{moreProviderPanel()}
</div>

View File

@ -26,7 +26,7 @@ const Field: FC<Props> = ({
return (
<div className={cn(className)}>
<div className='flex py-[7px]'>
<div className={cn(labelClassName, 'flex items-center h-[18px] text-[13px] font-medium text-text-primary')}>{label} </div>
<div className={cn(labelClassName, 'flex h-[18px] items-center text-[13px] font-medium text-text-primary')}>{label} </div>
{isRequired && <span className='ml-0.5 text-xs font-semibold text-[#D92D20]'>*</span>}
</div>
<Input

View File

@ -31,7 +31,7 @@ const Title = ({
const { t } = useTranslation()
return (
<div className={cn('flex items-center system-xl-semibold text-text-primary', className)}>
<div className={cn('system-xl-semibold flex items-center text-text-primary', className)}>
{t('common.appMenus.overview')}
</div>
)
@ -143,7 +143,7 @@ const Panel: FC = () => {
}, [setControlShowPopup])
if (!isLoaded) {
return (
<div className='flex items-center justify-between mb-3'>
<div className='mb-3 flex items-center justify-between'>
<Title className='h-[41px]' />
<div className='w-[200px]'>
<Loading />
@ -153,19 +153,19 @@ const Panel: FC = () => {
}
return (
<div className={cn('mb-3 flex justify-between items-center')}>
<div className={cn('mb-3 flex items-center justify-between')}>
<Title className='h-[41px]' />
<div
className={cn(
'flex items-center p-2 rounded-xl bg-background-default-dodge border-t border-l-[0.5px] border-effects-highlight shadow-xs cursor-pointer hover:bg-background-default-lighter hover:border-effects-highlight-lightmode-off',
controlShowPopup && 'bg-background-default-lighter border-effects-highlight-lightmode-off',
'flex cursor-pointer items-center rounded-xl border-l-[0.5px] border-t border-effects-highlight bg-background-default-dodge p-2 shadow-xs hover:border-effects-highlight-lightmode-off hover:bg-background-default-lighter',
controlShowPopup && 'border-effects-highlight-lightmode-off bg-background-default-lighter',
)}
onClick={showPopup}
>
{!inUseTracingProvider && (
<>
<TracingIcon size='md' />
<div className='mx-2 system-sm-semibold text-text-secondary'>{t(`${I18N_PREFIX}.title`)}</div>
<div className='system-sm-semibold mx-2 text-text-secondary'>{t(`${I18N_PREFIX}.title`)}</div>
<div className='flex items-center' onClick={e => e.stopPropagation()}>
<ConfigButton
appId={appId}
@ -184,8 +184,8 @@ const Panel: FC = () => {
/>
</div>
<Divider type='vertical' className='h-3.5' />
<div className='p-1 rounded-md'>
<RiArrowDownDoubleLine className='w-4 h-4 text-text-tertiary' />
<div className='rounded-md p-1'>
<RiArrowDownDoubleLine className='h-4 w-4 text-text-tertiary' />
</div>
</>
)}
@ -193,7 +193,7 @@ const Panel: FC = () => {
<>
<div className='ml-4 mr-1 flex items-center'>
<Indicator color={enabled ? 'green' : 'gray'} />
<div className='ml-1.5 system-xs-semibold-uppercase text-text-tertiary'>
<div className='system-xs-semibold-uppercase ml-1.5 text-text-tertiary'>
{t(`${I18N_PREFIX}.${enabled ? 'enabled' : 'disabled'}`)}
</div>
</div>

View File

@ -167,11 +167,11 @@ const ProviderConfigModal: FC<Props> = ({
{!isShowRemoveConfirm
? (
<PortalToFollowElem open>
<PortalToFollowElemContent className='w-full h-full z-[60]'>
<PortalToFollowElemContent className='z-[60] h-full w-full'>
<div className='fixed inset-0 flex items-center justify-center bg-background-overlay'>
<div className='mx-2 w-[640px] max-h-[calc(100vh-120px)] bg-components-panel-bg shadow-xl rounded-2xl overflow-y-auto'>
<div className='mx-2 max-h-[calc(100vh-120px)] w-[640px] overflow-y-auto rounded-2xl bg-components-panel-bg shadow-xl'>
<div className='px-8 pt-8'>
<div className='flex justify-between items-center mb-4'>
<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`)}</div>
</div>
@ -265,14 +265,14 @@ const ProviderConfigModal: FC<Props> = ({
)}
</div>
<div className='my-8 flex justify-between items-center h-8'>
<div className='my-8 flex h-8 items-center justify-between'>
<a
className='flex items-center space-x-1 leading-[18px] text-xs font-normal text-[#155EEF]'
className='flex items-center space-x-1 text-xs font-normal leading-[18px] text-[#155EEF]'
target='_blank'
href={docURL[type]}
>
<span>{t(`${I18N_PREFIX}.viewDocsLink`, { key: t(`app.tracing.${type}.title`) })}</span>
<LinkExternal02 className='w-3 h-3' />
<LinkExternal02 className='h-3 w-3' />
</a>
<div className='flex items-center'>
{isEdit && (
@ -305,11 +305,11 @@ const ProviderConfigModal: FC<Props> = ({
</div>
</div>
<div className='border-t-[0.5px] border-divider-regular'>
<div className='flex justify-center items-center py-3 bg-background-section-burn text-xs text-text-tertiary'>
<Lock01 className='mr-1 w-3 h-3 text-text-tertiary' />
<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')}
<a
className='text-primary-600 mx-1'
className='mx-1 text-primary-600'
target='_blank' rel='noopener noreferrer'
href='https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html'
>

View File

@ -65,36 +65,36 @@ const ProviderPanel: FC<Props> = ({
return (
<div
className={cn(
'px-4 py-3 rounded-xl border-[1.5px] bg-background-section-burn',
isChosen ? 'bg-background-section border-components-option-card-option-selected-border' : 'border-transparent',
'rounded-xl border-[1.5px] bg-background-section-burn px-4 py-3',
isChosen ? 'border-components-option-card-option-selected-border bg-background-section' : 'border-transparent',
!isChosen && hasConfigured && !readOnly && 'cursor-pointer',
)}
onClick={handleChosen}
>
<div className={'flex justify-between items-center space-x-1'}>
<div className={'flex items-center justify-between space-x-1'}>
<div className='flex items-center'>
<Icon className='h-6' />
{isChosen && <div className='ml-1 flex items-center h-4 px-1 rounded-[4px] border border-text-accent-secondary system-2xs-medium-uppercase 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`)}</div>}
</div>
{!readOnly && (
<div className={'flex justify-between items-center space-x-1'}>
<div className={'flex items-center justify-between space-x-1'}>
{hasConfigured && (
<div className='flex px-2 items-center h-6 bg-components-button-secondary-bg rounded-md border-[0.5px] border-components-button-secondary-border shadow-xs cursor-pointer text-text-secondary space-x-1' onClick={viewBtnClick} >
<View className='w-3 h-3' />
<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>
)}
<div
className='flex px-2 items-center h-6 bg-components-button-secondary-bg rounded-md border-[0.5px] border-components-button-secondary-border shadow-xs cursor-pointer text-text-secondary space-x-1'
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={handleConfigBtnClick}
>
<RiEqualizer2Line className='w-3 h-3' />
<RiEqualizer2Line className='h-3 w-3' />
<div className='text-xs font-medium'>{t(`${I18N_PREFIX}.config`)}</div>
</div>
</div>
)}
</div>
<div className='mt-2 system-xs-regular text-text-tertiary'>
<div className='system-xs-regular mt-2 text-text-tertiary'>
{t(`${I18N_PREFIX}.${type}.description`)}
</div>
</div>

View File

@ -21,7 +21,7 @@ const TracingIcon: FC<Props> = ({
const sizeClass = sizeClassMap[size]
return (
<div className={cn(className, sizeClass, 'bg-primary-500 shadow-md')}>
<Icon className='w-full h-full' />
<Icon className='h-full w-full' />
</div>
)
}

View File

@ -4,7 +4,7 @@ import Workflow from '@/app/components/workflow'
const Page = () => {
return (
<div className='w-full h-full overflow-x-auto'>
<div className='h-full w-full overflow-x-auto'>
<Workflow />
</div>
)

View File

@ -226,37 +226,37 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
}
return (
<div className="relative w-full py-1" onMouseLeave={onMouseLeave}>
<button className='h-8 w-[calc(100%_-_8px)] py-[6px] px-3 mx-1 flex items-center gap-2 hover:bg-state-base-hover rounded-lg cursor-pointer' onClick={onClickSettings}>
<span className='text-text-secondary system-sm-regular'>{t('app.editApp')}</span>
<button className='mx-1 flex h-8 w-[calc(100%_-_8px)] cursor-pointer items-center gap-2 rounded-lg px-3 py-[6px] hover:bg-state-base-hover' onClick={onClickSettings}>
<span className='system-sm-regular text-text-secondary'>{t('app.editApp')}</span>
</button>
<Divider className="!my-1" />
<button className='h-8 w-[calc(100%_-_8px)] py-[6px] px-3 mx-1 flex items-center gap-2 hover:bg-state-base-hover rounded-lg cursor-pointer' onClick={onClickDuplicate}>
<span className='text-text-secondary system-sm-regular'>{t('app.duplicate')}</span>
<button className='mx-1 flex h-8 w-[calc(100%_-_8px)] cursor-pointer items-center gap-2 rounded-lg px-3 py-[6px] hover:bg-state-base-hover' onClick={onClickDuplicate}>
<span className='system-sm-regular text-text-secondary'>{t('app.duplicate')}</span>
</button>
<button className='h-8 w-[calc(100%_-_8px)] py-[6px] px-3 mx-1 flex items-center gap-2 hover:bg-state-base-hover rounded-lg cursor-pointer' onClick={onClickExport}>
<span className='text-text-secondary system-sm-regular'>{t('app.export')}</span>
<button className='mx-1 flex h-8 w-[calc(100%_-_8px)] cursor-pointer items-center gap-2 rounded-lg px-3 py-[6px] hover:bg-state-base-hover' onClick={onClickExport}>
<span className='system-sm-regular text-text-secondary'>{t('app.export')}</span>
</button>
{(app.mode === 'completion' || app.mode === 'chat') && (
<>
<Divider className="!my-1" />
<div
className='h-9 py-2 px-3 mx-1 flex items-center hover:bg-state-base-hover rounded-lg cursor-pointer'
className='mx-1 flex h-9 cursor-pointer items-center rounded-lg px-3 py-2 hover:bg-state-base-hover'
onClick={onClickSwitch}
>
<span className='text-text-secondary text-sm leading-5'>{t('app.switch')}</span>
<span className='text-sm leading-5 text-text-secondary'>{t('app.switch')}</span>
</div>
</>
)}
<Divider className="!my-1" />
<button className='h-8 w-[calc(100%_-_8px)] py-[6px] px-3 mx-1 flex items-center gap-2 hover:bg-state-base-hover rounded-lg cursor-pointer' onClick={onClickInstalledApp}>
<span className='text-text-secondary system-sm-regular'>{t('app.openInExplore')}</span>
<button className='mx-1 flex h-8 w-[calc(100%_-_8px)] cursor-pointer items-center gap-2 rounded-lg px-3 py-[6px] hover:bg-state-base-hover' onClick={onClickInstalledApp}>
<span className='system-sm-regular text-text-secondary'>{t('app.openInExplore')}</span>
</button>
<Divider className="!my-1" />
<div
className='group h-8 w-[calc(100%_-_8px)] py-[6px] px-3 mx-1 flex items-center gap-2 hover:bg-state-destructive-hover rounded-lg cursor-pointer'
className='group mx-1 flex h-8 w-[calc(100%_-_8px)] cursor-pointer items-center gap-2 rounded-lg px-3 py-[6px] hover:bg-state-destructive-hover'
onClick={onClickDelete}
>
<span className='text-text-secondary system-sm-regular group-hover:text-text-destructive'>
<span className='system-sm-regular text-text-secondary group-hover:text-text-destructive'>
{t('common.operation.delete')}
</span>
</div>
@ -276,9 +276,9 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
e.preventDefault()
getRedirection(isCurrentWorkspaceEditor, app, push)
}}
className='relative h-[160px] group col-span-1 bg-components-card-bg border-[1px] border-solid border-components-card-border rounded-xl shadow-sm inline-flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg'
className='group relative col-span-1 inline-flex h-[160px] cursor-pointer flex-col rounded-xl border-[1px] border-solid border-components-card-border bg-components-card-bg shadow-sm transition-all duration-200 ease-in-out hover:shadow-lg'
>
<div className='flex pt-[14px] px-[14px] pb-3 h-[66px] items-center gap-3 grow-0 shrink-0'>
<div className='flex h-[66px] shrink-0 grow-0 items-center gap-3 px-[14px] pb-3 pt-[14px]'>
<div className='relative shrink-0'>
<AppIcon
size="large"
@ -287,13 +287,13 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
background={app.icon_background}
imageUrl={app.icon_url}
/>
<AppTypeIcon type={app.mode} wrapperClassName='absolute -bottom-0.5 -right-0.5 w-4 h-4 shadow-sm' className='w-3 h-3' />
<AppTypeIcon type={app.mode} wrapperClassName='absolute -bottom-0.5 -right-0.5 w-4 h-4 shadow-sm' className='h-3 w-3' />
</div>
<div className='grow w-0 py-[1px]'>
<div className='flex items-center text-sm leading-5 font-semibold text-text-secondary'>
<div className='w-0 grow py-[1px]'>
<div className='flex items-center text-sm font-semibold leading-5 text-text-secondary'>
<div className='truncate' title={app.name}>{app.name}</div>
</div>
<div className='flex items-center text-[10px] leading-[18px] text-text-tertiary font-medium'>
<div className='flex items-center text-[10px] font-medium leading-[18px] text-text-tertiary'>
{app.mode === 'advanced-chat' && <div className='truncate'>{t('app.types.advanced').toUpperCase()}</div>}
{app.mode === 'chat' && <div className='truncate'>{t('app.types.chatbot').toUpperCase()}</div>}
{app.mode === 'agent-chat' && <div className='truncate'>{t('app.types.agent').toUpperCase()}</div>}
@ -311,17 +311,17 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
</div>
</div>
<div className={cn(
'absolute bottom-1 left-0 right-0 items-center shrink-0 pt-1 pl-[14px] pr-[6px] pb-[6px] h-[42px]',
'absolute bottom-1 left-0 right-0 h-[42px] shrink-0 items-center pb-[6px] pl-[14px] pr-[6px] pt-1',
tags.length ? 'flex' : '!hidden group-hover:!flex',
)}>
{isCurrentWorkspaceEditor && (
<>
<div className={cn('grow flex items-center gap-1 w-0')} onClick={(e) => {
<div className={cn('flex w-0 grow items-center gap-1')} onClick={(e) => {
e.stopPropagation()
e.preventDefault()
}}>
<div className={cn(
'group-hover:!block group-hover:!mr-0 mr-[41px] grow w-full',
'mr-[41px] w-full grow group-hover:!mr-0 group-hover:!block',
tags.length ? '!block' : '!hidden',
)}>
<TagSelector
@ -335,23 +335,23 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
/>
</div>
</div>
<div className='!hidden group-hover:!flex shrink-0 mx-1 w-[1px] h-[14px]' />
<div className='!hidden group-hover:!flex shrink-0'>
<div className='mx-1 !hidden h-[14px] w-[1px] shrink-0 group-hover:!flex' />
<div className='!hidden shrink-0 group-hover:!flex'>
<CustomPopover
htmlContent={<Operations />}
position="br"
trigger="click"
btnElement={
<div
className='flex items-center justify-center w-8 h-8 cursor-pointer rounded-md'
className='flex h-8 w-8 cursor-pointer items-center justify-center rounded-md'
>
<RiMoreFill className='w-4 h-4 text-text-tertiary' />
<RiMoreFill className='h-4 w-4 text-text-tertiary' />
</div>
}
btnClassName={open =>
cn(
open ? '!bg-black/5 !shadow-none' : '!bg-transparent',
'h-8 w-8 !p-2 rounded-md border-none hover:!bg-black/5',
'h-8 w-8 rounded-md border-none !p-2 hover:!bg-black/5',
)
}
popupClassName={
@ -359,7 +359,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
? '!w-[256px] translate-x-[-224px]'
: '!w-[160px] translate-x-[-128px]'
}
className={'h-fit !z-20'}
className={'!z-20 h-fit'}
/>
</div>
</>

View File

@ -8,6 +8,7 @@ import { useDebounceFn } from 'ahooks'
import {
RiApps2Line,
RiExchange2Line,
RiFile4Line,
RiMessage3Line,
RiRobot3Line,
} from '@remixicon/react'
@ -78,10 +79,12 @@ const Apps = () => {
const anchorRef = useRef<HTMLDivElement>(null)
const options = [
{ value: 'all', text: t('app.types.all'), icon: <RiApps2Line className='w-[14px] h-[14px] mr-1' /> },
{ value: 'chat', text: t('app.types.chatbot'), icon: <RiMessage3Line className='w-[14px] h-[14px] mr-1' /> },
{ value: 'agent-chat', text: t('app.types.agent'), icon: <RiRobot3Line className='w-[14px] h-[14px] mr-1' /> },
{ value: 'workflow', text: t('app.types.workflow'), icon: <RiExchange2Line className='w-[14px] h-[14px] mr-1' /> },
{ value: 'all', text: t('app.types.all'), icon: <RiApps2Line className='mr-1 h-[14px] w-[14px]' /> },
{ value: 'chat', text: t('app.types.chatbot'), icon: <RiMessage3Line className='mr-1 h-[14px] w-[14px]' /> },
{ value: 'agent-chat', text: t('app.types.agent'), icon: <RiRobot3Line className='mr-1 h-[14px] w-[14px]' /> },
{ value: 'completion', text: t('app.types.completion'), icon: <RiFile4Line className='mr-1 h-[14px] w-[14px]' /> },
{ value: 'advanced-chat', text: t('app.types.advanced'), icon: <RiMessage3Line className='mr-1 h-[14px] w-[14px]' /> },
{ value: 'workflow', text: t('app.types.workflow'), icon: <RiExchange2Line className='mr-1 h-[14px] w-[14px]' /> },
]
useEffect(() => {
@ -134,7 +137,7 @@ const Apps = () => {
return (
<>
<div className='sticky top-0 flex justify-between items-center pt-4 px-12 pb-2 leading-[56px] bg-background-body z-10 flex-wrap gap-y-2'>
<div className='sticky top-0 z-10 flex flex-wrap items-center justify-between gap-y-2 bg-background-body px-12 pb-2 pt-4 leading-[56px]'>
<TabSliderNew
value={activeTab}
onChange={setActiveTab}
@ -159,14 +162,14 @@ const Apps = () => {
</div>
</div>
{(data && data[0].total > 0)
? <div className='grid content-start grid-cols-1 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5 2k:grid-cols-6 gap-4 px-12 pt-2 grow relative'>
? <div className='relative grid grow grid-cols-1 content-start gap-4 px-12 pt-2 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5 2k:grid-cols-6'>
{isCurrentWorkspaceEditor
&& <NewAppCard onSuccess={mutate} />}
{data.map(({ data: apps }) => apps.map(app => (
<AppCard key={app.id} app={app} onRefresh={mutate} />
)))}
</div>
: <div className='grid content-start grid-cols-1 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5 2k:grid-cols-6 gap-4 px-12 pt-2 grow relative overflow-hidden'>
: <div className='relative grid grow grid-cols-1 content-start gap-4 overflow-hidden px-12 pt-2 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5 2k:grid-cols-6'>
{isCurrentWorkspaceEditor
&& <NewAppCard className='z-10' onSuccess={mutate} />}
<NoAppsFound />
@ -186,14 +189,14 @@ function NoAppsFound() {
const { t } = useTranslation()
function renderDefaultCard() {
const defaultCards = Array.from({ length: 36 }, (_, index) => (
<div key={index} className='h-[160px] inline-flex rounded-xl bg-background-default-lighter'></div>
<div key={index} className='inline-flex h-[160px] rounded-xl bg-background-default-lighter'></div>
))
return defaultCards
}
return (
<>
{renderDefaultCard()}
<div className='absolute top-0 left-0 right-0 bottom-0 flex items-center justify-center bg-gradient-to-t from-background-body to-transparent'>
<div className='absolute bottom-0 left-0 right-0 top-0 flex items-center justify-center bg-gradient-to-t from-background-body to-transparent'>
<span className='system-md-medium text-text-tertiary'>{t('app.newApp.noAppsFound')}</span>
</div>
</>

View File

@ -1,6 +1,6 @@
'use client'
import { forwardRef, useMemo, useState } from 'react'
import { useMemo, useState } from 'react'
import {
useRouter,
useSearchParams,
@ -18,7 +18,15 @@ export type CreateAppCardProps = {
onSuccess?: () => void
}
const CreateAppCard = forwardRef<HTMLDivElement, CreateAppCardProps>(({ className, onSuccess }, ref) => {
const CreateAppCard = (
{
ref,
className,
onSuccess,
}: CreateAppCardProps & {
ref: React.RefObject<HTMLDivElement>;
},
) => {
const { t } = useTranslation()
const { onPlanInfoChanged } = useProviderContext()
const searchParams = useSearchParams()
@ -39,22 +47,22 @@ const CreateAppCard = forwardRef<HTMLDivElement, CreateAppCardProps>(({ classNam
return (
<div
ref={ref}
className={cn('relative col-span-1 inline-flex flex-col justify-between h-[160px] bg-components-card-bg rounded-xl border-[0.5px] border-components-card-border', className)}
className={cn('relative col-span-1 inline-flex h-[160px] flex-col justify-between rounded-xl border-[0.5px] border-components-card-border bg-components-card-bg', className)}
>
<div className='grow p-2 rounded-t-xl'>
<div className='px-6 pt-2 pb-1 text-xs font-medium leading-[18px] text-text-tertiary'>{t('app.createApp')}</div>
<button className='w-full flex items-center mb-1 px-6 py-[7px] rounded-lg text-[13px] font-medium leading-[18px] text-text-tertiary cursor-pointer hover:text-text-secondary hover:bg-state-base-hover' onClick={() => setShowNewAppModal(true)}>
<FilePlus01 className='shrink-0 mr-2 w-4 h-4' />
<div className='grow rounded-t-xl p-2'>
<div className='px-6 pb-1 pt-2 text-xs font-medium leading-[18px] text-text-tertiary'>{t('app.createApp')}</div>
<button className='mb-1 flex w-full cursor-pointer items-center rounded-lg px-6 py-[7px] text-[13px] font-medium leading-[18px] text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary' onClick={() => setShowNewAppModal(true)}>
<FilePlus01 className='mr-2 h-4 w-4 shrink-0' />
{t('app.newApp.startFromBlank')}
</button>
<button className='w-full flex items-center px-6 py-[7px] rounded-lg text-[13px] font-medium leading-[18px] text-text-tertiary cursor-pointer hover:text-text-secondary hover:bg-state-base-hover' onClick={() => setShowNewAppTemplateDialog(true)}>
<FilePlus02 className='shrink-0 mr-2 w-4 h-4' />
<button className='flex w-full cursor-pointer items-center rounded-lg px-6 py-[7px] text-[13px] font-medium leading-[18px] text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary' onClick={() => setShowNewAppTemplateDialog(true)}>
<FilePlus02 className='mr-2 h-4 w-4 shrink-0' />
{t('app.newApp.startFromTemplate')}
</button>
<button
onClick={() => setShowCreateFromDSLModal(true)}
className='w-full flex items-center px-6 py-[7px] rounded-lg text-[13px] font-medium leading-[18px] text-text-tertiary cursor-pointer hover:text-text-secondary hover:bg-state-base-hover'>
<FileArrow01 className='shrink-0 mr-2 w-4 h-4' />
className='flex w-full cursor-pointer items-center rounded-lg px-6 py-[7px] text-[13px] font-medium leading-[18px] text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary'>
<FileArrow01 className='mr-2 h-4 w-4 shrink-0' />
{t('app.importDSL')}
</button>
</div>
@ -103,7 +111,7 @@ const CreateAppCard = forwardRef<HTMLDivElement, CreateAppCardProps>(({ classNam
/>
</div>
)
})
}
CreateAppCard.displayName = 'CreateAppCard'
export default CreateAppCard

View File

@ -13,17 +13,17 @@ const AppList = () => {
const systemFeatures = useContextSelector(AppContext, v => v.systemFeatures)
return (
<div className='relative flex flex-col overflow-y-auto bg-background-body shrink-0 h-0 grow'>
<div className='relative flex h-0 shrink-0 grow flex-col overflow-y-auto bg-background-body'>
<Apps />
{systemFeatures.license.status === LicenseStatus.NONE && <footer className='px-12 py-6 grow-0 shrink-0'>
<h3 className='text-xl font-semibold leading-tight text-gradient'>{t('app.join')}</h3>
<p className='mt-1 system-sm-regular text-text-tertiary'>{t('app.communityIntro')}</p>
<div className='flex items-center gap-2 mt-3'>
{systemFeatures.license.status === LicenseStatus.NONE && <footer className='shrink-0 grow-0 px-12 py-6'>
<h3 className='text-gradient text-xl font-semibold leading-tight'>{t('app.join')}</h3>
<p className='system-sm-regular mt-1 text-text-tertiary'>{t('app.communityIntro')}</p>
<div className='mt-3 flex items-center gap-2'>
<Link className={style.socialMediaLink} target='_blank' rel='noopener noreferrer' href='https://github.com/langgenius/dify'>
<RiGithubFill className='w-5 h-5 text-text-tertiary' />
<RiGithubFill className='h-5 w-5 text-text-tertiary' />
</Link>
<Link className={style.socialMediaLink} target='_blank' rel='noopener noreferrer' href='https://discord.gg/FngNHpbcY7'>
<RiDiscordFill className='w-5 h-5 text-text-tertiary' />
<RiDiscordFill className='h-5 w-5 text-text-tertiary' />
</Link>
</div>
</footer>}

View File

@ -2,12 +2,17 @@ import React from 'react'
import MainDetail from '@/app/components/datasets/documents/detail'
export type IDocumentDetailProps = {
params: { datasetId: string; documentId: string }
params: Promise<{ datasetId: string; documentId: string }>
}
const DocumentDetail = async ({
params: { datasetId, documentId },
}: IDocumentDetailProps) => {
const DocumentDetail = async (props: IDocumentDetailProps) => {
const params = await props.params
const {
datasetId,
documentId,
} = params
return (
<MainDetail datasetId={datasetId} documentId={documentId} />
)

View File

@ -2,12 +2,17 @@ import React from 'react'
import Settings from '@/app/components/datasets/documents/detail/settings'
export type IProps = {
params: { datasetId: string; documentId: string }
params: Promise<{ datasetId: string; documentId: string }>
}
const DocumentSettings = async ({
params: { datasetId, documentId },
}: IProps) => {
const DocumentSettings = async (props: IProps) => {
const params = await props.params
const {
datasetId,
documentId,
} = params
return (
<Settings datasetId={datasetId} documentId={documentId} />
)

View File

@ -2,12 +2,16 @@ import React from 'react'
import DatasetUpdateForm from '@/app/components/datasets/create'
export type IProps = {
params: { datasetId: string }
params: Promise<{ datasetId: string }>
}
const Create = async ({
params: { datasetId },
}: IProps) => {
const Create = async (props: IProps) => {
const params = await props.params
const {
datasetId,
} = params
return (
<DatasetUpdateForm datasetId={datasetId} />
)

View File

@ -2,12 +2,16 @@ import React from 'react'
import Main from '@/app/components/datasets/documents'
export type IProps = {
params: { datasetId: string }
params: Promise<{ datasetId: string }>
}
const Documents = async ({
params: { datasetId },
}: IProps) => {
const Documents = async (props: IProps) => {
const params = await props.params
const {
datasetId,
} = params
return (
<Main datasetId={datasetId} />
)

View File

@ -2,12 +2,16 @@ import React from 'react'
import Main from '@/app/components/datasets/hit-testing'
type Props = {
params: { datasetId: string }
params: Promise<{ datasetId: string }>
}
const HitTesting = ({
params: { datasetId },
}: Props) => {
const HitTesting = async (props: Props) => {
const params = await props.params
const {
datasetId,
} = params
return (
<Main datasetId={datasetId} />
)

View File

@ -0,0 +1,221 @@
'use client'
import type { FC, SVGProps } from 'react'
import React, { useEffect, useMemo } from 'react'
import { usePathname } from 'next/navigation'
import useSWR from 'swr'
import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks'
import {
Cog8ToothIcon,
DocumentTextIcon,
PaperClipIcon,
} from '@heroicons/react/24/outline'
import {
Cog8ToothIcon as Cog8ToothSolidIcon,
// CommandLineIcon as CommandLineSolidIcon,
DocumentTextIcon as DocumentTextSolidIcon,
} from '@heroicons/react/24/solid'
import { RiApps2AddLine, RiBookOpenLine, RiInformation2Line } from '@remixicon/react'
import s from './style.module.css'
import classNames from '@/utils/classnames'
import { fetchDatasetDetail, fetchDatasetRelatedApps } from '@/service/datasets'
import type { RelatedAppResponse } from '@/models/datasets'
import AppSideBar from '@/app/components/app-sidebar'
import Loading from '@/app/components/base/loading'
import DatasetDetailContext from '@/context/dataset-detail'
import { DataSourceType } from '@/models/datasets'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import { LanguagesSupported } from '@/i18n/language'
import { useStore } from '@/app/components/app/store'
import { getLocaleOnClient } from '@/i18n'
import { useAppContext } from '@/context/app-context'
import Tooltip from '@/app/components/base/tooltip'
import LinkedAppsPanel from '@/app/components/base/linked-apps-panel'
export type IAppDetailLayoutProps = {
children: React.ReactNode
params: { datasetId: string }
}
const TargetIcon = ({ className }: SVGProps<SVGElement>) => {
return <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
<g clipPath="url(#clip0_4610_6951)">
<path d="M10.6666 5.33325V3.33325L12.6666 1.33325L13.3332 2.66659L14.6666 3.33325L12.6666 5.33325H10.6666ZM10.6666 5.33325L7.9999 7.99988M14.6666 7.99992C14.6666 11.6818 11.6818 14.6666 7.99992 14.6666C4.31802 14.6666 1.33325 11.6818 1.33325 7.99992C1.33325 4.31802 4.31802 1.33325 7.99992 1.33325M11.3333 7.99992C11.3333 9.84087 9.84087 11.3333 7.99992 11.3333C6.15897 11.3333 4.66659 9.84087 4.66659 7.99992C4.66659 6.15897 6.15897 4.66659 7.99992 4.66659" stroke="#344054" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
</g>
<defs>
<clipPath id="clip0_4610_6951">
<rect width="16" height="16" fill="white" />
</clipPath>
</defs>
</svg>
}
const TargetSolidIcon = ({ className }: SVGProps<SVGElement>) => {
return <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
<path fillRule="evenodd" clipRule="evenodd" d="M12.7733 0.67512C12.9848 0.709447 13.1669 0.843364 13.2627 1.03504L13.83 2.16961L14.9646 2.73689C15.1563 2.83273 15.2902 3.01486 15.3245 3.22639C15.3588 3.43792 15.2894 3.65305 15.1379 3.80458L13.1379 5.80458C13.0128 5.92961 12.8433 5.99985 12.6665 5.99985H10.9426L8.47124 8.47124C8.21089 8.73159 7.78878 8.73159 7.52843 8.47124C7.26808 8.21089 7.26808 7.78878 7.52843 7.52843L9.9998 5.05707V3.33318C9.9998 3.15637 10.07 2.9868 10.1951 2.86177L12.1951 0.861774C12.3466 0.710244 12.5617 0.640794 12.7733 0.67512Z" fill="#155EEF" />
<path d="M1.99984 7.99984C1.99984 4.68613 4.68613 1.99984 7.99984 1.99984C8.36803 1.99984 8.6665 1.70136 8.6665 1.33317C8.6665 0.964981 8.36803 0.666504 7.99984 0.666504C3.94975 0.666504 0.666504 3.94975 0.666504 7.99984C0.666504 12.0499 3.94975 15.3332 7.99984 15.3332C12.0499 15.3332 15.3332 12.0499 15.3332 7.99984C15.3332 7.63165 15.0347 7.33317 14.6665 7.33317C14.2983 7.33317 13.9998 7.63165 13.9998 7.99984C13.9998 11.3135 11.3135 13.9998 7.99984 13.9998C4.68613 13.9998 1.99984 11.3135 1.99984 7.99984Z" fill="#155EEF" />
<path d="M5.33317 7.99984C5.33317 6.52708 6.52708 5.33317 7.99984 5.33317C8.36803 5.33317 8.6665 5.03469 8.6665 4.6665C8.6665 4.29831 8.36803 3.99984 7.99984 3.99984C5.7907 3.99984 3.99984 5.7907 3.99984 7.99984C3.99984 10.209 5.7907 11.9998 7.99984 11.9998C10.209 11.9998 11.9998 10.209 11.9998 7.99984C11.9998 7.63165 11.7014 7.33317 11.3332 7.33317C10.965 7.33317 10.6665 7.63165 10.6665 7.99984C10.6665 9.4726 9.4726 10.6665 7.99984 10.6665C6.52708 10.6665 5.33317 9.4726 5.33317 7.99984Z" fill="#155EEF" />
</svg>
}
type IExtraInfoProps = {
isMobile: boolean
relatedApps?: RelatedAppResponse
expand: boolean
}
const ExtraInfo = ({ isMobile, relatedApps, expand }: IExtraInfoProps) => {
const locale = getLocaleOnClient()
const [isShowTips, { toggle: toggleTips, set: setShowTips }] = useBoolean(!isMobile)
const { t } = useTranslation()
const hasRelatedApps = relatedApps?.data && relatedApps?.data?.length > 0
const relatedAppsTotal = relatedApps?.data?.length || 0
useEffect(() => {
setShowTips(!isMobile)
}, [isMobile, setShowTips])
return <div>
{hasRelatedApps && (
<>
{!isMobile && (
<Tooltip
position='right'
noDecoration
needsDelay
popupContent={
<LinkedAppsPanel
relatedApps={relatedApps.data}
isMobile={isMobile}
/>
}
>
<div className='system-xs-medium-uppercase inline-flex cursor-pointer items-center space-x-1 text-text-secondary'>
<span>{relatedAppsTotal || '--'} {t('common.datasetMenus.relatedApp')}</span>
<RiInformation2Line className='h-4 w-4' />
</div>
</Tooltip>
)}
{isMobile && <div className={classNames(s.subTitle, 'flex items-center justify-center !px-0 gap-1')}>
{relatedAppsTotal || '--'}
<PaperClipIcon className='h-4 w-4 text-gray-700' />
</div>}
</>
)}
{!hasRelatedApps && !expand && (
<Tooltip
position='right'
noDecoration
needsDelay
popupContent={
<div className='w-[240px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-4'>
<div className='inline-flex rounded-lg border-[0.5px] border-components-panel-border-subtle bg-background-default-subtle p-2'>
<RiApps2AddLine className='h-4 w-4 text-text-tertiary' />
</div>
<div className='my-2 text-xs text-text-tertiary'>{t('common.datasetMenus.emptyTip')}</div>
<a
className='mt-2 inline-flex cursor-pointer items-center text-xs text-text-accent'
href={
locale === LanguagesSupported[1]
? 'https://docs.dify.ai/v/zh-hans/guides/knowledge-base/integrate-knowledge-within-application'
: 'https://docs.dify.ai/guides/knowledge-base/integrate-knowledge-within-application'
}
target='_blank' rel='noopener noreferrer'
>
<RiBookOpenLine className='mr-1 text-text-accent' />
{t('common.datasetMenus.viewDoc')}
</a>
</div>
}
>
<div className='system-xs-medium-uppercase inline-flex cursor-pointer items-center space-x-1 text-text-secondary'>
<span>{t('common.datasetMenus.noRelatedApp')}</span>
<RiInformation2Line className='h-4 w-4' />
</div>
</Tooltip>
)}
</div>
}
const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
const {
children,
params: { datasetId },
} = props
const pathname = usePathname()
const hideSideBar = /documents\/create$/.test(pathname)
const { t } = useTranslation()
const { isCurrentWorkspaceDatasetOperator } = useAppContext()
const media = useBreakpoints()
const isMobile = media === MediaType.mobile
const { data: datasetRes, error, mutate: mutateDatasetRes } = useSWR({
url: 'fetchDatasetDetail',
datasetId,
}, apiParams => fetchDatasetDetail(apiParams.datasetId))
const { data: relatedApps } = useSWR({
action: 'fetchDatasetRelatedApps',
datasetId,
}, apiParams => fetchDatasetRelatedApps(apiParams.datasetId))
const navigation = useMemo(() => {
const baseNavigation = [
{ name: t('common.datasetMenus.hitTesting'), href: `/datasets/${datasetId}/hitTesting`, icon: TargetIcon, selectedIcon: TargetSolidIcon },
// { name: 'api & webhook', href: `/datasets/${datasetId}/api`, icon: CommandLineIcon, selectedIcon: CommandLineSolidIcon },
{ name: t('common.datasetMenus.settings'), href: `/datasets/${datasetId}/settings`, icon: Cog8ToothIcon, selectedIcon: Cog8ToothSolidIcon },
]
if (datasetRes?.provider !== 'external') {
baseNavigation.unshift({
name: t('common.datasetMenus.documents'),
href: `/datasets/${datasetId}/documents`,
icon: DocumentTextIcon,
selectedIcon: DocumentTextSolidIcon,
})
}
return baseNavigation
}, [datasetRes?.provider, datasetId, t])
useEffect(() => {
if (datasetRes)
document.title = `${datasetRes.name || 'Dataset'} - Dify`
}, [datasetRes])
const setAppSiderbarExpand = useStore(state => state.setAppSiderbarExpand)
useEffect(() => {
const localeMode = localStorage.getItem('app-detail-collapse-or-expand') || 'expand'
const mode = isMobile ? 'collapse' : 'expand'
setAppSiderbarExpand(isMobile ? mode : localeMode)
}, [isMobile, setAppSiderbarExpand])
if (!datasetRes && !error)
return <Loading type='app' />
return (
<div className='flex grow overflow-hidden'>
{!hideSideBar && <AppSideBar
title={datasetRes?.name || '--'}
icon={datasetRes?.icon || 'https://static.dify.ai/images/dataset-default-icon.png'}
icon_background={datasetRes?.icon_background || '#F5F5F5'}
desc={datasetRes?.description || '--'}
isExternal={datasetRes?.provider === 'external'}
navigation={navigation}
extraInfo={!isCurrentWorkspaceDatasetOperator ? mode => <ExtraInfo isMobile={mode === 'collapse'} relatedApps={relatedApps} expand={mode === 'collapse'} /> : undefined}
iconType={datasetRes?.data_source_type === DataSourceType.NOTION ? 'notion' : 'dataset'}
/>}
<DatasetDetailContext.Provider value={{
indexingTechnique: datasetRes?.indexing_technique,
dataset: datasetRes,
mutateDatasetRes: () => mutateDatasetRes(),
}}>
<div className="grow overflow-hidden bg-background-default-subtle">{children}</div>
</DatasetDetailContext.Provider>
</div>
)
}
export default React.memo(DatasetDetailLayout)

View File

@ -1,221 +1,17 @@
'use client'
import type { FC, SVGProps } from 'react'
import React, { useEffect, useMemo } from 'react'
import { usePathname } from 'next/navigation'
import useSWR from 'swr'
import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks'
import {
Cog8ToothIcon,
DocumentTextIcon,
PaperClipIcon,
} from '@heroicons/react/24/outline'
import {
Cog8ToothIcon as Cog8ToothSolidIcon,
// CommandLineIcon as CommandLineSolidIcon,
DocumentTextIcon as DocumentTextSolidIcon,
} from '@heroicons/react/24/solid'
import { RiApps2AddLine, RiBookOpenLine, RiInformation2Line } from '@remixicon/react'
import s from './style.module.css'
import classNames from '@/utils/classnames'
import { fetchDatasetDetail, fetchDatasetRelatedApps } from '@/service/datasets'
import type { RelatedAppResponse } from '@/models/datasets'
import AppSideBar from '@/app/components/app-sidebar'
import Loading from '@/app/components/base/loading'
import DatasetDetailContext from '@/context/dataset-detail'
import { DataSourceType } from '@/models/datasets'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import { LanguagesSupported } from '@/i18n/language'
import { useStore } from '@/app/components/app/store'
import { getLocaleOnClient } from '@/i18n'
import { useAppContext } from '@/context/app-context'
import Tooltip from '@/app/components/base/tooltip'
import LinkedAppsPanel from '@/app/components/base/linked-apps-panel'
import Main from './layout-main'
export type IAppDetailLayoutProps = {
children: React.ReactNode
params: { datasetId: string }
}
const DatasetDetailLayout = async (
props: {
children: React.ReactNode
params: Promise<{ datasetId: string }>
},
) => {
const params = await props.params
const TargetIcon = ({ className }: SVGProps<SVGElement>) => {
return <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
<g clipPath="url(#clip0_4610_6951)">
<path d="M10.6666 5.33325V3.33325L12.6666 1.33325L13.3332 2.66659L14.6666 3.33325L12.6666 5.33325H10.6666ZM10.6666 5.33325L7.9999 7.99988M14.6666 7.99992C14.6666 11.6818 11.6818 14.6666 7.99992 14.6666C4.31802 14.6666 1.33325 11.6818 1.33325 7.99992C1.33325 4.31802 4.31802 1.33325 7.99992 1.33325M11.3333 7.99992C11.3333 9.84087 9.84087 11.3333 7.99992 11.3333C6.15897 11.3333 4.66659 9.84087 4.66659 7.99992C4.66659 6.15897 6.15897 4.66659 7.99992 4.66659" stroke="#344054" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
</g>
<defs>
<clipPath id="clip0_4610_6951">
<rect width="16" height="16" fill="white" />
</clipPath>
</defs>
</svg>
}
const TargetSolidIcon = ({ className }: SVGProps<SVGElement>) => {
return <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
<path fillRule="evenodd" clipRule="evenodd" d="M12.7733 0.67512C12.9848 0.709447 13.1669 0.843364 13.2627 1.03504L13.83 2.16961L14.9646 2.73689C15.1563 2.83273 15.2902 3.01486 15.3245 3.22639C15.3588 3.43792 15.2894 3.65305 15.1379 3.80458L13.1379 5.80458C13.0128 5.92961 12.8433 5.99985 12.6665 5.99985H10.9426L8.47124 8.47124C8.21089 8.73159 7.78878 8.73159 7.52843 8.47124C7.26808 8.21089 7.26808 7.78878 7.52843 7.52843L9.9998 5.05707V3.33318C9.9998 3.15637 10.07 2.9868 10.1951 2.86177L12.1951 0.861774C12.3466 0.710244 12.5617 0.640794 12.7733 0.67512Z" fill="#155EEF" />
<path d="M1.99984 7.99984C1.99984 4.68613 4.68613 1.99984 7.99984 1.99984C8.36803 1.99984 8.6665 1.70136 8.6665 1.33317C8.6665 0.964981 8.36803 0.666504 7.99984 0.666504C3.94975 0.666504 0.666504 3.94975 0.666504 7.99984C0.666504 12.0499 3.94975 15.3332 7.99984 15.3332C12.0499 15.3332 15.3332 12.0499 15.3332 7.99984C15.3332 7.63165 15.0347 7.33317 14.6665 7.33317C14.2983 7.33317 13.9998 7.63165 13.9998 7.99984C13.9998 11.3135 11.3135 13.9998 7.99984 13.9998C4.68613 13.9998 1.99984 11.3135 1.99984 7.99984Z" fill="#155EEF" />
<path d="M5.33317 7.99984C5.33317 6.52708 6.52708 5.33317 7.99984 5.33317C8.36803 5.33317 8.6665 5.03469 8.6665 4.6665C8.6665 4.29831 8.36803 3.99984 7.99984 3.99984C5.7907 3.99984 3.99984 5.7907 3.99984 7.99984C3.99984 10.209 5.7907 11.9998 7.99984 11.9998C10.209 11.9998 11.9998 10.209 11.9998 7.99984C11.9998 7.63165 11.7014 7.33317 11.3332 7.33317C10.965 7.33317 10.6665 7.63165 10.6665 7.99984C10.6665 9.4726 9.4726 10.6665 7.99984 10.6665C6.52708 10.6665 5.33317 9.4726 5.33317 7.99984Z" fill="#155EEF" />
</svg>
}
type IExtraInfoProps = {
isMobile: boolean
relatedApps?: RelatedAppResponse
expand: boolean
}
const ExtraInfo = ({ isMobile, relatedApps, expand }: IExtraInfoProps) => {
const locale = getLocaleOnClient()
const [isShowTips, { toggle: toggleTips, set: setShowTips }] = useBoolean(!isMobile)
const { t } = useTranslation()
const hasRelatedApps = relatedApps?.data && relatedApps?.data?.length > 0
const relatedAppsTotal = relatedApps?.data?.length || 0
useEffect(() => {
setShowTips(!isMobile)
}, [isMobile, setShowTips])
return <div>
{hasRelatedApps && (
<>
{!isMobile && (
<Tooltip
position='right'
noDecoration
needsDelay
popupContent={
<LinkedAppsPanel
relatedApps={relatedApps.data}
isMobile={isMobile}
/>
}
>
<div className='inline-flex items-center system-xs-medium-uppercase text-text-secondary space-x-1 cursor-pointer'>
<span>{relatedAppsTotal || '--'} {t('common.datasetMenus.relatedApp')}</span>
<RiInformation2Line className='w-4 h-4' />
</div>
</Tooltip>
)}
{isMobile && <div className={classNames(s.subTitle, 'flex items-center justify-center !px-0 gap-1')}>
{relatedAppsTotal || '--'}
<PaperClipIcon className='h-4 w-4 text-gray-700' />
</div>}
</>
)}
{!hasRelatedApps && !expand && (
<Tooltip
position='right'
noDecoration
needsDelay
popupContent={
<div className='p-4 w-[240px] bg-components-panel-bg-blur border-[0.5px] border-components-panel-border rounded-xl'>
<div className='inline-flex p-2 rounded-lg border-[0.5px] border-components-panel-border-subtle bg-background-default-subtle'>
<RiApps2AddLine className='h-4 w-4 text-text-tertiary' />
</div>
<div className='text-xs text-text-tertiary my-2'>{t('common.datasetMenus.emptyTip')}</div>
<a
className='inline-flex items-center text-xs text-text-accent mt-2 cursor-pointer'
href={
locale === LanguagesSupported[1]
? 'https://docs.dify.ai/v/zh-hans/guides/knowledge-base/integrate-knowledge-within-application'
: 'https://docs.dify.ai/guides/knowledge-base/integrate-knowledge-within-application'
}
target='_blank' rel='noopener noreferrer'
>
<RiBookOpenLine className='mr-1 text-text-accent' />
{t('common.datasetMenus.viewDoc')}
</a>
</div>
}
>
<div className='inline-flex items-center system-xs-medium-uppercase text-text-secondary space-x-1 cursor-pointer'>
<span>{t('common.datasetMenus.noRelatedApp')}</span>
<RiInformation2Line className='w-4 h-4' />
</div>
</Tooltip>
)}
</div>
}
const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
const {
children,
params: { datasetId },
} = props
const pathname = usePathname()
const hideSideBar = /documents\/create$/.test(pathname)
const { t } = useTranslation()
const { isCurrentWorkspaceDatasetOperator } = useAppContext()
const media = useBreakpoints()
const isMobile = media === MediaType.mobile
const { data: datasetRes, error, mutate: mutateDatasetRes } = useSWR({
url: 'fetchDatasetDetail',
datasetId,
}, apiParams => fetchDatasetDetail(apiParams.datasetId))
const { data: relatedApps } = useSWR({
action: 'fetchDatasetRelatedApps',
datasetId,
}, apiParams => fetchDatasetRelatedApps(apiParams.datasetId))
const navigation = useMemo(() => {
const baseNavigation = [
{ name: t('common.datasetMenus.hitTesting'), href: `/datasets/${datasetId}/hitTesting`, icon: TargetIcon, selectedIcon: TargetSolidIcon },
// { name: 'api & webhook', href: `/datasets/${datasetId}/api`, icon: CommandLineIcon, selectedIcon: CommandLineSolidIcon },
{ name: t('common.datasetMenus.settings'), href: `/datasets/${datasetId}/settings`, icon: Cog8ToothIcon, selectedIcon: Cog8ToothSolidIcon },
]
if (datasetRes?.provider !== 'external') {
baseNavigation.unshift({
name: t('common.datasetMenus.documents'),
href: `/datasets/${datasetId}/documents`,
icon: DocumentTextIcon,
selectedIcon: DocumentTextSolidIcon,
})
}
return baseNavigation
}, [datasetRes?.provider, datasetId, t])
useEffect(() => {
if (datasetRes)
document.title = `${datasetRes.name || 'Dataset'} - Dify`
}, [datasetRes])
const setAppSiderbarExpand = useStore(state => state.setAppSiderbarExpand)
useEffect(() => {
const localeMode = localStorage.getItem('app-detail-collapse-or-expand') || 'expand'
const mode = isMobile ? 'collapse' : 'expand'
setAppSiderbarExpand(isMobile ? mode : localeMode)
}, [isMobile, setAppSiderbarExpand])
if (!datasetRes && !error)
return <Loading type='app' />
return (
<div className='grow flex overflow-hidden'>
{!hideSideBar && <AppSideBar
title={datasetRes?.name || '--'}
icon={datasetRes?.icon || 'https://static.dify.ai/images/dataset-default-icon.png'}
icon_background={datasetRes?.icon_background || '#F5F5F5'}
desc={datasetRes?.description || '--'}
isExternal={datasetRes?.provider === 'external'}
navigation={navigation}
extraInfo={!isCurrentWorkspaceDatasetOperator ? mode => <ExtraInfo isMobile={mode === 'collapse'} relatedApps={relatedApps} expand={mode === 'collapse'} /> : undefined}
iconType={datasetRes?.data_source_type === DataSourceType.NOTION ? 'notion' : 'dataset'}
/>}
<DatasetDetailContext.Provider value={{
indexingTechnique: datasetRes?.indexing_technique,
dataset: datasetRes,
mutateDatasetRes: () => mutateDatasetRes(),
}}>
<div className="bg-background-default-subtle grow overflow-hidden">{children}</div>
</DatasetDetailContext.Provider>
</div>
)
return <Main params={(await params)}>{children}</Main>
}
export default React.memo(DatasetDetailLayout)
export default DatasetDetailLayout

View File

@ -3,13 +3,13 @@ import { getLocaleOnServer, useTranslation as translate } from '@/i18n/server'
import Form from '@/app/components/datasets/settings/form'
const Settings = async () => {
const locale = getLocaleOnServer()
const locale = await getLocaleOnServer()
const { t } = await translate(locale, 'dataset-settings')
return (
<div className='h-full overflow-y-auto'>
<div className='px-6 py-3'>
<div className='mb-1 system-xl-semibold text-text-primary'>{t('title')}</div>
<div className='system-xl-semibold mb-1 text-text-primary'>{t('title')}</div>
<div className='system-sm-regular text-text-tertiary'>{t('desc')}</div>
</div>
<Form />

View File

@ -15,22 +15,22 @@ const ApiServer: FC<ApiServerProps> = ({
const { t } = useTranslation()
return (
<div className='flex items-center flex-wrap gap-y-2'>
<div className='flex items-center mr-2 pl-1.5 pr-1 h-8 bg-white/80 border-[0.5px] border-white rounded-lg leading-5'>
<div className='mr-0.5 px-1.5 h-5 border border-gray-200 text-[11px] text-gray-500 rounded-md shrink-0'>{t('appApi.apiServer')}</div>
<div className='px-1 truncate w-fit sm:w-[248px] text-[13px] font-medium text-gray-800'>{apiBaseUrl}</div>
<div className='mx-1 w-[1px] h-[14px] bg-gray-200'></div>
<div className='flex flex-wrap items-center gap-y-2'>
<div className='mr-2 flex h-8 items-center rounded-lg border-[0.5px] border-white bg-white/80 pl-1.5 pr-1 leading-5'>
<div className='mr-0.5 h-5 shrink-0 rounded-md border border-gray-200 px-1.5 text-[11px] text-gray-500'>{t('appApi.apiServer')}</div>
<div className='w-fit truncate px-1 text-[13px] font-medium text-gray-800 sm:w-[248px]'>{apiBaseUrl}</div>
<div className='mx-1 h-[14px] w-[1px] bg-gray-200'></div>
<CopyFeedback
content={apiBaseUrl}
selectorId={randomString(8)}
className={'!w-6 !h-6 hover:bg-gray-200'}
className={'!h-6 !w-6 hover:bg-gray-200'}
/>
</div>
<div className='flex items-center mr-2 px-3 h-8 bg-[#ECFDF3] text-xs font-semibold text-[#039855] rounded-lg border-[0.5px] border-[#D1FADF]'>
<div className='mr-2 flex h-8 items-center rounded-lg border-[0.5px] border-[#D1FADF] bg-[#ECFDF3] px-3 text-xs font-semibold text-[#039855]'>
{t('appApi.ok')}
</div>
<SecretKeyButton
className='shrink-0 !h-8 bg-white'
className='!h-8 shrink-0 bg-white'
/>
</div>
)

View File

@ -82,8 +82,8 @@ const Container = () => {
}, [currentWorkspace, router])
return (
<div ref={containerRef} className='grow relative flex flex-col bg-background-body overflow-y-auto scroll-container'>
<div className='sticky top-0 flex justify-between pt-4 px-12 pb-2 leading-[56px] bg-background-body z-10 flex-wrap gap-y-2'>
<div ref={containerRef} className='scroll-container relative flex grow flex-col overflow-y-auto bg-background-body'>
<div className='sticky top-0 z-10 flex flex-wrap justify-between gap-y-2 bg-background-body px-12 pb-2 pt-4 leading-[56px]'>
<TabSliderNew
value={activeTab}
onChange={newActiveTab => setActiveTab(newActiveTab)}
@ -108,13 +108,13 @@ const Container = () => {
onChange={e => handleKeywordsChange(e.target.value)}
onClear={() => handleKeywordsChange('')}
/>
<div className="w-[1px] h-4 bg-divider-regular" />
<div className="h-4 w-[1px] bg-divider-regular" />
<Button
className='gap-0.5 shadows-shadow-xs'
className='shadows-shadow-xs gap-0.5'
onClick={() => setShowExternalApiPanel(true)}
>
<ApiConnectionMod className='w-4 h-4 text-components-button-secondary-text' />
<div className='flex px-0.5 justify-center items-center gap-1 text-components-button-secondary-text system-sm-medium'>{t('dataset.externalAPIPanelTitle')}</div>
<ApiConnectionMod className='h-4 w-4 text-components-button-secondary-text' />
<div className='system-sm-medium flex items-center justify-center gap-1 px-0.5 text-components-button-secondary-text'>{t('dataset.externalAPIPanelTitle')}</div>
</Button>
</div>
)}

View File

@ -84,17 +84,17 @@ const DatasetCard = ({
}
return (
<div className="relative w-full py-1" onMouseLeave={onMouseLeave}>
<div className='h-8 py-[6px] px-3 mx-1 flex items-center gap-2 hover:bg-state-base-hover rounded-lg cursor-pointer' onClick={onClickRename}>
<span className='text-text-secondary text-sm'>{t('common.operation.settings')}</span>
<div className='mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 py-[6px] hover:bg-state-base-hover' onClick={onClickRename}>
<span className='text-sm text-text-secondary'>{t('common.operation.settings')}</span>
</div>
{props.showDelete && (
<>
<Divider className="!my-1" />
<div
className='group h-8 py-[6px] px-3 mx-1 flex items-center gap-2 hover:bg-state-destructive-hover rounded-lg cursor-pointer'
className='group mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 py-[6px] hover:bg-state-destructive-hover'
onClick={onClickDelete}
>
<span className={cn('text-text-secondary text-sm', 'group-hover:text-text-destructive')}>
<span className={cn('text-sm text-text-secondary', 'group-hover:text-text-destructive')}>
{t('common.operation.delete')}
</span>
</div>
@ -111,7 +111,7 @@ const DatasetCard = ({
return (
<>
<div
className='group relative col-span-1 bg-components-card-bg border-[0.5px] border-solid border-components-card-border rounded-xl shadow-sm min-h-[160px] flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg'
className='group relative col-span-1 flex min-h-[160px] cursor-pointer flex-col rounded-xl border-[0.5px] border-solid border-components-card-border bg-components-card-bg shadow-sm transition-all duration-200 ease-in-out hover:shadow-lg'
data-disable-nprogress={true}
onClick={(e) => {
e.preventDefault()
@ -121,25 +121,25 @@ const DatasetCard = ({
}}
>
{isExternalProvider(dataset.provider) && <CornerLabel label='External' className='absolute right-0' labelClassName='rounded-tr-xl' />}
<div className='flex pt-[14px] px-[14px] pb-3 h-[66px] items-center gap-3 grow-0 shrink-0'>
<div className='flex h-[66px] shrink-0 grow-0 items-center gap-3 px-[14px] pb-3 pt-[14px]'>
<div className={cn(
'shrink-0 flex items-center justify-center p-2.5 bg-[#F5F8FF] rounded-md border-[0.5px] border-[#E0EAFF]',
'flex shrink-0 items-center justify-center rounded-md border-[0.5px] border-[#E0EAFF] bg-[#F5F8FF] p-2.5',
!dataset.embedding_available && 'opacity-50 hover:opacity-100',
)}>
<Folder className='w-5 h-5 text-[#444CE7]' />
<Folder className='h-5 w-5 text-[#444CE7]' />
</div>
<div className='grow w-0 py-[1px]'>
<div className='flex items-center text-sm leading-5 font-semibold text-text-secondary'>
<div className={cn('truncate', !dataset.embedding_available && 'opacity-50 hover:opacity-100 text-text-tertiary')} title={dataset.name}>{dataset.name}</div>
<div className='w-0 grow py-[1px]'>
<div className='flex items-center text-sm font-semibold leading-5 text-text-secondary'>
<div className={cn('truncate', !dataset.embedding_available && 'text-text-tertiary opacity-50 hover:opacity-100')} title={dataset.name}>{dataset.name}</div>
{!dataset.embedding_available && (
<Tooltip
popupContent={t('dataset.unavailableTip')}
>
<span className='shrink-0 inline-flex w-max ml-1 px-1 border border-divider-regular rounded-md text-text-tertiary text-xs font-normal leading-[18px]'>{t('dataset.unavailable')}</span>
<span className='ml-1 inline-flex w-max shrink-0 rounded-md border border-divider-regular px-1 text-xs font-normal leading-[18px] text-text-tertiary'>{t('dataset.unavailable')}</span>
</Tooltip>
)}
</div>
<div className='flex items-center mt-[1px] text-xs leading-[18px] text-text-tertiary'>
<div className='mt-[1px] flex items-center text-xs leading-[18px] text-text-tertiary'>
<div
className={cn('truncate', (!dataset.embedding_available || !dataset.document_count) && 'opacity-50')}
title={dataset.provider === 'external' ? `${dataset.app_count}${t('dataset.appCount')}` : `${dataset.document_count}${t('dataset.documentCount')} · ${Math.round(dataset.word_count / 1000)}${t('dataset.wordCount')} · ${dataset.app_count}${t('dataset.appCount')}`}
@ -150,9 +150,9 @@ const DatasetCard = ({
</>
: <>
<span>{dataset.document_count}{t('dataset.documentCount')}</span>
<span className='shrink-0 mx-0.5 w-1 text-text-tertiary'>·</span>
<span className='mx-0.5 w-1 shrink-0 text-text-tertiary'>·</span>
<span>{Math.round(dataset.word_count / 1000)}{t('dataset.wordCount')}</span>
<span className='shrink-0 mx-0.5 w-1 text-text-tertiary'>·</span>
<span className='mx-0.5 w-1 shrink-0 text-text-tertiary'>·</span>
<span>{dataset.app_count}{t('dataset.appCount')}</span>
</>
}
@ -162,7 +162,7 @@ const DatasetCard = ({
</div>
<div
className={cn(
'grow mb-2 px-[14px] max-h-[72px] text-xs leading-normal text-text-tertiary group-hover:line-clamp-2 group-hover:max-h-[36px]',
'mb-2 max-h-[72px] grow px-[14px] text-xs leading-normal text-text-tertiary group-hover:line-clamp-2 group-hover:max-h-[36px]',
tags.length ? 'line-clamp-2' : 'line-clamp-4',
!dataset.embedding_available && 'opacity-50 hover:opacity-100',
)}
@ -170,15 +170,15 @@ const DatasetCard = ({
{dataset.description}
</div>
<div className={cn(
'items-center shrink-0 mt-1 pt-1 pl-[14px] pr-[6px] pb-[6px] h-[42px]',
'mt-1 h-[42px] shrink-0 items-center pb-[6px] pl-[14px] pr-[6px] pt-1',
tags.length ? 'flex' : '!hidden group-hover:!flex',
)}>
<div className={cn('grow flex items-center gap-1 w-0', !dataset.embedding_available && 'opacity-50 hover:opacity-100')} onClick={(e) => {
<div className={cn('flex w-0 grow items-center gap-1', !dataset.embedding_available && 'opacity-50 hover:opacity-100')} onClick={(e) => {
e.stopPropagation()
e.preventDefault()
}}>
<div className={cn(
'group-hover:!block group-hover:!mr-0 mr-[41px] grow w-full',
'mr-[41px] w-full grow group-hover:!mr-0 group-hover:!block',
tags.length ? '!block' : '!hidden',
)}>
<TagSelector
@ -192,26 +192,26 @@ const DatasetCard = ({
/>
</div>
</div>
<div className='!hidden group-hover:!flex shrink-0 mx-1 w-[1px] h-[14px] bg-divider-regular' />
<div className='!hidden group-hover:!flex shrink-0'>
<div className='mx-1 !hidden h-[14px] w-[1px] shrink-0 bg-divider-regular group-hover:!flex' />
<div className='!hidden shrink-0 group-hover:!flex'>
<CustomPopover
htmlContent={<Operations showDelete={!isCurrentWorkspaceDatasetOperator} />}
position="br"
trigger="click"
btnElement={
<div
className='flex items-center justify-center w-8 h-8 cursor-pointer rounded-md'
className='flex h-8 w-8 cursor-pointer items-center justify-center rounded-md'
>
<RiMoreFill className='w-4 h-4 text-text-secondary' />
<RiMoreFill className='h-4 w-4 text-text-secondary' />
</div>
}
btnClassName={open =>
cn(
open ? '!bg-black/5 !shadow-none' : '!bg-transparent',
'h-8 w-8 !p-2 rounded-md border-none hover:!bg-black/5',
'h-8 w-8 rounded-md border-none !p-2 hover:!bg-black/5',
)
}
className={'!w-[128px] h-fit !z-20'}
className={'!z-20 h-fit !w-[128px]'}
/>
</div>
</div>

View File

@ -6,8 +6,8 @@ const DatasetFooter = () => {
const { t } = useTranslation()
return (
<footer className='px-12 py-6 grow-0 shrink-0'>
<h3 className='text-xl font-semibold leading-tight text-gradient'>{t('dataset.didYouKnow')}</h3>
<footer className='shrink-0 grow-0 px-12 py-6'>
<h3 className='text-gradient text-xl font-semibold leading-tight'>{t('dataset.didYouKnow')}</h3>
<p className='mt-1 text-sm font-normal leading-tight text-text-secondary'>
{t('dataset.intro1')}<span className='inline-flex items-center gap-1 text-text-accent'>{t('dataset.intro2')}</span>{t('dataset.intro3')}<br />
{t('dataset.intro4')}<span className='inline-flex items-center gap-1 text-text-accent'>{t('dataset.intro5')}</span>{t('dataset.intro6')}

View File

@ -86,7 +86,7 @@ const Datasets = ({
}, [onScroll])
return (
<nav className='grid content-start grid-cols-1 gap-4 px-12 pt-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 grow shrink-0'>
<nav className='grid shrink-0 grow grid-cols-1 content-start gap-4 px-12 pt-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4'>
{ isCurrentWorkspaceEditor && <NewDatasetCard ref={anchorRef} /> }
{data?.map(({ data: datasets }) => datasets.map(dataset => (
<DatasetCard key={dataset.id} dataset={dataset} onSuccess={mutate} />),

View File

@ -71,8 +71,8 @@ const Doc = ({ apiBaseUrl }: DocProps) => {
<div className={`fixed right-20 top-32 z-10 transition-all ${isTocExpanded ? 'w-64' : 'w-10'}`}>
{isTocExpanded
? (
<nav className="toc w-full bg-gray-50 p-4 rounded-lg shadow-md max-h-[calc(100vh-150px)] overflow-y-auto">
<div className="flex justify-between items-center mb-4">
<nav className="toc max-h-[calc(100vh-150px)] w-full overflow-y-auto rounded-lg bg-gray-50 p-4 shadow-md">
<div className="mb-4 flex items-center justify-between">
<h3 className="text-lg font-semibold">{t('appApi.develop.toc')}</h3>
<button
onClick={() => setIsTocExpanded(false)}
@ -86,7 +86,7 @@ const Doc = ({ apiBaseUrl }: DocProps) => {
<li key={index}>
<a
href={item.href}
className="text-gray-600 hover:text-gray-900 hover:underline transition-colors duration-200"
className="text-gray-600 transition-colors duration-200 hover:text-gray-900 hover:underline"
onClick={e => handleTocClick(e, item)}
>
{item.text}
@ -99,13 +99,13 @@ const Doc = ({ apiBaseUrl }: DocProps) => {
: (
<button
onClick={() => setIsTocExpanded(true)}
className="w-10 h-10 bg-gray-50 rounded-full shadow-md flex items-center justify-center hover:bg-gray-100 transition-colors duration-200"
className="flex h-10 w-10 items-center justify-center rounded-full bg-gray-50 shadow-md transition-colors duration-200 hover:bg-gray-100"
>
<RiListUnordered className="w-6 h-6" />
<RiListUnordered className="h-6 w-6" />
</button>
)}
</div>
<article className='mx-1 px-4 sm:mx-12 pt-16 bg-white rounded-t-xl prose prose-xl'>
<article className='prose-xl prose mx-1 rounded-t-xl bg-white px-4 pt-16 sm:mx-12'>
{locale !== LanguagesSupported[1]
? <TemplateEn apiBaseUrl={apiBaseUrl} />
: <TemplateZh apiBaseUrl={apiBaseUrl} />

View File

@ -1,37 +1,40 @@
'use client'
import { forwardRef } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiAddLine,
RiArrowRightLine,
} from '@remixicon/react'
const CreateAppCard = forwardRef<HTMLAnchorElement>((_, ref) => {
const CreateAppCard = (
{
ref,
..._
},
) => {
const { t } = useTranslation()
return (
<div className='flex flex-col bg-background-default-dimm border-[0.5px] border-components-panel-border rounded-xl
min-h-[160px] transition-all duration-200 ease-in-out'
<div className='bg-background-default-dimm flex min-h-[160px] flex-col rounded-xl border-[0.5px]
border-components-panel-border transition-all duration-200 ease-in-out'
>
<a ref={ref} className='group flex flex-grow items-start p-4 cursor-pointer' href='/datasets/create'>
<a ref={ref} className='group flex grow cursor-pointer items-start p-4' href='/datasets/create'>
<div className='flex items-center gap-3'>
<div className='w-10 h-10 p-2 flex items-center justify-center border border-dashed border-divider-regular rounded-lg
bg-background-default-lighter group-hover:border-solid group-hover:border-effects-highlight group-hover:bg-background-default-dodge'
<div className='flex h-10 w-10 items-center justify-center rounded-lg border border-dashed border-divider-regular bg-background-default-lighter
p-2 group-hover:border-solid group-hover:border-effects-highlight group-hover:bg-background-default-dodge'
>
<RiAddLine className='w-4 h-4 text-text-tertiary group-hover:text-text-accent'/>
<RiAddLine className='h-4 w-4 text-text-tertiary group-hover:text-text-accent'/>
</div>
<div className='system-md-semibold text-text-secondary group-hover:text-text-accent'>{t('dataset.createDataset')}</div>
</div>
</a>
<div className='p-4 pt-0 text-text-tertiary system-xs-regular'>{t('dataset.createDatasetIntro')}</div>
<a className='group flex p-4 items-center gap-1 border-t-[0.5px] border-divider-subtle rounded-b-xl cursor-pointer' href='/datasets/connect'>
<div className='system-xs-regular p-4 pt-0 text-text-tertiary'>{t('dataset.createDatasetIntro')}</div>
<a className='group flex cursor-pointer items-center gap-1 rounded-b-xl border-t-[0.5px] border-divider-subtle p-4' href='/datasets/connect'>
<div className='system-xs-medium text-text-tertiary group-hover:text-text-accent'>{t('dataset.connectDataset')}</div>
<RiArrowRightLine className='w-3.5 h-3.5 text-text-tertiary group-hover:text-text-accent' />
<RiArrowRightLine className='h-3.5 w-3.5 text-text-tertiary group-hover:text-text-accent' />
</a>
</div>
)
})
}
CreateAppCard.displayName = 'CreateAppCard'

View File

@ -961,6 +961,12 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
<Property name='status' type='string' key='status'>
Search status, completed
</Property>
<Property name='page' type='string' key='page'>
Page number (optional)
</Property>
<Property name='limit' type='string' key='limit'>
Number of items returned, default 20, range 1-100 (optional)
</Property>
</Properties>
</Col>
<Col sticky>
@ -1004,7 +1010,11 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
"error": null,
"stopped_at": null
}],
"doc_form": "text_model"
"doc_form": "text_model",
"has_more": false,
"limit": 20,
"total": 9,
"page": 1
}
```
</CodeGroup>

View File

@ -961,6 +961,12 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
<Property name='status' type='string' key='status'>
搜索状态completed
</Property>
<Property name='page' type='string' key='page'>
页码,可选
</Property>
<Property name='limit' type='string' key='limit'>
返回条数,可选,默认 20范围 1-100
</Property>
</Properties>
</Col>
<Col sticky>
@ -1004,7 +1010,11 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
"error": null,
"stopped_at": null
}],
"doc_form": "text_model"
"doc_form": "text_model",
"has_more": false,
"limit": 20,
"total": 9,
"page": 1
}
```
</CodeGroup>

View File

@ -3,14 +3,14 @@ import React from 'react'
import Main from '@/app/components/explore/installed-app'
export type IInstalledAppProps = {
params: {
params: Promise<{
appId: string
}
}>
}
const InstalledApp: FC<IInstalledAppProps> = ({ params: { appId } }) => {
const InstalledApp: FC<IInstalledAppProps> = async ({ params }) => {
return (
<Main id={appId} />
<Main id={(await params).appId} />
)
}
export default React.memo(InstalledApp)

View File

@ -10,7 +10,7 @@ const Layout: FC<{
children: React.ReactNode
}> = ({ children }) => {
return (
<div className="min-w-[300px] h-full pb-[env(safe-area-inset-bottom)]">
<div className="h-full min-w-[300px] pb-[env(safe-area-inset-bottom)]">
{children}
</div>
)

View File

@ -92,8 +92,8 @@ const WebSSOForm: FC = () => {
}, [message, tokenFromUrl]) // Added dependencies to useEffect
return (
<div className="flex items-center justify-center h-full">
<div className={cn('flex flex-col items-center w-full grow justify-center', 'px-6', 'md:px-[108px]')}>
<div className="flex h-full items-center justify-center">
<div className={cn('flex w-full grow flex-col items-center justify-center', 'px-6', 'md:px-[108px]')}>
<Loading type='area' />
</div>
</div>

View File

@ -83,13 +83,13 @@ const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => {
return (
<>
<div>
<div className="relative group">
<div className="group relative">
<Avatar {...props} />
<div
onClick={() => { setIsShowAvatarPicker(true) }}
className="absolute inset-0 bg-black bg-opacity-50 rounded-full opacity-0 group-hover:opacity-100 transition-opacity cursor-pointer flex items-center justify-center"
className="absolute inset-0 flex cursor-pointer items-center justify-center rounded-full bg-black bg-opacity-50 opacity-0 transition-opacity group-hover:opacity-100"
>
<span className="text-white text-xs">
<span className="text-xs text-white">
<RiPencilLine />
</span>
</div>
@ -105,7 +105,7 @@ const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => {
<ImageInput onImageInput={handleImageInput} cropShape='round' />
<Divider className='m-0' />
<div className='w-full flex items-center justify-center p-3 gap-2'>
<div className='flex w-full items-center justify-center gap-2 p-3'>
<Button className='w-full' onClick={() => setIsShowAvatarPicker(false)}>
{t('app.iconPicker.cancel')}
</Button>

View File

@ -122,17 +122,17 @@ export default function AccountPage() {
<div className='mr-3'>
<AppIcon size='tiny' />
</div>
<div className='mt-[3px] system-sm-medium text-text-secondary'>{item.name}</div>
<div className='system-sm-medium mt-[3px] text-text-secondary'>{item.name}</div>
</div>
)
}
return (
<>
<div className='pt-2 pb-3'>
<div className='pb-3 pt-2'>
<h4 className='title-2xl-semi-bold text-text-primary'>{t('common.account.myAccount')}</h4>
</div>
<div className='mb-8 p-6 rounded-xl flex items-center bg-gradient-to-r from-background-gradient-bg-fill-chat-bg-2 to-background-gradient-bg-fill-chat-bg-1'>
<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} />
<div className='ml-4'>
<p className='system-xl-semibold text-text-primary'>{userProfile.name}</p>
@ -141,19 +141,19 @@ export default function AccountPage() {
</div>
<div className='mb-8'>
<div className={titleClassName}>{t('common.account.name')}</div>
<div className='flex items-center justify-between gap-2 w-full mt-2'>
<div className='flex-1 bg-components-input-bg-normal rounded-lg p-2 system-sm-regular text-components-input-text-filled '>
<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='bg-components-button-tertiary-bg rounded-lg py-2 px-3 cursor-pointer system-sm-medium text-components-button-tertiary-text' onClick={handleEditName}>
<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')}
</div>
</div>
</div>
<div className='mb-8'>
<div className={titleClassName}>{t('common.account.email')}</div>
<div className='flex items-center justify-between gap-2 w-full mt-2'>
<div className='flex-1 bg-components-input-bg-normal rounded-lg p-2 system-sm-regular text-components-input-text-filled '>
<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>
</div>
@ -162,8 +162,8 @@ export default function AccountPage() {
systemFeatures.enable_email_password_login && (
<div className='mb-8 flex justify-between gap-2'>
<div>
<div className='mb-1 system-sm-semibold text-text-secondary'>{t('common.account.password')}</div>
<div className='mb-2 body-xs-regular text-text-tertiary'>{t('common.account.passwordTip')}</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>
<Button onClick={() => setEditPasswordModalVisible(true)}>{userProfile.is_password_set ? t('common.account.resetPassword') : t('common.account.setPassword')}</Button>
</div>
@ -190,13 +190,13 @@ export default function AccountPage() {
onClose={() => setEditNameModalVisible(false)}
className={s.modal}
>
<div className='mb-6 title-2xl-semi-bold text-text-primary'>{t('common.account.editName')}</div>
<div className='title-2xl-semi-bold mb-6 text-text-primary'>{t('common.account.editName')}</div>
<div className={titleClassName}>{t('common.account.name')}</div>
<Input className='mt-2'
value={editName}
onChange={e => setEditName(e.target.value)}
/>
<div className='flex justify-end mt-10'>
<div className='mt-10 flex justify-end'>
<Button className='mr-2' onClick={() => setEditNameModalVisible(false)}>{t('common.operation.cancel')}</Button>
<Button
disabled={editing || !editName}
@ -219,7 +219,7 @@ export default function AccountPage() {
}}
className={s.modal}
>
<div className='mb-6 title-2xl-semi-bold 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('common.account.resetPassword') : t('common.account.setPassword')}</div>
{userProfile.is_password_set && (
<>
<div className={titleClassName}>{t('common.account.currentPassword')}</div>
@ -242,7 +242,7 @@ export default function AccountPage() {
</div>
</>
)}
<div className='mt-8 system-sm-semibold text-text-secondary'>
<div className='system-sm-semibold mt-8 text-text-secondary'>
{userProfile.is_password_set ? t('common.account.newPassword') : t('common.account.password')}
</div>
<div className='relative mt-2'>
@ -261,7 +261,7 @@ export default function AccountPage() {
</Button>
</div>
</div>
<div className='mt-8 system-sm-semibold text-text-secondary'>{t('common.account.confirmPassword')}</div>
<div className='system-sm-semibold mt-8 text-text-secondary'>{t('common.account.confirmPassword')}</div>
<div className='relative mt-2'>
<Input
type={showConfirmPassword ? 'text' : 'password'}
@ -278,7 +278,7 @@ export default function AccountPage() {
</Button>
</div>
</div>
<div className='flex justify-end mt-10'>
<div className='mt-10 flex justify-end'>
<Button className='mr-2' onClick={() => {
setEditPasswordModalVisible(false)
resetPasswordForm()

View File

@ -2,13 +2,13 @@
import { useTranslation } from 'react-i18next'
import { Fragment } from 'react'
import { useRouter } from 'next/navigation'
import { Menu, Transition } from '@headlessui/react'
import { Menu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/react'
import Avatar from '@/app/components/base/avatar'
import { logout } from '@/service/common'
import { useAppContext } from '@/context/app-context'
import { LogOut01 } from '@/app/components/base/icons/src/vender/line/general'
export interface IAppSelector {
export type IAppSelector = {
isMobile: boolean
}
@ -36,17 +36,17 @@ export default function AppSelector() {
({ open }) => (
<>
<div>
<Menu.Button
<MenuButton
className={`
inline-flex items-center
rounded-[20px] p-1x text-sm
p-1x inline-flex
items-center rounded-[20px] text-sm
text-text-primary
mobile:px-1
${open && 'bg-components-panel-bg-blur'}
`}
>
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size={32} />
</Menu.Button>
</MenuButton>
</div>
<Transition
as={Fragment}
@ -57,35 +57,35 @@ export default function AppSelector() {
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items
<MenuItems
className="
absolute -right-2 -top-1 w-60 max-w-80
divide-y divide-divider-subtle origin-top-right rounded-lg bg-components-panel-bg-blur
origin-top-right divide-y divide-divider-subtle rounded-lg bg-components-panel-bg-blur
shadow-lg
"
>
<Menu.Item>
<MenuItem>
<div className='p-1'>
<div className='flex flex-nowrap items-center px-3 py-2'>
<div className='grow'>
<div className='system-md-medium text-text-primary break-all'>{userProfile.name}</div>
<div className='system-xs-regular text-text-tertiary break-all'>{userProfile.email}</div>
<div className='system-md-medium break-all text-text-primary'>{userProfile.name}</div>
<div className='system-xs-regular break-all text-text-tertiary'>{userProfile.email}</div>
</div>
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size={32} />
</div>
</div>
</Menu.Item>
<Menu.Item>
</MenuItem>
<MenuItem>
<div className='p-1' onClick={() => handleLogout()}>
<div
className='flex items-center justify-start h-9 px-3 rounded-lg cursor-pointer group hover:bg-state-base-hover'
className='group flex h-9 cursor-pointer items-center justify-start rounded-lg px-3 hover:bg-state-base-hover'
>
<LogOut01 className='w-4 h-4 text-text-tertiary flex mr-1' />
<div className='font-normal text-[14px] text-text-secondary'>{t('common.userProfile.logout')}</div>
<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>
</div>
</Menu.Item>
</Menu.Items>
</MenuItem>
</MenuItems>
</Transition>
</>
)

View File

@ -29,18 +29,18 @@ export default function CheckEmail(props: DeleteAccountProps) {
}, [getDeleteEmailVerifyCode, props])
return <>
<div className='py-1 text-text-destructive body-md-medium'>
<div className='body-md-medium py-1 text-text-destructive'>
{t('common.account.deleteTip')}
</div>
<div className='pt-1 pb-2 text-text-secondary body-md-regular'>
<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>
</div>
<label className='mt-3 mb-1 h-6 flex items-center system-sm-semibold 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('common.account.deleteLabel')}</label>
<Input placeholder={t('common.account.deletePlaceholder') as string} onChange={(e) => {
setUserInputEmail(e.target.value)
}} />
<div className='w-full flex flex-col mt-3 gap-2'>
<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>
</div>

View File

@ -56,11 +56,11 @@ export default function FeedBack(props: DeleteAccountProps) {
className="max-w-[480px]"
footer={false}
>
<label className='mt-3 mb-1 flex items-center system-sm-semibold text-text-secondary'>{t('common.account.feedbackLabel')}</label>
<label className='system-sm-semibold mb-1 mt-3 flex items-center text-text-secondary'>{t('common.account.feedbackLabel')}</label>
<Textarea rows={6} value={userFeedback} placeholder={t('common.account.feedbackPlaceholder') as string} onChange={(e) => {
setUserFeedback(e.target.value)
}} />
<div className='w-full flex flex-col mt-3 gap-2'>
<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>
</div>

View File

@ -35,18 +35,18 @@ export default function VerifyEmail(props: DeleteAccountProps) {
catch (error) { console.error(error) }
}, [emailToken, verificationCode, confirmDeleteAccount, props])
return <>
<div className='pt-1 text-text-destructive body-md-medium'>
<div className='body-md-medium pt-1 text-text-destructive'>
{t('common.account.deleteTip')}
</div>
<div className='pt-1 pb-2 text-text-secondary body-md-regular'>
<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>
</div>
<label className='mt-3 mb-1 h-6 flex items-center system-sm-semibold 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('common.account.verificationLabel')}</label>
<Input minLength={6} maxLength={6} placeholder={t('common.account.verificationPlaceholder') as string} onChange={(e) => {
setVerificationCode(e.target.value)
}} />
<div className='w-full flex flex-col mt-3 gap-2'>
<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>
<Countdown onResend={sendEmail} />

View File

@ -16,19 +16,19 @@ const Header = () => {
return (
<div className='flex flex-1 items-center justify-between px-4'>
<div className='flex items-center gap-3'>
<div className='flex items-center cursor-pointer' onClick={back}>
<div className='flex cursor-pointer items-center' onClick={back}>
<LogoSite className='object-contain' />
</div>
<div className='w-[1px] h-4 bg-divider-regular' />
<p className='text-text-primary title-3xl-semi-bold'>{t('common.account.account')}</p>
<div className='h-4 w-[1px] bg-divider-regular' />
<p className='title-3xl-semi-bold text-text-primary'>{t('common.account.account')}</p>
</div>
<div className='flex items-center flex-shrink-0 gap-3'>
<Button className='gap-2 py-2 px-3 system-sm-medium' onClick={back}>
<RiRobot2Line className='w-4 h-4' />
<div className='flex shrink-0 items-center gap-3'>
<Button className='system-sm-medium gap-2 px-3 py-2' onClick={back}>
<RiRobot2Line className='h-4 w-4' />
<p>{t('common.account.studio')}</p>
<RiArrowRightUpLine className='w-4 h-4' />
<RiArrowRightUpLine className='h-4 w-4' />
</Button>
<div className='w-[1px] h-4 bg-divider-regular' />
<div className='h-4 w-[1px] bg-divider-regular' />
<Avatar />
</div>
</div>

View File

@ -21,7 +21,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
<HeaderWrapper>
<Header />
</HeaderWrapper>
<div className='relative flex flex-col overflow-y-auto bg-components-panel-bg shrink-0 h-0 grow'>
<div className='relative flex h-0 shrink-0 grow flex-col overflow-y-auto bg-components-panel-bg'>
{children}
</div>
</ModalContextProvider>

View File

@ -1,7 +1,7 @@
import AccountPage from './account-page'
export default function Account() {
return <div className='max-w-[640px] w-full mx-auto pt-12 px-6'>
return <div className='mx-auto w-full max-w-[640px] px-6 pt-12'>
<AccountPage />
</div>
}

View File

@ -41,7 +41,7 @@ const ActivateForm = () => {
return (
<div className={
cn(
'flex flex-col items-center w-full grow justify-center',
'flex w-full grow flex-col items-center justify-center',
'px-6',
'md:px-[108px]',
)
@ -49,11 +49,11 @@ const ActivateForm = () => {
{!checkRes && <Loading />}
{checkRes && !checkRes.is_valid && (
<div className="flex flex-col md:w-[400px]">
<div className="w-full mx-auto">
<div className="mb-3 flex justify-center items-center w-20 h-20 p-5 rounded-[20px] border border-gray-100 shadow-lg text-[40px] font-bold">🤷</div>
<div className="mx-auto w-full">
<div className="mb-3 flex h-20 w-20 items-center justify-center rounded-[20px] border border-gray-100 p-5 text-[40px] font-bold shadow-lg">🤷</div>
<h2 className="text-[32px] font-bold text-gray-900">{t('login.invalid')}</h2>
</div>
<div className="w-full mx-auto mt-6">
<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>
</Button>

View File

@ -8,14 +8,14 @@ const Activate = () => {
return (
<div className={cn(
style.background,
'flex w-full min-h-screen',
'flex min-h-screen w-full',
'sm:p-4 lg:p-8',
'gap-x-20',
'justify-center lg:justify-start',
)}>
<div className={
cn(
'flex w-full flex-col bg-white shadow rounded-2xl shrink-0',
'flex w-full shrink-0 flex-col rounded-2xl bg-white shadow',
'space-between',
)
}>

View File

@ -191,7 +191,7 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
}}
className='block w-full'
>
<div className={cn('flex rounded-lg', expand ? 'p-2 pb-2.5 flex-col gap-2' : 'p-1 gap-1 justify-center items-start', open && 'bg-state-base-hover', isCurrentWorkspaceEditor && 'hover:bg-state-base-hover cursor-pointer')}>
<div className={cn('flex rounded-lg', expand ? 'flex-col gap-2 p-2 pb-2.5' : 'items-start justify-center gap-1 p-1', open && 'bg-state-base-hover', isCurrentWorkspaceEditor && 'cursor-pointer hover:bg-state-base-hover')}>
<div className={`flex items-center self-stretch ${expand ? 'justify-between' : 'flex-col gap-1'}`}>
<AppIcon
size={expand ? 'large' : 'small'}
@ -200,9 +200,9 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
background={appDetail.icon_background}
imageUrl={appDetail.icon_url}
/>
<div className='flex p-0.5 justify-center items-center rounded-md'>
<div className='flex w-5 h-5 justify-center items-center'>
<RiEqualizer2Line className='w-4 h-4 text-text-tertiary' />
<div className='flex items-center justify-center rounded-md p-0.5'>
<div className='flex h-5 w-5 items-center justify-center'>
<RiEqualizer2Line className='h-4 w-4 text-text-tertiary' />
</div>
</div>
</div>
@ -210,9 +210,9 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
expand && (
<div className='flex flex-col items-start gap-1'>
<div className='flex w-full'>
<div className='text-text-secondary system-md-semibold truncate'>{appDetail.name}</div>
<div className='system-md-semibold truncate text-text-secondary'>{appDetail.name}</div>
</div>
<div className='text-text-tertiary system-2xs-medium-uppercase'>{appDetail.mode === 'advanced-chat' ? t('app.types.chatbot') : appDetail.mode === 'agent-chat' ? t('app.types.agent') : appDetail.mode === 'chat' ? t('app.types.chatbot') : appDetail.mode === 'completion' ? t('app.types.completion') : t('app.types.workflow')}</div>
<div className='system-2xs-medium-uppercase text-text-tertiary'>{appDetail.mode === 'advanced-chat' ? t('app.types.advanced') : appDetail.mode === 'agent-chat' ? t('app.types.agent') : appDetail.mode === 'chat' ? t('app.types.chatbot') : appDetail.mode === 'completion' ? t('app.types.completion') : t('app.types.workflow')}</div>
</div>
)
}
@ -221,9 +221,9 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
<ContentDialog
show={open}
onClose={() => setOpen(false)}
className='!p-0 flex flex-col absolute left-2 top-2 bottom-2 w-[420px] rounded-2xl'
className='absolute bottom-2 left-2 top-2 flex w-[420px] flex-col rounded-2xl !p-0'
>
<div className='flex p-4 flex-col justify-center items-start gap-3 self-stretch shrink-0'>
<div className='flex shrink-0 flex-col items-start justify-center gap-3 self-stretch p-4'>
<div className='flex items-center gap-3 self-stretch'>
<AppIcon
size="large"
@ -232,17 +232,17 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
background={appDetail.icon_background}
imageUrl={appDetail.icon_url}
/>
<div className='flex flex-col justify-center items-start grow w-full'>
<div className='text-text-secondary system-md-semibold truncate w-full'>{appDetail.name}</div>
<div className='text-text-tertiary system-2xs-medium-uppercase'>{appDetail.mode === 'advanced-chat' ? t('app.types.chatbot') : appDetail.mode === 'agent-chat' ? t('app.types.agent') : appDetail.mode === 'chat' ? t('app.types.chatbot') : appDetail.mode === 'completion' ? t('app.types.completion') : t('app.types.workflow')}</div>
<div className='flex w-full grow flex-col items-start justify-center'>
<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 === 'advanced-chat' ? t('app.types.advanced') : appDetail.mode === 'agent-chat' ? t('app.types.agent') : appDetail.mode === 'chat' ? t('app.types.chatbot') : appDetail.mode === 'completion' ? t('app.types.completion') : t('app.types.workflow')}</div>
</div>
</div>
{/* description */}
{appDetail.description && (
<div className='text-text-tertiary system-xs-regular'>{appDetail.description}</div>
<div className='system-xs-regular text-text-tertiary'>{appDetail.description}</div>
)}
{/* operations */}
<div className='flex items-center gap-1 self-stretch'>
<div className='flex flex-wrap items-center gap-1 self-stretch'>
<Button
size={'small'}
variant={'secondary'}
@ -252,8 +252,8 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
setShowEditModal(true)
}}
>
<RiEditLine className='w-3.5 h-3.5 text-components-button-secondary-text' />
<span className='text-components-button-secondary-text system-xs-medium'>{t('app.editApp')}</span>
<RiEditLine className='h-3.5 w-3.5 text-components-button-secondary-text' />
<span className='system-xs-medium text-components-button-secondary-text'>{t('app.editApp')}</span>
</Button>
<Button
size={'small'}
@ -264,8 +264,8 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
setShowDuplicateModal(true)
}}
>
<RiFileCopy2Line className='w-3.5 h-3.5 text-components-button-secondary-text' />
<span className='text-components-button-secondary-text system-xs-medium'>{t('app.duplicate')}</span>
<RiFileCopy2Line className='h-3.5 w-3.5 text-components-button-secondary-text' />
<span className='system-xs-medium text-components-button-secondary-text'>{t('app.duplicate')}</span>
</Button>
<Button
size={'small'}
@ -273,8 +273,8 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
className='gap-[1px]'
onClick={exportCheck}
>
<RiFileDownloadLine className='w-3.5 h-3.5 text-components-button-secondary-text' />
<span className='text-components-button-secondary-text system-xs-medium'>{t('app.export')}</span>
<RiFileDownloadLine className='h-3.5 w-3.5 text-components-button-secondary-text' />
<span className='system-xs-medium text-components-button-secondary-text'>{t('app.export')}</span>
</Button>
{
(appDetail.mode === 'advanced-chat' || appDetail.mode === 'workflow') && (
@ -287,8 +287,8 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
setShowImportDSLModal(true)
}}
>
<RiFileUploadLine className='w-3.5 h-3.5 text-components-button-secondary-text' />
<span className='text-components-button-secondary-text system-xs-medium'>{t('workflow.common.importDSL')}</span>
<RiFileUploadLine className='h-3.5 w-3.5 text-components-button-secondary-text' />
<span className='system-xs-medium text-components-button-secondary-text'>{t('workflow.common.importDSL')}</span>
</Button>
)
}
@ -298,10 +298,10 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
<CardView
appId={appDetail.id}
isInPanel={true}
className='flex flex-col px-2 py-1 gap-2 grow overflow-auto'
className='flex grow flex-col gap-2 overflow-auto px-2 py-1'
/>
</div>
<div className='flex p-2 flex-col justify-center items-start gap-3 self-stretch border-t-[0.5px] border-divider-subtle shrink-0 min-h-fit'>
<div className='flex min-h-fit shrink-0 flex-col items-start justify-center gap-3 self-stretch border-t-[0.5px] border-divider-subtle p-2'>
<Button
size={'medium'}
variant={'ghost'}
@ -311,8 +311,8 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
setShowConfirmDelete(true)
}}
>
<RiDeleteBinLine className='w-4 h-4 text-text-tertiary' />
<span className='text-text-tertiary system-sm-medium'>{t('common.operation.deleteApp')}</span>
<RiDeleteBinLine className='h-4 w-4 text-text-tertiary' />
<span className='system-sm-medium text-text-tertiary'>{t('common.operation.deleteApp')}</span>
</Button>
</div>
</ContentDialog>

View File

@ -48,9 +48,9 @@ const NotionSvg = <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xm
const ICON_MAP = {
app: <AppIcon className='border !border-[rgba(0,0,0,0.05)]' />,
api: <AppIcon innerIcon={ApiSvg} className='border !bg-purple-50 !border-purple-200' />,
api: <AppIcon innerIcon={ApiSvg} className='border !border-purple-200 !bg-purple-50' />,
dataset: <AppIcon innerIcon={DatasetSvg} className='!border-[0.5px] !border-indigo-100 !bg-indigo-25' />,
webapp: <AppIcon innerIcon={WebappSvg} className='border !bg-primary-100 !border-primary-200' />,
webapp: <AppIcon innerIcon={WebappSvg} className='border !border-primary-200 !bg-primary-100' />,
notion: <AppIcon innerIcon={NotionSvg} className='!border-[0.5px] !border-indigo-100 !bg-white' />,
}
@ -58,20 +58,20 @@ export default function AppBasic({ icon, icon_background, name, isExternal, type
const { t } = useTranslation()
return (
<div className="flex items-center grow">
<div className="flex grow items-center">
{icon && icon_background && iconType === 'app' && (
<div className='shrink-0 mr-3'>
<div className='mr-3 shrink-0'>
<AppIcon icon={icon} background={icon_background} />
</div>
)}
{iconType !== 'app'
&& <div className='shrink-0 mr-3'>
&& <div className='mr-3 shrink-0'>
{ICON_MAP[iconType]}
</div>
}
{mode === 'expand' && <div className="group">
<div className={`flex flex-row items-center system-md-semibold text-text-secondary group-hover:text-text-primary ${textStyle?.main ?? ''}`}>
<div className={`system-md-semibold flex flex-row items-center text-text-secondary group-hover:text-text-primary ${textStyle?.main ?? ''}`}>
<div className="max-w-[180px] truncate">
{name}
</div>
@ -88,7 +88,7 @@ export default function AppBasic({ icon, icon_background, name, isExternal, type
/>
}
</div>
<div className='text-text-tertiary system-2xs-medium-uppercase'>{isExternal ? t('dataset.externalTag') : ''}</div>
<div className='system-2xs-medium-uppercase text-text-tertiary'>{isExternal ? t('dataset.externalTag') : ''}</div>
</div>}
</div>
)

View File

@ -26,7 +26,7 @@ const DatasetInfo: FC<Props> = ({
const { t } = useTranslation()
return (
<div className='pl-1 pt-1'>
<div className='flex-shrink-0 mr-3'>
<div className='mr-3 shrink-0'>
<AppIcon innerIcon={DatasetSvg} className='!border-[0.5px] !border-indigo-100 !bg-indigo-25' />
</div>
{expand && (
@ -34,8 +34,8 @@ const DatasetInfo: FC<Props> = ({
<div className='system-md-semibold text-text-secondary'>
{name}
</div>
<div className='mt-1 text-text-tertiary system-2xs-medium-uppercase'>{isExternal ? t('dataset.externalTag') : t('dataset.localDocs')}</div>
<div className='my-3 system-xs-regular text-text-tertiary first-letter:capitalize'>{description}</div>
<div className='system-2xs-medium-uppercase mt-1 text-text-tertiary'>{isExternal ? t('dataset.externalTag') : t('dataset.localDocs')}</div>
<div className='system-xs-regular my-3 text-text-tertiary first-letter:capitalize'>{description}</div>
</div>
)}
{extraInfo}

View File

@ -50,7 +50,7 @@ const AppDetailNav = ({ title, desc, isExternal, icon, icon_background, navigati
return (
<div
className={`
shrink-0 flex flex-col bg-background-default-subtle border-r border-divider-burn transition-all
flex shrink-0 flex-col border-r border-divider-burn bg-background-default-subtle transition-all
${expand ? 'w-[216px]' : 'w-14'}
`}
>
@ -85,7 +85,7 @@ const AppDetailNav = ({ title, desc, isExternal, icon, icon_background, navigati
)}
</div>
<div className='px-4'>
<div className={cn('mt-1 mx-auto h-[1px] bg-divider-subtle', !expand && 'w-6')} />
<div className={cn('mx-auto mt-1 h-[1px] bg-divider-subtle', !expand && 'w-6')} />
</div>
<nav
className={`
@ -108,13 +108,13 @@ const AppDetailNav = ({ title, desc, isExternal, icon, icon_background, navigati
`}
>
<div
className='flex items-center justify-center w-6 h-6 text-gray-500 cursor-pointer'
className='flex h-6 w-6 cursor-pointer items-center justify-center text-gray-500'
onClick={() => handleToggle(appSidebarExpand)}
>
{
expand
? <RiLayoutRight2Line className='w-5 h-5 text-components-menu-item-text' />
: <LayoutRight2LineMod className='w-5 h-5 text-components-menu-item-text' />
? <RiLayoutRight2Line className='h-5 w-5 text-components-menu-item-text' />
: <LayoutRight2LineMod className='h-5 w-5 text-components-menu-item-text' />
}
</div>
</div>

View File

@ -21,17 +21,17 @@ const EditItem: FC<Props> = ({
onChange,
}) => {
const { t } = useTranslation()
const avatar = type === EditItemType.Query ? <User className='w-6 h-6' /> : <Robot className='w-6 h-6' />
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')
return (
<div className='flex' onClick={e => e.stopPropagation()}>
<div className='shrink-0 mr-3'>
<div className='mr-3 shrink-0'>
{avatar}
</div>
<div className='grow'>
<div className='mb-1 system-xs-semibold text-text-primary'>{name}</div>
<div className='system-xs-semibold mb-1 text-text-primary'>{name}</div>
<Textarea
value={content}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => onChange(e.target.value)}

View File

@ -76,7 +76,7 @@ const AddAnnotationModal: FC<Props> = ({
maxWidthClassName='!max-w-[480px]'
title={t('appAnnotation.addModal.title') as string}
body={(
<div className='p-6 pb-4 space-y-6'>
<div className='space-y-6 p-6 pb-4'>
<EditItem
type={EditItemType.Query}
content={question}
@ -93,11 +93,11 @@ const AddAnnotationModal: FC<Props> = ({
(
<div>
{isAnnotationFull && (
<div className='mt-6 mb-4 px-6'>
<div className='mb-4 mt-6 px-6'>
<AnnotationFull />
</div>
)}
<div className='px-4 flex h-16 items-center justify-between border-t border-divider-subtle bg-background-section-burn rounded-bl-xl rounded-br-xl system-sm-medium text-text-tertiary'>
<div className='system-sm-medium flex h-16 items-center justify-between rounded-bl-xl rounded-br-xl border-t border-divider-subtle bg-background-section-burn px-4 text-text-tertiary'>
<div
className='flex items-center space-x-2'
>

View File

@ -35,17 +35,17 @@ const CSVDownload: FC = () => {
<div className='mt-6'>
<div className='system-sm-medium text-text-primary'>{t('share.generation.csvStructureTitle')}</div>
<div className='mt-2 max-h-[500px] overflow-auto'>
<table className='table-fixed w-full border-separate border-spacing-0 border border-divider-regular rounded-lg text-xs'>
<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 pl-3 pr-2 border-b border-divider-regular'>{t('appAnnotation.batchModal.question')}</td>
<td className='h-9 pl-3 pr-2 border-b border-divider-regular'>{t('appAnnotation.batchModal.answer')}</td>
<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>
</tr>
</thead>
<tbody className='text-text-secondary'>
<tr>
<td className='h-9 pl-3 pr-2 border-b border-divider-subtle text-[13px]'>{t('appAnnotation.batchModal.question')} 1</td>
<td className='h-9 pl-3 pr-2 border-b border-divider-subtle text-[13px]'>{t('appAnnotation.batchModal.answer')} 1</td>
<td className='h-9 border-b border-divider-subtle pl-3 pr-2 text-[13px]'>{t('appAnnotation.batchModal.question')} 1</td>
<td className='h-9 border-b border-divider-subtle pl-3 pr-2 text-[13px]'>{t('appAnnotation.batchModal.answer')} 1</td>
</tr>
<tr>
<td className='h-9 pl-3 pr-2 text-[13px]'>{t('appAnnotation.batchModal.question')} 2</td>
@ -55,14 +55,14 @@ const CSVDownload: FC = () => {
</table>
</div>
<CSVDownloader
className="block mt-2 cursor-pointer"
className="mt-2 block cursor-pointer"
type={Type.Link}
filename={`template-${locale}`}
bom={true}
data={getTemplate()}
>
<div className='flex items-center h-[18px] space-x-1 text-text-accent system-xs-medium'>
<DownloadIcon className='w-3 h-3 mr-1' />
<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')}
</div>
</CSVDownloader>

View File

@ -91,29 +91,29 @@ const CSVUploader: FC<Props> = ({
/>
<div ref={dropRef}>
{!file && (
<div className={cn('flex items-center h-20 rounded-xl bg-components-dropzone-bg border border-dashed border-components-dropzone-border system-sm-regular', dragging && 'bg-components-dropzone-bg-accent border border-components-dropzone-border-accent')}>
<div className='w-full flex items-center justify-center space-x-2'>
<div className={cn('system-sm-regular flex h-20 items-center rounded-xl border border-dashed border-components-dropzone-border bg-components-dropzone-bg', dragging && 'border border-components-dropzone-border-accent bg-components-dropzone-bg-accent')}>
<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='text-text-accent cursor-pointer' onClick={selectHandle}>{t('appAnnotation.batchModal.browse')}</span>
<span className='cursor-pointer text-text-accent' onClick={selectHandle}>{t('appAnnotation.batchModal.browse')}</span>
</div>
</div>
{dragging && <div ref={dragRef} className='absolute w-full h-full top-0 left-0' />}
{dragging && <div ref={dragRef} className='absolute left-0 top-0 h-full w-full' />}
</div>
)}
{file && (
<div className={cn('flex items-center h-20 px-6 rounded-xl bg-components-panel-bg border border-components-panel-border text-sm font-normal group', 'hover:bg-components-panel-bg-blur hover:border-components-panel-bg-blur')}>
<div className={cn('group flex h-20 items-center rounded-xl border border-components-panel-border bg-components-panel-bg px-6 text-sm font-normal', 'hover:border-components-panel-bg-blur hover:bg-components-panel-bg-blur')}>
<CSVIcon className="shrink-0" />
<div className='flex ml-2 w-0 grow'>
<span className='max-w-[calc(100%_-_30px)] text-ellipsis whitespace-nowrap overflow-hidden text-text-primary'>{file.name.replace(/.csv$/, '')}</span>
<div className='ml-2 flex w-0 grow'>
<span className='max-w-[calc(100%_-_30px)] overflow-hidden text-ellipsis whitespace-nowrap text-text-primary'>{file.name.replace(/.csv$/, '')}</span>
<span className='shrink-0 text-text-tertiary'>.csv</span>
</div>
<div className='hidden group-hover:flex items-center'>
<div className='hidden items-center group-hover:flex'>
<Button variant='secondary' onClick={selectHandle}>{t('datasetCreation.stepOne.uploader.change')}</Button>
<div className='mx-2 w-px h-4 bg-divider-regular' />
<div className='p-2 cursor-pointer' onClick={removeFile}>
<RiDeleteBinLine className='w-4 h-4 text-text-tertiary' />
<div className='mx-2 h-4 w-px bg-divider-regular' />
<div className='cursor-pointer p-2' onClick={removeFile}>
<RiDeleteBinLine className='h-4 w-4 text-text-tertiary' />
</div>
</div>
</div>

View File

@ -87,10 +87,10 @@ const BatchModal: FC<IBatchModalProps> = ({
}
return (
<Modal isShow={isShow} onClose={() => { }} className='px-8 py-6 !max-w-[520px] !rounded-xl'>
<div className='relative pb-1 system-xl-medium text-text-primary'>{t('appAnnotation.batchModal.title')}</div>
<div className='absolute right-4 top-4 p-2 cursor-pointer' onClick={onCancel}>
<RiCloseLine className='w-4 h-4 text-text-tertiary' />
<Modal isShow={isShow} onClose={() => { }} 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='absolute right-4 top-4 cursor-pointer p-2' onClick={onCancel}>
<RiCloseLine className='h-4 w-4 text-text-tertiary' />
</div>
<CSVUploader
file={currentCSV}
@ -104,8 +104,8 @@ const BatchModal: FC<IBatchModalProps> = ({
</div>
)}
<div className='mt-[28px] pt-6 flex justify-end'>
<Button className='mr-2 text-text-tertiary system-sm-medium' onClick={onCancel}>
<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')}
</Button>
<Button

View File

@ -20,11 +20,11 @@ type Props = {
}
export const EditTitle: FC<{ className?: string; title: string }> = ({ className, title }) => (
<div className={cn(className, 'flex items-center h-[18px] system-xs-medium text-text-tertiary')}>
<RiEditFill className='mr-1 w-3.5 h-3.5' />
<div className={cn(className, 'system-xs-medium flex h-[18px] items-center text-text-tertiary')}>
<RiEditFill className='mr-1 h-3.5 w-3.5' />
<div>{title}</div>
<div
className='ml-2 grow h-[1px]'
className='ml-2 h-[1px] grow'
style={{
background: 'linear-gradient(90deg, rgba(0, 0, 0, 0.05) -1.65%, rgba(0, 0, 0, 0.00) 100%)',
}}
@ -40,7 +40,7 @@ const EditItem: FC<Props> = ({
const { t } = useTranslation()
const [newContent, setNewContent] = useState('')
const showNewContent = newContent && newContent !== content
const avatar = type === EditItemType.Query ? <User className='w-6 h-6' /> : <Robot className='w-6 h-6' />
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')
@ -58,11 +58,11 @@ const EditItem: FC<Props> = ({
return (
<div className='flex' onClick={e => e.stopPropagation()}>
<div className='shrink-0 mr-3'>
<div className='mr-3 shrink-0'>
{avatar}
</div>
<div className='grow'>
<div className='mb-1 system-xs-semibold text-text-primary'>{name}</div>
<div className='system-xs-semibold mb-1 text-text-primary'>{name}</div>
<div className='system-sm-regular text-text-primary'>{content}</div>
{!isEdit
? (
@ -70,34 +70,34 @@ const EditItem: FC<Props> = ({
{showNewContent && (
<div className='mt-3'>
<EditTitle title={editTitle} />
<div className='mt-1 system-sm-regular text-text-primary'>{newContent}</div>
<div className='system-sm-regular mt-1 text-text-primary'>{newContent}</div>
</div>
)}
<div className='mt-2 flex items-center'>
{!readonly && (
<div
className='flex items-center space-x-1 system-xs-medium text-text-accent cursor-pointer'
className='system-xs-medium flex cursor-pointer items-center space-x-1 text-text-accent'
onClick={() => {
setIsEdit(true)
}}
>
<RiEditLine className='mr-1 w-3.5 h-3.5' />
<RiEditLine className='mr-1 h-3.5 w-3.5' />
<div>{t('common.operation.edit')}</div>
</div>
)}
{showNewContent && (
<div className='ml-2 flex items-center system-xs-medium text-text-tertiary'>
<div className='system-xs-medium ml-2 flex items-center text-text-tertiary'>
<div className='mr-2'>·</div>
<div
className='flex items-center space-x-1 cursor-pointer'
className='flex cursor-pointer items-center space-x-1'
onClick={() => {
setNewContent(content)
onSave(content)
}}
>
<div className='w-3.5 h-3.5'>
<RiDeleteBinLine className='w-3.5 h-3.5' />
<div className='h-3.5 w-3.5'>
<RiDeleteBinLine className='h-3.5 w-3.5' />
</div>
<div>{t('common.operation.delete')}</div>
</div>

View File

@ -86,7 +86,7 @@ const EditAnnotationModal: FC<Props> = ({
title={t('appAnnotation.editModal.title') as string}
body={(
<div>
<div className='p-6 pb-4 space-y-6'>
<div className='space-y-6 p-6 pb-4'>
<EditItem
type={EditItemType.Query}
content={query}
@ -115,7 +115,7 @@ const EditAnnotationModal: FC<Props> = ({
foot={
<div>
{isAnnotationFull && (
<div className='mt-6 mb-4 px-6'>
<div className='mb-4 mt-6 px-6'>
<AnnotationFull />
</div>
)}
@ -123,9 +123,9 @@ const EditAnnotationModal: FC<Props> = ({
{
annotationId
? (
<div className='px-4 flex h-16 items-center justify-between border-t border-divider-subtle bg-background-section-burn rounded-bl-xl rounded-br-xl system-sm-medium text-text-tertiary'>
<div className='system-sm-medium flex h-16 items-center justify-between rounded-bl-xl rounded-br-xl border-t border-divider-subtle bg-background-section-burn px-4 text-text-tertiary'>
<div
className='flex items-center pl-3 space-x-2 cursor-pointer'
className='flex cursor-pointer items-center space-x-2 pl-3'
onClick={() => setShowModal(true)}
>
<MessageCheckRemove />

Some files were not shown because too many files have changed in this diff Show More