Commit 22d39ec7 authored by crazywoola's avatar crazywoola

Merge branch 'fix/2559-upload-powered-by-brand-image-not-showing-up' into deploy/dev

parents c4352960 0f53fc86
...@@ -124,19 +124,13 @@ class AppListApi(Resource): ...@@ -124,19 +124,13 @@ class AppListApi(Resource):
available_models_names = [f'{model.provider.provider}.{model.model}' for model in available_models] available_models_names = [f'{model.provider.provider}.{model.model}' for model in available_models]
provider_model = f"{model_config_dict['model']['provider']}.{model_config_dict['model']['name']}" provider_model = f"{model_config_dict['model']['provider']}.{model_config_dict['model']['name']}"
if provider_model not in available_models_names: if provider_model not in available_models_names:
model_manager = ModelManager() if not default_model_entity:
model_instance = model_manager.get_default_model_instance(
tenant_id=current_user.current_tenant_id,
model_type=ModelType.LLM
)
if not model_instance:
raise ProviderNotInitializeError( raise ProviderNotInitializeError(
"No Default System Reasoning Model available. Please configure " "No Default System Reasoning Model available. Please configure "
"in the Settings -> Model Provider.") "in the Settings -> Model Provider.")
else: else:
model_config_dict["model"]["provider"] = model_instance.provider model_config_dict["model"]["provider"] = default_model_entity.provider
model_config_dict["model"]["name"] = model_instance.model model_config_dict["model"]["name"] = default_model_entity.model
model_configuration = AppModelConfigService.validate_configuration( model_configuration = AppModelConfigService.validate_configuration(
tenant_id=current_user.current_tenant_id, tenant_id=current_user.current_tenant_id,
......
...@@ -178,7 +178,8 @@ class DataSourceNotionApi(Resource): ...@@ -178,7 +178,8 @@ class DataSourceNotionApi(Resource):
notion_workspace_id=workspace_id, notion_workspace_id=workspace_id,
notion_obj_id=page_id, notion_obj_id=page_id,
notion_page_type=page_type, notion_page_type=page_type,
notion_access_token=data_source_binding.access_token notion_access_token=data_source_binding.access_token,
tenant_id=current_user.current_tenant_id
) )
text_docs = extractor.extract() text_docs = extractor.extract()
...@@ -208,7 +209,8 @@ class DataSourceNotionApi(Resource): ...@@ -208,7 +209,8 @@ class DataSourceNotionApi(Resource):
notion_info={ notion_info={
"notion_workspace_id": workspace_id, "notion_workspace_id": workspace_id,
"notion_obj_id": page['page_id'], "notion_obj_id": page['page_id'],
"notion_page_type": page['type'] "notion_page_type": page['type'],
"tenant_id": current_user.current_tenant_id
}, },
document_model=args['doc_form'] document_model=args['doc_form']
) )
......
...@@ -298,7 +298,8 @@ class DatasetIndexingEstimateApi(Resource): ...@@ -298,7 +298,8 @@ class DatasetIndexingEstimateApi(Resource):
notion_info={ notion_info={
"notion_workspace_id": workspace_id, "notion_workspace_id": workspace_id,
"notion_obj_id": page['page_id'], "notion_obj_id": page['page_id'],
"notion_page_type": page['type'] "notion_page_type": page['type'],
"tenant_id": current_user.current_tenant_id
}, },
document_model=args['doc_form'] document_model=args['doc_form']
) )
......
...@@ -455,7 +455,8 @@ class DocumentBatchIndexingEstimateApi(DocumentResource): ...@@ -455,7 +455,8 @@ class DocumentBatchIndexingEstimateApi(DocumentResource):
notion_info={ notion_info={
"notion_workspace_id": data_source_info['notion_workspace_id'], "notion_workspace_id": data_source_info['notion_workspace_id'],
"notion_obj_id": data_source_info['notion_page_id'], "notion_obj_id": data_source_info['notion_page_id'],
"notion_page_type": data_source_info['type'] "notion_page_type": data_source_info['type'],
"tenant_id": current_user.current_tenant_id
}, },
document_model=document.doc_form document_model=document.doc_form
) )
......
...@@ -3,12 +3,12 @@ import logging ...@@ -3,12 +3,12 @@ import logging
from typing import Optional, cast from typing import Optional, cast
import numpy as np import numpy as np
from langchain.embeddings.base import Embeddings
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from core.model_manager import ModelInstance from core.model_manager import ModelInstance
from core.model_runtime.entities.model_entities import ModelPropertyKey from core.model_runtime.entities.model_entities import ModelPropertyKey
from core.model_runtime.model_providers.__base.text_embedding_model import TextEmbeddingModel from core.model_runtime.model_providers.__base.text_embedding_model import TextEmbeddingModel
from core.rag.datasource.entity.embedding import Embeddings
from extensions.ext_database import db from extensions.ext_database import db
from extensions.ext_redis import redis_client from extensions.ext_redis import redis_client
from libs import helper from libs import helper
......
...@@ -59,7 +59,7 @@ class AnnotationReplyFeature: ...@@ -59,7 +59,7 @@ class AnnotationReplyFeature:
documents = vector.search_by_vector( documents = vector.search_by_vector(
query=query, query=query,
k=1, top_k=1,
score_threshold=score_threshold, score_threshold=score_threshold,
filter={ filter={
'group_id': [dataset.id] 'group_id': [dataset.id]
......
...@@ -606,36 +606,42 @@ class BaseAssistantApplicationRunner(AppRunner): ...@@ -606,36 +606,42 @@ class BaseAssistantApplicationRunner(AppRunner):
for message in messages: for message in messages:
result.append(UserPromptMessage(content=message.query)) result.append(UserPromptMessage(content=message.query))
agent_thoughts: list[MessageAgentThought] = message.agent_thoughts agent_thoughts: list[MessageAgentThought] = message.agent_thoughts
for agent_thought in agent_thoughts: if agent_thoughts:
tools = agent_thought.tool for agent_thought in agent_thoughts:
if tools: tools = agent_thought.tool
tools = tools.split(';') if tools:
tool_calls: list[AssistantPromptMessage.ToolCall] = [] tools = tools.split(';')
tool_call_response: list[ToolPromptMessage] = [] tool_calls: list[AssistantPromptMessage.ToolCall] = []
tool_inputs = json.loads(agent_thought.tool_input) tool_call_response: list[ToolPromptMessage] = []
for tool in tools: tool_inputs = json.loads(agent_thought.tool_input)
# generate a uuid for tool call for tool in tools:
tool_call_id = str(uuid.uuid4()) # generate a uuid for tool call
tool_calls.append(AssistantPromptMessage.ToolCall( tool_call_id = str(uuid.uuid4())
id=tool_call_id, tool_calls.append(AssistantPromptMessage.ToolCall(
type='function', id=tool_call_id,
function=AssistantPromptMessage.ToolCall.ToolCallFunction( type='function',
function=AssistantPromptMessage.ToolCall.ToolCallFunction(
name=tool,
arguments=json.dumps(tool_inputs.get(tool, {})),
)
))
tool_call_response.append(ToolPromptMessage(
content=agent_thought.observation,
name=tool, name=tool,
arguments=json.dumps(tool_inputs.get(tool, {})), tool_call_id=tool_call_id,
) ))
))
tool_call_response.append(ToolPromptMessage( result.extend([
content=agent_thought.observation, AssistantPromptMessage(
name=tool, content=agent_thought.thought,
tool_call_id=tool_call_id, tool_calls=tool_calls,
)) ),
*tool_call_response
result.extend([ ])
AssistantPromptMessage( if not tools:
content=agent_thought.thought, result.append(AssistantPromptMessage(content=agent_thought.thought))
tool_calls=tool_calls, else:
), if message.answer:
*tool_call_response result.append(AssistantPromptMessage(content=message.answer))
])
return result return result
\ No newline at end of file
...@@ -154,7 +154,7 @@ class AssistantCotApplicationRunner(BaseAssistantApplicationRunner): ...@@ -154,7 +154,7 @@ class AssistantCotApplicationRunner(BaseAssistantApplicationRunner):
thought='', thought='',
action_str='', action_str='',
observation='', observation='',
action=None action=None,
) )
# publish agent thought if it's first iteration # publish agent thought if it's first iteration
...@@ -469,7 +469,7 @@ class AssistantCotApplicationRunner(BaseAssistantApplicationRunner): ...@@ -469,7 +469,7 @@ class AssistantCotApplicationRunner(BaseAssistantApplicationRunner):
thought=message.content, thought=message.content,
action_str='', action_str='',
action=None, action=None,
observation=None observation=None,
) )
if message.tool_calls: if message.tool_calls:
try: try:
...@@ -484,7 +484,7 @@ class AssistantCotApplicationRunner(BaseAssistantApplicationRunner): ...@@ -484,7 +484,7 @@ class AssistantCotApplicationRunner(BaseAssistantApplicationRunner):
elif isinstance(message, ToolPromptMessage): elif isinstance(message, ToolPromptMessage):
if current_scratchpad: if current_scratchpad:
current_scratchpad.observation = message.content current_scratchpad.observation = message.content
return agent_scratchpad return agent_scratchpad
def _check_cot_prompt_messages(self, mode: Literal["completion", "chat"], def _check_cot_prompt_messages(self, mode: Literal["completion", "chat"],
...@@ -607,6 +607,13 @@ class AssistantCotApplicationRunner(BaseAssistantApplicationRunner): ...@@ -607,6 +607,13 @@ class AssistantCotApplicationRunner(BaseAssistantApplicationRunner):
prompt_message.content = system_message prompt_message.content = system_message
overridden = True overridden = True
break break
# convert tool prompt messages to user prompt messages
for idx, prompt_message in enumerate(prompt_messages):
if isinstance(prompt_message, ToolPromptMessage):
prompt_messages[idx] = UserPromptMessage(
content=prompt_message.content
)
if not overridden: if not overridden:
prompt_messages.insert(0, SystemPromptMessage( prompt_messages.insert(0, SystemPromptMessage(
......
...@@ -366,7 +366,8 @@ class IndexingRunner: ...@@ -366,7 +366,8 @@ class IndexingRunner:
"notion_workspace_id": data_source_info['notion_workspace_id'], "notion_workspace_id": data_source_info['notion_workspace_id'],
"notion_obj_id": data_source_info['notion_page_id'], "notion_obj_id": data_source_info['notion_page_id'],
"notion_page_type": data_source_info['type'], "notion_page_type": data_source_info['type'],
"document": dataset_document "document": dataset_document,
"tenant_id": dataset_document.tenant_id
}, },
document_model=dataset_document.doc_form document_model=dataset_document.doc_form
) )
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
- bedrock - bedrock
- togetherai - togetherai
- ollama - ollama
- mistralai
- replicate - replicate
- huggingface_hub - huggingface_hub
- zhipuai - zhipuai
......
- open-mistral-7b
- open-mixtral-8x7b
- mistral-small-latest
- mistral-medium-latest
- mistral-large-latest
from collections.abc import Generator
from typing import Optional, Union
from core.model_runtime.entities.llm_entities import LLMResult
from core.model_runtime.entities.message_entities import PromptMessage, PromptMessageTool
from core.model_runtime.model_providers.openai_api_compatible.llm.llm import OAIAPICompatLargeLanguageModel
class MistralAILargeLanguageModel(OAIAPICompatLargeLanguageModel):
def _invoke(self, model: str, credentials: dict,
prompt_messages: list[PromptMessage], model_parameters: dict,
tools: Optional[list[PromptMessageTool]] = None, stop: Optional[list[str]] = None,
stream: bool = True, user: Optional[str] = None) \
-> Union[LLMResult, Generator]:
self._add_custom_parameters(credentials)
# mistral dose not support user/stop arguments
stop = []
user = None
return super()._invoke(model, credentials, prompt_messages, model_parameters, tools, stop, stream, user)
def validate_credentials(self, model: str, credentials: dict) -> None:
self._add_custom_parameters(credentials)
super().validate_credentials(model, credentials)
@staticmethod
def _add_custom_parameters(credentials: dict) -> None:
credentials['mode'] = 'chat'
credentials['endpoint_url'] = 'https://api.mistral.ai/v1'
model: mistral-large-latest
label:
zh_Hans: mistral-large-latest
en_US: mistral-large-latest
model_type: llm
features:
- agent-thought
model_properties:
context_size: 32000
parameter_rules:
- name: temperature
use_template: temperature
default: 0.7
min: 0
max: 1
- name: top_p
use_template: top_p
default: 1
min: 0
max: 1
- name: max_tokens
use_template: max_tokens
default: 1024
min: 1
max: 8000
- name: safe_prompt
defulat: false
type: boolean
help:
en_US: Whether to inject a safety prompt before all conversations.
zh_Hans: 是否开启提示词审查
label:
en_US: SafePrompt
zh_Hans: 提示词审查
- name: random_seed
type: int
help:
en_US: The seed to use for random sampling. If set, different calls will generate deterministic results.
zh_Hans: 当开启随机数种子以后,你可以通过指定一个固定的种子来使得回答结果更加稳定
label:
en_US: RandomSeed
zh_Hans: 随机数种子
default: 0
min: 0
max: 2147483647
pricing:
input: '0.008'
output: '0.024'
unit: '0.001'
currency: USD
model: mistral-medium-latest
label:
zh_Hans: mistral-medium-latest
en_US: mistral-medium-latest
model_type: llm
features:
- agent-thought
model_properties:
context_size: 32000
parameter_rules:
- name: temperature
use_template: temperature
default: 0.7
min: 0
max: 1
- name: top_p
use_template: top_p
default: 1
min: 0
max: 1
- name: max_tokens
use_template: max_tokens
default: 1024
min: 1
max: 8000
- name: safe_prompt
defulat: false
type: boolean
help:
en_US: Whether to inject a safety prompt before all conversations.
zh_Hans: 是否开启提示词审查
label:
en_US: SafePrompt
zh_Hans: 提示词审查
- name: random_seed
type: int
help:
en_US: The seed to use for random sampling. If set, different calls will generate deterministic results.
zh_Hans: 当开启随机数种子以后,你可以通过指定一个固定的种子来使得回答结果更加稳定
label:
en_US: RandomSeed
zh_Hans: 随机数种子
default: 0
min: 0
max: 2147483647
pricing:
input: '0.0027'
output: '0.0081'
unit: '0.001'
currency: USD
model: mistral-small-latest
label:
zh_Hans: mistral-small-latest
en_US: mistral-small-latest
model_type: llm
features:
- agent-thought
model_properties:
context_size: 32000
parameter_rules:
- name: temperature
use_template: temperature
default: 0.7
min: 0
max: 1
- name: top_p
use_template: top_p
default: 1
min: 0
max: 1
- name: max_tokens
use_template: max_tokens
default: 1024
min: 1
max: 8000
- name: safe_prompt
defulat: false
type: boolean
help:
en_US: Whether to inject a safety prompt before all conversations.
zh_Hans: 是否开启提示词审查
label:
en_US: SafePrompt
zh_Hans: 提示词审查
- name: random_seed
type: int
help:
en_US: The seed to use for random sampling. If set, different calls will generate deterministic results.
zh_Hans: 当开启随机数种子以后,你可以通过指定一个固定的种子来使得回答结果更加稳定
label:
en_US: RandomSeed
zh_Hans: 随机数种子
default: 0
min: 0
max: 2147483647
pricing:
input: '0.002'
output: '0.006'
unit: '0.001'
currency: USD
model: open-mistral-7b
label:
zh_Hans: open-mistral-7b
en_US: open-mistral-7b
model_type: llm
features:
- agent-thought
model_properties:
context_size: 8000
parameter_rules:
- name: temperature
use_template: temperature
default: 0.7
min: 0
max: 1
- name: top_p
use_template: top_p
default: 1
min: 0
max: 1
- name: max_tokens
use_template: max_tokens
default: 1024
min: 1
max: 2048
- name: safe_prompt
defulat: false
type: boolean
help:
en_US: Whether to inject a safety prompt before all conversations.
zh_Hans: 是否开启提示词审查
label:
en_US: SafePrompt
zh_Hans: 提示词审查
- name: random_seed
type: int
help:
en_US: The seed to use for random sampling. If set, different calls will generate deterministic results.
zh_Hans: 当开启随机数种子以后,你可以通过指定一个固定的种子来使得回答结果更加稳定
label:
en_US: RandomSeed
zh_Hans: 随机数种子
default: 0
min: 0
max: 2147483647
pricing:
input: '0.00025'
output: '0.00025'
unit: '0.001'
currency: USD
model: open-mixtral-8x7b
label:
zh_Hans: open-mixtral-8x7b
en_US: open-mixtral-8x7b
model_type: llm
features:
- agent-thought
model_properties:
context_size: 32000
parameter_rules:
- name: temperature
use_template: temperature
default: 0.7
min: 0
max: 1
- name: top_p
use_template: top_p
default: 1
min: 0
max: 1
- name: max_tokens
use_template: max_tokens
default: 1024
min: 1
max: 8000
- name: safe_prompt
defulat: false
type: boolean
help:
en_US: Whether to inject a safety prompt before all conversations.
zh_Hans: 是否开启提示词审查
label:
en_US: SafePrompt
zh_Hans: 提示词审查
- name: random_seed
type: int
help:
en_US: The seed to use for random sampling. If set, different calls will generate deterministic results.
zh_Hans: 当开启随机数种子以后,你可以通过指定一个固定的种子来使得回答结果更加稳定
label:
en_US: RandomSeed
zh_Hans: 随机数种子
default: 0
min: 0
max: 2147483647
pricing:
input: '0.0007'
output: '0.0007'
unit: '0.001'
currency: USD
import logging
from core.model_runtime.entities.model_entities import ModelType
from core.model_runtime.errors.validate import CredentialsValidateFailedError
from core.model_runtime.model_providers.__base.model_provider import ModelProvider
logger = logging.getLogger(__name__)
class MistralAIProvider(ModelProvider):
def validate_provider_credentials(self, credentials: dict) -> None:
"""
Validate provider credentials
if validate failed, raise exception
:param credentials: provider credentials, credentials form defined in `provider_credential_schema`.
"""
try:
model_instance = self.get_model_instance(ModelType.LLM)
model_instance.validate_credentials(
model='open-mistral-7b',
credentials=credentials
)
except CredentialsValidateFailedError as ex:
raise ex
except Exception as ex:
logger.exception(f'{self.get_provider_schema().provider} credentials validate failed')
raise ex
provider: mistralai
label:
en_US: MistralAI
description:
en_US: Models provided by MistralAI, such as open-mistral-7b and mistral-large-latest.
zh_Hans: MistralAI 提供的模型,例如 open-mistral-7b 和 mistral-large-latest。
icon_small:
en_US: icon_s_en.png
icon_large:
en_US: icon_l_en.png
background: "#FFFFFF"
help:
title:
en_US: Get your API Key from MistralAI
zh_Hans: 从 MistralAI 获取 API Key
url:
en_US: https://console.mistral.ai/api-keys/
supported_model_types:
- llm
configurate_methods:
- predefined-model
provider_credential_schema:
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
...@@ -13,6 +13,7 @@ class MoonshotLargeLanguageModel(OAIAPICompatLargeLanguageModel): ...@@ -13,6 +13,7 @@ class MoonshotLargeLanguageModel(OAIAPICompatLargeLanguageModel):
stream: bool = True, user: Optional[str] = None) \ stream: bool = True, user: Optional[str] = None) \
-> Union[LLMResult, Generator]: -> Union[LLMResult, Generator]:
self._add_custom_parameters(credentials) self._add_custom_parameters(credentials)
user = user[:32] if user else None
return super()._invoke(model, credentials, prompt_messages, model_parameters, tools, stop, stream, user) return super()._invoke(model, credentials, prompt_messages, model_parameters, tools, stop, stream, user)
def validate_credentials(self, model: str, credentials: dict) -> None: def validate_credentials(self, model: str, credentials: dict) -> None:
......
...@@ -39,7 +39,8 @@ class RetrievalService: ...@@ -39,7 +39,8 @@ class RetrievalService:
'flask_app': current_app._get_current_object(), 'flask_app': current_app._get_current_object(),
'dataset_id': dataset_id, 'dataset_id': dataset_id,
'query': query, 'query': query,
'top_k': top_k 'top_k': top_k,
'all_documents': all_documents
}) })
threads.append(keyword_thread) threads.append(keyword_thread)
keyword_thread.start() keyword_thread.start()
...@@ -100,7 +101,7 @@ class RetrievalService: ...@@ -100,7 +101,7 @@ class RetrievalService:
documents = keyword.search( documents = keyword.search(
query, query,
k=top_k top_k=top_k
) )
all_documents.extend(documents) all_documents.extend(documents)
...@@ -120,7 +121,7 @@ class RetrievalService: ...@@ -120,7 +121,7 @@ class RetrievalService:
documents = vector.search_by_vector( documents = vector.search_by_vector(
query, query,
search_type='similarity_score_threshold', search_type='similarity_score_threshold',
k=top_k, top_k=top_k,
score_threshold=score_threshold, score_threshold=score_threshold,
filter={ filter={
'group_id': [dataset.id] 'group_id': [dataset.id]
......
...@@ -7,4 +7,4 @@ class Field(Enum): ...@@ -7,4 +7,4 @@ class Field(Enum):
GROUP_KEY = "group_id" GROUP_KEY = "group_id"
VECTOR = "vector" VECTOR = "vector"
TEXT_KEY = "text" TEXT_KEY = "text"
PRIMARY_KEY = " id" PRIMARY_KEY = "id"
...@@ -124,7 +124,12 @@ class MilvusVector(BaseVector): ...@@ -124,7 +124,12 @@ class MilvusVector(BaseVector):
def delete_by_ids(self, doc_ids: list[str]) -> None: def delete_by_ids(self, doc_ids: list[str]) -> None:
self._client.delete(collection_name=self._collection_name, pks=doc_ids) result = self._client.query(collection_name=self._collection_name,
filter=f'metadata["doc_id"] in {doc_ids}',
output_fields=["id"])
if result:
ids = [item["id"] for item in result]
self._client.delete(collection_name=self._collection_name, pks=ids)
def delete(self) -> None: def delete(self) -> None:
alias = uuid4().hex alias = uuid4().hex
......
...@@ -12,6 +12,7 @@ class NotionInfo(BaseModel): ...@@ -12,6 +12,7 @@ class NotionInfo(BaseModel):
notion_obj_id: str notion_obj_id: str
notion_page_type: str notion_page_type: str
document: Document = None document: Document = None
tenant_id: str
class Config: class Config:
arbitrary_types_allowed = True arbitrary_types_allowed = True
......
...@@ -132,7 +132,8 @@ class ExtractProcessor: ...@@ -132,7 +132,8 @@ class ExtractProcessor:
notion_workspace_id=extract_setting.notion_info.notion_workspace_id, notion_workspace_id=extract_setting.notion_info.notion_workspace_id,
notion_obj_id=extract_setting.notion_info.notion_obj_id, notion_obj_id=extract_setting.notion_info.notion_obj_id,
notion_page_type=extract_setting.notion_info.notion_page_type, notion_page_type=extract_setting.notion_info.notion_page_type,
document_model=extract_setting.notion_info.document document_model=extract_setting.notion_info.document,
tenant_id=extract_setting.notion_info.tenant_id,
) )
return extractor.extract() return extractor.extract()
else: else:
......
"""Abstract interface for document loader implementations.""" """Abstract interface for document loader implementations."""
from typing import Optional from bs4 import BeautifulSoup
from core.rag.extractor.extractor_base import BaseExtractor from core.rag.extractor.extractor_base import BaseExtractor
from core.rag.extractor.helpers import detect_file_encodings
from core.rag.models.document import Document from core.rag.models.document import Document
class HtmlExtractor(BaseExtractor): class HtmlExtractor(BaseExtractor):
"""Load html files.
"""
Load html files.
Args: Args:
...@@ -15,57 +16,19 @@ class HtmlExtractor(BaseExtractor): ...@@ -15,57 +16,19 @@ class HtmlExtractor(BaseExtractor):
""" """
def __init__( def __init__(
self, self,
file_path: str, file_path: str
encoding: Optional[str] = None,
autodetect_encoding: bool = False,
source_column: Optional[str] = None,
csv_args: Optional[dict] = None,
): ):
"""Initialize with file path.""" """Initialize with file path."""
self._file_path = file_path self._file_path = file_path
self._encoding = encoding
self._autodetect_encoding = autodetect_encoding
self.source_column = source_column
self.csv_args = csv_args or {}
def extract(self) -> list[Document]: def extract(self) -> list[Document]:
"""Load data into document objects.""" return [Document(page_content=self._load_as_text())]
try:
with open(self._file_path, newline="", encoding=self._encoding) as csvfile:
docs = self._read_from_file(csvfile)
except UnicodeDecodeError as e:
if self._autodetect_encoding:
detected_encodings = detect_file_encodings(self._file_path)
for encoding in detected_encodings:
try:
with open(self._file_path, newline="", encoding=encoding.encoding) as csvfile:
docs = self._read_from_file(csvfile)
break
except UnicodeDecodeError:
continue
else:
raise RuntimeError(f"Error loading {self._file_path}") from e
return docs
def _read_from_file(self, csvfile) -> list[Document]: def _load_as_text(self) -> str:
docs = [] with open(self._file_path, "rb") as fp:
csv_reader = csv.DictReader(csvfile, **self.csv_args) # type: ignore soup = BeautifulSoup(fp, 'html.parser')
for i, row in enumerate(csv_reader): text = soup.get_text()
content = "\n".join(f"{k.strip()}: {v.strip()}" for k, v in row.items()) text = text.strip() if text else ''
try:
source = (
row[self.source_column]
if self.source_column is not None
else ''
)
except KeyError:
raise ValueError(
f"Source column '{self.source_column}' not found in CSV file."
)
metadata = {"source": source, "row": i}
doc = Document(page_content=content, metadata=metadata)
docs.append(doc)
return docs return text
\ No newline at end of file
...@@ -30,8 +30,10 @@ class NotionExtractor(BaseExtractor): ...@@ -30,8 +30,10 @@ class NotionExtractor(BaseExtractor):
notion_workspace_id: str, notion_workspace_id: str,
notion_obj_id: str, notion_obj_id: str,
notion_page_type: str, notion_page_type: str,
tenant_id: str,
document_model: Optional[DocumentModel] = None, document_model: Optional[DocumentModel] = None,
notion_access_token: Optional[str] = None notion_access_token: Optional[str] = None,
): ):
self._notion_access_token = None self._notion_access_token = None
self._document_model = document_model self._document_model = document_model
......
...@@ -200,7 +200,7 @@ class ApiTool(Tool): ...@@ -200,7 +200,7 @@ class ApiTool(Tool):
# replace path parameters # replace path parameters
for name, value in path_params.items(): for name, value in path_params.items():
url = url.replace(f'{{{name}}}', value) url = url.replace(f'{{{name}}}', f'{value}')
# parse http body data if needed, for GET/HEAD/OPTIONS/TRACE, the body is ignored # parse http body data if needed, for GET/HEAD/OPTIONS/TRACE, the body is ignored
if 'Content-Type' in headers: if 'Content-Type' in headers:
......
import re
import uuid
from json import loads as json_loads from json import loads as json_loads
from requests import get from requests import get
...@@ -46,7 +48,7 @@ class ApiBasedToolSchemaParser: ...@@ -46,7 +48,7 @@ class ApiBasedToolSchemaParser:
parameters = [] parameters = []
if 'parameters' in interface['operation']: if 'parameters' in interface['operation']:
for parameter in interface['operation']['parameters']: for parameter in interface['operation']['parameters']:
parameters.append(ToolParameter( tool_parameter = ToolParameter(
name=parameter['name'], name=parameter['name'],
label=I18nObject( label=I18nObject(
en_US=parameter['name'], en_US=parameter['name'],
...@@ -61,7 +63,14 @@ class ApiBasedToolSchemaParser: ...@@ -61,7 +63,14 @@ class ApiBasedToolSchemaParser:
form=ToolParameter.ToolParameterForm.LLM, form=ToolParameter.ToolParameterForm.LLM,
llm_description=parameter.get('description'), llm_description=parameter.get('description'),
default=parameter['schema']['default'] if 'schema' in parameter and 'default' in parameter['schema'] else None, default=parameter['schema']['default'] if 'schema' in parameter and 'default' in parameter['schema'] else None,
)) )
# check if there is a type
typ = ApiBasedToolSchemaParser._get_tool_parameter_type(parameter)
if typ:
tool_parameter.type = typ
parameters.append(tool_parameter)
# create tool bundle # create tool bundle
# check if there is a request body # check if there is a request body
if 'requestBody' in interface['operation']: if 'requestBody' in interface['operation']:
...@@ -80,13 +89,14 @@ class ApiBasedToolSchemaParser: ...@@ -80,13 +89,14 @@ class ApiBasedToolSchemaParser:
root = root[ref] root = root[ref]
# overwrite the content # overwrite the content
interface['operation']['requestBody']['content'][content_type]['schema'] = root interface['operation']['requestBody']['content'][content_type]['schema'] = root
# parse body parameters # parse body parameters
if 'schema' in interface['operation']['requestBody']['content'][content_type]: if 'schema' in interface['operation']['requestBody']['content'][content_type]:
body_schema = interface['operation']['requestBody']['content'][content_type]['schema'] body_schema = interface['operation']['requestBody']['content'][content_type]['schema']
required = body_schema['required'] if 'required' in body_schema else [] required = body_schema['required'] if 'required' in body_schema else []
properties = body_schema['properties'] if 'properties' in body_schema else {} properties = body_schema['properties'] if 'properties' in body_schema else {}
for name, property in properties.items(): for name, property in properties.items():
parameters.append(ToolParameter( tool = ToolParameter(
name=name, name=name,
label=I18nObject( label=I18nObject(
en_US=name, en_US=name,
...@@ -101,7 +111,14 @@ class ApiBasedToolSchemaParser: ...@@ -101,7 +111,14 @@ class ApiBasedToolSchemaParser:
form=ToolParameter.ToolParameterForm.LLM, form=ToolParameter.ToolParameterForm.LLM,
llm_description=property['description'] if 'description' in property else '', llm_description=property['description'] if 'description' in property else '',
default=property['default'] if 'default' in property else None, default=property['default'] if 'default' in property else None,
)) )
# check if there is a type
typ = ApiBasedToolSchemaParser._get_tool_parameter_type(property)
if typ:
tool.type = typ
parameters.append(tool)
# check if parameters is duplicated # check if parameters is duplicated
parameters_count = {} parameters_count = {}
...@@ -119,7 +136,11 @@ class ApiBasedToolSchemaParser: ...@@ -119,7 +136,11 @@ class ApiBasedToolSchemaParser:
path = interface['path'] path = interface['path']
if interface['path'].startswith('/'): if interface['path'].startswith('/'):
path = interface['path'][1:] path = interface['path'][1:]
path = path.replace('/', '_') # remove special characters like / to ensure the operation id is valid ^[a-zA-Z0-9_-]{1,64}$
path = re.sub(r'[^a-zA-Z0-9_-]', '', path)
if not path:
path = str(uuid.uuid4())
interface['operation']['operationId'] = f'{path}_{interface["method"]}' interface['operation']['operationId'] = f'{path}_{interface["method"]}'
bundles.append(ApiBasedToolBundle( bundles.append(ApiBasedToolBundle(
...@@ -134,7 +155,23 @@ class ApiBasedToolSchemaParser: ...@@ -134,7 +155,23 @@ class ApiBasedToolSchemaParser:
)) ))
return bundles return bundles
@staticmethod
def _get_tool_parameter_type(parameter: dict) -> ToolParameter.ToolParameterType:
parameter = parameter or {}
typ = None
if 'type' in parameter:
typ = parameter['type']
elif 'schema' in parameter and 'type' in parameter['schema']:
typ = parameter['schema']['type']
if typ == 'integer' or typ == 'number':
return ToolParameter.ToolParameterType.NUMBER
elif typ == 'boolean':
return ToolParameter.ToolParameterType.BOOLEAN
elif typ == 'string':
return ToolParameter.ToolParameterType.STRING
@staticmethod @staticmethod
def parse_openapi_yaml_to_tool_bundle(yaml: str, extra_info: dict = None, warning: dict = None) -> list[ApiBasedToolBundle]: def parse_openapi_yaml_to_tool_bundle(yaml: str, extra_info: dict = None, warning: dict = None) -> list[ApiBasedToolBundle]:
""" """
......
...@@ -133,8 +133,9 @@ class HitTestingService: ...@@ -133,8 +133,9 @@ class HitTestingService:
if embedding_length <= 1: if embedding_length <= 1:
return [{'x': 0, 'y': 0}] return [{'x': 0, 'y': 0}]
concatenate_data = np.array(embeddings).reshape(embedding_length, -1) noise = np.random.normal(0, 1e-4, np.array(embeddings).shape)
# concatenate_data = np.concatenate(embeddings) concatenate_data = np.array(embeddings) + noise
concatenate_data = concatenate_data.reshape(embedding_length, -1)
perplexity = embedding_length / 2 + 1 perplexity = embedding_length / 2 + 1
if perplexity >= embedding_length: if perplexity >= embedding_length:
......
from flask import current_app
from flask_login import current_user from flask_login import current_user
from extensions.ext_database import db from extensions.ext_database import db
...@@ -31,7 +33,17 @@ class WorkspaceService: ...@@ -31,7 +33,17 @@ class WorkspaceService:
can_replace_logo = FeatureService.get_features(tenant_info['id']).can_replace_logo can_replace_logo = FeatureService.get_features(tenant_info['id']).can_replace_logo
if can_replace_logo and TenantService.has_roles(tenant, [TenantAccountJoinRole.OWNER, TenantAccountJoinRole.ADMIN]): if can_replace_logo and TenantService.has_roles(tenant,
[TenantAccountJoinRole.OWNER, TenantAccountJoinRole.ADMIN]):
base_url = current_app.config.get('FILES_URL')
replace_webapp_logo = f'{base_url}/files/workspaces/{tenant.id}/webapp-logo' if tenant.custom_config_dict.get('replace_webapp_logo') else None
remove_webapp_brand = tenant.custom_config_dict.get('remove_webapp_brand', False)
# Make sure the custom_config_dict is updated with the new values
tenant.custom_config_dict.update({
'remove_webapp_brand': remove_webapp_brand,
'replace_webapp_logo': replace_webapp_logo,
})
tenant_info['custom_config'] = tenant.custom_config_dict tenant_info['custom_config'] = tenant.custom_config_dict
return tenant_info return tenant_info
...@@ -58,7 +58,8 @@ def document_indexing_sync_task(dataset_id: str, document_id: str): ...@@ -58,7 +58,8 @@ def document_indexing_sync_task(dataset_id: str, document_id: str):
notion_workspace_id=workspace_id, notion_workspace_id=workspace_id,
notion_obj_id=page_id, notion_obj_id=page_id,
notion_page_type=page_type, notion_page_type=page_type,
notion_access_token=data_source_binding.access_token notion_access_token=data_source_binding.access_token,
tenant_id=document.tenant_id
) )
last_edited_time = loader.get_notion_last_edited_time() last_edited_time = loader.get_notion_last_edited_time()
......
...@@ -3,13 +3,16 @@ import React from 'react' ...@@ -3,13 +3,16 @@ import React from 'react'
import Spinner from '../spinner' import Spinner from '../spinner'
export type IButtonProps = { export type IButtonProps = {
type?: string /**
* The style of the button
*/
type?: 'primary' | 'warning' | (string & {})
className?: string className?: string
disabled?: boolean disabled?: boolean
loading?: boolean loading?: boolean
tabIndex?: number tabIndex?: number
children: React.ReactNode children: React.ReactNode
onClick?: MouseEventHandler<HTMLDivElement> onClick?: MouseEventHandler<HTMLButtonElement>
} }
const Button: FC<IButtonProps> = ({ const Button: FC<IButtonProps> = ({
...@@ -35,15 +38,16 @@ const Button: FC<IButtonProps> = ({ ...@@ -35,15 +38,16 @@ const Button: FC<IButtonProps> = ({
} }
return ( return (
<div <button
className={`btn ${style} ${className && className}`} className={`btn ${style} ${className && className}`}
tabIndex={tabIndex} tabIndex={tabIndex}
onClick={disabled ? undefined : onClick} disabled={disabled}
onClick={onClick}
> >
{children} {children}
{/* Spinner is hidden when loading is false */} {/* Spinner is hidden when loading is false */}
<Spinner loading={loading} className='!text-white !h-3 !w-3 !border-2 !ml-1' /> <Spinner loading={loading} className='!text-white !h-3 !w-3 !border-2 !ml-1' />
</div> </button>
) )
} }
......
import type { FC } from 'react' import type { FC } from 'react'
import classNames from 'classnames'
type LogoSiteProps = { type LogoSiteProps = {
className?: string className?: string
} }
const LogoSite: FC<LogoSiteProps> = ({ const LogoSite: FC<LogoSiteProps> = ({
className, className,
}) => { }) => {
return ( return (
<img <img
src='/logo/logo-site.png' src='/logo/logo-site.png'
className={`block w-auto h-10 ${className}`} className={classNames('block w-auto h-10', className)}
alt='logo' alt='logo'
/> />
) )
......
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import React, { useEffect } from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import useSWR from 'swr'
import PlanComp from '../plan' import PlanComp from '../plan'
import { ReceiptList } from '../../base/icons/src/vender/line/financeAndECommerce' import { ReceiptList } from '../../base/icons/src/vender/line/financeAndECommerce'
import { LinkExternal01 } from '../../base/icons/src/vender/line/general' import { LinkExternal01 } from '../../base/icons/src/vender/line/general'
...@@ -12,17 +13,11 @@ import { useProviderContext } from '@/context/provider-context' ...@@ -12,17 +13,11 @@ import { useProviderContext } from '@/context/provider-context'
const Billing: FC = () => { const Billing: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { isCurrentWorkspaceManager } = useAppContext() const { isCurrentWorkspaceManager } = useAppContext()
const [billingUrl, setBillingUrl] = React.useState('')
const { enableBilling } = useProviderContext() const { enableBilling } = useProviderContext()
const { data: billingUrl } = useSWR(
useEffect(() => { (!enableBilling || !isCurrentWorkspaceManager) ? null : ['/billing/invoices'],
if (!enableBilling || !isCurrentWorkspaceManager) () => fetchBillingUrl().then(data => data.url),
return )
(async () => {
const { url } = await fetchBillingUrl()
setBillingUrl(url)
})()
}, [isCurrentWorkspaceManager])
return ( return (
<div> <div>
...@@ -39,4 +34,5 @@ const Billing: FC = () => { ...@@ -39,4 +34,5 @@ const Billing: FC = () => {
</div> </div>
) )
} }
export default React.memo(Billing) export default React.memo(Billing)
...@@ -16,8 +16,6 @@ import { ...@@ -16,8 +16,6 @@ import {
updateCurrentWorkspace, updateCurrentWorkspace,
} from '@/service/common' } from '@/service/common'
import { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
import { API_PREFIX } from '@/config'
import { getPurifyHref } from '@/utils'
const ALLOW_FILE_EXTENSIONS = ['svg', 'png'] const ALLOW_FILE_EXTENSIONS = ['svg', 'png']
...@@ -123,7 +121,7 @@ const CustomWebAppBrand = () => { ...@@ -123,7 +121,7 @@ const CustomWebAppBrand = () => {
POWERED BY POWERED BY
{ {
webappLogo webappLogo
? <img key={webappLogo} src={`${getPurifyHref(API_PREFIX.slice(0, -12))}/files/workspaces/${currentWorkspace.id}/webapp-logo`} alt='logo' className='ml-2 block w-auto h-5' /> ? <img key={webappLogo} src={webappLogo} alt='logo' className='ml-2 block w-auto h-5' />
: <LogoSite className='ml-2 !h-5' /> : <LogoSite className='ml-2 !h-5' />
} }
</div> </div>
......
...@@ -289,9 +289,9 @@ The text generation application offers non-session support and is ideal for tran ...@@ -289,9 +289,9 @@ The text generation application offers non-session support and is ideal for tran
</Col> </Col>
<Col sticky> <Col sticky>
### Request Example ### Request Example
<CodeGroup title="Request" tag="POST" label="/completion-messages/:task_id/stop" targetCode={`curl -X POST 'https://cloud.dify.ai/v1/completion-messages/:task_id/stop' \\\n-H 'Authorization: Bearer {api_key}' \\\n-H 'Content-Type: application/json' \\\n--data-raw '{ "user": "abc-123"}'`}> <CodeGroup title="Request" tag="POST" label="/completion-messages/:task_id/stop" targetCode={`curl -X POST '${props.appDetail.api_base_url}/v1/completion-messages/:task_id/stop' \\\n-H 'Authorization: Bearer {api_key}' \\\n-H 'Content-Type: application/json' \\\n--data-raw '{ "user": "abc-123"}'`}>
```bash {{ title: 'cURL' }} ```bash {{ title: 'cURL' }}
curl -X POST 'https://cloud.dify.ai/v1/completion-messages/:task_id/stop' \ curl -X POST '${props.appDetail.api_base_url}/v1/completion-messages/:task_id/stop' \
-H 'Authorization: Bearer {api_key}' \ -H 'Authorization: Bearer {api_key}' \
-H 'Content-Type: application/json' \ -H 'Content-Type: application/json' \
--data-raw '{ --data-raw '{
......
...@@ -266,9 +266,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' ...@@ -266,9 +266,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
- `result` (string) 固定返回 success - `result` (string) 固定返回 success
</Col> </Col>
<Col sticky> <Col sticky>
<CodeGroup title="Request" tag="POST" label="/completion-messages/:task_id/stop" targetCode={`curl -X POST 'https://cloud.dify.ai/v1/completion-messages/:task_id/stop' \\\n-H 'Authorization: Bearer {api_key}' \\\n-H 'Content-Type: application/json' \\\n--data-raw '{ "user": "abc-123"}'`}> <CodeGroup title="Request" tag="POST" label="/completion-messages/:task_id/stop" targetCode={`curl -X POST '${props.appDetail.api_base_url}/v1/completion-messages/:task_id/stop' \\\n-H 'Authorization: Bearer {api_key}' \\\n-H 'Content-Type: application/json' \\\n--data-raw '{ "user": "abc-123"}'`}>
```bash {{ title: 'cURL' }} ```bash {{ title: 'cURL' }}
curl -X POST 'https://cloud.dify.ai/v1/completion-messages/:task_id/stop' \ curl -X POST '${props.appDetail.api_base_url}/v1/completion-messages/:task_id/stop' \
-H 'Authorization: Bearer {api_key}' \ -H 'Authorization: Bearer {api_key}' \
-H 'Content-Type: application/json' \ -H 'Content-Type: application/json' \
--data-raw '{ --data-raw '{
......
...@@ -344,9 +344,9 @@ Chat applications support session persistence, allowing previous chat history to ...@@ -344,9 +344,9 @@ Chat applications support session persistence, allowing previous chat history to
</Col> </Col>
<Col sticky> <Col sticky>
### Request Example ### Request Example
<CodeGroup title="Request" tag="POST" label="/chat-messages/:task_id/stop" targetCode={`curl -X POST 'https://cloud.dify.ai/v1/chat-messages/:task_id/stop' \\\n-H 'Authorization: Bearer {api_key}' \\\n-H 'Content-Type: application/json' \\\n--data-raw '{"user": "abc-123"}'`}> <CodeGroup title="Request" tag="POST" label="/chat-messages/:task_id/stop" targetCode={`curl -X POST '${props.appDetail.api_base_url}/v1/chat-messages/:task_id/stop' \\\n-H 'Authorization: Bearer {api_key}' \\\n-H 'Content-Type: application/json' \\\n--data-raw '{"user": "abc-123"}'`}>
```bash {{ title: 'cURL' }} ```bash {{ title: 'cURL' }}
curl -X POST 'https://cloud.dify.ai/v1/chat-messages/:task_id/stop' \ curl -X POST '${props.appDetail.api_base_url}/v1/chat-messages/:task_id/stop' \
-H 'Authorization: Bearer {api_key}' \ -H 'Authorization: Bearer {api_key}' \
-H 'Content-Type: application/json' \ -H 'Content-Type: application/json' \
--data-raw '{ --data-raw '{
...@@ -1025,9 +1025,9 @@ Chat applications support session persistence, allowing previous chat history to ...@@ -1025,9 +1025,9 @@ Chat applications support session persistence, allowing previous chat history to
- (string) url of icon - (string) url of icon
</Col> </Col>
<Col> <Col>
<CodeGroup title="Request" tag="POST" label="/meta" targetCode={`curl -X GET 'https://cloud.dify.ai/v1/meta?user=abc-123' \\\n-H 'Authorization: Bearer {api_key}'`}> <CodeGroup title="Request" tag="POST" label="/meta" targetCode={`curl -X GET '${props.appDetail.api_base_url}/v1/meta?user=abc-123' \\\n-H 'Authorization: Bearer {api_key}'`}>
```bash {{ title: 'cURL' }} ```bash {{ title: 'cURL' }}
curl -X GET 'https://cloud.dify.ai/v1/meta?user=abc-123' \ curl -X GET '${props.appDetail.api_base_url}/v1/meta?user=abc-123' \
-H 'Authorization: Bearer {api_key}' -H 'Authorization: Bearer {api_key}'
``` ```
......
...@@ -360,9 +360,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' ...@@ -360,9 +360,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
- `result` (string) 固定返回 success - `result` (string) 固定返回 success
</Col> </Col>
<Col sticky> <Col sticky>
<CodeGroup title="Request" tag="POST" label="/chat-messages/:task_id/stop" targetCode={`curl -X POST 'https://cloud.dify.ai/v1/chat-messages/:task_id/stop' \\\n-H 'Authorization: Bearer {api_key}' \\\n-H 'Content-Type: application/json' \\\n--data-raw '{ "user": "abc-123"}'`}> <CodeGroup title="Request" tag="POST" label="/chat-messages/:task_id/stop" targetCode={`curl -X POST '${props.appDetail.api_base_url}/v1/chat-messages/:task_id/stop' \\\n-H 'Authorization: Bearer {api_key}' \\\n-H 'Content-Type: application/json' \\\n--data-raw '{ "user": "abc-123"}'`}>
```bash {{ title: 'cURL' }} ```bash {{ title: 'cURL' }}
curl -X POST 'https://cloud.dify.ai/v1/chat-messages/:task_id/stop' \ curl -X POST '${props.appDetail.api_base_url}/v1/chat-messages/:task_id/stop' \
-H 'Authorization: Bearer {api_key}' \ -H 'Authorization: Bearer {api_key}' \
-H 'Content-Type: application/json' \ -H 'Content-Type: application/json' \
--data-raw '{ --data-raw '{
...@@ -1022,9 +1022,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' ...@@ -1022,9 +1022,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
- (string) 图标URL - (string) 图标URL
</Col> </Col>
<Col> <Col>
<CodeGroup title="Request" tag="POST" label="/meta" targetCode={`curl -X GET 'https://cloud.dify.ai/v1/meta?user=abc-123' \\\n-H 'Authorization: Bearer {api_key}'`}> <CodeGroup title="Request" tag="POST" label="/meta" targetCode={`curl -X GET '${props.appDetail.api_base_url}/v1/meta?user=abc-123' \\\n-H 'Authorization: Bearer {api_key}'`}>
```bash {{ title: 'cURL' }} ```bash {{ title: 'cURL' }}
curl -X GET 'https://cloud.dify.ai/v1/meta?user=abc-123' \ curl -X GET '${props.appDetail.api_base_url}/v1/meta?user=abc-123' \
-H 'Authorization: Bearer {api_key}' -H 'Authorization: Bearer {api_key}'
``` ```
......
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import React, { useEffect } from 'react' import React from 'react'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector' import { useContext } from 'use-context-selector'
import useSWR from 'swr'
import Toast from '../../base/toast' import Toast from '../../base/toast'
import s from './style.module.css' import s from './style.module.css'
import ExploreContext from '@/context/explore-context' import ExploreContext from '@/context/explore-context'
import type { App, AppCategory } from '@/models/explore' import type { App } from '@/models/explore'
import Category from '@/app/components/explore/category' import Category from '@/app/components/explore/category'
import AppCard from '@/app/components/explore/app-card' import AppCard from '@/app/components/explore/app-card'
import { fetchAppDetail, fetchAppList } from '@/service/explore' import { fetchAppDetail, fetchAppList } from '@/service/explore'
...@@ -25,34 +26,44 @@ const Apps: FC = () => { ...@@ -25,34 +26,44 @@ const Apps: FC = () => {
const { isCurrentWorkspaceManager } = useAppContext() const { isCurrentWorkspaceManager } = useAppContext()
const router = useRouter() const router = useRouter()
const { hasEditPermission } = useContext(ExploreContext) const { hasEditPermission } = useContext(ExploreContext)
const allCategoriesEn = t('explore.apps.allCategories', { lng: 'en' })
const [currCategory, setCurrCategory] = useTabSearchParams({ const [currCategory, setCurrCategory] = useTabSearchParams({
defaultTab: '', defaultTab: allCategoriesEn,
}) })
const [allList, setAllList] = React.useState<App[]>([]) const {
const [isLoaded, setIsLoaded] = React.useState(false) data: { categories, allList },
isLoading,
} = useSWR(
['/explore/apps'],
() =>
fetchAppList().then(({ categories, recommended_apps }) => ({
categories,
allList: recommended_apps.sort((a, b) => a.position - b.position),
})),
{
fallbackData: {
categories: [],
allList: [],
},
},
)
const currList = (() => { const currList = (() => {
if (currCategory === '') if (currCategory === allCategoriesEn)
return allList return allList
return allList.filter(item => item.category === currCategory) return allList.filter(item => item.category === currCategory)
})() })()
const [categories, setCategories] = React.useState<AppCategory[]>([])
useEffect(() => {
(async () => {
const { categories, recommended_apps }: any = await fetchAppList()
const sortedRecommendedApps = [...recommended_apps]
sortedRecommendedApps.sort((a, b) => a.position - b.position) // position from small to big
setCategories(categories)
setAllList(sortedRecommendedApps)
setIsLoaded(true)
})()
}, [])
const [currApp, setCurrApp] = React.useState<App | null>(null) const [currApp, setCurrApp] = React.useState<App | null>(null)
const [isShowCreateModal, setIsShowCreateModal] = React.useState(false) const [isShowCreateModal, setIsShowCreateModal] = React.useState(false)
const onCreate: CreateAppModalProps['onConfirm'] = async ({ name, icon, icon_background }) => { const onCreate: CreateAppModalProps['onConfirm'] = async ({
const { app_model_config: model_config } = await fetchAppDetail(currApp?.app.id as string) name,
icon,
icon_background,
}) => {
const { app_model_config: model_config } = await fetchAppDetail(
currApp?.app.id as string,
)
try { try {
const app = await createApp({ const app = await createApp({
...@@ -68,36 +79,45 @@ const Apps: FC = () => { ...@@ -68,36 +79,45 @@ const Apps: FC = () => {
message: t('app.newApp.appCreated'), message: t('app.newApp.appCreated'),
}) })
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1') localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
router.push(`/app/${app.id}/${isCurrentWorkspaceManager ? 'configuration' : 'overview'}`) router.push(
`/app/${app.id}/${
isCurrentWorkspaceManager ? 'configuration' : 'overview'
}`,
)
} }
catch (e) { catch (e) {
Toast.notify({ type: 'error', message: t('app.newApp.appCreateFailed') }) Toast.notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
} }
} }
if (!isLoaded) { if (!isLoading) {
return ( return (
<div className='flex h-full items-center'> <div className="flex h-full items-center">
<Loading type='area' /> <Loading type="area" />
</div> </div>
) )
} }
return ( return (
<div className='h-full flex flex-col border-l border-gray-200'> <div className="h-full flex flex-col border-l border-gray-200">
<div className='shrink-0 pt-6 px-12'> <div className="shrink-0 pt-6 px-12">
<div className={`mb-1 ${s.textGradient} text-xl font-semibold`}>{t('explore.apps.title')}</div> <div className={`mb-1 ${s.textGradient} text-xl font-semibold`}>
<div className='text-gray-500 text-sm'>{t('explore.apps.description')}</div> {t('explore.apps.title')}
</div>
<div className="text-gray-500 text-sm">
{t('explore.apps.description')}
</div>
</div> </div>
<Category <Category
className='mt-6 px-12' className="mt-6 px-12"
list={categories} list={categories}
value={currCategory} value={currCategory}
onChange={setCurrCategory} onChange={setCurrCategory}
/> />
<div className='relative flex flex-1 mt-6 pb-6 flex-col overflow-auto bg-gray-100 shrink-0 grow'> <div className="relative flex flex-1 mt-6 pb-6 flex-col overflow-auto bg-gray-100 shrink-0 grow">
<nav <nav
className={`${s.appList} grid content-start gap-4 px-6 sm:px-12 shrink-0`}> className={`${s.appList} grid content-start gap-4 px-6 sm:px-12 shrink-0`}
>
{currList.map(app => ( {currList.map(app => (
<AppCard <AppCard
key={app.app_id} key={app.app_id}
......
...@@ -138,16 +138,12 @@ export default function AccountSetting({ ...@@ -138,16 +138,12 @@ export default function AccountSetting({
] ]
const scrollRef = useRef<HTMLDivElement>(null) const scrollRef = useRef<HTMLDivElement>(null)
const [scrolled, setScrolled] = useState(false) const [scrolled, setScrolled] = useState(false)
const scrollHandle = (e: Event) => {
if ((e.target as HTMLDivElement).scrollTop > 0)
setScrolled(true)
else
setScrolled(false)
}
useEffect(() => { useEffect(() => {
const targetElement = scrollRef.current const targetElement = scrollRef.current
const scrollHandle = (e: Event) => {
const userScrolled = (e.target as HTMLDivElement).scrollTop > 0
setScrolled(userScrolled)
}
targetElement?.addEventListener('scroll', scrollHandle) targetElement?.addEventListener('scroll', scrollHandle)
return () => { return () => {
targetElement?.removeEventListener('scroll', scrollHandle) targetElement?.removeEventListener('scroll', scrollHandle)
......
...@@ -54,7 +54,7 @@ const Header = () => { ...@@ -54,7 +54,7 @@ const Header = () => {
</div>} </div>}
{!isMobile && <> {!isMobile && <>
<Link href="/apps" className='flex items-center mr-4'> <Link href="/apps" className='flex items-center mr-4'>
<LogoSite /> <LogoSite className='object-contain' />
</Link> </Link>
<GithubStar /> <GithubStar />
</>} </>}
......
...@@ -16,7 +16,7 @@ export type App = { ...@@ -16,7 +16,7 @@ export type App = {
app_id: string app_id: string
description: string description: string
copyright: string copyright: string
privacy_policy: string privacy_policy: string | null
category: AppCategory category: AppCategory
position: number position: number
is_listed: boolean is_listed: boolean
......
import { del, get, patch, post } from './base' import { del, get, patch, post } from './base'
import type { App, AppCategory } from '@/models/explore'
export const fetchAppList = () => { export const fetchAppList = () => {
return get('/explore/apps') return get<{
categories: AppCategory[]
recommended_apps: App[]
}>('/explore/apps')
} }
export const fetchAppDetail = (id: string): Promise<any> => { export const fetchAppDetail = (id: string): Promise<any> => {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment