Compare commits

...

33 Commits

Author SHA1 Message Date
zxhlyh
f4154fb7d9 fix: tool info 2025-02-25 12:49:52 +08:00
zxhlyh
b0a804e22f fix: gemini model info 2025-02-25 10:29:51 +08:00
Zhang Xudong
ce545274a6 refactor(tool-engine): Optimize tool engine response handling (#14216)
Co-authored-by: xudong2.zhang <xudong2.zhang@zkh.com>
Co-authored-by: crazywoola <427733928@qq.com>
2025-02-25 09:58:43 +08:00
KVOJJJin
aa6c951e8c chore: compatible with es5 (#14268) 2025-02-25 09:46:54 +08:00
feiyang_deepnova
c4f4dfc3fb Fixed: The use of default parameters in API interfaces (#14138) 2025-02-25 09:43:36 +08:00
Rhys
548f6ef2b6 fix: incorrect score in the chroma vector (#14273) 2025-02-25 09:40:22 +08:00
L8ng
b15ff4eb8c fix: datasets documents update-by-file api missing assign field 'indexing_technique' internally (#14243) 2025-02-24 20:14:36 +08:00
Onelevenvy
7790214620 chore: Fix typos in simplified and traditional Chinese in i18n files (#14228) 2025-02-24 11:32:09 +08:00
Wu Tianwei
3942e45cab fix: update refresh logic for plugin list to avoid redundant request and fix model provider list update issue in settings (#14152) 2025-02-24 10:49:43 +08:00
Wu Tianwei
2ace9ae4e4 fix: add responsive layout for file uploader in datasets (#14159) 2025-02-23 19:35:10 +08:00
Obada Khalili
5ac0ef6253 Retain previous page's search params (#14176) 2025-02-23 13:47:03 +08:00
Bowen Liang
f552667312 chore: Simplify the unchanged overrided method in VariableAggregatorNode (#14175) 2025-02-23 13:46:48 +08:00
Vincent G
5669a18bd8 fix: typo in README_FR (#14179) (#14180) 2025-02-22 09:28:03 +08:00
E Chen
a97d73ab05 Update iteration_node.py to fix #14098: Token count increases exponen… (#14100) 2025-02-21 09:15:46 +08:00
Wu Tianwei
252d2c425b feat(docker): add PM2_INSTANCES variable and update entrypoint script… (#14083) 2025-02-20 17:30:19 +08:00
zxhlyh
09fc4bba61 fix: agent tools setting (#14097) 2025-02-20 17:20:23 +08:00
wellCh4n
79d4db8541 fix: support selecting yaml extension when importing dsl file (#14088) 2025-02-20 15:40:43 +08:00
Wu Tianwei
9c42626772 fix: fix chunk and segment detail components (#14002) 2025-02-20 15:13:43 +08:00
NFish
bbfe83c86b Fix/upgrade btn show logic (#14072) 2025-02-20 14:31:56 +08:00
Joel
55aa4e424a fix: quota less than zero show error (#14080) 2025-02-20 14:04:13 +08:00
w4-jinhyeonkim
8015f5c0c5 Fix ko-KR/workflow.ts (#14075) 2025-02-20 13:49:04 +08:00
zxhlyh
f3fe14863d fix: marketplace search url (#14061) 2025-02-20 10:00:04 +08:00
Junjie.M
d96c368660 fix: frontend for <think> tags conflicting with original <details> tags (#14039) 2025-02-19 22:04:11 +08:00
jiangbo721
3f34b8b0d1 fix: remove duplicated code (#14047)
Co-authored-by: 刘江波 <jiangbo721@163.com>
2025-02-19 22:03:24 +08:00
-LAN-
6a58ea9e56 chore(docker): Back to version 0.15.3 (#14042)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2025-02-19 20:09:14 +08:00
Yeuoly
23888398d1 refactor: Simplify plugin and provider ID generation logic and deduplicate plugin_ids (#14041) 2025-02-19 20:05:01 +08:00
Yeuoly
bfbc5eb91e fix: Prevent plugin installation with non-existent plugin IDs (#14038) 2025-02-19 19:37:09 +08:00
Jyong
98b0d4169e fix workspace model list (#14033) 2025-02-19 18:18:25 +08:00
wind
356cd271b2 fix: MessageLogModal closed unexpectedly (#13617) 2025-02-19 17:32:54 +08:00
Mitsunao Miyamoto
baf7561cf8 fix: There is an error in the Japanese in the Japanese documentation (#14026) 2025-02-19 17:29:11 +08:00
kenwoodjw
b09f22961c fix #13573 (#13843)
Signed-off-by: kenwoodjw <blackxin55+@gmail.com>
2025-02-19 17:28:32 +08:00
mobiusy
f3ad3a5dfd fix: "today" statistics are not displayed in chart view (#13854) 2025-02-19 17:28:10 +08:00
SToneX
ee49d321c5 fix(workflow): correct edge type mapping typo (#13988) 2025-02-19 17:27:26 +08:00
55 changed files with 254 additions and 218 deletions

View File

@@ -55,7 +55,7 @@
Dify est une plateforme de développement d'applications LLM open source. Son interface intuitive combine un flux de travail d'IA, un pipeline RAG, des capacités d'agent, une gestion de modèles, des fonctionnalités d'observabilité, et plus encore, vous permettant de passer rapidement du prototype à la production. Voici une liste des fonctionnalités principales:
</br> </br>
**1. Flux de travail**:
**1. Flux de travail** :
Construisez et testez des flux de travail d'IA puissants sur un canevas visuel, en utilisant toutes les fonctionnalités suivantes et plus encore.
@@ -63,27 +63,25 @@ Dify est une plateforme de développement d'applications LLM open source. Son in
**2. Prise en charge complète des modèles**:
**2. Prise en charge complète des modèles** :
Intégration transparente avec des centaines de LLM propriétaires / open source provenant de dizaines de fournisseurs d'inférence et de solutions auto-hébergées, couvrant GPT, Mistral, Llama3, et tous les modèles compatibles avec l'API OpenAI. Une liste complète des fournisseurs de modèles pris en charge se trouve [ici](https://docs.dify.ai/getting-started/readme/model-providers).
![providers-v5](https://github.com/langgenius/dify/assets/13230914/5a17bdbe-097a-4100-8363-40255b70f6e3)
**3. IDE de prompt**:
**3. IDE de prompt** :
Interface intuitive pour créer des prompts, comparer les performances des modèles et ajouter des fonctionnalités supplémentaires telles que la synthèse vocale à une application basée sur des chats.
**4. Pipeline RAG**:
**4. Pipeline RAG** :
Des capacités RAG étendues qui couvrent tout, de l'ingestion de documents à la récupération, avec un support prêt à l'emploi pour l'extraction de texte à partir de PDF, PPT et autres formats de document courants.
**5. Capac
ités d'agent**:
**5. Capacités d'agent** :
Vous pouvez définir des agents basés sur l'appel de fonction LLM ou ReAct, et ajouter des outils pré-construits ou personnalisés pour l'agent. Dify fournit plus de 50 outils intégrés pour les agents d'IA, tels que la recherche Google, DALL·E, Stable Diffusion et WolframAlpha.
**6. LLMOps**:
**6. LLMOps** :
Surveillez et analysez les journaux d'application et les performances au fil du temps. Vous pouvez continuellement améliorer les prompts, les ensembles de données et les modèles en fonction des données de production et des annotations.
**7. Backend-as-a-Service**:
**7. Backend-as-a-Service** :
Toutes les offres de Dify sont accompagnées d'API correspondantes, vous permettant d'intégrer facilement Dify dans votre propre logique métier.

View File

@@ -164,7 +164,7 @@ DifyはオープンソースのLLMアプリケーション開発プラットフ
- **企業/組織向けのDify</br>**
企業中心の機能を提供しています。[メールを送信](mailto:business@dify.ai?subject=[GitHub]Business%20License%20Inquiry)して企業のニーズについて相談してください。 </br>
> AWSを使用しているスタートアップ企業や中小企業の場合は、[AWS Marketplace](https://aws.amazon.com/marketplace/pp/prodview-t22mebxzwjhu6)のDify Premiumをチェックして、ワンクリックで自分のAWS VPCにデプロイできます。さらに、手頃な価格のAMIオファリングして、ロゴやブランディングをカスタマイズしてアプリケーションを作成するオプションがあります。
> AWSを使用しているスタートアップ企業や中小企業の場合は、[AWS Marketplace](https://aws.amazon.com/marketplace/pp/prodview-t23mebxzwjhu6)のDify Premiumをチェックして、ワンクリックで自分のAWS VPCにデプロイできます。さらに、手頃な価格のAMIオファリングして、ロゴやブランディングをカスタマイズしてアプリケーションを作成するオプションがあります。
## 最新の情報を入手

View File

@@ -1,3 +1,5 @@
from urllib.parse import quote
from flask import Response, request
from flask_restful import Resource, reqparse # type: ignore
from werkzeug.exceptions import NotFound
@@ -71,7 +73,8 @@ class FilePreviewApi(Resource):
if upload_file.size > 0:
response.headers["Content-Length"] = str(upload_file.size)
if args["as_attachment"]:
response.headers["Content-Disposition"] = f"attachment; filename={upload_file.name}"
encoded_filename = quote(upload_file.name)
response.headers["Content-Disposition"] = f"attachment; filename*=UTF-8''{encoded_filename}"
return response

View File

@@ -336,6 +336,10 @@ class DocumentUpdateByFileApi(DatasetApiResource):
if not dataset:
raise ValueError("Dataset is not exist.")
# indexing_technique is already set in dataset since this is an update
args["indexing_technique"] = dataset.indexing_technique
if "file" in request.files:
# save file info
file = request.files["file"]

View File

@@ -42,7 +42,6 @@ class MessageBasedAppGenerator(BaseAppGenerator):
ChatAppGenerateEntity,
CompletionAppGenerateEntity,
AgentChatAppGenerateEntity,
AgentChatAppGenerateEntity,
],
queue_manager: AppQueueManager,
conversation: Conversation,

View File

@@ -101,11 +101,13 @@ class ProviderManager:
)
# append providers with langgenius/openai/openai
for provider_name in list(provider_name_to_provider_records_dict.keys()):
provider_name_list = list(provider_name_to_provider_records_dict.keys())
for provider_name in provider_name_list:
provider_id = ModelProviderID(provider_name)
provider_name_to_provider_records_dict[str(provider_id)] = provider_name_to_provider_records_dict[
provider_name
]
if str(provider_id) not in provider_name_list:
provider_name_to_provider_records_dict[str(provider_id)] = provider_name_to_provider_records_dict[
provider_name
]
# Get all provider model records of the workspace
provider_name_to_provider_model_records_dict = self._get_all_provider_models(tenant_id)

View File

@@ -111,8 +111,9 @@ class ChromaVector(BaseVector):
for index in range(len(ids)):
distance = distances[index]
metadata = dict(metadatas[index])
if distance >= score_threshold:
metadata["score"] = distance
score = 1 - distance
if score > score_threshold:
metadata["score"] = score
doc = Document(
page_content=documents[index],
metadata=metadata,

View File

@@ -105,10 +105,10 @@ class ApiTool(Tool):
needed_parameters = [parameter for parameter in (self.api_bundle.parameters or []) if parameter.required]
for parameter in needed_parameters:
if parameter.required and parameter.name not in parameters:
raise ToolParameterValidationError(f"Missing required parameter {parameter.name}")
if parameter.default is not None and parameter.name not in parameters:
parameters[parameter.name] = parameter.default
if parameter.default is not None:
parameters[parameter.name] = parameter.default
else:
raise ToolParameterValidationError(f"Missing required parameter {parameter.name}")
return headers

View File

@@ -246,10 +246,11 @@ class ToolEngine:
+ "you do not need to create it, just tell the user to check it now."
)
elif response.type == ToolInvokeMessage.MessageType.JSON:
text = json.dumps(cast(ToolInvokeMessage.JsonMessage, response.message).json_object, ensure_ascii=False)
result += f"tool response: {text}."
result = json.dumps(
cast(ToolInvokeMessage.JsonMessage, response.message).json_object, ensure_ascii=False
)
else:
result += f"tool response: {response.message!r}."
result += str(response.message)
return result

View File

@@ -590,6 +590,7 @@ class IterationNode(BaseNode[IterationNodeData]):
with flask_app.app_context():
parallel_mode_run_id = uuid.uuid4().hex
graph_engine_copy = graph_engine.create_copy()
graph_engine_copy.graph_runtime_state.total_tokens = 0
variable_pool_copy = graph_engine_copy.graph_runtime_state.variable_pool
variable_pool_copy.add([self.node_id, "index"], index)
variable_pool_copy.add([self.node_id, "item"], item)

View File

@@ -1,6 +1,3 @@
from collections.abc import Mapping, Sequence
from typing import Any
from core.workflow.entities.node_entities import NodeRunResult
from core.workflow.nodes.base import BaseNode
from core.workflow.nodes.enums import NodeType
@@ -36,16 +33,3 @@ class VariableAggregatorNode(BaseNode[VariableAssignerNodeData]):
break
return NodeRunResult(status=WorkflowNodeExecutionStatus.SUCCEEDED, outputs=outputs, inputs=inputs)
@classmethod
def _extract_variable_selector_to_variable_mapping(
cls, *, graph_config: Mapping[str, Any], node_id: str, node_data: VariableAssignerNodeData
) -> Mapping[str, Sequence[str]]:
"""
Extract variable selector to variable mapping
:param graph_config: graph config
:param node_id: node id
:param node_data: node data
:return:
"""
return {}

View File

@@ -1,5 +1,5 @@
from core.helper import marketplace
from core.plugin.entities.plugin import GenericProviderID, PluginDependency, PluginInstallationSource
from core.plugin.entities.plugin import ModelProviderID, PluginDependency, PluginInstallationSource, ToolProviderID
from core.plugin.manager.plugin import PluginInstallationManager
@@ -12,10 +12,7 @@ class DependenciesAnalysisService:
Convert the tool id to the plugin_id
"""
try:
tool_provider_id = GenericProviderID(tool_id)
if tool_id in ["jina", "siliconflow"]:
tool_provider_id.plugin_name = tool_provider_id.plugin_name + "_tool"
return tool_provider_id.plugin_id
return ToolProviderID(tool_id).plugin_id
except Exception as e:
raise e
@@ -27,11 +24,7 @@ class DependenciesAnalysisService:
Convert the model provider id to the plugin_id
"""
try:
generic_provider_id = GenericProviderID(model_provider_id)
if model_provider_id == "google":
generic_provider_id.plugin_name = "gemini"
return generic_provider_id.plugin_id
return ModelProviderID(model_provider_id).plugin_id
except Exception as e:
raise e

View File

@@ -14,9 +14,8 @@ from flask import Flask, current_app
from sqlalchemy.orm import Session
from core.agent.entities import AgentToolEntity
from core.entities import DEFAULT_PLUGIN_ID
from core.helper import marketplace
from core.plugin.entities.plugin import PluginInstallationSource
from core.plugin.entities.plugin import ModelProviderID, PluginInstallationSource, ToolProviderID
from core.plugin.entities.plugin_daemon import PluginInstallTaskStatus
from core.plugin.manager.plugin import PluginInstallationManager
from core.tools.entities.tool_entities import ToolProviderType
@@ -203,13 +202,7 @@ class PluginMigration:
result = []
for row in rs:
provider_name = str(row[0])
if provider_name and "/" not in provider_name:
if provider_name == "google":
provider_name = "gemini"
result.append(DEFAULT_PLUGIN_ID + "/" + provider_name)
elif provider_name:
result.append(provider_name)
result.append(ModelProviderID(provider_name).plugin_id)
return result
@@ -222,30 +215,10 @@ class PluginMigration:
rs = session.query(BuiltinToolProvider).filter(BuiltinToolProvider.tenant_id == tenant_id).all()
result = []
for row in rs:
if "/" not in row.provider:
result.append(DEFAULT_PLUGIN_ID + "/" + row.provider)
else:
result.append(row.provider)
result.append(ToolProviderID(row.provider).plugin_id)
return result
@classmethod
def _handle_builtin_tool_provider(cls, provider_name: str) -> str:
"""
Handle builtin tool provider.
"""
if provider_name == "jina":
provider_name = "jina_tool"
elif provider_name == "siliconflow":
provider_name = "siliconflow_tool"
elif provider_name == "stepfun":
provider_name = "stepfun_tool"
if "/" not in provider_name:
return DEFAULT_PLUGIN_ID + "/" + provider_name
else:
return provider_name
@classmethod
def extract_workflow_tables(cls, tenant_id: str) -> Sequence[str]:
"""
@@ -266,8 +239,7 @@ class PluginMigration:
provider_name = data.get("provider_name")
provider_type = data.get("provider_type")
if provider_name not in excluded_providers and provider_type == ToolProviderType.BUILT_IN.value:
provider_name = cls._handle_builtin_tool_provider(provider_name)
result.append(provider_name)
result.append(ToolProviderID(provider_name).plugin_id)
return result
@@ -298,7 +270,7 @@ class PluginMigration:
tool_entity.provider_type == ToolProviderType.BUILT_IN.value
and tool_entity.provider_id not in excluded_providers
):
result.append(cls._handle_builtin_tool_provider(tool_entity.provider_id))
result.append(ToolProviderID(tool_entity.provider_id).plugin_id)
except Exception:
logger.exception(f"Failed to process tool {tool}")
@@ -386,7 +358,7 @@ class PluginMigration:
batch_plugin_identifiers = [
plugins["plugins"][plugin_id]
for plugin_id in batch_plugin_ids
if plugin_id not in installed_plugins_ids
if plugin_id not in installed_plugins_ids and plugin_id in plugins["plugins"]
]
manager.install_from_identifiers(
tenant_id,

View File

@@ -2,7 +2,7 @@ x-shared-env: &shared-api-worker-env
services:
# API service
api:
image: langgenius/dify-api:1.0.0
image: langgenius/dify-api:0.15.3
restart: always
environment:
# Use the shared environment variables.
@@ -27,7 +27,7 @@ services:
# worker service
# The Celery worker for processing the queue.
worker:
image: langgenius/dify-api:1.0.0
image: langgenius/dify-api:0.15.3
restart: always
environment:
# Use the shared environment variables.
@@ -51,7 +51,7 @@ services:
# Frontend web application.
web:
image: langgenius/dify-web:1.0.0
image: langgenius/dify-web:0.15.3
restart: always
environment:
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
@@ -64,6 +64,7 @@ services:
MARKETPLACE_URL: ${MARKETPLACE_URL:-https://marketplace.dify.ai}
TOP_K_MAX_VALUE: ${TOP_K_MAX_VALUE:-}
INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: ${INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH:-}
PM2_INSTANCES: ${PM2_INSTANCES:-2}
# The postgres database.
db:
@@ -128,7 +129,7 @@ services:
# plugin daemon
plugin_daemon:
image: langgenius/dify-plugin-daemon:0.0.1-local
image: langgenius/dify-plugin-daemon:0.0.2-local
restart: always
environment:
# Use the shared environment variables.

View File

@@ -66,7 +66,7 @@ services:
# plugin daemon
plugin_daemon:
image: langgenius/dify-plugin-daemon:0.0.1-local
image: langgenius/dify-plugin-daemon:0.0.2-local
restart: always
environment:
# Use the shared environment variables.

View File

@@ -414,7 +414,7 @@ x-shared-env: &shared-api-worker-env
services:
# API service
api:
image: langgenius/dify-api:1.0.0
image: langgenius/dify-api:0.15.3
restart: always
environment:
# Use the shared environment variables.
@@ -439,7 +439,7 @@ services:
# worker service
# The Celery worker for processing the queue.
worker:
image: langgenius/dify-api:1.0.0
image: langgenius/dify-api:0.15.3
restart: always
environment:
# Use the shared environment variables.
@@ -463,7 +463,7 @@ services:
# Frontend web application.
web:
image: langgenius/dify-web:1.0.0
image: langgenius/dify-web:0.15.3
restart: always
environment:
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
@@ -476,6 +476,7 @@ services:
MARKETPLACE_URL: ${MARKETPLACE_URL:-https://marketplace.dify.ai}
TOP_K_MAX_VALUE: ${TOP_K_MAX_VALUE:-}
INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: ${INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH:-}
PM2_INSTANCES: ${PM2_INSTANCES:-2}
# The postgres database.
db:
@@ -540,7 +541,7 @@ services:
# plugin daemon
plugin_daemon:
image: langgenius/dify-plugin-daemon:0.0.1-local
image: langgenius/dify-plugin-daemon:0.0.2-local
restart: always
environment:
# Use the shared environment variables.

View File

@@ -46,6 +46,7 @@ ENV MARKETPLACE_API_URL=http://127.0.0.1:5001
ENV MARKETPLACE_URL=http://127.0.0.1:5001
ENV PORT=3000
ENV NEXT_TELEMETRY_DISABLED=1
ENV PM2_INSTANCES=2
# set timezone
ENV TZ=UTC
@@ -58,7 +59,6 @@ COPY --from=builder /app/web/public ./public
COPY --from=builder /app/web/.next/standalone ./
COPY --from=builder /app/web/.next/static ./.next/static
COPY docker/pm2.json ./pm2.json
COPY docker/entrypoint.sh ./entrypoint.sh

View File

@@ -70,6 +70,8 @@ If you want to customize the host and port:
pnpm run start --port=3001 --host=0.0.0.0
```
If you want to customize the number of instances launched by PM2, you can configure `PM2_INSTANCES` in `docker-compose.yaml` or `Dockerfile`.
## Storybook
This project uses [Storybook](https://storybook.js.org/) for UI component development.

View File

@@ -53,7 +53,7 @@ export default function ChartView({ appId }: IChartViewProps) {
className='mt-0 !w-40'
onSelect={(item) => {
const id = item.value
const value = TIME_PERIOD_MAPPING[id]?.value || '-1'
const value = TIME_PERIOD_MAPPING[id]?.value ?? '-1'
const name = item.name || t('appLog.filter.period.allTime')
onSelect({ value, name })
}}

View File

@@ -59,8 +59,8 @@ const Apps = () => {
const [activeTab, setActiveTab] = useTabSearchParams({
defaultTab: 'all',
})
const { query: { tagIDs = [], keywords = '' }, setQuery } = useAppsQueryState()
const [isCreatedByMe, setIsCreatedByMe] = useState(false)
const { query: { tagIDs = [], keywords = '', isCreatedByMe: queryIsCreatedByMe = false }, setQuery } = useAppsQueryState()
const [isCreatedByMe, setIsCreatedByMe] = useState(queryIsCreatedByMe)
const [tagFilterValue, setTagFilterValue] = useState<string[]>(tagIDs)
const [searchKeywords, setSearchKeywords] = useState(keywords)
const setKeywords = useCallback((keywords: string) => {
@@ -126,6 +126,12 @@ const Apps = () => {
handleTagsUpdate()
}
const handleCreatedByMeChange = useCallback(() => {
const newValue = !isCreatedByMe
setIsCreatedByMe(newValue)
setQuery(prev => ({ ...prev, isCreatedByMe: newValue }))
}, [isCreatedByMe, setQuery])
return (
<>
<div className='sticky top-0 flex justify-between items-center pt-4 px-12 pb-2 leading-[56px] bg-background-body z-10 flex-wrap gap-y-2'>
@@ -139,7 +145,7 @@ const Apps = () => {
className='mr-2'
label={t('app.showMyCreatedAppsOnly')}
isChecked={isCreatedByMe}
onChange={() => setIsCreatedByMe(!isCreatedByMe)}
onChange={handleCreatedByMeChange}
/>
<TagFilter type='app' value={tagFilterValue} onChange={handleTagsChange} />
<Input

View File

@@ -4,18 +4,20 @@ import { useCallback, useEffect, useMemo, useState } from 'react'
type AppsQuery = {
tagIDs?: string[]
keywords?: string
isCreatedByMe?: boolean
}
// Parse the query parameters from the URL search string.
function parseParams(params: ReadonlyURLSearchParams): AppsQuery {
const tagIDs = params.get('tagIDs')?.split(';')
const keywords = params.get('keywords') || undefined
return { tagIDs, keywords }
const isCreatedByMe = params.get('isCreatedByMe') === 'true'
return { tagIDs, keywords, isCreatedByMe }
}
// Update the URL search string with the given query parameters.
function updateSearchParams(query: AppsQuery, current: URLSearchParams) {
const { tagIDs, keywords } = query || {}
const { tagIDs, keywords, isCreatedByMe } = query || {}
if (tagIDs && tagIDs.length > 0)
current.set('tagIDs', tagIDs.join(';'))
@@ -26,6 +28,11 @@ function updateSearchParams(query: AppsQuery, current: URLSearchParams) {
current.set('keywords', keywords)
else
current.delete('keywords')
if (isCreatedByMe)
current.set('isCreatedByMe', 'true')
else
current.delete('isCreatedByMe')
}
function useAppsQueryState() {

View File

@@ -26,12 +26,12 @@ import { MAX_TOOLS_NUM } from '@/config'
import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
import Tooltip from '@/app/components/base/tooltip'
import { DefaultToolIcon } from '@/app/components/base/icons/src/public/other'
// import AddToolModal from '@/app/components/tools/add-tool-modal'
import ConfigCredential from '@/app/components/tools/setting/build-in/config-credentials'
import { updateBuiltInToolCredential } from '@/service/tools'
import cn from '@/utils/classnames'
import ToolPicker from '@/app/components/workflow/block-selector/tool-picker'
import type { ToolDefaultValue } from '@/app/components/workflow/block-selector/types'
import { canFindTool } from '@/utils'
type AgentToolWithMoreInfo = AgentTool & { icon: any; collection?: Collection } | null
const AgentTools: FC = () => {
@@ -43,7 +43,7 @@ const AgentTools: FC = () => {
const [currentTool, setCurrentTool] = useState<AgentToolWithMoreInfo>(null)
const currentCollection = useMemo(() => {
if (!currentTool) return null
const collection = collectionList.find(collection => collection.id.split('/').pop() === currentTool?.provider_id.split('/').pop() && collection.type === currentTool?.provider_type)
const collection = collectionList.find(collection => canFindTool(collection.id, currentTool?.provider_id) && collection.type === currentTool?.provider_type)
return collection
}, [currentTool, collectionList])
const [isShowSettingTool, setIsShowSettingTool] = useState(false)
@@ -51,7 +51,7 @@ const AgentTools: FC = () => {
const tools = (modelConfig?.agentConfig?.tools as AgentTool[] || []).map((item) => {
const collection = collectionList.find(
collection =>
collection.id.split('/').pop() === item.provider_id.split('/').pop()
canFindTool(collection.id, item.provider_id)
&& collection.type === item.provider_type,
)
const icon = collection?.icon

View File

@@ -97,7 +97,7 @@ const Uploader: FC<Props> = ({
style={{ display: 'none' }}
type="file"
id="fileUploader"
accept='.yml'
accept='.yaml,.yml'
onChange={fileChangeHandle}
/>
<div ref={dropRef}>

View File

@@ -635,9 +635,10 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh })
const [currentConversation, setCurrentConversation] = useState<ChatConversationGeneralDetail | CompletionConversationGeneralDetail | undefined>() // Currently selected conversation
const isChatMode = appDetail.mode !== 'completion' // Whether the app is a chat app
const isChatflow = appDetail.mode === 'advanced-chat' // Whether the app is a chatflow app
const { setShowPromptLogModal, setShowAgentLogModal } = useAppStore(useShallow(state => ({
const { setShowPromptLogModal, setShowAgentLogModal, setShowMessageLogModal } = useAppStore(useShallow(state => ({
setShowPromptLogModal: state.setShowPromptLogModal,
setShowAgentLogModal: state.setShowAgentLogModal,
setShowMessageLogModal: state.setShowMessageLogModal,
})))
// Annotated data needs to be highlighted
@@ -664,6 +665,7 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh })
setCurrentConversation(undefined)
setShowPromptLogModal(false)
setShowAgentLogModal(false)
setShowMessageLogModal(false)
}
if (!logs)

View File

@@ -100,7 +100,7 @@ function getThreadMessages(tree: ChatItemInTree[], targetMessageId?: string): Ch
let targetNode: ChatItemInTree | undefined
// find path to the target message
const stack = tree.toReversed().map(rootNode => ({
const stack = tree.slice().reverse().map(rootNode => ({
node: rootNode,
path: [rootNode],
}))

View File

@@ -68,6 +68,9 @@ export const ThinkBlock = ({ children, ...props }: any) => {
const displayContent = removeEndThink(children)
const { t } = useTranslation()
if (!(props['data-think'] ?? false))
return (<details {...props}>{children}</details>)
return (
<details {...(!isComplete && { open: true })} className="group">
<summary className="text-gray-500 font-bold list-none pl-2 flex items-center cursor-pointer select-none whitespace-nowrap">

View File

@@ -68,13 +68,9 @@ const preprocessLaTeX = (content: string) => {
}
const preprocessThinkTag = (content: string) => {
if (!(content.trim().startsWith('<think>\n') || content.trim().startsWith('<details style=')))
return content
return flow([
(str: string) => str.replaceAll('<think>\n', '<details>\n'),
(str: string) => str.replaceAll('\n</think>', '\n[ENDTHINKFLAG]</details>'),
(str: string) => str.replaceAll('\n</details>', '\n[ENDTHINKFLAG]</details>'),
(str: string) => str.replace('<think>\n', '<details data-think=true>\n'),
(str: string) => str.replace('\n</think>', '\n[ENDTHINKFLAG]</details>'),
])(content)
}
@@ -269,7 +265,7 @@ export function Markdown(props: { content: string; className?: string }) {
}
},
]}
disallowedElements={['iframe', 'head', 'html', 'meta', 'link', 'style', 'body']}
disallowedElements={['iframe', 'head', 'html', 'meta', 'link', 'style', 'body', 'input']}
components={{
code: CodeBlock,
img: Img,

View File

@@ -243,7 +243,7 @@ const FileUploader = ({
}, [handleDrop])
return (
<div className="mb-5 w-[640px]">
<div className="mb-5 max-w-[640px]">
{!hideUpload && (
<input
ref={fileUploader}

View File

@@ -14,7 +14,7 @@
}
.dataSourceItem {
@apply box-border relative grow shrink-0 flex items-center p-3 h-14 bg-white rounded-xl cursor-pointer;
@apply w-full box-border relative flex items-center p-3 h-14 bg-white rounded-xl cursor-pointer;
border: 0.5px solid #EAECF0;
box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05);
font-weight: 500;
@@ -64,7 +64,7 @@
}
.datasetIcon {
@apply flex mr-2 w-8 h-8 rounded-lg bg-center bg-no-repeat;
@apply flex shrink-0 mr-2 w-8 h-8 rounded-lg bg-center bg-no-repeat;
background-color: #F5FAFF;
background-image: url(../assets/file.svg);
background-size: 16px;

View File

@@ -137,7 +137,7 @@ const StepOne = ({
}
{
shouldShowDataSourceTypeList && (
<div className='flex items-center mb-8 flex-wrap gap-4'>
<div className='grid grid-cols-3 mb-8 gap-4'>
<div
className={cn(
s.dataSourceItem,
@@ -153,7 +153,12 @@ const StepOne = ({
}}
>
<span className={cn(s.datasetIcon)} />
{t('datasetCreation.stepOne.dataSourceType.file')}
<span
title={t('datasetCreation.stepOne.dataSourceType.file')}
className='truncate'
>
{t('datasetCreation.stepOne.dataSourceType.file')}
</span>
</div>
<div
className={cn(
@@ -170,7 +175,12 @@ const StepOne = ({
}}
>
<span className={cn(s.datasetIcon, s.notion)} />
{t('datasetCreation.stepOne.dataSourceType.notion')}
<span
title={t('datasetCreation.stepOne.dataSourceType.notion')}
className='truncate'
>
{t('datasetCreation.stepOne.dataSourceType.notion')}
</span>
</div>
<div
className={cn(
@@ -181,7 +191,12 @@ const StepOne = ({
onClick={() => changeType(DataSourceType.WEB)}
>
<span className={cn(s.datasetIcon, s.web)} />
{t('datasetCreation.stepOne.dataSourceType.web')}
<span
title={t('datasetCreation.stepOne.dataSourceType.web')}
className='truncate'
>
{t('datasetCreation.stepOne.dataSourceType.web')}
</span>
</div>
</div>
)

View File

@@ -50,7 +50,6 @@ const ChildSegmentDetail: FC<IChildSegmentDetailProps> = ({
const handleCancel = () => {
onCancel()
setContent(childChunkInfo?.content || '')
}
const handleSave = () => {

View File

@@ -80,7 +80,7 @@ export const useSegmentListContext = (selector: (value: SegmentListContextValue)
return useContextSelector(SegmentListContext, selector)
}
interface ICompletedProps {
type ICompletedProps = {
embeddingAvailable: boolean
showNewSegmentModal: boolean
onNewSegmentModalChange: (state: boolean) => void
@@ -357,6 +357,7 @@ const Completed: FC<ICompletedProps> = ({
if (seg.id === segmentId) {
seg.answer = res.data.answer
seg.content = res.data.content
seg.sign_content = res.data.sign_content
seg.keywords = res.data.keywords
seg.word_count = res.data.word_count
seg.hit_count = res.data.hit_count

View File

@@ -68,7 +68,6 @@ const NewChildSegmentModal: FC<NewChildSegmentModalProps> = ({
const handleCancel = (actionType: 'esc' | 'add' = 'esc') => {
if (actionType === 'esc' || !addAnother)
onCancel()
setContent('')
}
const { mutateAsync: addChildSegment } = useAddChildSegment()

View File

@@ -49,7 +49,7 @@ const ChunkContent: FC<ChunkContentProps> = ({
isFullDocMode ? 'line-clamp-3' : isCollapsed ? 'line-clamp-2' : 'line-clamp-20',
className,
)}
content={sign_content || content}
content={sign_content || content || ''}
/>
}

View File

@@ -57,9 +57,6 @@ const SegmentDetail: FC<ISegmentDetailProps> = ({
const handleCancel = () => {
onCancel()
setQuestion(segInfo?.content || '')
setAnswer(segInfo?.answer || '')
setKeywords(segInfo?.keywords || [])
}
const handleSave = () => {

View File

@@ -70,9 +70,6 @@ const NewSegmentModal: FC<NewSegmentModalProps> = ({
const handleCancel = (actionType: 'esc' | 'add' = 'esc') => {
if (actionType === 'esc' || !addAnother)
onCancel()
setQuestion('')
setAnswer('')
setKeywords([])
}
const { mutateAsync: addSegment } = useAddSegment()

View File

@@ -8,7 +8,6 @@ import { switchWorkspace } from '@/service/common'
import { useWorkspacesContext } from '@/context/workspace-context'
import { useProviderContext } from '@/context/provider-context'
import { ToastContext } from '@/app/components/base/toast'
import PremiumBadge from '@/app/components/base/premium-badge'
const WorkplaceSelector = () => {
const { t } = useTranslation()
@@ -72,15 +71,6 @@ const WorkplaceSelector = () => {
<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-6 h-6 bg-[#EFF4FF] rounded-md text-xs font-medium text-primary-600'>{workspace.name[0].toLocaleUpperCase()}</div>
<div className='line-clamp-1 grow overflow-hidden text-text-secondary text-ellipsis system-md-regular cursor-pointer'>{workspace.name}</div>
{
<PremiumBadge size='s' color='gray' allowHover={false}>
<div className='system-2xs-medium'>
<span className='p-[2px]'>
{plan.type === 'professional' ? 'PRO' : plan.type.toUpperCase()}
</span>
</div>
</PremiumBadge>
}
</div>
))
}

View File

@@ -41,7 +41,7 @@ const QuotaPanel: FC<QuotaPanelProps> = ({
{
currentQuota && (
<div className='flex items-center h-4 text-xs text-text-tertiary'>
<span className='mr-0.5 system-md-semibold-uppercase text-text-secondary'>{formatNumber((currentQuota?.quota_limit || 0) - (currentQuota?.quota_used || 0))}</span>
<span className='mr-0.5 system-md-semibold-uppercase text-text-secondary'>{formatNumber(Math.max((currentQuota?.quota_limit || 0) - (currentQuota?.quota_used || 0), 0))}</span>
{
currentQuota?.quota_unit === QuotaUnitEnum.tokens && 'Tokens'
}

View File

@@ -4,8 +4,6 @@ import Link from 'next/link'
import { useBoolean } from 'ahooks'
import { useSelectedLayoutSegment } from 'next/navigation'
import { Bars3Icon } from '@heroicons/react/20/solid'
import { SparklesSoft } from '@/app/components/base/icons/src/public/common'
import PremiumBadge from '../base/premium-badge'
import AccountDropdown from './account-dropdown'
import AppNav from './app-nav'
import DatasetNav from './dataset-nav'
@@ -13,7 +11,6 @@ import EnvNav from './env-nav'
import PluginsNav from './plugins-nav'
import ExploreNav from './explore-nav'
import ToolsNav from './tools-nav'
import LicenseNav from './license-env'
import { WorkspaceProvider } from '@/context/workspace-context'
import { useAppContext } from '@/context/app-context'
import LogoSite from '@/app/components/base/logo/logo-site'
@@ -21,7 +18,9 @@ import WorkplaceSelector from '@/app/components/header/account-dropdown/workplac
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import { useProviderContext } from '@/context/provider-context'
import { useModalContext } from '@/context/modal-context'
import { useTranslation } from 'react-i18next'
import LicenseNav from './license-env'
import PlanBadge from './plan-badge'
import { Plan } from '../billing/type'
const navClassName = `
flex items-center relative mr-0 sm:mr-3 px-3 h-8 rounded-xl
@@ -31,15 +30,13 @@ const navClassName = `
const Header = () => {
const { isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator } = useAppContext()
const { t } = useTranslation()
const selectedSegment = useSelectedLayoutSegment()
const media = useBreakpoints()
const isMobile = media === MediaType.mobile
const [isShowNavMenu, { toggle, setFalse: hideNavMenu }] = useBoolean(false)
const { enableBilling, plan } = useProviderContext()
const { setShowPricingModal, setShowAccountSettingModal } = useModalContext()
const isFreePlan = plan.type === 'sandbox'
const isFreePlan = plan.type === Plan.sandbox
const handlePlanClick = useCallback(() => {
if (isFreePlan)
setShowPricingModal()
@@ -71,18 +68,7 @@ const Header = () => {
<WorkspaceProvider>
<WorkplaceSelector />
</WorkspaceProvider>
{enableBilling && (
<div className='select-none'>
<PremiumBadge color='blue' allowHover={true} onClick={handlePlanClick}>
<SparklesSoft className='flex items-center py-[1px] pl-[3px] w-3.5 h-3.5 text-components-premium-badge-indigo-text-stop-0' />
<div className='system-xs-medium'>
<span className='p-1'>
{t('billing.upgradeBtn.encourageShort')}
</span>
</div>
</PremiumBadge>
</div>
)}
{enableBilling ? <PlanBadge allowHover sandboxAsUpgrade plan={plan.type} onClick={handlePlanClick} /> : <LicenseNav />}
</div>
</div>
}
@@ -93,20 +79,7 @@ const Header = () => {
<LogoSite />
</Link>
<div className='font-light text-divider-deep'>/</div>
{
enableBilling && (
<div className='select-none'>
<PremiumBadge color='blue' allowHover={true} onClick={handlePlanClick}>
<SparklesSoft className='flex items-center py-[1px] pl-[3px] w-3.5 h-3.5 text-components-premium-badge-indigo-text-stop-0' />
<div className='system-xs-medium'>
<span className='p-1'>
{t('billing.upgradeBtn.encourageShort')}
</span>
</div>
</PremiumBadge>
</div>
)
}
{enableBilling ? <PlanBadge allowHover sandboxAsUpgrade plan={plan.type} onClick={handlePlanClick} /> : <LicenseNav />}
</div >
)}
{
@@ -120,7 +93,6 @@ const Header = () => {
)
}
<div className='flex items-center shrink-0'>
<LicenseNav />
<EnvNav />
<div className='mr-3'>
<PluginsNav />

View File

@@ -5,6 +5,8 @@ import { LicenseStatus } from '@/types/feature'
import { useTranslation } from 'react-i18next'
import { useContextSelector } from 'use-context-selector'
import dayjs from 'dayjs'
import PremiumBadge from '../../base/premium-badge'
import { RiHourglass2Fill } from '@remixicon/react'
const LicenseNav = () => {
const { t } = useTranslation()
@@ -13,15 +15,16 @@ const LicenseNav = () => {
if (systemFeatures.license?.status === LicenseStatus.EXPIRING) {
const expiredAt = systemFeatures.license?.expired_at
const count = dayjs(expiredAt).diff(dayjs(), 'days')
return <div className='px-2 py-1 mr-4 rounded-full bg-util-colors-orange-orange-50 border-util-colors-orange-orange-100 system-xs-medium text-util-colors-orange-orange-600'>
{count <= 1 && <span>{t('common.license.expiring', { count })}</span>}
{count > 1 && <span>{t('common.license.expiring_plural', { count })}</span>}
</div>
return <PremiumBadge color='orange' className='select-none'>
<RiHourglass2Fill className='flex items-center pl-0.5 size-3 text-components-premium-badge-indigo-text-stop-0' />
{count <= 1 && <span className='system-xs-medium px-0.5'>{t('common.license.expiring', { count })}</span>}
{count > 1 && <span className='system-xs-medium px-0.5'>{t('common.license.expiring_plural', { count })}</span>}
</PremiumBadge>
}
if (systemFeatures.license.status === LicenseStatus.ACTIVE) {
return <div className='px-2 py-1 mr-4 rounded-md bg-util-colors-indigo-indigo-50 border-util-colors-indigo-indigo-100 system-xs-medium text-util-colors-indigo-indigo-600'>
Enterprise
</div>
return <PremiumBadge color="indigo" className='select-none'>
<span className='system-xs-medium px-1'>Enterprise</span>
</PremiumBadge>
}
return null
}

View File

@@ -1,8 +1,8 @@
'use client'
import React, { useState } from 'react'
import React, { useEffect, useState } from 'react'
import Link from 'next/link'
import { useSelectedLayoutSegment } from 'next/navigation'
import { usePathname, useSearchParams, useSelectedLayoutSegment } from 'next/navigation'
import type { INavSelectorProps } from './nav-selector'
import NavSelector from './nav-selector'
import classNames from '@/utils/classnames'
@@ -35,6 +35,14 @@ const Nav = ({
const [hovered, setHovered] = useState(false)
const segment = useSelectedLayoutSegment()
const isActivated = Array.isArray(activeSegment) ? activeSegment.includes(segment!) : segment === activeSegment
const pathname = usePathname()
const searchParams = useSearchParams()
const [linkLastSearchParams, setLinkLastSearchParams] = useState('')
useEffect(() => {
if (pathname === link)
setLinkLastSearchParams(searchParams.toString())
}, [pathname, searchParams])
return (
<div className={`
@@ -42,7 +50,7 @@ const Nav = ({
${isActivated && 'bg-components-main-nav-nav-button-bg-active shadow-md font-semibold'}
${!curNav && !isActivated && 'hover:bg-components-main-nav-nav-button-bg-hover'}
`}>
<Link href={link}>
<Link href={link + (linkLastSearchParams && `?${linkLastSearchParams}`)}>
<div
onClick={() => setAppDetail()}
className={classNames(`

View File

@@ -0,0 +1,70 @@
import { useProviderContext } from '@/context/provider-context'
import classNames from '@/utils/classnames'
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { SparklesSoft } from '../../base/icons/src/public/common'
import PremiumBadge from '../../base/premium-badge'
import { Plan } from '../../billing/type'
type PlanBadgeProps = {
plan: Plan
size?: 's' | 'm'
allowHover?: boolean
sandboxAsUpgrade?: boolean
onClick?: () => void
}
const PlanBadge: FC<PlanBadgeProps> = ({ plan, allowHover, size = 'm', sandboxAsUpgrade = false, onClick }) => {
const { isFetchedPlan } = useProviderContext()
const { t } = useTranslation()
if (!isFetchedPlan) return null
if (plan === Plan.sandbox && sandboxAsUpgrade) {
return <div className='select-none'>
<PremiumBadge color='blue' allowHover={allowHover} onClick={onClick}>
<SparklesSoft className='flex items-center py-[1px] pl-[3px] w-3.5 h-3.5 text-components-premium-badge-indigo-text-stop-0' />
<div className='system-xs-medium'>
<span className='p-1'>
{t('billing.upgradeBtn.encourageShort')}
</span>
</div>
</PremiumBadge>
</div>
}
if (plan === Plan.sandbox) {
return <div className='select-none'>
<PremiumBadge size={size} color='gray' allowHover={allowHover} onClick={onClick}>
<div className={classNames(size === 's' ? 'system-2xs-medium-uppercase' : 'system-xs-medium-uppercase')}>
<span className='p-1'>
{plan}
</span>
</div>
</PremiumBadge>
</div>
}
if (plan === Plan.professional) {
return <div className='select-none'>
<PremiumBadge size={size} color='blue' allowHover={allowHover} onClick={onClick}>
<div className={classNames(size === 's' ? 'system-2xs-medium-uppercase' : 'system-xs-medium-uppercase')}>
<span className='p-1'>
pro
</span>
</div>
</PremiumBadge>
</div>
}
if (plan === Plan.team) {
return <div className='select-none'>
<PremiumBadge size={size} color='indigo' allowHover={allowHover} onClick={onClick}>
<div className={classNames(size === 's' ? 'system-2xs-medium-uppercase' : 'system-xs-medium-uppercase')}>
<span className='p-1'>
{plan}
</span>
</div>
</PremiumBadge>
</div>
}
return null
}
export default PlanBadge

View File

@@ -23,17 +23,15 @@ const useRefreshPluginList = () => {
// installed list
invalidateInstalledPluginList()
if (!manifest) return
// tool page, tool select
if (PluginType.tool.includes(manifest.category) || refreshAllType) {
if ((manifest && PluginType.tool.includes(manifest.category)) || refreshAllType) {
invalidateAllToolProviders()
invalidateAllBuiltInTools()
// TODO: update suggested tools. It's a function in hook useMarketplacePlugins,handleUpdatePlugins
}
// model select
if (PluginType.model.includes(manifest.category) || refreshAllType) {
if ((manifest && PluginType.model.includes(manifest.category)) || refreshAllType) {
refreshModelProviders()
refetchLLMModelList()
refetchEmbeddingModelList()
@@ -41,7 +39,7 @@ const useRefreshPluginList = () => {
}
// agent select
if (PluginType.agent.includes(manifest.category) || refreshAllType)
if ((manifest && PluginType.agent.includes(manifest.category)) || refreshAllType)
invalidateStrategyProviders()
},
}

View File

@@ -13,7 +13,7 @@ const ModelList = ({
detail,
}: Props) => {
const { t } = useTranslation()
const { data: res } = useModelProviderModelList(`${detail.plugin_id}/${detail.name}`)
const { data: res } = useModelProviderModelList(`${detail.plugin_id}/${detail.declaration.model.provider}`)
if (!res)
return null

View File

@@ -82,6 +82,7 @@ import {
initialNodes,
} from './utils'
import {
CUSTOM_EDGE,
CUSTOM_NODE,
DSL_EXPORT_CHECK,
ITERATION_CHILDREN_Z_INDEX,
@@ -103,7 +104,7 @@ const nodeTypes = {
[CUSTOM_ITERATION_START_NODE]: CustomIterationStartNode,
}
const edgeTypes = {
[CUSTOM_NODE]: CustomEdge,
[CUSTOM_EDGE]: CustomEdge,
}
type WorkflowProps = {

View File

@@ -28,4 +28,4 @@ export NEXT_PUBLIC_CSP_WHITELIST=${CSP_WHITELIST}
export NEXT_PUBLIC_TOP_K_MAX_VALUE=${TOP_K_MAX_VALUE}
export NEXT_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH=${INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH}
pm2 start ./pm2.json --no-daemon
pm2 start /app/web/server.js --name dify-web --cwd /app/web -i ${PM2_INSTANCES} --no-daemon

View File

@@ -552,16 +552,16 @@ const translation = {
'variable': '변수',
'operations': {
'*=': '*=',
'overwrite': '덮어',
'overwrite': '덮어쓰기',
'-=': '-=',
'append': '덧붙이다',
'over-write': '덮어',
'append': '추가',
'over-write': '덮어쓰기',
'+=': '+=',
'title': '수술',
'extend': '뻗치다',
'clear': '맑다',
'title': '제목',
'extend': '연장',
'clear': '초기화',
'/=': '/=',
'set': '집합',
'set': '설정',
},
'variables': '변수',
'noAssignedVars': '사용 가능한 할당된 변수가 없습니다.',

View File

@@ -148,15 +148,15 @@ const translation = {
},
avgSessionInteractions: {
title: '平均会话互动数',
explanation: '反每个会话用户的持续沟通次数,如果用户与 AI 问答了 10 轮,即为 10。该指标反映了用户粘性。仅在对话型应用提供。',
explanation: '反每个会话用户的持续沟通次数,如果用户与 AI 问答了 10 轮,即为 10。该指标反映了用户粘性。仅在对话型应用提供。',
},
avgUserInteractions: {
title: '平均用户调用次数',
explanation: '反每天用户的使用次数。该指标反映了用户粘性。',
explanation: '反每天用户的使用次数。该指标反映了用户粘性。',
},
userSatisfactionRate: {
title: '用户满意度',
explanation: '每 1000 条消息的点赞数。反了用户对回答十分满意的比例。',
explanation: '每 1000 条消息的点赞数。反了用户对回答十分满意的比例。',
},
avgResponseTime: {
title: '平均响应时间',

View File

@@ -52,7 +52,7 @@ const translation = {
communityForums: '社区论坛',
emailSupport: '电子邮件支持',
priorityEmail: '优先电子邮件和聊天支持',
logoChange: 'Logo更改',
logoChange: 'Logo 更改',
SSOAuthentication: 'SSO 认证',
personalizedSupport: '个性化支持',
dedicatedAPISupport: '专用 API 支持',

View File

@@ -19,7 +19,7 @@ const translation = {
steps: '运行步数',
},
resultEmpty: {
title: '本次运行仅输出JSON格式',
title: '本次运行仅输出 JSON 格式,',
tipLeft: '请转到',
link: '详细信息面板',
tipRight: '查看它。',

View File

@@ -148,15 +148,15 @@ const translation = {
},
avgSessionInteractions: {
title: '平均會話互動數',
explanation: '反每個會話使用者的持續溝通次數,如果使用者與 AI 問答了 10 輪,即為 10。該指標反映了使用者粘性。僅在對話型應用提供。',
explanation: '反每個會話使用者的持續溝通次數,如果使用者與 AI 問答了 10 輪,即為 10。該指標反映了使用者粘性。僅在對話型應用提供。',
},
avgUserInteractions: {
title: '平均使用者呼叫次數',
explanation: '反每天使用者的使用次數。該指標反映了使用者粘性。',
explanation: '反每天使用者的使用次數。該指標反映了使用者粘性。',
},
userSatisfactionRate: {
title: '使用者滿意度',
explanation: '每 1000 條訊息的點贊數。反了使用者對回答十分滿意的比例。',
explanation: '每 1000 條訊息的點贊數。反了使用者對回答十分滿意的比例。',
},
avgResponseTime: {
title: '平均響應時間',

View File

@@ -51,7 +51,7 @@ const translation = {
communityForums: '社群論壇',
emailSupport: '電子郵件支援',
priorityEmail: '優先電子郵件和聊天支援',
logoChange: 'Logo更改',
logoChange: 'Logo 更改',
SSOAuthentication: 'SSO 認證',
personalizedSupport: '個性化支援',
dedicatedAPISupport: '專用 API 支援',

View File

@@ -19,7 +19,7 @@ const translation = {
steps: '執行步數',
},
resultEmpty: {
title: '本運行僅輸出JSON格式',
title: '本運行僅輸出 JSON 格式,',
tipLeft: '請到',
link: '詳細資訊面板',
tipRight: '查看它。',

2
web/pnpm-lock.yaml generated
View File

@@ -18309,4 +18309,4 @@ snapshots:
immer: 9.0.21
react: 18.2.0
zwitch@2.0.4: {}
zwitch@2.0.4: {}

View File

@@ -1,4 +1,4 @@
import { useCallback } from 'react'
import { useCallback, useEffect } from 'react'
import type {
ModelProvider,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
@@ -39,6 +39,7 @@ import { useInvalidateAllBuiltInTools } from './use-tools'
import usePermission from '@/app/components/plugins/plugin-page/use-permission'
import { uninstallPlugin } from '@/service/plugins'
import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list'
import { cloneDeep } from 'lodash-es'
const NAME_SPACE = 'plugins'
@@ -326,7 +327,7 @@ export const useMutationPluginsFromMarketplace = () => {
pageSize = 40,
} = pluginsSearchParams
const pluginOrBundle = type === 'bundle' ? 'bundles' : 'plugins'
return postMarketplace<{ data: PluginsFromMarketplaceResponse }>(`/${pluginOrBundle}/search/basic`, {
return postMarketplace<{ data: PluginsFromMarketplaceResponse }>(`/${pluginOrBundle}/search/advanced`, {
body: {
page,
page_size: pageSize,
@@ -383,6 +384,7 @@ export const usePluginTaskList = (category?: PluginType) => {
const {
data,
isFetched,
isRefetching,
refetch,
...rest
} = useQuery({
@@ -392,16 +394,24 @@ export const usePluginTaskList = (category?: PluginType) => {
refetchInterval: (lastQuery) => {
const lastData = lastQuery.state.data
const taskDone = lastData?.tasks.every(task => task.status === TaskStatus.success || task.status === TaskStatus.failed)
return taskDone ? false : 5000
},
})
useEffect(() => {
// After first fetch, refresh plugin list each time all tasks are done
if (!isRefetching) {
const lastData = cloneDeep(data)
const taskDone = lastData?.tasks.every(task => task.status === TaskStatus.success || task.status === TaskStatus.failed)
const taskAllFailed = lastData?.tasks.every(task => task.status === TaskStatus.failed)
if (taskDone) {
if (lastData?.tasks.length && !taskAllFailed)
refreshPluginList(category ? { category } as any : undefined, !category)
return false
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isRefetching])
return 5000
},
})
const handleRefetch = useCallback(() => {
refetch()
}, [refetch])