Compare commits
119 Commits
dev/plugin
...
build/swit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f93e28d75 | ||
|
|
6ffedcd8f0 | ||
|
|
26358e971d | ||
|
|
effd5449bc | ||
|
|
a608e23613 | ||
|
|
be122ac974 | ||
|
|
1fe585fcdd | ||
|
|
2d034c57da | ||
|
|
2c102c9b34 | ||
|
|
4f3113febb | ||
|
|
dfd85c6d11 | ||
|
|
3f330c0b80 | ||
|
|
c379448673 | ||
|
|
05ed049cc8 | ||
|
|
37d47983a1 | ||
|
|
bdb81fe20d | ||
|
|
0f60fe7f2a | ||
|
|
425f624de5 | ||
|
|
b1919745e2 | ||
|
|
9a242bcac9 | ||
|
|
a6109a60b8 | ||
|
|
28f7bbf83a | ||
|
|
cac04c5f3c | ||
|
|
18f5f9cc37 | ||
|
|
1787c5c93f | ||
|
|
f981494613 | ||
|
|
fbc853af92 | ||
|
|
1a64c660ba | ||
|
|
846555af1b | ||
|
|
bca94854f7 | ||
|
|
1bd70bd8bf | ||
|
|
d1dcd39191 | ||
|
|
35384bda41 | ||
|
|
89fb6eb648 | ||
|
|
aa61a890b2 | ||
|
|
31ece363c3 | ||
|
|
70a5d78cc5 | ||
|
|
57f4dfdb6f | ||
|
|
aa9028a607 | ||
|
|
d83f94c55c | ||
|
|
a8c5e0b0b0 | ||
|
|
177e8cbf73 | ||
|
|
23828fd15a | ||
|
|
2cc37ac8e5 | ||
|
|
c9ee1e9ff2 | ||
|
|
4f10f5d5f4 | ||
|
|
c48c84674e | ||
|
|
1e9fbbf41b | ||
|
|
4dd144ce43 | ||
|
|
a387ff1c38 | ||
|
|
a9e367e6de | ||
|
|
e2fec587f8 | ||
|
|
39a6f0943d | ||
|
|
684896d100 | ||
|
|
54f911f6cd | ||
|
|
0e5c16d0c2 | ||
|
|
b8cd6ea478 | ||
|
|
fc61fd0f50 | ||
|
|
2fbfc988c4 | ||
|
|
99f5fea001 | ||
|
|
ecd2a1be9f | ||
|
|
49ee9ca5f1 | ||
|
|
6d0eef12b1 | ||
|
|
c1e0a939b0 | ||
|
|
060a894bd1 | ||
|
|
c75e02b5b2 | ||
|
|
fcf43ee845 | ||
|
|
466f61d044 | ||
|
|
27ae74af50 | ||
|
|
8dd941e3d2 | ||
|
|
dec4bf6b98 | ||
|
|
e2c33fc40f | ||
|
|
c74e59d1f4 | ||
|
|
1fcb902715 | ||
|
|
c08f98218c | ||
|
|
c6377f6e38 | ||
|
|
3cb0a5bd68 | ||
|
|
95777d23e0 | ||
|
|
e7dc16fd08 | ||
|
|
495dec143c | ||
|
|
4cc6dfa232 | ||
|
|
d1452d4af4 | ||
|
|
d68ca56b3a | ||
|
|
49856d8d17 | ||
|
|
902de72cc0 | ||
|
|
76f6b8d104 | ||
|
|
f111e605c4 | ||
|
|
b358ed3a5b | ||
|
|
88dbf639e0 | ||
|
|
aa8b525b48 | ||
|
|
c990bc61db | ||
|
|
6d7588f236 | ||
|
|
8257c7bf02 | ||
|
|
946068967b | ||
|
|
19f5684960 | ||
|
|
6d62840aff | ||
|
|
ab868ac979 | ||
|
|
1d74e693ea | ||
|
|
fa43d4202f | ||
|
|
6b29860788 | ||
|
|
36800eeaba | ||
|
|
67acd174ac | ||
|
|
b5edc64b2a | ||
|
|
d00b2724cc | ||
|
|
43f87c0b86 | ||
|
|
7a43f48c95 | ||
|
|
58a913b09d | ||
|
|
cd03795f2c | ||
|
|
36f8b5711d | ||
|
|
f9c48e9ea9 | ||
|
|
3b48f8c98e | ||
|
|
cef1010cb5 | ||
|
|
cb4875a3a7 | ||
|
|
bbca708832 | ||
|
|
05aec43ee3 | ||
|
|
e8127756e0 | ||
|
|
792595a46f | ||
|
|
d7d7281c93 | ||
|
|
21193c2fbf |
@@ -1,11 +1,12 @@
|
||||
#!/bin/bash
|
||||
|
||||
cd web && npm install
|
||||
npm add -g pnpm@9.12.2
|
||||
cd web && pnpm 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 && npm run dev"' >> ~/.bashrc
|
||||
echo 'alias start-web="cd /workspaces/dify/web && pnpm dev"' >> ~/.bashrc
|
||||
echo 'alias start-containers="cd /workspaces/dify/docker && docker-compose -f docker-compose.middleware.yaml -p dify up -d"' >> ~/.bashrc
|
||||
|
||||
source /home/vscode/.bashrc
|
||||
source /home/vscode/.bashrc
|
||||
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
|
||||
- name: Run npm script
|
||||
if: env.FILES_CHANGED == 'true'
|
||||
run: npm run auto-gen-i18n
|
||||
run: pnpm run auto-gen-i18n
|
||||
|
||||
- name: Create Pull Request
|
||||
if: env.FILES_CHANGED == 'true'
|
||||
|
||||
7
.gitignore
vendored
@@ -175,6 +175,8 @@ docker/volumes/pgvector/data/*
|
||||
docker/volumes/pgvecto_rs/data/*
|
||||
|
||||
docker/nginx/conf.d/default.conf
|
||||
docker/nginx/ssl/*
|
||||
!docker/nginx/ssl/.gitkeep
|
||||
docker/middleware.env
|
||||
|
||||
sdks/python-client/build
|
||||
@@ -187,4 +189,7 @@ pyrightconfig.json
|
||||
api/.vscode
|
||||
|
||||
.idea/
|
||||
.vscode
|
||||
.vscode
|
||||
|
||||
# pnpm
|
||||
/.pnpm-store
|
||||
|
||||
5
LICENSE
@@ -6,8 +6,9 @@ Dify is licensed under the Apache License 2.0, with the following additional con
|
||||
|
||||
a. Multi-tenant service: Unless explicitly authorized by Dify in writing, you may not use the Dify source code to operate a multi-tenant environment.
|
||||
- Tenant Definition: Within the context of Dify, one tenant corresponds to one workspace. The workspace provides a separated area for each tenant's data and configurations.
|
||||
|
||||
b. LOGO and copyright information: In the process of using Dify's frontend components, you may not remove or modify the LOGO or copyright information in the Dify console or applications. This restriction is inapplicable to uses of Dify that do not involve its frontend components.
|
||||
|
||||
b. LOGO and copyright information: In the process of using Dify's frontend, you may not remove or modify the LOGO or copyright information in the Dify console or applications. This restriction is inapplicable to uses of Dify that do not involve its frontend.
|
||||
- Frontend Definition: For the purposes of this license, the "frontend" of Dify includes all components located in the `web/` directory when running Dify from the raw source code, or the "web" image when running Dify with Docker.
|
||||
|
||||
Please contact business@dify.ai by email to inquire about licensing matters.
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ DB_DATABASE=dify
|
||||
|
||||
# Storage configuration
|
||||
# use for store upload files, private keys...
|
||||
# storage type: local, s3, azure-blob, google-storage, tencent-cos, huawei-obs, volcengine-tos, baidu-obs, supabase
|
||||
# storage type: local, s3, aliyun-oss, azure-blob, baidu-obs, google-storage, huawei-obs, oci-storage, tencent-cos, volcengine-tos, supabase
|
||||
STORAGE_TYPE=local
|
||||
STORAGE_LOCAL_PATH=storage
|
||||
S3_USE_AWS_MANAGED_IAM=false
|
||||
|
||||
@@ -20,6 +20,7 @@ from app_factory import create_app
|
||||
|
||||
# DO NOT REMOVE BELOW
|
||||
from events import event_handlers # noqa: F401
|
||||
from extensions.ext_database import db
|
||||
|
||||
# TODO: Find a way to avoid importing models here
|
||||
from models import account, dataset, model, source, task, tool, tools, web # noqa: F401
|
||||
|
||||
@@ -35,7 +35,8 @@ from configs.middleware.vdb.weaviate_config import WeaviateConfig
|
||||
class StorageConfig(BaseSettings):
|
||||
STORAGE_TYPE: str = Field(
|
||||
description="Type of storage to use."
|
||||
" Options: 'local', 's3', 'azure-blob', 'aliyun-oss', 'google-storage'. Default is 'local'.",
|
||||
" Options: 'local', 's3', 'aliyun-oss', 'azure-blob', 'baidu-obs', 'google-storage', 'huawei-obs', "
|
||||
"'oci-storage', 'tencent-cos', 'volcengine-tos', 'supabase'. Default is 'local'.",
|
||||
default="local",
|
||||
)
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ help:
|
||||
en_US: https://console.groq.com/
|
||||
supported_model_types:
|
||||
- llm
|
||||
- speech2text
|
||||
configurate_methods:
|
||||
- predefined-model
|
||||
provider_credential_schema:
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
model: llama-3.2-11b-vision-preview
|
||||
label:
|
||||
zh_Hans: Llama 3.2 11B Vision (Preview)
|
||||
en_US: Llama 3.2 11B Vision (Preview)
|
||||
model_type: llm
|
||||
features:
|
||||
- agent-thought
|
||||
- vision
|
||||
model_properties:
|
||||
mode: chat
|
||||
context_size: 131072
|
||||
parameter_rules:
|
||||
- name: temperature
|
||||
use_template: temperature
|
||||
- name: top_p
|
||||
use_template: top_p
|
||||
- name: max_tokens
|
||||
use_template: max_tokens
|
||||
default: 512
|
||||
min: 1
|
||||
max: 8192
|
||||
pricing:
|
||||
input: '0.05'
|
||||
output: '0.1'
|
||||
unit: '0.000001'
|
||||
currency: USD
|
||||
@@ -0,0 +1,26 @@
|
||||
model: llama-3.2-90b-vision-preview
|
||||
label:
|
||||
zh_Hans: Llama 3.2 90B Vision (Preview)
|
||||
en_US: Llama 3.2 90B Vision (Preview)
|
||||
model_type: llm
|
||||
features:
|
||||
- agent-thought
|
||||
- vision
|
||||
model_properties:
|
||||
mode: chat
|
||||
context_size: 131072
|
||||
parameter_rules:
|
||||
- name: temperature
|
||||
use_template: temperature
|
||||
- name: top_p
|
||||
use_template: top_p
|
||||
- name: max_tokens
|
||||
use_template: max_tokens
|
||||
default: 512
|
||||
min: 1
|
||||
max: 8192
|
||||
pricing:
|
||||
input: '0.05'
|
||||
output: '0.1'
|
||||
unit: '0.000001'
|
||||
currency: USD
|
||||
@@ -0,0 +1,5 @@
|
||||
model: distil-whisper-large-v3-en
|
||||
model_type: speech2text
|
||||
model_properties:
|
||||
file_upload_limit: 1
|
||||
supported_file_extensions: flac,mp3,mp4,mpeg,mpga,m4a,ogg,wav,webm
|
||||
@@ -0,0 +1,30 @@
|
||||
from typing import IO, Optional
|
||||
|
||||
from core.model_runtime.model_providers.openai_api_compatible.speech2text.speech2text import OAICompatSpeech2TextModel
|
||||
|
||||
|
||||
class GroqSpeech2TextModel(OAICompatSpeech2TextModel):
|
||||
"""
|
||||
Model class for Groq Speech to text model.
|
||||
"""
|
||||
|
||||
def _invoke(self, model: str, credentials: dict, file: IO[bytes], user: Optional[str] = None) -> str:
|
||||
"""
|
||||
Invoke speech2text model
|
||||
|
||||
:param model: model name
|
||||
:param credentials: model credentials
|
||||
:param file: audio file
|
||||
:param user: unique user id
|
||||
:return: text for given audio file
|
||||
"""
|
||||
self._add_custom_parameters(credentials)
|
||||
return super()._invoke(model, credentials, file)
|
||||
|
||||
def validate_credentials(self, model: str, credentials: dict) -> None:
|
||||
self._add_custom_parameters(credentials)
|
||||
return super().validate_credentials(model, credentials)
|
||||
|
||||
@classmethod
|
||||
def _add_custom_parameters(cls, credentials: dict) -> None:
|
||||
credentials["endpoint_url"] = "https://api.groq.com/openai/v1"
|
||||
@@ -0,0 +1,5 @@
|
||||
model: whisper-large-v3-turbo
|
||||
model_type: speech2text
|
||||
model_properties:
|
||||
file_upload_limit: 1
|
||||
supported_file_extensions: flac,mp3,mp4,mpeg,mpga,m4a,ogg,wav,webm
|
||||
@@ -0,0 +1,5 @@
|
||||
model: whisper-large-v3
|
||||
model_type: speech2text
|
||||
model_properties:
|
||||
file_upload_limit: 1
|
||||
supported_file_extensions: flac,mp3,mp4,mpeg,mpga,m4a,ogg,wav,webm
|
||||
@@ -8,6 +8,7 @@ supported_model_types:
|
||||
- llm
|
||||
- text-embedding
|
||||
- speech2text
|
||||
- rerank
|
||||
configurate_methods:
|
||||
- customizable-model
|
||||
model_credential_schema:
|
||||
@@ -83,6 +84,19 @@ model_credential_schema:
|
||||
placeholder:
|
||||
zh_Hans: 在此输入您的模型上下文长度
|
||||
en_US: Enter your Model context size
|
||||
- variable: context_size
|
||||
label:
|
||||
zh_Hans: 模型上下文长度
|
||||
en_US: Model context size
|
||||
required: true
|
||||
show_on:
|
||||
- variable: __model_type
|
||||
value: rerank
|
||||
type: text-input
|
||||
default: '4096'
|
||||
placeholder:
|
||||
zh_Hans: 在此输入您的模型上下文长度
|
||||
en_US: Enter your Model context size
|
||||
- variable: max_tokens_to_sample
|
||||
label:
|
||||
zh_Hans: 最大 token 上限
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
from json import dumps
|
||||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
from requests import post
|
||||
from yarl import URL
|
||||
|
||||
from core.model_runtime.entities.common_entities import I18nObject
|
||||
from core.model_runtime.entities.model_entities import AIModelEntity, FetchFrom, ModelType
|
||||
from core.model_runtime.entities.rerank_entities import RerankDocument, RerankResult
|
||||
from core.model_runtime.errors.invoke import (
|
||||
InvokeAuthorizationError,
|
||||
InvokeBadRequestError,
|
||||
InvokeConnectionError,
|
||||
InvokeError,
|
||||
InvokeRateLimitError,
|
||||
InvokeServerUnavailableError,
|
||||
)
|
||||
from core.model_runtime.errors.validate import CredentialsValidateFailedError
|
||||
from core.model_runtime.model_providers.__base.rerank_model import RerankModel
|
||||
|
||||
|
||||
class OAICompatRerankModel(RerankModel):
|
||||
"""
|
||||
rerank model API is compatible with Jina rerank model API. So copy the JinaRerankModel class code here.
|
||||
we need enhance for llama.cpp , which return raw score, not normalize score 0~1. It seems Dify need it
|
||||
"""
|
||||
|
||||
def _invoke(
|
||||
self,
|
||||
model: str,
|
||||
credentials: dict,
|
||||
query: str,
|
||||
docs: list[str],
|
||||
score_threshold: Optional[float] = None,
|
||||
top_n: Optional[int] = None,
|
||||
user: Optional[str] = None,
|
||||
) -> RerankResult:
|
||||
"""
|
||||
Invoke rerank model
|
||||
|
||||
:param model: model name
|
||||
:param credentials: model credentials
|
||||
:param query: search query
|
||||
:param docs: docs for reranking
|
||||
:param score_threshold: score threshold
|
||||
:param top_n: top n documents to return
|
||||
:param user: unique user id
|
||||
:return: rerank result
|
||||
"""
|
||||
if len(docs) == 0:
|
||||
return RerankResult(model=model, docs=[])
|
||||
|
||||
server_url = credentials["endpoint_url"]
|
||||
model_name = model
|
||||
|
||||
if not server_url:
|
||||
raise CredentialsValidateFailedError("server_url is required")
|
||||
if not model_name:
|
||||
raise CredentialsValidateFailedError("model_name is required")
|
||||
|
||||
url = server_url
|
||||
headers = {"Authorization": f"Bearer {credentials.get('api_key')}", "Content-Type": "application/json"}
|
||||
|
||||
# TODO: Do we need truncate docs to avoid llama.cpp return error?
|
||||
|
||||
data = {"model": model_name, "query": query, "documents": docs, "top_n": top_n}
|
||||
|
||||
try:
|
||||
response = post(str(URL(url) / "rerank"), headers=headers, data=dumps(data), timeout=60)
|
||||
response.raise_for_status()
|
||||
results = response.json()
|
||||
|
||||
rerank_documents = []
|
||||
scores = [result["relevance_score"] for result in results["results"]]
|
||||
|
||||
# Min-Max Normalization: Normalize scores to 0 ~ 1.0 range
|
||||
min_score = min(scores)
|
||||
max_score = max(scores)
|
||||
score_range = max_score - min_score if max_score != min_score else 1.0 # Avoid division by zero
|
||||
|
||||
for result in results["results"]:
|
||||
index = result["index"]
|
||||
|
||||
# Retrieve document text (fallback if llama.cpp rerank doesn't return it)
|
||||
text = result.get("document", {}).get("text", docs[index])
|
||||
|
||||
# Normalize the score
|
||||
normalized_score = (result["relevance_score"] - min_score) / score_range
|
||||
|
||||
# Create RerankDocument object with normalized score
|
||||
rerank_document = RerankDocument(
|
||||
index=index,
|
||||
text=text,
|
||||
score=normalized_score,
|
||||
)
|
||||
|
||||
# Apply threshold (if defined)
|
||||
if score_threshold is None or normalized_score >= score_threshold:
|
||||
rerank_documents.append(rerank_document)
|
||||
|
||||
# Sort rerank_documents by normalized score in descending order
|
||||
rerank_documents.sort(key=lambda doc: doc.score, reverse=True)
|
||||
|
||||
return RerankResult(model=model, docs=rerank_documents)
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
raise InvokeServerUnavailableError(str(e))
|
||||
|
||||
def validate_credentials(self, model: str, credentials: dict) -> None:
|
||||
"""
|
||||
Validate model credentials
|
||||
|
||||
:param model: model name
|
||||
:param credentials: model credentials
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
self._invoke(
|
||||
model=model,
|
||||
credentials=credentials,
|
||||
query="What is the capital of the United States?",
|
||||
docs=[
|
||||
"Carson City is the capital city of the American state of Nevada. At the 2010 United States "
|
||||
"Census, Carson City had a population of 55,274.",
|
||||
"The Commonwealth of the Northern Mariana Islands is a group of islands in the Pacific Ocean that "
|
||||
"are a political division controlled by the United States. Its capital is Saipan.",
|
||||
],
|
||||
score_threshold=0.8,
|
||||
)
|
||||
except Exception as ex:
|
||||
raise CredentialsValidateFailedError(str(ex))
|
||||
|
||||
@property
|
||||
def _invoke_error_mapping(self) -> dict[type[InvokeError], list[type[Exception]]]:
|
||||
"""
|
||||
Map model invoke error to unified error
|
||||
"""
|
||||
return {
|
||||
InvokeConnectionError: [httpx.ConnectError],
|
||||
InvokeServerUnavailableError: [httpx.RemoteProtocolError],
|
||||
InvokeRateLimitError: [],
|
||||
InvokeAuthorizationError: [httpx.HTTPStatusError],
|
||||
InvokeBadRequestError: [httpx.RequestError],
|
||||
}
|
||||
|
||||
def get_customizable_model_schema(self, model: str, credentials: dict) -> AIModelEntity:
|
||||
"""
|
||||
generate custom model entities from credentials
|
||||
"""
|
||||
entity = AIModelEntity(
|
||||
model=model,
|
||||
label=I18nObject(en_US=model),
|
||||
model_type=ModelType.RERANK,
|
||||
fetch_from=FetchFrom.CUSTOMIZABLE_MODEL,
|
||||
model_properties={},
|
||||
)
|
||||
|
||||
return entity
|
||||
@@ -2,20 +2,15 @@ from typing import Optional
|
||||
|
||||
import httpx
|
||||
|
||||
from core.model_runtime.entities.common_entities import I18nObject
|
||||
from core.model_runtime.entities.model_entities import AIModelEntity, FetchFrom, ModelPropertyKey, ModelType
|
||||
from core.model_runtime.entities.rerank_entities import RerankDocument, RerankResult
|
||||
from core.model_runtime.errors.invoke import (
|
||||
InvokeAuthorizationError,
|
||||
InvokeBadRequestError,
|
||||
InvokeConnectionError,
|
||||
InvokeError,
|
||||
InvokeRateLimitError,
|
||||
InvokeServerUnavailableError,
|
||||
)
|
||||
from core.model_runtime.errors.invoke import InvokeError
|
||||
from core.model_runtime.errors.validate import CredentialsValidateFailedError
|
||||
from core.model_runtime.model_providers.__base.rerank_model import RerankModel
|
||||
from core.model_runtime.model_providers.wenxin._common import _CommonWenxin
|
||||
from core.model_runtime.model_providers.wenxin.wenxin_errors import (
|
||||
InternalServerError,
|
||||
invoke_error_mapping,
|
||||
)
|
||||
|
||||
|
||||
class WenxinRerank(_CommonWenxin):
|
||||
@@ -32,7 +27,7 @@ class WenxinRerank(_CommonWenxin):
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except httpx.HTTPStatusError as e:
|
||||
raise InvokeServerUnavailableError(str(e))
|
||||
raise InternalServerError(str(e))
|
||||
|
||||
|
||||
class WenxinRerankModel(RerankModel):
|
||||
@@ -93,7 +88,7 @@ class WenxinRerankModel(RerankModel):
|
||||
|
||||
return RerankResult(model=model, docs=rerank_documents)
|
||||
except httpx.HTTPStatusError as e:
|
||||
raise InvokeServerUnavailableError(str(e))
|
||||
raise InternalServerError(str(e))
|
||||
|
||||
def validate_credentials(self, model: str, credentials: dict) -> None:
|
||||
"""
|
||||
@@ -124,24 +119,4 @@ class WenxinRerankModel(RerankModel):
|
||||
"""
|
||||
Map model invoke error to unified error
|
||||
"""
|
||||
return {
|
||||
InvokeConnectionError: [httpx.ConnectError],
|
||||
InvokeServerUnavailableError: [httpx.RemoteProtocolError],
|
||||
InvokeRateLimitError: [],
|
||||
InvokeAuthorizationError: [httpx.HTTPStatusError],
|
||||
InvokeBadRequestError: [httpx.RequestError],
|
||||
}
|
||||
|
||||
def get_customizable_model_schema(self, model: str, credentials: dict) -> AIModelEntity:
|
||||
"""
|
||||
generate custom model entities from credentials
|
||||
"""
|
||||
entity = AIModelEntity(
|
||||
model=model,
|
||||
label=I18nObject(en_US=model),
|
||||
model_type=ModelType.RERANK,
|
||||
fetch_from=FetchFrom.CUSTOMIZABLE_MODEL,
|
||||
model_properties={ModelPropertyKey.CONTEXT_SIZE: int(credentials.get("context_size"))},
|
||||
)
|
||||
|
||||
return entity
|
||||
return invoke_error_mapping()
|
||||
|
||||
@@ -4,12 +4,22 @@ from urllib.parse import urlparse
|
||||
|
||||
import tiktoken
|
||||
|
||||
from core.model_runtime.entities.llm_entities import LLMResult
|
||||
from core.model_runtime.entities.common_entities import I18nObject
|
||||
from core.model_runtime.entities.llm_entities import LLMMode, LLMResult
|
||||
from core.model_runtime.entities.message_entities import (
|
||||
PromptMessage,
|
||||
PromptMessageTool,
|
||||
SystemPromptMessage,
|
||||
)
|
||||
from core.model_runtime.entities.model_entities import (
|
||||
AIModelEntity,
|
||||
FetchFrom,
|
||||
ModelFeature,
|
||||
ModelPropertyKey,
|
||||
ModelType,
|
||||
ParameterRule,
|
||||
ParameterType,
|
||||
)
|
||||
from core.model_runtime.model_providers.openai.llm.llm import OpenAILargeLanguageModel
|
||||
|
||||
|
||||
@@ -125,3 +135,58 @@ class YiLargeLanguageModel(OpenAILargeLanguageModel):
|
||||
else:
|
||||
parsed_url = urlparse(credentials["endpoint_url"])
|
||||
credentials["openai_api_base"] = f"{parsed_url.scheme}://{parsed_url.netloc}"
|
||||
|
||||
def get_customizable_model_schema(self, model: str, credentials: dict) -> AIModelEntity | None:
|
||||
return AIModelEntity(
|
||||
model=model,
|
||||
label=I18nObject(en_US=model, zh_Hans=model),
|
||||
model_type=ModelType.LLM,
|
||||
features=[ModelFeature.TOOL_CALL, ModelFeature.MULTI_TOOL_CALL, ModelFeature.STREAM_TOOL_CALL]
|
||||
if credentials.get("function_calling_type") == "tool_call"
|
||||
else [],
|
||||
fetch_from=FetchFrom.CUSTOMIZABLE_MODEL,
|
||||
model_properties={
|
||||
ModelPropertyKey.CONTEXT_SIZE: int(credentials.get("context_size", 8000)),
|
||||
ModelPropertyKey.MODE: LLMMode.CHAT.value,
|
||||
},
|
||||
parameter_rules=[
|
||||
ParameterRule(
|
||||
name="temperature",
|
||||
use_template="temperature",
|
||||
label=I18nObject(en_US="Temperature", zh_Hans="温度"),
|
||||
type=ParameterType.FLOAT,
|
||||
),
|
||||
ParameterRule(
|
||||
name="max_tokens",
|
||||
use_template="max_tokens",
|
||||
default=512,
|
||||
min=1,
|
||||
max=int(credentials.get("max_tokens", 8192)),
|
||||
label=I18nObject(
|
||||
en_US="Max Tokens", zh_Hans="指定生成结果长度的上限。如果生成结果截断,可以调大该参数"
|
||||
),
|
||||
type=ParameterType.INT,
|
||||
),
|
||||
ParameterRule(
|
||||
name="top_p",
|
||||
use_template="top_p",
|
||||
label=I18nObject(
|
||||
en_US="Top P",
|
||||
zh_Hans="控制生成结果的随机性。数值越小,随机性越弱;数值越大,随机性越强。",
|
||||
),
|
||||
type=ParameterType.FLOAT,
|
||||
),
|
||||
ParameterRule(
|
||||
name="top_k",
|
||||
use_template="top_k",
|
||||
label=I18nObject(en_US="Top K", zh_Hans="取样数量"),
|
||||
type=ParameterType.FLOAT,
|
||||
),
|
||||
ParameterRule(
|
||||
name="frequency_penalty",
|
||||
use_template="frequency_penalty",
|
||||
label=I18nObject(en_US="Frequency Penalty", zh_Hans="重复惩罚"),
|
||||
type=ParameterType.FLOAT,
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -20,6 +20,7 @@ supported_model_types:
|
||||
- llm
|
||||
configurate_methods:
|
||||
- predefined-model
|
||||
- customizable-model
|
||||
provider_credential_schema:
|
||||
credential_form_schemas:
|
||||
- variable: api_key
|
||||
@@ -39,3 +40,57 @@ provider_credential_schema:
|
||||
placeholder:
|
||||
zh_Hans: Base URL, e.g. https://api.lingyiwanwu.com/v1
|
||||
en_US: Base URL, e.g. https://api.lingyiwanwu.com/v1
|
||||
model_credential_schema:
|
||||
model:
|
||||
label:
|
||||
en_US: Model Name
|
||||
zh_Hans: 模型名称
|
||||
placeholder:
|
||||
en_US: Enter your model name
|
||||
zh_Hans: 输入模型名称
|
||||
credential_form_schemas:
|
||||
- variable: api_key
|
||||
label:
|
||||
en_US: API Key
|
||||
type: secret-input
|
||||
required: true
|
||||
placeholder:
|
||||
zh_Hans: 在此输入您的 API Key
|
||||
en_US: Enter your API Key
|
||||
- variable: context_size
|
||||
label:
|
||||
zh_Hans: 模型上下文长度
|
||||
en_US: Model context size
|
||||
required: true
|
||||
type: text-input
|
||||
default: '4096'
|
||||
placeholder:
|
||||
zh_Hans: 在此输入您的模型上下文长度
|
||||
en_US: Enter your Model context size
|
||||
- variable: max_tokens
|
||||
label:
|
||||
zh_Hans: 最大 token 上限
|
||||
en_US: Upper bound for max tokens
|
||||
default: '4096'
|
||||
type: text-input
|
||||
show_on:
|
||||
- variable: __model_type
|
||||
value: llm
|
||||
- variable: function_calling_type
|
||||
label:
|
||||
en_US: Function calling
|
||||
type: select
|
||||
required: false
|
||||
default: no_call
|
||||
options:
|
||||
- value: no_call
|
||||
label:
|
||||
en_US: Not Support
|
||||
zh_Hans: 不支持
|
||||
- value: function_call
|
||||
label:
|
||||
en_US: Support
|
||||
zh_Hans: 支持
|
||||
show_on:
|
||||
- variable: __model_type
|
||||
value: llm
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
- vectorizer
|
||||
- qrcode
|
||||
- tianditu
|
||||
- aliyuque
|
||||
- google_translate
|
||||
- hap
|
||||
- json_process
|
||||
|
||||
32
api/core/tools/provider/builtin/aliyuque/_assets/icon.svg
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
19
api/core/tools/provider/builtin/aliyuque/aliyuque.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from core.tools.errors import ToolProviderCredentialValidationError
|
||||
from core.tools.provider.builtin.aliyuque.tools.base import AliYuqueTool
|
||||
from core.tools.provider.builtin_tool_provider import BuiltinToolProviderController
|
||||
|
||||
|
||||
class AliYuqueProvider(BuiltinToolProviderController):
|
||||
def _validate_credentials(self, credentials: dict) -> None:
|
||||
token = credentials.get("token")
|
||||
if not token:
|
||||
raise ToolProviderCredentialValidationError("token is required")
|
||||
|
||||
try:
|
||||
resp = AliYuqueTool.auth(token)
|
||||
if resp and resp.get("data", {}).get("id"):
|
||||
return
|
||||
|
||||
raise ToolProviderCredentialValidationError(resp)
|
||||
except Exception as e:
|
||||
raise ToolProviderCredentialValidationError(str(e))
|
||||
29
api/core/tools/provider/builtin/aliyuque/aliyuque.yaml
Normal file
@@ -0,0 +1,29 @@
|
||||
identity:
|
||||
author: 佐井
|
||||
name: aliyuque
|
||||
label:
|
||||
en_US: yuque
|
||||
zh_Hans: 语雀
|
||||
pt_BR: yuque
|
||||
description:
|
||||
en_US: Yuque, https://www.yuque.com.
|
||||
zh_Hans: 语雀,https://www.yuque.com。
|
||||
pt_BR: Yuque, https://www.yuque.com.
|
||||
icon: icon.svg
|
||||
tags:
|
||||
- productivity
|
||||
- search
|
||||
credentials_for_provider:
|
||||
token:
|
||||
type: secret-input
|
||||
required: true
|
||||
label:
|
||||
en_US: Yuque Team Token
|
||||
zh_Hans: 语雀团队Token
|
||||
placeholder:
|
||||
en_US: Please input your Yuque team token
|
||||
zh_Hans: 请输入你的语雀团队Token
|
||||
help:
|
||||
en_US: Get Alibaba Yuque team token
|
||||
zh_Hans: 先获取语雀团队Token
|
||||
url: https://www.yuque.com/settings/tokens
|
||||
50
api/core/tools/provider/builtin/aliyuque/tools/base.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""
|
||||
语雀客户端
|
||||
"""
|
||||
|
||||
__author__ = "佐井"
|
||||
__created__ = "2024-06-01 09:45:20"
|
||||
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
class AliYuqueTool:
|
||||
# yuque service url
|
||||
server_url = "https://www.yuque.com"
|
||||
|
||||
@staticmethod
|
||||
def auth(token):
|
||||
session = requests.Session()
|
||||
session.headers.update({"Accept": "application/json", "X-Auth-Token": token})
|
||||
login = session.request("GET", AliYuqueTool.server_url + "/api/v2/user")
|
||||
login.raise_for_status()
|
||||
resp = login.json()
|
||||
return resp
|
||||
|
||||
def request(self, method: str, token, tool_parameters: dict[str, Any], path: str) -> str:
|
||||
if not token:
|
||||
raise Exception("token is required")
|
||||
session = requests.Session()
|
||||
session.headers.update({"accept": "application/json", "X-Auth-Token": token})
|
||||
new_params = {**tool_parameters}
|
||||
# 找出需要替换的变量
|
||||
replacements = {k: v for k, v in new_params.items() if f"{{{k}}}" in path}
|
||||
|
||||
# 替换 path 中的变量
|
||||
for key, value in replacements.items():
|
||||
path = path.replace(f"{{{key}}}", str(value))
|
||||
del new_params[key] # 从 kwargs 中删除已经替换的变量
|
||||
# 请求接口
|
||||
if method.upper() in {"POST", "PUT"}:
|
||||
session.headers.update(
|
||||
{
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
)
|
||||
response = session.request(method.upper(), self.server_url + path, json=new_params)
|
||||
else:
|
||||
response = session.request(method, self.server_url + path, params=new_params)
|
||||
response.raise_for_status()
|
||||
return response.text
|
||||
@@ -0,0 +1,22 @@
|
||||
"""
|
||||
创建文档
|
||||
"""
|
||||
|
||||
__author__ = "佐井"
|
||||
__created__ = "2024-06-01 10:45:20"
|
||||
|
||||
from typing import Any, Union
|
||||
|
||||
from core.tools.entities.tool_entities import ToolInvokeMessage
|
||||
from core.tools.provider.builtin.aliyuque.tools.base import AliYuqueTool
|
||||
from core.tools.tool.builtin_tool import BuiltinTool
|
||||
|
||||
|
||||
class AliYuqueCreateDocumentTool(AliYuqueTool, BuiltinTool):
|
||||
def _invoke(
|
||||
self, user_id: str, tool_parameters: dict[str, Any]
|
||||
) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]:
|
||||
token = self.runtime.credentials.get("token", None)
|
||||
if not token:
|
||||
raise Exception("token is required")
|
||||
return self.create_text_message(self.request("POST", token, tool_parameters, "/api/v2/repos/{book_id}/docs"))
|
||||
@@ -0,0 +1,99 @@
|
||||
identity:
|
||||
name: aliyuque_create_document
|
||||
author: 佐井
|
||||
label:
|
||||
en_US: Create Document
|
||||
zh_Hans: 创建文档
|
||||
icon: icon.svg
|
||||
description:
|
||||
human:
|
||||
en_US: Creates a new document within a knowledge base without automatic addition to the table of contents. Requires a subsequent call to the "knowledge base directory update API". Supports setting visibility, format, and content. # 接口英文描述
|
||||
zh_Hans: 在知识库中创建新文档,但不会自动加入目录,需额外调用“知识库目录更新接口”。允许设置公开性、格式及正文内容。
|
||||
llm: Creates docs in a KB.
|
||||
|
||||
parameters:
|
||||
- name: book_id
|
||||
type: number
|
||||
required: true
|
||||
form: llm
|
||||
label:
|
||||
en_US: Knowledge Base ID
|
||||
zh_Hans: 知识库ID
|
||||
human_description:
|
||||
en_US: The unique identifier of the knowledge base where the document will be created.
|
||||
zh_Hans: 文档将被创建的知识库的唯一标识。
|
||||
llm_description: ID of the target knowledge base.
|
||||
|
||||
- name: title
|
||||
type: string
|
||||
required: false
|
||||
form: llm
|
||||
label:
|
||||
en_US: Title
|
||||
zh_Hans: 标题
|
||||
human_description:
|
||||
en_US: The title of the document, defaults to 'Untitled' if not provided.
|
||||
zh_Hans: 文档标题,默认为'无标题'如未提供。
|
||||
llm_description: Title of the document, defaults to 'Untitled'.
|
||||
|
||||
- name: public
|
||||
type: select
|
||||
required: false
|
||||
form: llm
|
||||
options:
|
||||
- value: 0
|
||||
label:
|
||||
en_US: Private
|
||||
zh_Hans: 私密
|
||||
- value: 1
|
||||
label:
|
||||
en_US: Public
|
||||
zh_Hans: 公开
|
||||
- value: 2
|
||||
label:
|
||||
en_US: Enterprise-only
|
||||
zh_Hans: 企业内公开
|
||||
label:
|
||||
en_US: Visibility
|
||||
zh_Hans: 公开性
|
||||
human_description:
|
||||
en_US: Document visibility (0 Private, 1 Public, 2 Enterprise-only).
|
||||
zh_Hans: 文档可见性(0 私密, 1 公开, 2 企业内公开)。
|
||||
llm_description: Doc visibility options, 0-private, 1-public, 2-enterprise.
|
||||
|
||||
- name: format
|
||||
type: select
|
||||
required: false
|
||||
form: llm
|
||||
options:
|
||||
- value: markdown
|
||||
label:
|
||||
en_US: markdown
|
||||
zh_Hans: markdown
|
||||
- value: html
|
||||
label:
|
||||
en_US: html
|
||||
zh_Hans: html
|
||||
- value: lake
|
||||
label:
|
||||
en_US: lake
|
||||
zh_Hans: lake
|
||||
label:
|
||||
en_US: Content Format
|
||||
zh_Hans: 内容格式
|
||||
human_description:
|
||||
en_US: Format of the document content (markdown, HTML, Lake).
|
||||
zh_Hans: 文档内容格式(markdown, HTML, Lake)。
|
||||
llm_description: Content format choices, markdown, HTML, Lake.
|
||||
|
||||
- name: body
|
||||
type: string
|
||||
required: true
|
||||
form: llm
|
||||
label:
|
||||
en_US: Body Content
|
||||
zh_Hans: 正文内容
|
||||
human_description:
|
||||
en_US: The actual content of the document.
|
||||
zh_Hans: 文档的实际内容。
|
||||
llm_description: Content of the document.
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
删除文档
|
||||
"""
|
||||
|
||||
__author__ = "佐井"
|
||||
__created__ = "2024-09-17 22:04"
|
||||
|
||||
from typing import Any, Union
|
||||
|
||||
from core.tools.entities.tool_entities import ToolInvokeMessage
|
||||
from core.tools.provider.builtin.aliyuque.tools.base import AliYuqueTool
|
||||
from core.tools.tool.builtin_tool import BuiltinTool
|
||||
|
||||
|
||||
class AliYuqueDeleteDocumentTool(AliYuqueTool, BuiltinTool):
|
||||
def _invoke(
|
||||
self, user_id: str, tool_parameters: dict[str, Any]
|
||||
) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]:
|
||||
token = self.runtime.credentials.get("token", None)
|
||||
if not token:
|
||||
raise Exception("token is required")
|
||||
return self.create_text_message(
|
||||
self.request("DELETE", token, tool_parameters, "/api/v2/repos/{book_id}/docs/{id}")
|
||||
)
|
||||
@@ -0,0 +1,37 @@
|
||||
identity:
|
||||
name: aliyuque_delete_document
|
||||
author: 佐井
|
||||
label:
|
||||
en_US: Delete Document
|
||||
zh_Hans: 删除文档
|
||||
icon: icon.svg
|
||||
description:
|
||||
human:
|
||||
en_US: Delete Document
|
||||
zh_Hans: 根据id删除文档
|
||||
llm: Delete document.
|
||||
|
||||
parameters:
|
||||
- name: book_id
|
||||
type: number
|
||||
required: true
|
||||
form: llm
|
||||
label:
|
||||
en_US: Knowledge Base ID
|
||||
zh_Hans: 知识库ID
|
||||
human_description:
|
||||
en_US: The unique identifier of the knowledge base where the document will be created.
|
||||
zh_Hans: 文档将被创建的知识库的唯一标识。
|
||||
llm_description: ID of the target knowledge base.
|
||||
|
||||
- name: id
|
||||
type: string
|
||||
required: true
|
||||
form: llm
|
||||
label:
|
||||
en_US: Document ID or Path
|
||||
zh_Hans: 文档 ID or 路径
|
||||
human_description:
|
||||
en_US: Document ID or path.
|
||||
zh_Hans: 文档 ID or 路径。
|
||||
llm_description: Document ID or path.
|
||||
@@ -0,0 +1,24 @@
|
||||
"""
|
||||
获取知识库首页
|
||||
"""
|
||||
|
||||
__author__ = "佐井"
|
||||
__created__ = "2024-06-01 22:57:14"
|
||||
|
||||
from typing import Any, Union
|
||||
|
||||
from core.tools.entities.tool_entities import ToolInvokeMessage
|
||||
from core.tools.provider.builtin.aliyuque.tools.base import AliYuqueTool
|
||||
from core.tools.tool.builtin_tool import BuiltinTool
|
||||
|
||||
|
||||
class AliYuqueDescribeBookIndexPageTool(AliYuqueTool, BuiltinTool):
|
||||
def _invoke(
|
||||
self, user_id: str, tool_parameters: dict[str, Any]
|
||||
) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]:
|
||||
token = self.runtime.credentials.get("token", None)
|
||||
if not token:
|
||||
raise Exception("token is required")
|
||||
return self.create_text_message(
|
||||
self.request("GET", token, tool_parameters, "/api/v2/repos/{group_login}/{book_slug}/index_page")
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
identity:
|
||||
name: aliyuque_describe_book_index_page
|
||||
author: 佐井
|
||||
label:
|
||||
en_US: Get Repo Index Page
|
||||
zh_Hans: 获取知识库首页
|
||||
icon: icon.svg
|
||||
|
||||
description:
|
||||
human:
|
||||
en_US: Retrieves the homepage of a knowledge base within a group, supporting both book ID and group login with book slug access.
|
||||
zh_Hans: 获取团队中知识库的首页信息,可通过书籍ID或团队登录名与书籍路径访问。
|
||||
llm: Fetches the knowledge base homepage using group and book identifiers with support for alternate access paths.
|
||||
|
||||
parameters:
|
||||
- name: group_login
|
||||
type: string
|
||||
required: true
|
||||
form: llm
|
||||
label:
|
||||
en_US: Group Login
|
||||
zh_Hans: 团队登录名
|
||||
human_description:
|
||||
en_US: The login name of the group that owns the knowledge base.
|
||||
zh_Hans: 拥有该知识库的团队登录名。
|
||||
llm_description: Team login identifier for the knowledge base owner.
|
||||
|
||||
- name: book_slug
|
||||
type: string
|
||||
required: true
|
||||
form: llm
|
||||
label:
|
||||
en_US: Book Slug
|
||||
zh_Hans: 知识库路径
|
||||
human_description:
|
||||
en_US: The unique slug representing the path of the knowledge base.
|
||||
zh_Hans: 知识库的唯一路径标识。
|
||||
llm_description: Unique path identifier for the knowledge base.
|
||||
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
获取知识库目录
|
||||
"""
|
||||
|
||||
__author__ = "佐井"
|
||||
__created__ = "2024-09-17 15:17:11"
|
||||
|
||||
from typing import Any, Union
|
||||
|
||||
from core.tools.entities.tool_entities import ToolInvokeMessage
|
||||
from core.tools.provider.builtin.aliyuque.tools.base import AliYuqueTool
|
||||
from core.tools.tool.builtin_tool import BuiltinTool
|
||||
|
||||
|
||||
class YuqueDescribeBookTableOfContentsTool(AliYuqueTool, BuiltinTool):
|
||||
def _invoke(
|
||||
self, user_id: str, tool_parameters: dict[str, Any]
|
||||
) -> (Union)[ToolInvokeMessage, list[ToolInvokeMessage]]:
|
||||
token = self.runtime.credentials.get("token", None)
|
||||
if not token:
|
||||
raise Exception("token is required")
|
||||
return self.create_text_message(self.request("GET", token, tool_parameters, "/api/v2/repos/{book_id}/toc"))
|
||||
@@ -0,0 +1,25 @@
|
||||
identity:
|
||||
name: aliyuque_describe_book_table_of_contents
|
||||
author: 佐井
|
||||
label:
|
||||
en_US: Get Book's Table of Contents
|
||||
zh_Hans: 获取知识库的目录
|
||||
icon: icon.svg
|
||||
description:
|
||||
human:
|
||||
en_US: Get Book's Table of Contents.
|
||||
zh_Hans: 获取知识库的目录。
|
||||
llm: Get Book's Table of Contents.
|
||||
|
||||
parameters:
|
||||
- name: book_id
|
||||
type: number
|
||||
required: true
|
||||
form: llm
|
||||
label:
|
||||
en_US: Book ID
|
||||
zh_Hans: 知识库 ID
|
||||
human_description:
|
||||
en_US: Book ID.
|
||||
zh_Hans: 知识库 ID。
|
||||
llm_description: Book ID.
|
||||
@@ -0,0 +1,61 @@
|
||||
"""
|
||||
获取文档
|
||||
"""
|
||||
|
||||
__author__ = "佐井"
|
||||
__created__ = "2024-06-02 07:11:45"
|
||||
|
||||
import json
|
||||
from typing import Any, Union
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from core.tools.entities.tool_entities import ToolInvokeMessage
|
||||
from core.tools.provider.builtin.aliyuque.tools.base import AliYuqueTool
|
||||
from core.tools.tool.builtin_tool import BuiltinTool
|
||||
|
||||
|
||||
class AliYuqueDescribeDocumentContentTool(AliYuqueTool, BuiltinTool):
|
||||
def _invoke(
|
||||
self, user_id: str, tool_parameters: dict[str, Any]
|
||||
) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]:
|
||||
new_params = {**tool_parameters}
|
||||
token = new_params.pop("token")
|
||||
if not token or token.lower() == "none":
|
||||
token = self.runtime.credentials.get("token", None)
|
||||
if not token:
|
||||
raise Exception("token is required")
|
||||
new_params = {**tool_parameters}
|
||||
url = new_params.pop("url")
|
||||
if not url or not url.startswith("http"):
|
||||
raise Exception("url is not valid")
|
||||
|
||||
parsed_url = urlparse(url)
|
||||
path_parts = parsed_url.path.strip("/").split("/")
|
||||
if len(path_parts) < 3:
|
||||
raise Exception("url is not correct")
|
||||
doc_id = path_parts[-1]
|
||||
book_slug = path_parts[-2]
|
||||
group_id = path_parts[-3]
|
||||
|
||||
# 1. 请求首页信息,获取book_id
|
||||
new_params["group_login"] = group_id
|
||||
new_params["book_slug"] = book_slug
|
||||
index_page = json.loads(
|
||||
self.request("GET", token, new_params, "/api/v2/repos/{group_login}/{book_slug}/index_page")
|
||||
)
|
||||
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}")
|
||||
# 2. 获取文档内容
|
||||
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}")
|
||||
data = json.loads(data)
|
||||
body_only = tool_parameters.get("body_only") or ""
|
||||
if body_only.lower() == "true":
|
||||
return self.create_text_message(data.get("data").get("body"))
|
||||
else:
|
||||
raw = data.get("data")
|
||||
del raw["body_lake"]
|
||||
del raw["body_html"]
|
||||
return self.create_text_message(json.dumps(data))
|
||||
@@ -0,0 +1,50 @@
|
||||
identity:
|
||||
name: aliyuque_describe_document_content
|
||||
author: 佐井
|
||||
label:
|
||||
en_US: Fetch Document Content
|
||||
zh_Hans: 获取文档内容
|
||||
icon: icon.svg
|
||||
|
||||
description:
|
||||
human:
|
||||
en_US: Retrieves document content from Yuque based on the provided document URL, which can be a normal or shared link.
|
||||
zh_Hans: 根据提供的语雀文档地址(支持正常链接或分享链接)获取文档内容。
|
||||
llm: Fetches Yuque document content given a URL.
|
||||
|
||||
parameters:
|
||||
- name: url
|
||||
type: string
|
||||
required: true
|
||||
form: llm
|
||||
label:
|
||||
en_US: Document URL
|
||||
zh_Hans: 文档地址
|
||||
human_description:
|
||||
en_US: The URL of the document to retrieve content from, can be normal or shared.
|
||||
zh_Hans: 需要获取内容的文档地址,可以是正常链接或分享链接。
|
||||
llm_description: URL of the Yuque document to fetch content.
|
||||
|
||||
- name: body_only
|
||||
type: string
|
||||
required: false
|
||||
form: llm
|
||||
label:
|
||||
en_US: return body content only
|
||||
zh_Hans: 仅返回body内容
|
||||
human_description:
|
||||
en_US: true:Body content only, false:Full response with metadata.
|
||||
zh_Hans: true:仅返回body内容,不返回其他元数据,false:返回所有元数据。
|
||||
llm_description: true:Body content only, false:Full response with metadata.
|
||||
|
||||
- name: token
|
||||
type: secret-input
|
||||
required: false
|
||||
form: llm
|
||||
label:
|
||||
en_US: Yuque API Token
|
||||
zh_Hans: 语雀接口Token
|
||||
human_description:
|
||||
en_US: The token for calling the Yuque API defaults to the Yuque token bound to the current tool if not provided.
|
||||
zh_Hans: 调用语雀接口的token,如果不传则默认为当前工具绑定的语雀Token。
|
||||
llm_description: If the token for calling the Yuque API is not provided, it will default to the Yuque token bound to the current tool.
|
||||
@@ -0,0 +1,24 @@
|
||||
"""
|
||||
获取文档
|
||||
"""
|
||||
|
||||
__author__ = "佐井"
|
||||
__created__ = "2024-06-01 10:45:20"
|
||||
|
||||
from typing import Any, Union
|
||||
|
||||
from core.tools.entities.tool_entities import ToolInvokeMessage
|
||||
from core.tools.provider.builtin.aliyuque.tools.base import AliYuqueTool
|
||||
from core.tools.tool.builtin_tool import BuiltinTool
|
||||
|
||||
|
||||
class AliYuqueDescribeDocumentsTool(AliYuqueTool, BuiltinTool):
|
||||
def _invoke(
|
||||
self, user_id: str, tool_parameters: dict[str, Any]
|
||||
) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]:
|
||||
token = self.runtime.credentials.get("token", None)
|
||||
if not token:
|
||||
raise Exception("token is required")
|
||||
return self.create_text_message(
|
||||
self.request("GET", token, tool_parameters, "/api/v2/repos/{book_id}/docs/{id}")
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
identity:
|
||||
name: aliyuque_describe_documents
|
||||
author: 佐井
|
||||
label:
|
||||
en_US: Get Doc Detail
|
||||
zh_Hans: 获取文档详情
|
||||
icon: icon.svg
|
||||
|
||||
description:
|
||||
human:
|
||||
en_US: Retrieves detailed information of a specific document identified by its ID or path within a knowledge base.
|
||||
zh_Hans: 根据知识库ID和文档ID或路径获取文档详细信息。
|
||||
llm: Fetches detailed doc info using ID/path from a knowledge base; supports doc lookup in Yuque.
|
||||
|
||||
parameters:
|
||||
- name: book_id
|
||||
type: number
|
||||
required: true
|
||||
form: llm
|
||||
label:
|
||||
en_US: Knowledge Base ID
|
||||
zh_Hans: 知识库 ID
|
||||
human_description:
|
||||
en_US: Identifier for the knowledge base where the document resides.
|
||||
zh_Hans: 文档所属知识库的唯一标识。
|
||||
llm_description: ID of the knowledge base holding the document.
|
||||
|
||||
- name: id
|
||||
type: string
|
||||
required: true
|
||||
form: llm
|
||||
label:
|
||||
en_US: Document ID or Path
|
||||
zh_Hans: 文档 ID 或路径
|
||||
human_description:
|
||||
en_US: The unique identifier or path of the document to retrieve.
|
||||
zh_Hans: 需要获取的文档的ID或其在知识库中的路径。
|
||||
llm_description: Unique doc ID or its path for retrieval.
|
||||
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
获取知识库目录
|
||||
"""
|
||||
|
||||
__author__ = "佐井"
|
||||
__created__ = "2024-09-17 15:17:11"
|
||||
|
||||
from typing import Any, Union
|
||||
|
||||
from core.tools.entities.tool_entities import ToolInvokeMessage
|
||||
from core.tools.provider.builtin.aliyuque.tools.base import AliYuqueTool
|
||||
from core.tools.tool.builtin_tool import BuiltinTool
|
||||
|
||||
|
||||
class YuqueDescribeBookTableOfContentsTool(AliYuqueTool, BuiltinTool):
|
||||
def _invoke(
|
||||
self, user_id: str, tool_parameters: dict[str, Any]
|
||||
) -> (Union)[ToolInvokeMessage, list[ToolInvokeMessage]]:
|
||||
token = self.runtime.credentials.get("token", None)
|
||||
if not token:
|
||||
raise Exception("token is required")
|
||||
|
||||
doc_ids = tool_parameters.get("doc_ids")
|
||||
if doc_ids:
|
||||
doc_ids = [int(doc_id.strip()) for doc_id in doc_ids.split(",")]
|
||||
tool_parameters["doc_ids"] = doc_ids
|
||||
|
||||
return self.create_text_message(self.request("PUT", token, tool_parameters, "/api/v2/repos/{book_id}/toc"))
|
||||
@@ -0,0 +1,222 @@
|
||||
identity:
|
||||
name: aliyuque_update_book_table_of_contents
|
||||
author: 佐井
|
||||
label:
|
||||
en_US: Update Book's Table of Contents
|
||||
zh_Hans: 更新知识库目录
|
||||
icon: icon.svg
|
||||
description:
|
||||
human:
|
||||
en_US: Update Book's Table of Contents.
|
||||
zh_Hans: 更新知识库目录。
|
||||
llm: Update Book's Table of Contents.
|
||||
|
||||
parameters:
|
||||
- name: book_id
|
||||
type: number
|
||||
required: true
|
||||
form: llm
|
||||
label:
|
||||
en_US: Book ID
|
||||
zh_Hans: 知识库 ID
|
||||
human_description:
|
||||
en_US: Book ID.
|
||||
zh_Hans: 知识库 ID。
|
||||
llm_description: Book ID.
|
||||
|
||||
- name: action
|
||||
type: select
|
||||
required: true
|
||||
form: llm
|
||||
options:
|
||||
- value: appendNode
|
||||
label:
|
||||
en_US: appendNode
|
||||
zh_Hans: appendNode
|
||||
pt_BR: appendNode
|
||||
- value: prependNode
|
||||
label:
|
||||
en_US: prependNode
|
||||
zh_Hans: prependNode
|
||||
pt_BR: prependNode
|
||||
- value: editNode
|
||||
label:
|
||||
en_US: editNode
|
||||
zh_Hans: editNode
|
||||
pt_BR: editNode
|
||||
- value: editNode
|
||||
label:
|
||||
en_US: removeNode
|
||||
zh_Hans: removeNode
|
||||
pt_BR: removeNode
|
||||
label:
|
||||
en_US: Action Type
|
||||
zh_Hans: 操作
|
||||
human_description:
|
||||
en_US: In the operation scenario, sibling node prepending is not supported, deleting a node doesn't remove associated documents, and node deletion has two modes, 'sibling' (delete current node) and 'child' (delete current node and its children).
|
||||
zh_Hans: 操作,创建场景下不支持同级头插 prependNode,删除节点不会删除关联文档,删除节点时action_mode=sibling (删除当前节点), action_mode=child (删除当前节点及子节点)
|
||||
llm_description: In the operation scenario, sibling node prepending is not supported, deleting a node doesn't remove associated documents, and node deletion has two modes, 'sibling' (delete current node) and 'child' (delete current node and its children).
|
||||
|
||||
|
||||
- name: action_mode
|
||||
type: select
|
||||
required: false
|
||||
form: llm
|
||||
options:
|
||||
- value: sibling
|
||||
label:
|
||||
en_US: sibling
|
||||
zh_Hans: 同级
|
||||
pt_BR: sibling
|
||||
- value: child
|
||||
label:
|
||||
en_US: child
|
||||
zh_Hans: 子集
|
||||
pt_BR: child
|
||||
label:
|
||||
en_US: Action Type
|
||||
zh_Hans: 操作
|
||||
human_description:
|
||||
en_US: Operation mode (sibling:same level, child:child level).
|
||||
zh_Hans: 操作模式 (sibling:同级, child:子级)。
|
||||
llm_description: Operation mode (sibling:same level, child:child level).
|
||||
|
||||
- name: target_uuid
|
||||
type: string
|
||||
required: false
|
||||
form: llm
|
||||
label:
|
||||
en_US: Target node UUID
|
||||
zh_Hans: 目标节点 UUID
|
||||
human_description:
|
||||
en_US: Target node UUID, defaults to root node if left empty.
|
||||
zh_Hans: 目标节点 UUID, 不填默认为根节点。
|
||||
llm_description: Target node UUID, defaults to root node if left empty.
|
||||
|
||||
- name: node_uuid
|
||||
type: string
|
||||
required: false
|
||||
form: llm
|
||||
label:
|
||||
en_US: Node UUID
|
||||
zh_Hans: 操作节点 UUID
|
||||
human_description:
|
||||
en_US: Operation node UUID [required for move/update/delete].
|
||||
zh_Hans: 操作节点 UUID [移动/更新/删除必填]。
|
||||
llm_description: Operation node UUID [required for move/update/delete].
|
||||
|
||||
- name: doc_ids
|
||||
type: string
|
||||
required: false
|
||||
form: llm
|
||||
label:
|
||||
en_US: Document IDs
|
||||
zh_Hans: 文档id列表
|
||||
human_description:
|
||||
en_US: Document IDs [required for creating documents], separate multiple IDs with ','.
|
||||
zh_Hans: 文档 IDs [创建文档必填],多个用','分隔。
|
||||
llm_description: Document IDs [required for creating documents], separate multiple IDs with ','.
|
||||
|
||||
|
||||
- name: type
|
||||
type: select
|
||||
required: false
|
||||
form: llm
|
||||
default: DOC
|
||||
options:
|
||||
- value: DOC
|
||||
label:
|
||||
en_US: DOC
|
||||
zh_Hans: 文档
|
||||
pt_BR: DOC
|
||||
- value: LINK
|
||||
label:
|
||||
en_US: LINK
|
||||
zh_Hans: 链接
|
||||
pt_BR: LINK
|
||||
- value: TITLE
|
||||
label:
|
||||
en_US: TITLE
|
||||
zh_Hans: 分组
|
||||
pt_BR: TITLE
|
||||
label:
|
||||
en_US: Node type
|
||||
zh_Hans: 操节点类型
|
||||
human_description:
|
||||
en_US: Node type [required for creation] (DOC:document, LINK:external link, TITLE:group).
|
||||
zh_Hans: 操节点类型 [创建必填] (DOC:文档, LINK:外链, TITLE:分组)。
|
||||
llm_description: Node type [required for creation] (DOC:document, LINK:external link, TITLE:group).
|
||||
|
||||
- name: title
|
||||
type: string
|
||||
required: false
|
||||
form: llm
|
||||
label:
|
||||
en_US: Node Name
|
||||
zh_Hans: 节点名称
|
||||
human_description:
|
||||
en_US: Node name [required for creating groups/external links].
|
||||
zh_Hans: 节点名称 [创建分组/外链必填]。
|
||||
llm_description: Node name [required for creating groups/external links].
|
||||
|
||||
- name: url
|
||||
type: string
|
||||
required: false
|
||||
form: llm
|
||||
label:
|
||||
en_US: Node URL
|
||||
zh_Hans: 节点URL
|
||||
human_description:
|
||||
en_US: Node URL [required for creating external links].
|
||||
zh_Hans: 节点 URL [创建外链必填]。
|
||||
llm_description: Node URL [required for creating external links].
|
||||
|
||||
|
||||
- name: open_window
|
||||
type: select
|
||||
required: false
|
||||
form: llm
|
||||
default: 0
|
||||
options:
|
||||
- value: 0
|
||||
label:
|
||||
en_US: DOC
|
||||
zh_Hans: Current Page
|
||||
pt_BR: DOC
|
||||
- value: 1
|
||||
label:
|
||||
en_US: LINK
|
||||
zh_Hans: New Page
|
||||
pt_BR: LINK
|
||||
label:
|
||||
en_US: Open in new window
|
||||
zh_Hans: 是否新窗口打开
|
||||
human_description:
|
||||
en_US: Open in new window [optional for external links] (0:open in current page, 1:open in new window).
|
||||
zh_Hans: 是否新窗口打开 [外链选填] (0:当前页打开, 1:新窗口打开)。
|
||||
llm_description: Open in new window [optional for external links] (0:open in current page, 1:open in new window).
|
||||
|
||||
|
||||
- name: visible
|
||||
type: select
|
||||
required: false
|
||||
form: llm
|
||||
default: 1
|
||||
options:
|
||||
- value: 0
|
||||
label:
|
||||
en_US: Invisible
|
||||
zh_Hans: 隐藏
|
||||
pt_BR: Invisible
|
||||
- value: 1
|
||||
label:
|
||||
en_US: Visible
|
||||
zh_Hans: 可见
|
||||
pt_BR: Visible
|
||||
label:
|
||||
en_US: Visibility
|
||||
zh_Hans: 是否可见
|
||||
human_description:
|
||||
en_US: Visibility (0:invisible, 1:visible).
|
||||
zh_Hans: 是否可见 (0:不可见, 1:可见)。
|
||||
llm_description: Visibility (0:invisible, 1:visible).
|
||||
@@ -0,0 +1,24 @@
|
||||
"""
|
||||
更新文档
|
||||
"""
|
||||
|
||||
__author__ = "佐井"
|
||||
__created__ = "2024-06-19 16:50:07"
|
||||
|
||||
from typing import Any, Union
|
||||
|
||||
from core.tools.entities.tool_entities import ToolInvokeMessage
|
||||
from core.tools.provider.builtin.aliyuque.tools.base import AliYuqueTool
|
||||
from core.tools.tool.builtin_tool import BuiltinTool
|
||||
|
||||
|
||||
class AliYuqueUpdateDocumentTool(AliYuqueTool, BuiltinTool):
|
||||
def _invoke(
|
||||
self, user_id: str, tool_parameters: dict[str, Any]
|
||||
) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]:
|
||||
token = self.runtime.credentials.get("token", None)
|
||||
if not token:
|
||||
raise Exception("token is required")
|
||||
return self.create_text_message(
|
||||
self.request("PUT", token, tool_parameters, "/api/v2/repos/{book_id}/docs/{id}")
|
||||
)
|
||||
@@ -0,0 +1,87 @@
|
||||
identity:
|
||||
name: aliyuque_update_document
|
||||
author: 佐井
|
||||
label:
|
||||
en_US: Update Document
|
||||
zh_Hans: 更新文档
|
||||
icon: icon.svg
|
||||
description:
|
||||
human:
|
||||
en_US: Update an existing document within a specified knowledge base by providing the document ID or path.
|
||||
zh_Hans: 通过提供文档ID或路径,更新指定知识库中的现有文档。
|
||||
llm: Update doc in a knowledge base via ID/path.
|
||||
parameters:
|
||||
- name: book_id
|
||||
type: number
|
||||
required: true
|
||||
form: llm
|
||||
label:
|
||||
en_US: Knowledge Base ID
|
||||
zh_Hans: 知识库 ID
|
||||
human_description:
|
||||
en_US: The unique identifier of the knowledge base where the document resides.
|
||||
zh_Hans: 文档所属知识库的ID。
|
||||
llm_description: ID of the knowledge base holding the doc.
|
||||
- name: id
|
||||
type: string
|
||||
required: true
|
||||
form: llm
|
||||
label:
|
||||
en_US: Document ID or Path
|
||||
zh_Hans: 文档 ID 或 路径
|
||||
human_description:
|
||||
en_US: The unique identifier or the path of the document to be updated.
|
||||
zh_Hans: 要更新的文档的唯一ID或路径。
|
||||
llm_description: Doc's ID or path for update.
|
||||
|
||||
- name: title
|
||||
type: string
|
||||
required: false
|
||||
form: llm
|
||||
label:
|
||||
en_US: Title
|
||||
zh_Hans: 标题
|
||||
human_description:
|
||||
en_US: The title of the document, defaults to 'Untitled' if not provided.
|
||||
zh_Hans: 文档标题,默认为'无标题'如未提供。
|
||||
llm_description: Title of the document, defaults to 'Untitled'.
|
||||
|
||||
- name: format
|
||||
type: select
|
||||
required: false
|
||||
form: llm
|
||||
options:
|
||||
- value: markdown
|
||||
label:
|
||||
en_US: markdown
|
||||
zh_Hans: markdown
|
||||
pt_BR: markdown
|
||||
- value: html
|
||||
label:
|
||||
en_US: html
|
||||
zh_Hans: html
|
||||
pt_BR: html
|
||||
- value: lake
|
||||
label:
|
||||
en_US: lake
|
||||
zh_Hans: lake
|
||||
pt_BR: lake
|
||||
label:
|
||||
en_US: Content Format
|
||||
zh_Hans: 内容格式
|
||||
human_description:
|
||||
en_US: Format of the document content (markdown, HTML, Lake).
|
||||
zh_Hans: 文档内容格式(markdown, HTML, Lake)。
|
||||
llm_description: Content format choices, markdown, HTML, Lake.
|
||||
|
||||
- name: body
|
||||
type: string
|
||||
required: true
|
||||
form: llm
|
||||
label:
|
||||
en_US: Body Content
|
||||
zh_Hans: 正文内容
|
||||
human_description:
|
||||
en_US: The actual content of the document.
|
||||
zh_Hans: 文档的实际内容。
|
||||
llm_description: Content of the document.
|
||||
@@ -21,7 +21,6 @@ class DiscordWebhookTool(BuiltinTool):
|
||||
return self.create_text_message("Invalid parameter content")
|
||||
|
||||
webhook_url = tool_parameters.get("webhook_url", "")
|
||||
|
||||
if not webhook_url.startswith("https://discord.com/api/webhooks/"):
|
||||
return self.create_text_message(
|
||||
f"Invalid parameter webhook_url ${webhook_url}, \
|
||||
@@ -31,13 +30,14 @@ class DiscordWebhookTool(BuiltinTool):
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
params = {}
|
||||
payload = {
|
||||
"username": tool_parameters.get("username") or user_id,
|
||||
"content": content,
|
||||
"avatar_url": tool_parameters.get("avatar_url") or None,
|
||||
}
|
||||
|
||||
try:
|
||||
res = httpx.post(webhook_url, headers=headers, params=params, json=payload)
|
||||
res = httpx.post(webhook_url, headers=headers, json=payload)
|
||||
if res.is_success:
|
||||
return self.create_text_message("Text message was sent successfully")
|
||||
else:
|
||||
|
||||
@@ -38,3 +38,28 @@ parameters:
|
||||
pt_BR: Content to sent to the channel or person.
|
||||
llm_description: Content of the message
|
||||
form: llm
|
||||
- name: username
|
||||
type: string
|
||||
required: false
|
||||
label:
|
||||
en_US: Discord Webhook Username
|
||||
zh_Hans: Discord Webhook用户名
|
||||
pt_BR: Discord Webhook Username
|
||||
human_description:
|
||||
en_US: Discord Webhook Username
|
||||
zh_Hans: Discord Webhook用户名
|
||||
pt_BR: Discord Webhook Username
|
||||
llm_description: Discord Webhook Username
|
||||
form: llm
|
||||
- name: avatar_url
|
||||
type: string
|
||||
required: false
|
||||
label:
|
||||
en_US: Discord Webhook Avatar
|
||||
zh_Hans: Discord Webhook头像
|
||||
pt_BR: Discord Webhook Avatar
|
||||
human_description:
|
||||
en_US: Discord Webhook Avatar URL
|
||||
zh_Hans: Discord Webhook头像地址
|
||||
pt_BR: Discord Webhook Avatar URL
|
||||
form: form
|
||||
|
||||
@@ -289,7 +289,7 @@ class WorkflowEntry:
|
||||
new_value.append(file)
|
||||
|
||||
if new_value:
|
||||
value = new_value
|
||||
input_value = new_value
|
||||
|
||||
# append variable and value to variable pool
|
||||
variable_pool.add([variable_node_id] + variable_key_list, input_value)
|
||||
|
||||
0
api/tests/unit_tests/oss/__mock/__init__.py
Normal file
100
api/tests/unit_tests/oss/__mock/volcengine_tos.py
Normal file
@@ -0,0 +1,100 @@
|
||||
import os
|
||||
from typing import Union
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from _pytest.monkeypatch import MonkeyPatch
|
||||
from tos import TosClientV2
|
||||
from tos.clientv2 import DeleteObjectOutput, GetObjectOutput, HeadObjectOutput, PutObjectOutput
|
||||
|
||||
|
||||
class AttrDict(dict):
|
||||
def __getattr__(self, item):
|
||||
return self.get(item)
|
||||
|
||||
|
||||
def get_example_bucket() -> str:
|
||||
return "dify"
|
||||
|
||||
|
||||
def get_example_filename() -> str:
|
||||
return "test.txt"
|
||||
|
||||
|
||||
def get_example_data() -> bytes:
|
||||
return b"test"
|
||||
|
||||
|
||||
def get_example_filepath() -> str:
|
||||
return "/test"
|
||||
|
||||
|
||||
class MockVolcengineTosClass:
|
||||
def __init__(self, ak="", sk="", endpoint="", region=""):
|
||||
self.bucket_name = get_example_bucket()
|
||||
self.key = get_example_filename()
|
||||
self.content = get_example_data()
|
||||
self.filepath = get_example_filepath()
|
||||
self.resp = AttrDict(
|
||||
{
|
||||
"x-tos-server-side-encryption": "kms",
|
||||
"x-tos-server-side-encryption-kms-key-id": "trn:kms:cn-beijing:****:keyrings/ring-test/keys/key-test",
|
||||
"x-tos-server-side-encryption-customer-algorithm": "AES256",
|
||||
"x-tos-version-id": "test",
|
||||
"x-tos-hash-crc64ecma": 123456,
|
||||
"request_id": "test",
|
||||
"headers": {
|
||||
"x-tos-id-2": "test",
|
||||
"ETag": "123456",
|
||||
},
|
||||
"status": 200,
|
||||
}
|
||||
)
|
||||
|
||||
def put_object(self, bucket: str, key: str, content=None) -> PutObjectOutput:
|
||||
assert bucket == self.bucket_name
|
||||
assert key == self.key
|
||||
assert content == self.content
|
||||
return PutObjectOutput(self.resp)
|
||||
|
||||
def get_object(self, bucket: str, key: str) -> GetObjectOutput:
|
||||
assert bucket == self.bucket_name
|
||||
assert key == self.key
|
||||
|
||||
get_object_output = MagicMock(GetObjectOutput)
|
||||
get_object_output.read.return_value = self.content
|
||||
return get_object_output
|
||||
|
||||
def get_object_to_file(self, bucket: str, key: str, file_path: str):
|
||||
assert bucket == self.bucket_name
|
||||
assert key == self.key
|
||||
assert file_path == self.filepath
|
||||
|
||||
def head_object(self, bucket: str, key: str) -> HeadObjectOutput:
|
||||
assert bucket == self.bucket_name
|
||||
assert key == self.key
|
||||
return HeadObjectOutput(self.resp)
|
||||
|
||||
def delete_object(self, bucket: str, key: str):
|
||||
assert bucket == self.bucket_name
|
||||
assert key == self.key
|
||||
return DeleteObjectOutput(self.resp)
|
||||
|
||||
|
||||
MOCK = os.getenv("MOCK_SWITCH", "false").lower() == "true"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def setup_volcengine_tos_mock(monkeypatch: MonkeyPatch):
|
||||
if MOCK:
|
||||
monkeypatch.setattr(TosClientV2, "__init__", MockVolcengineTosClass.__init__)
|
||||
monkeypatch.setattr(TosClientV2, "put_object", MockVolcengineTosClass.put_object)
|
||||
monkeypatch.setattr(TosClientV2, "get_object", MockVolcengineTosClass.get_object)
|
||||
monkeypatch.setattr(TosClientV2, "get_object_to_file", MockVolcengineTosClass.get_object_to_file)
|
||||
monkeypatch.setattr(TosClientV2, "head_object", MockVolcengineTosClass.head_object)
|
||||
monkeypatch.setattr(TosClientV2, "delete_object", MockVolcengineTosClass.delete_object)
|
||||
|
||||
yield
|
||||
|
||||
if MOCK:
|
||||
monkeypatch.undo()
|
||||
0
api/tests/unit_tests/oss/volcengine_tos/__init__.py
Normal file
@@ -0,0 +1,67 @@
|
||||
from collections.abc import Generator
|
||||
|
||||
from flask import Flask
|
||||
from tos import TosClientV2
|
||||
from tos.clientv2 import GetObjectOutput, HeadObjectOutput, PutObjectOutput
|
||||
|
||||
from extensions.storage.volcengine_tos_storage import VolcengineTosStorage
|
||||
from tests.unit_tests.oss.__mock.volcengine_tos import (
|
||||
get_example_bucket,
|
||||
get_example_data,
|
||||
get_example_filename,
|
||||
get_example_filepath,
|
||||
setup_volcengine_tos_mock,
|
||||
)
|
||||
|
||||
|
||||
class VolcengineTosTest:
|
||||
_instance = None
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance == None:
|
||||
cls._instance = object.__new__(cls)
|
||||
return cls._instance
|
||||
else:
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
self.storage = VolcengineTosStorage(app=Flask(__name__))
|
||||
self.storage.bucket_name = get_example_bucket()
|
||||
self.storage.client = TosClientV2(
|
||||
ak="dify",
|
||||
sk="dify",
|
||||
endpoint="https://xxx.volces.com",
|
||||
region="cn-beijing",
|
||||
)
|
||||
|
||||
|
||||
def test_save(setup_volcengine_tos_mock):
|
||||
volc_tos = VolcengineTosTest()
|
||||
volc_tos.storage.save(get_example_filename(), get_example_data())
|
||||
|
||||
|
||||
def test_load_once(setup_volcengine_tos_mock):
|
||||
volc_tos = VolcengineTosTest()
|
||||
assert volc_tos.storage.load_once(get_example_filename()) == get_example_data()
|
||||
|
||||
|
||||
def test_load_stream(setup_volcengine_tos_mock):
|
||||
volc_tos = VolcengineTosTest()
|
||||
generator = volc_tos.storage.load_stream(get_example_filename())
|
||||
assert isinstance(generator, Generator)
|
||||
assert next(generator) == get_example_data()
|
||||
|
||||
|
||||
def test_download(setup_volcengine_tos_mock):
|
||||
volc_tos = VolcengineTosTest()
|
||||
volc_tos.storage.download(get_example_filename(), get_example_filepath())
|
||||
|
||||
|
||||
def test_exists(setup_volcengine_tos_mock):
|
||||
volc_tos = VolcengineTosTest()
|
||||
assert volc_tos.storage.exists(get_example_filename())
|
||||
|
||||
|
||||
def test_delete(setup_volcengine_tos_mock):
|
||||
volc_tos = VolcengineTosTest()
|
||||
volc_tos.storage.delete(get_example_filename())
|
||||
@@ -96,6 +96,10 @@ x-shared-env: &shared-api-worker-env
|
||||
VOLCENGINE_TOS_ACCESS_KEY: ${VOLCENGINE_TOS_ACCESS_KEY:-}
|
||||
VOLCENGINE_TOS_ENDPOINT: ${VOLCENGINE_TOS_ENDPOINT:-}
|
||||
VOLCENGINE_TOS_REGION: ${VOLCENGINE_TOS_REGION:-}
|
||||
BAIDU_OBS_BUCKET_NAME: ${BAIDU_OBS_BUCKET_NAME:-}
|
||||
BAIDU_OBS_SECRET_KEY: ${BAIDU_OBS_SECRET_KEY:-}
|
||||
BAIDU_OBS_ACCESS_KEY: ${BAIDU_OBS_ACCESS_KEY:-}
|
||||
BAIDU_OBS_ENDPOINT: ${BAIDU_OBS_ENDPOINT:-}
|
||||
VECTOR_STORE: ${VECTOR_STORE:-weaviate}
|
||||
WEAVIATE_ENDPOINT: ${WEAVIATE_ENDPOINT:-http://weaviate:8080}
|
||||
WEAVIATE_API_KEY: ${WEAVIATE_API_KEY:-WVF5YThaHlkYwhGUSmCRgsX3tD5ngdN8pkih}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"root": true,
|
||||
"extends": [
|
||||
"next",
|
||||
"@antfu",
|
||||
@@ -28,4 +29,4 @@
|
||||
"react-hooks/exhaustive-deps": "warn",
|
||||
"react/display-name": "warn"
|
||||
}
|
||||
}
|
||||
}
|
||||
9
web/.gitignore
vendored
@@ -44,10 +44,9 @@ package-lock.json
|
||||
.pnp.cjs
|
||||
.pnp.loader.mjs
|
||||
.yarn/
|
||||
.yarnrc.yml
|
||||
|
||||
# pmpm
|
||||
pnpm-lock.yaml
|
||||
|
||||
.favorites.json
|
||||
*storybook.log
|
||||
|
||||
# storybook
|
||||
/storybook-static
|
||||
*storybook.log
|
||||
|
||||
@@ -63,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..."
|
||||
npm run test "../$test_file"
|
||||
pnpm run test "../$test_file"
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Unit tests failed. Please fix the errors before committing."
|
||||
|
||||
@@ -6,14 +6,12 @@ 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 [NPM version 8.x.x](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/).
|
||||
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).
|
||||
|
||||
First, install the dependencies:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
# or
|
||||
yarn install --frozen-lockfile
|
||||
pnpm install
|
||||
```
|
||||
|
||||
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:
|
||||
@@ -43,9 +41,7 @@ NEXT_PUBLIC_SENTRY_DSN=
|
||||
Finally, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
@@ -59,19 +55,19 @@ You can start editing the file under folder `app`. The page auto-updates as you
|
||||
First, build the app for production:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
Then, start the server:
|
||||
|
||||
```bash
|
||||
npm run start
|
||||
pnpm run start
|
||||
```
|
||||
|
||||
If you want to customize the host and port:
|
||||
|
||||
```bash
|
||||
npm run start --port=3001 --host=0.0.0.0
|
||||
pnpm run start --port=3001 --host=0.0.0.0
|
||||
```
|
||||
|
||||
## Storybook
|
||||
@@ -99,7 +95,7 @@ You can create a test file with a suffix of `.spec` beside the file that to be t
|
||||
Run test:
|
||||
|
||||
```bash
|
||||
npm run test
|
||||
pnpm run test
|
||||
```
|
||||
|
||||
If you are not familiar with writing tests, here is some code to refer to:
|
||||
|
||||
18
web/app/(commonLayout)/plugins/page.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
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'
|
||||
|
||||
const PluginList = async () => {
|
||||
return (
|
||||
<PluginPage
|
||||
plugins={<PluginsPanel />}
|
||||
marketplace={<Marketplace />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const metadata = {
|
||||
title: 'Plugins - Dify',
|
||||
}
|
||||
|
||||
export default PluginList
|
||||
14
web/app/(commonLayout)/plugins/test/card/actions.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
'use server'
|
||||
|
||||
import { revalidatePath } from 'next/cache'
|
||||
|
||||
// Server Actions
|
||||
export async function handleDelete() {
|
||||
// revalidatePath only invalidates the cache when the included path is next visited.
|
||||
revalidatePath('/')
|
||||
}
|
||||
|
||||
export async function fetchPluginDetail(org: string, name: string) {
|
||||
// Fetch plugin detail TODO
|
||||
return { org, name }
|
||||
}
|
||||
80
web/app/(commonLayout)/plugins/test/card/page.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import { handleDelete } from './actions'
|
||||
import Card from '@/app/components/plugins/card'
|
||||
import { customTool, extensionDallE, modelGPT4, toolNotion } from '@/app/components/plugins/card/card-mock'
|
||||
import PluginItem from '@/app/components/plugins/plugin-item'
|
||||
import CardMoreInfo from '@/app/components/plugins/card/card-more-info'
|
||||
import ProviderCard from '@/app/components/plugins/provider-card'
|
||||
import PluginDetailPanel from '@/app/components/plugins/plugin-detail-panel'
|
||||
import { getLocaleOnServer } from '@/i18n/server'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
|
||||
const PluginList = async () => {
|
||||
const locale = getLocaleOnServer()
|
||||
const pluginList = [toolNotion, extensionDallE, modelGPT4, customTool]
|
||||
|
||||
return (
|
||||
<div className='pb-3 bg-white'>
|
||||
<div className='mx-3 '>
|
||||
<h2 className='my-3'>Dify Plugin list</h2>
|
||||
<div className='grid grid-cols-2 gap-3'>
|
||||
{pluginList.map((plugin, index) => (
|
||||
<PluginItem
|
||||
key={index}
|
||||
payload={plugin as any}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<h2 className='my-3'>Install Plugin / Package under bundle</h2>
|
||||
<div className='w-[512px] rounded-2xl bg-background-section-burn p-2'>
|
||||
<Card
|
||||
payload={toolNotion as any}
|
||||
descriptionLineRows={1}
|
||||
titleLeft={
|
||||
<Badge className='ml-1' text={toolNotion.version} />
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<h3 className='my-1'>Installed</h3>
|
||||
<div className='w-[512px] rounded-2xl bg-background-section-burn p-2'>
|
||||
<Card
|
||||
payload={toolNotion as any}
|
||||
descriptionLineRows={1}
|
||||
installed
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3 className='my-1'>Install model provide</h3>
|
||||
<div className='grid grid-cols-2 gap-3'>
|
||||
{pluginList.map((plugin, index) => (
|
||||
<ProviderCard key={index} payload={plugin as any} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className='my-3 h-[px] bg-gray-50'></div>
|
||||
<h2 className='my-3'>Marketplace Plugin list</h2>
|
||||
<div className='grid grid-cols-4 gap-3'>
|
||||
{pluginList.map((plugin, index) => (
|
||||
<Card
|
||||
key={index}
|
||||
payload={plugin as any}
|
||||
footer={
|
||||
<CardMoreInfo downloadCount={index % 2 === 0 ? 1234 : 6} tags={index % 2 === 0 ? ['Search', 'Tag that has very very long name', 'Productivity', 'Tag2'] : []} />
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<PluginDetailPanel
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const metadata = {
|
||||
title: 'Plugins - Card',
|
||||
}
|
||||
|
||||
export default PluginList
|
||||
20
web/app/(commonLayout)/plugins/test/other/page.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
'use client'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import UpdatePlugin from '@/app/components/plugins/update-plugin'
|
||||
|
||||
const Page = () => {
|
||||
const [isShowUpdateModal, {
|
||||
setTrue: showUpdateModal,
|
||||
setFalse: hideUpdateModal,
|
||||
}] = useBoolean(false)
|
||||
return (
|
||||
<div>
|
||||
<div onClick={showUpdateModal}>Show Upgrade</div>
|
||||
{isShowUpdateModal && (
|
||||
<UpdatePlugin onHide={hideUpdateModal} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Page
|
||||
39
web/app/(commonLayout)/plugins/test/tools-picker/page.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
'use client'
|
||||
import { useEffect, useState } from 'react'
|
||||
import AllTools from '@/app/components/workflow/block-selector/all-tools'
|
||||
import {
|
||||
fetchAllBuiltInTools,
|
||||
fetchAllCustomTools,
|
||||
fetchAllWorkflowTools,
|
||||
} from '@/service/tools'
|
||||
import type { ToolWithProvider } from '@/app/components/workflow/types'
|
||||
|
||||
const ToolsPicker = () => {
|
||||
const [buildInTools, setBuildInTools] = useState<ToolWithProvider[]>([])
|
||||
const [customTools, setCustomTools] = useState<ToolWithProvider[]>([])
|
||||
const [workflowTools, setWorkflowTools] = useState<ToolWithProvider[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const buildInTools = await fetchAllBuiltInTools()
|
||||
const customTools = await fetchAllCustomTools()
|
||||
const workflowTools = await fetchAllWorkflowTools()
|
||||
setBuildInTools(buildInTools)
|
||||
setCustomTools(customTools)
|
||||
setWorkflowTools(workflowTools)
|
||||
})()
|
||||
})
|
||||
return (
|
||||
<div className="relative mt-5 mx-auto w-[320px] bg-white">
|
||||
<AllTools
|
||||
searchText=""
|
||||
onSelect={() => { }}
|
||||
buildInTools={buildInTools}
|
||||
customTools={customTools}
|
||||
workflowTools={workflowTools}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ToolsPicker
|
||||
@@ -2,9 +2,7 @@
|
||||
|
||||
@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 {
|
||||
@@ -29,21 +27,15 @@
|
||||
}
|
||||
|
||||
.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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -28,7 +28,7 @@ const actionButtonVariants = cva(
|
||||
)
|
||||
|
||||
export type ActionButtonProps = {
|
||||
size?: 'xs' | 'm' | 'l' | 'xl'
|
||||
size?: 'xs' | 's' | 'm' | 'l' | 'xl'
|
||||
state?: ActionButtonState
|
||||
styleCss?: CSSProperties
|
||||
} & React.ButtonHTMLAttributes<HTMLButtonElement> & VariantProps<typeof actionButtonVariants>
|
||||
|
||||
@@ -5,22 +5,28 @@ type BadgeProps = {
|
||||
className?: string
|
||||
text: string
|
||||
uppercase?: boolean
|
||||
hasRedCornerMark?: boolean
|
||||
}
|
||||
|
||||
const Badge = ({
|
||||
className,
|
||||
text,
|
||||
uppercase = true,
|
||||
hasRedCornerMark,
|
||||
}: BadgeProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'inline-flex items-center px-[5px] h-5 rounded-[5px] border border-divider-deep leading-3 text-text-tertiary',
|
||||
'relative inline-flex items-center px-[5px] h-5 rounded-[5px] border border-divider-deep leading-3 text-text-tertiary',
|
||||
uppercase ? 'system-2xs-medium-uppercase' : 'system-xs-medium',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{text}
|
||||
{hasRedCornerMark && (
|
||||
<div className='absolute top-[-2px] right-[-2px] w-1.5 h-1.5 border border-components-badge-status-light-error-border-inner bg-components-badge-status-light-error-bg rounded-[2px] shadow-sm'>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
28
web/app/components/base/badge/index.css
Normal file
@@ -0,0 +1,28 @@
|
||||
@tailwind components;
|
||||
|
||||
@layer components {
|
||||
.badge {
|
||||
@apply inline-flex justify-center items-center text-text-tertiary border border-divider-deep
|
||||
}
|
||||
|
||||
.badge-l {
|
||||
@apply rounded-md gap-1 min-w-6
|
||||
}
|
||||
|
||||
/* m is for the regular button */
|
||||
.badge-m {
|
||||
@apply rounded-md gap-[3px] min-w-5
|
||||
}
|
||||
|
||||
.badge-s {
|
||||
@apply rounded-[5px] gap-0.5 min-w-[18px]
|
||||
}
|
||||
|
||||
.badge.badge-warning {
|
||||
@apply text-text-warning border border-text-warning
|
||||
}
|
||||
|
||||
.badge.badge-accent {
|
||||
@apply text-text-accent-secondary border border-text-accent-secondary
|
||||
}
|
||||
}
|
||||
81
web/app/components/base/badge/index.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import type { CSSProperties, ReactNode } from 'react'
|
||||
import React from 'react'
|
||||
import { type VariantProps, cva } from 'class-variance-authority'
|
||||
import classNames from '@/utils/classnames'
|
||||
import './index.css'
|
||||
|
||||
enum BadgeState {
|
||||
Warning = 'warning',
|
||||
Accent = 'accent',
|
||||
Default = '',
|
||||
}
|
||||
|
||||
const BadgeVariants = cva(
|
||||
'badge',
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
s: 'badge-s',
|
||||
m: 'badge-m',
|
||||
l: 'badge-l',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 'm',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
type BadgeProps = {
|
||||
size?: 's' | 'm' | 'l'
|
||||
iconOnly?: boolean
|
||||
uppercase?: boolean
|
||||
state?: BadgeState
|
||||
styleCss?: CSSProperties
|
||||
children?: ReactNode
|
||||
} & React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof BadgeVariants>
|
||||
|
||||
function getBadgeState(state: BadgeState) {
|
||||
switch (state) {
|
||||
case BadgeState.Warning:
|
||||
return 'badge-warning'
|
||||
case BadgeState.Accent:
|
||||
return 'badge-accent'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
const Badge: React.FC<BadgeProps> = ({
|
||||
className,
|
||||
size,
|
||||
state = BadgeState.Default,
|
||||
iconOnly = false,
|
||||
uppercase = false,
|
||||
styleCss,
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
BadgeVariants({ size, className }),
|
||||
getBadgeState(state),
|
||||
size === 's'
|
||||
? (iconOnly ? 'p-[3px]' : 'px-[5px] py-[3px]')
|
||||
: size === 'l'
|
||||
? (iconOnly ? 'p-1.5' : 'px-2 py-1')
|
||||
: (iconOnly ? 'p-1' : 'px-[5px] py-[2px]'),
|
||||
uppercase ? 'system-2xs-medium-uppercase' : 'system-2xs-medium',
|
||||
)}
|
||||
style={styleCss}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Badge.displayName = 'Badge'
|
||||
|
||||
export default Badge
|
||||
export { Badge, BadgeState, BadgeVariants }
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
import { useState } from 'react'
|
||||
import { t } from 'i18next'
|
||||
import { debounce } from 'lodash-es'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import s from './style.module.css'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
@@ -18,22 +19,29 @@ const CopyBtn = ({
|
||||
}: ICopyBtnProps) => {
|
||||
const [isCopied, setIsCopied] = useState(false)
|
||||
|
||||
const onClickCopy = debounce(() => {
|
||||
copy(value)
|
||||
setIsCopied(true)
|
||||
}, 100)
|
||||
|
||||
const onMouseLeave = debounce(() => {
|
||||
setIsCopied(false)
|
||||
}, 100)
|
||||
|
||||
return (
|
||||
<div className={`${className}`}>
|
||||
<Tooltip
|
||||
popupContent={(isCopied ? t('appApi.copied') : t('appApi.copy'))}
|
||||
>
|
||||
<div
|
||||
onMouseLeave={onMouseLeave}
|
||||
className={'box-border p-0.5 flex items-center justify-center rounded-md bg-white cursor-pointer'}
|
||||
style={!isPlain
|
||||
? {
|
||||
boxShadow: '0px 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06)',
|
||||
}
|
||||
: {}}
|
||||
onClick={() => {
|
||||
copy(value)
|
||||
setIsCopied(true)
|
||||
}}
|
||||
onClick={onClickCopy}
|
||||
>
|
||||
<div className={`w-6 h-6 rounded-md hover:bg-gray-50 ${s.copyIcon} ${isCopied ? s.copied : ''}`}></div>
|
||||
</div>
|
||||
|
||||
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
@@ -0,0 +1,8 @@
|
||||
<svg width="14" height="16" viewBox="0 0 14 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Group">
|
||||
<path id="Vector" d="M5.6475 8.0115L0.333496 5.05884V11.3335C0.333491 11.4524 0.365258 11.569 0.425506 11.6715C0.485754 11.7739 0.572294 11.8584 0.676163 11.9162L6.3335 15.0588V9.17684C6.33344 8.93907 6.26981 8.70565 6.14919 8.50075C6.02857 8.29586 5.85536 8.12694 5.6475 8.0115Z" fill="#354052"/>
|
||||
<path id="Vector_2" d="M7.66699 9.17684V15.0588L13.3243 11.9162C13.4282 11.8584 13.5147 11.7739 13.575 11.6715C13.6352 11.569 13.667 11.4524 13.667 11.3335V5.05884L8.35299 8.0115C8.14513 8.12694 7.97192 8.29586 7.8513 8.50075C7.73068 8.70565 7.66705 8.93907 7.66699 9.17684Z" fill="#676F83"/>
|
||||
<path id="Vector_3" d="M10.1913 2.34351C9.804 3.33351 8.588 4.00017 7 4.00017C5.412 4.00017 4.196 3.33351 3.80867 2.34351L1 3.90417L6.35267 6.87817C6.5507 6.98815 6.77348 7.04586 7 7.04586C7.22652 7.04586 7.4493 6.98815 7.64733 6.87817L13 3.90417L10.1913 2.34351Z" fill="#676F83"/>
|
||||
<path id="Vector_4" d="M7 2.66675C8.10457 2.66675 9 2.21903 9 1.66675C9 1.11446 8.10457 0.666748 7 0.666748C5.89543 0.666748 5 1.11446 5 1.66675C5 2.21903 5.89543 2.66675 7 2.66675Z" fill="#354052"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,9 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Icon">
|
||||
<path id="Vector" fill-rule="evenodd" clip-rule="evenodd" d="M11.3891 2.41987C11.6635 2.58871 11.749 2.94802 11.5802 3.22239L10.3324 5.25H11.0833C11.4055 5.25 11.6667 5.51117 11.6667 5.83334V11.6667C11.6667 12.311 11.1444 12.8333 10.5 12.8333H3.50001C2.85568 12.8333 2.33334 12.311 2.33334 11.6667V5.83334C2.33334 5.51117 2.59451 5.25 2.91668 5.25H8.96252L10.5865 2.61094C10.7554 2.33657 11.1147 2.25102 11.3891 2.41987ZM5.83334 7.58334C5.51118 7.58334 5.25001 7.84449 5.25001 8.16667C5.25001 8.48884 5.51118 8.75 5.83334 8.75H8.16668C8.48885 8.75 8.75001 8.48884 8.75001 8.16667C8.75001 7.84449 8.48885 7.58334 8.16668 7.58334H5.83334Z" fill="#676F83"/>
|
||||
<g id="Vector_2" opacity="0.5">
|
||||
<path d="M6.91257 1.79347C6.96898 1.76525 7.01477 1.71948 7.043 1.66303L7.32195 1.10508C7.42946 0.890105 7.73623 0.890105 7.84374 1.10508L8.12269 1.66303C8.15093 1.71948 8.19672 1.76525 8.25313 1.79347L8.81108 2.07245C9.0261 2.17994 9.0261 2.48672 8.81108 2.5942L8.25313 2.87318C8.19672 2.9014 8.15093 2.94717 8.12269 3.00362L7.84374 3.56158C7.73623 3.77655 7.42946 3.77655 7.32195 3.56158L7.043 3.00362C7.01477 2.94717 6.96898 2.9014 6.91257 2.87318L6.35461 2.5942C6.13965 2.48672 6.13965 2.17994 6.35461 2.07245L6.91257 1.79347Z" fill="#676F83"/>
|
||||
<path d="M3.80145 2.7657C3.85789 2.73748 3.90366 2.69171 3.93189 2.63526L4.11364 2.27174C4.22113 2.05677 4.5279 2.05677 4.63539 2.27174L4.81715 2.63526C4.84537 2.6917 4.89114 2.73748 4.94759 2.7657L5.3111 2.94745C5.52607 3.05494 5.52607 3.36172 5.3111 3.4692L4.94759 3.65096C4.89114 3.67919 4.84537 3.72495 4.81715 3.7814L4.63539 4.14491C4.5279 4.35988 4.22113 4.35988 4.11364 4.14491L3.93189 3.7814C3.90366 3.72495 3.85789 3.67919 3.80145 3.65096L3.43793 3.4692C3.22296 3.36172 3.22296 3.05494 3.43793 2.94745L3.80145 2.7657Z" fill="#676F83"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="13" height="20" viewBox="0 0 13 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="Shape" d="M0 0H13V20C9.98017 20 7.26458 18.1615 6.14305 15.3576L0 0Z" fill="#F9FAFB"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 200 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,6 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Icon">
|
||||
<path id="Vector" d="M3.99999 1.33325H7.99999V5.33325C7.99999 6.06963 8.59692 6.66659 9.33332 6.66659H13.3333V13.3333C13.3333 14.0697 12.7364 14.6666 12 14.6666H6.66666V13.3333H7.99999V11.9999H6.66666V10.6666H7.99999V9.33325H6.66666V7.99992H5.33332V9.33325H6.66666V10.6666H5.33332V11.9999H6.66666V13.3333H5.33332V14.6666H3.99999C3.26361 14.6666 2.66666 14.0697 2.66666 13.3333V2.66659C2.66666 1.93021 3.26361 1.33325 3.99999 1.33325Z" fill="#676F83"/>
|
||||
<path id="Vector_2" opacity="0.5" d="M12.9428 4.99993C13.0415 5.09868 13.1232 5.21133 13.1859 5.33327H9.33334V1.48071C9.45528 1.54338 9.56794 1.62504 9.66668 1.72379L12.9428 4.99993Z" fill="#676F83"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 775 B |
@@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Icon">
|
||||
<path id="Vector" d="M8 1C4.1325 1 1 4.1325 1 8C1 11.0975 3.00375 13.7137 5.78625 14.6413C6.13625 14.7025 6.2675 14.4925 6.2675 14.3088C6.2675 14.1425 6.25875 13.5913 6.25875 13.005C4.5 13.3288 4.045 12.5763 3.905 12.1825C3.82625 11.9812 3.485 11.36 3.1875 11.1937C2.9425 11.0625 2.5925 10.7387 3.17875 10.73C3.73 10.7212 4.12375 11.2375 4.255 11.4475C4.885 12.5062 5.89125 12.2088 6.29375 12.025C6.355 11.57 6.53875 11.2638 6.74 11.0887C5.1825 10.9137 3.555 10.31 3.555 7.6325C3.555 6.87125 3.82625 6.24125 4.2725 5.75125C4.2025 5.57625 3.9575 4.85875 4.3425 3.89625C4.3425 3.89625 4.92875 3.7125 6.2675 4.61375C6.8275 4.45625 7.4225 4.3775 8.0175 4.3775C8.6125 4.3775 9.2075 4.45625 9.7675 4.61375C11.1063 3.70375 11.6925 3.89625 11.6925 3.89625C12.0775 4.85875 11.8325 5.57625 11.7625 5.75125C12.2087 6.24125 12.48 6.8625 12.48 7.6325C12.48 10.3187 10.8438 10.9137 9.28625 11.0887C9.54 11.3075 9.75875 11.7275 9.75875 12.3837C9.75875 13.32 9.75 14.0725 9.75 14.3088C9.75 14.4925 9.88125 14.7113 10.2312 14.6413C11.6209 14.1721 12.8284 13.279 13.6839 12.0877C14.5393 10.8963 14.9996 9.46668 15 8C15 4.1325 11.8675 1 8 1Z" fill="#676F83"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -1,8 +1,9 @@
|
||||
const path = require('node:path')
|
||||
const { open, readdir, access, mkdir, writeFile, appendFile, rm } = require('node:fs/promises')
|
||||
const { parseXml } = require('@rgrove/parse-xml')
|
||||
const camelCase = require('lodash/camelCase')
|
||||
const template = require('lodash/template')
|
||||
import path from 'node:path'
|
||||
import { access, appendFile, mkdir, open, readdir, rm, writeFile } from 'node:fs/promises'
|
||||
import { parseXml } from '@rgrove/parse-xml'
|
||||
import { camelCase, template } from 'lodash-es'
|
||||
|
||||
const __dirname = path.dirname(new URL(import.meta.url).pathname)
|
||||
|
||||
const generateDir = async (currentPath) => {
|
||||
try {
|
||||
66
web/app/components/base/icons/src/vender/other/Group.json
Normal file
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "14",
|
||||
"height": "16",
|
||||
"viewBox": "0 0 14 16",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "g",
|
||||
"attributes": {
|
||||
"id": "Group"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"id": "Vector",
|
||||
"d": "M5.6475 8.0115L0.333496 5.05884V11.3335C0.333491 11.4524 0.365258 11.569 0.425506 11.6715C0.485754 11.7739 0.572294 11.8584 0.676163 11.9162L6.3335 15.0588V9.17684C6.33344 8.93907 6.26981 8.70565 6.14919 8.50075C6.02857 8.29586 5.85536 8.12694 5.6475 8.0115Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"id": "Vector_2",
|
||||
"d": "M7.66699 9.17684V15.0588L13.3243 11.9162C13.4282 11.8584 13.5147 11.7739 13.575 11.6715C13.6352 11.569 13.667 11.4524 13.667 11.3335V5.05884L8.35299 8.0115C8.14513 8.12694 7.97192 8.29586 7.8513 8.50075C7.73068 8.70565 7.66705 8.93907 7.66699 9.17684Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"id": "Vector_3",
|
||||
"d": "M10.1913 2.34351C9.804 3.33351 8.588 4.00017 7 4.00017C5.412 4.00017 4.196 3.33351 3.80867 2.34351L1 3.90417L6.35267 6.87817C6.5507 6.98815 6.77348 7.04586 7 7.04586C7.22652 7.04586 7.4493 6.98815 7.64733 6.87817L13 3.90417L10.1913 2.34351Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"id": "Vector_4",
|
||||
"d": "M7 2.66675C8.10457 2.66675 9 2.21903 9 1.66675C9 1.11446 8.10457 0.666748 7 0.666748C5.89543 0.666748 5 1.11446 5 1.66675C5 2.21903 5.89543 2.66675 7 2.66675Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "Group"
|
||||
}
|
||||
16
web/app/components/base/icons/src/vender/other/Group.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './Group.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
Icon.displayName = 'Group'
|
||||
|
||||
export default Icon
|
||||
@@ -1 +1,2 @@
|
||||
export { default as Generator } from './Generator'
|
||||
export { default as Group } from './Group'
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "14",
|
||||
"height": "14",
|
||||
"viewBox": "0 0 14 14",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "g",
|
||||
"attributes": {
|
||||
"id": "Icon"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"id": "Vector",
|
||||
"fill-rule": "evenodd",
|
||||
"clip-rule": "evenodd",
|
||||
"d": "M11.3891 2.41987C11.6635 2.58871 11.749 2.94802 11.5802 3.22239L10.3324 5.25H11.0833C11.4055 5.25 11.6667 5.51117 11.6667 5.83334V11.6667C11.6667 12.311 11.1444 12.8333 10.5 12.8333H3.50001C2.85568 12.8333 2.33334 12.311 2.33334 11.6667V5.83334C2.33334 5.51117 2.59451 5.25 2.91668 5.25H8.96252L10.5865 2.61094C10.7554 2.33657 11.1147 2.25102 11.3891 2.41987ZM5.83334 7.58334C5.51118 7.58334 5.25001 7.84449 5.25001 8.16667C5.25001 8.48884 5.51118 8.75 5.83334 8.75H8.16668C8.48885 8.75 8.75001 8.48884 8.75001 8.16667C8.75001 7.84449 8.48885 7.58334 8.16668 7.58334H5.83334Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "g",
|
||||
"attributes": {
|
||||
"id": "Vector_2",
|
||||
"opacity": "0.5"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M6.91257 1.79347C6.96898 1.76525 7.01477 1.71948 7.043 1.66303L7.32195 1.10508C7.42946 0.890105 7.73623 0.890105 7.84374 1.10508L8.12269 1.66303C8.15093 1.71948 8.19672 1.76525 8.25313 1.79347L8.81108 2.07245C9.0261 2.17994 9.0261 2.48672 8.81108 2.5942L8.25313 2.87318C8.19672 2.9014 8.15093 2.94717 8.12269 3.00362L7.84374 3.56158C7.73623 3.77655 7.42946 3.77655 7.32195 3.56158L7.043 3.00362C7.01477 2.94717 6.96898 2.9014 6.91257 2.87318L6.35461 2.5942C6.13965 2.48672 6.13965 2.17994 6.35461 2.07245L6.91257 1.79347Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M3.80145 2.7657C3.85789 2.73748 3.90366 2.69171 3.93189 2.63526L4.11364 2.27174C4.22113 2.05677 4.5279 2.05677 4.63539 2.27174L4.81715 2.63526C4.84537 2.6917 4.89114 2.73748 4.94759 2.7657L5.3111 2.94745C5.52607 3.05494 5.52607 3.36172 5.3111 3.4692L4.94759 3.65096C4.89114 3.67919 4.84537 3.72495 4.81715 3.7814L4.63539 4.14491C4.5279 4.35988 4.22113 4.35988 4.11364 4.14491L3.93189 3.7814C3.90366 3.72495 3.85789 3.67919 3.80145 3.65096L3.43793 3.4692C3.22296 3.36172 3.22296 3.05494 3.43793 2.94745L3.80145 2.7657Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "BoxSparkleFill"
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './BoxSparkleFill.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
Icon.displayName = 'BoxSparkleFill'
|
||||
|
||||
export default Icon
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "13",
|
||||
"height": "20",
|
||||
"viewBox": "0 0 13 20",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"id": "Shape",
|
||||
"d": "M0 0H13V20C9.98017 20 7.26458 18.1615 6.14305 15.3576L0 0Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "LeftCorner"
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './LeftCorner.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
Icon.displayName = 'LeftCorner'
|
||||
|
||||
export default Icon
|
||||
2
web/app/components/base/icons/src/vender/plugin/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as BoxSparkleFill } from './BoxSparkleFill'
|
||||
export { default as LeftCorner } from './LeftCorner'
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "16",
|
||||
"height": "16",
|
||||
"viewBox": "0 0 16 16",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "g",
|
||||
"attributes": {
|
||||
"id": "Icon"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"id": "Vector",
|
||||
"d": "M3.99999 1.33325H7.99999V5.33325C7.99999 6.06963 8.59692 6.66659 9.33332 6.66659H13.3333V13.3333C13.3333 14.0697 12.7364 14.6666 12 14.6666H6.66666V13.3333H7.99999V11.9999H6.66666V10.6666H7.99999V9.33325H6.66666V7.99992H5.33332V9.33325H6.66666V10.6666H5.33332V11.9999H6.66666V13.3333H5.33332V14.6666H3.99999C3.26361 14.6666 2.66666 14.0697 2.66666 13.3333V2.66659C2.66666 1.93021 3.26361 1.33325 3.99999 1.33325Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"id": "Vector_2",
|
||||
"opacity": "0.5",
|
||||
"d": "M12.9428 4.99993C13.0415 5.09868 13.1232 5.21133 13.1859 5.33327H9.33334V1.48071C9.45528 1.54338 9.56794 1.62504 9.66668 1.72379L12.9428 4.99993Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "FileZip"
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './FileZip.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
Icon.displayName = 'FileZip'
|
||||
|
||||
export default Icon
|
||||
@@ -1,3 +1,4 @@
|
||||
export { default as File05 } from './File05'
|
||||
export { default as FileSearch02 } from './FileSearch02'
|
||||
export { default as FileZip } from './FileZip'
|
||||
export { default as Folder } from './Folder'
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "16",
|
||||
"height": "16",
|
||||
"viewBox": "0 0 16 16",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "g",
|
||||
"attributes": {
|
||||
"id": "Icon"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"id": "Vector",
|
||||
"d": "M8 1C4.1325 1 1 4.1325 1 8C1 11.0975 3.00375 13.7137 5.78625 14.6413C6.13625 14.7025 6.2675 14.4925 6.2675 14.3088C6.2675 14.1425 6.25875 13.5913 6.25875 13.005C4.5 13.3288 4.045 12.5763 3.905 12.1825C3.82625 11.9812 3.485 11.36 3.1875 11.1937C2.9425 11.0625 2.5925 10.7387 3.17875 10.73C3.73 10.7212 4.12375 11.2375 4.255 11.4475C4.885 12.5062 5.89125 12.2088 6.29375 12.025C6.355 11.57 6.53875 11.2638 6.74 11.0887C5.1825 10.9137 3.555 10.31 3.555 7.6325C3.555 6.87125 3.82625 6.24125 4.2725 5.75125C4.2025 5.57625 3.9575 4.85875 4.3425 3.89625C4.3425 3.89625 4.92875 3.7125 6.2675 4.61375C6.8275 4.45625 7.4225 4.3775 8.0175 4.3775C8.6125 4.3775 9.2075 4.45625 9.7675 4.61375C11.1063 3.70375 11.6925 3.89625 11.6925 3.89625C12.0775 4.85875 11.8325 5.57625 11.7625 5.75125C12.2087 6.24125 12.48 6.8625 12.48 7.6325C12.48 10.3187 10.8438 10.9137 9.28625 11.0887C9.54 11.3075 9.75875 11.7275 9.75875 12.3837C9.75875 13.32 9.75 14.0725 9.75 14.3088C9.75 14.4925 9.88125 14.7113 10.2312 14.6413C11.6209 14.1721 12.8284 13.279 13.6839 12.0877C14.5393 10.8963 14.9996 9.46668 15 8C15 4.1325 11.8675 1 8 1Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "Github"
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './Github.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
Icon.displayName = 'Github'
|
||||
|
||||
export default Icon
|
||||
@@ -5,6 +5,7 @@ export { default as Download02 } from './Download02'
|
||||
export { default as Edit03 } from './Edit03'
|
||||
export { default as Edit04 } from './Edit04'
|
||||
export { default as Eye } from './Eye'
|
||||
export { default as Github } from './Github'
|
||||
export { default as MessageClockCircle } from './MessageClockCircle'
|
||||
export { default as PlusCircle } from './PlusCircle'
|
||||
export { default as QuestionTriangle } from './QuestionTriangle'
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import classNames from '@/utils/classnames'
|
||||
import { useSelector } from '@/context/app-context'
|
||||
|
||||
type LogoSiteProps = {
|
||||
className?: string
|
||||
@@ -10,17 +9,10 @@ type LogoSiteProps = {
|
||||
const LogoSite: FC<LogoSiteProps> = ({
|
||||
className,
|
||||
}) => {
|
||||
const { theme } = useSelector((s) => {
|
||||
return {
|
||||
theme: s.theme,
|
||||
}
|
||||
})
|
||||
|
||||
const src = theme === 'light' ? '/logo/logo-site.png' : `/logo/logo-site-${theme}.png`
|
||||
return (
|
||||
<img
|
||||
src={src}
|
||||
className={classNames('block w-auto h-10', className)}
|
||||
src={'/logo/logo.png'}
|
||||
className={classNames('block w-[22.651px] h-[24.5px]', className)}
|
||||
alt='logo'
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -1,64 +1,81 @@
|
||||
import type { FC } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
import Badge, { BadgeState } from '@/app/components/base/badge/index'
|
||||
type Option = {
|
||||
value: string
|
||||
text: string
|
||||
}
|
||||
|
||||
type TabSliderProps = {
|
||||
className?: string
|
||||
itemWidth?: number
|
||||
value: string
|
||||
onChange: (v: string) => void
|
||||
options: Option[]
|
||||
}
|
||||
|
||||
const TabSlider: FC<TabSliderProps> = ({
|
||||
className,
|
||||
itemWidth = 118,
|
||||
value,
|
||||
onChange,
|
||||
options,
|
||||
}) => {
|
||||
const currentIndex = options.findIndex(option => option.value === value)
|
||||
const current = options[currentIndex]
|
||||
const [activeIndex, setActiveIndex] = useState(options.findIndex(option => option.value === value))
|
||||
const [sliderStyle, setSliderStyle] = useState({})
|
||||
|
||||
const updateSliderStyle = (index: number) => {
|
||||
const tabElement = document.getElementById(`tab-${index}`)
|
||||
if (tabElement) {
|
||||
const { offsetLeft, offsetWidth } = tabElement
|
||||
setSliderStyle({
|
||||
transform: `translateX(${offsetLeft}px)`,
|
||||
width: `${offsetWidth}px`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const newIndex = options.findIndex(option => option.value === value)
|
||||
setActiveIndex(newIndex)
|
||||
updateSliderStyle(newIndex)
|
||||
}, [value, options])
|
||||
|
||||
return (
|
||||
<div className={cn(className, 'relative flex p-0.5 rounded-lg bg-gray-200')}>
|
||||
{
|
||||
options.map((option, index) => (
|
||||
<div
|
||||
key={option.value}
|
||||
className={`
|
||||
flex justify-center items-center h-7 text-[13px]
|
||||
font-semibold text-gray-600 rounded-[7px] cursor-pointer
|
||||
hover:bg-gray-50
|
||||
${index !== options.length - 1 && 'mr-[1px]'}
|
||||
`}
|
||||
style={{
|
||||
width: itemWidth,
|
||||
}}
|
||||
onClick={() => onChange(option.value)}
|
||||
>
|
||||
{option.text}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
{
|
||||
current && (
|
||||
<div
|
||||
className={`
|
||||
absolute flex justify-center items-center h-7 bg-white text-[13px] font-semibold text-primary-600
|
||||
border-[0.5px] border-gray-200 rounded-[7px] shadow-xs transition-transform
|
||||
`}
|
||||
style={{
|
||||
width: itemWidth,
|
||||
transform: `translateX(${currentIndex * itemWidth + 1}px)`,
|
||||
}}
|
||||
>
|
||||
{current.text}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className={cn(className, 'inline-flex p-0.5 rounded-[10px] bg-components-segmented-control-bg-normal relative items-center justify-center')}>
|
||||
<div
|
||||
className="absolute top-0.5 bottom-0.5 left-0 right-0 bg-components-panel-bg rounded-[10px] transition-transform duration-300 ease-in-out shadows-shadow-xs"
|
||||
style={sliderStyle}
|
||||
/>
|
||||
{options.map((option, index) => (
|
||||
<div
|
||||
id={`tab-${index}`}
|
||||
key={option.value}
|
||||
className={cn(
|
||||
'relative flex justify-center items-center px-2.5 py-1.5 gap-1 rounded-[10px] transition-colors duration-300 ease-in-out cursor-pointer z-10',
|
||||
'system-md-semibold',
|
||||
index === activeIndex
|
||||
? 'text-text-primary'
|
||||
: 'text-text-tertiary',
|
||||
)}
|
||||
onClick={() => {
|
||||
if (index !== activeIndex) {
|
||||
onChange(option.value)
|
||||
updateSliderStyle(index)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{option.text}
|
||||
{option.value === 'plugins'
|
||||
&& <Badge
|
||||
size='s'
|
||||
uppercase={true}
|
||||
state={BadgeState.Default}
|
||||
>
|
||||
6
|
||||
</Badge>
|
||||
}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,11 +7,13 @@ import cn from '@/utils/classnames'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
|
||||
type Props = {
|
||||
onClick: () => void
|
||||
onClick?: () => void
|
||||
isDisplayOnly?: boolean
|
||||
}
|
||||
|
||||
const HeaderBillingBtn: FC<Props> = ({
|
||||
onClick,
|
||||
isDisplayOnly = false,
|
||||
}) => {
|
||||
const { plan, enableBilling, isFetchedPlan } = useProviderContext()
|
||||
const {
|
||||
@@ -25,9 +27,9 @@ const HeaderBillingBtn: FC<Props> = ({
|
||||
})()
|
||||
const classNames = (() => {
|
||||
if (type === Plan.professional)
|
||||
return 'border-[#E0F2FE] hover:border-[#B9E6FE] bg-[#E0F2FE] text-[#026AA2]'
|
||||
return `border-[#E0F2FE] ${!isDisplayOnly ? 'hover:border-[#B9E6FE]' : ''} bg-[#E0F2FE] text-[#026AA2]`
|
||||
if (type === Plan.team)
|
||||
return 'border-[#E0EAFF] hover:border-[#C7D7FE] bg-[#E0EAFF] text-[#3538CD]'
|
||||
return `border-[#E0EAFF] ${!isDisplayOnly ? 'hover:border-[#C7D7FE]' : ''} bg-[#E0EAFF] text-[#3538CD]`
|
||||
return ''
|
||||
})()
|
||||
|
||||
@@ -35,10 +37,22 @@ const HeaderBillingBtn: FC<Props> = ({
|
||||
return null
|
||||
|
||||
if (type === Plan.sandbox)
|
||||
return <UpgradeBtn onClick={onClick} isShort />
|
||||
return <UpgradeBtn onClick={isDisplayOnly ? undefined : onClick} isShort />
|
||||
|
||||
const handleClick = () => {
|
||||
if (!isDisplayOnly && onClick)
|
||||
onClick()
|
||||
}
|
||||
|
||||
return (
|
||||
<div onClick={onClick} className={cn(classNames, 'flex items-center h-[22px] px-2 rounded-md border text-xs font-semibold uppercase cursor-pointer')}>
|
||||
<div
|
||||
onClick={handleClick}
|
||||
className={cn(
|
||||
classNames,
|
||||
'flex items-center h-[22px] px-2 rounded-md border text-xs font-semibold uppercase',
|
||||
isDisplayOnly ? 'cursor-default' : 'cursor-pointer',
|
||||
)}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -9,7 +9,6 @@ import { Menu, Transition } from '@headlessui/react'
|
||||
import Indicator from '../indicator'
|
||||
import AccountAbout from '../account-about'
|
||||
import { mailToSupport } from '../utils/util'
|
||||
import WorkplaceSelector from './workplace-selector'
|
||||
import classNames from '@/utils/classnames'
|
||||
import I18n from '@/context/i18n'
|
||||
import Avatar from '@/app/components/base/avatar'
|
||||
@@ -101,10 +100,6 @@ export default function AppSelector({ isMobile }: IAppSelector) {
|
||||
</div>
|
||||
</div>
|
||||
</Menu.Item>
|
||||
<div className='px-1 py-1'>
|
||||
<div className='mt-2 px-3 text-xs font-medium text-gray-500'>{t('common.userProfile.workspace')}</div>
|
||||
<WorkplaceSelector />
|
||||
</div>
|
||||
<div className="px-1 py-1">
|
||||
<Menu.Item>
|
||||
<Link
|
||||
|
||||
@@ -2,33 +2,20 @@ import { Fragment } from 'react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Menu, Transition } from '@headlessui/react'
|
||||
import s from './index.module.css'
|
||||
import { RiArrowDownSLine } from '@remixicon/react'
|
||||
import cn from '@/utils/classnames'
|
||||
import { switchWorkspace } from '@/service/common'
|
||||
import { useWorkspacesContext } from '@/context/workspace-context'
|
||||
import { ChevronRight } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import { Check } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import HeaderBillingBtn from '@/app/components/billing/header-billing-btn'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
|
||||
const itemClassName = `
|
||||
flex items-center px-3 py-2 h-10 cursor-pointer
|
||||
`
|
||||
const itemIconClassName = `
|
||||
shrink-0 mr-2 flex items-center justify-center w-6 h-6 bg-[#EFF4FF] rounded-md text-xs font-medium text-primary-600
|
||||
`
|
||||
const itemNameClassName = `
|
||||
grow mr-2 text-sm text-gray-700 text-left
|
||||
`
|
||||
const itemCheckClassName = `
|
||||
shrink-0 w-4 h-4 text-primary-600
|
||||
`
|
||||
|
||||
const WorkplaceSelector = () => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const { workspaces } = useWorkspacesContext()
|
||||
const { enableBilling } = useProviderContext()
|
||||
const currentWorkspace = workspaces.find(v => v.current)
|
||||
|
||||
const handleSwitchWorkspace = async (tenant_id: string) => {
|
||||
try {
|
||||
if (currentWorkspace?.id === tenant_id)
|
||||
@@ -49,13 +36,13 @@ const WorkplaceSelector = () => {
|
||||
<>
|
||||
<Menu.Button className={cn(
|
||||
`
|
||||
${itemClassName} w-full
|
||||
group hover:bg-gray-50 cursor-pointer ${open && 'bg-gray-50'} rounded-lg
|
||||
flex items-center p-0.5 gap-1.5 w-full
|
||||
group hover:bg-state-base-hover cursor-pointer ${open && 'bg-state-base-hover'} rounded-[10px]
|
||||
`,
|
||||
)}>
|
||||
<div className={itemIconClassName}>{currentWorkspace?.name[0].toLocaleUpperCase()}</div>
|
||||
<div className={`${itemNameClassName} truncate`}>{currentWorkspace?.name}</div>
|
||||
<ChevronRight className='shrink-0 w-[14px] h-[14px] text-gray-500' />
|
||||
<div className='flex items-center justify-center w-7 h-7 bg-[#EFF4FF] rounded-md text-xs font-medium text-primary-600'>{currentWorkspace?.name[0].toLocaleUpperCase()}</div>
|
||||
<div className={'truncate max-w-[80px] text-text-secondary system-sm-medium'}>{currentWorkspace?.name}</div>
|
||||
<RiArrowDownSLine className='w-4 h-4 text-text-secondary' />
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
@@ -69,19 +56,24 @@ const WorkplaceSelector = () => {
|
||||
<Menu.Items
|
||||
className={cn(
|
||||
`
|
||||
absolute top-[1px] min-w-[200px] max-h-[70vh] overflow-y-scroll z-10 bg-white border-[0.5px] border-gray-200
|
||||
divide-y divide-gray-100 origin-top-right rounded-xl
|
||||
flex w-[280px] flex-col items-start absolute left-[-15px] mt-1 rounded-xl shadows-shadow-lg
|
||||
`,
|
||||
s.popup,
|
||||
)}
|
||||
>
|
||||
<div className="px-1 py-1">
|
||||
<div className="flex flex-col p-1 pb-2 items-start self-stretch w-full rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadows-shadow-lg ">
|
||||
<div className='flex px-3 pt-1 pb-0.5 items-start self-stretch'>
|
||||
<span className='flex-1 text-text-tertiary system-xs-medium-uppercase'>{t('common.userProfile.workspace')}</span>
|
||||
</div>
|
||||
{
|
||||
workspaces.map(workspace => (
|
||||
<div className={itemClassName} key={workspace.id} onClick={() => handleSwitchWorkspace(workspace.id)}>
|
||||
<div className={itemIconClassName}>{workspace.name[0].toLocaleUpperCase()}</div>
|
||||
<div className={itemNameClassName}>{workspace.name}</div>
|
||||
{workspace.current && <Check className={itemCheckClassName} />}
|
||||
<div className='flex py-1 pl-3 pr-2 items-center gap-2 self-stretch hover:bg-state-base-hover rounded-lg' key={workspace.id} onClick={() => handleSwitchWorkspace(workspace.id)}>
|
||||
<div className='flex items-center justify-center w-7 h-7 bg-[#EFF4FF] rounded-md text-xs font-medium text-primary-600'>{workspace.name[0].toLocaleUpperCase()}</div>
|
||||
<div className='line-clamp-1 flex-grow overflow-hidden text-text-secondary text-ellipsis system-md-regular'>{workspace.name}</div>
|
||||
{enableBilling && (
|
||||
<div className='select-none'>
|
||||
<HeaderBillingBtn isDisplayOnly={true} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
.modal {
|
||||
max-width: 1024px !important;
|
||||
border-radius: 12px !important;
|
||||
margin-top: 60px;
|
||||
margin-bottom: 60px;
|
||||
padding: 0 !important;
|
||||
overflow-y: auto;
|
||||
}
|
||||
@@ -2,9 +2,8 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import {
|
||||
RiBox3Fill,
|
||||
RiBox3Line,
|
||||
RiCloseLine,
|
||||
RiBrainFill,
|
||||
RiBrainLine,
|
||||
RiColorFilterFill,
|
||||
RiColorFilterLine,
|
||||
RiDatabase2Fill,
|
||||
@@ -22,21 +21,17 @@ import LanguagePage from './language-page'
|
||||
import ApiBasedExtensionPage from './api-based-extension-page'
|
||||
import DataSourcePage from './data-source-page'
|
||||
import ModelProviderPage from './model-provider-page'
|
||||
import s from './index.module.css'
|
||||
import cn from '@/utils/classnames'
|
||||
import BillingPage from '@/app/components/billing/billing-page'
|
||||
import CustomPage from '@/app/components/custom/custom-page'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import MenuDialog from '@/app/components/header/account-setting/menu-dialog'
|
||||
import Input from '@/app/components/base/input'
|
||||
|
||||
const iconClassName = `
|
||||
w-4 h-4 ml-3 mr-2
|
||||
`
|
||||
|
||||
const scrolledClassName = `
|
||||
border-b shadow-xs bg-white/[.98]
|
||||
w-5 h-5 mr-2
|
||||
`
|
||||
|
||||
type IAccountSettingProps = {
|
||||
@@ -68,8 +63,8 @@ export default function AccountSetting({
|
||||
{
|
||||
key: 'provider',
|
||||
name: t('common.settings.provider'),
|
||||
icon: <RiBox3Line className={iconClassName} />,
|
||||
activeIcon: <RiBox3Fill className={iconClassName} />,
|
||||
icon: <RiBrainLine className={iconClassName} />,
|
||||
activeIcon: <RiBrainFill className={iconClassName} />,
|
||||
},
|
||||
{
|
||||
key: 'members',
|
||||
@@ -117,7 +112,7 @@ export default function AccountSetting({
|
||||
},
|
||||
{
|
||||
key: 'account-group',
|
||||
name: t('common.settings.accountGroup'),
|
||||
name: t('common.settings.generalGroup'),
|
||||
items: [
|
||||
{
|
||||
key: 'language',
|
||||
@@ -144,32 +139,31 @@ export default function AccountSetting({
|
||||
|
||||
const activeItem = [...menuItems[0].items, ...menuItems[1].items].find(item => item.key === activeMenu)
|
||||
|
||||
const [searchValue, setSearchValue] = useState<string>('')
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow
|
||||
onClose={() => { }}
|
||||
className={s.modal}
|
||||
wrapperClassName='pt-[60px]'
|
||||
<MenuDialog
|
||||
show
|
||||
onClose={onCancel}
|
||||
>
|
||||
<div className='flex'>
|
||||
<div className='w-[44px] sm:w-[200px] px-[1px] py-4 sm:p-4 border border-gray-100 shrink-0 sm:shrink-1 flex flex-col items-center sm:items-start'>
|
||||
<div className='mb-8 ml-0 sm:ml-2 text-sm sm:text-base font-medium leading-6 text-gray-900'>{t('common.userProfile.settings')}</div>
|
||||
<div className='mx-auto max-w-[1048px] h-[100vh] flex'>
|
||||
<div className='w-[44px] sm:w-[224px] pl-4 pr-6 border-r border-divider-burn flex flex-col'>
|
||||
<div className='mt-6 mb-8 px-3 py-2 text-text-primary title-2xl-semi-bold'>{t('common.userProfile.settings')}</div>
|
||||
<div className='w-full'>
|
||||
{
|
||||
menuItems.map(menuItem => (
|
||||
<div key={menuItem.key} className='mb-4'>
|
||||
<div key={menuItem.key} className='mb-2'>
|
||||
{!isCurrentWorkspaceDatasetOperator && (
|
||||
<div className='px-2 mb-[6px] text-[10px] sm:text-xs font-medium text-gray-500'>{menuItem.name}</div>
|
||||
<div className='py-2 pl-3 pb-1 mb-0.5 text-text-tertiary system-xs-medium-uppercase'>{menuItem.name}</div>
|
||||
)}
|
||||
<div>
|
||||
{
|
||||
menuItem.items.map(item => (
|
||||
<div
|
||||
key={item.key}
|
||||
className={`
|
||||
flex items-center h-[37px] mb-[2px] text-sm cursor-pointer rounded-lg
|
||||
${activeMenu === item.key ? 'font-semibold text-primary-600 bg-primary-50' : 'font-light text-gray-700'}
|
||||
`}
|
||||
className={cn(
|
||||
'flex items-center mb-0.5 p-1 pl-3 h-[37px] text-sm cursor-pointer rounded-lg',
|
||||
activeMenu === item.key ? 'bg-state-base-active text-components-menu-item-text-active system-sm-semibold' : 'text-components-menu-item-text system-sm-medium')}
|
||||
title={item.name}
|
||||
onClick={() => setActiveMenu(item.key)}
|
||||
>
|
||||
@@ -184,19 +178,25 @@ export default function AccountSetting({
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div ref={scrollRef} className='relative w-[824px] h-[720px] pb-4 overflow-y-auto'>
|
||||
<div className={cn('sticky top-0 px-6 py-4 flex items-center h-14 mb-4 bg-white text-base font-medium text-gray-900 z-20', scrolled && scrolledClassName)}>
|
||||
<div className='shrink-0'>{activeItem?.name}</div>
|
||||
<div ref={scrollRef} className='relative w-[824px] pb-4 bg-components-panel-bg overflow-y-auto'>
|
||||
<div className={cn('sticky top-0 mx-8 pt-[27px] pb-2 mb-[18px] flex items-center bg-components-panel-bg z-20', scrolled && 'border-b')}>
|
||||
<div className='shrink-0 text-text-primary title-2xl-semi-bold'>{activeItem?.name}</div>
|
||||
{
|
||||
activeItem?.description && (
|
||||
<div className='shrink-0 ml-2 text-xs text-gray-600'>{activeItem?.description}</div>
|
||||
)
|
||||
}
|
||||
<div className='grow flex justify-end'>
|
||||
<div className='flex items-center justify-center -mr-4 w-6 h-6 cursor-pointer' onClick={onCancel}>
|
||||
<RiCloseLine className='w-4 h-4 text-gray-400' />
|
||||
{activeItem?.key === 'provider' && (
|
||||
<div className='grow flex justify-end'>
|
||||
<Input
|
||||
showLeftIcon
|
||||
wrapperClassName='!w-[200px]'
|
||||
className='!h-8 !text-[13px]'
|
||||
onChange={e => setSearchValue(e.target.value)}
|
||||
value={searchValue}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='px-4 sm:px-8 pt-2'>
|
||||
{activeMenu === 'members' && <MembersPage />}
|
||||
@@ -209,6 +209,6 @@ export default function AccountSetting({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</MenuDialog>
|
||||
)
|
||||
}
|
||||
|
||||
72
web/app/components/header/account-setting/menu-dialog.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { Fragment, useCallback, useEffect } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import { Dialog, Transition } from '@headlessui/react'
|
||||
import Button from '@/app/components/base/button'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type DialogProps = {
|
||||
className?: string
|
||||
children: ReactNode
|
||||
show: boolean
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
const MenuDialog = ({
|
||||
className,
|
||||
children,
|
||||
show,
|
||||
onClose,
|
||||
}: DialogProps) => {
|
||||
const close = useCallback(() => onClose?.(), [onClose])
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape')
|
||||
close()
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown)
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown)
|
||||
}
|
||||
}, [close])
|
||||
|
||||
return (
|
||||
<Transition appear show={show} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-40" onClose={() => {}}>
|
||||
<div className="fixed inset-0">
|
||||
<div className="flex flex-col items-center justify-center min-h-full">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className={cn('grow relative w-full h-full p-0 overflow-hidden text-left align-middle transition-all transform bg-background-sidenav-bg backdrop-blur-md', className)}>
|
||||
<div className='absolute right-0 top-0 h-full w-1/2 bg-components-panel-bg'/>
|
||||
<div className='absolute top-6 right-6 flex flex-col items-center'>
|
||||
<Button
|
||||
variant='tertiary'
|
||||
size='large'
|
||||
className='px-2'
|
||||
onClick={close}
|
||||
>
|
||||
<RiCloseLine className='w-5 h-5' />
|
||||
</Button>
|
||||
<div className='mt-1 text-text-tertiary system-2xs-medium-uppercase'>ESC</div>
|
||||
</div>
|
||||
{children}
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition >
|
||||
)
|
||||
}
|
||||
|
||||
export default MenuDialog
|
||||
@@ -1,8 +1,16 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Link from 'next/link'
|
||||
import {
|
||||
RiAlertFill,
|
||||
RiArrowDownSLine,
|
||||
RiArrowRightUpLine,
|
||||
RiBrainLine,
|
||||
} from '@remixicon/react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import SystemModelSelector from './system-model-selector'
|
||||
import ProviderAddedCard, { UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST } from './provider-added-card'
|
||||
import ProviderCard from './provider-card'
|
||||
// import ProviderCard from './provider-card'
|
||||
import type {
|
||||
CustomConfigurationModelFixedFields,
|
||||
ModelProvider,
|
||||
@@ -17,10 +25,15 @@ import {
|
||||
useUpdateModelList,
|
||||
useUpdateModelProviders,
|
||||
} from './hooks'
|
||||
import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import ProviderCard from '@/app/components/plugins/provider-card'
|
||||
import I18n from '@/context/i18n'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { useModalContextSelector } from '@/context/modal-context'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
import { extensionDallE, modelGPT4, toolNotion } from '@/app/components/plugins/card/card-mock'
|
||||
|
||||
const ModelProviderPage = () => {
|
||||
const { t } = useTranslation()
|
||||
@@ -57,25 +70,25 @@ const ModelProviderPage = () => {
|
||||
|
||||
const handleOpenModal = (
|
||||
provider: ModelProvider,
|
||||
configurateMethod: ConfigurationMethodEnum,
|
||||
configurationMethod: ConfigurationMethodEnum,
|
||||
CustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
|
||||
) => {
|
||||
setShowModelModal({
|
||||
payload: {
|
||||
currentProvider: provider,
|
||||
currentConfigurationMethod: configurateMethod,
|
||||
currentConfigurationMethod: configurationMethod,
|
||||
currentCustomConfigurationModelFixedFields: CustomConfigurationModelFixedFields,
|
||||
},
|
||||
onSaveCallback: () => {
|
||||
updateModelProviders()
|
||||
|
||||
if (configurateMethod === ConfigurationMethodEnum.predefinedModel) {
|
||||
if (configurationMethod === ConfigurationMethodEnum.predefinedModel) {
|
||||
provider.supported_model_types.forEach((type) => {
|
||||
updateModelList(type)
|
||||
})
|
||||
}
|
||||
|
||||
if (configurateMethod === ConfigurationMethodEnum.customizableModel && provider.custom_configuration.status === CustomConfigurationStatusEnum.active) {
|
||||
if (configurationMethod === ConfigurationMethodEnum.customizableModel && provider.custom_configuration.status === CustomConfigurationStatusEnum.active) {
|
||||
eventEmitter?.emit({
|
||||
type: UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST,
|
||||
payload: provider.provider,
|
||||
@@ -88,63 +101,95 @@ const ModelProviderPage = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const [collapse, setCollapse] = useState(false)
|
||||
const { locale } = useContext(I18n)
|
||||
|
||||
// TODO #Plugin list API#
|
||||
const pluginList = [toolNotion, extensionDallE, modelGPT4]
|
||||
|
||||
return (
|
||||
<div className='relative pt-1 -mt-2'>
|
||||
<div className={`flex items-center justify-between mb-2 h-8 ${defaultModelNotConfigured && 'px-3 bg-[#FFFAEB] rounded-lg border border-[#FEF0C7]'}`}>
|
||||
{
|
||||
defaultModelNotConfigured
|
||||
? (
|
||||
<div className='flex items-center text-xs font-medium text-gray-700'>
|
||||
<AlertTriangle className='mr-1 w-3 h-3 text-[#F79009]' />
|
||||
{t('common.modelProvider.notConfigured')}
|
||||
</div>
|
||||
)
|
||||
: <div className='text-sm font-medium text-gray-800'>{t('common.modelProvider.models')}</div>
|
||||
}
|
||||
<SystemModelSelector
|
||||
textGenerationDefaultModel={textGenerationDefaultModel}
|
||||
embeddingsDefaultModel={embeddingsDefaultModel}
|
||||
rerankDefaultModel={rerankDefaultModel}
|
||||
speech2textDefaultModel={speech2textDefaultModel}
|
||||
ttsDefaultModel={ttsDefaultModel}
|
||||
/>
|
||||
<div className={cn('flex items-center mb-2')}>
|
||||
<div className='grow text-text-primary system-md-semibold'>{t('common.modelProvider.models')}</div>
|
||||
<div className={cn(
|
||||
'shrink-0 relative flex items-center justify-end gap-2 p-0.5 rounded-lg border border-transparent',
|
||||
defaultModelNotConfigured && 'pl-2 bg-components-panel-bg-blur border-components-panel-border shadow-xs',
|
||||
)}>
|
||||
{defaultModelNotConfigured && <div className='absolute top-0 bottom-0 right-0 left-0 opacity-40' style={{ background: 'linear-gradient(92deg, rgba(247, 144, 9, 0.25) 0%, rgba(255, 255, 255, 0.00) 100%)' }} />}
|
||||
{defaultModelNotConfigured && (
|
||||
<div className='flex items-center gap-1 text-text-primary system-xs-medium'>
|
||||
<RiAlertFill className='w-4 h-4 text-text-warning-secondary' />
|
||||
{t('common.modelProvider.notConfigured')}
|
||||
</div>
|
||||
)}
|
||||
<SystemModelSelector
|
||||
notConfigured={defaultModelNotConfigured}
|
||||
textGenerationDefaultModel={textGenerationDefaultModel}
|
||||
embeddingsDefaultModel={embeddingsDefaultModel}
|
||||
rerankDefaultModel={rerankDefaultModel}
|
||||
speech2textDefaultModel={speech2textDefaultModel}
|
||||
ttsDefaultModel={ttsDefaultModel}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
!!configuredProviders?.length && (
|
||||
<div className='pb-3'>
|
||||
{
|
||||
configuredProviders?.map(provider => (
|
||||
<ProviderAddedCard
|
||||
key={provider.provider}
|
||||
provider={provider}
|
||||
onOpenModal={(configurateMethod: ConfigurationMethodEnum, currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields) => handleOpenModal(provider, configurateMethod, currentCustomConfigurationModelFixedFields)}
|
||||
/>
|
||||
))
|
||||
}
|
||||
{!configuredProviders?.length && (
|
||||
<div className='mb-2 p-4 rounded-[10px]' style={{ background: 'linear-gradient(90deg, rgba(200, 206, 218, 0.20) 0%, rgba(200, 206, 218, 0.04) 100%)' }}>
|
||||
<div className='w-10 h-10 flex items-center justify-center rounded-[10px] border-[0.5px] border-components-card-border bg-components-card-bg shadow-lg backdrop-blur'>
|
||||
<RiBrainLine className='w-5 h-5 text-text-primary' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
!!notConfiguredProviders?.length && (
|
||||
<>
|
||||
<div className='flex items-center mb-2 text-xs font-semibold text-gray-500'>
|
||||
+ {t('common.modelProvider.addMoreModelProvider')}
|
||||
<span className='grow ml-3 h-[1px] bg-gradient-to-r from-[#f3f4f6]' />
|
||||
</div>
|
||||
<div className='grid grid-cols-3 gap-2'>
|
||||
{
|
||||
notConfiguredProviders?.map(provider => (
|
||||
<ProviderCard
|
||||
key={provider.provider}
|
||||
provider={provider}
|
||||
onOpenModal={(configurateMethod: ConfigurationMethodEnum) => handleOpenModal(provider, configurateMethod)}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
<div className='mt-2 text-text-secondary system-sm-medium'>{t('common.modelProvider.emptyProviderTitle')}</div>
|
||||
<div className='mt-1 text-text-tertiary system-xs-regular'>{t('common.modelProvider.emptyProviderTip')}</div>
|
||||
</div>
|
||||
)}
|
||||
{!!configuredProviders?.length && (
|
||||
<div className='relative'>
|
||||
{configuredProviders?.map(provider => (
|
||||
<ProviderAddedCard
|
||||
key={provider.provider}
|
||||
provider={provider}
|
||||
onOpenModal={(configurationMethod: ConfigurationMethodEnum, currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields) => handleOpenModal(provider, configurationMethod, currentCustomConfigurationModelFixedFields)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{false && !!notConfiguredProviders?.length && (
|
||||
<>
|
||||
<div className='flex items-center mb-2 pt-2 text-text-primary system-md-semibold'>{t('common.modelProvider.configureRequired')}</div>
|
||||
<div className='relative'>
|
||||
{notConfiguredProviders?.map(provider => (
|
||||
<ProviderAddedCard
|
||||
notConfigured
|
||||
key={provider.provider}
|
||||
provider={provider}
|
||||
onOpenModal={(configurationMethod: ConfigurationMethodEnum, currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields) => handleOpenModal(provider, configurationMethod, currentCustomConfigurationModelFixedFields)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className='mb-2'>
|
||||
<Divider className='!mt-4 h-px' />
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center gap-1 text-text-primary system-md-semibold cursor-pointer' onClick={() => setCollapse(!collapse)}>
|
||||
<RiArrowDownSLine className={cn('w-4 h-4', collapse && '-rotate-90')} />
|
||||
{t('common.modelProvider.installProvider')}
|
||||
</div>
|
||||
<div className='flex items-center mb-2 pt-2'>
|
||||
<span className='pr-1 text-text-tertiary system-sm-regular'>{t('common.modelProvider.discoverMore')}</span>
|
||||
<Link target="_blank" href="/plugins" className='inline-flex items-center system-sm-medium text-text-accent'>
|
||||
Dify Marketplace
|
||||
<RiArrowRightUpLine className='w-4 h-4' />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
{!collapse && (
|
||||
<div className='grid grid-cols-2 gap-2'>
|
||||
{pluginList.map((plugin, index) => (
|
||||
<ProviderCard key={index} installed={false} payload={plugin as any} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiEqualizer2Line } from '@remixicon/react'
|
||||
import type { ModelProvider } from '../declarations'
|
||||
import {
|
||||
ConfigurationMethodEnum,
|
||||
@@ -14,7 +15,6 @@ import PrioritySelector from './priority-selector'
|
||||
import PriorityUseTip from './priority-use-tip'
|
||||
import { UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST } from './index'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import { Settings01 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { changeModelProviderPriority } from '@/service/common'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
@@ -69,7 +69,7 @@ const CredentialPanel: FC<CredentialPanelProps> = ({
|
||||
<div className='shrink-0 relative ml-1 p-1 w-[112px] rounded-lg bg-white/[0.3] border-[0.5px] border-black/5'>
|
||||
<div className='flex items-center justify-between mb-1 pt-1 pl-2 pr-[7px] h-5 text-xs font-medium text-gray-500'>
|
||||
API-KEY
|
||||
<Indicator color={isCustomConfigured ? 'green' : 'gray'} />
|
||||
<Indicator color={isCustomConfigured ? 'green' : 'red'} />
|
||||
</div>
|
||||
<div className='flex items-center gap-0.5'>
|
||||
<Button
|
||||
@@ -77,7 +77,7 @@ const CredentialPanel: FC<CredentialPanelProps> = ({
|
||||
size='small'
|
||||
onClick={onSetup}
|
||||
>
|
||||
<Settings01 className='mr-1 w-3 h-3' />
|
||||
<RiEqualizer2Line className='mr-1 w-3.5 h-3.5' />
|
||||
{t('common.operation.setup')}
|
||||
</Button>
|
||||
{
|
||||
|
||||
@@ -2,6 +2,8 @@ import type { FC } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiArrowRightSLine,
|
||||
RiInformation2Fill,
|
||||
RiLoader2Line,
|
||||
} from '@remixicon/react'
|
||||
import type {
|
||||
@@ -21,7 +23,6 @@ import CredentialPanel from './credential-panel'
|
||||
import QuotaPanel from './quota-panel'
|
||||
import ModelList from './model-list'
|
||||
import AddModelButton from './add-model-button'
|
||||
import { ChevronDownDouble } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import { fetchModelProviderModelList } from '@/service/common'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { IS_CE_EDITION } from '@/config'
|
||||
@@ -29,10 +30,12 @@ import { useAppContext } from '@/context/app-context'
|
||||
|
||||
export const UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST = 'UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST'
|
||||
type ProviderAddedCardProps = {
|
||||
notConfigured?: boolean
|
||||
provider: ModelProvider
|
||||
onOpenModal: (configurationMethod: ConfigurationMethodEnum, currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields) => void
|
||||
}
|
||||
const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
|
||||
notConfigured,
|
||||
provider,
|
||||
onOpenModal,
|
||||
}) => {
|
||||
@@ -47,6 +50,7 @@ const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
|
||||
const hasModelList = fetched && !!modelList.length
|
||||
const { isCurrentWorkspaceManager } = useAppContext()
|
||||
const showQuota = systemConfig.enabled && [...MODEL_PROVIDER_QUOTA_GET_PAID].includes(provider.provider) && !IS_CE_EDITION
|
||||
const showCredential = configurationMethods.includes(ConfigurationMethodEnum.predefinedModel) && isCurrentWorkspaceManager
|
||||
|
||||
const getModelList = async (providerName: string) => {
|
||||
if (loading)
|
||||
@@ -105,7 +109,7 @@ const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
|
||||
)
|
||||
}
|
||||
{
|
||||
configurationMethods.includes(ConfigurationMethodEnum.predefinedModel) && isCurrentWorkspaceManager && (
|
||||
showCredential && (
|
||||
<CredentialPanel
|
||||
onSetup={() => onOpenModal(ConfigurationMethodEnum.predefinedModel)}
|
||||
provider={provider}
|
||||
@@ -116,34 +120,45 @@ const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
|
||||
{
|
||||
collapsed && (
|
||||
<div className='group flex items-center justify-between pl-2 py-1.5 pr-[11px] border-t border-t-black/5 bg-white/30 text-xs font-medium text-gray-500'>
|
||||
<div className='group-hover:hidden pl-1 pr-1.5 h-6 leading-6'>
|
||||
{
|
||||
hasModelList
|
||||
? t('common.modelProvider.modelsNum', { num: modelList.length })
|
||||
: t('common.modelProvider.showModels')
|
||||
}
|
||||
</div>
|
||||
<div
|
||||
className='hidden group-hover:flex items-center pl-1 pr-1.5 h-6 rounded-lg hover:bg-white cursor-pointer'
|
||||
onClick={handleOpenModelList}
|
||||
>
|
||||
<ChevronDownDouble className='mr-0.5 w-3 h-3' />
|
||||
{
|
||||
hasModelList
|
||||
? t('common.modelProvider.showModelsNum', { num: modelList.length })
|
||||
: t('common.modelProvider.showModels')
|
||||
}
|
||||
{
|
||||
loading && (
|
||||
<RiLoader2Line className='ml-0.5 animate-spin w-3 h-3' />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{(showQuota || !notConfigured) && (
|
||||
<>
|
||||
<div className='group-hover:hidden flex items-center pl-1 pr-1.5 h-6 leading-6'>
|
||||
{
|
||||
hasModelList
|
||||
? t('common.modelProvider.modelsNum', { num: modelList.length })
|
||||
: t('common.modelProvider.showModels')
|
||||
}
|
||||
{!loading && <RiArrowRightSLine className='w-4 h-4' />}
|
||||
</div>
|
||||
<div
|
||||
className='hidden group-hover:flex items-center pl-1 pr-1.5 h-6 rounded-lg hover:bg-white cursor-pointer'
|
||||
onClick={handleOpenModelList}
|
||||
>
|
||||
{
|
||||
hasModelList
|
||||
? t('common.modelProvider.showModelsNum', { num: modelList.length })
|
||||
: t('common.modelProvider.showModels')
|
||||
}
|
||||
{!loading && <RiArrowRightSLine className='w-4 h-4' />}
|
||||
{
|
||||
loading && (
|
||||
<RiLoader2Line className='ml-0.5 animate-spin w-3 h-3' />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{!showQuota && notConfigured && (
|
||||
<div className='flex items-center pl-1 pr-1.5 h-6'>
|
||||
<RiInformation2Fill className='mr-1 w-4 h-4 text-text-accent' />
|
||||
<span>{t('common.modelProvider.configureTip')}</span>
|
||||
</div>
|
||||
)}
|
||||
{
|
||||
configurationMethods.includes(ConfigurationMethodEnum.customizableModel) && isCurrentWorkspaceManager && (
|
||||
<AddModelButton
|
||||
onClick={() => onOpenModal(ConfigurationMethodEnum.customizableModel)}
|
||||
className='hidden group-hover:flex group-hover:text-primary-600'
|
||||
className='flex'
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import type { FC } from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiArrowRightSLine,
|
||||
} from '@remixicon/react'
|
||||
import type {
|
||||
CustomConfigurationModelFixedFields,
|
||||
ModelItem,
|
||||
@@ -12,7 +15,6 @@ import {
|
||||
// import Tab from './tab'
|
||||
import AddModelButton from './add-model-button'
|
||||
import ModelListItem from './model-list-item'
|
||||
import { ChevronDownDouble } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import { useModalContextSelector } from '@/context/modal-context'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
|
||||
@@ -51,15 +53,16 @@ const ModelList: FC<ModelListProps> = ({
|
||||
<div className='py-1 bg-white rounded-lg'>
|
||||
<div className='flex items-center pl-1 pr-[3px]'>
|
||||
<span className='group shrink-0 flex items-center mr-2'>
|
||||
<span className='group-hover:hidden pl-1 pr-1.5 h-6 leading-6 text-xs font-medium text-gray-500'>
|
||||
<span className='group-hover:hidden inline-flex pl-1 pr-1.5 h-6 leading-6 text-xs font-medium text-gray-500'>
|
||||
{t('common.modelProvider.modelsNum', { num: models.length })}
|
||||
<RiArrowRightSLine className='mr-0.5 w-4 h-4 rotate-90' />
|
||||
</span>
|
||||
<span
|
||||
className='hidden group-hover:inline-flex items-center pl-1 pr-1.5 h-6 text-xs font-medium text-gray-500 bg-gray-50 cursor-pointer rounded-lg'
|
||||
onClick={() => onCollapse()}
|
||||
>
|
||||
<ChevronDownDouble className='mr-0.5 w-3 h-3 rotate-180' />
|
||||
{t('common.modelProvider.collapse')}
|
||||
{t('common.modelProvider.modelsNum', { num: models.length })}
|
||||
<RiArrowRightSLine className='mr-0.5 w-4 h-4 rotate-90' />
|
||||
</span>
|
||||
</span>
|
||||
{/* {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { FC } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiEqualizer2Line } from '@remixicon/react'
|
||||
import ModelSelector from '../model-selector'
|
||||
import {
|
||||
useModelList,
|
||||
@@ -13,7 +14,6 @@ import type {
|
||||
} from '../declarations'
|
||||
import { ModelTypeEnum } from '../declarations'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { Settings01 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
@@ -31,6 +31,7 @@ type SystemModelSelectorProps = {
|
||||
rerankDefaultModel: DefaultModelResponse | undefined
|
||||
speech2textDefaultModel: DefaultModelResponse | undefined
|
||||
ttsDefaultModel: DefaultModelResponse | undefined
|
||||
notConfigured: boolean
|
||||
}
|
||||
const SystemModel: FC<SystemModelSelectorProps> = ({
|
||||
textGenerationDefaultModel,
|
||||
@@ -38,6 +39,7 @@ const SystemModel: FC<SystemModelSelectorProps> = ({
|
||||
rerankDefaultModel,
|
||||
speech2textDefaultModel,
|
||||
ttsDefaultModel,
|
||||
notConfigured,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useToastContext()
|
||||
@@ -128,14 +130,13 @@ const SystemModel: FC<SystemModelSelectorProps> = ({
|
||||
}}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}>
|
||||
<div className={`
|
||||
flex items-center px-2 h-6 text-xs text-gray-700 cursor-pointer bg-white rounded-md border-[0.5px] border-gray-200 shadow-xs
|
||||
hover:bg-gray-100 hover:shadow-none
|
||||
${open && 'bg-gray-100 shadow-none'}
|
||||
`}>
|
||||
<Settings01 className='mr-1 w-3 h-3 text-gray-500' />
|
||||
<Button
|
||||
variant={notConfigured ? 'primary' : 'secondary'}
|
||||
size='small'
|
||||
>
|
||||
<RiEqualizer2Line className='mr-1 w-3.5 h-3.5' />
|
||||
{t('common.modelProvider.systemModelSettings')}
|
||||
</div>
|
||||
</Button>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-50'>
|
||||
<div className='pt-4 w-[360px] rounded-xl border-[0.5px] border-black/5 bg-white shadow-xl'>
|
||||
|
||||