Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
D
dify
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
ai-tech
dify
Commits
6040da75
Commit
6040da75
authored
Jul 17, 2023
by
jyong
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'main' into feat/milvus-support
# Conflicts: # api/app.py
parents
4a45e94b
24f34569
Changes
63
Hide whitespace changes
Inline
Side-by-side
Showing
63 changed files
with
1459 additions
and
375 deletions
+1459
-375
.env.example
api/.env.example
+15
-4
Dockerfile
api/Dockerfile
+5
-3
app.py
api/app.py
+11
-14
config.py
api/config.py
+17
-5
__init__.py
api/controllers/console/__init__.py
+1
-1
activate.py
api/controllers/console/auth/activate.py
+75
-0
data_source_oauth.py
api/controllers/console/auth/data_source_oauth.py
+5
-5
oauth.py
api/controllers/console/auth/oauth.py
+3
-3
error.py
api/controllers/console/error.py
+6
-0
account.py
api/controllers/console/workspace/account.py
+8
-4
error.py
api/controllers/console/workspace/error.py
+6
-0
members.py
api/controllers/console/workspace/members.py
+14
-4
conversation.py
api/controllers/service_api/app/conversation.py
+6
-6
ext_mail.py
api/extensions/ext_mail.py
+61
-0
account.py
api/models/account.py
+4
-0
model.py
api/models/model.py
+3
-2
requirements.txt
api/requirements.txt
+3
-2
account_service.py
api/services/account_service.py
+91
-11
mail_invite_member_task.py
api/tasks/mail_invite_member_task.py
+52
-0
docker-compose.yaml
docker/docker-compose.yaml
+32
-12
Dockerfile
web/Dockerfile
+2
-2
activateForm.tsx
web/app/activate/activateForm.tsx
+233
-0
page.tsx
web/app/activate/page.tsx
+32
-0
style.module.css
web/app/activate/style.module.css
+4
-0
team-28x28.png
web/app/activate/team-28x28.png
+0
-0
index.tsx
web/app/components/app/text-generate/item/index.tsx
+14
-1
locale.tsx
web/app/components/base/select/locale.tsx
+13
-12
template_chat.en.mdx
web/app/components/develop/template/template_chat.en.mdx
+47
-0
template_chat.zh.mdx
web/app/components/develop/template/template_chat.zh.mdx
+46
-0
index.module.css
web/app/components/header/account-about/index.module.css
+1
-1
index.tsx
web/app/components/header/account-about/index.tsx
+1
-1
index.tsx
.../components/header/account-setting/account-page/index.tsx
+155
-45
index.tsx
web/app/components/header/account-setting/index.tsx
+32
-21
index.tsx
.../components/header/account-setting/members-page/index.tsx
+4
-1
index.tsx
...eader/account-setting/members-page/invite-modal/index.tsx
+12
-12
copied.svg
...ount-setting/members-page/invited-modal/assets/copied.svg
+3
-0
copy-hover.svg
...-setting/members-page/invited-modal/assets/copy-hover.svg
+3
-0
copy.svg
...ccount-setting/members-page/invited-modal/assets/copy.svg
+3
-0
index.module.css
...count-setting/members-page/invited-modal/index.module.css
+16
-0
index.tsx
...ader/account-setting/members-page/invited-modal/index.tsx
+15
-9
invitation-link.tsx
...nt-setting/members-page/invited-modal/invitation-link.tsx
+63
-0
index.tsx
...components/header/account-setting/provider-page/index.tsx
+1
-1
index.tsx
web/app/components/share/text-generation/index.tsx
+40
-12
index.tsx
web/app/components/share/text-generation/result/index.tsx
+5
-5
index.tsx
...ts/share/text-generation/run-batch/res-download/index.tsx
+41
-0
installForm.tsx
web/app/install/installForm.tsx
+33
-37
_header.tsx
web/app/signin/_header.tsx
+1
-7
forms.tsx
web/app/signin/forms.tsx
+2
-3
normalForm.tsx
web/app/signin/normalForm.tsx
+23
-23
oneMoreStep.tsx
web/app/signin/oneMoreStep.tsx
+22
-12
page.tsx
web/app/signin/page.tsx
+5
-6
app-context.tsx
web/context/app-context.tsx
+2
-0
entrypoint.sh
web/docker/entrypoint.sh
+13
-2
common.en.ts
web/i18n/lang/common.en.ts
+14
-3
common.zh.ts
web/i18n/lang/common.zh.ts
+13
-3
login.en.ts
web/i18n/lang/login.en.ts
+53
-37
login.zh.ts
web/i18n/lang/login.zh.ts
+53
-37
share-app.en.ts
web/i18n/lang/share-app.en.ts
+1
-0
share-app.zh.ts
web/i18n/lang/share-app.zh.ts
+1
-0
common.ts
web/models/common.ts
+2
-0
package.json
web/package.json
+2
-2
base.ts
web/service/base.ts
+5
-2
common.ts
web/service/common.ts
+10
-2
No files found.
api/.env.example
View file @
6040da75
...
...
@@ -8,13 +8,19 @@ EDITION=SELF_HOSTED
SECRET_KEY=
# Console API base URL
CONSOLE_URL=http://127.0.0.1:5001
CONSOLE_API_URL=http://127.0.0.1:5001
# Console frontend web base URL
CONSOLE_WEB_URL=http://127.0.0.1:3000
# Service API base URL
API_URL=http://127.0.0.1:5001
SERVICE_API_URL=http://127.0.0.1:5001
# Web APP API base URL
APP_API_URL=http://127.0.0.1:5001
# Web APP base URL
APP_URL=http://127.0.0.1:3000
# Web APP
frontend web
base URL
APP_
WEB_
URL=http://127.0.0.1:3000
# celery configuration
CELERY_BROKER_URL=redis://:difyai123456@localhost:6379/1
...
...
@@ -79,6 +85,11 @@ WEAVIATE_BATCH_SIZE=100
QDRANT_URL=path:storage/qdrant
QDRANT_API_KEY=your-qdrant-api-key
# Mail configuration, support: resend
MAIL_TYPE=
MAIL_DEFAULT_SEND_FROM=no-reply <no-reply@dify.ai>
RESEND_API_KEY=
# Sentry configuration
SENTRY_DSN=
...
...
api/Dockerfile
View file @
6040da75
...
...
@@ -5,9 +5,11 @@ LABEL maintainer="takatost@gmail.com"
ENV
FLASK_APP app.py
ENV
EDITION SELF_HOSTED
ENV
DEPLOY_ENV PRODUCTION
ENV
CONSOLE_URL http://127.0.0.1:5001
ENV
API_URL http://127.0.0.1:5001
ENV
APP_URL http://127.0.0.1:5001
ENV
CONSOLE_API_URL http://127.0.0.1:5001
ENV
CONSOLE_WEB_URL http://127.0.0.1:3000
ENV
SERVICE_API_URL http://127.0.0.1:5001
ENV
APP_API_URL http://127.0.0.1:5001
ENV
APP_WEB_URL http://127.0.0.1:3000
EXPOSE
5001
...
...
api/app.py
View file @
6040da75
...
...
@@ -2,9 +2,7 @@
import
os
from
datetime
import
datetime
import
requests
from
tasks.generate_test_task
import
generate_test_task
from
werkzeug.exceptions
import
Forbidden
if
not
os
.
environ
.
get
(
"DEBUG"
)
or
os
.
environ
.
get
(
"DEBUG"
)
.
lower
()
!=
'true'
:
from
gevent
import
monkey
...
...
@@ -19,7 +17,7 @@ import flask_login
from
flask_cors
import
CORS
from
extensions
import
ext_session
,
ext_celery
,
ext_sentry
,
ext_redis
,
ext_login
,
ext_migrate
,
\
ext_database
,
ext_storage
ext_database
,
ext_storage
,
ext_mail
from
extensions.ext_database
import
db
from
extensions.ext_login
import
login_manager
...
...
@@ -31,7 +29,7 @@ from events import event_handlers
import
core
from
config
import
Config
,
CloudEditionConfig
from
commands
import
register_commands
from
models.account
import
TenantAccountJoin
from
models.account
import
TenantAccountJoin
,
AccountStatus
from
models.model
import
Account
,
EndUser
,
App
import
warnings
...
...
@@ -87,6 +85,7 @@ def initialize_extensions(app):
ext_celery
.
init_app
(
app
)
ext_session
.
init_app
(
app
)
ext_login
.
init_app
(
app
)
ext_mail
.
init_app
(
app
)
ext_sentry
.
init_app
(
app
)
...
...
@@ -104,6 +103,9 @@ def load_user(user_id):
account
=
db
.
session
.
query
(
Account
)
.
filter
(
Account
.
id
==
account_id
)
.
first
()
if
account
:
if
account
.
status
==
AccountStatus
.
BANNED
.
value
or
account
.
status
==
AccountStatus
.
CLOSED
.
value
:
raise
Forbidden
(
'Account is banned or closed.'
)
workspace_id
=
session
.
get
(
'workspace_id'
)
if
workspace_id
:
tenant_account_join
=
db
.
session
.
query
(
TenantAccountJoin
)
.
filter
(
...
...
@@ -153,6 +155,10 @@ def register_blueprints(app):
from
controllers.web
import
bp
as
web_bp
from
controllers.console
import
bp
as
console_app_bp
CORS
(
service_api_bp
,
allow_headers
=
[
'Content-Type'
,
'Authorization'
,
'X-App-Code'
],
methods
=
[
'GET'
,
'PUT'
,
'POST'
,
'DELETE'
,
'OPTIONS'
,
'PATCH'
]
)
app
.
register_blueprint
(
service_api_bp
)
CORS
(
web_bp
,
...
...
@@ -203,15 +209,6 @@ def health():
}),
status
=
200
,
content_type
=
"application/json"
)
@
app
.
route
(
'/test'
)
def
test
():
generate_test_task
.
delay
()
res
=
requests
.
post
(
'https://api.openai.com/v1/chat/completions'
)
print
(
res
)
return
Response
(
json
.
dumps
({
'status'
:
'ok'
,
}),
status
=
200
,
content_type
=
"application/json"
)
@
app
.
route
(
'/threads'
)
def
threads
():
num_threads
=
threading
.
active_count
()
...
...
api/config.py
View file @
6040da75
...
...
@@ -28,9 +28,11 @@ DEFAULTS = {
'SESSION_REDIS_USE_SSL'
:
'False'
,
'OAUTH_REDIRECT_PATH'
:
'/console/api/oauth/authorize'
,
'OAUTH_REDIRECT_INDEX_PATH'
:
'/'
,
'CONSOLE_URL'
:
'https://cloud.dify.ai'
,
'API_URL'
:
'https://api.dify.ai'
,
'APP_URL'
:
'https://udify.app'
,
'CONSOLE_WEB_URL'
:
'https://cloud.dify.ai'
,
'CONSOLE_API_URL'
:
'https://cloud.dify.ai'
,
'SERVICE_API_URL'
:
'https://api.dify.ai'
,
'APP_WEB_URL'
:
'https://udify.app'
,
'APP_API_URL'
:
'https://udify.app'
,
'STORAGE_TYPE'
:
'local'
,
'STORAGE_LOCAL_PATH'
:
'storage'
,
'CHECK_UPDATE_URL'
:
'https://updates.dify.ai'
,
...
...
@@ -76,10 +78,15 @@ class Config:
def
__init__
(
self
):
# app settings
self
.
CONSOLE_API_URL
=
get_env
(
'CONSOLE_URL'
)
if
get_env
(
'CONSOLE_URL'
)
else
get_env
(
'CONSOLE_API_URL'
)
self
.
CONSOLE_WEB_URL
=
get_env
(
'CONSOLE_URL'
)
if
get_env
(
'CONSOLE_URL'
)
else
get_env
(
'CONSOLE_WEB_URL'
)
self
.
SERVICE_API_URL
=
get_env
(
'API_URL'
)
if
get_env
(
'API_URL'
)
else
get_env
(
'SERVICE_API_URL'
)
self
.
APP_WEB_URL
=
get_env
(
'APP_URL'
)
if
get_env
(
'APP_URL'
)
else
get_env
(
'APP_WEB_URL'
)
self
.
APP_API_URL
=
get_env
(
'APP_URL'
)
if
get_env
(
'APP_URL'
)
else
get_env
(
'APP_API_URL'
)
self
.
CONSOLE_URL
=
get_env
(
'CONSOLE_URL'
)
self
.
API_URL
=
get_env
(
'API_URL'
)
self
.
APP_URL
=
get_env
(
'APP_URL'
)
self
.
CURRENT_VERSION
=
"0.3.
7
"
self
.
CURRENT_VERSION
=
"0.3.
8
"
self
.
COMMIT_SHA
=
get_env
(
'COMMIT_SHA'
)
self
.
EDITION
=
"SELF_HOSTED"
self
.
DEPLOY_ENV
=
get_env
(
'DEPLOY_ENV'
)
...
...
@@ -147,10 +154,15 @@ class Config:
# cors settings
self
.
CONSOLE_CORS_ALLOW_ORIGINS
=
get_cors_allow_origins
(
'CONSOLE_CORS_ALLOW_ORIGINS'
,
self
.
CONSOLE_URL
)
'CONSOLE_CORS_ALLOW_ORIGINS'
,
self
.
CONSOLE_
WEB_
URL
)
self
.
WEB_API_CORS_ALLOW_ORIGINS
=
get_cors_allow_origins
(
'WEB_API_CORS_ALLOW_ORIGINS'
,
'*'
)
# mail settings
self
.
MAIL_TYPE
=
get_env
(
'MAIL_TYPE'
)
self
.
MAIL_DEFAULT_SEND_FROM
=
get_env
(
'MAIL_DEFAULT_SEND_FROM'
)
self
.
RESEND_API_KEY
=
get_env
(
'RESEND_API_KEY'
)
# sentry settings
self
.
SENTRY_DSN
=
get_env
(
'SENTRY_DSN'
)
self
.
SENTRY_TRACES_SAMPLE_RATE
=
float
(
get_env
(
'SENTRY_TRACES_SAMPLE_RATE'
))
...
...
api/controllers/console/__init__.py
View file @
6040da75
...
...
@@ -12,7 +12,7 @@ from . import setup, version, apikey, admin
from
.app
import
app
,
site
,
completion
,
model_config
,
statistic
,
conversation
,
message
,
generator
,
audio
# Import auth controllers
from
.auth
import
login
,
oauth
,
data_source_oauth
from
.auth
import
login
,
oauth
,
data_source_oauth
,
activate
# Import datasets controllers
from
.datasets
import
datasets
,
datasets_document
,
datasets_segments
,
file
,
hit_testing
,
data_source
...
...
api/controllers/console/auth/activate.py
0 → 100644
View file @
6040da75
import
base64
import
secrets
from
datetime
import
datetime
from
flask_restful
import
Resource
,
reqparse
from
controllers.console
import
api
from
controllers.console.error
import
AlreadyActivateError
from
extensions.ext_database
import
db
from
libs.helper
import
email
,
str_len
,
supported_language
,
timezone
from
libs.password
import
valid_password
,
hash_password
from
models.account
import
AccountStatus
,
Tenant
from
services.account_service
import
RegisterService
class
ActivateCheckApi
(
Resource
):
def
get
(
self
):
parser
=
reqparse
.
RequestParser
()
parser
.
add_argument
(
'workspace_id'
,
type
=
str
,
required
=
True
,
nullable
=
False
,
location
=
'args'
)
parser
.
add_argument
(
'email'
,
type
=
email
,
required
=
True
,
nullable
=
False
,
location
=
'args'
)
parser
.
add_argument
(
'token'
,
type
=
str
,
required
=
True
,
nullable
=
False
,
location
=
'args'
)
args
=
parser
.
parse_args
()
account
=
RegisterService
.
get_account_if_token_valid
(
args
[
'workspace_id'
],
args
[
'email'
],
args
[
'token'
])
tenant
=
db
.
session
.
query
(
Tenant
)
.
filter
(
Tenant
.
id
==
args
[
'workspace_id'
],
Tenant
.
status
==
'normal'
)
.
first
()
return
{
'is_valid'
:
account
is
not
None
,
'workspace_name'
:
tenant
.
name
}
class
ActivateApi
(
Resource
):
def
post
(
self
):
parser
=
reqparse
.
RequestParser
()
parser
.
add_argument
(
'workspace_id'
,
type
=
str
,
required
=
True
,
nullable
=
False
,
location
=
'json'
)
parser
.
add_argument
(
'email'
,
type
=
email
,
required
=
True
,
nullable
=
False
,
location
=
'json'
)
parser
.
add_argument
(
'token'
,
type
=
str
,
required
=
True
,
nullable
=
False
,
location
=
'json'
)
parser
.
add_argument
(
'name'
,
type
=
str_len
(
30
),
required
=
True
,
nullable
=
False
,
location
=
'json'
)
parser
.
add_argument
(
'password'
,
type
=
valid_password
,
required
=
True
,
nullable
=
False
,
location
=
'json'
)
parser
.
add_argument
(
'interface_language'
,
type
=
supported_language
,
required
=
True
,
nullable
=
False
,
location
=
'json'
)
parser
.
add_argument
(
'timezone'
,
type
=
timezone
,
required
=
True
,
nullable
=
False
,
location
=
'json'
)
args
=
parser
.
parse_args
()
account
=
RegisterService
.
get_account_if_token_valid
(
args
[
'workspace_id'
],
args
[
'email'
],
args
[
'token'
])
if
account
is
None
:
raise
AlreadyActivateError
()
RegisterService
.
revoke_token
(
args
[
'workspace_id'
],
args
[
'email'
],
args
[
'token'
])
account
.
name
=
args
[
'name'
]
# generate password salt
salt
=
secrets
.
token_bytes
(
16
)
base64_salt
=
base64
.
b64encode
(
salt
)
.
decode
()
# encrypt password with salt
password_hashed
=
hash_password
(
args
[
'password'
],
salt
)
base64_password_hashed
=
base64
.
b64encode
(
password_hashed
)
.
decode
()
account
.
password
=
base64_password_hashed
account
.
password_salt
=
base64_salt
account
.
interface_language
=
args
[
'interface_language'
]
account
.
timezone
=
args
[
'timezone'
]
account
.
interface_theme
=
'light'
account
.
status
=
AccountStatus
.
ACTIVE
.
value
account
.
initialized_at
=
datetime
.
utcnow
()
db
.
session
.
commit
()
return
{
'result'
:
'success'
}
api
.
add_resource
(
ActivateCheckApi
,
'/activate/check'
)
api
.
add_resource
(
ActivateApi
,
'/activate'
)
api/controllers/console/auth/data_source_oauth.py
View file @
6040da75
...
...
@@ -20,7 +20,7 @@ def get_oauth_providers():
client_secret
=
current_app
.
config
.
get
(
'NOTION_CLIENT_SECRET'
),
redirect_uri
=
current_app
.
config
.
get
(
'CONSOLE_URL'
)
+
'/console/api/oauth/data-source/callback/notion'
)
'CONSOLE_
API_
URL'
)
+
'/console/api/oauth/data-source/callback/notion'
)
OAUTH_PROVIDERS
=
{
'notion'
:
notion_oauth
...
...
@@ -42,7 +42,7 @@ class OAuthDataSource(Resource):
if
current_app
.
config
.
get
(
'NOTION_INTEGRATION_TYPE'
)
==
'internal'
:
internal_secret
=
current_app
.
config
.
get
(
'NOTION_INTERNAL_SECRET'
)
oauth_provider
.
save_internal_access_token
(
internal_secret
)
return
redirect
(
f
'{current_app.config.get("CONSOLE_URL")}?oauth_data_source=success'
)
return
redirect
(
f
'{current_app.config.get("CONSOLE_
WEB_
URL")}?oauth_data_source=success'
)
else
:
auth_url
=
oauth_provider
.
get_authorization_url
()
return
redirect
(
auth_url
)
...
...
@@ -66,12 +66,12 @@ class OAuthDataSourceCallback(Resource):
f
"An error occurred during the OAuthCallback process with {provider}: {e.response.text}"
)
return
{
'error'
:
'OAuth data source process failed'
},
400
return
redirect
(
f
'{current_app.config.get("CONSOLE_URL")}?oauth_data_source=success'
)
return
redirect
(
f
'{current_app.config.get("CONSOLE_
WEB_
URL")}?oauth_data_source=success'
)
elif
'error'
in
request
.
args
:
error
=
request
.
args
.
get
(
'error'
)
return
redirect
(
f
'{current_app.config.get("CONSOLE_URL")}?oauth_data_source={error}'
)
return
redirect
(
f
'{current_app.config.get("CONSOLE_
WEB_
URL")}?oauth_data_source={error}'
)
else
:
return
redirect
(
f
'{current_app.config.get("CONSOLE_URL")}?oauth_data_source=access_denied'
)
return
redirect
(
f
'{current_app.config.get("CONSOLE_
WEB_
URL")}?oauth_data_source=access_denied'
)
class
OAuthDataSourceSync
(
Resource
):
...
...
api/controllers/console/auth/oauth.py
View file @
6040da75
...
...
@@ -20,13 +20,13 @@ def get_oauth_providers():
client_secret
=
current_app
.
config
.
get
(
'GITHUB_CLIENT_SECRET'
),
redirect_uri
=
current_app
.
config
.
get
(
'CONSOLE_URL'
)
+
'/console/api/oauth/authorize/github'
)
'CONSOLE_
API_
URL'
)
+
'/console/api/oauth/authorize/github'
)
google_oauth
=
GoogleOAuth
(
client_id
=
current_app
.
config
.
get
(
'GOOGLE_CLIENT_ID'
),
client_secret
=
current_app
.
config
.
get
(
'GOOGLE_CLIENT_SECRET'
),
redirect_uri
=
current_app
.
config
.
get
(
'CONSOLE_URL'
)
+
'/console/api/oauth/authorize/google'
)
'CONSOLE_
API_
URL'
)
+
'/console/api/oauth/authorize/google'
)
OAUTH_PROVIDERS
=
{
'github'
:
github_oauth
,
...
...
@@ -80,7 +80,7 @@ class OAuthCallback(Resource):
flask_login
.
login_user
(
account
,
remember
=
True
)
AccountService
.
update_last_login
(
account
,
request
)
return
redirect
(
f
'{current_app.config.get("CONSOLE_URL")}?oauth_login=success'
)
return
redirect
(
f
'{current_app.config.get("CONSOLE_
WEB_
URL")}?oauth_login=success'
)
def
_get_account_by_openid_or_email
(
provider
:
str
,
user_info
:
OAuthUserInfo
)
->
Optional
[
Account
]:
...
...
api/controllers/console/error.py
View file @
6040da75
...
...
@@ -18,3 +18,9 @@ class AccountNotLinkTenantError(BaseHTTPException):
error_code
=
'account_not_link_tenant'
description
=
"Account not link tenant."
code
=
403
class
AlreadyActivateError
(
BaseHTTPException
):
error_code
=
'already_activate'
description
=
"Auth Token is invalid or account already activated, please check again."
code
=
403
api/controllers/console/workspace/account.py
View file @
6040da75
...
...
@@ -6,22 +6,23 @@ from flask import current_app, request
from
flask_login
import
login_required
,
current_user
from
flask_restful
import
Resource
,
reqparse
,
fields
,
marshal_with
from
services.errors.account
import
CurrentPasswordIncorrectError
as
ServiceCurrentPasswordIncorrectError
from
controllers.console
import
api
from
controllers.console.setup
import
setup_required
from
controllers.console.workspace.error
import
AccountAlreadyInitedError
,
InvalidInvitationCodeError
,
\
RepeatPasswordNotMatchError
RepeatPasswordNotMatchError
,
CurrentPasswordIncorrectError
from
controllers.console.wraps
import
account_initialization_required
from
libs.helper
import
TimestampField
,
supported_language
,
timezone
from
extensions.ext_database
import
db
from
models.account
import
InvitationCode
,
AccountIntegrate
from
services.account_service
import
AccountService
account_fields
=
{
'id'
:
fields
.
String
,
'name'
:
fields
.
String
,
'avatar'
:
fields
.
String
,
'email'
:
fields
.
String
,
'is_password_set'
:
fields
.
Boolean
,
'interface_language'
:
fields
.
String
,
'interface_theme'
:
fields
.
String
,
'timezone'
:
fields
.
String
,
...
...
@@ -194,8 +195,11 @@ class AccountPasswordApi(Resource):
if
args
[
'new_password'
]
!=
args
[
'repeat_new_password'
]:
raise
RepeatPasswordNotMatchError
()
AccountService
.
update_account_password
(
current_user
,
args
[
'password'
],
args
[
'new_password'
])
try
:
AccountService
.
update_account_password
(
current_user
,
args
[
'password'
],
args
[
'new_password'
])
except
ServiceCurrentPasswordIncorrectError
:
raise
CurrentPasswordIncorrectError
()
return
{
"result"
:
"success"
}
...
...
api/controllers/console/workspace/error.py
View file @
6040da75
...
...
@@ -7,6 +7,12 @@ class RepeatPasswordNotMatchError(BaseHTTPException):
code
=
400
class
CurrentPasswordIncorrectError
(
BaseHTTPException
):
error_code
=
'current_password_incorrect'
description
=
"Current password is incorrect."
code
=
400
class
ProviderRequestFailedError
(
BaseHTTPException
):
error_code
=
'provider_request_failed'
description
=
None
...
...
api/controllers/console/workspace/members.py
View file @
6040da75
# -*- coding:utf-8 -*-
from
flask
import
current_app
from
flask_login
import
login_required
,
current_user
from
flask_restful
import
Resource
,
reqparse
,
marshal_with
,
abort
,
fields
,
marshal
...
...
@@ -60,7 +60,8 @@ class MemberInviteEmailApi(Resource):
inviter
=
current_user
try
:
RegisterService
.
invite_new_member
(
inviter
.
current_tenant
,
invitee_email
,
role
=
invitee_role
,
inviter
=
inviter
)
token
=
RegisterService
.
invite_new_member
(
inviter
.
current_tenant
,
invitee_email
,
role
=
invitee_role
,
inviter
=
inviter
)
account
=
db
.
session
.
query
(
Account
,
TenantAccountJoin
.
role
)
.
join
(
TenantAccountJoin
,
Account
.
id
==
TenantAccountJoin
.
account_id
)
.
filter
(
Account
.
email
==
args
[
'email'
])
.
first
()
...
...
@@ -78,7 +79,16 @@ class MemberInviteEmailApi(Resource):
# todo:413
return
{
'result'
:
'success'
,
'account'
:
account
},
201
return
{
'result'
:
'success'
,
'account'
:
account
,
'invite_url'
:
'{}/activate?workspace_id={}&email={}&token={}'
.
format
(
current_app
.
config
.
get
(
"CONSOLE_WEB_URL"
),
str
(
current_user
.
current_tenant_id
),
invitee_email
,
token
)
},
201
class
MemberCancelInviteApi
(
Resource
):
...
...
@@ -88,7 +98,7 @@ class MemberCancelInviteApi(Resource):
@
login_required
@
account_initialization_required
def
delete
(
self
,
member_id
):
member
=
Account
.
query
.
get
(
str
(
member_id
)
)
member
=
db
.
session
.
query
(
Account
)
.
filter
(
Account
.
id
==
str
(
member_id
))
.
first
(
)
if
not
member
:
abort
(
404
)
...
...
api/controllers/service_api/app/conversation.py
View file @
6040da75
# -*- coding:utf-8 -*-
from
flask
import
request
from
flask_restful
import
fields
,
marshal_with
,
reqparse
from
flask_restful.inputs
import
int_range
from
werkzeug.exceptions
import
NotFound
...
...
@@ -56,16 +57,14 @@ class ConversationDetailApi(AppApiResource):
conversation_id
=
str
(
c_id
)
parser
=
reqparse
.
RequestParser
()
parser
.
add_argument
(
'user'
,
type
=
str
,
location
=
'args'
)
args
=
parser
.
parse_args
()
user
=
request
.
get_json
()
.
get
(
'user'
)
if
end_user
is
None
and
args
[
'user'
]
is
not
None
:
end_user
=
create_or_update_end_user_for_user_id
(
app_model
,
args
[
'user'
]
)
if
end_user
is
None
and
user
is
not
None
:
end_user
=
create_or_update_end_user_for_user_id
(
app_model
,
user
)
try
:
ConversationService
.
delete
(
app_model
,
conversation_id
,
end_user
)
return
{
"result"
:
"success"
}
,
204
return
{
"result"
:
"success"
}
except
services
.
errors
.
conversation
.
ConversationNotExistsError
:
raise
NotFound
(
"Conversation Not Exists."
)
...
...
@@ -95,3 +94,4 @@ class ConversationRenameApi(AppApiResource):
api
.
add_resource
(
ConversationRenameApi
,
'/conversations/<uuid:c_id>/name'
,
endpoint
=
'conversation_name'
)
api
.
add_resource
(
ConversationApi
,
'/conversations'
)
api
.
add_resource
(
ConversationApi
,
'/conversations/<uuid:c_id>'
,
endpoint
=
'conversation'
)
api
.
add_resource
(
ConversationDetailApi
,
'/conversations/<uuid:c_id>'
,
endpoint
=
'conversation_detail'
)
api/extensions/ext_mail.py
0 → 100644
View file @
6040da75
from
typing
import
Optional
import
resend
from
flask
import
Flask
class
Mail
:
def
__init__
(
self
):
self
.
_client
=
None
self
.
_default_send_from
=
None
def
is_inited
(
self
)
->
bool
:
return
self
.
_client
is
not
None
def
init_app
(
self
,
app
:
Flask
):
if
app
.
config
.
get
(
'MAIL_TYPE'
):
if
app
.
config
.
get
(
'MAIL_DEFAULT_SEND_FROM'
):
self
.
_default_send_from
=
app
.
config
.
get
(
'MAIL_DEFAULT_SEND_FROM'
)
if
app
.
config
.
get
(
'MAIL_TYPE'
)
==
'resend'
:
api_key
=
app
.
config
.
get
(
'RESEND_API_KEY'
)
if
not
api_key
:
raise
ValueError
(
'RESEND_API_KEY is not set'
)
resend
.
api_key
=
api_key
self
.
_client
=
resend
.
Emails
else
:
raise
ValueError
(
'Unsupported mail type {}'
.
format
(
app
.
config
.
get
(
'MAIL_TYPE'
)))
def
send
(
self
,
to
:
str
,
subject
:
str
,
html
:
str
,
from_
:
Optional
[
str
]
=
None
):
if
not
self
.
_client
:
raise
ValueError
(
'Mail client is not initialized'
)
if
not
from_
and
self
.
_default_send_from
:
from_
=
self
.
_default_send_from
if
not
from_
:
raise
ValueError
(
'mail from is not set'
)
if
not
to
:
raise
ValueError
(
'mail to is not set'
)
if
not
subject
:
raise
ValueError
(
'mail subject is not set'
)
if
not
html
:
raise
ValueError
(
'mail html is not set'
)
self
.
_client
.
send
({
"from"
:
from_
,
"to"
:
to
,
"subject"
:
subject
,
"html"
:
html
})
def
init_app
(
app
:
Flask
):
mail
.
init_app
(
app
)
mail
=
Mail
()
api/models/account.py
View file @
6040da75
...
...
@@ -38,6 +38,10 @@ class Account(UserMixin, db.Model):
created_at
=
db
.
Column
(
db
.
DateTime
,
nullable
=
False
,
server_default
=
db
.
text
(
'CURRENT_TIMESTAMP(0)'
))
updated_at
=
db
.
Column
(
db
.
DateTime
,
nullable
=
False
,
server_default
=
db
.
text
(
'CURRENT_TIMESTAMP(0)'
))
@
property
def
is_password_set
(
self
):
return
self
.
password
is
not
None
@
property
def
current_tenant
(
self
):
return
self
.
_current_tenant
...
...
api/models/model.py
View file @
6040da75
...
...
@@ -56,7 +56,8 @@ class App(db.Model):
@
property
def
api_base_url
(
self
):
return
(
current_app
.
config
[
'API_URL'
]
if
current_app
.
config
[
'API_URL'
]
else
request
.
host_url
.
rstrip
(
'/'
))
+
'/v1'
return
(
current_app
.
config
[
'SERVICE_API_URL'
]
if
current_app
.
config
[
'SERVICE_API_URL'
]
else
request
.
host_url
.
rstrip
(
'/'
))
+
'/v1'
@
property
def
tenant
(
self
):
...
...
@@ -515,7 +516,7 @@ class Site(db.Model):
@
property
def
app_base_url
(
self
):
return
(
current_app
.
config
[
'APP_
URL'
]
if
current_app
.
config
[
'APP
_URL'
]
else
request
.
host_url
.
rstrip
(
'/'
))
return
(
current_app
.
config
[
'APP_
WEB_URL'
]
if
current_app
.
config
[
'APP_WEB
_URL'
]
else
request
.
host_url
.
rstrip
(
'/'
))
class
ApiToken
(
db
.
Model
):
...
...
api/requirements.txt
View file @
6040da75
...
...
@@ -21,7 +21,7 @@ Authlib==1.2.0
boto3~=1.26.123
tenacity==8.2.2
cachetools~=5.3.0
weaviate-client~=3.
16.2
weaviate-client~=3.
21.0
qdrant_client~=1.1.6
mailchimp-transactional~=1.0.50
scikit-learn==1.2.2
...
...
@@ -33,4 +33,5 @@ openpyxl==3.1.2
chardet~=5.1.0
docx2txt==0.8
pypdfium2==4.16.0
pyjwt~=2.6.0
\ No newline at end of file
resend~=0.5.1
pyjwt~=2.6.0
api/services/account_service.py
View file @
6040da75
...
...
@@ -2,13 +2,16 @@
import
base64
import
logging
import
secrets
import
uuid
from
datetime
import
datetime
from
hashlib
import
sha256
from
typing
import
Optional
from
flask
import
session
from
sqlalchemy
import
func
from
events.tenant_event
import
tenant_was_created
from
extensions.ext_redis
import
redis_client
from
services.errors.account
import
AccountLoginError
,
CurrentPasswordIncorrectError
,
LinkAccountIntegrateError
,
\
TenantNotFound
,
AccountNotLinkTenantError
,
InvalidActionError
,
CannotOperateSelfError
,
MemberNotInTenantError
,
\
RoleAlreadyAssignedError
,
NoPermissionError
,
AccountRegisterError
,
AccountAlreadyInTenantError
...
...
@@ -16,6 +19,7 @@ from libs.helper import get_remote_ip
from
libs.password
import
compare_password
,
hash_password
from
libs.rsa
import
generate_key_pair
from
models.account
import
*
from
tasks.mail_invite_member_task
import
send_invite_member_mail_task
class
AccountService
:
...
...
@@ -48,12 +52,18 @@ class AccountService:
@
staticmethod
def
update_account_password
(
account
,
password
,
new_password
):
"""update account password"""
# todo: split validation and update
if
account
.
password
and
not
compare_password
(
password
,
account
.
password
,
account
.
password_salt
):
raise
CurrentPasswordIncorrectError
(
"Current password is incorrect."
)
password_hashed
=
hash_password
(
new_password
,
account
.
password_salt
)
# generate password salt
salt
=
secrets
.
token_bytes
(
16
)
base64_salt
=
base64
.
b64encode
(
salt
)
.
decode
()
# encrypt password with salt
password_hashed
=
hash_password
(
new_password
,
salt
)
base64_password_hashed
=
base64
.
b64encode
(
password_hashed
)
.
decode
()
account
.
password
=
base64_password_hashed
account
.
password_salt
=
base64_salt
db
.
session
.
commit
()
return
account
...
...
@@ -283,8 +293,6 @@ class TenantService:
@
staticmethod
def
remove_member_from_tenant
(
tenant
:
Tenant
,
account
:
Account
,
operator
:
Account
)
->
None
:
"""Remove member from tenant"""
# todo: check permission
if
operator
.
id
==
account
.
id
and
TenantService
.
check_member_permission
(
tenant
,
operator
,
account
,
'remove'
):
raise
CannotOperateSelfError
(
"Cannot operate self."
)
...
...
@@ -293,6 +301,12 @@ class TenantService:
raise
MemberNotInTenantError
(
"Member not in tenant."
)
db
.
session
.
delete
(
ta
)
account
.
initialized_at
=
None
account
.
status
=
AccountStatus
.
PENDING
.
value
account
.
password
=
None
account
.
password_salt
=
None
db
.
session
.
commit
()
@
staticmethod
...
...
@@ -332,8 +346,8 @@ class TenantService:
class
RegisterService
:
@
static
method
def
register
(
email
,
name
,
password
:
str
=
None
,
open_id
:
str
=
None
,
provider
:
str
=
None
)
->
Account
:
@
class
method
def
register
(
cls
,
email
,
name
,
password
:
str
=
None
,
open_id
:
str
=
None
,
provider
:
str
=
None
)
->
Account
:
db
.
session
.
begin_nested
()
"""Register account"""
try
:
...
...
@@ -359,9 +373,9 @@ class RegisterService:
return
account
@
static
method
def
invite_new_member
(
tenant
:
Tenant
,
email
:
str
,
role
:
str
=
'normal'
,
inviter
:
Account
=
None
)
->
TenantAccountJoin
:
@
class
method
def
invite_new_member
(
cls
,
tenant
:
Tenant
,
email
:
str
,
role
:
str
=
'normal'
,
inviter
:
Account
=
None
)
->
str
:
"""Invite new member"""
account
=
Account
.
query
.
filter_by
(
email
=
email
)
.
first
()
...
...
@@ -380,5 +394,71 @@ class RegisterService:
if
ta
:
raise
AccountAlreadyInTenantError
(
"Account already in tenant."
)
ta
=
TenantService
.
create_tenant_member
(
tenant
,
account
,
role
)
return
ta
TenantService
.
create_tenant_member
(
tenant
,
account
,
role
)
token
=
cls
.
generate_invite_token
(
tenant
,
account
)
# send email
send_invite_member_mail_task
.
delay
(
to
=
email
,
token
=
cls
.
generate_invite_token
(
tenant
,
account
),
inviter_name
=
inviter
.
name
if
inviter
else
'Dify'
,
workspace_id
=
tenant
.
id
,
workspace_name
=
tenant
.
name
,
)
return
token
@
classmethod
def
generate_invite_token
(
cls
,
tenant
:
Tenant
,
account
:
Account
)
->
str
:
token
=
str
(
uuid
.
uuid4
())
email_hash
=
sha256
(
account
.
email
.
encode
())
.
hexdigest
()
cache_key
=
'member_invite_token:{}, {}:{}'
.
format
(
str
(
tenant
.
id
),
email_hash
,
token
)
redis_client
.
setex
(
cache_key
,
3600
,
str
(
account
.
id
))
return
token
@
classmethod
def
revoke_token
(
cls
,
workspace_id
:
str
,
email
:
str
,
token
:
str
):
email_hash
=
sha256
(
email
.
encode
())
.
hexdigest
()
cache_key
=
'member_invite_token:{}, {}:{}'
.
format
(
workspace_id
,
email_hash
,
token
)
redis_client
.
delete
(
cache_key
)
@
classmethod
def
get_account_if_token_valid
(
cls
,
workspace_id
:
str
,
email
:
str
,
token
:
str
)
->
Optional
[
Account
]:
tenant
=
db
.
session
.
query
(
Tenant
)
.
filter
(
Tenant
.
id
==
workspace_id
,
Tenant
.
status
==
'normal'
)
.
first
()
if
not
tenant
:
return
None
tenant_account
=
db
.
session
.
query
(
Account
,
TenantAccountJoin
.
role
)
.
join
(
TenantAccountJoin
,
Account
.
id
==
TenantAccountJoin
.
account_id
)
.
filter
(
Account
.
email
==
email
,
TenantAccountJoin
.
tenant_id
==
tenant
.
id
)
.
first
()
if
not
tenant_account
:
return
None
account_id
=
cls
.
_get_account_id_by_invite_token
(
workspace_id
,
email
,
token
)
if
not
account_id
:
return
None
account
=
tenant_account
[
0
]
if
not
account
:
return
None
if
account_id
!=
str
(
account
.
id
):
return
None
return
account
@
classmethod
def
_get_account_id_by_invite_token
(
cls
,
workspace_id
:
str
,
email
:
str
,
token
:
str
)
->
Optional
[
str
]:
email_hash
=
sha256
(
email
.
encode
())
.
hexdigest
()
cache_key
=
'member_invite_token:{}, {}:{}'
.
format
(
workspace_id
,
email_hash
,
token
)
account_id
=
redis_client
.
get
(
cache_key
)
if
not
account_id
:
return
None
return
account_id
.
decode
(
'utf-8'
)
api/tasks/mail_invite_member_task.py
0 → 100644
View file @
6040da75
import
logging
import
time
import
click
from
celery
import
shared_task
from
flask
import
current_app
from
extensions.ext_mail
import
mail
@
shared_task
def
send_invite_member_mail_task
(
to
:
str
,
token
:
str
,
inviter_name
:
str
,
workspace_id
:
str
,
workspace_name
:
str
):
"""
Async Send invite member mail
:param to
:param token
:param inviter_name
:param workspace_id
:param workspace_name
Usage: send_invite_member_mail_task.delay(to, token, inviter_name, workspace_id, workspace_name)
"""
if
not
mail
.
is_inited
():
return
logging
.
info
(
click
.
style
(
'Start send invite member mail to {} in workspace {}'
.
format
(
to
,
workspace_name
),
fg
=
'green'
))
start_at
=
time
.
perf_counter
()
try
:
mail
.
send
(
to
=
to
,
subject
=
"{} invited you to join {}"
.
format
(
inviter_name
,
workspace_name
),
html
=
"""<p>Hi there,</p>
<p>{inviter_name} invited you to join {workspace_name}.</p>
<p>Click <a href="{url}">here</a> to join.</p>
<p>Thanks,</p>
<p>Dify Team</p>"""
.
format
(
inviter_name
=
inviter_name
,
workspace_name
=
workspace_name
,
url
=
'{}/activate?workspace_id={}&email={}&token={}'
.
format
(
current_app
.
config
.
get
(
"CONSOLE_WEB_URL"
),
workspace_id
,
to
,
token
)
)
)
end_at
=
time
.
perf_counter
()
logging
.
info
(
click
.
style
(
'Send invite member mail to {} succeeded: latency: {}'
.
format
(
to
,
end_at
-
start_at
),
fg
=
'green'
))
except
Exception
:
logging
.
exception
(
"Send invite member mail to {} failed"
.
format
(
to
))
docker/docker-compose.yaml
View file @
6040da75
...
...
@@ -2,7 +2,7 @@ version: '3.1'
services
:
# API service
api
:
image
:
langgenius/dify-api:0.3.
7
image
:
langgenius/dify-api:0.3.
8
restart
:
always
environment
:
# Startup mode, 'api' starts the API server.
...
...
@@ -11,18 +11,26 @@ services:
LOG_LEVEL
:
INFO
# A secret key that is used for securely signing the session cookie and encrypting sensitive information on the database. You can generate a strong key using `openssl rand -base64 42`.
SECRET_KEY
:
sk-9f73s3ljTXVcMT3Blb3ljTqtsKiGHXVcMT3BlbkFJLK7U
# The base URL of console application, refers to the Console base URL of WEB service if console domain is
# The base URL of console application
web frontend
, refers to the Console base URL of WEB service if console domain is
# different from api or web app domain.
# example: http://cloud.dify.ai
CONSOLE_URL
:
'
'
CONSOLE_WEB_URL
:
'
'
# The base URL of console application api server, refers to the Console base URL of WEB service if console domain is
# different from api or web app domain.
# example: http://cloud.dify.ai
CONSOLE_API_URL
:
'
'
# The URL for Service API endpoints,refers to the base URL of the current API service if api domain is
# different from console domain.
# example: http://api.dify.ai
API_URL
:
'
'
# The URL for Web APP, refers to the Web App base URL of WEB service if web app domain is different from
SERVICE_API_URL
:
'
'
# The URL for Web APP api server, refers to the Web App base URL of WEB service if web app domain is different from
# console or api domain.
# example: http://udify.app
APP_API_URL
:
'
'
# The URL for Web APP frontend, refers to the Web App base URL of WEB service if web app domain is different from
# console or api domain.
# example: http://udify.app
APP_URL
:
'
'
APP_
WEB_
URL
:
'
'
# When enabled, migrations will be executed prior to application startup and the application will start after the migrations have completed.
MIGRATION_ENABLED
:
'
true'
# The configurations of postgres database connection.
...
...
@@ -93,6 +101,12 @@ services:
QDRANT_URL
:
'
https://your-qdrant-cluster-url.qdrant.tech/'
# The Qdrant API key.
QDRANT_API_KEY
:
'
ak-difyai'
# Mail configuration, support: resend
MAIL_TYPE
:
'
'
# default send from email address, if not specified
MAIL_DEFAULT_SEND_FROM
:
'
YOUR
EMAIL
FROM
(eg:
no-reply
<no-reply@dify.ai>)'
# the api-key for resend (https://resend.com)
RESEND_API_KEY
:
'
'
# The DSN for Sentry error reporting. If not set, Sentry error reporting will be disabled.
SENTRY_DSN
:
'
'
# The sample rate for Sentry events. Default: `1.0`
...
...
@@ -110,7 +124,7 @@ services:
# worker service
# The Celery worker for processing the queue.
worker
:
image
:
langgenius/dify-api:0.3.
7
image
:
langgenius/dify-api:0.3.
8
restart
:
always
environment
:
# Startup mode, 'worker' starts the Celery worker for processing the queue.
...
...
@@ -146,6 +160,12 @@ services:
VECTOR_STORE
:
weaviate
WEAVIATE_ENDPOINT
:
http://weaviate:8080
WEAVIATE_API_KEY
:
WVF5YThaHlkYwhGUSmCRgsX3tD5ngdN8pkih
# Mail configuration, support: resend
MAIL_TYPE
:
'
'
# default send from email address, if not specified
MAIL_DEFAULT_SEND_FROM
:
'
YOUR
EMAIL
FROM
(eg:
no-reply
<no-reply@dify.ai>)'
# the api-key for resend (https://resend.com)
RESEND_API_KEY
:
'
'
depends_on
:
-
db
-
redis
...
...
@@ -156,18 +176,18 @@ services:
# Frontend web application.
web
:
image
:
langgenius/dify-web:0.3.
7
image
:
langgenius/dify-web:0.3.
8
restart
:
always
environment
:
EDITION
:
SELF_HOSTED
# The base URL of console application, refers to the Console base URL of WEB service if console domain is
# The base URL of console application
api server
, refers to the Console base URL of WEB service if console domain is
# different from api or web app domain.
# example: http://cloud.dify.ai
CONSOLE_URL
:
'
'
# The URL for Web APP, refers to the Web App base URL of WEB service if web app domain is different from
CONSOLE_
API_
URL
:
'
'
# The URL for Web APP
api server
, refers to the Web App base URL of WEB service if web app domain is different from
# console or api domain.
# example: http://udify.app
APP_URL
:
'
'
APP_
API_
URL
:
'
'
# The DSN for Sentry error reporting. If not set, Sentry error reporting will be disabled.
SENTRY_DSN
:
'
'
...
...
web/Dockerfile
View file @
6040da75
...
...
@@ -4,8 +4,8 @@ LABEL maintainer="takatost@gmail.com"
ENV
EDITION SELF_HOSTED
ENV
DEPLOY_ENV PRODUCTION
ENV
CONSOLE_URL http://127.0.0.1:5001
ENV
APP_URL http://127.0.0.1:5001
ENV
CONSOLE_
API_
URL http://127.0.0.1:5001
ENV
APP_
API_
URL http://127.0.0.1:5001
EXPOSE
3000
...
...
web/app/activate/activateForm.tsx
0 → 100644
View file @
6040da75
'use client'
import
{
useState
}
from
'react'
import
{
useTranslation
}
from
'react-i18next'
import
useSWR
from
'swr'
import
{
useSearchParams
}
from
'next/navigation'
import
cn
from
'classnames'
import
Link
from
'next/link'
import
{
CheckCircleIcon
}
from
'@heroicons/react/24/solid'
import
style
from
'./style.module.css'
import
Button
from
'@/app/components/base/button'
import
{
SimpleSelect
}
from
'@/app/components/base/select'
import
{
timezones
}
from
'@/utils/timezone'
import
{
languageMaps
,
languages
}
from
'@/utils/language'
import
{
activateMember
,
invitationCheck
}
from
'@/service/common'
import
Toast
from
'@/app/components/base/toast'
import
Loading
from
'@/app/components/base/loading'
const
validPassword
=
/^
(?=
.*
[
a-zA-Z
])(?=
.*
\d)
.
{8,}
$/
const
ActivateForm
=
()
=>
{
const
{
t
}
=
useTranslation
()
const
searchParams
=
useSearchParams
()
const
workspaceID
=
searchParams
.
get
(
'workspace_id'
)
const
email
=
searchParams
.
get
(
'email'
)
const
token
=
searchParams
.
get
(
'token'
)
const
checkParams
=
{
url
:
'/activate/check'
,
params
:
{
workspace_id
:
workspaceID
,
email
,
token
,
},
}
const
{
data
:
checkRes
,
mutate
:
recheck
}
=
useSWR
(
checkParams
,
invitationCheck
,
{
revalidateOnFocus
:
false
,
})
const
[
name
,
setName
]
=
useState
(
''
)
const
[
password
,
setPassword
]
=
useState
(
''
)
const
[
timezone
,
setTimezone
]
=
useState
(
'Asia/Shanghai'
)
const
[
language
,
setLanguage
]
=
useState
(
'en-US'
)
const
[
showSuccess
,
setShowSuccess
]
=
useState
(
false
)
const
showErrorMessage
=
(
message
:
string
)
=>
{
Toast
.
notify
({
type
:
'error'
,
message
,
})
}
const
valid
=
()
=>
{
if
(
!
name
.
trim
())
{
showErrorMessage
(
t
(
'login.error.nameEmpty'
))
return
false
}
if
(
!
password
.
trim
())
{
showErrorMessage
(
t
(
'login.error.passwordEmpty'
))
return
false
}
if
(
!
validPassword
.
test
(
password
))
showErrorMessage
(
t
(
'login.error.passwordInvalid'
))
return
true
}
const
handleActivate
=
async
()
=>
{
if
(
!
valid
())
return
try
{
await
activateMember
({
url
:
'/activate'
,
body
:
{
workspace_id
:
workspaceID
,
email
,
token
,
name
,
password
,
interface_language
:
language
,
timezone
,
},
})
setShowSuccess
(
true
)
}
catch
{
recheck
()
}
}
return
(
<
div
className=
{
cn
(
'flex flex-col items-center w-full grow items-center justify-center'
,
'px-6'
,
'md:px-[108px]'
,
)
}
>
{
!
checkRes
&&
<
Loading
/>
}
{
checkRes
&&
!
checkRes
.
is_valid
&&
(
<
div
className=
"flex flex-col md:w-[400px]"
>
<
div
className=
"w-full mx-auto"
>
<
div
className=
"mb-3 flex justify-center items-center w-20 h-20 p-5 rounded-[20px] border border-gray-100 shadow-lg text-[40px] font-bold"
>
🤷♂️
</
div
>
<
h2
className=
"text-[32px] font-bold text-gray-900"
>
{
t
(
'login.invalid'
)
}
</
h2
>
</
div
>
<
div
className=
"w-full mx-auto mt-6"
>
<
Button
type=
'primary'
className=
'w-full !fone-medium !text-sm'
>
<
a
href=
"https://dify.ai"
>
{
t
(
'login.explore'
)
}
</
a
>
</
Button
>
</
div
>
</
div
>
)
}
{
checkRes
&&
checkRes
.
is_valid
&&
!
showSuccess
&&
(
<
div
className=
'flex flex-col md:w-[400px]'
>
<
div
className=
"w-full mx-auto"
>
<
div
className=
{
`mb-3 flex justify-center items-center w-20 h-20 p-5 rounded-[20px] border border-gray-100 shadow-lg text-[40px] font-bold ${style.logo}`
}
>
</
div
>
<
h2
className=
"text-[32px] font-bold text-gray-900"
>
{
`${t('login.join')} ${checkRes.workspace_name}`
}
</
h2
>
<
p
className=
'mt-1 text-sm text-gray-600 '
>
{
`${t('login.joinTipStart')} ${checkRes.workspace_name} ${t('login.joinTipEnd')}`
}
</
p
>
</
div
>
<
div
className=
"w-full mx-auto mt-6"
>
<
div
className=
"bg-white"
>
{
/* username */
}
<
div
className=
'mb-5'
>
<
label
htmlFor=
"name"
className=
"my-2 flex items-center justify-between text-sm font-medium text-gray-900"
>
{
t
(
'login.name'
)
}
</
label
>
<
div
className=
"mt-1 relative rounded-md shadow-sm"
>
<
input
id=
"name"
type=
"text"
value=
{
name
}
onChange=
{
e
=>
setName
(
e
.
target
.
value
)
}
placeholder=
{
t
(
'login.namePlaceholder'
)
||
''
}
className=
{
'appearance-none block w-full rounded-lg pl-[14px] px-3 py-2 border border-gray-200 hover:border-gray-300 hover:shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 placeholder-gray-400 caret-primary-600 sm:text-sm pr-10'
}
/>
</
div
>
</
div
>
{
/* password */
}
<
div
className=
'mb-5'
>
<
label
htmlFor=
"password"
className=
"my-2 flex items-center justify-between text-sm font-medium text-gray-900"
>
{
t
(
'login.password'
)
}
</
label
>
<
div
className=
"mt-1 relative rounded-md shadow-sm"
>
<
input
id=
"password"
type=
'password'
value=
{
password
}
onChange=
{
e
=>
setPassword
(
e
.
target
.
value
)
}
placeholder=
{
t
(
'login.passwordPlaceholder'
)
||
''
}
className=
{
'appearance-none block w-full rounded-lg pl-[14px] px-3 py-2 border border-gray-200 hover:border-gray-300 hover:shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 placeholder-gray-400 caret-primary-600 sm:text-sm pr-10'
}
/>
</
div
>
<
div
className=
'mt-1 text-xs text-gray-500'
>
{
t
(
'login.error.passwordInvalid'
)
}
</
div
>
</
div
>
{
/* language */
}
<
div
className=
'mb-5'
>
<
label
htmlFor=
"name"
className=
"my-2 flex items-center justify-between text-sm font-medium text-gray-900"
>
{
t
(
'login.interfaceLanguage'
)
}
</
label
>
<
div
className=
"relative mt-1 rounded-md shadow-sm"
>
<
SimpleSelect
defaultValue=
{
languageMaps
.
en
}
items=
{
languages
}
onSelect=
{
(
item
)
=>
{
setLanguage
(
item
.
value
as
string
)
}
}
/>
</
div
>
</
div
>
{
/* timezone */
}
<
div
className=
'mb-4'
>
<
label
htmlFor=
"timezone"
className=
"block text-sm font-medium text-gray-700"
>
{
t
(
'login.timezone'
)
}
</
label
>
<
div
className=
"relative mt-1 rounded-md shadow-sm"
>
<
SimpleSelect
defaultValue=
{
timezone
}
items=
{
timezones
}
onSelect=
{
(
item
)
=>
{
setTimezone
(
item
.
value
as
string
)
}
}
/>
</
div
>
</
div
>
<
div
>
<
Button
type=
'primary'
className=
'w-full !fone-medium !text-sm'
onClick=
{
handleActivate
}
>
{
`${t('login.join')} ${checkRes.workspace_name}`
}
</
Button
>
</
div
>
<
div
className=
"block w-hull mt-2 text-xs text-gray-600"
>
{
t
(
'login.license.tip'
)
}
<
Link
className=
'text-primary-600'
target=
{
'_blank'
}
href=
'https://docs.dify.ai/community/open-source'
>
{
t
(
'login.license.link'
)
}
</
Link
>
</
div
>
</
div
>
</
div
>
</
div
>
)
}
{
checkRes
&&
checkRes
.
is_valid
&&
showSuccess
&&
(
<
div
className=
"flex flex-col md:w-[400px]"
>
<
div
className=
"w-full mx-auto"
>
<
div
className=
"mb-3 flex justify-center items-center w-20 h-20 p-5 rounded-[20px] border border-gray-100 shadow-lg text-[40px] font-bold"
>
<
CheckCircleIcon
className=
'w-10 h-10 text-[#039855]'
/>
</
div
>
<
h2
className=
"text-[32px] font-bold text-gray-900"
>
{
`${t('login.activatedTipStart')} ${checkRes.workspace_name} ${t('login.activatedTipEnd')}`
}
</
h2
>
</
div
>
<
div
className=
"w-full mx-auto mt-6"
>
<
Button
type=
'primary'
className=
'w-full !fone-medium !text-sm'
>
<
a
href=
"/signin"
>
{
t
(
'login.activated'
)
}
</
a
>
</
Button
>
</
div
>
</
div
>
)
}
</
div
>
)
}
export
default
ActivateForm
web/app/activate/page.tsx
0 → 100644
View file @
6040da75
import
React
from
'react'
import
cn
from
'classnames'
import
Header
from
'../signin/_header'
import
style
from
'../signin/page.module.css'
import
ActivateForm
from
'./activateForm'
const
Activate
=
()
=>
{
return
(
<
div
className=
{
cn
(
style
.
background
,
'flex w-full min-h-screen'
,
'sm:p-4 lg:p-8'
,
'gap-x-20'
,
'justify-center lg:justify-start'
,
)
}
>
<
div
className=
{
cn
(
'flex w-full flex-col bg-white shadow rounded-2xl shrink-0'
,
'space-between'
,
)
}
>
<
Header
/>
<
ActivateForm
/>
<
div
className=
'px-8 py-6 text-sm font-normal text-gray-500'
>
©
{
new
Date
().
getFullYear
()
}
Dify, Inc. All rights reserved.
</
div
>
</
div
>
</
div
>
)
}
export
default
Activate
web/app/activate/style.module.css
0 → 100644
View file @
6040da75
.logo
{
background
:
#fff
center
no-repeat
url(./team-28x28.png)
;
background-size
:
56px
;
}
\ No newline at end of file
web/app/activate/team-28x28.png
0 → 100644
View file @
6040da75
7.18 KB
web/app/components/app/text-generate/item/index.tsx
View file @
6040da75
...
...
@@ -128,6 +128,9 @@ const GenerationItem: FC<IGenerationItemProps> = ({
startQuerying
()
const
res
:
any
=
await
fetchMoreLikeThis
(
messageId
as
string
,
isInstalledApp
,
installedAppId
)
setCompletionRes
(
res
.
answer
)
setChildFeedback
({
rating
:
null
,
})
setChildMessageId
(
res
.
id
)
stopQuerying
()
}
...
...
@@ -152,6 +155,12 @@ const GenerationItem: FC<IGenerationItemProps> = ({
}
},
[
controlClearMoreLikeThis
])
// regeneration clear child
useEffect
(()
=>
{
if
(
isLoading
)
setChildMessageId
(
null
)
},
[
isLoading
])
return
(
<
div
className=
{
cn
(
className
,
isTop
?
'rounded-xl border border-gray-200 bg-white'
:
'rounded-br-xl !mt-0'
)
}
style=
{
isTop
...
...
@@ -175,7 +184,11 @@ const GenerationItem: FC<IGenerationItemProps> = ({
{
taskId
}
</
div
>)
}
<
Markdown
content=
{
content
}
/>
<
div
className=
'flex'
>
<
div
className=
'grow w-0'
>
<
Markdown
content=
{
content
}
/>
</
div
>
</
div
>
{
messageId
&&
(
<
div
className=
'flex items-center justify-between mt-3'
>
<
div
className=
'flex items-center'
>
...
...
web/app/components/base/select/locale.tsx
View file @
6040da75
...
...
@@ -11,7 +11,7 @@ export const RFC_LOCALES = [
{
value
:
'en-US'
,
name
:
'EN'
},
{
value
:
'zh-Hans'
,
name
:
'简体中文'
},
]
interface
ISelectProps
{
type
ISelectProps
=
{
items
:
Array
<
{
value
:
string
;
name
:
string
}
>
value
?:
string
className
?:
string
...
...
@@ -21,7 +21,7 @@ interface ISelectProps {
export
default
function
Select
({
items
,
value
,
onChange
onChange
,
}:
ISelectProps
)
{
const
item
=
items
.
filter
(
item
=>
item
.
value
===
value
)[
0
]
...
...
@@ -29,11 +29,12 @@ export default function Select({
<
div
className=
"w-56 text-right"
>
<
Menu
as=
"div"
className=
"relative inline-block text-left"
>
<
div
>
<
Menu
.
Button
className=
"inline-flex w-full justify-center items-center
rounded-lg px-2 py-1
text-gray-600 text-xs font-medium
border border-gray-200"
>
<
GlobeAltIcon
className=
"w-5 h-5 mr-2 "
aria
-
hidden=
"true"
/>
<
Menu
.
Button
className=
"inline-flex w-full h-[44px]justify-center items-center
rounded-lg px-[10px] py-[6px]
text-gray-900 text-[13px] font-medium
border border-gray-200
hover:bg-gray-100"
>
<
GlobeAltIcon
className=
"w-5 h-5 mr-1"
aria
-
hidden=
"true"
/>
{
item
?.
name
}
</
Menu
.
Button
>
</
div
>
...
...
@@ -46,14 +47,14 @@ export default function Select({
leaveFrom=
"transform opacity-100 scale-100"
leaveTo=
"transform opacity-0 scale-95"
>
<
Menu
.
Items
className=
"absolute right-0 mt-2 w-
28
origin-top-right divide-y divide-gray-100 rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
>
<
Menu
.
Items
className=
"absolute right-0 mt-2 w-
[120px]
origin-top-right divide-y divide-gray-100 rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
>
<
div
className=
"px-1 py-1 "
>
{
items
.
map
((
item
)
=>
{
return
<
Menu
.
Item
key=
{
item
.
value
}
>
{
({
active
})
=>
(
<
button
className=
{
`${active ? 'bg-gray-100' : ''
} group flex w-full items-center rounded-md px-2 py-2 text-sm
`
}
} group flex w-full items-center rounded-lg px-3 py-2 text-sm text-gray-700
`
}
onClick=
{
(
evt
)
=>
{
evt
.
preventDefault
()
onChange
&&
onChange
(
item
.
value
)
...
...
@@ -77,7 +78,7 @@ export default function Select({
export
function
InputSelect
({
items
,
value
,
onChange
onChange
,
}:
ISelectProps
)
{
const
item
=
items
.
filter
(
item
=>
item
.
value
===
value
)[
0
]
return
(
...
...
@@ -104,7 +105,7 @@ export function InputSelect({
{
({
active
})
=>
(
<
button
className=
{
`${active ? 'bg-gray-100' : ''
} group flex w-full items-center rounded-md px-2 py-2 text-sm`
}
} group flex w-full items-center rounded-md px-2 py-2 text-sm`
}
onClick=
{
()
=>
{
onChange
&&
onChange
(
item
.
value
)
}
}
...
...
@@ -122,4 +123,4 @@ export function InputSelect({
</
Menu
>
</
div
>
)
}
\ No newline at end of file
}
web/app/components/develop/template/template_chat.en.mdx
View file @
6040da75
...
...
@@ -334,6 +334,53 @@ For versatile conversational apps using a Q&A format, call the chat-messages API
</Row>
---
<Heading
url='/conversations/{converation_id}'
method='DELETE'
title='Conversation deletion'
name='#delete'
/>
<Row>
<Col>
Delete conversation.
### Request Body
<Properties>
<Property name='user' type='string' key='user'>
The user identifier, defined by the developer, must ensure uniqueness within the app.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="DELETE" label="/conversations/{converation_id}" targetCode={`curl --location --request DELETE '${props.appDetail.api_base_url}/conversations/{conversation_id}' \\\n--header 'Authorization: Bearer ENTER-YOUR-SECRET-KEY' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{ \n "user": "abc-123"\n}'`}>
```bash {{ title: 'cURL' }}
curl --location --request DELETE 'https://cloud.langgenius.dev/api/conversations/{convsation_id}' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ENTER-YOUR-SECRET-KEY' \
--data '{
"user": "abc-123"
}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"result": "success"
}
```
</CodeGroup>
</Col>
</Row>
---
<Heading
...
...
web/app/components/develop/template/template_chat.zh.mdx
View file @
6040da75
...
...
@@ -333,6 +333,52 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
</Col>
</Row>
---
<Heading
url='/conversations/{converation_id}'
method='DELETE'
title='删除会话'
name='#delete'
/>
<Row>
<Col>
删除会话。
### Request Body
<Properties>
<Property name='user' type='string' key='user'>
用户标识,由开发者定义规则,需保证用户标识在应用内唯一。
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="DELETE" label="/conversations/{converation_id}" targetCode={`curl --location --request DELETE '${props.appDetail.api_base_url}/conversations/{conversation_id}' \\\n--header 'Authorization: Bearer ENTER-YOUR-SECRET-KEY' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{ \n "user": "abc-123"\n}'`}>
```bash {{ title: 'cURL' }}
curl --location --request DELETE 'https://cloud.langgenius.dev/api/conversations/{convsation_id}' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ENTER-YOUR-SECRET-KEY' \
--data '{
"user": "abc-123"
}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"result": "success"
}
```
</CodeGroup>
</Col>
</Row>
---
...
...
web/app/components/header/account-about/index.module.css
View file @
6040da75
.logo-icon
{
background
:
url(../assets/logo-icon.png)
center
center
no-repeat
;
background-size
:
contain
;
background-size
:
32px
;
box-shadow
:
0px
4px
6px
-1px
rgba
(
0
,
0
,
0
,
0.05
),
0px
2px
4px
-2px
rgba
(
0
,
0
,
0
,
0.05
);
}
...
...
web/app/components/header/account-about/index.tsx
View file @
6040da75
...
...
@@ -34,7 +34,7 @@ export default function AccountAbout({
<
div
>
<
div
className=
{
classNames
(
s
[
'logo-icon'
],
'mx-auto mb-3 w-12 h-12 bg-white rounded border border-gray-200'
,
'mx-auto mb-3 w-12 h-12 bg-white rounded
-xl
border border-gray-200'
,
)
}
/>
<
div
className=
{
classNames
(
s
[
'logo-text'
],
...
...
web/app/components/header/account-setting/account-page/index.tsx
View file @
6040da75
...
...
@@ -25,13 +25,19 @@ const inputClassName = `
text-sm font-normal text-gray-800
`
const
validPassword
=
/^
(?=
.*
[
a-zA-Z
])(?=
.*
\d)
.
{8,}
$/
export
default
function
AccountPage
()
{
const
{
t
}
=
useTranslation
()
const
{
mutateUserProfile
,
userProfile
,
apps
}
=
useAppContext
()
const
{
notify
}
=
useContext
(
ToastContext
)
const
[
editNameModalVisible
,
setEditNameModalVisible
]
=
useState
(
false
)
const
[
editName
,
setEditName
]
=
useState
(
''
)
const
[
editing
,
setEditing
]
=
useState
(
false
)
const
{
t
}
=
useTranslation
()
const
[
editPasswordModalVisible
,
setEditPasswordModalVisible
]
=
useState
(
false
)
const
[
currentPassword
,
setCurrentPassword
]
=
useState
(
''
)
const
[
password
,
setPassword
]
=
useState
(
''
)
const
[
confirmPassword
,
setConfirmPassword
]
=
useState
(
''
)
const
handleEditName
=
()
=>
{
setEditNameModalVisible
(
true
)
...
...
@@ -52,6 +58,56 @@ export default function AccountPage() {
setEditing
(
false
)
}
}
const
showErrorMessage
=
(
message
:
string
)
=>
{
notify
({
type
:
'error'
,
message
,
})
}
const
valid
=
()
=>
{
if
(
!
password
.
trim
())
{
showErrorMessage
(
t
(
'login.error.passwordEmpty'
))
return
false
}
if
(
!
validPassword
.
test
(
password
))
showErrorMessage
(
t
(
'login.error.passwordInvalid'
))
if
(
password
!==
confirmPassword
)
showErrorMessage
(
t
(
'common.account.notEqual'
))
return
true
}
const
resetPasswordForm
=
()
=>
{
setCurrentPassword
(
''
)
setPassword
(
''
)
setConfirmPassword
(
''
)
}
const
handleSavePassowrd
=
async
()
=>
{
if
(
!
valid
())
return
try
{
setEditing
(
true
)
await
updateUserProfile
({
url
:
'account/password'
,
body
:
{
password
:
currentPassword
,
new_password
:
password
,
repeat_new_password
:
confirmPassword
,
},
})
notify
({
type
:
'success'
,
message
:
t
(
'common.actionMsg.modifiedSuccessfully'
)
})
mutateUserProfile
()
setEditPasswordModalVisible
(
false
)
resetPasswordForm
()
setEditing
(
false
)
}
catch
(
e
)
{
notify
({
type
:
'error'
,
message
:
(
e
as
Error
).
message
})
setEditPasswordModalVisible
(
false
)
setEditing
(
false
)
}
}
const
renderAppItem
=
(
item
:
IItem
)
=>
{
return
(
<
div
className=
'flex px-3 py-1'
>
...
...
@@ -80,51 +136,105 @@ export default function AccountPage() {
<
div
className=
{
titleClassName
}
>
{
t
(
'common.account.email'
)
}
</
div
>
<
div
className=
{
classNames
(
inputClassName
,
'cursor-pointer'
)
}
>
{
userProfile
.
email
}
</
div
>
</
div
>
{
!!
apps
.
length
&&
(
<>
<
div
className=
'mb-6 border-[0.5px] border-gray-100'
/>
<
div
className=
'mb-8'
>
<
div
className=
{
titleClassName
}
>
{
t
(
'common.account.langGeniusAccount'
)
}
</
div
>
<
div
className=
{
descriptionClassName
}
>
{
t
(
'common.account.langGeniusAccountTip'
)
}
</
div
>
<
Collapse
title=
{
`${t('common.account.showAppLength', { length: apps.length })}`
}
items=
{
apps
.
map
(
app
=>
({
key
:
app
.
id
,
name
:
app
.
name
}))
}
renderItem=
{
renderAppItem
}
wrapperClassName=
'mt-2'
/>
</
div
>
</>
)
}
{
editNameModalVisible
&&
(
<
Modal
isShow
onClose=
{
()
=>
setEditNameModalVisible
(
false
)
}
className=
{
s
.
modal
}
>
<
div
className=
'mb-6 text-lg font-medium text-gray-900'
>
{
t
(
'common.account.editName'
)
}
</
div
>
<
div
className=
{
titleClassName
}
>
{
t
(
'common.account.name'
)
}
</
div
>
<
input
className=
{
inputClassName
}
value=
{
editName
}
onChange=
{
e
=>
setEditName
(
e
.
target
.
value
)
}
<
div
className=
'mb-8'
>
<
div
className=
'mb-1 text-sm font-medium text-gray-900'
>
{
t
(
'common.account.password'
)
}
</
div
>
<
div
className=
'mb-2 text-xs text-gray-500'
>
{
t
(
'common.account.passwordTip'
)
}
</
div
>
<
Button
className=
'font-medium !text-gray-700 !px-3 !py-[7px] !text-[13px]'
onClick=
{
()
=>
setEditPasswordModalVisible
(
true
)
}
>
{
userProfile
.
is_password_set
?
t
(
'common.account.resetPassword'
)
:
t
(
'common.account.setPassword'
)
}
</
Button
>
</
div
>
{
!!
apps
.
length
&&
(
<>
<
div
className=
'mb-6 border-[0.5px] border-gray-100'
/>
<
div
className=
'mb-8'
>
<
div
className=
{
titleClassName
}
>
{
t
(
'common.account.langGeniusAccount'
)
}
</
div
>
<
div
className=
{
descriptionClassName
}
>
{
t
(
'common.account.langGeniusAccountTip'
)
}
</
div
>
<
Collapse
title=
{
`${t('common.account.showAppLength', { length: apps.length })}`
}
items=
{
apps
.
map
(
app
=>
({
key
:
app
.
id
,
name
:
app
.
name
}))
}
renderItem=
{
renderAppItem
}
wrapperClassName=
'mt-2'
/>
<
div
className=
'flex justify-end mt-10'
>
<
Button
className=
'mr-2 text-sm font-medium'
onClick=
{
()
=>
setEditNameModalVisible
(
false
)
}
>
{
t
(
'common.operation.cancel'
)
}
</
Button
>
<
Button
disabled=
{
editing
||
!
editName
}
type=
'primary'
className=
'text-sm font-medium'
onClick=
{
handleSaveName
}
>
{
t
(
'common.operation.save'
)
}
</
Button
>
</
div
>
</
Modal
>
)
}
</
div
>
</>
)
}
{
editNameModalVisible
&&
(
<
Modal
isShow
onClose=
{
()
=>
setEditNameModalVisible
(
false
)
}
className=
{
s
.
modal
}
>
<
div
className=
'mb-6 text-lg font-medium text-gray-900'
>
{
t
(
'common.account.editName'
)
}
</
div
>
<
div
className=
{
titleClassName
}
>
{
t
(
'common.account.name'
)
}
</
div
>
<
input
className=
{
inputClassName
}
value=
{
editName
}
onChange=
{
e
=>
setEditName
(
e
.
target
.
value
)
}
/>
<
div
className=
'flex justify-end mt-10'
>
<
Button
className=
'mr-2 text-sm font-medium'
onClick=
{
()
=>
setEditNameModalVisible
(
false
)
}
>
{
t
(
'common.operation.cancel'
)
}
</
Button
>
<
Button
disabled=
{
editing
||
!
editName
}
type=
'primary'
className=
'text-sm font-medium'
onClick=
{
handleSaveName
}
>
{
t
(
'common.operation.save'
)
}
</
Button
>
</
div
>
</
Modal
>
)
}
{
editPasswordModalVisible
&&
(
<
Modal
isShow
onClose=
{
()
=>
{
setEditPasswordModalVisible
(
false
)
resetPasswordForm
()
}
}
className=
{
s
.
modal
}
>
<
div
className=
'mb-6 text-lg font-medium text-gray-900'
>
{
userProfile
.
is_password_set
?
t
(
'common.account.resetPassword'
)
:
t
(
'common.account.setPassword'
)
}
</
div
>
{
userProfile
.
is_password_set
&&
(
<>
<
div
className=
{
titleClassName
}
>
{
t
(
'common.account.currentPassword'
)
}
</
div
>
<
input
type=
"password"
className=
{
inputClassName
}
value=
{
currentPassword
}
onChange=
{
e
=>
setCurrentPassword
(
e
.
target
.
value
)
}
/>
</>
)
}
<
div
className=
'mt-8 text-sm font-medium text-gray-900'
>
{
userProfile
.
is_password_set
?
t
(
'common.account.newPassword'
)
:
t
(
'common.account.password'
)
}
</
div
>
<
input
type=
"password"
className=
{
inputClassName
}
value=
{
password
}
onChange=
{
e
=>
setPassword
(
e
.
target
.
value
)
}
/>
<
div
className=
'mt-8 text-sm font-medium text-gray-900'
>
{
t
(
'common.account.confirmPassword'
)
}
</
div
>
<
input
type=
"password"
className=
{
inputClassName
}
value=
{
confirmPassword
}
onChange=
{
e
=>
setConfirmPassword
(
e
.
target
.
value
)
}
/>
<
div
className=
'flex justify-end mt-10'
>
<
Button
className=
'mr-2 text-sm font-medium'
onClick=
{
()
=>
{
setEditPasswordModalVisible
(
false
)
resetPasswordForm
()
}
}
>
{
t
(
'common.operation.cancel'
)
}
</
Button
>
<
Button
disabled=
{
editing
}
type=
'primary'
className=
'text-sm font-medium'
onClick=
{
handleSavePassowrd
}
>
{
userProfile
.
is_password_set
?
t
(
'common.operation.reset'
)
:
t
(
'common.operation.save'
)
}
</
Button
>
</
div
>
</
Modal
>
)
}
</>
)
}
web/app/components/header/account-setting/index.tsx
View file @
6040da75
'use client'
import
{
useTranslation
}
from
'react-i18next'
import
{
useState
}
from
'react'
import
{
useEffect
,
useRef
,
useState
}
from
'react'
import
cn
from
'classnames'
import
{
AtSymbolIcon
,
CubeTransparentIcon
,
GlobeAltIcon
,
UserIcon
,
UsersIcon
,
XMarkIcon
}
from
'@heroicons/react/24/outline'
import
{
GlobeAltIcon
as
GlobalAltIconSolid
,
UserIcon
as
UserIconSolid
,
UsersIcon
as
UsersIconSolid
}
from
'@heroicons/react/24/solid'
import
AccountPage
from
'./account-page'
...
...
@@ -18,6 +19,10 @@ const iconClassName = `
w-4 h-4 ml-3 mr-2
`
const
scrolledClassName
=
`
border-b shadow-xs bg-white/[.98]
`
type
IAccountSettingProps
=
{
onCancel
:
()
=>
void
activeTab
?:
string
...
...
@@ -78,6 +83,22 @@ export default function AccountSetting({
],
},
]
const
scrollRef
=
useRef
<
HTMLDivElement
>
(
null
)
const
[
scrolled
,
setScrolled
]
=
useState
(
false
)
const
scrollHandle
=
(
e
:
any
)
=>
{
if
(
e
.
target
.
scrollTop
>
0
)
setScrolled
(
true
)
else
setScrolled
(
false
)
}
useEffect
(()
=>
{
const
targetElement
=
scrollRef
.
current
targetElement
?.
addEventListener
(
'scroll'
,
scrollHandle
)
return
()
=>
{
targetElement
?.
removeEventListener
(
'scroll'
,
scrollHandle
)
}
},
[])
return
(
<
Modal
...
...
@@ -115,29 +136,19 @@ export default function AccountSetting({
}
</
div
>
</
div
>
<
div
className=
'w-[520px] h-[580px] px-6 py
-4 overflow-y-auto'
>
<
div
className=
'flex items-center justify-between h-6 mb-8 text-base font-medium text-gray-900 '
>
<
div
ref=
{
scrollRef
}
className=
'relative w-[520px] h-[580px] pb
-4 overflow-y-auto'
>
<
div
className=
{
cn
(
'sticky top-0 px-6 py-4 flex items-center justify-between h-14 mb-4 bg-white text-base font-medium text-gray-900'
,
scrolled
&&
scrolledClassName
)
}
>
{
[...
menuItems
[
0
].
items
,
...
menuItems
[
1
].
items
].
find
(
item
=>
item
.
key
===
activeMenu
)?.
name
}
<
XMarkIcon
className=
'w-4 h-4 cursor-pointer'
onClick=
{
onCancel
}
/>
</
div
>
{
activeMenu
===
'account'
&&
<
AccountPage
/>
}
{
activeMenu
===
'members'
&&
<
MembersPage
/>
}
{
activeMenu
===
'integrations'
&&
<
IntegrationsPage
/>
}
{
activeMenu
===
'language'
&&
<
LanguagePage
/>
}
{
activeMenu
===
'provider'
&&
<
ProviderPage
/>
}
{
activeMenu
===
'data-source'
&&
<
DataSourcePage
/>
}
<
div
className=
'px-6'
>
{
activeMenu
===
'account'
&&
<
AccountPage
/>
}
{
activeMenu
===
'members'
&&
<
MembersPage
/>
}
{
activeMenu
===
'integrations'
&&
<
IntegrationsPage
/>
}
{
activeMenu
===
'language'
&&
<
LanguagePage
/>
}
{
activeMenu
===
'provider'
&&
<
ProviderPage
/>
}
{
activeMenu
===
'data-source'
&&
<
DataSourcePage
/>
}
</
div
>
</
div
>
</
div
>
</
Modal
>
...
...
web/app/components/header/account-setting/members-page/index.tsx
View file @
6040da75
...
...
@@ -30,6 +30,7 @@ const MembersPage = () => {
const
{
userProfile
}
=
useAppContext
()
const
{
data
,
mutate
}
=
useSWR
({
url
:
'/workspaces/current/members'
},
fetchMembers
)
const
[
inviteModalVisible
,
setInviteModalVisible
]
=
useState
(
false
)
const
[
invitationLink
,
setInvitationLink
]
=
useState
(
''
)
const
[
invitedModalVisible
,
setInvitedModalVisible
]
=
useState
(
false
)
const
accounts
=
data
?.
accounts
||
[]
const
owner
=
accounts
.
filter
(
account
=>
account
.
role
===
'owner'
)?.[
0
]?.
email
===
userProfile
.
email
...
...
@@ -93,8 +94,9 @@ const MembersPage = () => {
inviteModalVisible
&&
(
<
InviteModal
onCancel=
{
()
=>
setInviteModalVisible
(
false
)
}
onSend=
{
()
=>
{
onSend=
{
(
url
)
=>
{
setInvitedModalVisible
(
true
)
setInvitationLink
(
url
)
mutate
()
}
}
/>
...
...
@@ -103,6 +105,7 @@ const MembersPage = () => {
{
invitedModalVisible
&&
(
<
InvitedModal
invitationLink=
{
invitationLink
}
onCancel=
{
()
=>
setInvitedModalVisible
(
false
)
}
/>
)
...
...
web/app/components/header/account-setting/members-page/invite-modal/index.tsx
View file @
6040da75
...
...
@@ -3,16 +3,16 @@ import { useState } from 'react'
import
{
useContext
}
from
'use-context-selector'
import
{
XMarkIcon
}
from
'@heroicons/react/24/outline'
import
{
useTranslation
}
from
'react-i18next'
import
s
from
'./index.module.css'
import
Modal
from
'@/app/components/base/modal'
import
Button
from
'@/app/components/base/button'
import
s
from
'./index.module.css'
import
{
inviteMember
}
from
'@/service/common'
import
{
emailRegex
}
from
'@/config'
import
{
ToastContext
}
from
'@/app/components/base/toast'
interface
IInviteModalProps
{
onCancel
:
()
=>
void
,
onSend
:
(
)
=>
void
,
type
IInviteModalProps
=
{
onCancel
:
()
=>
void
onSend
:
(
url
:
string
)
=>
void
}
const
InviteModal
=
({
onCancel
,
...
...
@@ -25,16 +25,16 @@ const InviteModal = ({
const
handleSend
=
async
()
=>
{
if
(
emailRegex
.
test
(
email
))
{
try
{
const
res
=
await
inviteMember
({
url
:
'/workspaces/current/members/invite-email'
,
body
:
{
email
,
role
:
'admin'
}
})
const
res
=
await
inviteMember
({
url
:
'/workspaces/current/members/invite-email'
,
body
:
{
email
,
role
:
'admin'
}
})
if
(
res
.
result
===
'success'
)
{
onCancel
()
onSend
()
onSend
(
res
.
invite_url
)
}
}
catch
(
e
)
{
}
}
else
{
catch
(
e
)
{}
}
else
{
notify
({
type
:
'error'
,
message
:
t
(
'common.members.emailInvalid'
)
})
}
}
...
...
@@ -51,15 +51,15 @@ const InviteModal = ({
<
div
className=
'mb-2 text-sm font-medium text-gray-900'
>
{
t
(
'common.members.email'
)
}
</
div
>
<
input
className=
'
block w-full py-2 mb-9 px-3 bg-gray-50 outline-none border-none
block w-full py-2 mb-9 px-3 bg-gray-50 outline-none border-none
appearance-none text-sm text-gray-900 rounded-lg
'
value=
{
email
}
onChange=
{
e
=>
setEmail
(
e
.
target
.
value
)
}
placeholder=
{
t
(
'common.members.emailPlaceholder'
)
||
''
}
/>
<
Button
className=
'w-full text-sm font-medium'
<
Button
className=
'w-full text-sm font-medium'
onClick=
{
handleSend
}
type=
'primary'
>
...
...
web/app/components/header/account-setting/members-page/invited-modal/assets/copied.svg
0 → 100644
View file @
6040da75
<svg
width=
"16"
height=
"16"
viewBox=
"0 0 16 16"
fill=
"none"
xmlns=
"http://www.w3.org/2000/svg"
>
<path
d=
"M10.6665 2.66683C11.2865 2.66683 11.5965 2.66683 11.8508 2.73498C12.541 2.91991 13.0801 3.45901 13.265 4.14919C13.3332 4.40352 13.3332 4.71352 13.3332 5.3335V11.4668C13.3332 12.5869 13.3332 13.147 13.1152 13.5748C12.9234 13.9511 12.6175 14.2571 12.2412 14.4488C11.8133 14.6668 11.2533 14.6668 10.1332 14.6668H5.8665C4.7464 14.6668 4.18635 14.6668 3.75852 14.4488C3.3822 14.2571 3.07624 13.9511 2.88449 13.5748C2.6665 13.147 2.6665 12.5869 2.6665 11.4668V5.3335C2.6665 4.71352 2.6665 4.40352 2.73465 4.14919C2.91959 3.45901 3.45868 2.91991 4.14887 2.73498C4.4032 2.66683 4.71319 2.66683 5.33317 2.66683M5.99984 10.0002L7.33317 11.3335L10.3332 8.3335M6.39984 4.00016H9.59984C9.9732 4.00016 10.1599 4.00016 10.3025 3.9275C10.4279 3.86359 10.5299 3.7616 10.5938 3.63616C10.6665 3.49355 10.6665 3.30686 10.6665 2.9335V2.40016C10.6665 2.02679 10.6665 1.84011 10.5938 1.6975C10.5299 1.57206 10.4279 1.47007 10.3025 1.40616C10.1599 1.3335 9.97321 1.3335 9.59984 1.3335H6.39984C6.02647 1.3335 5.83978 1.3335 5.69718 1.40616C5.57174 1.47007 5.46975 1.57206 5.40583 1.6975C5.33317 1.84011 5.33317 2.02679 5.33317 2.40016V2.9335C5.33317 3.30686 5.33317 3.49355 5.40583 3.63616C5.46975 3.7616 5.57174 3.86359 5.69718 3.9275C5.83978 4.00016 6.02647 4.00016 6.39984 4.00016Z"
stroke=
"#1D2939"
stroke-width=
"1.5"
stroke-linecap=
"round"
stroke-linejoin=
"round"
/>
</svg>
web/app/components/header/account-setting/members-page/invited-modal/assets/copy-hover.svg
0 → 100644
View file @
6040da75
<svg
width=
"16"
height=
"16"
viewBox=
"0 0 16 16"
fill=
"none"
xmlns=
"http://www.w3.org/2000/svg"
>
<path
d=
"M10.6665 2.66634H11.9998C12.3535 2.66634 12.6926 2.80682 12.9426 3.05687C13.1927 3.30691 13.3332 3.64605 13.3332 3.99967V13.333C13.3332 13.6866 13.1927 14.0258 12.9426 14.2758C12.6926 14.5259 12.3535 14.6663 11.9998 14.6663H3.99984C3.64622 14.6663 3.30708 14.5259 3.05703 14.2758C2.80698 14.0258 2.6665 13.6866 2.6665 13.333V3.99967C2.6665 3.64605 2.80698 3.30691 3.05703 3.05687C3.30708 2.80682 3.64622 2.66634 3.99984 2.66634H5.33317M5.99984 1.33301H9.99984C10.368 1.33301 10.6665 1.63148 10.6665 1.99967V3.33301C10.6665 3.7012 10.368 3.99967 9.99984 3.99967H5.99984C5.63165 3.99967 5.33317 3.7012 5.33317 3.33301V1.99967C5.33317 1.63148 5.63165 1.33301 5.99984 1.33301Z"
stroke=
"#1D2939"
stroke-width=
"1.5"
stroke-linecap=
"round"
stroke-linejoin=
"round"
/>
</svg>
web/app/components/header/account-setting/members-page/invited-modal/assets/copy.svg
0 → 100644
View file @
6040da75
<svg
width=
"16"
height=
"16"
viewBox=
"0 0 16 16"
fill=
"none"
xmlns=
"http://www.w3.org/2000/svg"
>
<path
d=
"M10.6665 2.66634H11.9998C12.3535 2.66634 12.6926 2.80682 12.9426 3.05687C13.1927 3.30691 13.3332 3.64605 13.3332 3.99967V13.333C13.3332 13.6866 13.1927 14.0258 12.9426 14.2758C12.6926 14.5259 12.3535 14.6663 11.9998 14.6663H3.99984C3.64622 14.6663 3.30708 14.5259 3.05703 14.2758C2.80698 14.0258 2.6665 13.6866 2.6665 13.333V3.99967C2.6665 3.64605 2.80698 3.30691 3.05703 3.05687C3.30708 2.80682 3.64622 2.66634 3.99984 2.66634H5.33317M5.99984 1.33301H9.99984C10.368 1.33301 10.6665 1.63148 10.6665 1.99967V3.33301C10.6665 3.7012 10.368 3.99967 9.99984 3.99967H5.99984C5.63165 3.99967 5.33317 3.7012 5.33317 3.33301V1.99967C5.33317 1.63148 5.63165 1.33301 5.99984 1.33301Z"
stroke=
"#667085"
stroke-width=
"1.5"
stroke-linecap=
"round"
stroke-linejoin=
"round"
/>
</svg>
web/app/components/header/account-setting/members-page/invited-modal/index.module.css
View file @
6040da75
...
...
@@ -2,4 +2,20 @@
padding
:
32px
!important
;
width
:
480px
!important
;
background
:
linear-gradient
(
180deg
,
rgba
(
3
,
152
,
85
,
0.05
)
0%
,
rgba
(
3
,
152
,
85
,
0
)
22.44%
),
#F9FAFB
!important
;
}
.copyIcon
{
background-image
:
url(./assets/copy.svg)
;
background-position
:
center
;
background-repeat
:
no-repeat
;
}
.copyIcon
:hover
{
background-image
:
url(./assets/copy-hover.svg)
;
background-position
:
center
;
background-repeat
:
no-repeat
;
}
.copyIcon.copied
{
background-image
:
url(./assets/copied.svg)
;
}
\ No newline at end of file
web/app/components/header/account-setting/members-page/invited-modal/index.tsx
View file @
6040da75
import
{
CheckCircleIcon
}
from
'@heroicons/react/24/solid'
import
{
XMarkIcon
}
from
'@heroicons/react/24/outline'
import
{
useTranslation
}
from
'react-i18next'
import
InvitationLink
from
'./invitation-link'
import
s
from
'./index.module.css'
import
Modal
from
'@/app/components/base/modal'
import
Button
from
'@/app/components/base/button'
import
s
from
'./index.module.css'
interface
IInvitedModalProps
{
onCancel
:
()
=>
void
,
type
IInvitedModalProps
=
{
invitationLink
:
string
onCancel
:
()
=>
void
}
const
InvitedModal
=
({
onCancel
invitationLink
,
onCancel
,
}:
IInvitedModalProps
)
=>
{
const
{
t
}
=
useTranslation
()
...
...
@@ -27,10 +29,14 @@ const InvitedModal = ({
<
XMarkIcon
className=
'w-4 h-4 cursor-pointer'
onClick=
{
onCancel
}
/>
</
div
>
<
div
className=
'mb-1 text-xl font-semibold text-gray-900'
>
{
t
(
'common.members.invitationSent'
)
}
</
div
>
<
div
className=
'mb-10 text-sm text-gray-500'
>
{
t
(
'common.members.invitationSentTip'
)
}
</
div
>
<
div
className=
'mb-5 text-sm text-gray-500'
>
{
t
(
'common.members.invitationSentTip'
)
}
</
div
>
<
div
className=
'mb-9'
>
<
div
className=
'py-2 text-sm font-Medium text-gray-900'
>
{
t
(
'common.members.invitationLink'
)
}
</
div
>
<
InvitationLink
value=
{
invitationLink
}
/>
</
div
>
<
div
className=
'flex justify-end'
>
<
Button
className=
'w-[96px] text-sm font-medium'
<
Button
className=
'w-[96px] text-sm font-medium'
onClick=
{
onCancel
}
type=
'primary'
>
...
...
@@ -42,4 +48,4 @@ const InvitedModal = ({
)
}
export
default
InvitedModal
\ No newline at end of file
export
default
InvitedModal
web/app/components/header/account-setting/members-page/invited-modal/invitation-link.tsx
0 → 100644
View file @
6040da75
'use client'
import
React
,
{
useCallback
,
useEffect
,
useState
}
from
'react'
import
{
t
}
from
'i18next'
import
s
from
'./index.module.css'
import
Tooltip
from
'@/app/components/base/tooltip'
import
useCopyToClipboard
from
'@/hooks/use-copy-to-clipboard'
type
IInvitationLinkProps
=
{
value
?:
string
}
const
InvitationLink
=
({
value
=
''
,
}:
IInvitationLinkProps
)
=>
{
const
[
isCopied
,
setIsCopied
]
=
useState
(
false
)
const
[
_
,
copy
]
=
useCopyToClipboard
()
const
copyHandle
=
useCallback
(()
=>
{
copy
(
value
)
setIsCopied
(
true
)
},
[
value
,
copy
])
useEffect
(()
=>
{
if
(
isCopied
)
{
const
timeout
=
setTimeout
(()
=>
{
setIsCopied
(
false
)
},
1000
)
return
()
=>
{
clearTimeout
(
timeout
)
}
}
},
[
isCopied
])
return
(
<
div
className=
'flex rounded-lg bg-gray-100 hover:bg-gray-100 border border-gray-200 py-2 items-center'
>
<
div
className=
"flex items-center flex-grow h-5"
>
<
div
className=
'flex-grow bg-gray-100 text-[13px] relative h-full'
>
<
Tooltip
selector=
"top-uniq"
content=
{
isCopied
?
`${t('appApi.copied')}`
:
`${t('appApi.copy')}`
}
className=
'z-10'
>
<
div
className=
'absolute top-0 left-0 w-full pl-2 pr-2 truncate cursor-pointer r-0'
onClick=
{
copyHandle
}
>
{
value
}
</
div
>
</
Tooltip
>
</
div
>
<
div
className=
"flex-shrink-0 h-4 bg-gray-200 border"
/>
<
Tooltip
selector=
"top-uniq"
content=
{
isCopied
?
`${t('appApi.copied')}`
:
`${t('appApi.copy')}`
}
className=
'z-10'
>
<
div
className=
"px-0.5 flex-shrink-0"
>
<
div
className=
{
`box-border w-[30px] h-[30px] flex items-center justify-center rounded-lg hover:bg-gray-100 cursor-pointer ${s.copyIcon} ${isCopied ? s.copied : ''}`
}
onClick=
{
copyHandle
}
>
</
div
>
</
div
>
</
Tooltip
>
</
div
>
</
div
>
)
}
export
default
InvitationLink
web/app/components/header/account-setting/provider-page/index.tsx
View file @
6040da75
...
...
@@ -93,7 +93,7 @@ const ProviderPage = () => {
))
}
</
div
>
<
div
className=
'
absolute bottom-0 w-full
h-[42px] flex items-center bg-white text-xs text-gray-500'
>
<
div
className=
'
fixed bottom-0 w-[472px]
h-[42px] flex items-center bg-white text-xs text-gray-500'
>
<
LockClosedIcon
className=
'w-3 h-3 mr-1'
/>
{
t
(
'common.provider.encrypted.front'
)
}
<
Link
...
...
web/app/components/share/text-generation/index.tsx
View file @
6040da75
...
...
@@ -10,6 +10,7 @@ import Button from '../../base/button'
import
{
checkOrSetAccessToken
}
from
'../utils'
import
s
from
'./style.module.css'
import
RunBatch
from
'./run-batch'
import
ResDownload
from
'./run-batch/res-download'
import
useBreakpoints
,
{
MediaType
}
from
'@/hooks/use-breakpoints'
import
RunOnce
from
'@/app/components/share/text-generation/run-once'
import
{
fetchSavedMessage
as
doFetchSavedMessage
,
fetchAppInfo
,
fetchAppParams
,
removeMessage
,
saveMessage
}
from
'@/service/share'
...
...
@@ -24,7 +25,6 @@ import SavedItems from '@/app/components/app/text-generate/saved-items'
import
type
{
InstalledApp
}
from
'@/models/explore'
import
{
appDefaultIconBackground
}
from
'@/config'
import
Toast
from
'@/app/components/base/toast'
const
PARALLEL_LIMIT
=
5
enum
TaskStatus
{
pending
=
'pending'
,
...
...
@@ -105,6 +105,20 @@ const TextGeneration: FC<IMainProps> = ({
const
noPendingTask
=
pendingTaskList
.
length
===
0
const
showTaskList
=
allTaskList
.
filter
(
task
=>
task
.
status
!==
TaskStatus
.
pending
)
const
allTaskFinished
=
allTaskList
.
every
(
task
=>
task
.
status
===
TaskStatus
.
completed
)
const
[
batchCompletionRes
,
setBatchCompletionRes
,
getBatchCompletionRes
]
=
useGetState
<
Record
<
string
,
string
>>
({})
const
exportRes
=
allTaskList
.
map
((
task
)
=>
{
if
(
allTaskList
.
length
>
0
&&
!
allTaskFinished
)
return
{}
const
batchCompletionResLatest
=
getBatchCompletionRes
()
const
res
:
Record
<
string
,
string
>
=
{}
const
{
inputs
,
query
}
=
task
.
params
promptConfig
?.
prompt_variables
.
forEach
((
v
)
=>
{
res
[
v
.
name
]
=
inputs
[
v
.
key
]
})
res
[
t
(
'share.generation.queryTitle'
)]
=
query
res
[
t
(
'share.generation.completionResult'
)]
=
batchCompletionResLatest
[
task
.
id
]
return
res
})
const
checkBatchInputs
=
(
data
:
string
[][])
=>
{
if
(
!
data
||
data
.
length
===
0
)
{
notify
({
type
:
'error'
,
message
:
t
(
'share.generation.errorMsg.empty'
)
})
...
...
@@ -232,10 +246,9 @@ const TextGeneration: FC<IMainProps> = ({
// eslint-disable-next-line @typescript-eslint/no-use-before-define
showResSidebar
()
}
const
handleCompleted
=
(
taskId
?:
number
,
isSuccess
?:
boolean
)
=>
{
// console.log(taskId, isSuccess)
const
handleCompleted
=
(
completionRes
:
string
,
taskId
?:
number
)
=>
{
const
allTasklistLatest
=
getLatestTaskList
()
const
batchCompletionResLatest
=
getBatchCompletionRes
()
const
pendingTaskList
=
allTasklistLatest
.
filter
(
task
=>
task
.
status
===
TaskStatus
.
pending
)
const
nextPendingTaskId
=
pendingTaskList
[
0
]?.
id
// console.log(`start: ${allTasklistLatest.map(item => item.status).join(',')}`)
...
...
@@ -256,6 +269,12 @@ const TextGeneration: FC<IMainProps> = ({
})
// console.log(`end: ${newAllTaskList.map(item => item.status).join(',')}`)
setAllTaskList
(
newAllTaskList
)
if
(
taskId
)
{
setBatchCompletionRes
({
...
batchCompletionResLatest
,
[
`
${
taskId
}
`
]:
completionRes
,
})
}
}
const
fetchInitData
=
async
()
=>
{
...
...
@@ -344,14 +363,23 @@ const TextGeneration: FC<IMainProps> = ({
<
div
className=
{
s
.
starIcon
}
></
div
>
<
div
className=
'text-lg text-gray-800 font-semibold'
>
{
t
(
'share.generation.title'
)
}
</
div
>
</
div
>
{
!
isPC
&&
(
<
div
className=
'flex items-center justify-center cursor-pointer'
onClick=
{
hideResSidebar
}
>
<
XMarkIcon
className=
'w-4 h-4 text-gray-800'
/>
</
div
>
)
}
<
div
className=
'flex items-center space-x-2'
>
{
allTaskList
.
length
>
0
&&
allTaskFinished
&&
(
<
ResDownload
isMobile=
{
isMobile
}
values=
{
exportRes
}
/>
)
}
{
!
isPC
&&
(
<
div
className=
'flex items-center justify-center cursor-pointer'
onClick=
{
hideResSidebar
}
>
<
XMarkIcon
className=
'w-4 h-4 text-gray-800'
/>
</
div
>
)
}
</
div
>
</
div
>
<
div
className=
'grow overflow-y-auto'
>
...
...
web/app/components/share/text-generation/result/index.tsx
View file @
6040da75
'use client'
import
type
{
FC
}
from
'react'
import
React
,
{
useEffect
,
useState
}
from
'react'
import
{
useBoolean
}
from
'ahooks'
import
{
useBoolean
,
useGetState
}
from
'ahooks'
import
{
t
}
from
'i18next'
import
cn
from
'classnames'
import
TextGenerationRes
from
'@/app/components/app/text-generate/item'
...
...
@@ -27,7 +27,7 @@ export type IResultProps = {
onShowRes
:
()
=>
void
handleSaveMessage
:
(
messageId
:
string
)
=>
void
taskId
?:
number
onCompleted
:
(
taskId
?:
number
,
success
?:
boolean
)
=>
void
onCompleted
:
(
completionRes
:
string
,
taskId
?:
number
,
success
?:
boolean
)
=>
void
}
const
Result
:
FC
<
IResultProps
>
=
({
...
...
@@ -53,7 +53,7 @@ const Result: FC<IResultProps> = ({
setResponsingFalse
()
},
[
controlStopResponding
])
const
[
completionRes
,
setCompletionRes
]
=
use
State
(
''
)
const
[
completionRes
,
setCompletionRes
,
getCompletionRes
]
=
useGet
State
(
''
)
const
{
notify
}
=
Toast
const
isNoData
=
!
completionRes
...
...
@@ -141,11 +141,11 @@ const Result: FC<IResultProps> = ({
onCompleted
:
()
=>
{
setResponsingFalse
()
setMessageId
(
tempMessageId
)
onCompleted
(
taskId
,
true
)
onCompleted
(
getCompletionRes
(),
taskId
,
true
)
},
onError
()
{
setResponsingFalse
()
onCompleted
(
taskId
,
false
)
onCompleted
(
getCompletionRes
(),
taskId
,
false
)
},
},
isInstalledApp
,
installedAppInfo
?.
id
)
}
...
...
web/app/components/share/text-generation/run-batch/res-download/index.tsx
0 → 100644
View file @
6040da75
'use client'
import
type
{
FC
}
from
'react'
import
React
from
'react'
import
{
useCSVDownloader
,
}
from
'react-papaparse'
import
{
useTranslation
}
from
'react-i18next'
import
cn
from
'classnames'
import
{
Download02
as
DownloadIcon
}
from
'@/app/components/base/icons/src/vender/solid/general'
import
Button
from
'@/app/components/base/button'
export
type
IResDownloadProps
=
{
isMobile
:
boolean
values
:
Record
<
string
,
string
>
[]
}
const
ResDownload
:
FC
<
IResDownloadProps
>
=
({
isMobile
,
values
,
})
=>
{
const
{
t
}
=
useTranslation
()
const
{
CSVDownloader
,
Type
}
=
useCSVDownloader
()
return
(
<
CSVDownloader
className=
"block cursor-pointer"
type=
{
Type
.
Link
}
filename=
{
'result'
}
bom=
{
true
}
config=
{
{
// delimiter: ';',
}
}
data=
{
values
}
>
<
Button
className=
{
cn
(
'flex items-center !h-8 space-x-2 bg-white !text-[13px] font-medium'
,
isMobile
?
'!p-0 !w-8 justify-center'
:
'!px-3'
)
}
>
<
DownloadIcon
className=
'w-4 h-4 text-[#155EEF]'
/>
{
!
isMobile
&&
<
span
className=
'text-[#155EEF]'
>
{
t
(
'common.operation.download'
)
}
</
span
>
}
</
Button
>
</
CSVDownloader
>
)
}
export
default
React
.
memo
(
ResDownload
)
web/app/install/installForm.tsx
View file @
6040da75
'use client'
import
React
from
'react'
import
{
useTranslation
}
from
'react-i18next'
import
Button
from
'@/app/components/base/button'
import
Link
from
'next/link'
import
{
useRouter
}
from
'next/navigation'
import
Toast
from
'../components/base/toast'
import
Button
from
'@/app/components/base/button'
import
{
setup
}
from
'@/service/common'
const
validEmailReg
=
/^
[\w\.
-
]
+@
([\w
-
]
+
\.)
+
[\w
-
]{2,}
$/
...
...
@@ -40,36 +40,37 @@ const InstallForm = () => {
showErrorMessage
(
t
(
'login.error.passwordEmpty'
))
return
false
}
if
(
!
validPassword
.
test
(
password
))
{
if
(
!
validPassword
.
test
(
password
))
showErrorMessage
(
t
(
'login.error.passwordInvalid'
))
}
return
true
}
const
handleSetting
=
async
()
=>
{
if
(
!
valid
())
return
if
(
!
valid
())
return
await
setup
({
body
:
{
email
,
name
,
password
}
password
,
}
,
})
router
.
push
(
'/signin'
)
}
return
(
<>
<
div
className=
"sm:mx-auto sm:w-full sm:max-w-md"
>
<
h2
className=
"text-
3xl font-normal
text-gray-900"
>
{
t
(
'login.setAdminAccount'
)
}
</
h2
>
<
h2
className=
"text-
[32px] font-bold
text-gray-900"
>
{
t
(
'login.setAdminAccount'
)
}
</
h2
>
<
p
className=
'
mt-
2
text-sm text-gray-600
mt-
1
text-sm text-gray-600
'
>
{
t
(
'login.setAdminAccountDesc'
)
}
</
p
>
</
div
>
<
div
className=
"grow mt-8 sm:mx-auto sm:w-full sm:max-w-md"
>
<
div
className=
"bg-white "
>
<
form
className=
"space-y-6"
onSubmit=
{
()
=>
{
}
}
>
<
div
>
<
label
htmlFor=
"email"
className=
"
block text-sm font-medium text-gray-7
00"
>
<
form
onSubmit=
{
()
=>
{
}
}
>
<
div
className=
'mb-5'
>
<
label
htmlFor=
"email"
className=
"
my-2 flex items-center justify-between text-sm font-medium text-gray-9
00"
>
{
t
(
'login.email'
)
}
</
label
>
<
div
className=
"mt-1"
>
...
...
@@ -78,13 +79,14 @@ const InstallForm = () => {
type=
"email"
value=
{
email
}
onChange=
{
e
=>
setEmail
(
e
.
target
.
value
)
}
className=
{
'appearance-none block w-full px-3 py-2 border border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 rounded-md shadow-sm placeholder-gray-400 sm:text-sm'
}
placeholder=
{
t
(
'login.emailPlaceholder'
)
||
''
}
className=
{
'appearance-none block w-full rounded-lg pl-[14px] px-3 py-2 border border-gray-200 hover:border-gray-300 hover:shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 placeholder-gray-400 caret-primary-600 sm:text-sm'
}
/>
</
div
>
</
div
>
<
div
>
<
label
htmlFor=
"name"
className=
"
block text-sm font-medium text-gray-7
00"
>
<
div
className=
'mb-5'
>
<
label
htmlFor=
"name"
className=
"
my-2 flex items-center justify-between text-sm font-medium text-gray-9
00"
>
{
t
(
'login.name'
)
}
</
label
>
<
div
className=
"mt-1 relative rounded-md shadow-sm"
>
...
...
@@ -93,13 +95,14 @@ const InstallForm = () => {
type=
"text"
value=
{
name
}
onChange=
{
e
=>
setName
(
e
.
target
.
value
)
}
className=
{
'appearance-none block w-full px-3 py-2 border border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 rounded-md shadow-sm placeholder-gray-400 sm:text-sm pr-10'
}
placeholder=
{
t
(
'login.namePlaceholder'
)
||
''
}
className=
{
'appearance-none block w-full rounded-lg pl-[14px] px-3 py-2 border border-gray-200 hover:border-gray-300 hover:shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 placeholder-gray-400 caret-primary-600 sm:text-sm pr-10'
}
/>
</
div
>
</
div
>
<
div
>
<
label
htmlFor=
"password"
className=
"block text-sm font-medium text-gray-700"
>
<
div
className=
'mb-5'
>
<
label
htmlFor=
"password"
className=
"my-2 flex items-center justify-between text-sm font-medium text-gray-900"
>
{
t
(
'login.password'
)
}
</
label
>
<
div
className=
"mt-1 relative rounded-md shadow-sm"
>
...
...
@@ -108,7 +111,8 @@ const InstallForm = () => {
type=
'password'
value=
{
password
}
onChange=
{
e
=>
setPassword
(
e
.
target
.
value
)
}
className=
{
'appearance-none block w-full px-3 py-2 border border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 rounded-md shadow-sm placeholder-gray-400 sm:text-sm pr-10'
}
placeholder=
{
t
(
'login.passwordPlaceholder'
)
||
''
}
className=
{
'appearance-none block w-full rounded-lg pl-[14px] px-3 py-2 border border-gray-200 hover:border-gray-300 hover:shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 placeholder-gray-400 caret-primary-600 sm:text-sm pr-10'
}
/>
</
div
>
<
div
className=
'mt-1 text-xs text-gray-500'
>
{
t
(
'login.error.passwordInvalid'
)
}
</
div
>
...
...
@@ -123,29 +127,21 @@ const InstallForm = () => {
</div>
</div>
</div> */
}
{
/* agree to our Terms and Privacy Policy. */
}
<
div
className=
"block mt-6 text-xs text-gray-600"
>
{
t
(
'login.tosDesc'
)
}
<
Link
className=
'text-primary-600'
target=
{
'_blank'
}
href=
'https://docs.dify.ai/user-agreement/terms-of-service'
>
{
t
(
'login.tos'
)
}
</
Link
>
&
<
Link
className=
'text-primary-600'
target=
{
'_blank'
}
href=
'https://langgenius.ai/privacy-policy'
>
{
t
(
'login.pp'
)
}
</
Link
>
</
div
>
<
div
>
<
Button
type=
'primary'
onClick=
{
handleSetting
}
>
<
Button
type=
'primary'
className=
'w-full !fone-medium !text-sm'
onClick=
{
handleSetting
}
>
{
t
(
'login.installBtn'
)
}
</
Button
>
</
div
>
</
form
>
<
div
className=
"block w-hull mt-2 text-xs text-gray-600"
>
{
t
(
'login.license.tip'
)
}
<
Link
className=
'text-primary-600'
target=
{
'_blank'
}
href=
'https://docs.dify.ai/community/open-source'
>
{
t
(
'login.license.link'
)
}
</
Link
>
</
div
>
</
div
>
</
div
>
</>
...
...
web/app/signin/_header.tsx
View file @
6040da75
'use client'
import
React
from
'react'
import
{
useContext
}
from
'use-context-selector'
import
style
from
'./page.module.css'
import
Select
,
{
LOCALES
}
from
'@/app/components/base/select/locale'
import
{
type
Locale
}
from
'@/i18n'
import
I18n
from
'@/context/i18n'
import
{
setLocaleOnClient
}
from
'@/i18n/client'
import
{
useContext
}
from
'use-context-selector'
type
IHeaderProps
=
{
locale
:
string
}
const
Header
=
()
=>
{
const
{
locale
,
setLocaleOnClient
}
=
useContext
(
I18n
)
...
...
web/app/signin/forms.tsx
View file @
6040da75
...
...
@@ -2,9 +2,9 @@
import
React
from
'react'
import
{
useSearchParams
}
from
'next/navigation'
import
cn
from
'classnames'
import
NormalForm
from
'./normalForm'
import
OneMoreStep
from
'./oneMoreStep'
import
classNames
from
'classnames'
const
Forms
=
()
=>
{
const
searchParams
=
useSearchParams
()
...
...
@@ -19,7 +19,7 @@ const Forms = () => {
}
}
return
<
div
className=
{
c
lassNames
(
c
n
(
'flex flex-col items-center w-full grow items-center justify-center'
,
'px-6'
,
'md:px-[108px]'
,
...
...
@@ -28,7 +28,6 @@ const Forms = () => {
<
div
className=
'flex flex-col md:w-[400px]'
>
{
getForm
()
}
</
div
>
</
div
>
}
...
...
web/app/signin/normalForm.tsx
View file @
6040da75
...
...
@@ -2,16 +2,15 @@
import
React
,
{
useEffect
,
useReducer
,
useState
}
from
'react'
import
{
useTranslation
}
from
'react-i18next'
import
{
useRouter
}
from
'next/navigation'
import
{
IS_CE_EDITION
}
from
'@/config'
import
classNames
from
'classnames'
import
useSWR
from
'swr'
import
Link
from
'next/link'
import
Toast
from
'../components/base/toast'
import
style
from
'./page.module.css'
// import Tooltip from '@/app/components/base/tooltip/index'
import
Toast
from
'../components/base/toast
'
import
{
IS_CE_EDITION
,
apiPrefix
}
from
'@/config
'
import
Button
from
'@/app/components/base/button'
import
{
login
,
oauth
}
from
'@/service/common'
import
{
apiPrefix
}
from
'@/config'
const
validEmailReg
=
/^
[\w\.
-
]
+@
([\w
-
]
+
\.)
+
[\w
-
]{2,}
$/
...
...
@@ -91,8 +90,9 @@ const NormalForm = () => {
remember_me
:
true
,
},
})
router
.
push
(
'/'
)
}
finally
{
router
.
push
(
'/apps'
)
}
finally
{
setIsLoading
(
false
)
}
}
...
...
@@ -132,8 +132,8 @@ const NormalForm = () => {
return
(
<>
<
div
className=
"w-full mx-auto"
>
<
h2
className=
"text-
3xl font-normal
text-gray-900"
>
{
t
(
'login.pageTitle'
)
}
</
h2
>
<
p
className=
'mt-
2 text-sm text-gray-600
'
>
{
t
(
'login.welcome'
)
}
</
p
>
<
h2
className=
"text-
[32px] font-bold
text-gray-900"
>
{
t
(
'login.pageTitle'
)
}
</
h2
>
<
p
className=
'mt-
1 text-sm text-gray-600
'
>
{
t
(
'login.welcome'
)
}
</
p
>
</
div
>
<
div
className=
"w-full mx-auto mt-8"
>
...
...
@@ -145,7 +145,7 @@ const NormalForm = () => {
<
Button
type=
'default'
disabled=
{
isLoading
}
className=
'w-full'
className=
'w-full
hover:!bg-gray-50 !text-sm !font-medium
'
>
<>
<
span
className=
{
...
...
@@ -154,7 +154,7 @@ const NormalForm = () => {
'w-5 h-5 mr-2'
,
)
}
/>
<
span
className=
"truncate"
>
{
t
(
'login.withGitHub'
)
}
</
span
>
<
span
className=
"truncate
text-gray-800
"
>
{
t
(
'login.withGitHub'
)
}
</
span
>
</>
</
Button
>
</
a
>
...
...
@@ -164,7 +164,7 @@ const NormalForm = () => {
<
Button
type=
'default'
disabled=
{
isLoading
}
className=
'w-full'
className=
'w-full
hover:!bg-gray-50 !text-sm !font-medium
'
>
<>
<
span
className=
{
...
...
@@ -173,7 +173,7 @@ const NormalForm = () => {
'w-5 h-5 mr-2'
,
)
}
/>
<
span
className=
"truncate"
>
{
t
(
'login.withGoogle'
)
}
</
span
>
<
span
className=
"truncate
text-gray-800
"
>
{
t
(
'login.withGoogle'
)
}
</
span
>
</>
</
Button
>
</
a
>
...
...
@@ -192,9 +192,9 @@ const NormalForm = () => {
</div>
</div> */
}
<
form
className=
"space-y-6"
onSubmit=
{
()
=>
{
}
}
>
<
div
>
<
label
htmlFor=
"email"
className=
"
block text-sm font-medium text-gray-7
00"
>
<
form
onSubmit=
{
()
=>
{
}
}
>
<
div
className=
'mb-5'
>
<
label
htmlFor=
"email"
className=
"
my-2 block text-sm font-medium text-gray-9
00"
>
{
t
(
'login.email'
)
}
</
label
>
<
div
className=
"mt-1"
>
...
...
@@ -204,13 +204,14 @@ const NormalForm = () => {
id=
"email"
type=
"email"
autoComplete=
"email"
className=
{
'appearance-none block w-full px-3 py-2 border border-gray-300 focus:outline-none focus:ring-primary-500 focus:border-primary-500 rounded-md shadow-sm placeholder-gray-400 sm:text-sm'
}
placeholder=
{
t
(
'login.emailPlaceholder'
)
||
''
}
className=
{
'appearance-none block w-full rounded-lg pl-[14px] px-3 py-2 border border-gray-200 hover:border-gray-300 hover:shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 placeholder-gray-400 caret-primary-600 sm:text-sm'
}
/>
</
div
>
</
div
>
<
div
>
<
label
htmlFor=
"password"
className=
"
flex items-center justify-between text-sm font-medium text-gray-7
00"
>
<
div
className=
'mb-4'
>
<
label
htmlFor=
"password"
className=
"
my-2 flex items-center justify-between text-sm font-medium text-gray-9
00"
>
<
span
>
{
t
(
'login.password'
)
}
</
span
>
{
/* <Tooltip
selector='forget-password'
...
...
@@ -235,10 +236,8 @@ const NormalForm = () => {
onChange=
{
e
=>
setPassword
(
e
.
target
.
value
)
}
type=
{
showPassword
?
'text'
:
'password'
}
autoComplete=
"current-password"
className=
{
`appearance-none block w-full px-3 py-2
border border-gray-300
focus:outline-none focus:ring-indigo-500 focus:border-indigo-500
rounded-md shadow-sm placeholder-gray-400 sm:text-sm pr-10`
}
placeholder=
{
t
(
'login.passwordPlaceholder'
)
||
''
}
className=
{
'appearance-none block w-full rounded-lg pl-[14px] px-3 py-2 border border-gray-200 hover:border-gray-300 hover:shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 placeholder-gray-400 caret-primary-600 sm:text-sm pr-10'
}
/>
<
div
className=
"absolute inset-y-0 right-0 flex items-center pr-3"
>
<
button
...
...
@@ -252,18 +251,19 @@ const NormalForm = () => {
</
div
>
</
div
>
<
div
>
<
div
className=
'mb-2'
>
<
Button
type=
'primary'
onClick=
{
handleEmailPasswordLogin
}
disabled=
{
isLoading
}
className=
"w-full !fone-medium !text-sm"
>
{
t
(
'login.signBtn'
)
}
</
Button
>
</
div
>
</
form
>
</>
}
{
/* agree to our Terms and Privacy Policy. */
}
<
div
className=
"
block mt-6
text-xs text-gray-600"
>
<
div
className=
"
w-hull text-center block mt-2
text-xs text-gray-600"
>
{
t
(
'login.tosDesc'
)
}
<
Link
...
...
web/app/signin/oneMoreStep.tsx
View file @
6040da75
'use client'
import
React
,
{
useEffect
,
useReducer
}
from
'react'
import
{
useTranslation
}
from
'react-i18next'
import
Link
from
'next/link'
import
useSWR
from
'swr'
import
{
useRouter
}
from
'next/navigation'
import
Button
from
'@/app/components/base/button'
...
...
@@ -74,14 +75,14 @@ const OneMoreStep = () => {
return
(
<>
<
div
className=
"w-full mx-auto"
>
<
h2
className=
"text-
3xl font-normal
text-gray-900"
>
{
t
(
'login.oneMoreStep'
)
}
</
h2
>
<
p
className=
'mt-
2
text-sm text-gray-600 '
>
{
t
(
'login.createSample'
)
}
</
p
>
<
h2
className=
"text-
[32px] font-bold
text-gray-900"
>
{
t
(
'login.oneMoreStep'
)
}
</
h2
>
<
p
className=
'mt-
1
text-sm text-gray-600 '
>
{
t
(
'login.createSample'
)
}
</
p
>
</
div
>
<
div
className=
"w-full mx-auto mt-
8
"
>
<
div
className=
"
space-y-6
bg-white"
>
<
div
className=
""
>
<
label
className=
"flex items-center justify-between text-sm font-medium text-gray-900"
>
<
div
className=
"w-full mx-auto mt-
6
"
>
<
div
className=
"bg-white"
>
<
div
className=
"
mb-5
"
>
<
label
className=
"
my-2
flex items-center justify-between text-sm font-medium text-gray-900"
>
{
t
(
'login.invitationCode'
)
}
<
Tooltip
clickable
...
...
@@ -103,16 +104,16 @@ const OneMoreStep = () => {
id=
"invitation_code"
value=
{
state
.
invitation_code
}
type=
"text"
className=
{
'appearance-none block w-full px-3 py-2 border border-gray-300 focus:outline-none focus:ring-primary-600 focus:border-primary-600 rounded-md shadow-sm placeholder-gray-400 sm:text-sm'
}
placeholder=
{
t
(
'login.invitationCodePlaceholder'
)
||
''
}
className=
{
'appearance-none block w-full rounded-lg pl-[14px] px-3 py-2 border border-gray-200 hover:border-gray-300 hover:shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 placeholder-gray-400 caret-primary-600 sm:text-sm'
}
onChange=
{
(
e
)
=>
{
dispatch
({
type
:
'invitation_code'
,
value
:
e
.
target
.
value
.
trim
()
})
}
}
/>
</
div
>
</
div
>
<
div
>
<
label
htmlFor=
"name"
className=
"block text-sm font-medium text-gray-700"
>
<
div
className=
'mb-5'
>
<
label
htmlFor=
"name"
className=
"my-2 flex items-center justify-between text-sm font-medium text-gray-900"
>
{
t
(
'login.interfaceLanguage'
)
}
</
label
>
<
div
className=
"relative mt-1 rounded-md shadow-sm"
>
...
...
@@ -125,8 +126,7 @@ const OneMoreStep = () => {
/>
</
div
>
</
div
>
<
div
>
<
div
className=
'mb-4'
>
<
label
htmlFor=
"timezone"
className=
"block text-sm font-medium text-gray-700"
>
{
t
(
'login.timezone'
)
}
</
label
>
...
...
@@ -143,6 +143,7 @@ const OneMoreStep = () => {
<
div
>
<
Button
type=
'primary'
className=
'w-full !fone-medium !text-sm'
disabled=
{
state
.
formState
===
'processing'
}
onClick=
{
()
=>
{
dispatch
({
type
:
'formState'
,
value
:
'processing'
})
...
...
@@ -151,6 +152,15 @@ const OneMoreStep = () => {
{
t
(
'login.go'
)
}
</
Button
>
</
div
>
<
div
className=
"block w-hull mt-2 text-xs text-gray-600"
>
{
t
(
'login.license.tip'
)
}
<
Link
className=
'text-primary-600'
target=
{
'_blank'
}
href=
'https://docs.dify.ai/community/open-source'
>
{
t
(
'login.license.link'
)
}
</
Link
>
</
div
>
</
div
>
</
div
>
</>
...
...
web/app/signin/page.tsx
View file @
6040da75
import
React
from
'react'
import
cn
from
'classnames'
import
Forms
from
'./forms'
import
Header
from
'./_header'
import
style
from
'./page.module.css'
import
classNames
from
'classnames'
const
SignIn
=
()
=>
{
return
(
<>
<
div
className=
{
c
lassNames
(
<
div
className=
{
c
n
(
style
.
background
,
'flex w-full min-h-screen'
,
'sm:p-4 lg:p-8'
,
'gap-x-20'
,
'justify-center lg:justify-start'
'justify-center lg:justify-start'
,
)
}
>
<
div
className=
{
c
lassNames
(
c
n
(
'flex w-full flex-col bg-white shadow rounded-2xl shrink-0'
,
'space-between'
'space-between'
,
)
}
>
<
Header
/>
...
...
web/context/app-context.tsx
View file @
6040da75
...
...
@@ -37,6 +37,8 @@ const AppContext = createContext<AppContextValue>({
id
:
''
,
name
:
''
,
email
:
''
,
avatar
:
''
,
is_password_set
:
false
,
},
mutateUserProfile
:
()
=>
{
},
pageContainerRef
:
createRef
(),
...
...
web/docker/entrypoint.sh
View file @
6040da75
...
...
@@ -4,8 +4,19 @@ set -e
export
NEXT_PUBLIC_DEPLOY_ENV
=
${
DEPLOY_ENV
}
export
NEXT_PUBLIC_EDITION
=
${
EDITION
}
export
NEXT_PUBLIC_API_PREFIX
=
${
CONSOLE_URL
}
/console/api
export
NEXT_PUBLIC_PUBLIC_API_PREFIX
=
${
APP_URL
}
/api
if
[[
-z
"
$CONSOLE_URL
"
]]
;
then
export
NEXT_PUBLIC_API_PREFIX
=
${
CONSOLE_API_URL
}
/console/api
else
export
NEXT_PUBLIC_API_PREFIX
=
${
CONSOLE_URL
}
/console/api
fi
if
[[
-z
"
$APP_URL
"
]]
;
then
export
NEXT_PUBLIC_PUBLIC_API_PREFIX
=
${
APP_API_URL
}
/api
else
export
NEXT_PUBLIC_PUBLIC_API_PREFIX
=
${
APP_URL
}
/api
fi
export
NEXT_PUBLIC_SENTRY_DSN
=
${
SENTRY_DSN
}
/usr/local/bin/pm2
-v
...
...
web/i18n/lang/common.en.ts
View file @
6040da75
...
...
@@ -14,6 +14,7 @@ const translation = {
edit
:
'Edit'
,
add
:
'Add'
,
refresh
:
'Restart'
,
reset
:
'Reset'
,
search
:
'Search'
,
change
:
'Change'
,
remove
:
'Remove'
,
...
...
@@ -21,6 +22,7 @@ const translation = {
copy
:
'Copy'
,
lineBreak
:
'Line break'
,
sure
:
'I
\'
m sure'
,
download
:
'Download'
,
},
placeholder
:
{
input
:
'Please enter'
,
...
...
@@ -94,6 +96,14 @@ const translation = {
avatar
:
'Avatar'
,
name
:
'Name'
,
email
:
'Email'
,
password
:
'Password'
,
passwordTip
:
'You can set a permanent password if you don’t want to use temporary login codes'
,
setPassword
:
'Set a password'
,
resetPassword
:
'Reset password'
,
currentPassword
:
'Current password'
,
newPassword
:
'New password'
,
confirmPassword
:
'Confirm password'
,
notEqual
:
'Two passwords are different.'
,
langGeniusAccount
:
'Dify account'
,
langGeniusAccountTip
:
'Your Dify account and associated user data.'
,
editName
:
'Edit Name'
,
...
...
@@ -110,15 +120,16 @@ const translation = {
admin
:
'Admin'
,
adminTip
:
'Can build apps & manage team settings'
,
normal
:
'Normal'
,
normalTip
:
'Only can use apps
,
can not build apps'
,
normalTip
:
'Only can use apps
,
can not build apps'
,
inviteTeamMember
:
'Add team member'
,
inviteTeamMemberTip
:
'They can access your team data directly after signing in.'
,
email
:
'Email'
,
emailInvalid
:
'Invalid Email Format'
,
emailPlaceholder
:
'Input Email'
,
sendInvite
:
'Add'
,
invitationSent
:
'Added'
,
invitationSentTip
:
'Added, and they can sign in to Dify to access your team data.'
,
invitationSent
:
'Invitation sent'
,
invitationSentTip
:
'Invitation sent, and they can sign in to Dify to access your team data.'
,
invitationLink
:
'Invitation Link'
,
ok
:
'OK'
,
removeFromTeam
:
'Remove from team'
,
removeFromTeamTip
:
'Will remove team access'
,
...
...
web/i18n/lang/common.zh.ts
View file @
6040da75
...
...
@@ -14,6 +14,7 @@ const translation = {
edit
:
'编辑'
,
add
:
'添加'
,
refresh
:
'重新开始'
,
reset
:
'重置'
,
search
:
'搜索'
,
change
:
'更改'
,
remove
:
'移除'
,
...
...
@@ -21,6 +22,7 @@ const translation = {
copy
:
'复制'
,
lineBreak
:
'换行'
,
sure
:
'我确定'
,
download
:
'下载'
,
},
placeholder
:
{
input
:
'请输入'
,
...
...
@@ -94,7 +96,14 @@ const translation = {
avatar
:
'头像'
,
name
:
'用户名'
,
email
:
'邮箱'
,
edit
:
'编辑'
,
password
:
'密码'
,
passwordTip
:
'如果您不想使用验证码登录,可以设置永久密码'
,
setPassword
:
'设置密码'
,
resetPassword
:
'重置密码'
,
currentPassword
:
'原密码'
,
newPassword
:
'新密码'
,
notEqual
:
'两个密码不相同'
,
confirmPassword
:
'确认密码'
,
langGeniusAccount
:
'Dify 账号'
,
langGeniusAccountTip
:
'您的 Dify 账号和相关的用户数据。'
,
editName
:
'编辑名字'
,
...
...
@@ -118,8 +127,9 @@ const translation = {
emailInvalid
:
'邮箱格式无效'
,
emailPlaceholder
:
'输入邮箱'
,
sendInvite
:
'添加'
,
invitationSent
:
'已添加'
,
invitationSentTip
:
'已添加,对方登录 Dify 后即可访问你的团队数据。'
,
invitationSent
:
'邀请已发送'
,
invitationSentTip
:
'邀请已发送,对方登录 Dify 后即可访问你的团队数据。'
,
invitationLink
:
'邀请链接'
,
ok
:
'好的'
,
removeFromTeam
:
'移除团队'
,
removeFromTeamTip
:
'将取消团队访问'
,
...
...
web/i18n/lang/login.en.ts
View file @
6040da75
const
translation
=
{
"pageTitle"
:
"Hey, let's get started!👋"
,
"welcome"
:
"Welcome to Dify, please log in to continue."
,
"email"
:
"Email address"
,
"password"
:
"Password"
,
"name"
:
"Name"
,
"forget"
:
"Forgot your password?"
,
"signBtn"
:
"Sign in"
,
"installBtn"
:
"Setting"
,
"setAdminAccount"
:
"Setting up an admin account"
,
"setAdminAccountDesc"
:
"Maximum privileges for admin account, which can be used to create applications and manage LLM providers, etc."
,
"createAndSignIn"
:
"Create and sign in"
,
"oneMoreStep"
:
"One more step"
,
"createSample"
:
"Based on this information, we’ll create sample application for you"
,
"invitationCode"
:
"Invitation Code"
,
"interfaceLanguage"
:
"Interface Dify"
,
"timezone"
:
"Time zone"
,
"go"
:
"Go to Dify"
,
"sendUsMail"
:
"Email us your introduction, and we'll handle the invitation request."
,
"acceptPP"
:
"I have read and accept the privacy policy"
,
"reset"
:
"Please run following command to reset your password"
,
"withGitHub"
:
"Continue with GitHub"
,
"withGoogle"
:
"Continue with Google"
,
"rightTitle"
:
"Unlock the full potential of LLM"
,
"rightDesc"
:
"Effortlessly build visually captivating, operable, and improvable AI applications."
,
"tos"
:
"Terms of Service"
,
"pp"
:
"Privacy Policy"
,
"tosDesc"
:
"By signing up, you agree to our"
,
"donthave"
:
"Don't have?"
,
"invalidInvitationCode"
:
"Invalid invitation code"
,
"accountAlreadyInited"
:
"Account already inited"
,
"error"
:
{
"emailEmpty"
:
"Email address is required"
,
"emailInValid"
:
"Please enter a valid email address"
,
"nameEmpty"
:
"Name is required"
,
"passwordEmpty"
:
"Password is required"
,
"passwordInvalid"
:
"Password must contain letters and numbers, and the length must be greater than 8"
,
}
pageTitle
:
'Hey, let
\'
s get started!👋'
,
welcome
:
'Welcome to Dify, please log in to continue.'
,
email
:
'Email address'
,
emailPlaceholder
:
'Your email'
,
password
:
'Password'
,
passwordPlaceholder
:
'Your password'
,
name
:
'Username'
,
namePlaceholder
:
'Your username'
,
forget
:
'Forgot your password?'
,
signBtn
:
'Sign in'
,
installBtn
:
'Setting'
,
setAdminAccount
:
'Setting up an admin account'
,
setAdminAccountDesc
:
'Maximum privileges for admin account, which can be used to create applications and manage LLM providers, etc.'
,
createAndSignIn
:
'Create and sign in'
,
oneMoreStep
:
'One more step'
,
createSample
:
'Based on this information, we’ll create sample application for you'
,
invitationCode
:
'Invitation Code'
,
invitationCodePlaceholder
:
'Your invitation code'
,
interfaceLanguage
:
'Interface Language'
,
timezone
:
'Time zone'
,
go
:
'Go to Dify'
,
sendUsMail
:
'Email us your introduction, and we
\'
ll handle the invitation request.'
,
acceptPP
:
'I have read and accept the privacy policy'
,
reset
:
'Please run following command to reset your password'
,
withGitHub
:
'Continue with GitHub'
,
withGoogle
:
'Continue with Google'
,
rightTitle
:
'Unlock the full potential of LLM'
,
rightDesc
:
'Effortlessly build visually captivating, operable, and improvable AI applications.'
,
tos
:
'Terms of Service'
,
pp
:
'Privacy Policy'
,
tosDesc
:
'By signing up, you agree to our'
,
donthave
:
'Don
\'
t have?'
,
invalidInvitationCode
:
'Invalid invitation code'
,
accountAlreadyInited
:
'Account already inited'
,
error
:
{
emailEmpty
:
'Email address is required'
,
emailInValid
:
'Please enter a valid email address'
,
nameEmpty
:
'Name is required'
,
passwordEmpty
:
'Password is required'
,
passwordInvalid
:
'Password must contain letters and numbers, and the length must be greater than 8'
,
},
license
:
{
tip
:
'Before starting Dify Community Edition, read the GitHub'
,
link
:
'Open-source License'
,
},
join
:
'Join'
,
joinTipStart
:
'Invite you join'
,
joinTipEnd
:
'team on Dify'
,
invalid
:
'The link has expired'
,
explore
:
'Explore Dify'
,
activatedTipStart
:
'You have joined the'
,
activatedTipEnd
:
'team'
,
activated
:
'Sign In Now'
,
}
export
default
translation
web/i18n/lang/login.zh.ts
View file @
6040da75
const
translation
=
{
"pageTitle"
:
"嗨,近来可好 👋"
,
"welcome"
:
"欢迎来到 Dify, 登录以继续"
,
"email"
:
"邮箱"
,
"password"
:
"密码"
,
"name"
:
"用户名"
,
"forget"
:
"忘记密码?"
,
"signBtn"
:
"登录"
,
"installBtn"
:
"设置"
,
"setAdminAccount"
:
"设置管理员账户"
,
"setAdminAccountDesc"
:
"管理员拥有的最大权限,可用于创建应用和管理 LLM 供应商等。"
,
"createAndSignIn"
:
"创建账户"
,
"oneMoreStep"
:
"还差一步"
,
"createSample"
:
"基于这些信息,我们将为您创建一个示例应用"
,
"invitationCode"
:
"邀请码"
,
"interfaceLanguage"
:
"界面语言"
,
"timezone"
:
"时区"
,
"go"
:
"跳转至 Dify"
,
"sendUsMail"
:
"发封邮件介绍你自己,我们会尽快处理。"
,
"acceptPP"
:
"我已阅读并接受隐私政策"
,
"reset"
:
"请运行以下命令重置密码"
,
"withGitHub"
:
"使用 GitHub 登录"
,
"withGoogle"
:
"使用 Google 登录"
,
"rightTitle"
:
"释放大型语言模型的全部潜能"
,
"rightDesc"
:
"简单构建可视化、可运营、可改进的 AI 应用"
,
"tos"
:
"使用协议"
,
"pp"
:
"隐私政策"
,
"tosDesc"
:
"使用即代表你并同意我们的"
,
"donthave"
:
"还没有邀请码?"
,
"invalidInvitationCode"
:
"无效的邀请码"
,
"accountAlreadyInited"
:
"账户已经初始化"
,
"error"
:
{
"emailEmpty"
:
"邮箱不能为空"
,
"emailInValid"
:
"请输入有效的邮箱地址"
,
"nameEmpty"
:
"用户名不能为空"
,
"passwordEmpty"
:
"密码不能为空"
,
"passwordInvalid"
:
"密码必须包含字母和数字,且长度不小于8位"
,
}
pageTitle
:
'嗨,近来可好 👋'
,
welcome
:
'欢迎来到 Dify, 登录以继续'
,
email
:
'邮箱'
,
emailPlaceholder
:
'输入邮箱地址'
,
password
:
'密码'
,
passwordPlaceholder
:
'输入密码'
,
name
:
'用户名'
,
namePlaceholder
:
'输入用户名'
,
forget
:
'忘记密码?'
,
signBtn
:
'登录'
,
installBtn
:
'设置'
,
setAdminAccount
:
'设置管理员账户'
,
setAdminAccountDesc
:
'管理员拥有的最大权限,可用于创建应用和管理 LLM 供应商等。'
,
createAndSignIn
:
'创建账户'
,
oneMoreStep
:
'还差一步'
,
createSample
:
'基于这些信息,我们将为您创建一个示例应用'
,
invitationCode
:
'邀请码'
,
invitationCodePlaceholder
:
'输入邀请码'
,
interfaceLanguage
:
'界面语言'
,
timezone
:
'时区'
,
go
:
'跳转至 Dify'
,
sendUsMail
:
'发封邮件介绍你自己,我们会尽快处理。'
,
acceptPP
:
'我已阅读并接受隐私政策'
,
reset
:
'请运行以下命令重置密码'
,
withGitHub
:
'使用 GitHub 登录'
,
withGoogle
:
'使用 Google 登录'
,
rightTitle
:
'释放大型语言模型的全部潜能'
,
rightDesc
:
'简单构建可视化、可运营、可改进的 AI 应用'
,
tos
:
'使用协议'
,
pp
:
'隐私政策'
,
tosDesc
:
'使用即代表你并同意我们的'
,
donthave
:
'还没有邀请码?'
,
invalidInvitationCode
:
'无效的邀请码'
,
accountAlreadyInited
:
'账户已经初始化'
,
error
:
{
emailEmpty
:
'邮箱不能为空'
,
emailInValid
:
'请输入有效的邮箱地址'
,
nameEmpty
:
'用户名不能为空'
,
passwordEmpty
:
'密码不能为空'
,
passwordInvalid
:
'密码必须包含字母和数字,且长度不小于8位'
,
},
license
:
{
tip
:
'启动 Dify 社区版之前, 请阅读 GitHub 上的'
,
link
:
'开源协议'
,
},
join
:
'加入'
,
joinTipStart
:
'邀请你加入'
,
joinTipEnd
:
'团队'
,
invalid
:
'链接已失效'
,
explore
:
'探索 Dify'
,
activatedTipStart
:
'您已加入'
,
activatedTipEnd
:
'团队'
,
activated
:
'现在登录'
,
}
export
default
translation
web/i18n/lang/share-app.en.ts
View file @
6040da75
...
...
@@ -41,6 +41,7 @@ const translation = {
},
title
:
'AI Completion'
,
queryTitle
:
'Query content'
,
completionResult
:
'Completion result'
,
queryPlaceholder
:
'Write your query content...'
,
run
:
'Execute'
,
copy
:
'Copy'
,
...
...
web/i18n/lang/share-app.zh.ts
View file @
6040da75
...
...
@@ -37,6 +37,7 @@ const translation = {
},
title
:
'AI 智能书写'
,
queryTitle
:
'查询内容'
,
completionResult
:
'生成结果'
,
queryPlaceholder
:
'请输入文本内容'
,
run
:
'运行'
,
copy
:
'拷贝'
,
...
...
web/models/common.ts
View file @
6040da75
...
...
@@ -10,6 +10,8 @@ export type UserProfileResponse = {
id
:
string
name
:
string
email
:
string
avatar
:
string
is_password_set
:
boolean
interface_language
?:
string
interface_theme
?:
string
timezone
?:
string
...
...
web/package.json
View file @
6040da75
{
"name"
:
"dify-web"
,
"version"
:
"0.3.
7
"
,
"version"
:
"0.3.
8
"
,
"private"
:
true
,
"scripts"
:
{
"dev"
:
"next dev"
,
...
...
@@ -109,4 +109,4 @@
"eslint --fix"
]
}
}
\ No newline at end of file
}
web/service/base.ts
View file @
6040da75
...
...
@@ -334,13 +334,16 @@ export const ssePost = (url: string, fetchOptions: any, { isPublicAPI = false, o
return
handleStream
(
res
,
(
str
:
string
,
isFirstMessage
:
boolean
,
moreInfo
:
IOnDataMoreInfo
)
=>
{
if
(
moreInfo
.
errorMessage
)
{
onError
?.(
moreInfo
.
errorMessage
)
Toast
.
notify
({
type
:
'error'
,
message
:
moreInfo
.
errorMessage
})
if
(
moreInfo
.
errorMessage
!==
'AbortError: The user aborted a request.'
)
Toast
.
notify
({
type
:
'error'
,
message
:
moreInfo
.
errorMessage
})
return
}
onData
?.(
str
,
isFirstMessage
,
moreInfo
)
},
onCompleted
)
}).
catch
((
e
)
=>
{
Toast
.
notify
({
type
:
'error'
,
message
:
e
})
if
(
e
.
toString
()
!==
'AbortError: The user aborted a request.'
)
Toast
.
notify
({
type
:
'error'
,
message
:
e
})
onError
?.(
e
)
})
}
...
...
web/service/common.ts
View file @
6040da75
...
...
@@ -66,8 +66,8 @@ export const fetchAccountIntegrates: Fetcher<{ data: AccountIntegrate[] | null }
return
get
(
url
,
{
params
})
as
Promise
<
{
data
:
AccountIntegrate
[]
|
null
}
>
}
export
const
inviteMember
:
Fetcher
<
CommonResponse
&
{
account
:
Member
},
{
url
:
string
;
body
:
Record
<
string
,
any
>
}
>
=
({
url
,
body
})
=>
{
return
post
(
url
,
{
body
})
as
Promise
<
CommonResponse
&
{
account
:
Member
}
>
export
const
inviteMember
:
Fetcher
<
CommonResponse
&
{
account
:
Member
;
invite_url
:
string
},
{
url
:
string
;
body
:
Record
<
string
,
any
>
}
>
=
({
url
,
body
})
=>
{
return
post
(
url
,
{
body
})
as
Promise
<
CommonResponse
&
{
account
:
Member
;
invite_url
:
string
}
>
}
export
const
updateMemberRole
:
Fetcher
<
CommonResponse
,
{
url
:
string
;
body
:
Record
<
string
,
any
>
}
>
=
({
url
,
body
})
=>
{
...
...
@@ -101,3 +101,11 @@ export const syncDataSourceNotion: Fetcher<CommonResponse, { url: string }> = ({
export
const
updateDataSourceNotionAction
:
Fetcher
<
CommonResponse
,
{
url
:
string
}
>
=
({
url
})
=>
{
return
patch
(
url
)
as
Promise
<
CommonResponse
>
}
export
const
invitationCheck
:
Fetcher
<
CommonResponse
&
{
is_valid
:
boolean
;
workspace_name
:
string
},
{
url
:
string
;
params
:
{
workspace_id
:
string
;
email
:
string
;
token
:
string
}
}
>
=
({
url
,
params
})
=>
{
return
get
(
url
,
{
params
})
as
Promise
<
CommonResponse
&
{
is_valid
:
boolean
;
workspace_name
:
string
}
>
}
export
const
activateMember
:
Fetcher
<
CommonResponse
,
{
url
:
string
;
body
:
any
}
>
=
({
url
,
body
})
=>
{
return
post
(
url
,
{
body
})
as
Promise
<
CommonResponse
>
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment