merge feat/plugins

This commit is contained in:
zxhlyh 2024-12-16 18:06:06 +08:00
commit 9f90d70b38
72 changed files with 15535 additions and 16686 deletions

View File

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

View File

@ -0,0 +1,338 @@
from core.workflow.entities.variable_pool import VariablePool
from core.workflow.nodes.http_request import (
BodyData,
HttpRequestNodeAuthorization,
HttpRequestNodeBody,
HttpRequestNodeData,
)
from core.workflow.nodes.http_request.entities import HttpRequestNodeTimeout
from core.workflow.nodes.http_request.executor import Executor
def test_executor_with_json_body_and_number_variable():
# Prepare the variable pool
variable_pool = VariablePool(
system_variables={},
user_inputs={},
)
variable_pool.add(["pre_node_id", "number"], 42)
# Prepare the node data
node_data = HttpRequestNodeData(
title="Test JSON Body with Number Variable",
method="post",
url="https://api.example.com/data",
authorization=HttpRequestNodeAuthorization(type="no-auth"),
headers="Content-Type: application/json",
params="",
body=HttpRequestNodeBody(
type="json",
data=[
BodyData(
key="",
type="text",
value='{"number": {{#pre_node_id.number#}}}',
)
],
),
)
# Initialize the Executor
executor = Executor(
node_data=node_data,
timeout=HttpRequestNodeTimeout(connect=10, read=30, write=30),
variable_pool=variable_pool,
)
# Check the executor's data
assert executor.method == "post"
assert executor.url == "https://api.example.com/data"
assert executor.headers == {"Content-Type": "application/json"}
assert executor.params == []
assert executor.json == {"number": 42}
assert executor.data is None
assert executor.files is None
assert executor.content is None
# Check the raw request (to_log method)
raw_request = executor.to_log()
assert "POST /data HTTP/1.1" in raw_request
assert "Host: api.example.com" in raw_request
assert "Content-Type: application/json" in raw_request
assert '{"number": 42}' in raw_request
def test_executor_with_json_body_and_object_variable():
# Prepare the variable pool
variable_pool = VariablePool(
system_variables={},
user_inputs={},
)
variable_pool.add(["pre_node_id", "object"], {
"name": "John Doe", "age": 30, "email": "john@example.com"})
# Prepare the node data
node_data = HttpRequestNodeData(
title="Test JSON Body with Object Variable",
method="post",
url="https://api.example.com/data",
authorization=HttpRequestNodeAuthorization(type="no-auth"),
headers="Content-Type: application/json",
params="",
body=HttpRequestNodeBody(
type="json",
data=[
BodyData(
key="",
type="text",
value="{{#pre_node_id.object#}}",
)
],
),
)
# Initialize the Executor
executor = Executor(
node_data=node_data,
timeout=HttpRequestNodeTimeout(connect=10, read=30, write=30),
variable_pool=variable_pool,
)
# Check the executor's data
assert executor.method == "post"
assert executor.url == "https://api.example.com/data"
assert executor.headers == {"Content-Type": "application/json"}
assert executor.params == []
assert executor.json == {"name": "John Doe", "age": 30, "email": "john@example.com"}
assert executor.data is None
assert executor.files is None
assert executor.content is None
# Check the raw request (to_log method)
raw_request = executor.to_log()
assert "POST /data HTTP/1.1" in raw_request
assert "Host: api.example.com" in raw_request
assert "Content-Type: application/json" in raw_request
assert '"name": "John Doe"' in raw_request
assert '"age": 30' in raw_request
assert '"email": "john@example.com"' in raw_request
def test_executor_with_json_body_and_nested_object_variable():
# Prepare the variable pool
variable_pool = VariablePool(
system_variables={},
user_inputs={},
)
variable_pool.add(["pre_node_id", "object"], {
"name": "John Doe", "age": 30, "email": "john@example.com"})
# Prepare the node data
node_data = HttpRequestNodeData(
title="Test JSON Body with Nested Object Variable",
method="post",
url="https://api.example.com/data",
authorization=HttpRequestNodeAuthorization(type="no-auth"),
headers="Content-Type: application/json",
params="",
body=HttpRequestNodeBody(
type="json",
data=[
BodyData(
key="",
type="text",
value='{"object": {{#pre_node_id.object#}}}',
)
],
),
)
# Initialize the Executor
executor = Executor(
node_data=node_data,
timeout=HttpRequestNodeTimeout(connect=10, read=30, write=30),
variable_pool=variable_pool,
)
# Check the executor's data
assert executor.method == "post"
assert executor.url == "https://api.example.com/data"
assert executor.headers == {"Content-Type": "application/json"}
assert executor.params == []
assert executor.json == {"object": {"name": "John Doe", "age": 30, "email": "john@example.com"}}
assert executor.data is None
assert executor.files is None
assert executor.content is None
# Check the raw request (to_log method)
raw_request = executor.to_log()
assert "POST /data HTTP/1.1" in raw_request
assert "Host: api.example.com" in raw_request
assert "Content-Type: application/json" in raw_request
assert '"object": {' in raw_request
assert '"name": "John Doe"' in raw_request
assert '"age": 30' in raw_request
assert '"email": "john@example.com"' in raw_request
def test_extract_selectors_from_template_with_newline():
variable_pool = VariablePool()
variable_pool.add(("node_id", "custom_query"), "line1\nline2")
node_data = HttpRequestNodeData(
title="Test JSON Body with Nested Object Variable",
method="post",
url="https://api.example.com/data",
authorization=HttpRequestNodeAuthorization(type="no-auth"),
headers="Content-Type: application/json",
params="test: {{#node_id.custom_query#}}",
body=HttpRequestNodeBody(
type="none",
data=[],
),
)
executor = Executor(
node_data=node_data,
timeout=HttpRequestNodeTimeout(connect=10, read=30, write=30),
variable_pool=variable_pool,
)
assert executor.params == [("test", "line1\nline2")]
def test_executor_with_form_data():
# Prepare the variable pool
variable_pool = VariablePool(
system_variables={},
user_inputs={},
)
variable_pool.add(["pre_node_id", "text_field"], "Hello, World!")
variable_pool.add(["pre_node_id", "number_field"], 42)
# Prepare the node data
node_data = HttpRequestNodeData(
title="Test Form Data",
method="post",
url="https://api.example.com/upload",
authorization=HttpRequestNodeAuthorization(type="no-auth"),
headers="Content-Type: multipart/form-data",
params="",
body=HttpRequestNodeBody(
type="form-data",
data=[
BodyData(
key="text_field",
type="text",
value="{{#pre_node_id.text_field#}}",
),
BodyData(
key="number_field",
type="text",
value="{{#pre_node_id.number_field#}}",
),
],
),
)
# Initialize the Executor
executor = Executor(
node_data=node_data,
timeout=HttpRequestNodeTimeout(connect=10, read=30, write=30),
variable_pool=variable_pool,
)
# Check the executor's data
assert executor.method == "post"
assert executor.url == "https://api.example.com/upload"
assert "Content-Type" in executor.headers
assert "multipart/form-data" in executor.headers["Content-Type"]
assert executor.params == []
assert executor.json is None
assert executor.files is None
assert executor.content is None
# Check that the form data is correctly loaded in executor.data
assert isinstance(executor.data, dict)
assert "text_field" in executor.data
assert executor.data["text_field"] == "Hello, World!"
assert "number_field" in executor.data
assert executor.data["number_field"] == "42"
# Check the raw request (to_log method)
raw_request = executor.to_log()
assert "POST /upload HTTP/1.1" in raw_request
assert "Host: api.example.com" in raw_request
assert "Content-Type: multipart/form-data" in raw_request
assert "text_field" in raw_request
assert "Hello, World!" in raw_request
assert "number_field" in raw_request
assert "42" in raw_request
def test_init_headers():
def create_executor(headers: str) -> Executor:
node_data = HttpRequestNodeData(
title="test",
method="get",
url="http://example.com",
headers=headers,
params="",
authorization=HttpRequestNodeAuthorization(type="no-auth"),
)
timeout = HttpRequestNodeTimeout(connect=10, read=30, write=30)
return Executor(node_data=node_data, timeout=timeout, variable_pool=VariablePool())
executor = create_executor("aa\n cc:")
executor._init_headers()
assert executor.headers == {"aa": "", "cc": ""}
executor = create_executor("aa:bb\n cc:dd")
executor._init_headers()
assert executor.headers == {"aa": "bb", "cc": "dd"}
executor = create_executor("aa:bb\n cc:dd\n")
executor._init_headers()
assert executor.headers == {"aa": "bb", "cc": "dd"}
executor = create_executor("aa:bb\n\n cc : dd\n\n")
executor._init_headers()
assert executor.headers == {"aa": "bb", "cc": "dd"}
def test_init_params():
def create_executor(params: str) -> Executor:
node_data = HttpRequestNodeData(
title="test",
method="get",
url="http://example.com",
headers="",
params=params,
authorization=HttpRequestNodeAuthorization(type="no-auth"),
)
timeout = HttpRequestNodeTimeout(connect=10, read=30, write=30)
return Executor(node_data=node_data, timeout=timeout, variable_pool=VariablePool())
# Test basic key-value pairs
executor = create_executor("key1:value1\nkey2:value2")
executor._init_params()
assert executor.params == [("key1", "value1"), ("key2", "value2")]
# Test empty values
executor = create_executor("key1:\nkey2:")
executor._init_params()
assert executor.params == [("key1", ""), ("key2", "")]
# Test duplicate keys (which is allowed for params)
executor = create_executor("key1:value1\nkey1:value2")
executor._init_params()
assert executor.params == [("key1", "value1"), ("key1", "value2")]
# Test whitespace handling
executor = create_executor(" key1 : value1 \n key2 : value2 ")
executor._init_params()
assert executor.params == [("key1", "value1"), ("key2", "value2")]
# Test empty lines and extra whitespace
executor = create_executor("key1:value1\n\nkey2:value2\n\n")
executor._init_params()
assert executor.params == [("key1", "value1"), ("key2", "value2")]

View File

@ -0,0 +1,203 @@
import httpx
from core.app.entities.app_invoke_entities import InvokeFrom
from core.file import File, FileTransferMethod, FileType
from core.variables import FileVariable
from core.workflow.entities.variable_pool import VariablePool
from core.workflow.graph_engine import Graph, GraphInitParams, GraphRuntimeState
from core.workflow.nodes.answer import AnswerStreamGenerateRoute
from core.workflow.nodes.end import EndStreamParam
from core.workflow.nodes.http_request import (
BodyData,
HttpRequestNode,
HttpRequestNodeAuthorization,
HttpRequestNodeBody,
HttpRequestNodeData,
)
from models.enums import UserFrom
from models.workflow import WorkflowNodeExecutionStatus, WorkflowType
def test_plain_text_to_dict():
assert _plain_text_to_dict("aa\n cc:") == {"aa": "", "cc": ""}
assert _plain_text_to_dict("aa:bb\n cc:dd") == {"aa": "bb", "cc": "dd"}
assert _plain_text_to_dict("aa:bb\n cc:dd\n") == {"aa": "bb", "cc": "dd"}
assert _plain_text_to_dict("aa:bb\n\n cc : dd\n\n") == {
"aa": "bb", "cc": "dd"}
def test_http_request_node_binary_file(monkeypatch):
data = HttpRequestNodeData(
title="test",
method="post",
url="http://example.org/post",
authorization=HttpRequestNodeAuthorization(type="no-auth"),
headers="",
params="",
body=HttpRequestNodeBody(
type="binary",
data=[
BodyData(
key="file",
type="file",
value="",
file=["1111", "file"],
)
],
),
)
variable_pool = VariablePool(
system_variables={},
user_inputs={},
)
variable_pool.add(
["1111", "file"],
FileVariable(
name="file",
value=File(
tenant_id="1",
type=FileType.IMAGE,
transfer_method=FileTransferMethod.LOCAL_FILE,
related_id="1111",
),
),
)
node = HttpRequestNode(
id="1",
config={
"id": "1",
"data": data.model_dump(),
},
graph_init_params=GraphInitParams(
tenant_id="1",
app_id="1",
workflow_type=WorkflowType.WORKFLOW,
workflow_id="1",
graph_config={},
user_id="1",
user_from=UserFrom.ACCOUNT,
invoke_from=InvokeFrom.SERVICE_API,
call_depth=0,
),
graph=Graph(
root_node_id="1",
answer_stream_generate_routes=AnswerStreamGenerateRoute(
answer_dependencies={},
answer_generate_route={},
),
end_stream_param=EndStreamParam(
end_dependencies={},
end_stream_variable_selector_mapping={},
),
),
graph_runtime_state=GraphRuntimeState(
variable_pool=variable_pool,
start_at=0,
),
)
monkeypatch.setattr(
"core.workflow.nodes.http_request.executor.file_manager.download",
lambda *args, **kwargs: b"test",
)
monkeypatch.setattr(
"core.helper.ssrf_proxy.post",
lambda *args, **kwargs: httpx.Response(200, content=kwargs["content"]),
)
result = node._run()
assert result.status == WorkflowNodeExecutionStatus.SUCCEEDED
assert result.outputs is not None
assert result.outputs["body"] == "test"
def test_http_request_node_form_with_file(monkeypatch):
data = HttpRequestNodeData(
title="test",
method="post",
url="http://example.org/post",
authorization=HttpRequestNodeAuthorization(type="no-auth"),
headers="",
params="",
body=HttpRequestNodeBody(
type="form-data",
data=[
BodyData(
key="file",
type="file",
file=["1111", "file"],
),
BodyData(
key="name",
type="text",
value="test",
),
],
),
)
variable_pool = VariablePool(
system_variables={},
user_inputs={},
)
variable_pool.add(
["1111", "file"],
FileVariable(
name="file",
value=File(
tenant_id="1",
type=FileType.IMAGE,
transfer_method=FileTransferMethod.LOCAL_FILE,
related_id="1111",
),
),
)
node = HttpRequestNode(
id="1",
config={
"id": "1",
"data": data.model_dump(),
},
graph_init_params=GraphInitParams(
tenant_id="1",
app_id="1",
workflow_type=WorkflowType.WORKFLOW,
workflow_id="1",
graph_config={},
user_id="1",
user_from=UserFrom.ACCOUNT,
invoke_from=InvokeFrom.SERVICE_API,
call_depth=0,
),
graph=Graph(
root_node_id="1",
answer_stream_generate_routes=AnswerStreamGenerateRoute(
answer_dependencies={},
answer_generate_route={},
),
end_stream_param=EndStreamParam(
end_dependencies={},
end_stream_variable_selector_mapping={},
),
),
graph_runtime_state=GraphRuntimeState(
variable_pool=variable_pool,
start_at=0,
),
)
monkeypatch.setattr(
"core.workflow.nodes.http_request.executor.file_manager.download",
lambda *args, **kwargs: b"test",
)
def attr_checker(*args, **kwargs):
assert kwargs["data"] == {"name": "test"}
assert kwargs["files"] == {
"file": (None, b"test", "application/octet-stream")}
return httpx.Response(200, content=b"")
monkeypatch.setattr(
"core.helper.ssrf_proxy.post",
attr_checker,
)
result = node._run()
assert result.status == WorkflowNodeExecutionStatus.SUCCEEDED
assert result.outputs is not None
assert result.outputs["body"] == ""

View File

@ -2,7 +2,7 @@ version: '3'
services:
# API service
api:
image: langgenius/dify-api:0.13.2
image: langgenius/dify-api:0.14.0
restart: always
environment:
# Startup mode, 'api' starts the API server.
@ -227,7 +227,7 @@ services:
# worker service
# The Celery worker for processing the queue.
worker:
image: langgenius/dify-api:0.13.2
image: langgenius/dify-api:0.14.0
restart: always
environment:
CONSOLE_WEB_URL: ''
@ -397,7 +397,7 @@ services:
# Frontend web application.
web:
image: langgenius/dify-web:0.13.2
image: langgenius/dify-web:0.14.0
restart: always
environment:
# The base URL of console application api server, refers to the Console base URL of WEB service if console domain is

View File

@ -292,7 +292,7 @@ x-shared-env: &shared-api-worker-env
services:
# API service
api:
image: langgenius/dify-api:0.13.2
image: langgenius/dify-api:0.14.0
restart: always
environment:
# Use the shared environment variables.
@ -312,7 +312,7 @@ services:
# worker service
# The Celery worker for processing the queue.
worker:
image: langgenius/dify-api:0.13.2
image: langgenius/dify-api:0.14.0
restart: always
environment:
# Use the shared environment variables.
@ -331,7 +331,7 @@ services:
# Frontend web application.
web:
image: langgenius/dify-web:0.13.2
image: langgenius/dify-web:0.14.0
restart: always
environment:
CONSOLE_API_URL: ${CONSOLE_API_URL:-}

View File

@ -39,8 +39,7 @@ import Tooltip from '@/app/components/base/tooltip'
import { CopyIcon } from '@/app/components/base/copy-icon'
import { buildChatItemTree, getThreadMessages } from '@/app/components/base/chat/utils'
import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
import { correctProvider } from '@/utils'
import { TONE_LIST } from '@/config'
import cn from '@/utils/classnames'
dayjs.extend(utc)
dayjs.extend(timezone)
@ -300,22 +299,6 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
const isChatMode = appDetail?.mode !== 'completion'
const isAdvanced = appDetail?.mode === 'advanced-chat'
const targetTone = TONE_LIST.find((item: any) => {
let res = true
validatedParams.forEach((param) => {
res = item.config?.[param] === detail?.model_config.model?.completion_params?.[param]
})
return res
})?.name ?? 'custom'
const modelName = (detail.model_config as any).model?.name
const provideName = correctProvider((detail.model_config as any).model?.provider as any)
const {
currentModel,
currentProvider,
} = useTextGenerationCurrentProviderAndModelAndModelList(
{ provider: provideName, model: modelName },
)
const varList = (detail.model_config as any).user_input_form?.map((item: any) => {
const itemContent = item[Object.keys(item)[0]]
return {

View File

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

View File

@ -36,7 +36,7 @@ const CopyBtn = ({
>
<div
onMouseLeave={onMouseLeave}
className={'box-border p-0.5 flex items-center justify-center rounded-md bg-white cursor-pointer'}
className={'box-border p-0.5 flex items-center justify-center rounded-md bg-components-button-secondary-bg cursor-pointer'}
style={!isPlain
? {
boxShadow: '0px 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06)',
@ -44,7 +44,7 @@ const CopyBtn = ({
: {}}
onClick={onClickCopy}
>
<div className={`w-6 h-6 rounded-md hover:bg-gray-50 ${s.copyIcon} ${isCopied ? s.copied : ''}`}></div>
<div className={`w-6 h-6 rounded-md hover:bg-components-button-secondary-bg-hover ${s.copyIcon} ${isCopied ? s.copied : ''}`}></div>
</div>
</Tooltip>
</div>

View File

@ -49,18 +49,18 @@ export default function Drawer({
<Dialog.Overlay
className={cn('z-40 fixed inset-0', mask && 'bg-black bg-opacity-30')}
/>
<div className={cn('relative z-50 flex flex-col justify-between bg-background-body w-full max-w-sm p-6 overflow-hidden text-left align-middle shadow-xl', panelClassname)}>
<div className={cn('relative z-50 flex flex-col justify-between bg-components-panel-bg w-full max-w-sm p-6 overflow-hidden text-left align-middle shadow-xl', panelClassname)}>
<>
{title && <Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-gray-900"
className="text-lg font-medium leading-6 text-text-primary"
>
{title}
</Dialog.Title>}
{showClose && <Dialog.Title className="flex items-center mb-4" as="div">
<XMarkIcon className='w-4 h-4 text-gray-500' onClick={onClose} />
<XMarkIcon className='w-4 h-4 text-text-tertiary' onClick={onClose} />
</Dialog.Title>}
{description && <Dialog.Description className='text-gray-500 text-xs font-normal mt-2'>{description}</Dialog.Description>}
{description && <Dialog.Description className='text-text-tertiary text-xs font-normal mt-2'>{description}</Dialog.Description>}
{children}
</>
{footer || (footer === null

View File

@ -7,9 +7,10 @@ import { init } from 'emoji-mart'
import {
MagnifyingGlassIcon,
} from '@heroicons/react/24/outline'
import cn from '@/utils/classnames'
import Input from '@/app/components/base/input'
import Divider from '@/app/components/base/divider'
import { searchEmoji } from '@/utils/emoji'
import cn from '@/utils/classnames'
declare global {
// eslint-disable-next-line ts/no-namespace
@ -72,12 +73,12 @@ const EmojiPickerInner: FC<IEmojiPickerInnerProps> = ({
<div className='flex flex-col items-center w-full px-3 pb-2'>
<div className="relative w-full">
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<MagnifyingGlassIcon className="w-5 h-5 text-gray-400" aria-hidden="true" />
<MagnifyingGlassIcon className="w-5 h-5 text-text-quaternary" aria-hidden="true" />
</div>
<input
<Input
className="pl-10"
type="search"
id="search"
className='block w-full h-10 px-3 pl-10 text-sm font-normal bg-gray-100 rounded-lg'
placeholder="Search emojis..."
onChange={async (e: ChangeEvent<HTMLInputElement>) => {
if (e.target.value === '') {
@ -92,12 +93,12 @@ const EmojiPickerInner: FC<IEmojiPickerInnerProps> = ({
/>
</div>
</div>
<Divider className='m-0 mb-3' />
<Divider className='my-3' />
<div className="w-full max-h-[200px] overflow-x-hidden overflow-y-auto px-3">
{isSearching && <>
<div key={'category-search'} className='flex flex-col'>
<p className='font-medium uppercase text-xs text-[#101828] mb-1'>Search</p>
<p className='system-xs-medium-uppercase text-text-primary mb-1'>Search</p>
<div className='w-full h-full grid grid-cols-8 gap-1'>
{searchedEmojis.map((emoji: string, index: number) => {
return <div
@ -107,7 +108,7 @@ const EmojiPickerInner: FC<IEmojiPickerInnerProps> = ({
setSelectedEmoji(emoji)
}}
>
<div className='cursor-pointer w-8 h-8 p-1 flex items-center justify-center rounded-lg hover:ring-1 ring-offset-1 ring-gray-300'>
<div className='cursor-pointer w-8 h-8 p-1 flex items-center justify-center rounded-lg hover:ring-1 ring-offset-1 ring-components-input-border-hover'>
<em-emoji id={emoji} />
</div>
</div>
@ -118,7 +119,7 @@ const EmojiPickerInner: FC<IEmojiPickerInnerProps> = ({
{categories.map((category, index: number) => {
return <div key={`category-${index}`} className='flex flex-col'>
<p className='font-medium uppercase text-xs text-[#101828] mb-1'>{category.id}</p>
<p className='system-xs-medium-uppercase text-text-primary mb-1'>{category.id}</p>
<div className='w-full h-full grid grid-cols-8 gap-1'>
{category.emojis.map((emoji, index: number) => {
return <div
@ -128,7 +129,7 @@ const EmojiPickerInner: FC<IEmojiPickerInnerProps> = ({
setSelectedEmoji(emoji)
}}
>
<div className='cursor-pointer w-8 h-8 p-1 flex items-center justify-center rounded-lg hover:ring-1 ring-offset-1 ring-gray-300'>
<div className='cursor-pointer w-8 h-8 p-1 flex items-center justify-center rounded-lg hover:ring-1 ring-offset-1 ring-components-input-border-hover'>
<em-emoji id={emoji} />
</div>
</div>
@ -141,7 +142,7 @@ const EmojiPickerInner: FC<IEmojiPickerInnerProps> = ({
{/* Color Select */}
<div className={cn('p-3 pb-0', selectedEmoji === '' ? 'opacity-25' : '')}>
<p className='font-medium uppercase text-xs text-[#101828] mb-2'>Choose Style</p>
<p className='system-xs-medium-uppercase text-text-primary mb-2'>Choose Style</p>
<div className='w-full h-full grid grid-cols-8 gap-1'>
{backgroundColors.map((color) => {
return <div
@ -151,7 +152,7 @@ const EmojiPickerInner: FC<IEmojiPickerInnerProps> = ({
'cursor-pointer',
'hover:ring-1 ring-offset-1',
'inline-flex w-10 h-10 rounded-lg items-center justify-center',
color === selectedBackground ? 'ring-1 ring-gray-300' : '',
color === selectedBackground ? 'ring-1 ring-components-input-border-hover' : '',
)}
onClick={() => {
setSelectedBackground(color)

View File

@ -2,7 +2,6 @@
import type { FC } from 'react'
import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import s from './style.module.css'
import EmojiPickerInner from './Inner'
import cn from '@/utils/classnames'
import Divider from '@/app/components/base/divider'
@ -37,12 +36,12 @@ const EmojiPicker: FC<IEmojiPickerProps> = ({
isShow
closable={false}
wrapperClassName={className}
className={cn(s.container, '!w-[362px] !p-0')}
className={cn('flex flex-col max-h-[552px] border-[0.5px] border-divider-subtle rounded-xl shadow-xl p-0')}
>
<EmojiPickerInner
className="pt-3"
onSelect={handleSelectEmoji} />
<Divider className='m-0' />
<Divider className='mb-0 mt-3' />
<div className='w-full flex items-center justify-center p-3 gap-2'>
<Button className='w-full' onClick={() => {
onClose && onClose()

View File

@ -1,12 +0,0 @@
.container {
display: flex;
flex-direction: column;
align-items: flex-start;
width: 362px;
max-height: 552px;
border: 0.5px solid #EAECF0;
box-shadow: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03);
border-radius: 12px;
background: #fff;
}

View File

@ -36,8 +36,8 @@ const GridMask: FC<GridMaskProps> = ({
const drawRecord = useCallback(() => {
const canvas = canvasRef.current!
const ctx = ctxRef.current!
const rowNumber = parseInt(`${canvas.width / 24}`)
const colNumber = parseInt(`${canvas.height / 24}`)
const rowNumber = Number.parseInt(`${canvas.width / 24}`)
const colNumber = Number.parseInt(`${canvas.height / 24}`)
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.beginPath()
@ -82,9 +82,9 @@ const GridMask: FC<GridMaskProps> = ({
}, [])
return (
<div className={`relative bg-white ${wrapperClassName}`}>
<div className={`relative bg-components-panel-bg ${wrapperClassName}`}>
<canvas ref={canvasRef} className={`absolute inset-0 w-full h-full ${canvasClassName}`} />
<div className={`absolute w-full h-full z-[1] bg-gradient-to-b from-white/80 to-white rounded-lg ${gradientClassName}`} />
<div className={`absolute w-full h-full z-[1] bg-gradient-to-b from-background-body to-background-gradient-mask-transparent rounded-lg ${gradientClassName}`} />
<div className='relative z-[2]'>{children}</div>
</div>
)

View File

@ -11,7 +11,7 @@ const RadioUI: FC<Props> = ({
isChecked,
}) => {
return (
<div className={cn(isChecked ? 'border-[5px] border-[#155eef]' : 'border-[2px] border-gray-200', 'w-4 h-4 rounded-full')}>
<div className={cn(isChecked ? 'border-[5px] border-components-radio-border-checked' : 'border-[2px] border-components-radio-border', 'w-4 h-4 rounded-full')}>
</div>
)
}

View File

@ -23,10 +23,14 @@ const Item: FC<ItemProps> = ({
return (
<div
key={option.value}
className={cn('relative pb-2.5 text-text-tertiary system-sm-semibold-uppercase', !isActive && 'cursor-pointer', className)}
className={cn(
'relative pb-2.5 system-xl-semibold',
!isActive && 'cursor-pointer',
className,
)}
onClick={() => !isActive && onClick(option.value)}
>
<div className={cn(isActive && 'text-text-primary')}>{option.text}</div>
<div className={cn(isActive ? 'text-text-primary' : 'text-text-tertiary')}>{option.text}</div>
{isActive && (
<div className='absolute bottom-0 left-0 right-0 h-0.5 bg-util-colors-blue-blue-500'></div>
)}
@ -52,7 +56,7 @@ const TabSlider: FC<Props> = ({
itemClassName,
}) => {
return (
<div className={cn('flex space-x-6', !noBorderBottom && 'border-b border-[#EAECF0]', className)}>
<div className={cn(className, !noBorderBottom && 'border-b border-divider-subtle', 'flex space-x-6')}>
{options.map(option => (
<Item
isActive={option.value === value}

View File

@ -193,11 +193,11 @@ export default function AccountSetting({
<div className='mt-1 text-text-tertiary system-2xs-medium-uppercase'>ESC</div>
</div>
<div ref={scrollRef} className='w-full pb-4 bg-components-panel-bg overflow-y-auto'>
<div className={cn('sticky top-0 mx-8 pt-[27px] pb-2 mb-[18px] flex items-center bg-components-panel-bg z-20', scrolled && 'border-b')}>
<div className={cn('sticky top-0 mx-8 pt-[27px] pb-2 mb-[18px] flex items-center bg-components-panel-bg z-20', scrolled && 'border-b border-divider-regular')}>
<div className='shrink-0 text-text-primary title-2xl-semi-bold'>{activeItem?.name}</div>
{
activeItem?.description && (
<div className='shrink-0 ml-2 text-xs text-gray-600'>{activeItem?.description}</div>
<div className='shrink-0 ml-2 text-xs text-text-tertiary'>{activeItem?.description}</div>
)
}
{activeItem?.key === 'provider' && (

View File

@ -32,8 +32,7 @@ import {
} from '@/app/components/plugins/marketplace/hooks'
import type { Plugin } from '@/app/components/plugins/types'
import { PluginType } from '@/app/components/plugins/types'
// import { getMarketplacePluginsByCollectionId } from '@/app/components/plugins/marketplace/utils'
import type { MarketplaceCollection } from '@/app/components/plugins/marketplace/types'
import { getMarketplacePluginsByCollectionId } from '@/app/components/plugins/marketplace/utils'
type UseDefaultModelAndModelList = (
defaultModel: DefaultModelResponse | undefined,
@ -242,34 +241,11 @@ export const useUpdateModelProviders = () => {
return updateModelProviders
}
export const useMarketplace = () => {
const [marketplaceCollections] = useState<MarketplaceCollection[]>([])
const [marketplaceCollectionPluginsMap] = useState<Record<string, Plugin[]>>()
const [isLoading] = useState(false)
// const getCollectionPlugins = useCallback(async () => {
// setIsLoading(true)
// const collectionPlugins = await getMarketplacePluginsByCollectionId('')
// setIsLoading(false)
// setCollectionPlugins(collectionPlugins)
// }, [])
// useEffect(() => {
// getCollectionPlugins()
// }, [getCollectionPlugins])
return {
isLoading,
marketplaceCollections,
marketplaceCollectionPluginsMap,
}
}
export const useMarketplaceAllPlugins = (providers: ModelProvider[], searchText: string) => {
const exclude = useMemo(() => {
return providers.map(provider => provider.provider.replace(/(.+)\/([^/]+)$/, '$1'))
}, [providers])
const [collectionPlugins, setCollectionPlugins] = useState<Plugin[]>([])
const {
plugins,
@ -278,6 +254,16 @@ export const useMarketplaceAllPlugins = (providers: ModelProvider[], searchText:
isLoading,
} = useMarketplacePlugins()
const getCollectionPlugins = useCallback(async () => {
const collectionPlugins = await getMarketplacePluginsByCollectionId('__model-settings-pinned-models')
setCollectionPlugins(collectionPlugins)
}, [])
useEffect(() => {
getCollectionPlugins()
}, [getCollectionPlugins])
useEffect(() => {
if (searchText) {
queryPluginsWithDebounced({
@ -298,8 +284,23 @@ export const useMarketplaceAllPlugins = (providers: ModelProvider[], searchText:
}
}, [queryPlugins, queryPluginsWithDebounced, searchText, exclude])
const allPlugins = useMemo(() => {
const allPlugins = [...collectionPlugins.filter(plugin => !exclude.includes(plugin.plugin_id))]
if (plugins?.length) {
for (let i = 0; i < plugins.length; i++) {
const plugin = plugins[i]
if (plugin.type !== 'bundle' && !allPlugins.find(p => p.plugin_id === plugin.plugin_id))
allPlugins.push(plugin)
}
}
return allPlugins
}, [plugins, collectionPlugins, exclude])
return {
plugins: plugins?.filter(plugin => plugin.type !== 'bundle'),
plugins: allPlugins,
isLoading,
}
}

View File

@ -21,7 +21,6 @@ import {
} from './declarations'
import {
useDefaultModel,
useMarketplace,
useMarketplaceAllPlugins,
useUpdateModelList,
useUpdateModelProviders,
@ -121,11 +120,6 @@ const ModelProviderPage = ({ searchText }: Props) => {
const [collapse, setCollapse] = useState(false)
const locale = getLocaleOnClient()
const {
marketplaceCollections,
marketplaceCollectionPluginsMap,
isLoading: isPluginsLoading,
} = useMarketplace()
const {
plugins: allPlugins,
isLoading: isAllPluginsLoading,
@ -143,7 +137,7 @@ const ModelProviderPage = ({ searchText }: Props) => {
<div className={cn('flex items-center mb-2')}>
<div className='grow text-text-primary system-md-semibold'>{t('common.modelProvider.models')}</div>
<div className={cn(
'shrink-0 relative flex items-center justify-end gap-2 p-0.5 rounded-lg border border-transparent',
'shrink-0 relative flex items-center justify-end gap-2 p-px rounded-lg border border-transparent',
defaultModelNotConfigured && 'pl-2 bg-components-panel-bg-blur border-components-panel-border shadow-xs',
)}>
{defaultModelNotConfigured && <div className='absolute top-0 bottom-0 right-0 left-0 opacity-40' style={{ background: 'linear-gradient(92deg, rgba(247, 144, 9, 0.25) 0%, rgba(255, 255, 255, 0.00) 100%)' }} />}
@ -164,7 +158,7 @@ const ModelProviderPage = ({ searchText }: Props) => {
</div>
</div>
{!filteredConfiguredProviders?.length && (
<div className='mb-2 p-4 rounded-[10px]' style={{ background: 'linear-gradient(90deg, rgba(200, 206, 218, 0.20) 0%, rgba(200, 206, 218, 0.04) 100%)' }}>
<div className='mb-2 p-4 rounded-[10px] bg-workflow-process-bg'>
<div className='w-10 h-10 flex items-center justify-center rounded-[10px] border-[0.5px] border-components-card-border bg-components-card-bg shadow-lg backdrop-blur'>
<RiBrainLine className='w-5 h-5 text-text-primary' />
</div>
@ -213,20 +207,7 @@ const ModelProviderPage = ({ searchText }: Props) => {
</Link>
</div>
</div>
{!collapse && (isPluginsLoading || isAllPluginsLoading) && <Loading type='area' />}
{
!isPluginsLoading && !collapse && (
<List
marketplaceCollections={marketplaceCollections || []}
marketplaceCollectionPluginsMap={marketplaceCollectionPluginsMap || {}}
plugins={undefined}
showInstallButton
locale={locale}
cardContainerClassName='grid grid-cols-2 gap-2'
cardRender={cardRender}
/>
)
}
{!collapse && isAllPluginsLoading && <Loading type='area' />}
{
!isAllPluginsLoading && !collapse && (
<List

View File

@ -6,6 +6,7 @@ import type {
import { useLanguage } from '../hooks'
import { CubeOutline } from '@/app/components/base/icons/src/vender/line/shapes'
import { OpenaiViolet } from '@/app/components/base/icons/src/public/llm'
import cn from '@/utils/classnames'
type ModelIconProps = {
provider?: Model | ModelProvider
@ -20,7 +21,7 @@ const ModelIcon: FC<ModelIconProps> = ({
const language = useLanguage()
if (provider?.provider.includes('openai') && (modelName?.startsWith('gpt-4') || modelName?.includes('4o')))
return <OpenaiViolet className={`w-4 h-4 ${className}`}/>
return <OpenaiViolet className={cn('w-4 h-4', className)}/>
if (provider?.icon_small) {
return (
@ -28,17 +29,17 @@ const ModelIcon: FC<ModelIconProps> = ({
<img
alt='model-icon'
src={`${provider.icon_small[language] || provider.icon_small.en_US}`}
className={`w-4 h-4 ${className}`}
className={cn('w-4 h-4', className)}
/>
)
}
return (
<div className={`
flex items-center justify-center w-6 h-6 rounded border-[0.5px] border-black/5 bg-gray-50
${className}
`}>
<CubeOutline className='w-4 h-4 text-gray-400' />
<div className={cn(
'flex items-center justify-center w-6 h-6 rounded border-[0.5px] border-black/5 bg-gray-50',
className,
)}>
<CubeOutline className='w-4 h-4 text-text-quaternary' />
</div>
)
}

View File

@ -20,6 +20,7 @@ import Radio from '@/app/components/base/radio'
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
import ToolSelector from '@/app/components/plugins/plugin-detail-panel/tool-selector'
import AppSelector from '@/app/components/plugins/plugin-detail-panel/app-selector'
import RadioE from '@/app/components/base/radio/ui'
type FormProps = {
className?: string
@ -177,17 +178,15 @@ const Form: FC<FormProps> = ({
}).map(option => (
<div
className={`
flex items-center px-3 py-2 rounded-lg border border-gray-100 bg-gray-25 cursor-pointer
${value[variable] === option.value && 'bg-white border-[1.5px] border-primary-400 shadow-sm'}
flex items-center gap-2 px-3 py-2 rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg cursor-pointer
${value[variable] === option.value && 'bg-components-option-card-option-selected-bg border-[1.5px] border-components-option-card-option-selected-border shadow-sm'}
${disabled && '!cursor-not-allowed opacity-60'}
`}
onClick={() => handleFormChange(variable, option.value)}
key={`${variable}-${option.value}`}
>
<div className={`
flex justify-center items-center mr-2 w-4 h-4 border border-gray-300 rounded-full
${value[variable] === option.value && 'border-[5px] border-primary-600'}
`} />
<RadioE isChecked={value[variable] === option.value} />
<div className='system-sm-regular text-text-secondary'>{option.label[language] || option.label.en_US}</div>
</div>
))

View File

@ -41,11 +41,11 @@ const Input: FC<InputProps> = ({
<input
tabIndex={0}
className={`
block px-3 w-full h-8 bg-components-input-bg-normal text-sm rounded-lg border border-transparent
block px-3 w-full h-8 bg-components-input-bg-normal text-sm text-components-input-text-filled rounded-lg border border-transparent
appearance-none outline-none caret-primary-600
hover:border-[rgba(0,0,0,0.08)] hover:bg-state-hover-alt
focus:bg-white focus:border-gray-300 focus:shadow-xs
placeholder:text-sm placeholder:text-gray-400
hover:border-components-input-border-hover hover:bg-components-input-bg-hover
focus:bg-components-input-bg-active focus:border-components-input-border-active focus:shadow-xs
placeholder:text-sm placeholder:text-text-tertiary
${validated && 'pr-[30px]'}
${className}
`}

View File

@ -280,10 +280,10 @@ const ModelModal: FC<ModelModalProps> = ({
<PortalToFollowElem open>
<PortalToFollowElemContent className='w-full h-full z-[60]'>
<div className='fixed inset-0 flex items-center justify-center bg-black/[.25]'>
<div className='mx-2 w-[640px] max-h-[calc(100vh-120px)] bg-white shadow-xl rounded-2xl overflow-y-auto'>
<div className='mx-2 w-[640px] max-h-[calc(100vh-120px)] bg-components-panel-bg shadow-xl rounded-2xl overflow-y-auto'>
<div className='px-8 pt-8'>
<div className='flex justify-between items-center mb-2'>
<div className='text-xl font-semibold text-gray-900'>{renderTitlePrefix()}</div>
<div className='text-xl font-semibold text-text-primary'>{renderTitlePrefix()}</div>
<ProviderIcon provider={provider} />
</div>
@ -297,7 +297,7 @@ const ModelModal: FC<ModelModalProps> = ({
isEditMode={isEditMode}
/>
<div className='mt-1 mb-4 border-t-[0.5px] border-t-gray-100' />
<div className='mt-1 mb-4 border-t-[0.5px] border-t-divider-regular' />
<ModelLoadBalancingConfigs withSwitch {...{
draftConfig,
setDraftConfig,
@ -306,7 +306,7 @@ const ModelModal: FC<ModelModalProps> = ({
configurationMethod: configurateMethod,
}} />
<div className='sticky bottom-0 flex justify-between items-center mt-2 -mx-2 pt-4 px-2 pb-6 flex-wrap gap-y-2 bg-white'>
<div className='sticky bottom-0 flex justify-between items-center mt-2 -mx-2 pt-4 px-2 pb-6 flex-wrap gap-y-2 bg-components-panel-bg'>
{
(provider.help && (provider.help.title || provider.help.url))
? (
@ -326,8 +326,9 @@ const ModelModal: FC<ModelModalProps> = ({
{
isEditMode && (
<Button
variant='warning'
size='large'
className='mr-2 text-[#D92D20]'
className='mr-2'
onClick={() => setShowConfirm(true)}
>
{t('common.operation.remove')}
@ -357,21 +358,21 @@ const ModelModal: FC<ModelModalProps> = ({
</div>
</div>
</div>
<div className='border-t-[0.5px] border-t-black/5'>
<div className='border-t-[0.5px] border-t-divider-regular'>
{
(validatedStatusState.status === ValidatedStatus.Error && validatedStatusState.message)
? (
<div className='flex px-[10px] py-3 bg-[#FEF3F2] text-xs text-[#D92D20]'>
<div className='flex px-[10px] py-3 bg-background-section-burn text-xs text-[#D92D20]'>
<RiErrorWarningFill className='mt-[1px] mr-2 w-[14px] h-[14px]' />
{validatedStatusState.message}
</div>
)
: (
<div className='flex justify-center items-center py-3 bg-gray-50 text-xs text-gray-500'>
<Lock01 className='mr-1 w-3 h-3 text-gray-500' />
<div className='flex justify-center items-center py-3 bg-background-section-burn text-xs text-text-tertiary'>
<Lock01 className='mr-1 w-3 h-3 text-text-tertiary' />
{t('common.modelProvider.encrypted.front')}
<a
className='text-primary-600 mx-1'
className='text-text-accent mx-1'
target='_blank' rel='noopener noreferrer'
href='https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html'
>

View File

@ -1,6 +1,7 @@
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { PlusCircle } from '@/app/components/base/icons/src/vender/solid/general'
import cn from '@/utils/classnames'
type AddModelButtonProps = {
className?: string
@ -14,10 +15,7 @@ const AddModelButton: FC<AddModelButtonProps> = ({
return (
<span
className={`
shrink-0 flex items-center px-1.5 h-6 text-xs font-medium text-gray-500 cursor-pointer
hover:bg-primary-50 hover:text-primary-600 rounded-md ${className}
`}
className={cn('shrink-0 flex items-center px-1.5 h-6 text-text-tertiary system-xs-medium cursor-pointer hover:bg-components-button-ghost-bg-hover hover:text-components-button-ghost-text rounded-md', className)}
onClick={onClick}
>
<PlusCircle className='mr-1 w-3 h-3' />

View File

@ -66,8 +66,8 @@ const CredentialPanel: FC<CredentialPanelProps> = ({
<>
{
provider.provider_credential_schema && (
<div className='shrink-0 relative ml-1 p-1 w-[112px] rounded-lg bg-white/[0.3] border-[0.5px] border-black/5'>
<div className='flex items-center justify-between mb-1 pt-1 pl-2 pr-[7px] h-5 text-xs font-medium text-gray-500'>
<div className='shrink-0 relative ml-1 p-1 w-[112px] rounded-lg bg-white/[0.18] border-[0.5px] border-components-panel-border'>
<div className='flex items-center justify-between mb-1 pt-1 pl-2 pr-[7px] h-5 system-xs-medium-uppercase text-text-tertiary'>
API-KEY
<Indicator color={isCustomConfigured ? 'green' : 'red'} />
</div>

View File

@ -13,7 +13,6 @@ import type {
} from '../declarations'
import { ConfigurationMethodEnum } from '../declarations'
import {
DEFAULT_BACKGROUND_COLOR,
MODEL_PROVIDER_QUOTA_GET_PAID,
modelTypeFormat,
} from '../utils'
@ -27,6 +26,7 @@ import { fetchModelProviderModelList } from '@/service/common'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import { IS_CE_EDITION } from '@/config'
import { useAppContext } from '@/context/app-context'
import cn from '@/utils/classnames'
export const UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST = 'UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST'
type ProviderAddedCardProps = {
@ -82,8 +82,11 @@ const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
return (
<div
className='mb-2 rounded-xl border-[0.5px] border-black/5 shadow-xs'
style={{ background: provider.background || DEFAULT_BACKGROUND_COLOR }}
className={cn(
'mb-2 rounded-xl border-[0.5px] border-divider-regular shadow-xs bg-third-party-model-bg-default',
provider.provider === 'langgenius/openai/openai' && 'bg-third-party-model-bg-openai',
provider.provider === 'langgenius/anthropic/anthropic' && 'bg-third-party-model-bg-anthropic',
)}
>
<div className='flex pl-3 py-2 pr-2 rounded-t-xl'>
<div className='grow px-1 pt-1 pb-0.5'>
@ -119,7 +122,7 @@ const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
</div>
{
collapsed && (
<div className='group flex items-center justify-between pl-2 py-1.5 pr-[11px] border-t border-t-black/5 bg-white/30 text-xs font-medium text-gray-500'>
<div className='group flex items-center justify-between pl-2 py-1.5 pr-[11px] border-t border-t-divider-subtle text-text-tertiary system-xs-medium'>
{(showQuota || !notConfigured) && (
<>
<div className='group-hover:hidden flex items-center pl-1 pr-1.5 h-6 leading-6'>
@ -131,7 +134,7 @@ const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
{!loading && <RiArrowRightSLine className='w-4 h-4' />}
</div>
<div
className='hidden group-hover:flex items-center pl-1 pr-1.5 h-6 rounded-lg hover:bg-white cursor-pointer'
className='hidden group-hover:flex items-center pl-1 pr-1.5 h-6 rounded-lg hover:bg-components-button-ghost-bg-hover cursor-pointer'
onClick={handleOpenModelList}
>
{
@ -151,7 +154,7 @@ const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
{!showQuota && notConfigured && (
<div className='flex items-center pl-1 pr-1.5 h-6'>
<RiInformation2Fill className='mr-1 w-4 h-4 text-text-accent' />
<span>{t('common.modelProvider.configureTip')}</span>
<span className='text-text-secondary system-xs-medium'>{t('common.modelProvider.configureTip')}</span>
</div>
)}
{

View File

@ -49,7 +49,7 @@ const ModelListItem = ({ model, provider, isConfigurable, onConfig, onModifyLoad
key={model.model}
className={classNames(
'group flex items-center pl-2 pr-2.5 h-8 rounded-lg',
isConfigurable && 'hover:bg-gray-50',
isConfigurable && 'hover:bg-components-panel-on-panel-item-bg-hover',
model.deprecated && 'opacity-60',
)}
>
@ -59,14 +59,14 @@ const ModelListItem = ({ model, provider, isConfigurable, onConfig, onModifyLoad
modelName={model.model}
/>
<ModelName
className='grow text-sm font-normal text-gray-900'
className='grow system-md-regular text-text-secondary'
modelItem={model}
showModelType
showMode
showContextSize
>
{modelLoadBalancingEnabled && !model.deprecated && model.load_balancing_enabled && (
<ModelBadge className='ml-1 uppercase text-indigo-600 border-indigo-300'>
<ModelBadge className='ml-1 uppercase text-text-accent-secondary border-text-accent-secondary'>
<Balance className='w-3 h-3 mr-0.5' />
{t('common.modelProvider.loadBalancingHeadline')}
</ModelBadge>
@ -77,20 +77,22 @@ const ModelListItem = ({ model, provider, isConfigurable, onConfig, onModifyLoad
model.fetch_from === ConfigurationMethodEnum.customizableModel
? (isCurrentWorkspaceManager && (
<Button
className='hidden group-hover:flex h-7'
size='small'
className='hidden group-hover:flex'
onClick={() => onConfig({ __model_name: model.model, __model_type: model.model_type })}
>
<Settings01 className='mr-[5px] w-3.5 h-3.5' />
<Settings01 className='mr-1 w-3.5 h-3.5' />
{t('common.modelProvider.config')}
</Button>
))
: (isCurrentWorkspaceManager && (modelLoadBalancingEnabled || plan.type === Plan.sandbox) && !model.deprecated && [ModelStatusEnum.active, ModelStatusEnum.disabled].includes(model.status))
? (
<Button
className='opacity-0 group-hover:opacity-100 h-[28px] transition-opacity'
size='small'
className='opacity-0 group-hover:opacity-100 transition-opacity'
onClick={() => onModifyLoadBalancing?.(model)}
>
<Balance className='mr-1 w-[14px] h-[14px]' />
<Balance className='mr-1 w-3.5 h-3.5' />
{t('common.modelProvider.configLoadBalancing')}
</Button>
)

View File

@ -145,19 +145,19 @@ const ModelLoadBalancingConfigs = ({
<>
<div
className={classNames(
'min-h-16 bg-gray-50 border rounded-xl transition-colors',
(withSwitch || !draftConfig.enabled) ? 'border-gray-200' : 'border-primary-400',
'min-h-16 bg-components-panel-bg border rounded-xl transition-colors',
(withSwitch || !draftConfig.enabled) ? 'border-components-panel-border' : 'border-util-colors-blue-blue-600',
(withSwitch || draftConfig.enabled) ? 'cursor-default' : 'cursor-pointer',
className,
)}
onClick={(!withSwitch && !draftConfig.enabled) ? () => toggleModalBalancing(true) : undefined}
>
<div className='flex items-center px-[15px] py-3 gap-2 select-none'>
<div className='grow-0 shrink-0 flex items-center justify-center w-8 h-8 text-primary-600 bg-indigo-50 border border-indigo-100 rounded-lg'>
<div className='grow-0 shrink-0 flex items-center justify-center w-8 h-8 text-util-colors-blue-blue-600 bg-util-colors-indigo-indigo-50 border border-util-colors-indigo-indigo-100 rounded-lg'>
<Balance className='w-4 h-4' />
</div>
<div className='grow'>
<div className='flex items-center gap-1 text-sm'>
<div className='flex items-center gap-1 text-sm text-text-primary'>
{t('common.modelProvider.loadBalancing')}
<Tooltip
popupContent={t('common.modelProvider.loadBalancingInfo')}
@ -165,7 +165,7 @@ const ModelLoadBalancingConfigs = ({
triggerClassName='w-3 h-3'
/>
</div>
<div className='text-xs text-gray-500'>{t('common.modelProvider.loadBalancingDescription')}</div>
<div className='text-xs text-text-tertiary'>{t('common.modelProvider.loadBalancingDescription')}</div>
</div>
{
withSwitch && (
@ -184,7 +184,7 @@ const ModelLoadBalancingConfigs = ({
{draftConfig.configs.map((config, index) => {
const isProviderManaged = config.name === '__inherit__'
return (
<div key={config.id || index} className='group flex items-center px-3 h-10 bg-white border border-gray-200 rounded-lg shadow-xs'>
<div key={config.id || index} className='group flex items-center px-3 h-10 bg-components-panel-on-panel-item-bg border border-components-panel-border rounded-lg shadow-xs'>
<div className='grow flex items-center'>
<div className='flex items-center justify-center mr-2 w-3 h-3'>
{(config.in_cooldown && Boolean(config.ttl))
@ -201,7 +201,7 @@ const ModelLoadBalancingConfigs = ({
{isProviderManaged ? t('common.modelProvider.defaultConfig') : config.name}
</div>
{isProviderManaged && (
<span className='px-1 text-2xs uppercase text-gray-500 border border-black/8 rounded-[5px]'>{t('common.modelProvider.providerManaged')}</span>
<span className='px-1 text-2xs uppercase text-text-tertiary border border-divider-regular rounded-[5px]'>{t('common.modelProvider.providerManaged')}</span>
)}
</div>
<div className='flex items-center gap-1'>
@ -209,18 +209,18 @@ const ModelLoadBalancingConfigs = ({
<>
<div className='flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100'>
<span
className='flex items-center justify-center w-8 h-8 text-gray-500 bg-white rounded-lg transition-colors cursor-pointer hover:bg-black/5'
className='flex items-center justify-center w-8 h-8 text-text-tertiary bg-components-button-secondary-bg rounded-lg transition-colors cursor-pointer hover:bg-components-button-secondary-bg-hover'
onClick={() => toggleEntryModal(index, config)}
>
<Edit02 className='w-4 h-4' />
</span>
<span
className='flex items-center justify-center w-8 h-8 text-gray-500 bg-white rounded-lg transition-colors cursor-pointer hover:bg-black/5'
className='flex items-center justify-center w-8 h-8 text-text-tertiary bg-components-button-secondary-bg rounded-lg transition-colors cursor-pointer hover:bg-components-button-secondary-bg-hover'
onClick={() => updateConfigEntry(index, () => undefined)}
>
<RiDeleteBinLine className='w-4 h-4' />
</span>
<span className='mr-2 h-3 border-r border-r-gray-100' />
<span className='mr-2 h-3 border-r border-r-divider-subtle' />
</div>
</>
)}
@ -247,7 +247,7 @@ const ModelLoadBalancingConfigs = ({
)}
{
draftConfig.enabled && draftConfig.configs.length < 2 && (
<div className='flex items-center px-6 h-[34px] text-xs text-gray-700 bg-black/2 border-t border-t-black/5'>
<div className='flex items-center px-6 h-[34px] text-xs text-text-secondary bg-components-panel-bg border-t border-t-divider-subtle'>
<AlertTriangle className='mr-1 w-3 h-3 text-[#f79009]' />
{t('common.modelProvider.loadBalancingLeastKeyWarning')}
</div>
@ -257,7 +257,7 @@ const ModelLoadBalancingConfigs = ({
{!modelLoadBalancingEnabled && !IS_CE_EDITION && (
<GridMask canvasClassName='!rounded-xl'>
<div className='flex items-center justify-between mt-2 px-4 h-14 border-[0.5px] border-gray-200 rounded-xl shadow-md'>
<div className='flex items-center justify-between mt-2 px-4 h-14 border-[0.5px] border-components-panel-border rounded-xl shadow-md'>
<div
className={classNames('text-sm font-semibold leading-tight text-gradient', s.textGradient)}
>

View File

@ -119,7 +119,7 @@ const ModelLoadBalancingModal = ({ provider, model, open = false, onClose, onSav
modelName={model!.model}
/>
<ModelName
className='grow text-sm font-normal text-gray-900'
className='grow system-md-regular text-text-secondary'
modelItem={model!}
showModelType
showMode
@ -137,20 +137,20 @@ const ModelLoadBalancingModal = ({ provider, model, open = false, onClose, onSav
<div className='py-2'>
<div
className={classNames(
'min-h-16 bg-gray-50 border rounded-xl transition-colors',
draftConfig.enabled ? 'border-gray-200 cursor-pointer' : 'border-primary-400 cursor-default',
'min-h-16 bg-components-panel-bg border rounded-xl transition-colors',
draftConfig.enabled ? 'border-components-panel-border cursor-pointer' : 'border-util-colors-blue-blue-600 cursor-default',
)}
onClick={draftConfig.enabled ? () => toggleModalBalancing(false) : undefined}
>
<div className='flex items-center px-[15px] py-3 gap-2 select-none'>
<div className='grow-0 shrink-0 flex items-center justify-center w-8 h-8 bg-white border rounded-lg'>
<div className='grow-0 shrink-0 flex items-center justify-center w-8 h-8 bg-components-card-bg border border-components-card-border rounded-lg'>
{Boolean(model) && (
<ModelIcon className='shrink-0' provider={provider} modelName={model!.model} />
)}
</div>
<div className='grow'>
<div className='text-sm'>{t('common.modelProvider.providerManaged')}</div>
<div className='text-xs text-gray-500'>{t('common.modelProvider.providerManagedDescription')}</div>
<div className='text-sm text-text-secondary'>{t('common.modelProvider.providerManaged')}</div>
<div className='text-xs text-text-tertiary'>{t('common.modelProvider.providerManagedDescription')}</div>
</div>
</div>
</div>

View File

@ -8,6 +8,7 @@ import {
} from '@remixicon/react'
import { PreferredProviderTypeEnum } from '../declarations'
import Button from '@/app/components/base/button'
import cn from '@/utils/classnames'
type SelectorProps = {
value?: string
@ -34,11 +35,11 @@ const Selector: FC<SelectorProps> = ({
<Popover.Button>
{
({ open }) => (
<Button className={`
px-0 w-6 h-6 bg-white rounded-md
${open && '!bg-gray-100'}
`}>
<RiMoreFill className='w-3 h-3 text-gray-700' />
<Button className={cn(
'px-0 w-6 h-6 rounded-md',
open && 'bg-components-button-secondary-bg-hover',
)}>
<RiMoreFill className='w-3 h-3' />
</Button>
)
}
@ -49,18 +50,18 @@ const Selector: FC<SelectorProps> = ({
leaveFrom='opacity-100'
leaveTo='opacity-0'
>
<Popover.Panel className='absolute top-7 right-0 w-[144px] bg-white border-[0.5px] border-gray-200 rounded-lg shadow-lg z-10'>
<Popover.Panel className='absolute top-7 right-0 w-[144px] bg-components-panel-bg border-[0.5px] border-components-panel-border rounded-lg shadow-lg z-10'>
<div className='p-1'>
<div className='px-3 pt-2 pb-1 text-sm font-medium text-gray-700'>{t('common.modelProvider.card.priorityUse')}</div>
<div className='px-3 pt-2 pb-1 text-sm font-medium text-text-secondary'>{t('common.modelProvider.card.priorityUse')}</div>
{
options.map(option => (
<Popover.Button as={Fragment} key={option.key}>
<div
className='flex items-center justify-between px-3 h-9 text-sm text-gray-700 rounded-lg cursor-pointer hover:bg-gray-50'
className='flex items-center justify-between px-3 h-9 text-sm text-text-secondary rounded-lg cursor-pointer hover:bg-components-panel-on-panel-item-bg-hover'
onClick={() => onSelect(option.key)}
>
<div className='grow'>{option.text}</div>
{value === option.key && <RiCheckLine className='w-4 h-4 text-primary-600' />}
{value === option.key && <RiCheckLine className='w-4 h-4 text-text-accent' />}
</div>
</Popover.Button>
))

View File

@ -9,8 +9,8 @@ const PriorityUseTip = () => {
<Tooltip
popupContent={t('common.modelProvider.priorityUsing') || ''}
>
<div className='absolute -right-[5px] -top-[5px] bg-indigo-50 rounded-[5px] border-[0.5px] border-indigo-100 cursor-pointer'>
<ChevronDownDouble className='rotate-180 w-3 h-3 text-indigo-600' />
<div className='absolute -right-[5px] -top-[5px] bg-util-colors-indigo-indigo-50 rounded-[5px] border-[0.5px] border-components-panel-border-subtle shadow-xs cursor-pointer'>
<ChevronDownDouble className='rotate-180 w-3 h-3 text-util-colors-indigo-indigo-600' />
</div>
</Tooltip>
)

View File

@ -28,8 +28,8 @@ const QuotaPanel: FC<QuotaPanelProps> = ({
const openaiOrAnthropic = MODEL_PROVIDER_QUOTA_GET_PAID.includes(provider.provider)
return (
<div className='group relative shrink-0 min-w-[112px] px-3 py-2 rounded-lg bg-white/[0.3] border-[0.5px] border-black/5'>
<div className='flex items-center mb-2 h-4 text-xs font-medium text-gray-500'>
<div className='group relative shrink-0 min-w-[112px] px-3 py-2 rounded-lg bg-white/[0.18] border-[0.5px] border-components-panel-border shadow-xs'>
<div className='flex items-center mb-2 h-4 system-xs-medium-uppercase text-text-tertiary'>
{t('common.modelProvider.quota')}
<Tooltip popupContent={
openaiOrAnthropic
@ -40,8 +40,8 @@ const QuotaPanel: FC<QuotaPanelProps> = ({
</div>
{
currentQuota && (
<div className='flex items-center h-4 text-xs text-gray-500'>
<span className='mr-0.5 text-sm font-semibold text-gray-700'>{formatNumber((currentQuota?.quota_limit || 0) - (currentQuota?.quota_used || 0))}</span>
<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>
{
currentQuota?.quota_unit === QuotaUnitEnum.tokens && 'Tokens'
}

View File

@ -1,45 +0,0 @@
import type { FC } from 'react'
type TabProps = {
active: string
onSelect: (active: string) => void
}
const Tab: FC<TabProps> = ({
active,
onSelect,
}) => {
const tabs = [
{
key: 'all',
text: 'All',
},
{
key: 'added',
text: 'Added',
},
{
key: 'build-in',
text: 'Build-in',
},
]
return (
<div className='flex items-center'>
{
tabs.map(tab => (
<div
key={tab.key}
className={`
flex items-center mr-1 px-[5px] h-[18px] rounded-md text-xs cursor-pointer
${active === tab.key ? 'bg-gray-200 font-medium text-gray-900' : 'text-gray-500 font-normal'}
`}
onClick={() => onSelect(tab.key)}
>
{tab.text}
</div>
))
}
</div>
)
}
export default Tab

View File

@ -1,4 +0,0 @@
.vender {
background: linear-gradient(131deg, #2250F2 0%, #0EBCF3 100%);
background-clip: text;
}

View File

@ -1,103 +0,0 @@
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiAddLine,
} from '@remixicon/react'
import type {
ModelProvider,
} from '../declarations'
import { ConfigurationMethodEnum } from '../declarations'
import {
DEFAULT_BACKGROUND_COLOR,
modelTypeFormat,
} from '../utils'
import {
useLanguage,
} from '../hooks'
import ModelBadge from '../model-badge'
import ProviderIcon from '../provider-icon'
import s from './index.module.css'
import { Settings01 } from '@/app/components/base/icons/src/vender/line/general'
import Button from '@/app/components/base/button'
import { useAppContext } from '@/context/app-context'
type ProviderCardProps = {
provider: ModelProvider
onOpenModal: (configurateMethod: ConfigurationMethodEnum) => void
}
const ProviderCard: FC<ProviderCardProps> = ({
provider,
onOpenModal,
}) => {
const { t } = useTranslation()
const language = useLanguage()
const { isCurrentWorkspaceManager } = useAppContext()
const configurateMethods = provider.configurate_methods.filter(method => method !== ConfigurationMethodEnum.fetchFromRemote)
return (
<div
className='group relative flex flex-col px-4 py-3 h-[148px] border-[0.5px] border-black/5 rounded-xl shadow-xs hover:shadow-lg'
style={{ background: provider.background || DEFAULT_BACKGROUND_COLOR }}
>
<div className='grow h-0'>
<div className='py-0.5'>
<ProviderIcon provider={provider} />
</div>
{
provider.description && (
<div
className='mt-1 leading-4 text-xs text-black/[48] line-clamp-4'
title={provider.description[language] || provider.description.en_US}
>
{provider.description[language] || provider.description.en_US}
</div>
)
}
</div>
<div className='shrink-0'>
<div className={'flex flex-wrap group-hover:hidden gap-0.5'}>
{
provider.supported_model_types.map(modelType => (
<ModelBadge key={modelType}>
{modelTypeFormat(modelType)}
</ModelBadge>
))
}
</div>
<div className={`hidden group-hover:grid grid-cols-${configurateMethods.length} gap-1`}>
{
configurateMethods.map((method) => {
if (method === ConfigurationMethodEnum.predefinedModel) {
return (
<Button
key={method}
className={'h-7 text-xs shrink-0'}
onClick={() => onOpenModal(method)}
disabled={!isCurrentWorkspaceManager}
>
<Settings01 className={`mr-[5px] w-3.5 h-3.5 ${s.icon}`} />
<span className='text-xs inline-flex items-center justify-center overflow-ellipsis shrink-0'>{t('common.operation.setup')}</span>
</Button>
)
}
return (
<Button
key={method}
className='px-0 h-7 text-xs'
onClick={() => onOpenModal(method)}
disabled={!isCurrentWorkspaceManager}
>
<RiAddLine className='mr-[5px] w-3.5 h-3.5' />
{t('common.modelProvider.addModel')}
</Button>
)
})
}
</div>
</div>
</div>
)
}
export default ProviderCard

View File

@ -20,8 +20,6 @@ import {
export const MODEL_PROVIDER_QUOTA_GET_PAID = ['langgenius/anthropic/anthropic', 'langgenius/openai/openai', 'langgenius/azure_openai/azure_openai']
export const DEFAULT_BACKGROUND_COLOR = '#F3F4F6'
export const isNullOrUndefined = (value: any) => {
return value === undefined || value === null
}

View File

@ -24,6 +24,7 @@ import type {
MarketplaceCollection,
PluginsSort,
SearchParams,
SearchParamsFromCollection,
} from './types'
import { DEFAULT_SORT } from './constants'
import {
@ -49,10 +50,12 @@ export type MarketplaceContextValue = {
page: number
handlePageChange: (page: number) => void
plugins?: Plugin[]
pluginsTotal?: number
resetPlugins: () => void
sort: PluginsSort
handleSortChange: (sort: PluginsSort) => void
handleQueryPlugins: () => void
handleMoreClick: (searchParams: SearchParamsFromCollection) => void
marketplaceCollectionsFromClient?: MarketplaceCollection[]
setMarketplaceCollectionsFromClient: (collections: MarketplaceCollection[]) => void
marketplaceCollectionPluginsMapFromClient?: Record<string, Plugin[]>
@ -73,10 +76,12 @@ export const MarketplaceContext = createContext<MarketplaceContextValue>({
page: 1,
handlePageChange: () => {},
plugins: undefined,
pluginsTotal: 0,
resetPlugins: () => {},
sort: DEFAULT_SORT,
handleSortChange: () => {},
handleQueryPlugins: () => {},
handleMoreClick: () => {},
marketplaceCollectionsFromClient: [],
setMarketplaceCollectionsFromClient: () => {},
marketplaceCollectionPluginsMapFromClient: {},
@ -248,7 +253,7 @@ export const MarketplaceContextProvider = ({
}, [handleQueryPlugins])
const handlePageChange = useCallback(() => {
if (pluginsTotal && plugins && pluginsTotal > plugins.length && (!!searchPluginTextRef.current || !!filterPluginTagsRef.current.length)) {
if (pluginsTotal && plugins && pluginsTotal > plugins.length) {
setPage(pageRef.current + 1)
pageRef.current++
@ -256,6 +261,23 @@ export const MarketplaceContextProvider = ({
}
}, [handleQueryPlugins, plugins, pluginsTotal])
const handleMoreClick = useCallback((searchParams: SearchParamsFromCollection) => {
setSearchPluginText(searchParams?.query || '')
searchPluginTextRef.current = searchParams?.query || ''
setSort({
sortBy: searchParams?.sort_by || DEFAULT_SORT.sortBy,
sortOrder: searchParams?.sort_order || DEFAULT_SORT.sortOrder,
})
sortRef.current = {
sortBy: searchParams?.sort_by || DEFAULT_SORT.sortBy,
sortOrder: searchParams?.sort_order || DEFAULT_SORT.sortOrder,
}
setPage(1)
pageRef.current = 1
handleQueryPlugins()
}, [handleQueryPlugins])
useMarketplaceContainerScroll(handlePageChange, scrollContainerId)
return (
@ -272,10 +294,12 @@ export const MarketplaceContextProvider = ({
page,
handlePageChange,
plugins,
pluginsTotal,
resetPlugins,
sort,
handleSortChange,
handleQueryPlugins,
handleMoreClick,
marketplaceCollectionsFromClient,
setMarketplaceCollectionsFromClient,
marketplaceCollectionPluginsMapFromClient,

View File

@ -32,7 +32,7 @@ const CardWrapper = ({
if (showInstallButton) {
return (
<div
className='group relative rounded-xl cursor-pointer'
className='group relative rounded-xl cursor-pointer hover:bg-components-panel-on-panel-item-bg-hover'
>
<Card
key={plugin.name}
@ -47,7 +47,7 @@ const CardWrapper = ({
/>
{
showInstallButton && (
<div className='hidden absolute bottom-0 group-hover:flex items-center space-x-2 px-4 pt-8 pb-4 w-full bg-gradient-to-tr from-[#f9fafb] to-[rgba(249,250,251,0)] rounded-b-xl'>
<div className='hidden absolute bottom-0 group-hover:flex items-center space-x-2 px-4 pt-8 pb-4 w-full bg-gradient-to-tr from-components-panel-on-panel-item-bg to-background-gradient-mask-transparent rounded-b-xl'>
<Button
variant='primary'
className='w-[calc(50%-4px)]'

View File

@ -1,9 +1,13 @@
'use client'
import { useTranslation } from 'react-i18next'
import { RiArrowRightSLine } from '@remixicon/react'
import type { MarketplaceCollection } from '../types'
import CardWrapper from './card-wrapper'
import type { Plugin } from '@/app/components/plugins/types'
import { getLanguage } from '@/i18n/language'
import cn from '@/utils/classnames'
import type { SearchParamsFromCollection } from '@/app/components/plugins/marketplace/types'
type ListWithCollectionProps = {
marketplaceCollections: MarketplaceCollection[]
@ -12,7 +16,7 @@ type ListWithCollectionProps = {
locale: string
cardContainerClassName?: string
cardRender?: (plugin: Plugin) => JSX.Element | null
onMoreClick?: () => void
onMoreClick?: (searchParams?: SearchParamsFromCollection) => void
}
const ListWithCollection = ({
marketplaceCollections,
@ -21,8 +25,10 @@ const ListWithCollection = ({
locale,
cardContainerClassName,
cardRender,
// onMoreClick,
onMoreClick,
}: ListWithCollectionProps) => {
const { t } = useTranslation()
return (
<>
{
@ -31,15 +37,22 @@ const ListWithCollection = ({
key={collection.name}
className='py-3'
>
<div className='flex justify-between'>
<div className='flex justify-between items-end'>
<div>
<div className='title-xl-semi-bold text-text-primary'>{collection.label[getLanguage(locale)]}</div>
<div className='system-xs-regular text-text-tertiary'>{collection.description[getLanguage(locale)]}</div>
</div>
{/* <div
className='system-xs-regular text-text-tertiary cursor-pointer hover:underline'
onClick={() => onMoreClick?.()}
>more</div> */}
{
collection.searchable && onMoreClick && (
<div
className='flex items-center system-xs-medium text-text-accent cursor-pointer '
onClick={() => onMoreClick?.(collection.search_params)}
>
{t('plugin.marketplace.viewMore')}
<RiArrowRightSLine className='w-4 h-4' />
</div>
)
}
</div>
<div className={cn(
'grid grid-cols-4 gap-3 mt-2',

View File

@ -22,12 +22,14 @@ const ListWrapper = ({
}: ListWrapperProps) => {
const { t } = useTranslation()
const plugins = useMarketplaceContext(v => v.plugins)
const pluginsTotal = useMarketplaceContext(v => v.pluginsTotal)
const marketplaceCollectionsFromClient = useMarketplaceContext(v => v.marketplaceCollectionsFromClient)
const marketplaceCollectionPluginsMapFromClient = useMarketplaceContext(v => v.marketplaceCollectionPluginsMapFromClient)
const isLoading = useMarketplaceContext(v => v.isLoading)
const isSuccessCollections = useMarketplaceContext(v => v.isSuccessCollections)
const handleQueryPlugins = useMarketplaceContext(v => v.handleQueryPlugins)
const page = useMarketplaceContext(v => v.page)
const handleMoreClick = useMarketplaceContext(v => v.handleMoreClick)
useEffect(() => {
if (!marketplaceCollectionsFromClient?.length && isSuccessCollections)
@ -39,7 +41,7 @@ const ListWrapper = ({
{
plugins && (
<div className='top-5 flex items-center mb-4 pt-3'>
<div className='title-xl-semi-bold text-text-primary'>{t('plugin.marketplace.pluginsResult', { num: plugins.length })}</div>
<div className='title-xl-semi-bold text-text-primary'>{t('plugin.marketplace.pluginsResult', { num: pluginsTotal })}</div>
<div className='mx-3 w-[1px] h-3.5 bg-divider-regular'></div>
<SortDropdown />
</div>
@ -60,6 +62,7 @@ const ListWrapper = ({
plugins={plugins}
showInstallButton={showInstallButton}
locale={locale}
onMoreClick={handleMoreClick}
/>
)
}

View File

@ -53,22 +53,22 @@ const SortDropdown = () => {
>
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}>
<div className='flex items-center px-2 pr-3 h-8 rounded-lg bg-state-base-hover-alt cursor-pointer'>
<span className='mr-1 system-sm-regular'>
<span className='mr-1 system-sm-regular text-text-secondary'>
{t('plugin.marketplace.sortBy')}
</span>
<span className='mr-1 system-sm-medium'>
<span className='mr-1 system-sm-medium text-text-primary'>
{selectedOption.text}
</span>
<RiArrowDownSLine className='w-4 h-4 text-text-tertiary' />
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent>
<div className='p-1 rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg'>
<div className='p-1 rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur backdrop-blur-sm shadow-lg'>
{
options.map(option => (
<div
key={`${option.value}-${option.order}`}
className='flex items-center justify-between px-3 pr-2 h-8 cursor-pointer system-md-regular'
className='flex items-center justify-between px-3 pr-2 h-8 cursor-pointer system-md-regular text-text-primary rounded-lg hover:bg-components-panel-on-panel-item-bg-hover'
onClick={() => handleSortChange({ sortBy: option.value, sortOrder: option.order })}
>
{option.text}

View File

@ -1,5 +1,11 @@
import type { Plugin } from '../types'
export type SearchParamsFromCollection = {
query?: string
sort_by?: string
sort_order?: string
}
export type MarketplaceCollection = {
name: string
label: Record<string, string>
@ -7,6 +13,8 @@ export type MarketplaceCollection = {
rule: string
created_at: string
updated_at: string
searchable?: boolean
search_params?: SearchParamsFromCollection
}
export type MarketplaceCollectionsResponse = {

View File

@ -1,17 +1,19 @@
import React, { useMemo, useState } from 'react'
import React, { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks'
import { RiDeleteBinLine, RiEditLine, RiLoginCircleLine } from '@remixicon/react'
import copy from 'copy-to-clipboard'
import { RiClipboardLine, RiDeleteBinLine, RiEditLine, RiLoginCircleLine } from '@remixicon/react'
import type { EndpointListItem } from '../types'
import EndpointModal from './endpoint-modal'
import { NAME_FIELD } from './utils'
import { addDefaultValue, toolCredentialToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
import { ClipboardCheck } from '@/app/components/base/icons/src/vender/line/files'
import ActionButton from '@/app/components/base/action-button'
import CopyBtn from '@/app/components/base/copy-btn'
import Confirm from '@/app/components/base/confirm'
import Indicator from '@/app/components/header/indicator'
import Switch from '@/app/components/base/switch'
import Toast from '@/app/components/base/toast'
import Tooltip from '@/app/components/base/tooltip'
import {
useDeleteEndpoint,
useDisableEndpoint,
@ -111,6 +113,25 @@ const EndpointCard = ({
state,
})
const [isCopied, setIsCopied] = useState(false)
const handleCopy = (value: string) => {
copy(value)
setIsCopied(true)
}
useEffect(() => {
if (isCopied) {
const timer = setTimeout(() => {
setIsCopied(false)
}, 2000)
return () => {
clearTimeout(timer)
}
}
}, [isCopied])
const CopyIcon = isCopied ? ClipboardCheck : RiClipboardLine
return (
<div className='p-0.5 bg-background-section-burn rounded-xl'>
<div className='group p-2.5 pl-3 bg-components-panel-on-panel-item-bg rounded-[10px] border-[0.5px] border-components-panel-border'>
@ -133,11 +154,11 @@ const EndpointCard = ({
<div className='shrink-0 w-12 text-text-tertiary system-xs-regular'>{endpoint.method}</div>
<div className='group/item grow flex items-center text-text-secondary system-xs-regular truncate'>
<div title={`${data.url}${endpoint.path}`} className='truncate'>{`${data.url}${endpoint.path}`}</div>
<CopyBtn
className='hidden shrink-0 ml-2 group-hover/item:block'
value={`${data.url}${endpoint.path}`}
isPlain
/>
<Tooltip popupContent={t(`common.operation.${isCopied ? 'copied' : 'copy'}`)} position='top'>
<ActionButton className='hidden shrink-0 ml-2 group-hover/item:flex' onClick={() => handleCopy(`${data.url}${endpoint.path}`)}>
<CopyIcon className='w-3.5 h-3.5 text-text-tertiary' />
</ActionButton>
</Tooltip>
</div>
</div>
))}

View File

@ -1,5 +1,6 @@
import React, { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import { useBoolean } from 'ahooks'
import {
RiAddLine,
@ -19,6 +20,8 @@ import {
useInvalidateEndpointList,
} from '@/service/use-endpoints'
import type { PluginDetail } from '@/app/components/plugins/types'
import { LanguagesSupported } from '@/i18n/language'
import I18n from '@/context/i18n'
import cn from '@/utils/classnames'
type Props = {
@ -26,6 +29,7 @@ type Props = {
}
const EndpointList = ({ detail }: Props) => {
const { t } = useTranslation()
const { locale } = useContext(I18n)
const pluginUniqueID = detail.plugin_unique_identifier
const declaration = detail.declaration.endpoint
const showTopBorder = detail.declaration.tool
@ -74,9 +78,8 @@ const EndpointList = ({ detail }: Props) => {
<RiApps2AddLine className='w-4 h-4 text-text-tertiary' />
</div>
<div className='text-text-tertiary system-xs-regular'>{t('plugin.detailPanel.endpointsTip')}</div>
{/* TODO endpoints doc link */}
<a
href=''
href={`https://docs.dify.ai/${locale === LanguagesSupported[1] ? 'v/zh-hans/' : ''}guides/api-documentation/endpoint`}
target='_blank'
rel='noopener noreferrer'
>

View File

@ -8,7 +8,7 @@ type IPluginListProps = {
const PluginList: FC<IPluginListProps> = ({ pluginList }) => {
return (
<div className='pb-3 bg-white'>
<div className='pb-3'>
<div className='grid grid-cols-2 gap-3'>
{pluginList.map(plugin => (
<PluginItem

View File

@ -36,7 +36,7 @@ const ProviderCard: FC<Props> = ({
const { locale } = useI18N()
return (
<div className={cn('group relative p-4 pb-3 border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg hover-bg-components-panel-on-panel-item-bg rounded-xl shadow-xs', className)}>
<div className={cn('group relative p-4 pb-3 border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg hover:bg-components-panel-on-panel-item-bg rounded-xl shadow-xs', className)}>
{/* Header */}
<div className="flex">
<Icon src={payload.icon} />
@ -61,7 +61,7 @@ const ProviderCard: FC<Props> = ({
))}
</div>
<div
className='hidden group-hover:flex items-center gap-2 absolute bottom-0 left-0 right-0 p-4 pt-8 rounded-xl bg-gradient-to-tr from-[#f9fafb] to-[rgba(249,250,251,0)]'
className='hidden group-hover:flex items-center gap-2 absolute bottom-0 left-0 right-0 p-4 pt-8 rounded-xl bg-gradient-to-tr from-components-panel-on-panel-item-bg to-background-gradient-mask-transparent'
>
<Button
className='grow'

View File

@ -6,8 +6,8 @@ const Empty = () => {
return (
<div className='flex flex-col items-center'>
<div className="shrink-0 w-[163px] h-[149px] bg-cover bg-no-repeat bg-[url('~@/app/components/tools/add-tool-modal/empty.png')]"></div>
<div className='mb-1 text-[13px] font-medium text-gray-700 leading-[18px]'>{t('tools.addToolModal.emptyTitle')}</div>
<div className='text-[13px] text-gray-500 leading-[18px]'>{t('tools.addToolModal.emptyTip')}</div>
<div className='mb-1 text-[13px] font-medium text-text-secondary leading-[18px]'>{t('tools.addToolModal.emptyTitle')}</div>
<div className='text-[13px] text-text-tertiary leading-[18px]'>{t('tools.addToolModal.emptyTip')}</div>
</div>
)
}

View File

@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next'
import Tooltip from '@/app/components/base/tooltip'
import cn from '@/utils/classnames'
import type { Credential } from '@/app/components/tools/types'
import Input from '@/app/components/base/input'
import Drawer from '@/app/components/base/drawer-plus'
import Button from '@/app/components/base/button'
import Radio from '@/app/components/base/radio/ui'
@ -16,7 +17,6 @@ type Props = {
onChange: (credential: Credential) => void
onHide: () => void
}
const keyClassNames = 'py-2 leading-5 text-sm font-medium text-gray-900'
type ItemProps = {
text: string
@ -28,11 +28,11 @@ type ItemProps = {
const SelectItem: FC<ItemProps> = ({ text, value, isChecked, onClick }) => {
return (
<div
className={cn(isChecked ? 'border-[2px] border-indigo-600 shadow-sm bg-white' : 'border border-gray-100', 'mb-2 flex items-center h-9 pl-3 w-[150px] rounded-xl bg-gray-25 hover:bg-gray-50 cursor-pointer space-x-2')}
className={cn(isChecked ? 'border-[2px] border-util-colors-indigo-indigo-600 shadow-sm bg-components-panel-on-panel-item-bg' : 'border border-components-card-border', 'mb-2 flex items-center h-9 pl-3 w-[150px] rounded-xl bg-components-panel-on-panel-item-bg hover:bg-components-panel-on-panel-item-bg-hover cursor-pointer space-x-2')}
onClick={() => onClick(value)}
>
<Radio isChecked={isChecked} />
<div className='text-sm font-normal text-gray-900'>{text}</div>
<div className='system-sm-regular text-text-primary'>{text}</div>
</div>
)
}
@ -55,12 +55,12 @@ const ConfigCredential: FC<Props> = ({
panelClassName='mt-2 !w-[520px] h-fit'
maxWidthClassName='!max-w-[520px]'
height={'fit-content'}
headerClassName='!border-b-black/5'
headerClassName='!border-b-divider-regular'
body={
<div className='pt-2 px-6'>
<div className='space-y-4'>
<div>
<div className={keyClassNames}>{t('tools.createTool.authMethod.type')}</div>
<div className='py-2 system-sm-medium text-text-primary'>{t('tools.createTool.authMethod.type')}</div>
<div className='flex space-x-3'>
<SelectItem
text={t('tools.createTool.authMethod.types.none')}
@ -84,52 +84,52 @@ const ConfigCredential: FC<Props> = ({
</div>
{tempCredential.auth_type === AuthType.apiKey && (
<>
<div className={keyClassNames}>{t('tools.createTool.authHeaderPrefix.title')}</div>
<div className='flex space-x-3'>
<SelectItem
text={t('tools.createTool.authHeaderPrefix.types.basic')}
value={AuthHeaderPrefix.basic}
isChecked={tempCredential.api_key_header_prefix === AuthHeaderPrefix.basic}
onClick={value => setTempCredential({ ...tempCredential, api_key_header_prefix: value as AuthHeaderPrefix })}
/>
<SelectItem
text={t('tools.createTool.authHeaderPrefix.types.bearer')}
value={AuthHeaderPrefix.bearer}
isChecked={tempCredential.api_key_header_prefix === AuthHeaderPrefix.bearer}
onClick={value => setTempCredential({ ...tempCredential, api_key_header_prefix: value as AuthHeaderPrefix })}
/>
<SelectItem
text={t('tools.createTool.authHeaderPrefix.types.custom')}
value={AuthHeaderPrefix.custom}
isChecked={tempCredential.api_key_header_prefix === AuthHeaderPrefix.custom}
onClick={value => setTempCredential({ ...tempCredential, api_key_header_prefix: value as AuthHeaderPrefix })}
/>
<div>
<div className='py-2 system-sm-medium text-text-primary'>{t('tools.createTool.authHeaderPrefix.title')}</div>
<div className='flex space-x-3'>
<SelectItem
text={t('tools.createTool.authHeaderPrefix.types.basic')}
value={AuthHeaderPrefix.basic}
isChecked={tempCredential.api_key_header_prefix === AuthHeaderPrefix.basic}
onClick={value => setTempCredential({ ...tempCredential, api_key_header_prefix: value as AuthHeaderPrefix })}
/>
<SelectItem
text={t('tools.createTool.authHeaderPrefix.types.bearer')}
value={AuthHeaderPrefix.bearer}
isChecked={tempCredential.api_key_header_prefix === AuthHeaderPrefix.bearer}
onClick={value => setTempCredential({ ...tempCredential, api_key_header_prefix: value as AuthHeaderPrefix })}
/>
<SelectItem
text={t('tools.createTool.authHeaderPrefix.types.custom')}
value={AuthHeaderPrefix.custom}
isChecked={tempCredential.api_key_header_prefix === AuthHeaderPrefix.custom}
onClick={value => setTempCredential({ ...tempCredential, api_key_header_prefix: value as AuthHeaderPrefix })}
/>
</div>
</div>
<div>
<div className='flex items-center h-8 text-[13px] font-medium text-gray-900'>
<div className='flex items-center py-2 system-sm-medium text-text-primary'>
{t('tools.createTool.authMethod.key')}
<Tooltip
popupContent={
<div className='w-[261px] text-gray-500'>
<div className='w-[261px] text-text-tertiary'>
{t('tools.createTool.authMethod.keyTooltip')}
</div>
}
triggerClassName='ml-0.5 w-4 h-4'
/>
</div>
<input
<Input
value={tempCredential.api_key_header}
onChange={e => setTempCredential({ ...tempCredential, api_key_header: e.target.value })}
className='w-full h-10 px-3 text-sm font-normal border border-transparent bg-gray-100 rounded-lg grow outline-none focus:bg-components-input-bg-active focus:border-components-input-border-active focus:shadow-xs'
placeholder={t('tools.createTool.authMethod.types.apiKeyPlaceholder')!}
/>
</div>
<div>
<div className={keyClassNames}>{t('tools.createTool.authMethod.value')}</div>
<input
<div className='py-2 system-sm-medium text-text-primary'>{t('tools.createTool.authMethod.value')}</div>
<Input
value={tempCredential.api_key_value}
onChange={e => setTempCredential({ ...tempCredential, api_key_value: e.target.value })}
className='w-full h-10 px-3 text-sm font-normal border border-transparent bg-gray-100 rounded-lg grow outline-none focus:bg-components-input-bg-active focus:border-components-input-border-active focus:shadow-xs'
placeholder={t('tools.createTool.authMethod.types.apiValuePlaceholder')!}
/>
</div>

View File

@ -10,6 +10,7 @@ import {
import Toast from '../../base/toast'
import examples from './examples'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import { importSchemaFromURL } from '@/service/tools'
type Props = {
@ -63,14 +64,14 @@ const GetSchema: FC<Props> = ({
onClick={() => { setShowImportFromUrl(!showImportFromUrl) }}
>
<RiAddLine className='w-3 h-3' />
<div className='text-xs font-medium text-gray-700'>{t('tools.createTool.importFromUrl')}</div>
<div className='system-xs-medium text-text-secondary'>{t('tools.createTool.importFromUrl')}</div>
</Button>
{showImportFromUrl && (
<div className=' absolute left-[-35px] top-[26px] p-2 rounded-lg border border-gray-200 bg-white shadow-lg'>
<div className=' absolute left-[-35px] top-[26px] p-2 rounded-lg border border-components-panel-border bg-components-panel-bg shadow-lg'>
<div className='relative'>
<input
<Input
type='text'
className='w-[244px] h-8 pl-1.5 pr-[44px] overflow-x-auto border border-gray-200 rounded-lg text-[13px] focus:outline-none focus:border-components-input-border-active'
className='w-[244px]'
placeholder={t('tools.createTool.importFromUrlPlaceHolder')!}
value={importUrl}
onChange={e => setImportUrl(e.target.value)}
@ -95,11 +96,11 @@ const GetSchema: FC<Props> = ({
className='space-x-1'
onClick={() => { setShowExamples(!showExamples) }}
>
<div className='text-xs font-medium text-gray-700'>{t('tools.createTool.examples')}</div>
<div className='system-xs-medium text-text-secondary'>{t('tools.createTool.examples')}</div>
<RiArrowDownSLine className='w-3 h-3' />
</Button>
{showExamples && (
<div className='absolute top-7 right-0 p-1 rounded-lg bg-white shadow-sm'>
<div className='absolute top-7 right-0 p-1 rounded-lg bg-components-panel-bg shadow-sm'>
{examples.map(item => (
<div
key={item.key}
@ -107,7 +108,7 @@ const GetSchema: FC<Props> = ({
onChange(item.content)
setShowExamples(false)
}}
className='px-3 py-1.5 rounded-lg hover:bg-gray-50 leading-5 text-sm font-normal text-gray-700 cursor-pointer whitespace-nowrap'
className='px-3 py-1.5 rounded-lg hover:bg-components-panel-on-panel-item-bg-hover leading-5 system-sm-regular text-text-secondary cursor-pointer whitespace-nowrap'
>
{t(`tools.createTool.exampleOptions.${item.key}`)}
</div>

View File

@ -3,8 +3,9 @@ import type { FC } from 'react'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useDebounce, useGetState } from 'ahooks'
import { RiSettings2Line } from '@remixicon/react'
import produce from 'immer'
import { LinkExternal02, Settings01 } from '../../base/icons/src/vender/line/general'
import { LinkExternal02 } from '../../base/icons/src/vender/line/general'
import type { Credential, CustomCollectionBackend, CustomParamSchema, Emoji } from '../types'
import { AuthHeaderPrefix, AuthType } from '../types'
import GetSchema from './get-schema'
@ -21,7 +22,6 @@ import { parseParamsSchema } from '@/service/tools'
import LabelSelector from '@/app/components/tools/labels/selector'
import Toast from '@/app/components/base/toast'
const fieldNameClassNames = 'py-2 leading-5 text-sm font-medium text-gray-900'
type Props = {
positionLeft?: boolean
payload: any
@ -189,12 +189,12 @@ const EditCustomCollectionModal: FC<Props> = ({
panelClassName='mt-2 !w-[640px]'
maxWidthClassName='!max-w-[640px]'
height='calc(100vh - 16px)'
headerClassName='!border-b-black/5'
headerClassName='!border-b-divider-regular'
body={
<div className='flex flex-col h-full'>
<div className='grow h-0 overflow-y-auto px-6 py-3 space-y-4'>
<div>
<div className={fieldNameClassNames}>{t('tools.createTool.name')} <span className='ml-1 text-red-500'>*</span></div>
<div className='py-2 system-sm-medium text-text-primary'>{t('tools.createTool.name')} <span className='ml-1 text-red-500'>*</span></div>
<div className='flex items-center justify-between gap-3'>
<AppIcon size='large' onClick={() => { setShowEmojiPicker(true) }} className='cursor-pointer' icon={emoji.content} background={emoji.background} />
<Input
@ -214,12 +214,12 @@ const EditCustomCollectionModal: FC<Props> = ({
<div className='select-none'>
<div className='flex justify-between items-center'>
<div className='flex items-center'>
<div className={fieldNameClassNames}>{t('tools.createTool.schema')}<span className='ml-1 text-red-500'>*</span></div>
<div className='mx-2 w-px h-3 bg-black/5'></div>
<div className='py-2 system-sm-medium text-text-primary'>{t('tools.createTool.schema')}<span className='ml-1 text-red-500'>*</span></div>
<div className='mx-2 w-px h-3 bg-divider-regular'></div>
<a
href="https://swagger.io/specification/"
target='_blank' rel='noopener noreferrer'
className='flex items-center h-[18px] space-x-1 text-[#155EEF]'
className='flex items-center h-[18px] space-x-1 text-text-accent'
>
<div className='text-xs font-normal'>{t('tools.createTool.viewSchemaSpec')}</div>
<LinkExternal02 className='w-3 h-3' />
@ -238,11 +238,11 @@ const EditCustomCollectionModal: FC<Props> = ({
{/* Available Tools */}
<div>
<div className={fieldNameClassNames}>{t('tools.createTool.availableTools.title')}</div>
<div className='rounded-lg border border-gray-200 w-full overflow-x-auto'>
<table className='w-full leading-[18px] text-xs text-gray-700 font-normal'>
<thead className='text-gray-500 uppercase'>
<tr className={cn(paramsSchemas.length > 0 && 'border-b', 'border-gray-200')}>
<div className='py-2 system-sm-medium text-text-primary'>{t('tools.createTool.availableTools.title')}</div>
<div className='rounded-lg border border-divider-regular w-full overflow-x-auto'>
<table className='w-full system-xs-regular text-text-secondary'>
<thead className='text-text-tertiary uppercase'>
<tr className={cn(paramsSchemas.length > 0 && 'border-b', 'border-divider-regular')}>
<th className="p-2 pl-3 font-medium">{t('tools.createTool.availableTools.name')}</th>
<th className="p-2 pl-3 font-medium w-[236px]">{t('tools.createTool.availableTools.description')}</th>
<th className="p-2 pl-3 font-medium">{t('tools.createTool.availableTools.method')}</th>
@ -252,9 +252,9 @@ const EditCustomCollectionModal: FC<Props> = ({
</thead>
<tbody>
{paramsSchemas.map((item, index) => (
<tr key={index} className='border-b last:border-0 border-gray-200'>
<tr key={index} className='border-b last:border-0 border-divider-regular'>
<td className="p-2 pl-3">{item.operation_id}</td>
<td className="p-2 pl-3 text-gray-500 w-[236px]">{item.summary}</td>
<td className="p-2 pl-3 w-[236px]">{item.summary}</td>
<td className="p-2 pl-3">{item.method}</td>
<td className="p-2 pl-3">{getPath(item.server_url)}</td>
<td className="p-2 pl-3 w-[62px]">
@ -277,22 +277,22 @@ const EditCustomCollectionModal: FC<Props> = ({
{/* Authorization method */}
<div>
<div className={fieldNameClassNames}>{t('tools.createTool.authMethod.title')}</div>
<div className='flex items-center h-9 justify-between px-2.5 bg-gray-100 rounded-lg cursor-pointer' onClick={() => setCredentialsModalShow(true)}>
<div className='text-sm font-normal text-gray-900'>{t(`tools.createTool.authMethod.types.${credential.auth_type}`)}</div>
<Settings01 className='w-4 h-4 text-gray-700 opacity-60' />
<div className='py-2 system-sm-medium text-text-primary'>{t('tools.createTool.authMethod.title')}</div>
<div className='flex items-center h-9 justify-between px-2.5 bg-components-input-bg-normal rounded-lg cursor-pointer' onClick={() => setCredentialsModalShow(true)}>
<div className='system-xs-regular text-text-primary'>{t(`tools.createTool.authMethod.types.${credential.auth_type}`)}</div>
<RiSettings2Line className='w-4 h-4 text-text-secondary' />
</div>
</div>
{/* Labels */}
<div>
<div className='py-2 leading-5 text-sm font-medium text-gray-900'>{t('tools.createTool.toolInput.label')}</div>
<div className='py-2 system-sm-medium text-text-primary'>{t('tools.createTool.toolInput.label')}</div>
<LabelSelector value={labels} onChange={handleLabelSelect} />
</div>
{/* Privacy Policy */}
<div>
<div className={fieldNameClassNames}>{t('tools.createTool.privacyPolicy')}</div>
<div className='py-2 system-sm-medium text-text-primary'>{t('tools.createTool.privacyPolicy')}</div>
<Input
value={customCollection.privacy_policy}
onChange={(e) => {
@ -305,7 +305,7 @@ const EditCustomCollectionModal: FC<Props> = ({
</div>
<div>
<div className={fieldNameClassNames}>{t('tools.createTool.customDisclaimer')}</div>
<div className='py-2 system-sm-medium text-text-primary'>{t('tools.createTool.customDisclaimer')}</div>
<Input
value={customCollection.custom_disclaimer}
onChange={(e) => {
@ -318,10 +318,10 @@ const EditCustomCollectionModal: FC<Props> = ({
</div>
</div>
<div className={cn(isEdit ? 'justify-between' : 'justify-end', 'mt-2 shrink-0 flex py-4 px-6 rounded-b-[10px] bg-gray-50 border-t border-black/5')} >
<div className={cn(isEdit ? 'justify-between' : 'justify-end', 'mt-2 shrink-0 flex py-4 px-6 rounded-b-[10px] bg-background-section-burn border-t border-divider-regular')} >
{
isEdit && (
<Button onClick={onRemove} className='text-red-500 border-red-50 hover:border-red-500'>{t('common.operation.delete')}</Button>
<Button variant='warning' onClick={onRemove}>{t('common.operation.delete')}</Button>
)
}
<div className='flex space-x-2 '>

View File

@ -21,7 +21,6 @@ import Toast from '@/app/components/base/toast'
import Modal from '../../base/modal'
import Button from '@/app/components/base/button'
const fieldNameClassNames = 'py-2 leading-5 text-sm font-medium text-gray-900'
type Props = {
positionLeft?: boolean
payload: any
@ -187,12 +186,12 @@ const EditCustomCollectionModal: FC<Props> = ({
className='!p-0 !max-w-[630px] !h-[calc(100vh-16px)]'
>
<div className='flex flex-col h-full'>
<div className='ml-6 mt-6 text-base font-semibold text-gray-900'>
<div className='ml-6 mt-6 text-base font-semibold text-text-primary'>
{t('tools.createTool.title')}
</div>
<div className='grow h-0 overflow-y-auto px-6 py-3 space-y-4'>
<div>
<div className={fieldNameClassNames}>{t('tools.createTool.name')} <span className='ml-1 text-red-500'>*</span></div>
<div className='py-2 system-sm-medium text-text-primary'>{t('tools.createTool.name')} <span className='ml-1 text-red-500'>*</span></div>
<div className='flex items-center justify-between gap-3'>
<AppIcon size='large' onClick={() => { setShowEmojiPicker(true) }} className='cursor-pointer' icon={emoji.content} background={emoji.background} />
<Input
@ -212,12 +211,12 @@ const EditCustomCollectionModal: FC<Props> = ({
<div className='select-none'>
<div className='flex justify-between items-center'>
<div className='flex items-center'>
<div className={fieldNameClassNames}>{t('tools.createTool.schema')}<span className='ml-1 text-red-500'>*</span></div>
<div className='mx-2 w-px h-3 bg-black/5'></div>
<div className='py-2 system-sm-medium text-text-primary'>{t('tools.createTool.schema')}<span className='ml-1 text-red-500'>*</span></div>
<div className='mx-2 w-px h-3 bg-divider-regular'></div>
<a
href="https://swagger.io/specification/"
target='_blank' rel='noopener noreferrer'
className='flex items-center h-[18px] space-x-1 text-[#155EEF]'
className='flex items-center h-[18px] space-x-1 text-text-accent'
>
<div className='text-xs font-normal'>{t('tools.createTool.viewSchemaSpec')}</div>
<LinkExternal02 className='w-3 h-3' />
@ -236,11 +235,11 @@ const EditCustomCollectionModal: FC<Props> = ({
{/* Available Tools */}
<div>
<div className={fieldNameClassNames}>{t('tools.createTool.availableTools.title')}</div>
<div className='rounded-lg border border-gray-200 w-full overflow-x-auto'>
<table className='w-full leading-[18px] text-xs text-gray-700 font-normal'>
<thead className='text-gray-500 uppercase'>
<tr className={cn(paramsSchemas.length > 0 && 'border-b', 'border-gray-200')}>
<div className='py-2 system-sm-medium text-text-primary'>{t('tools.createTool.availableTools.title')}</div>
<div className='rounded-lg border border-divider-regular w-full overflow-x-auto'>
<table className='w-full system-xs-regular text-text-secondary'>
<thead className='text-text-tertiary uppercase'>
<tr className={cn(paramsSchemas.length > 0 && 'border-b', 'border-divider-regular')}>
<th className="p-2 pl-3 font-medium">{t('tools.createTool.availableTools.name')}</th>
<th className="p-2 pl-3 font-medium w-[236px]">{t('tools.createTool.availableTools.description')}</th>
<th className="p-2 pl-3 font-medium">{t('tools.createTool.availableTools.method')}</th>
@ -250,9 +249,9 @@ const EditCustomCollectionModal: FC<Props> = ({
</thead>
<tbody>
{paramsSchemas.map((item, index) => (
<tr key={index} className='border-b last:border-0 border-gray-200'>
<tr key={index} className='border-b last:border-0 border-divider-regular'>
<td className="p-2 pl-3">{item.operation_id}</td>
<td className="p-2 pl-3 text-gray-500 w-[236px]">{item.summary}</td>
<td className="p-2 pl-3 w-[236px]">{item.summary}</td>
<td className="p-2 pl-3">{item.method}</td>
<td className="p-2 pl-3">{getPath(item.server_url)}</td>
<td className="p-2 pl-3 w-[62px]">
@ -275,22 +274,22 @@ const EditCustomCollectionModal: FC<Props> = ({
{/* Authorization method */}
<div>
<div className={fieldNameClassNames}>{t('tools.createTool.authMethod.title')}</div>
<div className='flex items-center h-9 justify-between px-2.5 bg-gray-100 rounded-lg cursor-pointer' onClick={() => setCredentialsModalShow(true)}>
<div className='text-sm font-normal text-gray-900'>{t(`tools.createTool.authMethod.types.${credential.auth_type}`)}</div>
<Settings01 className='w-4 h-4 text-gray-700 opacity-60' />
<div className='py-2 system-sm-medium text-text-primary'>{t('tools.createTool.authMethod.title')}</div>
<div className='flex items-center h-9 justify-between px-2.5 bg-components-input-bg-normal rounded-lg cursor-pointer' onClick={() => setCredentialsModalShow(true)}>
<div className='system-xs-regular text-text-primary'>{t(`tools.createTool.authMethod.types.${credential.auth_type}`)}</div>
<Settings01 className='w-4 h-4 text-text-secondary' />
</div>
</div>
{/* Labels */}
<div>
<div className='py-2 leading-5 text-sm font-medium text-gray-900'>{t('tools.createTool.toolInput.label')}</div>
<div className='py-2 system-sm-medium text-text-primary'>{t('tools.createTool.toolInput.label')}</div>
<LabelSelector value={labels} onChange={handleLabelSelect} />
</div>
{/* Privacy Policy */}
<div>
<div className={fieldNameClassNames}>{t('tools.createTool.privacyPolicy')}</div>
<div className='py-2 system-sm-medium text-text-primary'>{t('tools.createTool.privacyPolicy')}</div>
<Input
value={customCollection.privacy_policy}
onChange={(e) => {
@ -303,7 +302,7 @@ const EditCustomCollectionModal: FC<Props> = ({
</div>
<div>
<div className={fieldNameClassNames}>{t('tools.createTool.customDisclaimer')}</div>
<div className='py-2 system-sm-medium text-text-primary'>{t('tools.createTool.customDisclaimer')}</div>
<Input
value={customCollection.custom_disclaimer}
onChange={(e) => {
@ -316,10 +315,10 @@ const EditCustomCollectionModal: FC<Props> = ({
</div>
</div>
<div className={cn(isEdit ? 'justify-between' : 'justify-end', 'mt-2 shrink-0 flex py-4 px-6 rounded-b-[10px] bg-gray-50 border-t border-black/5')} >
<div className={cn(isEdit ? 'justify-between' : 'justify-end', 'mt-2 shrink-0 flex py-4 px-6 rounded-b-[10px] bg-background-section-burn border-t border-divider-regular')} >
{
isEdit && (
<Button onClick={onRemove} className='text-red-500 border-red-50 hover:border-red-500'>{t('common.operation.delete')}</Button>
<Button variant='warning' onClick={onRemove}>{t('common.operation.delete')}</Button>
)
}
<div className='flex space-x-2 '>

View File

@ -3,10 +3,11 @@ import type { FC } from 'react'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import { Settings01 } from '../../base/icons/src/vender/line/general'
import { RiSettings2Line } from '@remixicon/react'
import ConfigCredentials from './config-credentials'
import { AuthType, type Credential, type CustomCollectionBackend, type CustomParamSchema } from '@/app/components/tools/types'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import Drawer from '@/app/components/base/drawer-plus'
import I18n from '@/context/i18n'
import { testAPIAvailable } from '@/service/tools'
@ -19,8 +20,6 @@ type Props = {
onHide: () => void
}
const keyClassNames = 'py-2 leading-5 text-sm font-medium text-gray-900'
const TestApi: FC<Props> = ({
positionCenter,
customCollection,
@ -65,39 +64,40 @@ const TestApi: FC<Props> = ({
panelClassName='mt-2 !w-[600px]'
maxWidthClassName='!max-w-[600px]'
height='calc(100vh - 16px)'
headerClassName='!border-b-black/5'
headerClassName='!border-b-divider-regular'
body={
<div className='pt-2 px-6 overflow-y-auto'>
<div className='space-y-4'>
<div>
<div className={keyClassNames}>{t('tools.createTool.authMethod.title')}</div>
<div className='flex items-center h-9 justify-between px-2.5 bg-gray-100 rounded-lg cursor-pointer' onClick={() => setCredentialsModalShow(true)}>
<div className='text-sm font-normal text-gray-900'>{t(`tools.createTool.authMethod.types.${tempCredential.auth_type}`)}</div>
<Settings01 className='w-4 h-4 text-gray-700 opacity-60' />
<div className='py-2 system-sm-medium text-text-primary'>{t('tools.createTool.authMethod.title')}</div>
<div className='flex items-center h-9 justify-between px-2.5 bg-components-input-bg-normal rounded-lg cursor-pointer' onClick={() => setCredentialsModalShow(true)}>
<div className='system-xs-regular text-text-primary'>{t(`tools.createTool.authMethod.types.${tempCredential.auth_type}`)}</div>
<RiSettings2Line className='w-4 h-4 text-text-secondary' />
</div>
</div>
<div>
<div className={keyClassNames}>{t('tools.test.parametersValue')}</div>
<div className='rounded-lg border border-gray-200'>
<table className='w-full leading-[18px] text-xs text-gray-700 font-normal'>
<thead className='text-gray-500 uppercase'>
<tr className='border-b border-gray-200'>
<div className='py-2 system-sm-medium text-text-primary'>{t('tools.test.parametersValue')}</div>
<div className='rounded-lg border border-divider-regular'>
<table className='w-full system-xs-regular text-text-secondary font-normal'>
<thead className='text-text-tertiary uppercase'>
<tr className='border-b border-divider-regular'>
<th className="p-2 pl-3 font-medium">{t('tools.test.parameters')}</th>
<th className="p-2 pl-3 font-medium">{t('tools.test.value')}</th>
</tr>
</thead>
<tbody>
{parameters.map((item, index) => (
<tr key={index} className='border-b last:border-0 border-gray-200'>
<tr key={index} className='border-b last:border-0 border-divider-regular'>
<td className="py-2 pl-3 pr-2.5">
{item.label[language]}
</td>
<td className="">
<input
<Input
value={parametersValue[item.name] || ''}
onChange={e => setParametersValue({ ...parametersValue, [item.name]: e.target.value })}
type='text' className='px-3 h-[34px] w-full outline-none focus:bg-gray-100' ></input>
type='text'
className='!bg-transparent !border-transparent !hover:border-transparent !hover:bg-transparent !focus:border-transparent !focus:bg-transparent' />
</td>
</tr>
))}
@ -110,11 +110,11 @@ const TestApi: FC<Props> = ({
<Button variant='primary' className=' mt-4 w-full h-10' onClick={handleTest}>{t('tools.test.title')}</Button>
<div className='mt-6'>
<div className='flex items-center space-x-3'>
<div className='leading-[18px] text-xs font-semibold text-gray-500'>{t('tools.test.testResult')}</div>
<div className='system-xs-semibold text-text-tertiary'>{t('tools.test.testResult')}</div>
<div className='grow w-0 h-px bg-[rgb(243, 244, 246)]'></div>
</div>
<div className='mt-2 px-3 py-2 h-[200px] overflow-y-auto overflow-x-hidden rounded-lg bg-gray-100 leading-4 text-xs font-normal text-gray-700'>
{result || <span className='text-gray-400'>{t('tools.test.testResultPlaceholder')}</span>}
<div className='mt-2 px-3 py-2 h-[200px] overflow-y-auto overflow-x-hidden rounded-lg bg-components-input-bg-normal system-xs-regular text-text-secondary'>
{result || <span className='text-text-quaternary'>{t('tools.test.testResultPlaceholder')}</span>}
</div>
</div>
</div>

View File

@ -67,24 +67,23 @@ const LabelFilter: FC<LabelFilterProps> = ({
className='block'
>
<div className={cn(
'flex items-center gap-1 px-2 h-8 rounded-lg border-[0.5px] border-transparent bg-gray-200 cursor-pointer hover:bg-gray-300',
open && !value.length && '!bg-gray-300 hover:bg-gray-300',
!open && !!value.length && '!bg-white/80 shadow-xs !border-black/5 hover:!bg-gray-200',
open && !!value.length && '!bg-gray-200 !border-black/5 shadow-xs hover:!bg-gray-200',
'flex items-center gap-1 px-2 h-8 rounded-lg border-[0.5px] border-transparent bg-components-input-bg-normal cursor-pointer hover:bg-components-input-bg-hover',
!open && !!value.length && 'shadow-xs',
open && !!value.length && 'shadow-xs',
)}>
<div className='p-[1px]'>
<Tag01 className='h-3.5 w-3.5 text-gray-700' />
<Tag01 className='h-3.5 w-3.5 text-text-tertiary' />
</div>
<div className='text-[13px] leading-[18px] text-gray-700'>
<div className='text-[13px] leading-[18px] text-text-tertiary'>
{!value.length && t('common.tag.placeholder')}
{!!value.length && currentLabel?.label}
</div>
{value.length > 1 && (
<div className='text-xs font-medium leading-[18px] text-gray-500'>{`+${value.length - 1}`}</div>
<div className='text-xs font-medium leading-[18px] text-text-tertiary'>{`+${value.length - 1}`}</div>
)}
{!value.length && (
<div className='p-[1px]'>
<RiArrowDownSLine className='h-3.5 w-3.5 text-gray-700' />
<RiArrowDownSLine className='h-3.5 w-3.5 text-text-tertiary' />
</div>
)}
{!!value.length && (
@ -92,14 +91,14 @@ const LabelFilter: FC<LabelFilterProps> = ({
e.stopPropagation()
onChange([])
}}>
<XCircle className='h-3.5 w-3.5 text-gray-400 group-hover/clear:text-gray-600' />
<XCircle className='h-3.5 w-3.5 text-text-tertiary group-hover/clear:text-text-secondary' />
</div>
)}
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1002]'>
<div className='relative w-[240px] bg-white rounded-lg border-[0.5px] border-gray-200 shadow-lg'>
<div className='p-2 border-b-[0.5px] border-black/5'>
<div className='relative w-[240px] bg-components-panel-bg-blur rounded-lg border-[0.5px] backdrop-blur-[5px] border-components-panel-border shadow-lg'>
<div className='p-2'>
<Input
showLeftIcon
showClearIcon
@ -112,17 +111,17 @@ const LabelFilter: FC<LabelFilterProps> = ({
{filteredLabelList.map(label => (
<div
key={label.name}
className='flex items-center gap-2 pl-3 py-[6px] pr-2 rounded-lg cursor-pointer hover:bg-gray-100'
className='flex items-center gap-2 pl-3 py-[6px] pr-2 rounded-lg cursor-pointer hover:bg-state-base-hover'
onClick={() => selectLabel(label)}
>
<div title={label.label} className='grow text-sm text-gray-700 leading-5 truncate'>{label.label}</div>
{value.includes(label.name) && <Check className='shrink-0 w-4 h-4 text-primary-600' />}
<div title={label.label} className='grow text-sm text-text-secondary leading-5 truncate'>{label.label}</div>
{value.includes(label.name) && <Check className='shrink-0 w-4 h-4 text-text-accent' />}
</div>
))}
{!filteredLabelList.length && (
<div className='p-3 flex flex-col items-center gap-1'>
<Tag03 className='h-6 w-6 text-gray-300' />
<div className='text-gray-500 text-xs leading-[14px]'>{t('common.tag.noTag')}</div>
<Tag03 className='h-6 w-6 text-text-quaternary' />
<div className='text-text-tertiary text-xs leading-[14px]'>{t('common.tag.noTag')}</div>
</div>
)}
</div>

View File

@ -66,21 +66,21 @@ const LabelSelector: FC<LabelSelectorProps> = ({
className='block'
>
<div className={cn(
'flex items-center gap-1 px-3 h-10 rounded-lg border-[0.5px] border-transparent bg-gray-100 cursor-pointer hover:bg-gray-200',
open && '!bg-gray-200 hover:bg-gray-200',
'flex items-center gap-1 px-3 h-10 rounded-lg border-[0.5px] border-transparent bg-components-input-bg-normal cursor-pointer hover:bg-components-input-bg-hover',
open && '!hover:bg-components-input-bg-hover hover:bg-components-input-bg-hover',
)}>
<div title={value.length > 0 ? selectedLabels : ''} className={cn('grow text-[13px] leading-[18px] text-gray-700 truncate', !value.length && '!text-gray-400')}>
<div title={value.length > 0 ? selectedLabels : ''} className={cn('grow text-[13px] leading-[18px] text-text-secondary truncate', !value.length && '!text-text-quaternary')}>
{!value.length && t('tools.createTool.toolInput.labelPlaceholder')}
{!!value.length && selectedLabels}
</div>
<div className='shrink-0 ml-1 text-gray-700 opacity-60'>
<div className='shrink-0 ml-1 text-text-secondary opacity-60'>
<RiArrowDownSLine className='h-4 w-4' />
</div>
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1040]'>
<div className='relative w-[591px] bg-white rounded-lg border-[0.5px] border-gray-200 shadow-lg'>
<div className='p-2 border-b-[0.5px] border-black/5'>
<div className='relative w-[591px] bg-components-panel-bg-blur backdrop-blur-[5px] rounded-lg border-[0.5px] border-components-panel-border shadow-lg'>
<div className='p-2 border-b-[0.5px] border-divider-regular'>
<Input
showLeftIcon
showClearIcon
@ -93,7 +93,7 @@ const LabelSelector: FC<LabelSelectorProps> = ({
{filteredLabelList.map(label => (
<div
key={label.name}
className='flex items-center gap-2 pl-3 py-[6px] pr-2 rounded-lg cursor-pointer hover:bg-gray-100'
className='flex items-center gap-2 pl-3 py-[6px] pr-2 rounded-lg cursor-pointer hover:bg-components-panel-on-panel-item-bg-hover'
onClick={() => selectLabel(label)}
>
<Checkbox
@ -101,13 +101,13 @@ const LabelSelector: FC<LabelSelectorProps> = ({
checked={value.includes(label.name)}
onCheck={() => { }}
/>
<div title={label.label} className='grow text-sm text-gray-700 leading-5 truncate'>{label.label}</div>
<div title={label.label} className='grow text-sm text-text-secondary leading-5 truncate'>{label.label}</div>
</div>
))}
{!filteredLabelList.length && (
<div className='p-3 flex flex-col items-center gap-1'>
<Tag03 className='h-6 w-6 text-gray-300' />
<div className='text-gray-500 text-xs leading-[14px]'>{t('common.tag.noTag')}</div>
<Tag03 className='h-6 w-6 text-text-quaternary' />
<div className='text-text-tertiary text-xs leading-[14px]'>{t('common.tag.noTag')}</div>
</div>
)}
</div>

View File

@ -63,13 +63,13 @@ const ProviderList = () => {
return (
<>
<div className='relative flex overflow-hidden bg-gray-100 shrink-0 h-0 grow'>
<div className='relative flex overflow-hidden shrink-0 h-0 grow'>
<div
ref={containerRef}
className='relative flex flex-col overflow-y-auto bg-gray-100 grow'
className='relative flex flex-col overflow-y-auto bg-background-body grow'
>
<div className={cn(
'sticky top-0 flex justify-between items-center pt-4 px-12 pb-2 leading-[56px] bg-gray-100 z-20 flex-wrap gap-y-2',
'sticky top-0 flex justify-between items-center pt-4 px-12 pb-2 leading-[56px] z-20 flex-wrap gap-y-2',
currentProvider && 'pr-6',
)}>
<TabSliderNew
@ -96,6 +96,7 @@ const ProviderList = () => {
{(filteredCollectionList.length > 0 || activeTab !== 'builtin') && (
<div className={cn(
'relative grid content-start grid-cols-1 gap-4 px-12 pt-2 pb-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 shrink-0',
!filteredCollectionList.length && activeTab === 'workflow' && 'grow',
)}>
{activeTab === 'api' && <CustomCreateCard onRefreshData={refetch} />}
{filteredCollectionList.map(collection => (

View File

@ -45,16 +45,16 @@ const Contribute = ({ onRefreshData }: Props) => {
return (
<>
{isCurrentWorkspaceManager && (
<div className='flex flex-col col-span-1 bg-gray-200 border-[0.5px] border-black/5 rounded-xl min-h-[135px] transition-all duration-200 ease-in-out cursor-pointer hover:bg-gray-50 hover:shadow-lg'>
<div className='group grow rounded-t-xl hover:bg-white' onClick={() => setIsShowEditCustomCollectionModal(true)}>
<div className='flex flex-col col-span-1 bg-components-panel-on-panel-item-bg border-[0.5px] border-divider-subtle rounded-xl min-h-[135px] transition-all duration-200 ease-in-out cursor-pointer hover:bg-components-panel-on-panel-item-bg-hover hover:shadow-lg'>
<div className='group grow rounded-t-xl hover:bg-background-body' onClick={() => setIsShowEditCustomCollectionModal(true)}>
<div className='shrink-0 flex items-center p-4 pb-3'>
<div className='w-10 h-10 flex items-center justify-center border border-gray-200 bg-gray-100 rounded-lg group-hover:border-primary-100 group-hover:bg-primary-50'>
<RiAddLine className='w-4 h-4 text-gray-500 group-hover:text-primary-600'/>
<div className='w-10 h-10 flex items-center justify-center border border-components-option-card-option-border bg-components-option-card-option-bg rounded-lg group-hover:border-components-option-card-option-border-hover group-hover:bg-components-option-card-option-bg-hover'>
<RiAddLine className='w-4 h-4 text-text-tertiary group-hover:text-text-accent'/>
</div>
<div className='ml-3 text-sm font-semibold leading-5 text-gray-800 group-hover:text-primary-600'>{t('tools.createCustomTool')}</div>
<div className='ml-3 text-sm font-semibold leading-5 text-text-primary group-hover:text-text-accent'>{t('tools.createCustomTool')}</div>
</div>
</div>
<div className='px-4 py-3 rounded-b-xl border-t-[0.5px] border-black/5 text-gray-500 hover:text-[#155EEF] hover:bg-white'>
<div className='px-4 py-3 rounded-b-xl border-t-[0.5px] border-divider-regular text-text-tertiary hover:text-text-accent hover:bg-background-body'>
<a href={linkUrl} target='_blank' rel='noopener noreferrer' className='flex items-center space-x-1'>
<BookOpen01 className='shrink-0 w-3 h-3' />
<div className='grow leading-[18px] text-xs font-normal truncate' title={t('tools.customToolTip') || ''}>{t('tools.customToolTip')}</div>

View File

@ -260,14 +260,14 @@ const ProviderDetail = ({
{!!collection.description[language] && (
<Description text={collection.description[language]} descriptionLineRows={2}></Description>
)}
<div className='flex gap-1 border-b-[0.5px] border-black/5'>
<div className='flex gap-1 border-b-[0.5px] border-divider-subtle'>
{collection.type === CollectionType.custom && !isDetailLoading && (
<Button
className={cn('shrink-0 my-3 w-full')}
onClick={() => setIsShowEditCustomCollectionModal(true)}
>
<Settings01 className='mr-1 w-4 h-4 text-gray-500' />
<div className='leading-5 text-sm font-medium text-gray-700'>{t('tools.createTool.editAction')}</div>
<Settings01 className='mr-1 w-4 h-4 text-text-tertiary' />
<div className='system-sm-medium text-text-secondary'>{t('tools.createTool.editAction')}</div>
</Button>
)}
{collection.type === CollectionType.workflow && !isDetailLoading && customCollection && (
@ -276,8 +276,8 @@ const ProviderDetail = ({
variant='primary'
className={cn('shrink-0 my-3 w-[183px]')}
>
<a className='flex items-center text-white' href={`/app/${(customCollection as WorkflowToolProviderResponse).workflow_app_id}/workflow`} rel='noreferrer' target='_blank'>
<div className='leading-5 text-sm font-medium'>{t('tools.openInStudio')}</div>
<a className='flex items-center text-text-primary' href={`/app/${(customCollection as WorkflowToolProviderResponse).workflow_app_id}/workflow`} rel='noreferrer' target='_blank'>
<div className='system-sm-medium'>{t('tools.openInStudio')}</div>
<LinkExternal02 className='ml-1 w-4 h-4' />
</a>
</Button>
@ -286,7 +286,7 @@ const ProviderDetail = ({
onClick={() => setIsShowEditWorkflowToolModal(true)}
disabled={!isCurrentWorkspaceManager}
>
<div className='leading-5 text-sm font-medium text-gray-700'>{t('tools.createTool.editAction')}</div>
<div className='system-sm-medium text-text-secondary'>{t('tools.createTool.editAction')}</div>
</Button>
</>
)}
@ -319,7 +319,7 @@ const ProviderDetail = ({
<div className='text-text-secondary system-sm-semibold-uppercase'>
<span className=''>{t('tools.includeToolNum', { num: toolList.length }).toLocaleUpperCase()}</span>
<span className='px-1'>·</span>
<span className='text-[#DC6803]'>{t('tools.auth.setup').toLocaleUpperCase()}</span>
<span className='text-util-colors-orange-orange-600'>{t('tools.auth.setup').toLocaleUpperCase()}</span>
</div>
<Button
variant='primary'

View File

@ -71,11 +71,11 @@ const ConfigCredential: FC<Props> = ({
onHide={onCancel}
title={t('tools.auth.setupModalTitle') as string}
titleDescription={t('tools.auth.setupModalTitleDescription') as string}
panelClassName='mt-[64px] mb-2 !w-[420px]'
panelClassName='mt-[64px] mb-2 !w-[420px] border-components-panel-border'
maxWidthClassName='!max-w-[420px]'
height='calc(100vh - 64px)'
contentClassName='!bg-gray-100'
headerClassName='!border-b-black/5'
contentClassName='!bg-components-panel-bg'
headerClassName='!border-b-divider-subtle'
body={
<div className='px-6 py-3 h-full'>
@ -92,12 +92,12 @@ const ConfigCredential: FC<Props> = ({
isEditMode={true}
showOnVariableMap={{}}
validating={false}
inputClassName='!bg-gray-50'
inputClassName='!bg-components-input-bg-normal'
fieldMoreInfo={item => item.url
? (<a
href={item.url}
target='_blank' rel='noopener noreferrer'
className='inline-flex items-center text-xs text-primary-600'
className='inline-flex items-center text-xs text-text-accent'
>
{t('tools.howToGet')}
<LinkExternal02 className='ml-1 w-3 h-3' />

View File

@ -2,7 +2,6 @@
import { useTranslation } from 'react-i18next'
import { RiCloseLine } from '@remixicon/react'
import s from './style.module.css'
import cn from '@/utils/classnames'
import Button from '@/app/components/base/button'
import Modal from '@/app/components/base/modal'
@ -19,24 +18,24 @@ const ConfirmModal = ({ show, onConfirm, onClose }: ConfirmModalProps) => {
return (
<Modal
className={cn('p-8 max-w-[600px] w-[600px]', s.bg)}
className={cn('p-8 max-w-[600px] w-[600px]')}
isShow={show}
onClose={() => { }}
>
<div className='absolute right-4 top-4 p-2 cursor-pointer' onClick={onClose}>
<RiCloseLine className='w-4 h-4 text-gray-500' />
<RiCloseLine className='w-4 h-4 text-text-tertiary' />
</div>
<div className='w-12 h-12 p-3 bg-white rounded-xl border-[0.5px] border-gray-100 shadow-xl'>
<div className='w-12 h-12 p-3 bg-background-section rounded-xl border-[0.5px] border-divider-regular shadow-xl'>
<AlertTriangle className='w-6 h-6 text-[rgb(247,144,9)]' />
</div>
<div className='relative mt-3 text-xl font-semibold leading-[30px] text-gray-900'>{t('tools.createTool.confirmTitle')}</div>
<div className='my-1 text-gray-500 text-sm leading-5'>
<div className='relative mt-3 text-xl font-semibold leading-[30px] text-text-primary'>{t('tools.createTool.confirmTitle')}</div>
<div className='my-1 text-text-tertiary text-sm leading-5'>
{t('tools.createTool.confirmTip')}
</div>
<div className='pt-6 flex justify-end items-center'>
<div className='flex items-center'>
<Button className='mr-2' onClick={onClose}>{t('common.operation.cancel')}</Button>
<Button className='border-red-700' variant="warning" onClick={onConfirm}>{t('common.operation.confirm')}</Button>
<Button variant="warning" onClick={onConfirm}>{t('common.operation.confirm')}</Button>
</div>
</div>
</Modal>

View File

@ -1,3 +0,0 @@
.bg {
background: linear-gradient(180deg, rgba(247, 144, 9, 0.05) 0%, rgba(247, 144, 9, 0.00) 24.41%), #F9FAFB;
}

View File

@ -124,13 +124,13 @@ const WorkflowToolAsModal: FC<Props> = ({
panelClassName='mt-2 !w-[640px]'
maxWidthClassName='!max-w-[640px]'
height='calc(100vh - 16px)'
headerClassName='!border-b-black/5'
headerClassName='!border-b-divider'
body={
<div className='flex flex-col h-full'>
<div className='grow h-0 overflow-y-auto px-6 py-3 space-y-4'>
{/* name & icon */}
<div>
<div className='py-2 leading-5 text-sm font-medium text-gray-900'>{t('tools.createTool.name')} <span className='ml-1 text-red-500'>*</span></div>
<div className='py-2 system-sm-medium text-text-primary'>{t('tools.createTool.name')} <span className='ml-1 text-red-500'>*</span></div>
<div className='flex items-center justify-between gap-3'>
<AppIcon size='large' onClick={() => { setShowEmojiPicker(true) }} className='cursor-pointer' iconType='emoji' icon={emoji.content} background={emoji.background} />
<Input
@ -143,7 +143,7 @@ const WorkflowToolAsModal: FC<Props> = ({
</div>
{/* name for tool call */}
<div>
<div className='flex items-center py-2 leading-5 text-sm font-medium text-gray-900'>
<div className='flex items-center py-2 system-sm-medium text-text-primary'>
{t('tools.createTool.nameForToolCall')} <span className='ml-1 text-red-500'>*</span>
<Tooltip
popupContent={
@ -165,7 +165,7 @@ const WorkflowToolAsModal: FC<Props> = ({
</div>
{/* description */}
<div>
<div className='py-2 leading-5 text-sm font-medium text-gray-900'>{t('tools.createTool.description')}</div>
<div className='py-2 system-sm-medium text-text-primary'>{t('tools.createTool.description')}</div>
<Textarea
placeholder={t('tools.createTool.descriptionPlaceholder') || ''}
value={description}
@ -174,11 +174,11 @@ const WorkflowToolAsModal: FC<Props> = ({
</div>
{/* Tool Input */}
<div>
<div className='py-2 leading-5 text-sm font-medium text-gray-900'>{t('tools.createTool.toolInput.title')}</div>
<div className='rounded-lg border border-gray-200 w-full overflow-x-auto'>
<table className='w-full leading-[18px] text-xs text-gray-700 font-normal'>
<thead className='text-gray-500 uppercase'>
<tr className='border-b border-gray-200'>
<div className='py-2 system-sm-medium text-text-primary'>{t('tools.createTool.toolInput.title')}</div>
<div className='rounded-lg border border-divider-regular w-full overflow-x-auto'>
<table className='w-full leading-[18px] text-xs text-text-secondary font-normal'>
<thead className='text-text-tertiary uppercase'>
<tr className='border-b border-divider-regular'>
<th className="p-2 pl-3 font-medium w-[156px]">{t('tools.createTool.toolInput.name')}</th>
<th className="p-2 pl-3 font-medium w-[102px]">{t('tools.createTool.toolInput.method')}</th>
<th className="p-2 pl-3 font-medium">{t('tools.createTool.toolInput.description')}</th>
@ -186,22 +186,22 @@ const WorkflowToolAsModal: FC<Props> = ({
</thead>
<tbody>
{parameters.map((item, index) => (
<tr key={index} className='border-b last:border-0 border-gray-200'>
<tr key={index} className='border-b last:border-0 border-divider-regular'>
<td className="p-2 pl-3 max-w-[156px]">
<div className='text-[13px] leading-[18px]'>
<div title={item.name} className='flex'>
<span className='font-medium text-gray-900 truncate'>{item.name}</span>
<span className='font-medium text-text-primary truncate'>{item.name}</span>
<span className='shrink-0 pl-1 text-[#ec4a0a] text-xs leading-[18px]'>{item.required ? t('tools.createTool.toolInput.required') : ''}</span>
</div>
<div className='text-gray-500'>{item.type}</div>
<div className='text-text-tertiary'>{item.type}</div>
</div>
</td>
<td>
{item.name === '__image' && (
<div className={cn(
'flex items-center gap-1 min-h-[56px] px-3 py-2 h-9 bg-white cursor-default',
'flex items-center gap-1 min-h-[56px] px-3 py-2 h-9 bg-transparent cursor-default',
)}>
<div className={cn('grow text-[13px] leading-[18px] text-gray-700 truncate')}>
<div className={cn('grow text-[13px] leading-[18px] text-text-secondary truncate')}>
{t('tools.createTool.toolInput.methodParameter')}
</div>
</div>
@ -210,10 +210,10 @@ const WorkflowToolAsModal: FC<Props> = ({
<MethodSelector value={item.form} onChange={value => handleParameterChange('form', value, index)} />
)}
</td>
<td className="p-2 pl-3 text-gray-500 w-[236px]">
<td className="p-2 pl-3 text-text-tertiary w-[236px]">
<input
type='text'
className='grow text-gray-700 text-[13px] leading-[18px] font-normal bg-white outline-none appearance-none caret-primary-600 placeholder:text-gray-300'
className='w-full text-text-secondary text-[13px] leading-[18px] font-normal bg-transparent outline-none appearance-none caret-primary-600 placeholder:text-text-quaternary'
placeholder={t('tools.createTool.toolInput.descriptionPlaceholder')!}
value={item.description}
onChange={e => handleParameterChange('description', e.target.value, index)}
@ -227,12 +227,12 @@ const WorkflowToolAsModal: FC<Props> = ({
</div>
{/* Tags */}
<div>
<div className='py-2 leading-5 text-sm font-medium text-gray-900'>{t('tools.createTool.toolInput.label')}</div>
<div className='py-2 system-sm-medium text-text-primary'>{t('tools.createTool.toolInput.label')}</div>
<LabelSelector value={labels} onChange={handleLabelSelect} />
</div>
{/* Privacy Policy */}
<div>
<div className='py-2 leading-5 text-sm font-medium text-gray-900'>{t('tools.createTool.privacyPolicy')}</div>
<div className='py-2 system-sm-medium text-text-primary'>{t('tools.createTool.privacyPolicy')}</div>
<Input
className='h-10'
value={privacyPolicy}
@ -240,9 +240,9 @@ const WorkflowToolAsModal: FC<Props> = ({
placeholder={t('tools.createTool.privacyPolicyPlaceholder') || ''} />
</div>
</div>
<div className={cn((!isAdd && onRemove) ? 'justify-between' : 'justify-end', 'mt-2 shrink-0 flex py-4 px-6 rounded-b-[10px] bg-gray-50 border-t border-black/5')} >
<div className={cn((!isAdd && onRemove) ? 'justify-between' : 'justify-end', 'mt-2 shrink-0 flex py-4 px-6 rounded-b-[10px] bg-background-section-burn border-t border-divider-regular')} >
{!isAdd && onRemove && (
<Button onClick={onRemove} className='text-red-500 border-red-50 hover:border-red-500'>{t('common.operation.delete')}</Button>
<Button variant='warning' onClick={onRemove}>{t('common.operation.delete')}</Button>
)}
<div className='flex space-x-2 '>
<Button onClick={onHide}>{t('common.operation.cancel')}</Button>

View File

@ -34,37 +34,37 @@ const MethodSelector: FC<MethodSelectorProps> = ({
className='block'
>
<div className={cn(
'flex items-center gap-1 min-h-[56px] px-3 py-2 h-9 bg-white cursor-pointer hover:bg-gray-100',
open && '!bg-gray-100 hover:bg-gray-100',
'flex items-center gap-1 min-h-[56px] px-3 py-2 h-9 bg-transparent cursor-pointer hover:bg-background-section-burn',
open && '!bg-background-section-burn hover:bg-background-section-burn',
)}>
<div className={cn('grow text-[13px] leading-[18px] text-gray-700 truncate')}>
<div className={cn('grow text-[13px] leading-[18px] text-text-secondary truncate')}>
{value === 'llm' ? t('tools.createTool.toolInput.methodParameter') : t('tools.createTool.toolInput.methodSetting')}
</div>
<div className='shrink-0 ml-1 text-gray-700 opacity-60'>
<div className='shrink-0 ml-1 text-text-secondary opacity-60'>
<RiArrowDownSLine className='h-4 w-4' />
</div>
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1040]'>
<div className='relative w-[320px] bg-white rounded-lg border-[0.5px] border-gray-200 shadow-lg'>
<div className='relative w-[320px] bg-components-panel-bg-blur backdrop-blur-sm rounded-lg border-[0.5px] border-components-panel-border shadow-lg'>
<div className='p-1'>
<div className='pl-3 pr-2 py-2.5 rounded-lg hover:bg-gray-50 cursor-pointer' onClick={() => onChange('llm')}>
<div className='pl-3 pr-2 py-2.5 rounded-lg hover:bg-components-panel-on-panel-item-bg-hover cursor-pointer' onClick={() => onChange('llm')}>
<div className='flex item-center gap-1'>
<div className='shrink-0 w-4 h-4'>
{value === 'llm' && <Check className='shrink-0 w-4 h-4 text-primary-600' />}
{value === 'llm' && <Check className='shrink-0 w-4 h-4 text-text-accent' />}
</div>
<div className='text-[13px] text-gray-700 font-medium leading-[18px]'>{t('tools.createTool.toolInput.methodParameter')}</div>
<div className='text-[13px] text-text-secondary font-medium leading-[18px]'>{t('tools.createTool.toolInput.methodParameter')}</div>
</div>
<div className='pl-5 text-gray-500 text-[13px] leading-[18px]'>{t('tools.createTool.toolInput.methodParameterTip')}</div>
<div className='pl-5 text-text-tertiary text-[13px] leading-[18px]'>{t('tools.createTool.toolInput.methodParameterTip')}</div>
</div>
<div className='pl-3 pr-2 py-2.5 rounded-lg hover:bg-gray-50 cursor-pointer' onClick={() => onChange('form')}>
<div className='pl-3 pr-2 py-2.5 rounded-lg hover:bg-components-panel-on-panel-item-bg-hover cursor-pointer' onClick={() => onChange('form')}>
<div className='flex item-center gap-1'>
<div className='shrink-0 w-4 h-4'>
{value === 'form' && <Check className='shrink-0 w-4 h-4 text-primary-600' />}
{value === 'form' && <Check className='shrink-0 w-4 h-4 text-text-accent' />}
</div>
<div className='text-[13px] text-gray-700 font-medium leading-[18px]'>{t('tools.createTool.toolInput.methodSetting')}</div>
<div className='text-[13px] text-text-secondary font-medium leading-[18px]'>{t('tools.createTool.toolInput.methodSetting')}</div>
</div>
<div className='pl-5 text-gray-500 text-[13px] leading-[18px]'>{t('tools.createTool.toolInput.methodSettingTip')}</div>
<div className='pl-5 text-text-tertiary text-[13px] leading-[18px]'>{t('tools.createTool.toolInput.methodSettingTip')}</div>
</div>
</div>
</div>

View File

@ -192,6 +192,7 @@ export const useWorkflowRun = () => {
const newNodes = produce(nodes, (draft) => {
draft.forEach((node) => {
node.data._waitingRun = true
node.data._runningBranchId = undefined
})
})
setNodes(newNodes)

View File

@ -1,9 +1,17 @@
import { useCallback, useState } from 'react'
import produce from 'immer'
import { useBoolean } from 'ahooks'
import type { OutputVar } from '../../code/types'
import type { ValueSelector } from '@/app/components/workflow/types'
import { VarType } from '@/app/components/workflow/types'
import type {
CodeNodeType,
OutputVar,
} from '../../code/types'
import type {
ValueSelector,
} from '@/app/components/workflow/types'
import {
BlockEnum,
VarType,
} from '@/app/components/workflow/types'
import {
useWorkflow,
} from '@/app/components/workflow/hooks'

View File

@ -52,6 +52,12 @@ const useConfig = (id: string, payload: IterationNodeType) => {
[VarType.number]: VarType.arrayNumber,
[VarType.object]: VarType.arrayObject,
[VarType.file]: VarType.arrayFile,
// list operator node can output array
[VarType.array]: VarType.array,
[VarType.arrayFile]: VarType.arrayFile,
[VarType.arrayString]: VarType.arrayString,
[VarType.arrayNumber]: VarType.arrayNumber,
[VarType.arrayObject]: VarType.arrayObject,
} as Record<VarType, VarType>)[outputItemType] || VarType.arrayString
})
setInputs(newInputs)

View File

@ -59,20 +59,12 @@ const InputVarList: FC<Props> = ({
const newValue = produce(value, (draft: ToolVarInputs) => {
const target = draft[variable]
if (target) {
if (!isSupportConstantValue || varKindType === VarKindType.variable) {
if (isSupportConstantValue)
target.type = VarKindType.variable
target.value = varValue as ValueSelector
}
else {
target.type = VarKindType.constant
target.value = varValue as string
}
target.type = varKindType
target.value = varValue
}
else {
draft[variable] = {
type: VarKindType.variable,
type: varKindType,
value: varValue,
}
}
@ -170,7 +162,7 @@ const InputVarList: FC<Props> = ({
value={varInput?.type === VarKindType.constant ? (varInput?.value || '') : (varInput?.value || [])}
onChange={handleNotMixedTypeChange(variable)}
onOpen={handleOpen(index)}
defaultVarKindType={isNumber ? VarKindType.constant : VarKindType.variable}
defaultVarKindType={varInput?.type || (isNumber ? VarKindType.constant : VarKindType.variable)}
isSupportConstantValue={isSupportConstantValue}
filterVar={isNumber ? filterVar : undefined}
availableVars={isSelect ? availableVars : undefined}

View File

@ -132,7 +132,7 @@ const useConfig = (id: string, payload: ToolNodeType) => {
draft.tool_parameters = {}
})
setInputs(inputsWithDefaultValue)
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currTool])
// setting when call
@ -214,8 +214,13 @@ const useConfig = (id: string, payload: ToolNodeType) => {
.map(k => inputs.tool_parameters[k])
const varInputs = getInputVars(hadVarParams.map((p) => {
if (p.type === VarType.variable)
if (p.type === VarType.variable) {
// handle the old wrong value not crash the page
if (!(p.value as any).join)
return `{{#${p.value}#}}`
return `{{#${(p.value as ValueSelector).join('.')}#}}`
}
return p.value as string
}))

View File

@ -161,6 +161,7 @@ const translation = {
newlyReleased: 'Newly Released',
firstReleased: 'First Released',
},
viewMore: 'View more',
},
task: {
installing: 'Installing {{installingLength}} plugins, 0 done.',

View File

@ -161,6 +161,7 @@ const translation = {
newlyReleased: '最新发布',
firstReleased: '首次发布',
},
viewMore: '查看更多',
},
task: {
installing: '{{installingLength}} 个插件安装中0 已完成',

View File

@ -1,6 +1,6 @@
{
"name": "dify-web",
"version": "0.13.2",
"version": "0.14.0",
"private": true,
"engines": {
"node": ">=18.17.0"
@ -39,7 +39,7 @@
"@next/mdx": "^14.0.4",
"@octokit/core": "^6.1.2",
"@octokit/request-error": "^6.1.5",
"@remixicon/react": "^4.3.0",
"@remixicon/react": "^4.5.0",
"@sentry/react": "^7.54.0",
"@sentry/utils": "^7.54.0",
"@svgdotjs/svg.js": "^3.2.4",
@ -91,6 +91,8 @@
"react-multi-email": "^1.0.25",
"react-papaparse": "^4.4.0",
"react-slider": "^2.0.6",
"react-hotkeys-hook": "^4.6.1",
"react-pdf-highlighter": "^8.0.0-rc.0",
"react-sortablejs": "^6.1.4",
"react-syntax-highlighter": "^15.6.1",
"react-tooltip": "5.8.3",

File diff suppressed because it is too large Load Diff

View File

@ -66,6 +66,7 @@ const config = {
// => @media (min-width: 600px) { ... }
pc: '769px',
// => @media (min-width: 769px) { ... }
'2k': '2560px',
},
boxShadow: {
'xs': '0px 1px 2px 0px rgba(16, 24, 40, 0.05)',

View File

@ -2242,10 +2242,10 @@
classcat "^5.0.3"
zustand "^4.4.1"
"@remixicon/react@^4.3.0":
version "4.3.0"
resolved "https://registry.npmjs.org/@remixicon/react/-/react-4.3.0.tgz#8ab34d03fccca53bf66f87c6e3f943ef5c65684f"
integrity sha512-mAVDn8pAa9dURltGwiYrf7bPIqjG4ZAnCUHfjpgz3g+HLSDNXOaJ67Z5wmjVB5KMGpp9JbbTN5vsp2z+ajVLWg==
"@remixicon/react@^4.5.0":
version "4.5.0"
resolved "https://registry.yarnpkg.com/@remixicon/react/-/react-4.5.0.tgz#5600d122ee4995bff2c4442cb056eeb4f11ecb5a"
integrity sha512-Xr20SxMpRNlgXZnoF5BCMyZuQEhXY3yJCyms8kxB/vJCCiV1nWdiO48XqRG5LBd1192iSHC4m658AIWi6rmBFg==
"@rgrove/parse-xml@^4.1.0":
version "4.1.0"