Compare commits

..

10 Commits

748 changed files with 8868 additions and 48444 deletions

View File

@@ -1,12 +1,11 @@
#!/bin/bash
npm add -g pnpm@9.12.2
cd web && pnpm install
cd web && npm install
pipx install poetry
echo 'alias start-api="cd /workspaces/dify/api && poetry run python -m flask run --host 0.0.0.0 --port=5001 --debug"' >> ~/.bashrc
echo 'alias start-worker="cd /workspaces/dify/api && poetry run python -m celery -A app.celery worker -P gevent -c 1 --loglevel INFO -Q dataset,generation,mail,ops_trace,app_deletion"' >> ~/.bashrc
echo 'alias start-web="cd /workspaces/dify/web && pnpm dev"' >> ~/.bashrc
echo 'alias start-web="cd /workspaces/dify/web && npm run dev"' >> ~/.bashrc
echo 'alias start-containers="cd /workspaces/dify/docker && docker-compose -f docker-compose.middleware.yaml -p dify up -d"' >> ~/.bashrc
echo 'alias stop-containers="cd /workspaces/dify/docker && docker-compose -f docker-compose.middleware.yaml -p dify down"' >> ~/.bashrc

View File

@@ -71,16 +71,16 @@ jobs:
if: steps.changed-files.outputs.any_changed == 'true'
with:
node-version: 20
cache: pnpm
cache: yarn
cache-dependency-path: ./web/package.json
- name: Web dependencies
if: steps.changed-files.outputs.any_changed == 'true'
run: pnpm install --frozen-lockfile
run: yarn install --frozen-lockfile
- name: Web style check
if: steps.changed-files.outputs.any_changed == 'true'
run: pnpm run lint
run: yarn run lint
docker-compose-template:
name: Docker Compose Template

View File

@@ -32,10 +32,10 @@ jobs:
with:
node-version: ${{ matrix.node-version }}
cache: ''
cache-dependency-path: 'pnpm-lock.yaml'
cache-dependency-path: 'yarn.lock'
- name: Install Dependencies
run: pnpm install
run: yarn install
- name: Test
run: pnpm test
run: yarn test

View File

@@ -38,11 +38,11 @@ jobs:
- name: Install dependencies
if: env.FILES_CHANGED == 'true'
run: pnpm install --frozen-lockfile
run: yarn install --frozen-lockfile
- name: Run npm script
if: env.FILES_CHANGED == 'true'
run: pnpm run auto-gen-i18n
run: npm run auto-gen-i18n
- name: Create Pull Request
if: env.FILES_CHANGED == 'true'

View File

@@ -34,13 +34,13 @@ jobs:
if: steps.changed-files.outputs.any_changed == 'true'
with:
node-version: 20
cache: pnpm
cache: yarn
cache-dependency-path: ./web/package.json
- name: Install dependencies
if: steps.changed-files.outputs.any_changed == 'true'
run: pnpm install --frozen-lockfile
run: yarn install --frozen-lockfile
- name: Run tests
if: steps.changed-files.outputs.any_changed == 'true'
run: pnpm test
run: yarn test

3
.gitignore vendored
View File

@@ -193,6 +193,3 @@ api/.vscode
.idea/
.vscode
# pnpm
/.pnpm-store

View File

@@ -7,4 +7,4 @@ api = ExternalApi(bp)
from . import index
from .app import app, audio, completion, conversation, file, message, workflow
from .dataset import dataset, document, hit_testing, segment
from .dataset import dataset, document, hit_testing, segment, upload_file

View File

@@ -0,0 +1,54 @@
from werkzeug.exceptions import NotFound
from controllers.service_api import api
from controllers.service_api.wraps import (
DatasetApiResource,
)
from core.file import helpers as file_helpers
from extensions.ext_database import db
from models.dataset import Dataset
from models.model import UploadFile
from services.dataset_service import DocumentService
class UploadFileApi(DatasetApiResource):
def get(self, tenant_id, dataset_id, document_id):
"""Get upload file."""
# check dataset
dataset_id = str(dataset_id)
tenant_id = str(tenant_id)
dataset = db.session.query(Dataset).filter(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id).first()
if not dataset:
raise NotFound("Dataset not found.")
# check document
document_id = str(document_id)
document = DocumentService.get_document(dataset.id, document_id)
if not document:
raise NotFound("Document not found.")
# check upload file
if document.data_source_type != "upload_file":
raise ValueError(f"Document data source type ({document.data_source_type}) is not upload_file.")
data_source_info = document.data_source_info_dict
if data_source_info and "upload_file_id" in data_source_info:
file_id = data_source_info["upload_file_id"]
upload_file = db.session.query(UploadFile).filter(UploadFile.id == file_id).first()
if not upload_file:
raise NotFound("UploadFile not found.")
else:
raise ValueError("Upload file id not found in document data source info.")
url = file_helpers.get_signed_file_url(upload_file_id=upload_file.id)
return {
"id": upload_file.id,
"name": upload_file.name,
"size": upload_file.size,
"extension": upload_file.extension,
"url": url,
"download_url": f"{url}&as_attachment=true",
"mime_type": upload_file.mime_type,
"created_by": upload_file.created_by,
"created_at": upload_file.created_at.timestamp(),
}, 200
api.add_resource(UploadFileApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/upload-file")

View File

@@ -61,20 +61,17 @@ def make_request(method, url, max_retries=SSRF_DEFAULT_MAX_RETRIES, **kwargs):
if response.status_code not in STATUS_FORCELIST:
return response
else:
logging.warning(
f"Received status code {response.status_code} for URL {url} which is in the force list")
logging.warning(f"Received status code {response.status_code} for URL {url} which is in the force list")
except httpx.RequestError as e:
logging.warning(f"Request to URL {url} failed on attempt {
retries + 1}: {e}")
logging.warning(f"Request to URL {url} failed on attempt {retries + 1}: {e}")
if max_retries == 0:
raise
retries += 1
if retries <= max_retries:
time.sleep(BACKOFF_FACTOR * (2 ** (retries - 1)))
raise MaxRetriesExceededError(
f"Reached maximum retries ({max_retries}) for URL {url}")
raise MaxRetriesExceededError(f"Reached maximum retries ({max_retries}) for URL {url}")
def get(url, max_retries=SSRF_DEFAULT_MAX_RETRIES, **kwargs):

View File

@@ -530,7 +530,6 @@ class IndexingRunner:
# chunk nodes by chunk size
indexing_start_at = time.perf_counter()
tokens = 0
chunk_size = 10
if dataset_document.doc_form != IndexType.PARENT_CHILD_INDEX:
# create keyword index
create_keyword_thread = threading.Thread(
@@ -539,11 +538,22 @@ class IndexingRunner:
)
create_keyword_thread.start()
max_workers = 10
if dataset.indexing_technique == "high_quality":
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = []
for i in range(0, len(documents), chunk_size):
chunk_documents = documents[i : i + chunk_size]
# Distribute documents into multiple groups based on the hash values of page_content
# This is done to prevent multiple threads from processing the same document,
# Thereby avoiding potential database insertion deadlocks
document_groups: list[list[Document]] = [[] for _ in range(max_workers)]
for document in documents:
hash = helper.generate_text_hash(document.page_content)
group_index = int(hash, 16) % max_workers
document_groups[group_index].append(document)
for chunk_documents in document_groups:
if len(chunk_documents) == 0:
continue
futures.append(
executor.submit(
self._process_chunk,

View File

@@ -1,6 +1,9 @@
import logging
from threading import Lock
from typing import Any
logger = logging.getLogger(__name__)
_tokenizer: Any = None
_lock = Lock()
@@ -43,5 +46,6 @@ class GPT2Tokenizer:
base_path = abspath(__file__)
gpt2_tokenizer_path = join(dirname(base_path), "gpt2")
_tokenizer = TransformerGPT2Tokenizer.from_pretrained(gpt2_tokenizer_path)
logger.info("Fallback to Transformers' GPT-2 tokenizer from tiktoken")
return _tokenizer

View File

@@ -17,8 +17,7 @@ from extensions.ext_redis import redis_client
from models.dataset import Dataset
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s")
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
logging.getLogger("lindorm").setLevel(logging.WARN)
ROUTING_FIELD = "routing_field"
@@ -135,8 +134,7 @@ class LindormVectorStore(BaseVector):
self._client.delete(index=self._collection_name, id=id, params=params)
self.refresh()
else:
logger.warning(
f"DELETE BY ID: ID {id} does not exist in the index.")
logger.warning(f"DELETE BY ID: ID {id} does not exist in the index.")
def delete(self) -> None:
if self._using_ugc:
@@ -147,8 +145,7 @@ class LindormVectorStore(BaseVector):
self.refresh()
else:
if self._client.indices.exists(index=self._collection_name):
self._client.indices.delete(
index=self._collection_name, params={"timeout": 60})
self._client.indices.delete(index=self._collection_name, params={"timeout": 60})
logger.info("Delete index success")
else:
logger.warning(f"Index '{self._collection_name}' does not exist. No deletion performed.")
@@ -171,8 +168,7 @@ class LindormVectorStore(BaseVector):
raise ValueError("All elements in query_vector should be floats")
top_k = kwargs.get("top_k", 10)
query = default_vector_search_query(
query_vector=query_vector, k=top_k, **kwargs)
query = default_vector_search_query(query_vector=query_vector, k=top_k, **kwargs)
try:
params = {}
if self._using_ugc:
@@ -224,8 +220,7 @@ class LindormVectorStore(BaseVector):
routing=routing,
routing_field=self._routing_field,
)
response = self._client.search(
index=self._collection_name, body=full_text_query)
response = self._client.search(index=self._collection_name, body=full_text_query)
docs = []
for hit in response["hits"]["hits"]:
docs.append(
@@ -243,8 +238,7 @@ class LindormVectorStore(BaseVector):
with redis_client.lock(lock_name, timeout=20):
collection_exist_cache_key = f"vector_indexing_{self._collection_name}"
if redis_client.get(collection_exist_cache_key):
logger.info(
f"Collection {self._collection_name} already exists.")
logger.info(f"Collection {self._collection_name} already exists.")
return
if self._client.indices.exists(index=self._collection_name):
logger.info(f"{self._collection_name.lower()} already exists.")
@@ -264,13 +258,10 @@ class LindormVectorStore(BaseVector):
hnsw_ef_construction = kwargs.pop("hnsw_ef_construction", 500)
ivfpq_m = kwargs.pop("ivfpq_m", dimension)
nlist = kwargs.pop("nlist", 1000)
centroids_use_hnsw = kwargs.pop(
"centroids_use_hnsw", True if nlist >= 5000 else False)
centroids_use_hnsw = kwargs.pop("centroids_use_hnsw", True if nlist >= 5000 else False)
centroids_hnsw_m = kwargs.pop("centroids_hnsw_m", 24)
centroids_hnsw_ef_construct = kwargs.pop(
"centroids_hnsw_ef_construct", 500)
centroids_hnsw_ef_search = kwargs.pop(
"centroids_hnsw_ef_search", 100)
centroids_hnsw_ef_construct = kwargs.pop("centroids_hnsw_ef_construct", 500)
centroids_hnsw_ef_search = kwargs.pop("centroids_hnsw_ef_search", 100)
mapping = default_text_mapping(
dimension,
method_name,
@@ -290,8 +281,7 @@ class LindormVectorStore(BaseVector):
using_ugc=self._using_ugc,
**kwargs,
)
self._client.indices.create(
index=self._collection_name.lower(), body=mapping)
self._client.indices.create(index=self._collection_name.lower(), body=mapping)
redis_client.set(collection_exist_cache_key, 1, ex=3600)
# logger.info(f"create index success: {self._collection_name}")
@@ -357,8 +347,7 @@ def default_text_mapping(dimension: int, method_name: str, **kwargs: Any) -> dic
}
if excludes_from_source:
# e.g. {"excludes": ["vector_field"]}
mapping["mappings"]["_source"] = {"excludes": excludes_from_source}
mapping["mappings"]["_source"] = {"excludes": excludes_from_source} # e.g. {"excludes": ["vector_field"]}
if using_ugc and method_name == "ivfpq":
mapping["settings"]["index"]["knn_routing"] = True
@@ -396,8 +385,7 @@ def default_text_search_query(
# build complex search_query when either of must/must_not/should/filter is specified
if must:
if not isinstance(must, list):
raise RuntimeError(
f"unexpected [must] clause with {type(filters)}")
raise RuntimeError(f"unexpected [must] clause with {type(filters)}")
if query_clause not in must:
must.append(query_clause)
else:
@@ -407,22 +395,19 @@ def default_text_search_query(
if must_not:
if not isinstance(must_not, list):
raise RuntimeError(
f"unexpected [must_not] clause with {type(filters)}")
raise RuntimeError(f"unexpected [must_not] clause with {type(filters)}")
boolean_query["must_not"] = must_not
if should:
if not isinstance(should, list):
raise RuntimeError(
f"unexpected [should] clause with {type(filters)}")
raise RuntimeError(f"unexpected [should] clause with {type(filters)}")
boolean_query["should"] = should
if minimum_should_match != 0:
boolean_query["minimum_should_match"] = minimum_should_match
if filters:
if not isinstance(filters, list):
raise RuntimeError(
f"unexpected [filter] clause with {type(filters)}")
raise RuntimeError(f"unexpected [filter] clause with {type(filters)}")
boolean_query["filter"] = filters
search_query = {"size": k, "query": {"bool": boolean_query}}

View File

@@ -50,7 +50,7 @@ class WordExtractor(BaseExtractor):
self.web_path = self.file_path
# TODO: use a better way to handle the file
self.temp_file = tempfile.NamedTemporaryFile()
self.temp_file = tempfile.NamedTemporaryFile() # noqa: SIM115
self.temp_file.write(r.content)
self.file_path = self.temp_file.name
elif not os.path.isfile(self.file_path):

View File

@@ -38,6 +38,7 @@ class AliYuqueDescribeDocumentContentTool(AliYuqueTool, BuiltinTool):
book_id = index_page.get("data", {}).get("book", {}).get("id")
if not book_id:
raise Exception(f"can not parse book_id from {index_page}")
new_params["book_id"] = book_id
new_params["id"] = doc_id
data = self.request("GET", token, new_params, "/api/v2/repos/{book_id}/docs/{id}")

View File

@@ -112,7 +112,7 @@ class ApiBasedToolSchemaParser:
llm_description=property.get("description", ""),
default=property.get("default", None),
placeholder=I18nObject(
en_US=parameter.get("description", ""), zh_Hans=parameter.get("description", "")
en_US=property.get("description", ""), zh_Hans=property.get("description", "")
),
)

View File

@@ -44,13 +44,11 @@ class QuestionClassifierNode(LLMNode):
variable_pool = self.graph_runtime_state.variable_pool
# extract variables
variable = variable_pool.get(
node_data.query_variable_selector) if node_data.query_variable_selector else None
variable = variable_pool.get(node_data.query_variable_selector) if node_data.query_variable_selector else None
query = variable.value if variable else None
variables = {"query": query}
# fetch model config
model_instance, model_config = self._fetch_model_config(
node_data.model)
model_instance, model_config = self._fetch_model_config(node_data.model)
# fetch memory
memory = self._fetch_memory(
node_data_memory=node_data.memory,
@@ -58,8 +56,7 @@ class QuestionClassifierNode(LLMNode):
)
# fetch instruction
node_data.instruction = node_data.instruction or ""
node_data.instruction = variable_pool.convert_template(
node_data.instruction).text
node_data.instruction = variable_pool.convert_template(node_data.instruction).text
files = (
self._fetch_files(
@@ -181,15 +178,12 @@ class QuestionClassifierNode(LLMNode):
variable_mapping = {"query": node_data.query_variable_selector}
variable_selectors = []
if node_data.instruction:
variable_template_parser = VariableTemplateParser(
template=node_data.instruction)
variable_selectors.extend(
variable_template_parser.extract_variable_selectors())
variable_template_parser = VariableTemplateParser(template=node_data.instruction)
variable_selectors.extend(variable_template_parser.extract_variable_selectors())
for variable_selector in variable_selectors:
variable_mapping[variable_selector.variable] = variable_selector.value_selector
variable_mapping = {node_id + "." + key: value for key,
value in variable_mapping.items()}
variable_mapping = {node_id + "." + key: value for key, value in variable_mapping.items()}
return variable_mapping
@@ -210,8 +204,7 @@ class QuestionClassifierNode(LLMNode):
context: Optional[str],
) -> int:
prompt_transform = AdvancedPromptTransform(with_variable_tmpl=True)
prompt_template = self._get_prompt_template(
node_data, query, None, 2000)
prompt_template = self._get_prompt_template(node_data, query, None, 2000)
prompt_messages = prompt_transform.get_prompt(
prompt_template=prompt_template,
inputs={},
@@ -224,15 +217,13 @@ class QuestionClassifierNode(LLMNode):
)
rest_tokens = 2000
model_context_tokens = model_config.model_schema.model_properties.get(
ModelPropertyKey.CONTEXT_SIZE)
model_context_tokens = model_config.model_schema.model_properties.get(ModelPropertyKey.CONTEXT_SIZE)
if model_context_tokens:
model_instance = ModelInstance(
provider_model_bundle=model_config.provider_model_bundle, model=model_config.model
)
curr_message_tokens = model_instance.get_llm_num_tokens(
prompt_messages)
curr_message_tokens = model_instance.get_llm_num_tokens(prompt_messages)
max_tokens = 0
for parameter_rule in model_config.model_schema.parameter_rules:
@@ -273,8 +264,7 @@ class QuestionClassifierNode(LLMNode):
prompt_messages: list[LLMNodeChatModelMessage] = []
if model_mode == ModelMode.CHAT:
system_prompt_messages = LLMNodeChatModelMessage(
role=PromptMessageRole.SYSTEM, text=QUESTION_CLASSIFIER_SYSTEM_PROMPT.format(
histories=memory_str)
role=PromptMessageRole.SYSTEM, text=QUESTION_CLASSIFIER_SYSTEM_PROMPT.format(histories=memory_str)
)
prompt_messages.append(system_prompt_messages)
user_prompt_message_1 = LLMNodeChatModelMessage(
@@ -315,5 +305,4 @@ class QuestionClassifierNode(LLMNode):
)
else:
raise InvalidModelTypeError(
f"Model mode {model_mode} not support.")
raise InvalidModelTypeError(f"Model mode {model_mode} not support.")

View File

@@ -10,6 +10,7 @@ from collections.abc import Generator, Mapping
from datetime import datetime
from hashlib import sha256
from typing import Any, Optional, Union, cast
from zoneinfo import available_timezones
from flask import Response, stream_with_context
from flask_restful import fields # type: ignore

View File

@@ -68,8 +68,7 @@ def test_executor_with_json_body_and_object_variable():
system_variables={},
user_inputs={},
)
variable_pool.add(["pre_node_id", "object"], {
"name": "John Doe", "age": 30, "email": "john@example.com"})
variable_pool.add(["pre_node_id", "object"], {"name": "John Doe", "age": 30, "email": "john@example.com"})
# Prepare the node data
node_data = HttpRequestNodeData(
@@ -124,8 +123,7 @@ def test_executor_with_json_body_and_nested_object_variable():
system_variables={},
user_inputs={},
)
variable_pool.add(["pre_node_id", "object"], {
"name": "John Doe", "age": 30, "email": "john@example.com"})
variable_pool.add(["pre_node_id", "object"], {"name": "John Doe", "age": 30, "email": "john@example.com"})
# Prepare the node data
node_data = HttpRequestNodeData(

View File

@@ -18,14 +18,6 @@ from models.enums import UserFrom
from models.workflow import WorkflowNodeExecutionStatus, WorkflowType
def test_plain_text_to_dict():
assert _plain_text_to_dict("aa\n cc:") == {"aa": "", "cc": ""}
assert _plain_text_to_dict("aa:bb\n cc:dd") == {"aa": "bb", "cc": "dd"}
assert _plain_text_to_dict("aa:bb\n cc:dd\n") == {"aa": "bb", "cc": "dd"}
assert _plain_text_to_dict("aa:bb\n\n cc : dd\n\n") == {
"aa": "bb", "cc": "dd"}
def test_http_request_node_binary_file(monkeypatch):
data = HttpRequestNodeData(
title="test",
@@ -191,8 +183,7 @@ def test_http_request_node_form_with_file(monkeypatch):
def attr_checker(*args, **kwargs):
assert kwargs["data"] == {"name": "test"}
assert kwargs["files"] == {
"file": (None, b"test", "application/octet-stream")}
assert kwargs["files"] == {"file": (None, b"test", "application/octet-stream")}
return httpx.Response(200, content=b"")
monkeypatch.setattr(

View File

@@ -443,8 +443,6 @@ services:
environment:
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
APP_API_URL: ${APP_API_URL:-}
MARKETPLACE_API_URL: ${MARKETPLACE_API_URL:-}
MARKETPLACE_URL: ${MARKETPLACE_URL:-}
SENTRY_DSN: ${WEB_SENTRY_DSN:-}
NEXT_TELEMETRY_DISABLED: ${NEXT_TELEMETRY_DISABLED:-0}
TEXT_GENERATION_TIMEOUT_MS: ${TEXT_GENERATION_TIMEOUT_MS:-60000}

View File

@@ -0,0 +1,5 @@
{
"presets": [
"@babel/preset-env"
]
}

View File

@@ -10,10 +10,6 @@ NEXT_PUBLIC_API_PREFIX=http://localhost:5001/console/api
# console or api domain.
# example: http://udify.app/api
NEXT_PUBLIC_PUBLIC_API_PREFIX=http://localhost:5001/api
# The APIFREX for MARKETPLACE
NEXT_PUBLIC_MARKETPLACE_API_PREFIX=http://localhost:5002/api
# The URL for MARKETPLACE
NEXT_PUBLIC_MARKETPLACE_URL_PREFIX=
# SENTRY
NEXT_PUBLIC_SENTRY_DSN=
@@ -30,7 +26,5 @@ NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS=60000
# CSP https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
NEXT_PUBLIC_CSP_WHITELIST=
# Github Access Token, used for invoking Github API
NEXT_PUBLIC_GITHUB_ACCESS_TOKEN=
# The maximum number of top-k value for RAG.
NEXT_PUBLIC_TOP_K_MAX_VALUE=10

7
web/.eslintignore Normal file
View File

@@ -0,0 +1,7 @@
/**/node_modules/*
node_modules/
dist/
build/
out/
.next/

31
web/.eslintrc.json Normal file
View File

@@ -0,0 +1,31 @@
{
"extends": [
"next",
"@antfu",
"plugin:storybook/recommended"
],
"rules": {
"@typescript-eslint/consistent-type-definitions": [
"error",
"type"
],
"@typescript-eslint/no-var-requires": "off",
"no-console": "off",
"indent": "off",
"@typescript-eslint/indent": [
"error",
2,
{
"SwitchCase": 1,
"flatTernaryExpressions": false,
"ignoredNodes": [
"PropertyDefinition[decorators]",
"TSUnionType",
"FunctionExpression[params]:has(Identifier[decorators])"
]
}
],
"react-hooks/exhaustive-deps": "warn",
"react/display-name": "warn"
}
}

7
web/.gitignore vendored
View File

@@ -44,11 +44,12 @@ package-lock.json
.pnp.cjs
.pnp.loader.mjs
.yarn/
.yarnrc.yml
# pmpm
pnpm-lock.yaml
.favorites.json
# storybook
/storybook-static
*storybook.log
# mise

View File

@@ -1,3 +1,6 @@
#!/usr/bin/env bash
. "$(dirname -- "$0")/_/husky.sh"
# get the list of modified files
files=$(git diff --cached --name-only)
@@ -47,7 +50,7 @@ fi
if $web_modified; then
echo "Running ESLint on web module"
cd ./web || exit 1
lint-staged
npx lint-staged
echo "Running unit tests check"
modified_files=$(git diff --cached --name-only -- utils | grep -v '\.spec\.ts$' || true)
@@ -60,7 +63,7 @@ if $web_modified; then
# check if the test file exists
if [ -f "../$test_file" ]; then
echo "Detected changes in $file, running corresponding unit tests..."
pnpm run test "../$test_file"
npm run test "../$test_file"
if [ $? -ne 0 ]; then
echo "Unit tests failed. Please fix the errors before committing."

View File

@@ -1,19 +1,19 @@
import type { StorybookConfig } from '@storybook/nextjs'
const config: StorybookConfig = {
// stories: ['../stories/**/*.mdx', '../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
stories: ['../app/components/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: [
'@storybook/addon-onboarding',
'@storybook/addon-links',
'@storybook/addon-essentials',
'@chromatic-com/storybook',
'@storybook/addon-interactions',
],
framework: {
name: '@storybook/nextjs',
options: {},
},
staticDirs: ['../public'],
// stories: ['../stories/**/*.mdx', '../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
stories: ['../app/components/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: [
'@storybook/addon-onboarding',
'@storybook/addon-links',
'@storybook/addon-essentials',
'@chromatic-com/storybook',
'@storybook/addon-interactions',
],
framework: {
name: '@storybook/nextjs',
options: {},
},
staticDirs: ['../public'],
}
export default config

View File

@@ -1,6 +1,6 @@
import React from 'react'
import type { Preview } from '@storybook/react'
import { withThemeByDataAttribute } from '@storybook/addon-themes'
import { withThemeByDataAttribute } from '@storybook/addon-themes';
import I18nServer from '../app/components/i18n-server'
import '../app/styles/globals.css'
@@ -8,30 +8,30 @@ import '../app/styles/markdown.scss'
import './storybook.css'
export const decorators = [
withThemeByDataAttribute({
themes: {
light: 'light',
dark: 'dark',
},
defaultTheme: 'light',
attributeName: 'data-theme',
}),
(Story) => {
return <I18nServer>
<Story />
</I18nServer>
},
]
withThemeByDataAttribute({
themes: {
light: 'light',
dark: 'dark',
},
defaultTheme: 'light',
attributeName: 'data-theme',
}),
Story => {
return <I18nServer>
<Story />
</I18nServer>
}
];
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
},
}
export default preview

View File

@@ -21,6 +21,5 @@
"editor.defaultFormatter": "vscode.json-language-features"
},
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true,
"npm.packageManager": "pnpm"
"typescript.enablePromptUseWorkspaceTsdk": true
}

View File

@@ -6,9 +6,6 @@ LABEL maintainer="takatost@gmail.com"
# RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
RUN apk add --no-cache tzdata
RUN npm install -g pnpm@9.12.2
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
# install packages
@@ -17,12 +14,12 @@ FROM base AS packages
WORKDIR /app/web
COPY package.json .
COPY pnpm-lock.yaml .
COPY yarn.lock .
# if you located in China, you can use taobao registry to speed up
# RUN pnpm install --frozen-lockfile --registry https://registry.npmmirror.com/
# RUN yarn install --frozen-lockfile --registry https://registry.npmmirror.com/
RUN pnpm install --frozen-lockfile
RUN yarn install --frozen-lockfile
# build resources
FROM base AS builder
@@ -30,8 +27,7 @@ WORKDIR /app/web
COPY --from=packages /app/web/ .
COPY . .
ENV NODE_OPTIONS="--max-old-space-size=4096"
RUN pnpm build
RUN yarn build
# production stage
@@ -42,8 +38,6 @@ ENV EDITION=SELF_HOSTED
ENV DEPLOY_ENV=PRODUCTION
ENV CONSOLE_API_URL=http://127.0.0.1:5001
ENV APP_API_URL=http://127.0.0.1:5001
ENV MARKETPLACE_API_URL=http://127.0.0.1:5001
ENV MARKETPLACE_URL=http://127.0.0.1:5001
ENV PORT=3000
ENV NEXT_TELEMETRY_DISABLED=1
@@ -63,7 +57,8 @@ COPY docker/entrypoint.sh ./entrypoint.sh
# global runtime packages
RUN pnpm add -g pm2 \
RUN yarn global add pm2 \
&& yarn cache clean \
&& mkdir /.pm2 \
&& chown -R 1001:0 /.pm2 /app/web \
&& chmod -R g=u /.pm2 /app/web

View File

@@ -6,12 +6,14 @@ This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next
### Run by source code
To start the web frontend service, you will need [Node.js v18.x (LTS)](https://nodejs.org/en) and [pnpm version 9.12.2](https://pnpm.io).
To start the web frontend service, you will need [Node.js v18.x (LTS)](https://nodejs.org/en) and [NPM version 8.x.x](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/).
First, install the dependencies:
```bash
pnpm install
npm install
# or
yarn install --frozen-lockfile
```
Then, configure the environment variables. Create a file named `.env.local` in the current directory and copy the contents from `.env.example`. Modify the values of these environment variables according to your requirements:
@@ -41,7 +43,9 @@ NEXT_PUBLIC_SENTRY_DSN=
Finally, run the development server:
```bash
pnpm run dev
npm run dev
# or
yarn dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
@@ -55,19 +59,19 @@ You can start editing the file under folder `app`. The page auto-updates as you
First, build the app for production:
```bash
pnpm run build
npm run build
```
Then, start the server:
```bash
pnpm run start
npm run start
```
If you want to customize the host and port:
```bash
pnpm run start --port=3001 --host=0.0.0.0
npm run start --port=3001 --host=0.0.0.0
```
## Storybook
@@ -77,7 +81,7 @@ This project uses [Storybook](https://storybook.js.org/) for UI component develo
To start the storybook server, run:
```bash
pnpm storybook
yarn storybook
```
Open [http://localhost:6006](http://localhost:6006) with your browser to see the result.
@@ -95,7 +99,7 @@ You can create a test file with a suffix of `.spec` beside the file that to be t
Run test:
```bash
pnpm run test
npm run test
```
If you are not familiar with writing tests, here is some code to refer to:

View File

@@ -2,7 +2,7 @@ import React from 'react'
import Main from '@/app/components/app/log-annotation'
import { PageType } from '@/app/components/base/features/new-feature-panel/annotation-reply/type'
export interface IProps {
export type IProps = {
params: { appId: string }
}

View File

@@ -1,5 +1,5 @@
import React from 'react'
import type { Locale } from '@/i18n'
import { type Locale } from '@/i18n'
import DevelopMain from '@/app/components/develop'
export type IDevelopProps = {

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='flex flex-row items-center mt-8 mb-4 text-gray-900 text-base'>
<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}`) }))}

View File

@@ -12,7 +12,7 @@ const Overview = async ({
params: { appId },
}: IDevelopProps) => {
return (
<div className="h-full px-4 sm:px-12 py-6 overflow-scroll bg-chatbot-bg">
<div className="h-full px-4 sm:px-16 py-6 overflow-scroll">
<ApikeyInfoPanel />
<TracingPanel />
<CardView appId={appId} />

View File

@@ -1,18 +1,20 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import {
RiEqualizer2Line,
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import type { PopupProps } from './config-popup'
import ConfigPopup from './config-popup'
import cn from '@/utils/classnames'
import Button from '@/app/components/base/button'
import { Settings04 } from '@/app/components/base/icons/src/vender/line/general'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
const I18N_PREFIX = 'app.tracing'
type Props = {
readOnly: boolean
className?: string
@@ -26,6 +28,7 @@ const ConfigBtn: FC<Props> = ({
controlShowPopup,
...popupProps
}) => {
const { t } = useTranslation()
const [open, doSetOpen] = useState(false)
const openRef = useRef(open)
const setOpen = useCallback((v: boolean) => {
@@ -47,6 +50,21 @@ const ConfigBtn: FC<Props> = ({
if (popupProps.readOnly && !hasConfigured)
return null
const triggerContent = hasConfigured
? (
<div className={cn(className, 'p-1 rounded-md hover:bg-black/5 cursor-pointer')}>
<Settings04 className='w-4 h-4 text-gray-500' />
</div>
)
: (
<Button variant='primary'
className={cn(className, '!h-8 !px-3 select-none')}
>
<Settings04 className='mr-1 w-4 h-4' />
<span className='text-[13px]'>{t(`${I18N_PREFIX}.config`)}</span>
</Button>
)
return (
<PortalToFollowElem
open={open}
@@ -54,13 +72,11 @@ const ConfigBtn: FC<Props> = ({
placement='bottom-end'
offset={{
mainAxis: 12,
crossAxis: hasConfigured ? 8 : 49,
crossAxis: hasConfigured ? 8 : 0,
}}
>
<PortalToFollowElemTrigger onClick={handleTrigger}>
<div className={cn(className, 'p-1 rounded-md')}>
<RiEqualizer2Line className='w-4 h-4 text-text-tertiary' />
</div>
{triggerContent}
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[11]'>
<ConfigPopup {...popupProps} />

View File

@@ -11,8 +11,6 @@ import ProviderConfigModal from './provider-config-modal'
import Indicator from '@/app/components/header/indicator'
import Switch from '@/app/components/base/switch'
import Tooltip from '@/app/components/base/tooltip'
import Divider from '@/app/components/base/divider'
import cn from '@/utils/classnames'
const I18N_PREFIX = 'app.tracing'
@@ -79,6 +77,7 @@ const ConfigPopup: FC<PopupProps> = ({
className='ml-3'
defaultValue={enabled}
onChange={onStatusChange}
size='l'
disabled={providerAllNotConfigured}
/>
)
@@ -107,15 +106,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='w-[420px] p-4 rounded-2xl bg-white border-[0.5px] border-black/5 shadow-lg'>
<div className='flex justify-between items-center'>
<div className='flex items-center'>
<TracingIcon size='md' className='mr-2' />
<div className='text-text-primary title-2xl-semibold'>{t(`${I18N_PREFIX}.tracing`)}</div>
<div className='leading-[120%] text-[18px] font-semibold text-gray-900'>{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='ml-1.5 text-xs font-semibold text-gray-500 uppercase'>
{t(`${I18N_PREFIX}.${enabled ? 'enabled' : 'disabled'}`)}
</div>
{!readOnly && (
@@ -131,18 +130,19 @@ const ConfigPopup: FC<PopupProps> = ({
: switchContent}
</>
)}
</div>
</div>
<div className='mt-2 system-xs-regular text-text-tertiary'>
<div className='mt-2 leading-4 text-xs font-normal text-gray-500'>
{t(`${I18N_PREFIX}.tracingDescription`)}
</div>
<Divider className='my-3' />
<div className='relative'>
<div className='mt-3 h-px bg-gray-100'></div>
<div className='mt-3'>
{(providerAllConfigured || providerAllNotConfigured)
? (
<>
<div className='system-xs-medium-uppercase text-text-tertiary'>{t(`${I18N_PREFIX}.configProviderTitle.${providerAllConfigured ? 'configured' : 'notConfigured'}`)}</div>
<div className='leading-4 text-xs font-medium text-gray-500 uppercase'>{t(`${I18N_PREFIX}.configProviderTitle.${providerAllConfigured ? 'configured' : 'notConfigured'}`)}</div>
<div className='mt-2 space-y-2'>
{langSmithPanel}
{langfusePanel}
@@ -151,11 +151,11 @@ const ConfigPopup: FC<PopupProps> = ({
)
: (
<>
<div className='system-xs-medium-uppercase text-text-tertiary'>{t(`${I18N_PREFIX}.configProviderTitle.configured`)}</div>
<div className='leading-4 text-xs font-medium text-gray-500 uppercase'>{t(`${I18N_PREFIX}.configProviderTitle.configured`)}</div>
<div className='mt-2'>
{langSmithConfig ? langSmithPanel : langfusePanel}
</div>
<div className='mt-3 system-xs-medium-uppercase text-text-tertiary'>{t(`${I18N_PREFIX}.configProviderTitle.moreProvider`)}</div>
<div className='mt-3 leading-4 text-xs font-medium text-gray-500 uppercase'>{t(`${I18N_PREFIX}.configProviderTitle.moreProvider`)}</div>
<div className='mt-2'>
{!langSmithConfig ? langSmithPanel : langfusePanel}
</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 items-center h-[18px] text-[13px] font-medium text-gray-900')}>{label} </div>
{isRequired && <span className='ml-0.5 text-xs font-semibold text-[#D92D20]'>*</span>}
</div>
<Input

View File

@@ -1,9 +1,6 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useEffect, useState } from 'react'
import {
RiArrowDownDoubleLine,
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { usePathname } from 'next/navigation'
import { useBoolean } from 'ahooks'
@@ -11,6 +8,7 @@ import type { LangFuseConfig, LangSmithConfig } from './type'
import { TracingProvider } from './type'
import TracingIcon from './tracing-icon'
import ConfigButton from './config-button'
import cn from '@/utils/classnames'
import { LangfuseIcon, LangsmithIcon } from '@/app/components/base/icons/src/public/tracing'
import Indicator from '@/app/components/header/indicator'
import { fetchTracingConfig as doFetchTracingConfig, fetchTracingStatus, updateTracingStatus } from '@/service/apps'
@@ -18,8 +16,6 @@ import type { TracingStatus } from '@/models/app'
import Toast from '@/app/components/base/toast'
import { useAppContext } from '@/context/app-context'
import Loading from '@/app/components/base/loading'
import Divider from '@/app/components/base/divider'
import cn from '@/utils/classnames'
const I18N_PREFIX = 'app.tracing'
@@ -31,7 +27,7 @@ const Title = ({
const { t } = useTranslation()
return (
<div className={cn('flex items-center system-xl-semibold text-text-primary', className)}>
<div className={cn(className, 'flex items-center text-lg font-semibold text-gray-900')}>
{t('common.appMenus.overview')}
</div>
)
@@ -139,68 +135,43 @@ const Panel: FC = () => {
return (
<div className={cn('mb-3 flex justify-between items-center')}>
<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',
)}
onClick={showPopup}
>
{!inUseTracingProvider && (
<>
<TracingIcon size='md' />
<div className='mx-2 system-sm-semibold text-text-secondary'>{t(`${I18N_PREFIX}.title`)}</div>
<div className='flex items-center' onClick={e => e.stopPropagation()}>
<ConfigButton
appId={appId}
readOnly={readOnly}
hasConfigured={false}
enabled={enabled}
onStatusChange={handleTracingEnabledChange}
chosenProvider={inUseTracingProvider}
onChooseProvider={handleChooseProvider}
langSmithConfig={langSmithConfig}
langFuseConfig={langFuseConfig}
onConfigUpdated={handleTracingConfigUpdated}
onConfigRemoved={handleTracingConfigRemoved}
controlShowPopup={controlShowPopup}
/>
</div>
<Divider type='vertical' className='h-3.5' />
<div className='p-1 rounded-md'>
<RiArrowDownDoubleLine className='w-4 h-4 text-text-tertiary' />
</div>
<div className='flex items-center p-2 rounded-xl border-[0.5px] border-gray-200 shadow-xs cursor-pointer hover:bg-gray-100' onClick={showPopup}>
{!inUseTracingProvider
? <>
<TracingIcon size='md' className='mr-2' />
<div className='leading-5 text-sm font-semibold text-gray-700'>{t(`${I18N_PREFIX}.title`)}</div>
</>
)}
: <InUseProviderIcon className='ml-1 h-4' />}
{hasConfiguredTracing && (
<>
<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'>
{t(`${I18N_PREFIX}.${enabled ? 'enabled' : 'disabled'}`)}
</div>
<div className='ml-4 mr-1 flex items-center'>
<Indicator color={enabled ? 'green' : 'gray'} />
<div className='ml-1.5 text-xs font-semibold text-gray-500 uppercase'>
{t(`${I18N_PREFIX}.${enabled ? 'enabled' : 'disabled'}`)}
</div>
<InUseProviderIcon className='ml-1 h-4' />
<Divider type='vertical' className='h-3.5' />
<div className='flex items-center' onClick={e => e.stopPropagation()}>
<ConfigButton
appId={appId}
readOnly={readOnly}
hasConfigured
className='ml-2'
enabled={enabled}
onStatusChange={handleTracingEnabledChange}
chosenProvider={inUseTracingProvider}
onChooseProvider={handleChooseProvider}
langSmithConfig={langSmithConfig}
langFuseConfig={langFuseConfig}
onConfigUpdated={handleTracingConfigUpdated}
onConfigRemoved={handleTracingConfigRemoved}
controlShowPopup={controlShowPopup}
/>
</div>
</>
</div>
)}
{hasConfiguredTracing && (
<div className='ml-2 w-px h-3.5 bg-gray-200'></div>
)}
<div className='flex items-center' onClick={e => e.stopPropagation()}>
<ConfigButton
appId={appId}
readOnly={readOnly}
hasConfigured
className='ml-2'
enabled={enabled}
onStatusChange={handleTracingEnabledChange}
chosenProvider={inUseTracingProvider}
onChooseProvider={handleChooseProvider}
langSmithConfig={langSmithConfig}
langFuseConfig={langFuseConfig}
onConfigUpdated={handleTracingConfigUpdated}
onConfigRemoved={handleTracingConfigRemoved}
controlShowPopup={controlShowPopup}
/>
</div>
</div>
</div>
)

View File

@@ -17,7 +17,6 @@ import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/gene
import Confirm from '@/app/components/base/confirm'
import { addTracingConfig, removeTracingConfig, updateTracingConfig } from '@/service/apps'
import Toast from '@/app/components/base/toast'
import Divider from '@/app/components/base/divider'
type Props = {
appId: string
@@ -153,11 +152,11 @@ const ProviderConfigModal: FC<Props> = ({
? (
<PortalToFollowElem open>
<PortalToFollowElemContent className='w-full h-full z-[60]'>
<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='fixed inset-0 flex items-center justify-center bg-black/[.25]'>
<div className='mx-2 w-[640px] max-h-[calc(100vh-120px)] bg-white shadow-xl rounded-2xl overflow-y-auto'>
<div className='px-8 pt-8'>
<div className='flex justify-between items-center mb-4'>
<div className='title-2xl-semibold text-text-primary'>{t(`${I18N_PREFIX}.title`)}{t(`app.tracing.${type}.title`)}</div>
<div className='text-xl font-semibold text-gray-900'>{t(`${I18N_PREFIX}.title`)}{t(`app.tracing.${type}.title`)}</div>
</div>
<div className='space-y-4'>
@@ -231,16 +230,16 @@ const ProviderConfigModal: FC<Props> = ({
{isEdit && (
<>
<Button
className='h-9 text-sm font-medium text-text-secondary'
className='h-9 text-sm font-medium text-gray-700'
onClick={showRemoveConfirm}
>
<span className='text-[#D92D20]'>{t('common.operation.remove')}</span>
</Button>
<Divider className='mx-3 h-[18px]'/>
<div className='mx-3 w-px h-[18px] bg-gray-200'></div>
</>
)}
<Button
className='mr-2 h-9 text-sm font-medium text-text-secondary'
className='mr-2 h-9 text-sm font-medium text-gray-700'
onClick={onCancel}
>
{t('common.operation.cancel')}
@@ -257,9 +256,9 @@ 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='border-t-[0.5px] border-t-black/5'>
<div className='flex justify-center items-center py-3 bg-gray-50 text-xs text-gray-500'>
<Lock01 className='mr-1 w-3 h-3 text-gray-500' />
{t('common.modelProvider.encrypted.front')}
<a
className='text-primary-600 mx-1'

View File

@@ -1,13 +1,11 @@
'use client'
import type { FC } from 'react'
import React, { useCallback } from 'react'
import {
RiEqualizer2Line,
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { TracingProvider } from './type'
import cn from '@/utils/classnames'
import { LangfuseIconBig, LangsmithIconBig } from '@/app/components/base/icons/src/public/tracing'
import { Settings04 } from '@/app/components/base/icons/src/vender/line/general'
import { Eye as View } from '@/app/components/base/icons/src/vender/solid/general'
const I18N_PREFIX = 'app.tracing'
@@ -63,37 +61,34 @@ const ProviderPanel: FC<Props> = ({
}, [hasConfigured, isChosen, onChoose, readOnly])
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',
!isChosen && hasConfigured && !readOnly && 'cursor-pointer',
)}
className={cn(isChosen ? 'border-primary-400' : 'border-transparent', !isChosen && hasConfigured && !readOnly && 'cursor-pointer', 'px-4 py-3 rounded-xl border-[1.5px] bg-gray-100')}
onClick={handleChosen}
>
<div className={'flex justify-between items-center 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='ml-1 flex items-center h-4 px-1 rounded-[4px] border border-primary-500 leading-4 text-xs font-medium text-primary-500 uppercase '>{t(`${I18N_PREFIX}.inUse`)}</div>}
</div>
{!readOnly && (
<div className={'flex justify-between items-center 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} >
<div className='flex px-2 items-center h-6 bg-white rounded-md border-[0.5px] border-gray-200 shadow-xs cursor-pointer text-gray-700 space-x-1' onClick={viewBtnClick} >
<View className='w-3 h-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 px-2 items-center h-6 bg-white rounded-md border-[0.5px] border-gray-200 shadow-xs cursor-pointer text-gray-700 space-x-1'
onClick={handleConfigBtnClick}
>
<RiEqualizer2Line className='w-3 h-3' />
<Settings04 className='w-3 h-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='mt-2 leading-4 text-xs font-normal text-gray-500'>
{t(`${I18N_PREFIX}.${type}.description`)}
</div>
</div>

View File

@@ -0,0 +1,45 @@
'use client'
import { ChevronDoubleDownIcon } from '@heroicons/react/20/solid'
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import React, { useCallback } from 'react'
import Tooltip from '@/app/components/base/tooltip'
const I18N_PREFIX = 'app.tracing'
type Props = {
isFold: boolean
onFoldChange: (isFold: boolean) => void
}
const ToggleFoldBtn: FC<Props> = ({
isFold,
onFoldChange,
}) => {
const { t } = useTranslation()
const handleFoldChange = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
onFoldChange(!isFold)
}, [isFold, onFoldChange])
return (
// text-[0px] to hide spacing between tooltip elements
<div className='shrink-0 cursor-pointer text-[0px]' onClick={handleFoldChange}>
<Tooltip
popupContent={t(`${I18N_PREFIX}.${isFold ? 'expand' : 'collapse'}`)}
>
{isFold && (
<div className='p-1 rounded-md text-gray-500 hover:text-gray-800 hover:bg-black/5'>
<ChevronDoubleDownIcon className='w-4 h-4' />
</div>
)}
{!isFold && (
<div className='p-2 rounded-lg text-gray-500 border-[0.5px] border-gray-200 hover:text-gray-800 hover:bg-black/5'>
<ChevronDoubleDownIcon className='w-4 h-4 transform rotate-180' />
</div>
)}
</Tooltip>
</div>
)
}
export default React.memo(ToggleFoldBtn)

View File

@@ -26,7 +26,7 @@ import { useTabSearchParams } from '@/hooks/use-tab-searchparams'
import { useStore as useTagStore } from '@/app/components/base/tag-management/store'
import { useAppContext } from '@/context/app-context'
import { useExternalApiPanel } from '@/context/external-api-panel-context'
// eslint-disable-next-line import/order
import { useQuery } from '@tanstack/react-query'
import Input from '@/app/components/base/input'

View File

@@ -1106,6 +1106,57 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/documents/{document_id}/upload-file'
method='GET'
title='Get Upload File'
name='#get_upload_file'
/>
<Row>
<Col>
### Path
<Properties>
<Property name='dataset_id' type='string' key='dataset_id'>
Knowledge ID
</Property>
<Property name='document_id' type='string' key='document_id'>
Document ID
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="GET"
label="/datasets/{dataset_id}/documents/{document_id}/upload-file"
targetCode={`curl --location --request GET '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/upload-file' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json'`}
>
```bash {{ title: 'cURL' }}
curl --location --request GET '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/upload-file' \
--header 'Authorization: Bearer {api_key}' \
--header 'Content-Type: application/json'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"id": "file_id",
"name": "file_name",
"size": 1024,
"extension": "txt",
"url": "preview_url",
"download_url": "download_url",
"mime_type": "text/plain",
"created_by": "user_id",
"created_at": 1728734540,
}
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/retrieve'
method='POST'

View File

@@ -1107,6 +1107,57 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/documents/{document_id}/upload-file'
method='GET'
title='获取上传文件'
name='#get_upload_file'
/>
<Row>
<Col>
### Path
<Properties>
<Property name='dataset_id' type='string' key='dataset_id'>
知识库 ID
</Property>
<Property name='document_id' type='string' key='document_id'>
文档 ID
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="GET"
label="/datasets/{dataset_id}/documents/{document_id}/upload-file"
targetCode={`curl --location --request GET '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/upload-file' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json'`}
>
```bash {{ title: 'cURL' }}
curl --location --request GET '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/upload-file' \
--header 'Authorization: Bearer {api_key}' \
--header 'Content-Type: application/json'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"id": "file_id",
"name": "file_name",
"size": 1024,
"extension": "txt",
"url": "preview_url",
"download_url": "download_url",
"mime_type": "text/plain",
"created_by": "user_id",
"created_at": 1728734540,
}
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/retrieve'
method='POST'

View File

@@ -1,20 +0,0 @@
import PluginPage from '@/app/components/plugins/plugin-page'
import PluginsPanel from '@/app/components/plugins/plugin-page/plugins-panel'
import Marketplace from '@/app/components/plugins/marketplace'
import { getLocaleOnServer } from '@/i18n/server'
const PluginList = async () => {
const locale = await getLocaleOnServer()
return (
<PluginPage
plugins={<PluginsPanel />}
marketplace={<Marketplace locale={locale} pluginTypeSwitchClassName='top-[60px]' searchBoxAutoAnimate={false} />}
/>
)
}
export const metadata = {
title: 'Plugins - Dify',
}
export default PluginList

View File

@@ -8,7 +8,7 @@ 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
}

View File

@@ -60,18 +60,18 @@ export default function AppBasic({ icon, icon_background, name, isExternal, type
return (
<div className="flex items-start p-1">
{icon && icon_background && iconType === 'app' && (
<div className='shrink-0 mr-3'>
<div className='flex-shrink-0 mr-3'>
<AppIcon icon={icon} background={icon_background} />
</div>
)}
{iconType !== 'app'
&& <div className='shrink-0 mr-3'>
&& <div className='flex-shrink-0 mr-3'>
{ICON_MAP[iconType]}
</div>
}
{mode === 'expand' && <div className="group">
<div className={`flex flex-row items-center text-sm font-semibold text-text-secondary group-hover:text-text-primary break-all ${textStyle?.main ?? ''}`}>
<div className={`flex flex-row items-center text-sm font-semibold text-gray-700 group-hover:text-gray-900 break-all ${textStyle?.main ?? ''}`}>
{name}
{hoverTip
&& <Tooltip
@@ -86,7 +86,7 @@ export default function AppBasic({ icon, icon_background, name, isExternal, type
/>
}
</div>
<div className={`text-xs font-normal text-text-tertiary group-hover:text-text-secondary break-all ${textStyle?.extra ?? ''}`}>{type}</div>
<div className={`text-xs font-normal text-gray-500 group-hover:text-gray-700 break-all ${textStyle?.extra ?? ''}`}>{type}</div>
<div className='text-text-tertiary system-2xs-medium-uppercase'>{isExternal ? t('dataset.externalTag') : ''}</div>
</div>}
</div>

View File

@@ -6,11 +6,11 @@ import useSWR from 'swr'
import Input from '@/app/components/base/input'
import { fetchAnnotationsCount } from '@/service/log'
export interface QueryParam {
export type QueryParam = {
keyword?: string
}
interface IFilterProps {
type IFilterProps = {
appId: string
queryParams: QueryParam
setQueryParams: (v: QueryParam) => void

View File

@@ -9,7 +9,7 @@ import ActionButton from '@/app/components/base/action-button'
import useTimestamp from '@/hooks/use-timestamp'
import cn from '@/utils/classnames'
interface Props {
type Props = {
list: AnnotationItem[]
onRemove: (id: string) => void
onView: (item: AnnotationItem) => void

View File

@@ -23,7 +23,7 @@ const FeaturePanel: FC<IFeaturePanelProps> = ({
children,
}) => {
return (
<div className={cn('rounded-xl border-t-[0.5px] border-l-[0.5px] bg-background-section-burn pb-3', noBodySpacing && 'pb-0', className)}>
<div className={cn('rounded-xl border-t-[0.5px] border-l-[0.5px] bg-background-section-burn pb-3', noBodySpacing && '!pb-0', className)}>
{/* Header */}
<div className={cn('px-3 pt-2', hasHeaderBottomBorder && 'border-b border-divider-subtle')}>
<div className='flex justify-between items-center h-8'>

View File

@@ -1,29 +1,25 @@
'use client'
import React, { useState } from 'react'
import React from 'react'
import cn from '@/utils/classnames'
type IRemoveIconProps = {
className?: string
isHoverStatus?: boolean
onClick: () => void
}
} & React.HTMLAttributes<HTMLDivElement>
const RemoveIcon = ({
className,
isHoverStatus,
onClick,
...props
}: IRemoveIconProps) => {
const [isHovered, setIsHovered] = useState(false)
const computedIsHovered = isHoverStatus || isHovered
return (
<div
className={cn(className, computedIsHovered && 'bg-[#FEE4E2]', 'flex w-6 h-6 items-center justify-center rounded-md cursor-pointer hover:bg-[#FEE4E2]')}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
className={cn('flex w-6 h-6 items-center justify-center rounded-md cursor-pointer hover:bg-state-destructive-hover text-text-tertiary hover:text-text-destructive', className)}
onClick={onClick}
{...props}
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 6H14M6 8H18M16.6667 8L16.1991 15.0129C16.129 16.065 16.0939 16.5911 15.8667 16.99C15.6666 17.3412 15.3648 17.6235 15.0011 17.7998C14.588 18 14.0607 18 13.0062 18H10.9938C9.93927 18 9.41202 18 8.99889 17.7998C8.63517 17.6235 8.33339 17.3412 8.13332 16.99C7.90607 16.5911 7.871 16.065 7.80086 15.0129L7.33333 8M10.6667 11V14.3333M13.3333 11V14.3333" stroke={computedIsHovered ? '#D92D20' : '#667085'} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M10 6H14M6 8H18M16.6667 8L16.1991 15.0129C16.129 16.065 16.0939 16.5911 15.8667 16.99C15.6666 17.3412 15.3648 17.6235 15.0011 17.7998C14.588 18 14.0607 18 13.0062 18H10.9938C9.93927 18 9.41202 18 8.99889 17.7998C8.63517 17.6235 8.33339 17.3412 8.13332 16.99C7.90607 16.5911 7.871 16.065 7.80086 15.0129L7.33333 8M10.6667 11V14.3333M13.3333 11V14.3333" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
)

View File

@@ -9,7 +9,7 @@ import { MessageClockCircle } from '@/app/components/base/icons/src/vender/solid
import I18n from '@/context/i18n'
import { LanguagesSupported } from '@/i18n/language'
interface Props {
type Props = {
showWarning: boolean
onShowEditModal: () => void
}

View File

@@ -9,7 +9,7 @@ import ConfirmAddVar from './confirm-add-var'
import s from './style.module.css'
import PromptEditorHeightResizeWrap from './prompt-editor-height-resize-wrap'
import cn from '@/utils/classnames'
import type { PromptVariable } from '@/models/debug'
import { type PromptVariable } from '@/models/debug'
import Tooltip from '@/app/components/base/tooltip'
import type { CompletionParams } from '@/types/app'
import { AppType } from '@/types/app'

View File

@@ -3,7 +3,7 @@ import type { FC } from 'react'
import React from 'react'
import cn from '@/utils/classnames'
interface Props {
type Props = {
className?: string
title: string
children: JSX.Element

View File

@@ -23,7 +23,7 @@ import { DEFAULT_VALUE_MAX_LEN } from '@/config'
const TEXT_MAX_LENGTH = 256
export interface IConfigModalProps {
export type IConfigModalProps = {
isCreate?: boolean
payload?: InputVar
isShow: boolean

View File

@@ -1,12 +1,13 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { PlusIcon } from '@heroicons/react/24/outline'
import { ReactSortable } from 'react-sortablejs'
import RemoveIcon from '../../base/icons/remove-icon'
import s from './style.module.css'
import cn from '@/utils/classnames'
export type Options = string[]
export type IConfigSelectProps = {
@@ -19,6 +20,8 @@ const ConfigSelect: FC<IConfigSelectProps> = ({
onChange,
}) => {
const { t } = useTranslation()
const [delBtnHoverIndex, setDelBtnHoverIndex] = useState(-1)
const [focusedIndex, setFocusedIndex] = useState(-1)
const optionList = options.map((content, index) => {
return ({
@@ -36,48 +39,62 @@ const ConfigSelect: FC<IConfigSelectProps> = ({
list={optionList}
setList={list => onChange(list.map(item => item.name))}
handle='.handle'
ghostClass="opacity-50"
ghostClass="opacity-30"
animation={150}
>
{options.map((o, index) => (
<div className={`${s.inputWrap} relative`} key={index}>
<div className='handle flex items-center justify-center w-4 h-4 cursor-grab'>
<svg width="6" height="10" viewBox="0 0 6 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M1 2C1.55228 2 2 1.55228 2 1C2 0.447715 1.55228 0 1 0C0.447715 0 0 0.447715 0 1C0 1.55228 0.447715 2 1 2ZM1 6C1.55228 6 2 5.55228 2 5C2 4.44772 1.55228 4 1 4C0.447715 4 0 4.44772 0 5C0 5.55228 0.447715 6 1 6ZM6 1C6 1.55228 5.55228 2 5 2C4.44772 2 4 1.55228 4 1C4 0.447715 4.44772 0 5 0C5.55228 0 6 0.447715 6 1ZM5 6C5.55228 6 6 5.55228 6 5C6 4.44772 5.55228 4 5 4C4.44772 4 4 4.44772 4 5C4 5.55228 4.44772 6 5 6ZM2 9C2 9.55229 1.55228 10 1 10C0.447715 10 0 9.55229 0 9C0 8.44771 0.447715 8 1 8C1.55228 8 2 8.44771 2 9ZM5 10C5.55228 10 6 9.55229 6 9C6 8.44771 5.55228 8 5 8C4.44772 8 4 8.44771 4 9C4 9.55229 4.44772 10 5 10Z" fill="#98A2B3" />
</svg>
</div>
<input
{options.map((o, index) => {
const delBtnHover = delBtnHoverIndex === index
const inputFocused = focusedIndex === index
return (
<div
className={cn(
`${s.inputWrap} relative border border-components-panel-border-subtle bg-components-panel-on-panel-item-bg`,
inputFocused && 'border-components-input-border-active bg-components-input-bg-active',
delBtnHover && 'bg-state-destructive-hover',
)}
key={index}
type="input"
value={o || ''}
onChange={(e) => {
const value = e.target.value
onChange(options.map((item, i) => {
if (index === i)
return value
>
<div className='handle flex items-center justify-center w-3.5 h-3.5 cursor-grab text-text-quaternary'>
<svg width="6" height="10" viewBox="0 0 6 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M1 2C1.55228 2 2 1.55228 2 1C2 0.447715 1.55228 0 1 0C0.447715 0 0 0.447715 0 1C0 1.55228 0.447715 2 1 2ZM1 6C1.55228 6 2 5.55228 2 5C2 4.44772 1.55228 4 1 4C0.447715 4 0 4.44772 0 5C0 5.55228 0.447715 6 1 6ZM6 1C6 1.55228 5.55228 2 5 2C4.44772 2 4 1.55228 4 1C4 0.447715 4.44772 0 5 0C5.55228 0 6 0.447715 6 1ZM5 6C5.55228 6 6 5.55228 6 5C6 4.44772 5.55228 4 5 4C4.44772 4 4 4.44772 4 5C4 5.55228 4.44772 6 5 6ZM2 9C2 9.55229 1.55228 10 1 10C0.447715 10 0 9.55229 0 9C0 8.44771 0.447715 8 1 8C1.55228 8 2 8.44771 2 9ZM5 10C5.55228 10 6 9.55229 6 9C6 8.44771 5.55228 8 5 8C4.44772 8 4 8.44771 4 9C4 9.55229 4.44772 10 5 10Z" fill="currentColor" />
</svg>
</div>
<input
key={index}
type="input"
value={o || ''}
onChange={(e) => {
const value = e.target.value
onChange(options.map((item, i) => {
if (index === i)
return value
return item
}))
}}
className={'w-full pl-1.5 pr-8 text-sm leading-9 text-gray-900 border-0 grow h-9 bg-transparent focus:outline-none cursor-pointer'}
/>
<RemoveIcon
className={`${s.deleteBtn} absolute top-1/2 translate-y-[-50%] right-1.5 items-center justify-center w-6 h-6 rounded-md cursor-pointer hover:bg-[#FEE4E2]`}
onClick={() => {
onChange(options.filter((_, i) => index !== i))
}}
/>
</div>
))}
return item
}))
}}
onFocus={() => { setFocusedIndex(index) }}
onBlur={() => { setFocusedIndex(-1) }}
className={'w-full pl-1 pr-8 system-sm-medium text-text-secondary border-0 grow h-8 bg-transparent group focus:outline-none cursor-pointer caret-[#295EFF]'}
/>
<RemoveIcon
className={`${s.deleteBtn} absolute top-1/2 translate-y-[-50%] right-1 items-center justify-center w-6 h-6 rounded-lg cursor-pointer`}
onClick={() => {
onChange(options.filter((_, i) => index !== i))
}}
onMouseEnter={() => setDelBtnHoverIndex(index)}
onMouseLeave={() => setDelBtnHoverIndex(-1)}
/>
</div>)
})}
</ReactSortable>
</div>
)}
<div
onClick={() => { onChange([...options, '']) }}
className='flex items-center h-9 px-3 gap-2 rounded-lg cursor-pointer text-gray-400 bg-gray-100'>
<PlusIcon width={16} height={16}></PlusIcon>
<div className='text-gray-500 text-[13px]'>{t('appDebug.variableConfig.addOption')}</div>
className='flex items-center h-8 px-2 gap-1 rounded-lg cursor-pointer bg-components-button-tertiary-bg'>
<PlusIcon className='text-components-button-tertiary-text' width={16} height={16} />
<div className='text-components-button-tertiary-text system-sm-medium'>{t('appDebug.variableConfig.addOption')}</div>
</div>
</div>
)

View File

@@ -2,7 +2,6 @@
display: flex;
align-items: center;
border-radius: 8px;
border: 1px solid #EAECF0;
padding-left: 10px;
cursor: pointer;
}

View File

@@ -3,7 +3,7 @@ import type { FC } from 'react'
import React, { useEffect } from 'react'
import Input from '@/app/components/base/input'
export interface IConfigStringProps {
export type IConfigStringProps = {
value: number | undefined
maxLength: number
modelId: string
@@ -28,7 +28,7 @@ const ConfigString: FC<IConfigStringProps> = ({
min={1}
value={value || ''}
onChange={(e) => {
let value = Number.parseInt(e.target.value, 10)
let value = parseInt(e.target.value, 10)
if (value > maxLength)
value = maxLength

View File

@@ -3,6 +3,7 @@ import type { FC } from 'react'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks'
import type { Timeout } from 'ahooks/lib/useRequest/src/types'
import { useContext } from 'use-context-selector'
import produce from 'immer'
import {
@@ -33,7 +34,7 @@ import { InputVarType } from '@/app/components/workflow/types'
export const ADD_EXTERNAL_DATA_TOOL = 'ADD_EXTERNAL_DATA_TOOL'
interface ExternalDataToolParams {
type ExternalDataToolParams = {
key: string
type: string
index: number
@@ -43,13 +44,13 @@ interface ExternalDataToolParams {
icon_background?: string
}
export interface IConfigVarProps {
export type IConfigVarProps = {
promptVariables: PromptVariable[]
readonly?: boolean
onPromptVariablesChange?: (promptVariables: PromptVariable[]) => void
}
let conflictTimer: number
let conflictTimer: Timeout
const ConfigVar: FC<IConfigVarProps> = ({ promptVariables, readonly, onPromptVariablesChange }) => {
const { t } = useTranslation()
@@ -106,7 +107,7 @@ const ConfigVar: FC<IConfigVarProps> = ({ promptVariables, readonly, onPromptVar
onPromptVariablesChange?.(newPromptVariables)
}
const updatePromptKey = (index: number, newKey: string) => {
window.clearTimeout(conflictTimer)
clearTimeout(conflictTimer)
const { isValid, errorKey, errorMessageKey } = checkKeys([newKey], true)
if (!isValid) {
Toast.notify({
@@ -126,7 +127,7 @@ const ConfigVar: FC<IConfigVarProps> = ({ promptVariables, readonly, onPromptVar
return item
})
conflictTimer = window.setTimeout(() => {
conflictTimer = setTimeout(() => {
const isKeyExists = promptVariables.some(item => item.key?.trim() === newKey.trim())
if (isKeyExists) {
Toast.notify({

View File

@@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'
import cn from '@/utils/classnames'
import type { InputVarType } from '@/app/components/workflow/types'
import InputVarTypeIcon from '@/app/components/workflow/nodes/_base/components/input-var-type-icon'
export interface ISelectTypeItemProps {
export type ISelectTypeItemProps = {
type: InputVarType
selected: boolean
onClick: () => void
@@ -32,9 +32,9 @@ const SelectTypeItem: FC<ISelectTypeItemProps> = ({
onClick={onClick}
>
<div className='shrink-0'>
<InputVarTypeIcon type={type} className='w-5 h-5' />
<InputVarTypeIcon type={type} className='w-5 h-5 text-text-secondary' />
</div>
<span>{typeName}</span>
<span className='text-text-secondary'>{typeName}</span>
</div>
)
}

View File

@@ -1,25 +1,21 @@
'use client'
import type { FC } from 'react'
import React, { useMemo, useState } from 'react'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import copy from 'copy-to-clipboard'
import produce from 'immer'
import {
RiDeleteBinLine,
RiEqualizer2Line,
RiHammerFill,
RiInformation2Line,
} from '@remixicon/react'
import { useFormattingChangedDispatcher } from '../../../debug/hooks'
import SettingBuiltInTool from './setting-built-in-tool'
import cn from '@/utils/classnames'
import Panel from '@/app/components/app/configuration/base/feature-panel'
import { InfoCircle } from '@/app/components/base/icons/src/vender/line/general'
import OperationBtn from '@/app/components/app/configuration/base/operation-btn'
import AppIcon from '@/app/components/base/app-icon'
import Button from '@/app/components/base/button'
import Indicator from '@/app/components/header/indicator'
import Switch from '@/app/components/base/switch'
import Toast from '@/app/components/base/toast'
import ConfigContext from '@/context/debug-configuration'
import type { AgentTool } from '@/types/app'
import { type Collection, CollectionType } from '@/app/components/tools/types'
@@ -27,12 +23,7 @@ import { MAX_TOOLS_NUM } from '@/config'
import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
import Tooltip from '@/app/components/base/tooltip'
import { DefaultToolIcon } from '@/app/components/base/icons/src/public/other'
// import AddToolModal from '@/app/components/tools/add-tool-modal'
import ConfigCredential from '@/app/components/tools/setting/build-in/config-credentials'
import { updateBuiltInToolCredential } from '@/service/tools'
import cn from '@/utils/classnames'
import ToolPicker from '@/app/components/workflow/block-selector/tool-picker'
import type { ToolDefaultValue } from '@/app/components/workflow/block-selector/types'
import AddToolModal from '@/app/components/tools/add-tool-modal'
type AgentToolWithMoreInfo = AgentTool & { icon: any; collection?: Collection } | null
const AgentTools: FC = () => {
@@ -42,19 +33,9 @@ const AgentTools: FC = () => {
const formattingChangedDispatcher = useFormattingChangedDispatcher()
const [currentTool, setCurrentTool] = useState<AgentToolWithMoreInfo>(null)
const currentCollection = useMemo(() => {
if (!currentTool) return null
const collection = collectionList.find(collection => collection.id.split('/').pop() === currentTool?.provider_id.split('/').pop() && collection.type === currentTool?.provider_type)
return collection
}, [currentTool, collectionList])
const [isShowSettingTool, setIsShowSettingTool] = useState(false)
const [isShowSettingAuth, setShowSettingAuth] = useState(false)
const tools = (modelConfig?.agentConfig?.tools as AgentTool[] || []).map((item) => {
const collection = collectionList.find(
collection =>
collection.id.split('/').pop() === item.provider_id.split('/').pop()
&& collection.type === item.provider_type,
)
const collection = collectionList.find(collection => collection.id === item.provider_id && collection.type === item.provider_type)
const icon = collection?.icon
return {
...item,
@@ -74,39 +55,10 @@ const AgentTools: FC = () => {
formattingChangedDispatcher()
}
const handleToolAuthSetting = (value: any) => {
const newModelConfig = produce(modelConfig, (draft) => {
const tool = (draft.agentConfig.tools).find((item: any) => item.provider_id === value?.collection?.id && item.tool_name === value?.tool_name)
if (tool)
(tool as AgentTool).notAuthor = false
})
setModelConfig(newModelConfig)
setIsShowSettingTool(false)
formattingChangedDispatcher()
}
const [isDeleting, setIsDeleting] = useState<number>(-1)
const handleSelectTool = (tool: ToolDefaultValue) => {
const newModelConfig = produce(modelConfig, (draft) => {
draft.agentConfig.tools.push({
provider_id: tool.provider_id,
provider_type: tool.provider_type as CollectionType,
provider_name: tool.provider_name,
tool_name: tool.tool_name,
tool_label: tool.tool_label,
tool_parameters: tool.params,
notAuthor: !tool.is_team_authorization,
enabled: true,
})
})
setModelConfig(newModelConfig)
}
return (
<>
<Panel
className={cn('mt-2', tools.length === 0 && 'pb-2')}
className="mt-2"
noBodySpacing={tools.length === 0}
headerIcon={
<RiHammerFill className='w-4 h-4 text-primary-500' />
@@ -129,14 +81,7 @@ const AgentTools: FC = () => {
{tools.length < MAX_TOOLS_NUM && (
<>
<div className='ml-3 mr-1 h-3.5 w-px bg-gray-200'></div>
<ToolPicker
trigger={<OperationBtn type="add" />}
isShow={isShowChooseTool}
onShowChange={setIsShowChooseTool}
disabled={false}
supportAddCustomTool
onSelect={handleSelectTool}
/>
<OperationBtn type="add" onClick={() => setIsShowChooseTool(true)} />
</>
)}
</div>
@@ -145,77 +90,72 @@ const AgentTools: FC = () => {
<div className='grid gap-1 grid-cols-1 2xl:grid-cols-2 items-center flex-wrap justify-between'>
{tools.map((item: AgentTool & { icon: any; collection?: Collection }, index) => (
<div key={index}
className={cn(
'group relative flex justify-between items-center last-of-type:mb-0 p-1.5 pr-2 w-full bg-components-panel-on-panel-item-bg rounded-lg border-[0.5px] border-components-panel-border-subtle shadow-xs hover:bg-components-panel-on-panel-item-bg-hover hover:shadow-sm cursor',
isDeleting === index && 'hover:bg-state-destructive-hover border-state-destructive-border',
)}
className={cn((item.isDeleted || item.notAuthor) ? 'bg-white/50' : 'bg-white', (item.enabled && !item.isDeleted && !item.notAuthor) && 'shadow-xs', index > 1 && 'mt-1', 'group relative flex justify-between items-center last-of-type:mb-0 pl-2.5 py-2 pr-3 w-full rounded-lg border-[0.5px] border-gray-200 ')}
>
<div className='grow w-0 flex items-center'>
{item.isDeleted && <DefaultToolIcon className='w-5 h-5' />}
{!item.isDeleted && (
<div className={cn((item.notAuthor || !item.enabled) && 'opacity-50')}>
{typeof item.icon === 'string' && <div className='w-5 h-5 bg-cover bg-center rounded-md' style={{ backgroundImage: `url(${item.icon})` }} />}
{typeof item.icon !== 'string' && <AppIcon className='rounded-md' size='xs' icon={item.icon?.content} background={item.icon?.background} />}
</div>
)}
{(item.isDeleted || item.notAuthor)
? (
<DefaultToolIcon className='w-6 h-6' />
)
: (
typeof item.icon === 'string'
? (
<div
className='w-6 h-6 bg-cover bg-center rounded-md'
style={{
backgroundImage: `url(${item.icon})`,
}}
></div>
)
: (
<AppIcon
className='rounded-md'
size='tiny'
icon={item.icon?.content}
background={item.icon?.background}
/>
))}
<div
className={cn(
'grow w-0 ml-1.5 flex items-center system-xs-regular truncate',
(item.isDeleted || item.notAuthor || !item.enabled) ? 'opacity-50' : '',
)}
className={cn((item.isDeleted || item.notAuthor) ? 'line-through opacity-50' : '', 'grow w-0 ml-2 leading-[18px] text-[13px] font-medium text-gray-800 truncate')}
>
<span className='text-text-secondary system-xs-medium pr-1.5'>{item.provider_type === CollectionType.builtIn ? item.provider_name.split('/').pop() : item.tool_label}</span>
<span className='text-text-tertiary'>{item.tool_name}</span>
{!item.isDeleted && (
<Tooltip
needsDelay
popupContent={
<div className='w-[180px]'>
<div className='mb-1.5 text-text-secondary'>{item.tool_name}</div>
<div className='mb-1.5 text-text-tertiary'>{t('tools.toolNameUsageTip')}</div>
<div className='text-text-accent cursor-pointer' onClick={() => copy(item.tool_name)}>{t('tools.copyToolName')}</div>
</div>
}
>
<div className='w-4 h-4'>
<div className='hidden group-hover:inline-block ml-0.5'>
<RiInformation2Line className='w-4 h-4 text-text-tertiary' />
</div>
</div>
</Tooltip>
)}
<span className='text-gray-800 pr-2'>{item.provider_type === CollectionType.builtIn ? item.provider_name : item.tool_label}</span>
<Tooltip
popupContent={t('tools.toolNameUsageTip')}
>
<span className='text-gray-500'>{item.tool_name}</span>
</Tooltip>
</div>
</div>
<div className='shrink-0 ml-1 flex items-center'>
{item.isDeleted && (
<div className='flex items-center mr-2'>
<Tooltip
popupContent={t('tools.toolRemoved')}
needsDelay
>
<div className='mr-1 p-1 rounded-md hover:bg-black/5 cursor-pointer'>
<AlertTriangle className='w-4 h-4 text-[#F79009]' />
</div>
</Tooltip>
<div
className='p-1 rounded-md text-text-tertiary cursor-pointer hover:text-text-destructive'
onClick={() => {
{(item.isDeleted || item.notAuthor)
? (
<div className='flex items-center'>
<Tooltip
popupContent={t(`tools.${item.isDeleted ? 'toolRemoved' : 'notAuthorized'}`)}
needsDelay
>
<div className='mr-1 p-1 rounded-md hover:bg-black/5 cursor-pointer' onClick={() => {
if (item.notAuthor)
setIsShowChooseTool(true)
}}>
<AlertTriangle className='w-4 h-4 text-[#F79009]' />
</div>
</Tooltip>
<div className='p-1 rounded-md hover:bg-black/5 cursor-pointer' onClick={() => {
const newModelConfig = produce(modelConfig, (draft) => {
draft.agentConfig.tools.splice(index, 1)
})
setModelConfig(newModelConfig)
formattingChangedDispatcher()
}}
onMouseOver={() => setIsDeleting(index)}
onMouseLeave={() => setIsDeleting(-1)}
>
<RiDeleteBinLine className='w-4 h-4' />
}}>
<RiDeleteBinLine className='w-4 h-4 text-gray-500' />
</div>
<div className='ml-2 mr-3 w-px h-3.5 bg-gray-200'></div>
</div>
</div>
)}
{!item.isDeleted && (
<div className='hidden group-hover:flex items-center gap-1 mr-2'>
{!item.notAuthor && (
)
: (
<div className='hidden group-hover:flex items-center'>
<Tooltip
popupContent={t('tools.setBuiltInTools.infoAndSetting')}
needsDelay
@@ -224,81 +164,55 @@ const AgentTools: FC = () => {
setCurrentTool(item)
setIsShowSettingTool(true)
}}>
<RiEqualizer2Line className='w-4 h-4 text-text-tertiary' />
<InfoCircle className='w-4 h-4 text-gray-500' />
</div>
</Tooltip>
)}
<div
className='p-1 rounded-md text-text-tertiary cursor-pointer hover:text-text-destructive'
onClick={() => {
<div className='p-1 rounded-md hover:bg-black/5 cursor-pointer' onClick={() => {
const newModelConfig = produce(modelConfig, (draft) => {
draft.agentConfig.tools.splice(index, 1)
})
setModelConfig(newModelConfig)
formattingChangedDispatcher()
}}
onMouseOver={() => setIsDeleting(index)}
onMouseLeave={() => setIsDeleting(-1)}
>
<RiDeleteBinLine className='w-4 h-4' />
}}>
<RiDeleteBinLine className='w-4 h-4 text-gray-500' />
</div>
<div className='ml-2 mr-3 w-px h-3.5 bg-gray-200'></div>
</div>
</div>
)}
<div className={cn(item.isDeleted && 'opacity-50')}>
{!item.notAuthor && (
<Switch
defaultValue={item.isDeleted ? false : item.enabled}
disabled={item.isDeleted}
size='md'
onChange={(enabled) => {
const newModelConfig = produce(modelConfig, (draft) => {
(draft.agentConfig.tools[index] as any).enabled = enabled
})
setModelConfig(newModelConfig)
formattingChangedDispatcher()
}} />
)}
{item.notAuthor && (
<Button variant='secondary' size='small' onClick={() => {
setCurrentTool(item)
setShowSettingAuth(true)
}}>
{t('tools.notAuthorized')}
<Indicator className='ml-2' color='orange' />
</Button>
)}
<div className={cn((item.isDeleted || item.notAuthor) && 'opacity-50')}>
<Switch
defaultValue={(item.isDeleted || item.notAuthor) ? false : item.enabled}
disabled={(item.isDeleted || item.notAuthor)}
size='md'
onChange={(enabled) => {
const newModelConfig = produce(modelConfig, (draft) => {
(draft.agentConfig.tools[index] as any).enabled = enabled
})
setModelConfig(newModelConfig)
formattingChangedDispatcher()
}} />
</div>
</div>
</div>
))}
</div >
</Panel >
{isShowSettingTool && (
<SettingBuiltInTool
toolName={currentTool?.tool_name as string}
setting={currentTool?.tool_parameters as any}
collection={currentTool?.collection as Collection}
isBuiltIn={currentTool?.collection?.type === CollectionType.builtIn}
isModel={currentTool?.collection?.type === CollectionType.model}
onSave={handleToolSettingChange}
onHide={() => setIsShowSettingTool(false)}
/>
)}
{isShowSettingAuth && (
<ConfigCredential
collection={currentCollection as any}
onCancel={() => setShowSettingAuth(false)}
onSaved={async (value) => {
await updateBuiltInToolCredential((currentCollection as any).name, value)
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
handleToolAuthSetting(currentTool as any)
setShowSettingAuth(false)
}}
/>
{isShowChooseTool && (
<AddToolModal onHide={() => setIsShowChooseTool(false)} />
)}
{
isShowSettingTool && (
<SettingBuiltInTool
toolName={currentTool?.tool_name as string}
setting={currentTool?.tool_parameters as any}
collection={currentTool?.collection as Collection}
isBuiltIn={currentTool?.collection?.type === CollectionType.builtIn}
isModel={currentTool?.collection?.type === CollectionType.model}
onSave={handleToolSettingChange}
onHide={() => setIsShowSettingTool(false)}
/>)
}
</>
)
}

View File

@@ -3,30 +3,21 @@ import type { FC } from 'react'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import {
RiArrowLeftLine,
RiCloseLine,
} from '@remixicon/react'
import Drawer from '@/app/components/base/drawer'
import Loading from '@/app/components/base/loading'
import ActionButton from '@/app/components/base/action-button'
import Icon from '@/app/components/plugins/card/base/card-icon'
import OrgInfo from '@/app/components/plugins/card/base/org-info'
import Description from '@/app/components/plugins/card/base/description'
import TabSlider from '@/app/components/base/tab-slider-plain'
import Button from '@/app/components/base/button'
import cn from '@/utils/classnames'
import Drawer from '@/app/components/base/drawer-plus'
import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form'
import { addDefaultValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
import type { Collection, Tool } from '@/app/components/tools/types'
import { CollectionType } from '@/app/components/tools/types'
import { fetchBuiltInToolList, fetchCustomToolList, fetchModelToolList, fetchWorkflowToolList } from '@/service/tools'
import I18n from '@/context/i18n'
import Button from '@/app/components/base/button'
import Loading from '@/app/components/base/loading'
import { DiagonalDividingLine } from '@/app/components/base/icons/src/public/common'
import { getLanguage } from '@/i18n/language'
import cn from '@/utils/classnames'
import AppIcon from '@/app/components/base/app-icon'
type Props = {
showBackButton?: boolean
collection: Collection
isBuiltIn?: boolean
isModel?: boolean
@@ -38,7 +29,6 @@ type Props = {
}
const SettingBuiltInTool: FC<Props> = ({
showBackButton = false,
collection,
isBuiltIn = true,
isModel = true,
@@ -106,38 +96,39 @@ const SettingBuiltInTool: FC<Props> = ({
return valid
})()
const getType = (type: string) => {
if (type === 'number-input')
return t('tools.setBuiltInTools.number')
if (type === 'text-input')
return t('tools.setBuiltInTools.string')
if (type === 'file')
return t('tools.setBuiltInTools.file')
return type
}
const infoUI = (
<div className=''>
<div className='pt-2'>
<div className='leading-5 text-sm font-medium text-gray-900'>
{t('tools.setBuiltInTools.toolDescription')}
</div>
<div className='mt-1 leading-[18px] text-xs font-normal text-gray-600'>
{currTool?.description[language]}
</div>
{infoSchemas.length > 0 && (
<div className='py-2 space-y-1'>
{infoSchemas.map((item: any, index) => (
<div key={index} className='py-1'>
<div className='flex items-center gap-2'>
<div className='text-text-secondary code-sm-semibold'>{item.label[language]}</div>
<div className='text-text-tertiary system-xs-regular'>
{getType(item.type)}
<div className='mt-6'>
<div className='flex items-center mb-4 leading-[18px] text-xs font-semibold text-gray-500 uppercase'>
<div className='mr-3'>{t('tools.setBuiltInTools.parameters')}</div>
<div className='grow w-0 h-px bg-[#f3f4f6]'></div>
</div>
<div className='space-y-4'>
{infoSchemas.map((item: any, index) => (
<div key={index}>
<div className='flex items-center space-x-2 leading-[18px]'>
<div className='text-[13px] font-semibold text-gray-900'>{item.label[language]}</div>
<div className='text-xs font-medium text-gray-500'>{item.type === 'number-input' ? t('tools.setBuiltInTools.number') : t('tools.setBuiltInTools.string')}</div>
{item.required && (
<div className='text-xs font-medium text-[#EC4A0A]'>{t('tools.setBuiltInTools.required')}</div>
)}
</div>
{item.required && (
<div className='text-text-warning-secondary system-xs-medium'>{t('tools.setBuiltInTools.required')}</div>
{item.human_description && (
<div className='mt-1 leading-[18px] text-xs font-normal text-gray-600'>
{item.human_description?.[language]}
</div>
)}
</div>
{item.human_description && (
<div className='mt-0.5 text-text-tertiary system-xs-regular'>
{item.human_description?.[language]}
</div>
)}
</div>
))}
))}
</div>
</div>
)}
</div>
@@ -158,82 +149,75 @@ const SettingBuiltInTool: FC<Props> = ({
return (
<Drawer
isOpen
isShow
onHide={onHide}
title={(
<div className='flex items-center'>
{typeof collection.icon === 'string'
? (
<div
className='w-6 h-6 bg-cover bg-center rounded-md flex-shrink-0'
style={{
backgroundImage: `url(${collection.icon})`,
}}
></div>
)
: (
<AppIcon
className='rounded-md'
size='tiny'
icon={(collection.icon as any)?.content}
background={(collection.icon as any)?.background}
/>
)}
<div className='ml-2 leading-6 text-base font-semibold text-gray-900'>{currTool?.label[language]}</div>
{(hasSetting && !readonly) && (<>
<DiagonalDividingLine className='mx-4' />
<div className='flex space-x-6'>
<div
className={cn(isInfoActive ? 'text-gray-900 font-semibold' : 'font-normal text-gray-600 cursor-pointer', 'relative text-base')}
onClick={() => setCurrType('info')}
>
{t('tools.setBuiltInTools.info')}
{isInfoActive && <div className='absolute left-0 bottom-[-16px] w-full h-0.5 bg-primary-600'></div>}
</div>
<div className={cn(!isInfoActive ? 'text-gray-900 font-semibold' : 'font-normal text-gray-600 cursor-pointer', 'relative text-base ')}
onClick={() => setCurrType('setting')}
>
{t('tools.setBuiltInTools.setting')}
{!isInfoActive && <div className='absolute left-0 bottom-[-16px] w-full h-0.5 bg-primary-600'></div>}
</div>
</div>
</>)}
</div>
)}
panelClassName='mt-[65px] !w-[405px]'
maxWidthClassName='!max-w-[405px]'
height='calc(100vh - 65px)'
headerClassName='!border-b-black/5'
body={
<div className='h-full pt-3'>
{isLoading
? <div className='flex h-full items-center'>
<Loading type='app' />
</div>
: (<div className='flex flex-col h-full'>
<div className='grow h-0 overflow-y-auto px-6'>
{isInfoActive ? infoUI : settingUI}
</div>
{!readonly && !isInfoActive && (
<div className='mt-2 shrink-0 flex justify-end py-4 px-6 space-x-2 rounded-b-[10px] bg-gray-50 border-t border-black/5'>
<Button className='flex items-center h-8 !px-3 !text-[13px] font-medium !text-gray-700' onClick={onHide}>{t('common.operation.cancel')}</Button>
<Button className='flex items-center h-8 !px-3 !text-[13px] font-medium' variant='primary' disabled={!isValid} onClick={() => onSave?.(addDefaultValue(tempSetting, formSchemas))}>{t('common.operation.save')}</Button>
</div>
)}
</div>)}
</div>
}
isShowMask={false}
clickOutsideNotOpen={false}
onClose={onHide}
footer={null}
mask={false}
positionCenter={false}
panelClassname={cn('justify-start mt-[64px] mr-2 mb-2 !w-[420px] !max-w-[420px] !p-0 !bg-components-panel-bg rounded-2xl border-[0.5px] border-components-panel-border shadow-xl')}
>
<>
{isLoading && <Loading type='app' />}
{!isLoading && (
<>
{/* header */}
<div className='relative p-4 pb-3 border-b border-divider-subtle'>
<div className='absolute top-3 right-3'>
<ActionButton onClick={onHide}>
<RiCloseLine className='w-4 h-4' />
</ActionButton>
</div>
{showBackButton && (
<div
className='mb-2 flex items-center gap-1 text-text-accent-secondary system-xs-semibold-uppercase cursor-pointer'
onClick={onHide}
>
<RiArrowLeftLine className='w-4 h-4' />
BACK
</div>
)}
<div className='flex items-center gap-1'>
<Icon size='tiny' className='w-6 h-6' src={collection.icon} />
<OrgInfo
packageNameClassName='w-auto'
orgName={collection.author}
packageName={collection.name.split('/').pop() || ''}
/>
</div>
<div className='mt-1 text-text-primary system-md-semibold'>{currTool?.label[language]}</div>
{!!currTool?.description[language] && (
<Description className='mt-3' text={currTool.description[language]} descriptionLineRows={2}></Description>
)}
</div>
{/* form */}
<div className='h-full'>
<div className='flex flex-col h-full'>
{(hasSetting && !readonly) ? (
<TabSlider
className='shrink-0 mt-1 px-4'
itemClassName='py-3'
noBorderBottom
value={currType}
onChange={(value) => {
setCurrType(value)
}}
options={[
{ value: 'info', text: t('tools.setBuiltInTools.parameters')! },
{ value: 'setting', text: t('tools.setBuiltInTools.setting')! },
]}
/>
) : (
<div className='p-4 pb-1 text-text-primary system-sm-semibold-uppercase'>{t('tools.setBuiltInTools.parameters')}</div>
)}
<div className='grow h-0 overflow-y-auto px-4'>
{isInfoActive ? infoUI : settingUI}
</div>
{!readonly && !isInfoActive && (
<div className='mt-2 shrink-0 flex justify-end py-4 px-6 space-x-2 rounded-b-[10px] bg-components-panel-bg border-t border-divider-regular'>
<Button className='flex items-center h-8 !px-3 !text-[13px] font-medium !text-gray-700' onClick={onHide}>{t('common.operation.cancel')}</Button>
<Button className='flex items-center h-8 !px-3 !text-[13px] font-medium' variant='primary' disabled={!isValid} onClick={() => onSave?.(addDefaultValue(tempSetting, formSchemas))}>{t('common.operation.save')}</Button>
</div>
)}
</div>
</div>
</>
)}
</>
</Drawer>
/>
)
}
export default React.memo(SettingBuiltInTool)

View File

@@ -38,7 +38,7 @@ import ModelName from '@/app/components/header/account-setting/model-provider-pa
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
export interface IGetAutomaticResProps {
export type IGetAutomaticResProps = {
mode: AppType
model: Model
isShow: boolean

View File

@@ -12,7 +12,7 @@ import AgentTools from './agent/agent-tools'
import ConfigContext from '@/context/debug-configuration'
import ConfigPrompt from '@/app/components/app/configuration/config-prompt'
import ConfigVar from '@/app/components/app/configuration/config-var'
import type { ModelConfig, PromptVariable } from '@/models/debug'
import { type ModelConfig, type PromptVariable } from '@/models/debug'
import type { AppType } from '@/types/app'
import { ModelModeType } from '@/types/app'

View File

@@ -140,11 +140,11 @@ const ParamsConfig = ({
/>
<div className='mt-6 flex justify-end'>
<Button className='mr-2 shrink-0' onClick={() => {
<Button className='mr-2 flex-shrink-0' onClick={() => {
setTempDataSetConfigs(datasetConfigs)
setRerankSettingModalOpen(false)
}}>{t('common.operation.cancel')}</Button>
<Button variant='primary' className='shrink-0' onClick={handleSave} >{t('common.operation.save')}</Button>
<Button variant='primary' className='flex-shrink-0' onClick={handleSave} >{t('common.operation.save')}</Button>
</div>
</Modal>
)

View File

@@ -33,7 +33,7 @@ import { ModelTypeEnum } from '@/app/components/header/account-setting/model-pro
import { fetchMembers } from '@/service/common'
import type { Member } from '@/models/common'
interface SettingsModalProps {
type SettingsModalProps = {
currentDataset: DataSet
onCancel: () => void
onSave: (newDataset: DataSet) => void

View File

@@ -31,7 +31,7 @@ import { useFeatures } from '@/app/components/base/features/hooks'
import type { InputForm } from '@/app/components/base/chat/chat/type'
import { getLastAnswer } from '@/app/components/base/chat/utils'
interface ChatItemProps {
type ChatItemProps = {
modelAndParameter: ModelAndParameter
}
const ChatItem: FC<ChatItemProps> = ({

View File

@@ -15,7 +15,7 @@ import { useEventEmitterContextContext } from '@/context/event-emitter'
import { useProviderContext } from '@/context/provider-context'
import { useFeatures } from '@/app/components/base/features/hooks'
interface TextGenerationItemProps {
type TextGenerationItemProps = {
modelAndParameter: ModelAndParameter
}
const TextGenerationItem: FC<TextGenerationItemProps> = ({

View File

@@ -27,10 +27,10 @@ import { useFeatures } from '@/app/components/base/features/hooks'
import { getLastAnswer } from '@/app/components/base/chat/utils'
import type { InputForm } from '@/app/components/base/chat/chat/type'
interface DebugWithSingleModelProps {
type DebugWithSingleModelProps = {
checkCanSend?: () => boolean
}
export interface DebugWithSingleModelRefType {
export type DebugWithSingleModelRefType = {
handleRestart: () => void
}
const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSingleModelProps>(({

View File

@@ -48,7 +48,7 @@ import PromptLogModal from '@/app/components/base/prompt-log-modal'
import { useStore as useAppStore } from '@/app/components/app/store'
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
interface IDebug {
type IDebug = {
isAPIKeySet: boolean
onSetting: () => void
inputs: Inputs

View File

@@ -1,3 +1,4 @@
/* eslint-disable multiline-ternary */
'use client'
import type { FC } from 'react'
import React, { useEffect, useRef, useState } from 'react'

View File

@@ -59,7 +59,7 @@ import {
useTextGenerationCurrentProviderAndModelAndModelList,
} from '@/app/components/header/account-setting/model-provider-page/hooks'
import { fetchCollectionList } from '@/service/tools'
import type { Collection } from '@/app/components/tools/types'
import { type Collection } from '@/app/components/tools/types'
import { useStore as useAppStore } from '@/app/components/app/store'
import {
getMultipleRetrievalConfig,
@@ -71,8 +71,6 @@ import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
import NewFeaturePanel from '@/app/components/base/features/new-feature-panel'
import { fetchFileUploadConfig } from '@/service/common'
import { correctProvider } from '@/utils'
import PluginDependency from '@/app/components/workflow/plugin-dependency'
type PublishConfig = {
modelConfig: ModelConfig
@@ -158,7 +156,7 @@ const Configuration: FC = () => {
const setCompletionParams = (value: FormValue) => {
const params = { ...value }
// eslint-disable-next-line ts/no-use-before-define
// eslint-disable-next-line @typescript-eslint/no-use-before-define
if ((!params.stop || params.stop.length === 0) && (modeModeTypeRef.current === ModelModeType.completion)) {
params.stop = getTempStop()
setTempStop([])
@@ -167,7 +165,7 @@ const Configuration: FC = () => {
}
const [modelConfig, doSetModelConfig] = useState<ModelConfig>({
provider: 'langgenius/openai/openai',
provider: 'openai',
model_id: 'gpt-3.5-turbo',
mode: ModelModeType.unset,
configs: {
@@ -190,7 +188,7 @@ const Configuration: FC = () => {
const isAgent = mode === 'agent-chat'
const isOpenAI = modelConfig.provider === 'langgenius/openai/openai'
const isOpenAI = modelConfig.provider === 'openai'
const [collectionList, setCollectionList] = useState<Collection[]>([])
useEffect(() => {
@@ -363,7 +361,7 @@ const Configuration: FC = () => {
const [canReturnToSimpleMode, setCanReturnToSimpleMode] = useState(true)
const setPromptMode = async (mode: PromptMode) => {
if (mode === PromptMode.advanced) {
// eslint-disable-next-line ts/no-use-before-define
// eslint-disable-next-line @typescript-eslint/no-use-before-define
await migrateToDefaultPrompt()
setCanReturnToSimpleMode(true)
}
@@ -549,19 +547,8 @@ const Configuration: FC = () => {
if (modelConfig.retriever_resource)
setCitationConfig(modelConfig.retriever_resource)
if (modelConfig.annotation_reply) {
let annotationConfig = modelConfig.annotation_reply
if (modelConfig.annotation_reply.enabled) {
annotationConfig = {
...modelConfig.annotation_reply,
embedding_model: {
...modelConfig.annotation_reply.embedding_model,
embedding_provider_name: correctProvider(modelConfig.annotation_reply.embedding_model.embedding_provider_name),
},
}
}
setAnnotationConfig(annotationConfig, true)
}
if (modelConfig.annotation_reply)
setAnnotationConfig(modelConfig.annotation_reply, true)
if (modelConfig.sensitive_word_avoidance)
setModerationConfig(modelConfig.sensitive_word_avoidance)
@@ -571,7 +558,7 @@ const Configuration: FC = () => {
const config = {
modelConfig: {
provider: correctProvider(model.provider),
provider: model.provider,
model_id: model.name,
mode: model.mode,
configs: {
@@ -613,6 +600,7 @@ const Configuration: FC = () => {
annotation_reply: modelConfig.annotation_reply,
external_data_tools: modelConfig.external_data_tools,
dataSets: datasets || [],
// eslint-disable-next-line multiline-ternary
agentConfig: res.mode === 'agent-chat' ? {
max_iteration: DEFAULT_AGENT_SETTING.max_iteration,
...modelConfig.agent_mode,
@@ -623,12 +611,8 @@ const Configuration: FC = () => {
}).map((tool: any) => {
return {
...tool,
isDeleted: res.deleted_tools?.some((deletedTool: any) => deletedTool.id === tool.id && deletedTool.tool_name === tool.tool_name),
isDeleted: res.deleted_tools?.includes(tool.tool_name),
notAuthor: collectionList.find(c => tool.provider_id === c.id)?.is_team_authorization === false,
...(tool.provider_type === 'builtin' ? {
provider_id: correctProvider(tool.provider_name),
provider_name: correctProvider(tool.provider_name),
} : {}),
}
}),
} : DEFAULT_AGENT_SETTING,
@@ -649,12 +633,6 @@ const Configuration: FC = () => {
retrieval_model: RETRIEVE_TYPE.multiWay,
...modelConfig.dataset_configs,
...retrievalConfig,
...(retrievalConfig.reranking_model ? {
reranking_model: {
...retrievalConfig.reranking_model,
reranking_provider_name: correctProvider(modelConfig.dataset_configs.reranking_model.reranking_provider_name),
},
} : {}),
})
setHasFetchedDetail(true)
})
@@ -1042,7 +1020,6 @@ const Configuration: FC = () => {
onAutoAddPromptVariable={handleAddPromptVariable}
/>
)}
<PluginDependency />
</>
</FeaturesProvider>
</ConfigContext.Provider>

View File

@@ -23,7 +23,7 @@ import { DEFAULT_VALUE_MAX_LEN } from '@/config'
import { useStore as useAppStore } from '@/app/components/app/store'
import cn from '@/utils/classnames'
export interface IPromptValuePanelProps {
export type IPromptValuePanelProps = {
appType: AppType
onSend?: () => void
inputs: Inputs

View File

@@ -0,0 +1,124 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import { usePathname, useRouter } from 'next/navigation'
import ConfigParamModal from './config-param-modal'
import Panel from '@/app/components/app/configuration/base/feature-panel'
import { MessageFast } from '@/app/components/base/icons/src/vender/solid/communication'
import Tooltip from '@/app/components/base/tooltip'
import { LinkExternal02, Settings04 } from '@/app/components/base/icons/src/vender/line/general'
import ConfigContext from '@/context/debug-configuration'
import type { EmbeddingModelConfig } from '@/app/components/app/annotation/type'
import { fetchAnnotationConfig, updateAnnotationScore } from '@/service/annotation'
import type { AnnotationReplyConfig as AnnotationReplyConfigType } from '@/models/debug'
type Props = {
onEmbeddingChange: (embeddingModel: EmbeddingModelConfig) => void
onScoreChange: (score: number, embeddingModel?: EmbeddingModelConfig) => void
}
export const Item: FC<{ title: string; tooltip: string; children: JSX.Element }> = ({
title,
tooltip,
children,
}) => {
return (
<div>
<div className='flex items-center space-x-1'>
<div>{title}</div>
<Tooltip
popupContent={
<div className='max-w-[200px] leading-[18px] text-[13px] font-medium text-gray-800'>{tooltip}</div>
}
/>
</div>
<div>{children}</div>
</div>
)
}
const AnnotationReplyConfig: FC<Props> = ({
onEmbeddingChange,
onScoreChange,
}) => {
const { t } = useTranslation()
const router = useRouter()
const pathname = usePathname()
const matched = pathname.match(/\/app\/([^/]+)/)
const appId = (matched?.length && matched[1]) ? matched[1] : ''
const {
annotationConfig,
} = useContext(ConfigContext)
const [isShowEdit, setIsShowEdit] = React.useState(false)
return (
<>
<Panel
className="mt-4"
headerIcon={
<MessageFast className='w-4 h-4 text-[#444CE7]' />
}
title={t('appDebug.feature.annotation.title')}
headerRight={
<div className='flex items-center'>
<div
className='flex items-center rounded-md h-7 px-3 space-x-1 text-gray-700 cursor-pointer hover:bg-gray-200'
onClick={() => { setIsShowEdit(true) }}
>
<Settings04 className="w-[14px] h-[14px]" />
<div className='text-xs font-medium'>
{t('common.operation.params')}
</div>
</div>
<div
className='ml-1 flex items-center h-7 px-3 space-x-1 leading-[18px] text-xs font-medium text-gray-700 rounded-md cursor-pointer hover:bg-gray-200'
onClick={() => {
router.push(`/app/${appId}/annotations`)
}}>
<div>{t('appDebug.feature.annotation.cacheManagement')}</div>
<LinkExternal02 className='w-3.5 h-3.5' />
</div>
</div>
}
noBodySpacing
/>
{isShowEdit && (
<ConfigParamModal
appId={appId}
isShow
onHide={() => {
setIsShowEdit(false)
}}
onSave={async (embeddingModel, score) => {
const annotationConfig = await fetchAnnotationConfig(appId) as AnnotationReplyConfigType
let isEmbeddingModelChanged = false
if (
embeddingModel.embedding_model_name !== annotationConfig.embedding_model.embedding_model_name
|| embeddingModel.embedding_provider_name !== annotationConfig.embedding_model.embedding_provider_name
) {
await onEmbeddingChange(embeddingModel)
isEmbeddingModelChanged = true
}
if (score !== annotationConfig.score_threshold) {
await updateAnnotationScore(appId, annotationConfig.id, score)
if (isEmbeddingModelChanged)
onScoreChange(score, embeddingModel)
else
onScoreChange(score)
}
setIsShowEdit(false)
}}
annotationConfig={annotationConfig}
/>
)}
</>
)
}
export default React.memo(AnnotationReplyConfig)

View File

@@ -0,0 +1,45 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import GroupName from '../base/group-name'
import Moderation from './moderation'
import Annotation from './annotation/config-param'
import type { EmbeddingModelConfig } from '@/app/components/app/annotation/type'
export type ToolboxProps = {
showModerationSettings: boolean
showAnnotation: boolean
onEmbeddingChange: (embeddingModel: EmbeddingModelConfig) => void
onScoreChange: (score: number, embeddingModel?: EmbeddingModelConfig) => void
}
const Toolbox: FC<ToolboxProps> = ({
showModerationSettings,
showAnnotation,
onEmbeddingChange,
onScoreChange,
}) => {
const { t } = useTranslation()
return (
<div className='mt-7'>
<GroupName name={t('appDebug.feature.toolbox.title')} />
{
showModerationSettings && (
<Moderation />
)
}
{
showAnnotation && (
<Annotation
onEmbeddingChange={onEmbeddingChange}
onScoreChange={onScoreChange}
/>
)
}
</div>
)
}
export default React.memo(Toolbox)

View File

@@ -21,13 +21,13 @@ import { useToastContext } from '@/app/components/base/toast'
import AppIcon from '@/app/components/base/app-icon'
const systemTypes = ['api']
interface ExternalDataToolModalProps {
type ExternalDataToolModalProps = {
data: ExternalDataTool
onCancel: () => void
onSave: (externalDataTool: ExternalDataTool) => void
onValidateBeforeSave?: (externalDataTool: ExternalDataTool) => boolean
}
interface Provider {
type Provider = {
key: string
name: string
form_schema?: CodeBasedExtensionItem['form_schema']

View File

@@ -25,7 +25,6 @@ import AppsFull from '@/app/components/billing/apps-full-in-dialog'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { getRedirection } from '@/utils/app-redirection'
import cn from '@/utils/classnames'
import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks'
type CreateFromDSLModalProps = {
show: boolean
@@ -51,7 +50,6 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
const [showErrorModal, setShowErrorModal] = useState(false)
const [versions, setVersions] = useState<{ importedVersion: string; systemVersion: string }>()
const [importId, setImportId] = useState<string>()
const { handleCheckPluginDependencies } = usePluginDependencies()
const readFile = (file: File) => {
const reader = new FileReader()
@@ -116,8 +114,6 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
children: status === DSLImportStatus.COMPLETED_WITH_WARNINGS && t('app.newApp.appCreateDSLWarning'),
})
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
if (app_id)
await handleCheckPluginDependencies(app_id)
getRedirection(isCurrentWorkspaceEditor, { id: app_id }, push)
}
else if (status === DSLImportStatus.PENDING) {
@@ -136,7 +132,6 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
}
}
// eslint-disable-next-line unused-imports/no-unused-vars
catch (e) {
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
}
@@ -163,8 +158,6 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
type: 'success',
message: t('app.newApp.appCreated'),
})
if (app_id)
await handleCheckPluginDependencies(app_id)
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
getRedirection(isCurrentWorkspaceEditor, { id: app_id }, push)
}
@@ -172,7 +165,6 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
}
}
// eslint-disable-next-line unused-imports/no-unused-vars
catch (e) {
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
}
@@ -276,7 +268,7 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
>
<div className='flex pb-4 flex-col items-start gap-2 self-stretch'>
<div className='text-text-primary title-2xl-semi-bold'>{t('app.newApp.appCreateDSLErrorTitle')}</div>
<div className='flex grow flex-col text-text-secondary system-md-regular'>
<div className='flex flex-grow flex-col text-text-secondary system-md-regular'>
<div>{t('app.newApp.appCreateDSLErrorPart1')}</div>
<div>{t('app.newApp.appCreateDSLErrorPart2')}</div>
<br />

View File

@@ -13,7 +13,7 @@ import { useProviderContext } from '@/context/provider-context'
import AppsFull from '@/app/components/billing/apps-full-in-dialog'
import type { AppIconType } from '@/types/app'
export interface DuplicateAppModalProps {
export type DuplicateAppModalProps = {
appName: string
icon_type: AppIconType | null
icon: string

View File

@@ -79,9 +79,6 @@ const HandThumbIconWithCount: FC<{ count: number; iconType: 'up' | 'down' }> = (
}
const statusTdRender = (statusCount: StatusCount) => {
if (!statusCount)
return null
if (statusCount.partial_success + statusCount.failed === 0) {
return (
<div className='inline-flex items-center gap-1 system-xs-semibold-uppercase'>

View File

@@ -27,8 +27,8 @@ const APIKeyInfoPanel: FC = () => {
return null
return (
<div className={cn('bg-components-panel-bg border-components-panel-border', 'mb-6 relative rounded-2xl shadow-md border p-8 ')}>
<div className={cn('text-[24px] text-text-primary font-semibold', isCloud ? 'flex items-center h-8 space-x-1' : 'leading-8 mb-6')}>
<div className={cn('bg-[#EFF4FF] border-[#D1E0FF]', 'mb-6 relative rounded-2xl shadow-md border p-8 ')}>
<div className={cn('text-[24px] text-gray-800 font-semibold', isCloud ? 'flex items-center h-8 space-x-1' : 'leading-8 mb-6')}>
{isCloud && <em-emoji id={'😀'} />}
{isCloud
? (
@@ -42,11 +42,11 @@ const APIKeyInfoPanel: FC = () => {
)}
</div>
{isCloud && (
<div className='mt-1 text-sm text-text-tertiary font-normal'>{t(`appOverview.apiKeyInfo.cloud.${'trial'}.description`)}</div>
<div className='mt-1 text-sm text-gray-600 font-normal'>{t(`appOverview.apiKeyInfo.cloud.${'trial'}.description`)}</div>
)}
<Button
variant='primary'
className='mt-2 space-x-2'
className='space-x-2'
onClick={() => setShowAccountSettingModal({ payload: 'provider' })}
>
<div className='text-sm font-medium'>{t('appOverview.apiKeyInfo.setAPIBtn')}</div>
@@ -65,7 +65,7 @@ const APIKeyInfoPanel: FC = () => {
<div
onClick={() => setIsShow(false)}
className='absolute right-4 top-4 flex items-center justify-center w-8 h-8 cursor-pointer '>
<RiCloseLine className='w-4 h-4 text-text-tertiary' />
<RiCloseLine className='w-4 h-4 text-gray-500' />
</div>
</div>
)

View File

@@ -0,0 +1,29 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import s from './style.module.css'
import cn from '@/utils/classnames'
export type IProgressProps = {
className?: string
value: number // percent
}
const Progress: FC<IProgressProps> = ({
className,
value,
}) => {
const exhausted = value === 100
return (
<div className={cn(className, 'relative grow h-2 flex bg-gray-200 rounded-md overflow-hidden')}>
<div
className={cn(s.bar, exhausted && s['bar-error'], 'absolute top-0 left-0 right-0 bottom-0')}
style={{ width: `${value}%` }}
/>
{Array(10).fill(0).map((i, k) => (
<div key={k} className={s['bar-item']} />
))}
</div>
)
}
export default React.memo(Progress)

View File

@@ -0,0 +1,16 @@
.bar {
background: linear-gradient(90deg, rgba(41, 112, 255, 0.9) 0%, rgba(21, 94, 239, 0.9) 100%);
}
.bar-error {
background: linear-gradient(90deg, rgba(240, 68, 56, 0.72) 0%, rgba(217, 45, 32, 0.9) 100%);
}
.bar-item {
width: 10%;
border-right: 1px solid rgba(255, 255, 255, 0.5);
}
.bar-item:last-of-type {
border-right: 0;
}

View File

@@ -1,9 +1,6 @@
'use client'
import type { HTMLProps } from 'react'
import React, { useMemo, useState } from 'react'
import {
RiLoopLeftLine,
} from '@remixicon/react'
import {
Cog8ToothIcon,
DocumentTextIcon,
@@ -19,25 +16,24 @@ import style from './style.module.css'
import type { ConfigParams } from './settings'
import Tooltip from '@/app/components/base/tooltip'
import AppBasic from '@/app/components/app-sidebar/basic'
import { asyncRunSafe } from '@/utils'
import { asyncRunSafe, randomString } from '@/utils'
import Button from '@/app/components/base/button'
import Tag from '@/app/components/base/tag'
import Switch from '@/app/components/base/switch'
import Divider from '@/app/components/base/divider'
import CopyFeedback from '@/app/components/base/copy-feedback'
import ActionButton from '@/app/components/base/action-button'
import Confirm from '@/app/components/base/confirm'
import ShareQRCode from '@/app/components/base/qrcode'
import SecretKeyButton from '@/app/components/develop/secret-key/secret-key-button'
import type { AppDetailResponse } from '@/models/app'
import { useAppContext } from '@/context/app-context'
import type { AppSSO } from '@/types/app'
import cn from '@/utils/classnames'
export type IAppCardProps = {
className?: string
appInfo: AppDetailResponse & Partial<AppSSO>
cardType?: 'api' | 'webapp'
customBgColor?: string
onChangeStatus: (val: boolean) => Promise<void>
onSaveSiteConfig?: (params: ConfigParams) => Promise<void>
onGenerateCode?: () => Promise<void>
@@ -50,6 +46,7 @@ const EmbedIcon = ({ className = '' }: HTMLProps<HTMLDivElement>) => {
function AppCard({
appInfo,
cardType = 'webapp',
customBgColor,
onChangeStatus,
onSaveSiteConfig,
onGenerateCode,
@@ -95,6 +92,10 @@ function AppCard({
const appUrl = `${app_base_url}/${appMode}/${access_token}`
const apiUrl = appInfo?.api_base_url
let bgColor = 'bg-primary-50 bg-opacity-40'
if (cardType === 'api')
bgColor = 'bg-purple-50'
const genClickFuncByName = (opName: string) => {
switch (opName) {
case t('appOverview.overview.appInfo.preview'):
@@ -132,8 +133,11 @@ function AppCard({
}
return (
<div className={cn('rounded-xl border-effects-highlight border-t border-l-[0.5px] bg-background-default', className)}>
<div className={cn('px-6 py-5')}>
<div
className={
`shadow-xs border-[0.5px] rounded-lg border-gray-200 ${className ?? ''}`}
>
<div className={`px-6 py-5 ${customBgColor ?? bgColor} rounded-lg`}>
<div className="mb-2.5 flex flex-row items-start justify-between">
<AppBasic
iconType={cardType}
@@ -157,20 +161,23 @@ function AppCard({
</div>
<div className="flex flex-col justify-center py-2">
<div className="py-1">
<div className="pb-1 text-xs text-text-tertiary">
<div className="pb-1 text-xs text-gray-500">
{isApp
? t('appOverview.overview.appInfo.accessibleAddress')
: t('appOverview.overview.apiInfo.accessibleAddress')}
</div>
<div className="w-full h-9 px-2 py-0.5 bg-components-input-bg-normal rounded-lg justify-start items-center inline-flex">
<div className="w-full h-9 pl-2 pr-0.5 py-0.5 bg-black bg-opacity-2 rounded-lg border border-black border-opacity-5 justify-start items-center inline-flex">
<div className="h-4 px-2 justify-start items-start gap-2 flex flex-1 min-w-0">
<div className="text-text-secondary system-xs-medium truncate">
<div className="text-gray-700 text-xs font-medium text-ellipsis overflow-hidden whitespace-nowrap">
{isApp ? appUrl : apiUrl}
</div>
</div>
<Divider type="vertical" className="!h-3.5 shrink-0" />
{isApp && <ShareQRCode content={isApp ? appUrl : apiUrl} />}
<CopyFeedback content={isApp ? appUrl : apiUrl}/>
<Divider type="vertical" className="!h-3.5 shrink-0 !mx-0.5" />
{isApp && <ShareQRCode content={isApp ? appUrl : apiUrl} selectorId={randomString(8)} className={'hover:bg-gray-200'} />}
<CopyFeedback
content={isApp ? appUrl : apiUrl}
className={'hover:bg-gray-200'}
/>
{/* button copy link/ button regenerate */}
{showConfirmDelete && (
<Confirm
@@ -189,16 +196,22 @@ function AppCard({
<Tooltip
popupContent={t('appOverview.overview.appInfo.regenerate') || ''}
>
<ActionButton onClick={() => setShowConfirmDelete(true)}>
<RiLoopLeftLine className={cn('w-4 h-4', genLoading && 'animate-spin')} />
</ActionButton>
<div
className="w-8 h-8 ml-0.5 cursor-pointer hover:bg-gray-200 rounded-lg"
onClick={() => setShowConfirmDelete(true)}
>
<div
className={
`w-full h-full ${style.refreshIcon} ${genLoading ? style.generateLogo : ''}`}
></div>
</div>
</Tooltip>
)}
</div>
</div>
</div>
<div className={'pt-2 flex flex-row items-center flex-wrap gap-y-2'}>
{!isApp && <SecretKeyButton className='shrink-0 !h-8 mr-2' textCls='!text-text-secondary font-medium' iconCls='stroke-[1.2px]' appId={appInfo.id} />}
{!isApp && <SecretKeyButton className='flex-shrink-0 !h-8 bg-white mr-2' textCls='!text-gray-700 font-medium' iconCls='stroke-[1.2px]' appId={appInfo.id} />}
{OPERATIONS_MAP[cardType].map((op) => {
const disabled
= op.opName === t('appOverview.overview.appInfo.settings.entry')

View File

@@ -6,6 +6,7 @@ import type { EChartsOption } from 'echarts'
import useSWR from 'swr'
import dayjs from 'dayjs'
import { get } from 'lodash-es'
import Decimal from 'decimal.js'
import { useTranslation } from 'react-i18next'
import { formatNumber } from '@/utils/format'
import Basic from '@/app/components/app-sidebar/basic'
@@ -60,10 +61,8 @@ const CHART_TYPE_CONFIG: Record<string, IChartConfigType> = {
},
}
const sum = (arr: number[]): number => {
return arr.reduce((acr, cur) => {
return acr + cur
})
const sum = (arr: Decimal.Value[]): number => {
return Decimal.sum(...arr).toNumber()
}
const defaultPeriod = {
@@ -216,8 +215,8 @@ const Chart: React.FC<IChartProps> = ({
return `<div style='color:#6B7280;font-size:12px'>${params.name}</div>
<div style='font-size:14px;color:#1F2A37'>${valueFormatter((params.data as any)[yField])}
${!CHART_TYPE_CONFIG[chartType].showTokens
? ''
: `<span style='font-size:12px'>
? ''
: `<span style='font-size:12px'>
<span style='margin-left:4px;color:#6B7280'>(</span>
<span style='color:#FF8A4C'>~$${get(params.data, 'total_price', 0)}</span>
<span style='color:#6B7280'>)</span>
@@ -231,7 +230,7 @@ const Chart: React.FC<IChartProps> = ({
const sumData = isAvg ? (sum(yData) / yData.length) : sum(yData)
return (
<div className={`flex flex-col w-full px-6 py-4 rounded-xl bg-components-chart-bg shadow-xs ${className ?? ''}`}>
<div className={`flex flex-col w-full px-6 py-4 border-[0.5px] rounded-lg border-gray-200 shadow-xs ${className ?? ''}`}>
<div className='mb-3'>
<Basic name={title} type={timePeriod} hoverTip={explanation} />
</div>
@@ -242,11 +241,11 @@ const Chart: React.FC<IChartProps> = ({
type={!CHART_TYPE_CONFIG[chartType].showTokens
? ''
: <span>{t('appOverview.analysis.tokenUsage.consumed')} Tokens<span className='text-sm'>
<span className='ml-1 text-text-tertiary'>(</span>
<span className='text-orange-400'>~{sum(statistics.map(item => Number.parseFloat(get(item, 'total_price', '0')))).toLocaleString('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 4 })}</span>
<span className='text-text-tertiary'>)</span>
<span className='ml-1 text-gray-500'>(</span>
<span className='text-orange-400'>~{sum(statistics.map(item => parseFloat(get(item, 'total_price', '0')))).toLocaleString('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 4 })}</span>
<span className='text-gray-500'>)</span>
</span></span>}
textStyle={{ main: `!text-3xl !font-normal ${sumData === 0 ? '!text-text-quaternary' : ''}` }} />
textStyle={{ main: `!text-3xl !font-normal ${sumData === 0 ? '!text-gray-300' : ''}` }} />
</div>
<ReactECharts option={options} style={{ height: 160 }} />
</div>

View File

@@ -21,7 +21,7 @@ type IShareLinkProps = {
}
const StepNum: FC<{ children: React.ReactNode }> = ({ children }) =>
<div className='h-7 w-7 flex justify-center items-center shrink-0 mr-3 text-text-accent bg-util-colors-blue-blue-50 rounded-2xl'>
<div className='h-7 w-7 flex justify-center items-center flex-shrink-0 mr-3 text-primary-600 bg-primary-50 rounded-2xl'>
{children}
</div>
@@ -54,27 +54,27 @@ const CustomizeModal: FC<IShareLinkProps> = ({
className='!max-w-2xl w-[640px]'
closable={true}
>
<div className='w-full mt-4 px-6 py-5 border-components-panel-border rounded-lg border-[0.5px]'>
<Tag bordered={true} hideBg={true} className='text-text-accent-secondary border-text-accent-secondary uppercase'>{t(`${prefixCustomize}.way`)} 1</Tag>
<p className='my-2 system-sm-medium text-text-secondary'>{t(`${prefixCustomize}.way1.name`)}</p>
<div className='w-full mt-4 px-6 py-5 border-gray-200 rounded-lg border-[0.5px]'>
<Tag bordered={true} hideBg={true} className='text-primary-600 border-primary-600 uppercase'>{t(`${prefixCustomize}.way`)} 1</Tag>
<p className='my-2 text-base font-medium text-gray-800'>{t(`${prefixCustomize}.way1.name`)}</p>
<div className='flex py-4'>
<StepNum>1</StepNum>
<div className='flex flex-col'>
<div className='text-text-primary'>{t(`${prefixCustomize}.way1.step1`)}</div>
<div className='text-text-tertiary text-xs mt-1 mb-2'>{t(`${prefixCustomize}.way1.step1Tip`)}</div>
<div className='text-gray-900'>{t(`${prefixCustomize}.way1.step1`)}</div>
<div className='text-gray-500 text-xs mt-1 mb-2'>{t(`${prefixCustomize}.way1.step1Tip`)}</div>
<a href={`https://github.com/langgenius/${isChatApp ? 'webapp-conversation' : 'webapp-text-generator'}`} target='_blank' rel='noopener noreferrer'>
<Button><GithubIcon className='text-text-secondary mr-2' />{t(`${prefixCustomize}.way1.step1Operation`)}</Button>
<Button><GithubIcon className='text-gray-800 mr-2' />{t(`${prefixCustomize}.way1.step1Operation`)}</Button>
</a>
</div>
</div>
<div className='flex pt-4'>
<StepNum>2</StepNum>
<div className='flex flex-col'>
<div className='text-text-primary'>{t(`${prefixCustomize}.way1.step3`)}</div>
<div className='text-text-tertiary text-xs mt-1 mb-2'>{t(`${prefixCustomize}.way1.step2Tip`)}</div>
<div className='text-gray-900'>{t(`${prefixCustomize}.way1.step3`)}</div>
<div className='text-gray-500 text-xs mt-1 mb-2'>{t(`${prefixCustomize}.way1.step2Tip`)}</div>
<a href="https://vercel.com/docs/concepts/deployments/git/vercel-for-github" target='_blank' rel='noopener noreferrer'>
<Button>
<div className='mr-1.5 border-solid border-t-0 border-r-[7px] border-l-[7px] border-b-[12px] border-r-transparent border-text-primary border-l-transparent border-t-transparent'></div>
<div className='mr-1.5 border-solid border-t-0 border-r-[7px] border-l-[7px] border-b-[12px] border-r-transparent border-b-black border-l-transparent border-t-transparent'></div>
<span>{t(`${prefixCustomize}.way1.step2Operation`)}</span>
</Button>
</a>
@@ -83,9 +83,9 @@ const CustomizeModal: FC<IShareLinkProps> = ({
<div className='flex py-4'>
<StepNum>3</StepNum>
<div className='flex flex-col w-full overflow-hidden'>
<div className='text-text-primary'>{t(`${prefixCustomize}.way1.step3`)}</div>
<div className='text-text-tertiary text-xs mt-1 mb-2'>{t(`${prefixCustomize}.way1.step3Tip`)}</div>
<pre className='overflow-x-scroll box-border py-3 px-4 bg-background-section text-xs font-medium rounded-lg select-text text-text-secondary border-[0.5px] border-components-panel-border'>
<div className='text-gray-900'>{t(`${prefixCustomize}.way1.step3`)}</div>
<div className='text-gray-500 text-xs mt-1 mb-2'>{t(`${prefixCustomize}.way1.step3Tip`)}</div>
<pre className='overflow-x-scroll box-border py-3 px-4 bg-gray-100 text-xs font-medium rounded-lg select-text'>
NEXT_PUBLIC_APP_ID={`'${appId}'`} <br />
NEXT_PUBLIC_APP_KEY={'\'<Web API Key From Dify>\''} <br />
NEXT_PUBLIC_API_URL={`'${api_base_url}'`}
@@ -94,9 +94,9 @@ const CustomizeModal: FC<IShareLinkProps> = ({
</div>
</div>
<div className='w-full mt-4 px-6 py-5 border-components-panel-border rounded-lg border-[0.5px]'>
<Tag bordered={true} hideBg={true} className='text-text-accent-secondary border-text-accent-secondary uppercase'>{t(`${prefixCustomize}.way`)} 2</Tag>
<p className='my-2 system-sm-medium text-text-secondary'>{t(`${prefixCustomize}.way2.name`)}</p>
<div className='w-full mt-4 px-6 py-5 border-gray-200 rounded-lg border-[0.5px]'>
<Tag bordered={true} hideBg={true} className='text-primary-600 border-primary-600 uppercase'>{t(`${prefixCustomize}.way`)} 2</Tag>
<p className='mt-2 text-base font-medium text-gray-800'>{t(`${prefixCustomize}.way2.name`)}</p>
<Button
className='mt-2'
onClick={() =>
@@ -109,8 +109,8 @@ const CustomizeModal: FC<IShareLinkProps> = ({
)
}
>
<span className='text-sm text-text-secondary'>{t(`${prefixCustomize}.way2.operation`)}</span>
<ArrowTopRightOnSquareIcon className='w-4 h-4 ml-1 text-text-secondary shrink-0' />
<span className='text-sm text-gray-800'>{t(`${prefixCustomize}.way2.operation`)}</span>
<ArrowTopRightOnSquareIcon className='w-4 h-4 ml-1 text-gray-800 shrink-0' />
</Button>
</div>
</Modal>

View File

@@ -1,19 +1,15 @@
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiClipboardFill,
RiClipboardLine,
} from '@remixicon/react'
import copy from 'copy-to-clipboard'
import style from './style.module.css'
import cn from '@/utils/classnames'
import Modal from '@/app/components/base/modal'
import copyStyle from '@/app/components/base/copy-btn/style.module.css'
import Tooltip from '@/app/components/base/tooltip'
import { useAppContext } from '@/context/app-context'
import { IS_CE_EDITION } from '@/config'
import type { SiteInfo } from '@/models/share'
import { useThemeContext } from '@/app/components/base/chat/embedded-chatbot/theme/theme-context'
import ActionButton from '@/app/components/base/action-button'
import cn from '@/utils/classnames'
type Props = {
siteInfo?: SiteInfo
@@ -39,12 +35,12 @@ const OPTION_MAP = {
`<script>
window.difyChatbotConfig = {
token: '${token}'${isTestEnv
? `,
? `,
isDev: true`
: ''}${IS_CE_EDITION
? `,
: ''}${IS_CE_EDITION
? `,
baseUrl: '${url}'`
: ''}
: ''}
}
</script>
<script
@@ -123,7 +119,7 @@ const Embedded = ({ siteInfo, isShow, onClose, appBaseUrl, accessToken, classNam
wrapperClassName={className}
closable={true}
>
<div className="mb-4 mt-8 text-text-primary system-sm-medium">
<div className="mb-4 mt-8 text-gray-900 text-[14px] font-medium leading-tight">
{t(`${prefixEmbedded}.explanation`)}
</div>
<div className="flex flex-wrap items-center justify-between gap-y-2">
@@ -147,37 +143,30 @@ const Embedded = ({ siteInfo, isShow, onClose, appBaseUrl, accessToken, classNam
{option === 'chromePlugin' && (
<div className="w-full mt-6">
<div className={cn('gap-2 py-3 justify-center items-center inline-flex w-full rounded-lg',
'bg-primary-600 hover:bg-primary-600/75 cursor-pointer text-white hover:shadow-sm flex-shrink-0')}>
'bg-primary-600 hover:bg-primary-600/75 hover:shadow-md cursor-pointer text-white hover:shadow-sm flex-shrink-0')}>
<div className={`w-4 h-4 relative ${style.pluginInstallIcon}`}></div>
<div className="text-white text-sm font-medium font-['Inter'] leading-tight" onClick={navigateToChromeUrl}>{t(`${prefixEmbedded}.chromePlugin`)}</div>
</div>
</div>
)}
<div className={cn('w-full bg-background-section border-[0.5px] border-components-panel-border rounded-lg flex-col justify-start items-start inline-flex',
<div className={cn('w-full bg-gray-100 rounded-lg flex-col justify-start items-start inline-flex',
'mt-6')}>
<div className="inline-flex items-center self-stretch justify-start gap-2 py-1 pl-3 pr-1 rounded-t-lg bg-background-section-burn">
<div className="grow shrink-0 text-text-secondary system-sm-medium">
<div className="inline-flex items-center self-stretch justify-start gap-2 py-1 pl-3 pr-1 border border-black rounded-tl-lg rounded-tr-lg bg-gray-50 border-opacity-5">
<div className="grow shrink basis-0 text-slate-700 text-[13px] font-medium leading-none">
{t(`${prefixEmbedded}.${option}`)}
</div>
<Tooltip
popupContent={
(isCopied[option]
? t(`${prefixEmbedded}.copied`)
: t(`${prefixEmbedded}.copy`)) || ''
}
>
<ActionButton>
<div
onClick={onClickCopy}
>
{isCopied[option] && <RiClipboardFill className='w-4 h-4' />}
{!isCopied[option] && <RiClipboardLine className='w-4 h-4' />}
<div className="flex items-center justify-center gap-1 p-2 rounded-lg">
<Tooltip
popupContent={(isCopied[option] ? t(`${prefixEmbedded}.copied`) : t(`${prefixEmbedded}.copy`)) || ''}
>
<div className="w-8 h-8 rounded-lg cursor-pointer hover:bg-gray-100">
<div onClick={onClickCopy} className={`w-full h-full ${copyStyle.copyIcon} ${isCopied[option] ? copyStyle.copied : ''}`}></div>
</div>
</ActionButton>
</Tooltip>
</Tooltip>
</div>
</div>
<div className="flex items-start justify-start w-full gap-2 p-3 overflow-x-auto">
<div className="grow shrink basis-0 text-text-secondary text-[13px] leading-tight font-mono">
<div className="grow shrink basis-0 text-slate-700 text-[13px] leading-tight font-mono">
<pre className='select-text'>{OPTION_MAP[option].getContent(appBaseUrl, accessToken, themeBuilder.theme?.primaryColor ?? '#1C64F2', isTestEnv)}</pre>
</div>
</div>

View File

@@ -5,6 +5,7 @@ import { ChevronRightIcon } from '@heroicons/react/20/solid'
import Link from 'next/link'
import { Trans, useTranslation } from 'react-i18next'
import { useContextSelector } from 'use-context-selector'
import s from './style.module.css'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
@@ -20,8 +21,6 @@ import Tooltip from '@/app/components/base/tooltip'
import AppContext, { useAppContext } from '@/context/app-context'
import type { AppIconSelection } from '@/app/components/base/app-icon-picker'
import AppIconPicker from '@/app/components/base/app-icon-picker'
import Divider from '@/app/components/base/divider'
import cn from '@/utils/classnames'
export type ISettingsModalProps = {
isChat: boolean
@@ -196,9 +195,9 @@ const SettingsModal: FC<ISettingsModalProps> = ({
title={t(`${prefixSettings}.title`)}
isShow={isShow}
onClose={onHide}
className='max-w-[520px]'
className={`${s.settingsModal}`}
>
<div className={cn('mt-6 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.webName`)}</div>
<div className={`mt-6 font-medium ${s.settingTitle} text-gray-900`}>{t(`${prefixSettings}.webName`)}</div>
<div className='flex mt-2'>
<AppIcon size='large'
onClick={() => { setShowAppIconPicker(true) }}
@@ -215,8 +214,8 @@ const SettingsModal: FC<ISettingsModalProps> = ({
placeholder={t('app.appNamePlaceholder') || ''}
/>
</div>
<div className={cn('mt-6 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.webDesc`)}</div>
<p className={cn('mt-1 body-xs-regular text-text-tertiary')}>{t(`${prefixSettings}.webDescTip`)}</p>
<div className={`mt-6 font-medium ${s.settingTitle} text-gray-900 `}>{t(`${prefixSettings}.webDesc`)}</div>
<p className={`mt-1 ${s.settingsTip} text-gray-500`}>{t(`${prefixSettings}.webDescTip`)}</p>
<Textarea
className='mt-2'
value={inputInfo.desc}
@@ -226,36 +225,36 @@ const SettingsModal: FC<ISettingsModalProps> = ({
{isChatBot && (
<div className='w-full mt-4'>
<div className='flex justify-between items-center'>
<div className={cn('system-sm-semibold text-text-secondary')}>{t('app.answerIcon.title')}</div>
<div className={`font-medium ${s.settingTitle} text-gray-900 `}>{t('app.answerIcon.title')}</div>
<Switch
defaultValue={inputInfo.use_icon_as_answer_icon}
onChange={v => setInputInfo({ ...inputInfo, use_icon_as_answer_icon: v })}
/>
</div>
<p className='body-xs-regular text-text-tertiary'>{t('app.answerIcon.description')}</p>
<p className='body-xs-regular text-gray-500'>{t('app.answerIcon.description')}</p>
</div>
)}
<div className={cn('mt-6 mb-2 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.language`)}</div>
<div className={`mt-6 mb-2 font-medium ${s.settingTitle} text-gray-900 `}>{t(`${prefixSettings}.language`)}</div>
<SimpleSelect
items={languages.filter(item => item.supported)}
defaultValue={language}
onSelect={item => setLanguage(item.value as Language)}
/>
<div className='w-full mt-8'>
<p className='system-xs-medium text-text-tertiary'>{t(`${prefixSettings}.workflow.title`)}</p>
<p className='system-xs-medium text-gray-500'>{t(`${prefixSettings}.workflow.title`)}</p>
<div className='flex justify-between items-center'>
<div className='font-medium system-sm-semibold grow text-text-primary'>{t(`${prefixSettings}.workflow.subTitle`)}</div>
<div className='font-medium system-sm-semibold flex-grow text-gray-900'>{t(`${prefixSettings}.workflow.subTitle`)}</div>
<Switch
disabled={!(appInfo.mode === 'workflow' || appInfo.mode === 'advanced-chat')}
defaultValue={inputInfo.show_workflow_steps}
onChange={v => setInputInfo({ ...inputInfo, show_workflow_steps: v })}
/>
</div>
<p className='body-xs-regular text-text-tertiary'>{t(`${prefixSettings}.workflow.showDesc`)}</p>
<p className='body-xs-regular text-gray-500'>{t(`${prefixSettings}.workflow.showDesc`)}</p>
</div>
{isChat && <> <div className={cn('mt-8 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.chatColorTheme`)}</div>
<p className='mt-1 body-xs-regular text-text-tertiary'>{t(`${prefixSettings}.chatColorThemeDesc`)}</p>
{isChat && <> <div className={`mt-8 font-medium ${s.settingTitle} text-gray-900`}>{t(`${prefixSettings}.chatColorTheme`)}</div>
<p className={`mt-1 ${s.settingsTip} text-gray-500`}>{t(`${prefixSettings}.chatColorThemeDesc`)}</p>
<Input
className='mt-2 h-10'
value={inputInfo.chatColorTheme ?? ''}
@@ -263,14 +262,14 @@ const SettingsModal: FC<ISettingsModalProps> = ({
placeholder='E.g #A020F0'
/>
<div className="mt-1 flex justify-between items-center">
<p className='ml-2 body-xs-regular text-text-tertiary'>{t(`${prefixSettings}.chatColorThemeInverted`)}</p>
<p className={`ml-2 ${s.settingsTip} text-gray-500`}>{t(`${prefixSettings}.chatColorThemeInverted`)}</p>
<Switch defaultValue={inputInfo.chatColorThemeInverted} onChange={v => setInputInfo({ ...inputInfo, chatColorThemeInverted: v })}></Switch>
</div>
</>}
{systemFeatures.enable_web_sso_switch_component && <div className='w-full mt-8'>
<p className='system-xs-medium text-text-tertiary'>{t(`${prefixSettings}.sso.label`)}</p>
<p className='system-xs-medium text-gray-500'>{t(`${prefixSettings}.sso.label`)}</p>
<div className='flex justify-between items-center'>
<div className='system-sm-semibold grow text-text-secondary'>{t(`${prefixSettings}.sso.title`)}</div>
<div className='font-medium system-sm-semibold flex-grow text-gray-900'>{t(`${prefixSettings}.sso.title`)}</div>
<Tooltip
disabled={systemFeatures.sso_enforced_for_web}
popupContent={
@@ -281,31 +280,31 @@ const SettingsModal: FC<ISettingsModalProps> = ({
<Switch disabled={!systemFeatures.sso_enforced_for_web || !isCurrentWorkspaceEditor} defaultValue={systemFeatures.sso_enforced_for_web && inputInfo.enable_sso} onChange={v => setInputInfo({ ...inputInfo, enable_sso: v })}></Switch>
</Tooltip>
</div>
<p className='body-xs-regular text-text-tertiary'>{t(`${prefixSettings}.sso.description`)}</p>
<p className='body-xs-regular text-gray-500'>{t(`${prefixSettings}.sso.description`)}</p>
</div>}
{!isShowMore && <div className='w-full cursor-pointer mt-8' onClick={() => setIsShowMore(true)}>
<div className='flex justify-between'>
<div className='system-sm-semibold text-text-secondary'>{t(`${prefixSettings}.more.entry`)}</div>
<div className='shrink-0 w-4 h-4 text-text-tertiary'>
<div className={`font-medium ${s.settingTitle} flex-grow text-gray-900`}>{t(`${prefixSettings}.more.entry`)}</div>
<div className='flex-shrink-0 w-4 h-4 text-gray-500'>
<ChevronRightIcon />
</div>
</div>
<p className='mt-1 body-xs-regular text-text-tertiary'>{t(`${prefixSettings}.more.copyright`)} & {t(`${prefixSettings}.more.privacyPolicy`)}</p>
<p className={`mt-1 ${s.policy} text-gray-500`}>{t(`${prefixSettings}.more.copyright`)} & {t(`${prefixSettings}.more.privacyPolicy`)}</p>
</div>}
{isShowMore && <>
<Divider className='my-6' />
<div className='system-sm-semibold text-text-secondary'>{t(`${prefixSettings}.more.copyright`)}</div>
<hr className='w-full mt-6' />
<div className={`mt-6 font-medium ${s.settingTitle} text-gray-900`}>{t(`${prefixSettings}.more.copyright`)}</div>
<Input
className='mt-2 h-10'
value={inputInfo.copyright}
onChange={onChange('copyright')}
placeholder={t(`${prefixSettings}.more.copyRightPlaceholder`) as string}
/>
<div className='mt-8 system-sm-semibold text-text-secondary'>{t(`${prefixSettings}.more.privacyPolicy`)}</div>
<p className='mt-1 body-xs-regular text-text-tertiary'>
<div className={`mt-8 font-medium ${s.settingTitle} text-gray-900`}>{t(`${prefixSettings}.more.privacyPolicy`)}</div>
<p className={`mt-1 ${s.settingsTip} text-gray-500`}>
<Trans
i18nKey={`${prefixSettings}.more.privacyPolicyTip`}
components={{ privacyPolicyLink: <Link href={'https://docs.dify.ai/user-agreement/privacy-policy'} target='_blank' rel='noopener noreferrer' className='text-text-accent' /> }}
components={{ privacyPolicyLink: <Link href={'https://docs.dify.ai/user-agreement/privacy-policy'} target='_blank' rel='noopener noreferrer' className='text-primary-600' /> }}
/>
</p>
<Input
@@ -314,8 +313,8 @@ const SettingsModal: FC<ISettingsModalProps> = ({
onChange={onChange('privacyPolicy')}
placeholder={t(`${prefixSettings}.more.privacyPolicyPlaceholder`) as string}
/>
<div className='mt-8 system-sm-semibold text-text-secondary'>{t(`${prefixSettings}.more.customDisclaimer`)}</div>
<p className='mt-1 body-xs-regular text-text-tertiary'>{t(`${prefixSettings}.more.customDisclaimerTip`)}</p>
<div className={`mt-8 font-medium ${s.settingTitle} text-gray-900`}>{t(`${prefixSettings}.more.customDisclaimer`)}</div>
<p className={`mt-1 ${s.settingsTip} text-gray-500`}>{t(`${prefixSettings}.more.customDisclaimerTip`)}</p>
<Input
className='mt-2 h-10'
value={inputInfo.customDisclaimer}

View File

@@ -0,0 +1,18 @@
.settingsModal {
max-width: 32.5rem !important;
}
.settingTitle {
line-height: 21px;
font-size: 0.875rem;
}
.settingsTip {
line-height: 1.125rem;
font-size: 0.75rem;
}
.policy {
font-size: 0.75rem;
line-height: 1.125rem;
}

View File

@@ -2,7 +2,7 @@ import { create } from 'zustand'
import type { App, AppSSO } from '@/types/app'
import type { IChatItem } from '@/app/components/base/chat/chat/type'
interface State {
type State = {
appDetail?: App & Partial<AppSSO>
appSidebarExpand: string
currentLogItem?: IChatItem
@@ -13,7 +13,7 @@ interface State {
showAppConfigureFeaturesModal: boolean
}
interface Action {
type Action = {
setAppDetail: (appDetail?: App & Partial<AppSSO>) => void
setAppSiderbarExpand: (state: string) => void
setCurrentLogItem: (item?: IChatItem) => void

View File

@@ -25,7 +25,7 @@ import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/aler
import AppIcon from '@/app/components/base/app-icon'
import { useStore as useAppStore } from '@/app/components/app/store'
interface SwitchAppModalProps {
type SwitchAppModalProps = {
show: boolean
appDetail: App
onSuccess?: () => void

View File

@@ -33,7 +33,7 @@ import { useChatContext } from '@/app/components/base/chat/chat/context'
const MAX_DEPTH = 3
export interface IGenerationItemProps {
export type IGenerationItemProps = {
isWorkflow?: boolean
workflowProcessData?: WorkflowProcess
className?: string
@@ -306,8 +306,14 @@ const GenerationItem: FC<IGenerationItemProps> = ({
}
<div className={`flex ${contentClassName}`}>
<div className='grow w-0'>
{siteInfo && siteInfo.show_workflow_steps && workflowProcessData && (
<WorkflowProcessItem data={workflowProcessData} expand={workflowProcessData.expand} hideProcessDetail={hideProcessDetail} />
{siteInfo && workflowProcessData && (
<WorkflowProcessItem
data={workflowProcessData}
expand={workflowProcessData.expand}
hideProcessDetail={hideProcessDetail}
hideInfo={hideProcessDetail}
readonly={!siteInfo.show_workflow_steps}
/>
)}
{workflowProcessData && !isError && (
<ResultTab data={workflowProcessData} content={content} currentTab={currentTab} onCurrentTabChange={setCurrentTab} />

View File

@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'
import { RiCloseLine } from '@remixicon/react'
import Run from '@/app/components/workflow/run'
interface ILogDetail {
type ILogDetail = {
runID: string
onClose: () => void
}

View File

@@ -6,7 +6,7 @@ import type { QueryParam } from './index'
import Chip from '@/app/components/base/chip'
import Input from '@/app/components/base/input'
interface IFilterProps {
type IFilterProps = {
queryParams: QueryParam
setQueryParams: (v: QueryParam) => void
}

View File

@@ -2,7 +2,9 @@
@layer components {
.action-btn {
@apply inline-flex justify-center items-center cursor-pointer text-text-tertiary hover:text-text-secondary hover:bg-state-base-hover
@apply inline-flex justify-center items-center cursor-pointer text-text-tertiary
hover:text-text-secondary
hover:bg-state-base-hover
}
.action-btn-disabled {
@@ -27,15 +29,21 @@
}
.action-btn.action-btn-active {
@apply text-text-accent bg-state-accent-active hover:bg-state-accent-active-alt
@apply
text-text-accent
bg-state-accent-active
hover:bg-state-accent-active-alt
}
.action-btn.action-btn-disabled {
@apply text-text-disabled
@apply
text-text-disabled
}
.action-btn.action-btn-destructive {
@apply text-text-destructive bg-state-destructive-hover
@apply
text-text-destructive
bg-state-destructive-hover
}
}

View File

@@ -28,7 +28,7 @@ const actionButtonVariants = cva(
)
export type ActionButtonProps = {
size?: 'xs' | 's' | 'm' | 'l' | 'xl'
size?: 'xs' | 'm' | 'l' | 'xl'
state?: ActionButtonState
styleCss?: CSSProperties
} & React.ButtonHTMLAttributes<HTMLButtonElement> & VariantProps<typeof actionButtonVariants>

View File

@@ -1,23 +0,0 @@
.appIcon {
@apply flex items-center justify-center relative w-9 h-9 text-lg rounded-lg grow-0 shrink-0;
}
.appIcon.large {
@apply w-10 h-10;
}
.appIcon.small {
@apply w-8 h-8;
}
.appIcon.tiny {
@apply w-6 h-6 text-base;
}
.appIcon.xs {
@apply w-5 h-5 text-base;
}
.appIcon.rounded {
@apply rounded-full;
}

View File

@@ -1,6 +1,6 @@
import AudioPlayer from '@/app/components/base/audio-btn/audio'
declare global {
// eslint-disable-next-line ts/consistent-type-definitions
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
interface AudioPlayerManager {
instance: AudioPlayerManager
}
@@ -12,7 +12,6 @@ export class AudioPlayerManager {
private audioPlayers: AudioPlayer | null = null
private msgId: string | undefined
// eslint-disable-next-line
private constructor() {
}

View File

@@ -2,7 +2,7 @@ import Toast from '@/app/components/base/toast'
import { textToAudioStream } from '@/service/share'
declare global {
// eslint-disable-next-line ts/consistent-type-definitions
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
interface Window {
ManagedMediaSource: any
}

View File

@@ -7,7 +7,7 @@ import Tooltip from '@/app/components/base/tooltip'
import Loading from '@/app/components/base/loading'
import { AudioPlayerManager } from '@/app/components/base/audio-btn/audio.player.manager'
interface AudioBtnProps {
type AudioBtnProps = {
id?: string
voice?: string
value?: string

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