mirror of
https://github.com/langgenius/dify.git
synced 2026-01-07 23:04:12 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
216fc5d312 | ||
|
|
7a8590980e | ||
|
|
e8c14bb732 | ||
|
|
bf45f08e78 | ||
|
|
2c77a74c40 | ||
|
|
440cf63317 | ||
|
|
50b11e925b | ||
|
|
7cc81b4269 | ||
|
|
93b0813b73 | ||
|
|
649b44aefa | ||
|
|
1e95d74ae2 | ||
|
|
700d5f2673 |
20
README.md
20
README.md
@@ -19,15 +19,23 @@ Visual data analysis, log review, and annotation for applications
|
||||
## Highlighted Features
|
||||
**1. LLMs support:** Choose capabilities based on different models when building your Dify AI apps. Dify is compatible with Langchain, meaning it will support various LLMs. Currently supported:
|
||||
|
||||
>* OpenAI: GPT-4, GPT-3.5-turbo, GPT-3.5-turbo-16k, text-davinci-003
|
||||
>* Azure OpenAI Service
|
||||
>* Anthropic: Claude2, Claude-instant
|
||||
>* Hugging Face Hub (coming soon)
|
||||
- [x] **OpenAI**: GPT4, GPT3.5-turbo, GPT3.5-turbo-16k, text-davinci-003
|
||||
- [x] **Azure OpenAI Service**
|
||||
- [x] **Anthropic**: Claude2, Claude-instant
|
||||
- [x] **Replicate**
|
||||
- [x] **Hugging Face Hub**
|
||||
- [x] **MiniMax**
|
||||
- [x] **Spark**
|
||||
- [x] **Wenxin**
|
||||
- [x] **Tongyi**
|
||||
- [x] **ChatGLM**
|
||||
|
||||
We provide the following free resources for registered Dify cloud users (sign up at [dify.ai](https://dify.ai)):
|
||||
* 1000 free Claude model queries to build Claude-powered apps
|
||||
* 200 free OpenAI queries to build OpenAI-based apps
|
||||
|
||||
* 3 million Xunfei Spark Tokens are provided for creating AI applications based on Spark.
|
||||
* 1 million MiniMax Tokens are provided for creating AI applications based on the MiniMax.
|
||||
|
||||
**2. Visual orchestration:** Build an AI app in minutes by writing and debugging prompts visually.
|
||||
|
||||
**3. Text embedding:** Fully automated text preprocessing embeds your data as context without complex concepts. Supports PDF, TXT, and syncing data from Notion, webpages, APIs.
|
||||
@@ -55,7 +63,7 @@ Visit [Dify.ai](https://dify.ai)
|
||||
|
||||
Before installing Dify, make sure your machine meets the following minimum system requirements:
|
||||
|
||||
- CPU >= 1 Core
|
||||
- CPU >= 2 Core
|
||||
- RAM >= 4GB
|
||||
|
||||
### Quick Start
|
||||
|
||||
18
README_CN.md
18
README_CN.md
@@ -22,14 +22,22 @@
|
||||
## 核心能力
|
||||
1. **模型支持:** 你可以在 Dify 上选择基于不同模型的能力来开发你的 AI 应用。Dify 兼容 Langchain,这意味着我们将逐步支持多种 LLMs ,目前支持的模型供应商:
|
||||
|
||||
> * **OpenAI**:GPT4、GPT3.5-turbo、GPT3.5-turbo-16k、text-davinci-003
|
||||
> * **Azure OpenAI Service**
|
||||
> * **Anthropic**:Claude2、Claude-instant
|
||||
> * **Hugging Face Hub**(即将推出)
|
||||
- [x] **OpenAI**:GPT4、GPT3.5-turbo、GPT3.5-turbo-16k、text-davinci-003
|
||||
- [x] **Azure OpenAI Service**
|
||||
- [x] **Anthropic**:Claude2、Claude-instant
|
||||
- [x] **Replicate**
|
||||
- [x] **Hugging Face Hub**
|
||||
- [x] **MiniMax**
|
||||
- [x] **讯飞星火大模型**
|
||||
- [x] **文心一言**
|
||||
- [x] **通义千问**
|
||||
- [x] **ChatGLM**
|
||||
|
||||
我们为所有注册云端版的用户免费提供以下资源(登录 [dify.ai](https://cloud.dify.ai) 即可使用):
|
||||
* 1000 次 Claude 模型的消息调用额度,用于创建基于 Claude 模型的 AI 应用
|
||||
* 200 次 OpenAI 模型的消息调用额度,用于创建基于 OpenAI 模型的 AI 应用
|
||||
* 300 万 讯飞星火大模型 Token 的调用额度,用于创建基于讯飞星火大模型的 AI 应用
|
||||
* 100 万 MiniMax Token 的调用额度,用于创建基于 MiniMax 模型的 AI 应用
|
||||
2. **可视化编排 Prompt:** 通过界面化编写 prompt 并调试,只需几分钟即可发布一个 AI 应用。
|
||||
3. **文本 Embedding 处理(数据集)**:全自动完成文本预处理,使用你的数据作为上下文,无需理解晦涩的概念和技术处理。支持 PDF、txt 等文件格式,支持从 Notion、网页、API 同步数据。
|
||||
4. **基于 API 开发:** 后端即服务。您可以直接访问网页应用,也可以接入 API 集成到您的应用中,无需关注复杂的后端架构和部署过程。
|
||||
@@ -53,7 +61,7 @@
|
||||
|
||||
在安装 Dify 之前,请确保您的机器满足以下最低系统要求:
|
||||
|
||||
- CPU >= 1 Core
|
||||
- CPU >= 2 Core
|
||||
- RAM >= 4GB
|
||||
|
||||
### 快速启动
|
||||
|
||||
@@ -32,7 +32,7 @@ Visita [Dify.ai](https://dify.ai)
|
||||
|
||||
Antes de instalar Dify, asegúrate de que tu máquina cumple con los siguientes requisitos mínimos del sistema:
|
||||
|
||||
- CPU >= 1 Core
|
||||
- CPU >= 2 Core
|
||||
- RAM >= 4GB
|
||||
|
||||
### Inicio rápido
|
||||
|
||||
@@ -98,7 +98,7 @@ class Config:
|
||||
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.13"
|
||||
self.CURRENT_VERSION = "0.3.14"
|
||||
self.COMMIT_SHA = get_env('COMMIT_SHA')
|
||||
self.EDITION = "SELF_HOSTED"
|
||||
self.DEPLOY_ENV = get_env('DEPLOY_ENV')
|
||||
|
||||
@@ -397,29 +397,6 @@ class AppApiStatus(Resource):
|
||||
return app
|
||||
|
||||
|
||||
class AppRateLimit(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@marshal_with(app_detail_fields)
|
||||
def post(self, app_id):
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument('api_rpm', type=inputs.natural, required=False, location='json')
|
||||
parser.add_argument('api_rph', type=inputs.natural, required=False, location='json')
|
||||
args = parser.parse_args()
|
||||
|
||||
app_id = str(app_id)
|
||||
app = _get_app(app_id, current_user.current_tenant_id)
|
||||
|
||||
if args.get('api_rpm'):
|
||||
app.api_rpm = args.get('api_rpm')
|
||||
if args.get('api_rph'):
|
||||
app.api_rph = args.get('api_rph')
|
||||
app.updated_at = datetime.utcnow()
|
||||
db.session.commit()
|
||||
return app
|
||||
|
||||
|
||||
class AppCopy(Resource):
|
||||
@staticmethod
|
||||
def create_app_copy(app):
|
||||
@@ -482,16 +459,6 @@ class AppCopy(Resource):
|
||||
return copy_app, 201
|
||||
|
||||
|
||||
class AppExport(Resource):
|
||||
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
def post(self, app_id):
|
||||
# todo
|
||||
pass
|
||||
|
||||
|
||||
api.add_resource(AppListApi, '/apps')
|
||||
api.add_resource(AppTemplateApi, '/app-templates')
|
||||
api.add_resource(AppApi, '/apps/<uuid:app_id>')
|
||||
@@ -500,4 +467,3 @@ api.add_resource(AppNameApi, '/apps/<uuid:app_id>/name')
|
||||
api.add_resource(AppIconApi, '/apps/<uuid:app_id>/icon')
|
||||
api.add_resource(AppSiteStatus, '/apps/<uuid:app_id>/site-enable')
|
||||
api.add_resource(AppApiStatus, '/apps/<uuid:app_id>/api-enable')
|
||||
api.add_resource(AppRateLimit, '/apps/<uuid:app_id>/rate-limit')
|
||||
|
||||
@@ -80,6 +80,13 @@ class AppSite(Resource):
|
||||
if value is not None:
|
||||
setattr(site, attr_name, value)
|
||||
|
||||
if attr_name == 'title':
|
||||
app_model.name = value
|
||||
elif attr_name == 'icon':
|
||||
app_model.icon = value
|
||||
elif attr_name == 'icon_background':
|
||||
app_model.icon_background = value
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return site
|
||||
|
||||
@@ -10,7 +10,7 @@ from langchain.schema import AgentAction, AgentFinish, OutputParserException
|
||||
class StructuredChatOutputParser(LCStructuredChatOutputParser):
|
||||
def parse(self, text: str) -> Union[AgentAction, AgentFinish]:
|
||||
try:
|
||||
action_match = re.search(r"```(.*?)\n(.*?)```?", text, re.DOTALL)
|
||||
action_match = re.search(r"```(.*?)\n?(.*?)```", text, re.DOTALL)
|
||||
if action_match is not None:
|
||||
response = json.loads(action_match.group(2).strip(), strict=False)
|
||||
if isinstance(response, list):
|
||||
|
||||
@@ -72,7 +72,7 @@ class OrchestratorRuleParser:
|
||||
|
||||
# only OpenAI chat model (include Azure) support function call, use ReACT instead
|
||||
if agent_model_instance.model_mode != ModelMode.CHAT \
|
||||
or agent_model_instance.name not in ['openai', 'azure_openai']:
|
||||
or agent_model_instance.model_provider.provider_name not in ['openai', 'azure_openai']:
|
||||
if planning_strategy in [PlanningStrategy.FUNCTION_CALL, PlanningStrategy.MULTI_FUNCTION_CALL]:
|
||||
planning_strategy = PlanningStrategy.REACT
|
||||
elif planning_strategy == PlanningStrategy.ROUTER:
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
from flask_login import current_user
|
||||
from extensions.ext_database import db
|
||||
from models.account import Tenant
|
||||
from models.account import Tenant, TenantAccountJoin
|
||||
from models.provider import Provider
|
||||
|
||||
|
||||
class WorkspaceService:
|
||||
@classmethod
|
||||
def get_tenant_info(cls, tenant: Tenant):
|
||||
if not tenant:
|
||||
return None
|
||||
tenant_info = {
|
||||
'id': tenant.id,
|
||||
'name': tenant.name,
|
||||
@@ -13,10 +16,18 @@ class WorkspaceService:
|
||||
'status': tenant.status,
|
||||
'created_at': tenant.created_at,
|
||||
'providers': [],
|
||||
'in_trial': True,
|
||||
'trial_end_reason': None
|
||||
'in_trail': True,
|
||||
'trial_end_reason': None,
|
||||
'role': 'normal',
|
||||
}
|
||||
|
||||
# Get role of user
|
||||
tenant_account_join = db.session.query(TenantAccountJoin).filter(
|
||||
TenantAccountJoin.tenant_id == tenant.id,
|
||||
TenantAccountJoin.account_id == current_user.id
|
||||
).first()
|
||||
tenant_info['role'] = tenant_account_join.role
|
||||
|
||||
# Get providers
|
||||
providers = db.session.query(Provider).filter(
|
||||
Provider.tenant_id == tenant.id
|
||||
|
||||
@@ -2,7 +2,7 @@ version: '3.1'
|
||||
services:
|
||||
# API service
|
||||
api:
|
||||
image: langgenius/dify-api:0.3.13
|
||||
image: langgenius/dify-api:0.3.14
|
||||
restart: always
|
||||
environment:
|
||||
# Startup mode, 'api' starts the API server.
|
||||
@@ -124,7 +124,7 @@ services:
|
||||
# worker service
|
||||
# The Celery worker for processing the queue.
|
||||
worker:
|
||||
image: langgenius/dify-api:0.3.13
|
||||
image: langgenius/dify-api:0.3.14
|
||||
restart: always
|
||||
environment:
|
||||
# Startup mode, 'worker' starts the Celery worker for processing the queue.
|
||||
@@ -176,7 +176,7 @@ services:
|
||||
|
||||
# Frontend web application.
|
||||
web:
|
||||
image: langgenius/dify-web:0.3.13
|
||||
image: langgenius/dify-web:0.3.14
|
||||
restart: always
|
||||
environment:
|
||||
EDITION: SELF_HOSTED
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect } from 'react'
|
||||
import React, { useEffect, useMemo } from 'react'
|
||||
import cn from 'classnames'
|
||||
import useSWR from 'swr'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
import s from './style.module.css'
|
||||
import AppSideBar from '@/app/components/app-sidebar'
|
||||
import { fetchAppDetail } from '@/service/apps'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
|
||||
export type IAppDetailLayoutProps = {
|
||||
children: React.ReactNode
|
||||
@@ -31,15 +32,21 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
||||
params: { appId }, // get appId in path
|
||||
} = props
|
||||
const { t } = useTranslation()
|
||||
const { isCurrentWorkspaceManager } = useAppContext()
|
||||
const detailParams = { url: '/apps', id: appId }
|
||||
const { data: response } = useSWR(detailParams, fetchAppDetail)
|
||||
|
||||
const navigation = [
|
||||
{ name: t('common.appMenus.overview'), href: `/app/${appId}/overview`, icon: ChartBarSquareIcon, selectedIcon: ChartBarSquareSolidIcon },
|
||||
{ name: t('common.appMenus.promptEng'), href: `/app/${appId}/configuration`, icon: Cog8ToothIcon, selectedIcon: Cog8ToothSolidIcon },
|
||||
{ name: t('common.appMenus.apiAccess'), href: `/app/${appId}/develop`, icon: CommandLineIcon, selectedIcon: CommandLineSolidIcon },
|
||||
{ name: t('common.appMenus.logAndAnn'), href: `/app/${appId}/logs`, icon: DocumentTextIcon, selectedIcon: DocumentTextSolidIcon },
|
||||
]
|
||||
const navigation = useMemo(() => {
|
||||
const navs = [
|
||||
{ name: t('common.appMenus.overview'), href: `/app/${appId}/overview`, icon: ChartBarSquareIcon, selectedIcon: ChartBarSquareSolidIcon },
|
||||
{ name: t('common.appMenus.apiAccess'), href: `/app/${appId}/develop`, icon: CommandLineIcon, selectedIcon: CommandLineSolidIcon },
|
||||
{ name: t('common.appMenus.logAndAnn'), href: `/app/${appId}/logs`, icon: DocumentTextIcon, selectedIcon: DocumentTextSolidIcon },
|
||||
]
|
||||
if (isCurrentWorkspaceManager)
|
||||
navs.push({ name: t('common.appMenus.promptEng'), href: `/app/${appId}/configuration`, icon: Cog8ToothIcon, selectedIcon: Cog8ToothSolidIcon })
|
||||
return navs
|
||||
}, [appId, isCurrentWorkspaceManager, t])
|
||||
|
||||
const appModeName = response?.mode?.toUpperCase() === 'COMPLETION' ? t('common.appModes.completionApp') : t('common.appModes.chatApp')
|
||||
useEffect(() => {
|
||||
if (response?.name)
|
||||
|
||||
@@ -12,7 +12,7 @@ import Confirm from '@/app/components/base/confirm'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { deleteApp } from '@/service/apps'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import AppsContext from '@/context/app-context'
|
||||
import AppsContext, { useAppContext } from '@/context/app-context'
|
||||
|
||||
export type AppCardProps = {
|
||||
app: App
|
||||
@@ -25,6 +25,7 @@ const AppCard = ({
|
||||
}: AppCardProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const { isCurrentWorkspaceManager } = useAppContext()
|
||||
|
||||
const mutateApps = useContextSelector(AppsContext, state => state.mutateApps)
|
||||
|
||||
@@ -55,7 +56,8 @@ const AppCard = ({
|
||||
<div className={style.listItemHeading}>
|
||||
<div className={style.listItemHeadingContent}>{app.name}</div>
|
||||
</div>
|
||||
<span className={style.deleteAppIcon} onClick={onDeleteClick} />
|
||||
{ isCurrentWorkspaceManager
|
||||
&& <span className={style.deleteAppIcon} onClick={onDeleteClick} />}
|
||||
</div>
|
||||
<div className={style.listItemDescription}>{app.model_config?.pre_prompt}</div>
|
||||
<div className={style.listItemFooter}>
|
||||
|
||||
@@ -8,7 +8,7 @@ import AppCard from './AppCard'
|
||||
import NewAppCard from './NewAppCard'
|
||||
import type { AppListResponse } from '@/models/app'
|
||||
import { fetchAppList } from '@/service/apps'
|
||||
import { useSelector } from '@/context/app-context'
|
||||
import { useAppContext, useSelector } from '@/context/app-context'
|
||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
|
||||
const getKey = (pageIndex: number, previousPageData: AppListResponse) => {
|
||||
@@ -19,6 +19,7 @@ const getKey = (pageIndex: number, previousPageData: AppListResponse) => {
|
||||
|
||||
const Apps = () => {
|
||||
const { t } = useTranslation()
|
||||
const { isCurrentWorkspaceManager } = useAppContext()
|
||||
const { data, isLoading, setSize, mutate } = useSWRInfinite(getKey, fetchAppList, { revalidateFirstPage: false })
|
||||
const loadingStateRef = useRef(false)
|
||||
const pageContainerRef = useSelector(state => state.pageContainerRef)
|
||||
@@ -55,7 +56,8 @@ const Apps = () => {
|
||||
{data?.map(({ data: apps }) => apps.map(app => (
|
||||
<AppCard key={app.id} app={app} onDelete={mutate} />
|
||||
)))}
|
||||
<NewAppCard ref={anchorRef} onSuccess={mutate} />
|
||||
{ isCurrentWorkspaceManager
|
||||
&& <NewAppCard ref={anchorRef} onSuccess={mutate} />}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import NewDatasetCard from './NewDatasetCard'
|
||||
import DatasetCard from './DatasetCard'
|
||||
import type { DataSetListResponse } from '@/models/datasets'
|
||||
import { fetchDatasets } from '@/service/datasets'
|
||||
import { useSelector } from '@/context/app-context'
|
||||
import { useAppContext, useSelector } from '@/context/app-context'
|
||||
|
||||
const getKey = (pageIndex: number, previousPageData: DataSetListResponse) => {
|
||||
if (!pageIndex || previousPageData.has_more)
|
||||
@@ -16,6 +16,7 @@ const getKey = (pageIndex: number, previousPageData: DataSetListResponse) => {
|
||||
}
|
||||
|
||||
const Datasets = () => {
|
||||
const { isCurrentWorkspaceManager } = useAppContext()
|
||||
const { data, isLoading, setSize, mutate } = useSWRInfinite(getKey, fetchDatasets, { revalidateFirstPage: false })
|
||||
const loadingStateRef = useRef(false)
|
||||
const pageContainerRef = useSelector(state => state.pageContainerRef)
|
||||
@@ -44,7 +45,7 @@ const Datasets = () => {
|
||||
{data?.map(({ data: datasets }) => datasets.map(dataset => (
|
||||
<DatasetCard key={dataset.id} dataset={dataset} onDelete={mutate} />),
|
||||
))}
|
||||
<NewDatasetCard ref={anchorRef} />
|
||||
{ isCurrentWorkspaceManager && <NewDatasetCard ref={anchorRef} /> }
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,30 +1,32 @@
|
||||
'use client'
|
||||
import React, { FC, useState, useEffect } from 'react'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import ModalFoot from '../modal-foot'
|
||||
import ConfigSelect, { Options } from '../config-select'
|
||||
import type { Options } from '../config-select'
|
||||
import ConfigSelect from '../config-select'
|
||||
import ConfigString from '../config-string'
|
||||
import SelectTypeItem from '../select-type-item'
|
||||
import s from './style.module.css'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import type { PromptVariable } from '@/models/debug'
|
||||
import SelectTypeItem from '../select-type-item'
|
||||
import { getNewVar } from '@/utils/var'
|
||||
|
||||
import s from './style.module.css'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
|
||||
export interface IConfigModalProps {
|
||||
export type IConfigModalProps = {
|
||||
payload: PromptVariable
|
||||
type?: string
|
||||
isShow: boolean
|
||||
onClose: () => void
|
||||
onConfirm: (newValue: { type: string, value: any }) => void
|
||||
onConfirm: (newValue: { type: string; value: any }) => void
|
||||
}
|
||||
|
||||
const ConfigModal: FC<IConfigModalProps> = ({
|
||||
payload,
|
||||
isShow,
|
||||
onClose,
|
||||
onConfirm
|
||||
onConfirm,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { type, name, key, options, max_length } = payload || getNewVar('')
|
||||
@@ -42,7 +44,7 @@ const ConfigModal: FC<IConfigModalProps> = ({
|
||||
const isStringInput = tempType === 'string'
|
||||
const title = isStringInput ? t('appDebug.variableConig.maxLength') : t('appDebug.variableConig.options')
|
||||
|
||||
// string type
|
||||
// string type
|
||||
const [tempMaxLength, setTempMaxValue] = useState(max_length)
|
||||
useEffect(() => {
|
||||
setTempMaxValue(max_length)
|
||||
@@ -57,14 +59,15 @@ const ConfigModal: FC<IConfigModalProps> = ({
|
||||
const handleConfirm = () => {
|
||||
if (isStringInput) {
|
||||
onConfirm({ type: tempType, value: tempMaxLength })
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
if (tempOptions.length === 0) {
|
||||
Toast.notify({ type: 'error', message: 'At least one option requied' })
|
||||
return
|
||||
}
|
||||
const obj: Record<string, boolean> = {}
|
||||
let hasRepeatedItem = false
|
||||
tempOptions.forEach(o => {
|
||||
tempOptions.forEach((o) => {
|
||||
if (obj[o]) {
|
||||
hasRepeatedItem = true
|
||||
return
|
||||
@@ -97,11 +100,13 @@ const ConfigModal: FC<IConfigModalProps> = ({
|
||||
|
||||
<div className='mt-6'>
|
||||
<div className={s.title}>{title}</div>
|
||||
{isStringInput ? (
|
||||
<ConfigString value={tempMaxLength} onChange={setTempMaxValue} />
|
||||
) : (
|
||||
<ConfigSelect options={tempOptions} onChange={setTempOptions} />
|
||||
)}
|
||||
{isStringInput
|
||||
? (
|
||||
<ConfigString value={tempMaxLength} onChange={setTempMaxValue} />
|
||||
)
|
||||
: (
|
||||
<ConfigSelect options={tempOptions} onChange={setTempOptions} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
'use client'
|
||||
import React, { FC, } from 'react'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
|
||||
export interface IConfigStringProps {
|
||||
export type IConfigStringProps = {
|
||||
value: number | undefined
|
||||
onChange: (value: number | undefined) => void
|
||||
}
|
||||
|
||||
const MAX_LENGTH = 64
|
||||
const MAX_LENGTH = 256
|
||||
|
||||
const ConfigString: FC<IConfigStringProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
@@ -20,13 +20,8 @@ const ConfigString: FC<IConfigStringProps> = ({
|
||||
max={MAX_LENGTH}
|
||||
min={1}
|
||||
value={value || ''}
|
||||
onChange={e => {
|
||||
let value = parseInt(e.target.value, 10)
|
||||
if (value > MAX_LENGTH) {
|
||||
value = MAX_LENGTH
|
||||
} else if (value < 1) {
|
||||
value = 1
|
||||
}
|
||||
onChange={(e) => {
|
||||
const value = Math.max(1, Math.min(MAX_LENGTH, parseInt(e.target.value))) || 1
|
||||
onChange(value)
|
||||
}}
|
||||
className="w-full px-3 text-sm leading-9 text-gray-900 border-0 rounded-lg grow h-9 bg-gray-50 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200"
|
||||
|
||||
@@ -4,6 +4,7 @@ import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Cog8ToothIcon, TrashIcon } from '@heroicons/react/24/outline'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import type { Timeout } from 'ahooks/lib/useRequest/src/types'
|
||||
import Panel from '../base/feature-panel'
|
||||
import OperationBtn from '../base/operation-btn'
|
||||
import VarIcon from '../base/icons/var-icon'
|
||||
@@ -16,7 +17,6 @@ import { DEFAULT_VALUE_MAX_LEN, getMaxVarNameLength } from '@/config'
|
||||
import { checkKeys, getNewVar } from '@/utils/var'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { Timeout } from 'ahooks/lib/useRequest/src/types'
|
||||
|
||||
export type IConfigVarProps = {
|
||||
promptVariables: PromptVariable[]
|
||||
@@ -37,9 +37,9 @@ const ConfigVar: FC<IConfigVarProps> = ({ promptVariables, readonly, onPromptVar
|
||||
return obj
|
||||
})()
|
||||
|
||||
const updatePromptVariable = (index: number, updateKey: string, newValue: any) => {
|
||||
const updatePromptVariable = (key: string, updateKey: string, newValue: any) => {
|
||||
const newPromptVariables = promptVariables.map((item, i) => {
|
||||
if (i === index) {
|
||||
if (item.key === key) {
|
||||
return {
|
||||
...item,
|
||||
[updateKey]: newValue,
|
||||
@@ -48,13 +48,12 @@ const ConfigVar: FC<IConfigVarProps> = ({ promptVariables, readonly, onPromptVar
|
||||
|
||||
return item
|
||||
})
|
||||
|
||||
onPromptVariablesChange?.(newPromptVariables)
|
||||
}
|
||||
|
||||
const batchUpdatePromptVariable = (index: number, updateKeys: string[], newValues: any[]) => {
|
||||
const newPromptVariables = promptVariables.map((item, i) => {
|
||||
if (i === index) {
|
||||
const batchUpdatePromptVariable = (key: string, updateKeys: string[], newValues: any[]) => {
|
||||
const newPromptVariables = promptVariables.map((item) => {
|
||||
if (item.key === key) {
|
||||
const newItem: any = { ...item }
|
||||
updateKeys.forEach((updateKey, i) => {
|
||||
newItem[updateKey] = newValues[i]
|
||||
@@ -93,11 +92,10 @@ const ConfigVar: FC<IConfigVarProps> = ({ promptVariables, readonly, onPromptVar
|
||||
if (isKeyExists) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t(`appDebug.varKeyError.keyAlreadyExists`, { key: newKey }),
|
||||
message: t('appDebug.varKeyError.keyAlreadyExists', { key: newKey }),
|
||||
})
|
||||
return
|
||||
}
|
||||
},1000)
|
||||
}, 1000)
|
||||
|
||||
onPromptVariablesChange?.(newPromptVariables)
|
||||
}
|
||||
@@ -206,7 +204,7 @@ const ConfigVar: FC<IConfigVarProps> = ({ promptVariables, readonly, onPromptVar
|
||||
type="text"
|
||||
placeholder={key}
|
||||
value={name}
|
||||
onChange={e => updatePromptVariable(index, 'name', e.target.value)}
|
||||
onChange={e => updatePromptVariable(key, 'name', e.target.value)}
|
||||
maxLength={getMaxVarNameLength(name)}
|
||||
className="h-6 leading-6 block w-full rounded-md border-0 py-1.5 text-gray-900 placeholder:text-gray-400 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200"
|
||||
/>)
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { FC } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import Progress from './progress'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { LinkExternal02, XClose } from '@/app/components/base/icons/src/vender/line/general'
|
||||
@@ -10,9 +11,13 @@ import AccountSetting from '@/app/components/header/account-setting'
|
||||
import { IS_CE_EDITION } from '@/config'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
import I18n from '@/context/i18n'
|
||||
import ProviderConfig from '@/app/components/header/account-setting/model-page/configs'
|
||||
|
||||
const APIKeyInfoPanel: FC = () => {
|
||||
const isCloud = !IS_CE_EDITION
|
||||
const { locale } = useContext(I18n)
|
||||
|
||||
const { textGenerationModelList } = useProviderContext()
|
||||
|
||||
const { t } = useTranslation()
|
||||
@@ -36,7 +41,8 @@ const APIKeyInfoPanel: FC = () => {
|
||||
// first show in trail and not used exhausted, else find the exhausted
|
||||
const [used, total, unit, providerName] = (() => {
|
||||
if (!textGenerationModelList || !isCloud)
|
||||
return [0, 0, '']
|
||||
return [0, 0, '', '']
|
||||
|
||||
let used = 0
|
||||
let total = 0
|
||||
let unit = 'times'
|
||||
@@ -69,7 +75,7 @@ const APIKeyInfoPanel: FC = () => {
|
||||
{isCloud && <em-emoji id={exhausted ? '🤔' : '😀'} />}
|
||||
{isCloud
|
||||
? (
|
||||
<div>{t(`appOverview.apiKeyInfo.cloud.${exhausted ? 'exhausted' : 'trial'}.title`, { providerName })}</div>
|
||||
<div>{t(`appOverview.apiKeyInfo.cloud.${exhausted ? 'exhausted' : 'trial'}.title`, { providerName: (ProviderConfig as any)[providerName as string]?.selector?.name[locale] || providerName })}</div>
|
||||
)
|
||||
: (
|
||||
<div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import {
|
||||
Cog8ToothIcon,
|
||||
DocumentTextIcon,
|
||||
@@ -22,6 +22,7 @@ import Switch from '@/app/components/base/switch'
|
||||
import type { AppDetailResponse } from '@/models/app'
|
||||
import './style.css'
|
||||
import { AppType } from '@/types/app'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
|
||||
export type IAppCardProps = {
|
||||
className?: string
|
||||
@@ -48,22 +49,30 @@ function AppCard({
|
||||
}: IAppCardProps) {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const { currentWorkspace, isCurrentWorkspaceManager } = useAppContext()
|
||||
const [showSettingsModal, setShowSettingsModal] = useState(false)
|
||||
const [showShareModal, setShowShareModal] = useState(false)
|
||||
const [showEmbedded, setShowEmbedded] = useState(false)
|
||||
const [showCustomizeModal, setShowCustomizeModal] = useState(false)
|
||||
const { t } = useTranslation()
|
||||
|
||||
const OPERATIONS_MAP = {
|
||||
webapp: [
|
||||
{ opName: t('appOverview.overview.appInfo.preview'), opIcon: RocketLaunchIcon },
|
||||
{ opName: t('appOverview.overview.appInfo.share.entry'), opIcon: ShareIcon },
|
||||
appInfo.mode === AppType.chat ? { opName: t('appOverview.overview.appInfo.embedded.entry'), opIcon: EmbedIcon } : false,
|
||||
{ opName: t('appOverview.overview.appInfo.settings.entry'), opIcon: Cog8ToothIcon },
|
||||
].filter(item => !!item),
|
||||
api: [{ opName: t('appOverview.overview.apiInfo.doc'), opIcon: DocumentTextIcon }],
|
||||
app: [],
|
||||
}
|
||||
const OPERATIONS_MAP = useMemo(() => {
|
||||
const operationsMap = {
|
||||
webapp: [
|
||||
{ opName: t('appOverview.overview.appInfo.preview'), opIcon: RocketLaunchIcon },
|
||||
{ opName: t('appOverview.overview.appInfo.share.entry'), opIcon: ShareIcon },
|
||||
] as { opName: string; opIcon: any }[],
|
||||
api: [{ opName: t('appOverview.overview.apiInfo.doc'), opIcon: DocumentTextIcon }],
|
||||
app: [],
|
||||
}
|
||||
if (appInfo.mode === AppType.chat)
|
||||
operationsMap.webapp.push({ opName: t('appOverview.overview.appInfo.embedded.entry'), opIcon: EmbedIcon })
|
||||
|
||||
if (isCurrentWorkspaceManager)
|
||||
operationsMap.webapp.push({ opName: t('appOverview.overview.appInfo.settings.entry'), opIcon: Cog8ToothIcon })
|
||||
|
||||
return operationsMap
|
||||
}, [isCurrentWorkspaceManager, appInfo, t])
|
||||
|
||||
const isApp = cardType === 'app' || cardType === 'webapp'
|
||||
const basicName = isApp ? appInfo?.site?.title : t('appOverview.overview.apiInfo.title')
|
||||
@@ -129,7 +138,7 @@ function AppCard({
|
||||
<Tag className="mr-2" color={runningStatus ? 'green' : 'yellow'}>
|
||||
{runningStatus ? t('appOverview.overview.status.running') : t('appOverview.overview.status.disable')}
|
||||
</Tag>
|
||||
<Switch defaultValue={runningStatus} onChange={onChangeStatus} />
|
||||
<Switch defaultValue={runningStatus} onChange={onChangeStatus} disabled={currentWorkspace?.role === 'normal'} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col justify-center py-2">
|
||||
@@ -200,6 +209,7 @@ function AppCard({
|
||||
onClose={() => setShowShareModal(false)}
|
||||
linkUrl={appUrl}
|
||||
onGenerateCode={onGenerateCode}
|
||||
regeneratable={isCurrentWorkspaceManager}
|
||||
/>
|
||||
<SettingsModal
|
||||
appInfo={appInfo}
|
||||
|
||||
@@ -17,6 +17,7 @@ type IShareLinkProps = {
|
||||
onClose: () => void
|
||||
onGenerateCode: () => Promise<void>
|
||||
linkUrl: string
|
||||
regeneratable?: boolean
|
||||
}
|
||||
|
||||
const prefixShare = 'appOverview.overview.appInfo.share'
|
||||
@@ -26,6 +27,7 @@ const ShareLinkModal: FC<IShareLinkProps> = ({
|
||||
isShow,
|
||||
onClose,
|
||||
onGenerateCode,
|
||||
regeneratable,
|
||||
}) => {
|
||||
const [genLoading, setGenLoading] = useState(false)
|
||||
const [isCopied, setIsCopied] = useState(false)
|
||||
@@ -51,7 +53,7 @@ const ShareLinkModal: FC<IShareLinkProps> = ({
|
||||
<LinkIcon className='w-4 h-4 mr-2' />
|
||||
{ t(`${prefixShare}.${isCopied ? 'linkCopied' : 'copyLink'}`) }
|
||||
</Button>
|
||||
<Button className='w-32 !px-0' onClick={async () => {
|
||||
{regeneratable && <Button className='w-32 !px-0' onClick={async () => {
|
||||
setGenLoading(true)
|
||||
await onGenerateCode()
|
||||
setGenLoading(false)
|
||||
@@ -59,7 +61,7 @@ const ShareLinkModal: FC<IShareLinkProps> = ({
|
||||
}}>
|
||||
<ArrowPathIcon className={`w-4 h-4 mr-2 ${genLoading ? 'generateLogo' : ''}`} />
|
||||
{t(`${prefixShare}.regenerate`)}
|
||||
</Button>
|
||||
</Button>}
|
||||
</div>
|
||||
</Modal>
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import Tooltip from '@/app/components/base/tooltip'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import I18n from '@/context/i18n'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
|
||||
type ISecretKeyModalProps = {
|
||||
isShow: boolean
|
||||
@@ -31,6 +32,7 @@ const SecretKeyModal = ({
|
||||
onClose,
|
||||
}: ISecretKeyModalProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { currentWorkspace, isCurrentWorkspaceManager } = useAppContext()
|
||||
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
|
||||
const [isVisible, setVisible] = useState(false)
|
||||
const [newKey, setNewKey] = useState<CreateApiKeyResponse | undefined>(undefined)
|
||||
@@ -118,11 +120,13 @@ const SecretKeyModal = ({
|
||||
setCopyValue(api.token)
|
||||
}}></div>
|
||||
</Tooltip>
|
||||
<div className={`flex items-center justify-center flex-shrink-0 w-6 h-6 rounded-lg cursor-pointer ${s.trashIcon}`} onClick={() => {
|
||||
setDelKeyId(api.id)
|
||||
setShowConfirmDelete(true)
|
||||
}}>
|
||||
</div>
|
||||
{ isCurrentWorkspaceManager
|
||||
&& <div className={`flex items-center justify-center flex-shrink-0 w-6 h-6 rounded-lg cursor-pointer ${s.trashIcon}`} onClick={() => {
|
||||
setDelKeyId(api.id)
|
||||
setShowConfirmDelete(true)
|
||||
}}>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -131,9 +135,7 @@ const SecretKeyModal = ({
|
||||
)
|
||||
}
|
||||
<div className='flex'>
|
||||
<Button type='default' className={`flex flex-shrink-0 mt-4 ${s.autoWidth}`} onClick={() =>
|
||||
onCreate()
|
||||
}>
|
||||
<Button type='default' className={`flex flex-shrink-0 mt-4 ${s.autoWidth}`} onClick={onCreate} disabled={ !currentWorkspace || currentWorkspace.role === 'normal'}>
|
||||
<PlusIcon className='flex flex-shrink-0 w-4 h-4' />
|
||||
<div className='text-xs font-medium text-gray-800'>{t('appApi.apiKeyModal.createNewSecretKey')}</div>
|
||||
</Button>
|
||||
|
||||
@@ -8,6 +8,7 @@ import s from './style.module.css'
|
||||
import NotionIcon from '@/app/components/base/notion-icon'
|
||||
import { apiPrefix } from '@/config'
|
||||
import type { DataSourceNotion as TDataSourceNotion } from '@/models/common'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
|
||||
type DataSourceNotionProps = {
|
||||
workspaces: TDataSourceNotion[]
|
||||
@@ -16,6 +17,8 @@ const DataSourceNotion = ({
|
||||
workspaces,
|
||||
}: DataSourceNotionProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { isCurrentWorkspaceManager } = useAppContext()
|
||||
|
||||
const connected = !!workspaces.length
|
||||
|
||||
return (
|
||||
@@ -35,18 +38,25 @@ const DataSourceNotion = ({
|
||||
}
|
||||
</div>
|
||||
{
|
||||
!connected
|
||||
connected
|
||||
? (
|
||||
<Link
|
||||
className='flex items-center ml-3 px-3 h-7 bg-white border border-gray-200 rounded-md text-xs font-medium text-gray-700 cursor-pointer'
|
||||
href={`${apiPrefix}/oauth/data-source/notion`}>
|
||||
className={
|
||||
`flex items-center ml-3 px-3 h-7 bg-white border border-gray-200
|
||||
rounded-md text-xs font-medium text-gray-700
|
||||
${isCurrentWorkspaceManager ? 'cursor-pointer' : 'grayscale opacity-50 cursor-default'}`
|
||||
}
|
||||
href={isCurrentWorkspaceManager ? `${apiPrefix}/oauth/data-source/notion` : '/'}>
|
||||
{t('common.dataSource.connect')}
|
||||
</Link>
|
||||
)
|
||||
: (
|
||||
<Link
|
||||
href={`${apiPrefix}/oauth/data-source/notion`}
|
||||
className='flex items-center px-3 h-7 bg-white border-[0.5px] border-gray-200 text-xs font-medium text-primary-600 rounded-md cursor-pointer'>
|
||||
href={isCurrentWorkspaceManager ? `${apiPrefix}/oauth/data-source/notion` : '/' }
|
||||
className={
|
||||
`flex items-center px-3 h-7 bg-white border-[0.5px] border-gray-200 text-xs font-medium text-primary-600 rounded-md
|
||||
${isCurrentWorkspaceManager ? 'cursor-pointer' : 'grayscale opacity-50 cursor-default'}`
|
||||
}>
|
||||
<PlusIcon className='w-[14px] h-[14px] mr-[5px]' />
|
||||
{t('common.dataSource.notion.addWorkspace')}
|
||||
</Link>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
.modal {
|
||||
max-width: 1024px !important;
|
||||
border-radius: 12px !important;
|
||||
margin-top: 60px;
|
||||
margin-bottom: 60px;
|
||||
padding: 0 !important;
|
||||
overflow-y: auto;
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import type { Status } from './declarations'
|
||||
type OperateProps = {
|
||||
isOpen: boolean
|
||||
status: Status
|
||||
disabled?: boolean
|
||||
onCancel: () => void
|
||||
onSave: () => void
|
||||
onAdd: () => void
|
||||
@@ -14,6 +15,7 @@ type OperateProps = {
|
||||
const Operate = ({
|
||||
isOpen,
|
||||
status,
|
||||
disabled,
|
||||
onCancel,
|
||||
onSave,
|
||||
onAdd,
|
||||
@@ -44,10 +46,10 @@ const Operate = ({
|
||||
|
||||
if (status === 'add') {
|
||||
return (
|
||||
<div className='
|
||||
px-3 h-[28px] bg-white border border-gray-200 rounded-md cursor-pointer
|
||||
text-xs font-medium text-gray-700 flex items-center
|
||||
' onClick={onAdd}>
|
||||
<div className={
|
||||
`px-3 h-[28px] bg-white border border-gray-200 rounded-md cursor-pointer
|
||||
text-xs font-medium text-gray-700 flex items-center ${disabled && 'opacity-50 cursor-default'}}`
|
||||
} onClick={() => !disabled && onAdd()}>
|
||||
{t('common.provider.addKey')}
|
||||
</div>
|
||||
)
|
||||
@@ -69,10 +71,10 @@ const Operate = ({
|
||||
<Indicator color='green' className='mr-4' />
|
||||
)
|
||||
}
|
||||
<div className='
|
||||
px-3 h-[28px] bg-white border border-gray-200 rounded-md cursor-pointer
|
||||
text-xs font-medium text-gray-700 flex items-center
|
||||
' onClick={onEdit}>
|
||||
<div className={
|
||||
`px-3 h-[28px] bg-white border border-gray-200 rounded-md cursor-pointer
|
||||
text-xs font-medium text-gray-700 flex items-center ${disabled && 'opacity-50 cursor-default'}}`
|
||||
} onClick={() => !disabled && onEdit()}>
|
||||
{t('common.provider.editKey')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,6 +13,7 @@ export type KeyValidatorProps = {
|
||||
forms: Form[]
|
||||
keyFrom: KeyFrom
|
||||
onSave: (v: ValidateValue) => Promise<boolean | undefined>
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const KeyValidator = ({
|
||||
@@ -22,6 +23,7 @@ const KeyValidator = ({
|
||||
forms,
|
||||
keyFrom,
|
||||
onSave,
|
||||
disabled,
|
||||
}: KeyValidatorProps) => {
|
||||
const triggerKey = `plugins/${type}`
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
@@ -85,10 +87,11 @@ const KeyValidator = ({
|
||||
onSave={handleSave}
|
||||
onAdd={handleAdd}
|
||||
onEdit={handleEdit}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
isOpen && (
|
||||
isOpen && !disabled && (
|
||||
<div className='px-4 py-3'>
|
||||
{
|
||||
forms.map(form => (
|
||||
|
||||
@@ -16,9 +16,9 @@ import { fetchMembers } from '@/service/common'
|
||||
import I18n from '@/context/i18n'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import Avatar from '@/app/components/base/avatar'
|
||||
import { useWorkspacesContext } from '@/context/workspace-context'
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
const MembersPage = () => {
|
||||
const { t } = useTranslation()
|
||||
const RoleMap = {
|
||||
@@ -27,15 +27,13 @@ const MembersPage = () => {
|
||||
normal: t('common.members.normal'),
|
||||
}
|
||||
const { locale } = useContext(I18n)
|
||||
const { userProfile } = useAppContext()
|
||||
const { userProfile, currentWorkspace, isCurrentWorkspaceManager } = 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
|
||||
const { workspaces } = useWorkspacesContext()
|
||||
const currentWrokspace = workspaces.filter(item => item.current)?.[0]
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -43,14 +41,14 @@ const MembersPage = () => {
|
||||
<div className='flex items-center mb-4 p-3 bg-gray-50 rounded-2xl'>
|
||||
<div className={cn(s['logo-icon'], 'shrink-0')}></div>
|
||||
<div className='grow mx-2'>
|
||||
<div className='text-sm font-medium text-gray-900'>{currentWrokspace?.name}</div>
|
||||
<div className='text-sm font-medium text-gray-900'>{currentWorkspace?.name}</div>
|
||||
<div className='text-xs text-gray-500'>{t('common.userProfile.workspace')}</div>
|
||||
</div>
|
||||
<div className='
|
||||
shrink-0 flex items-center py-[7px] px-3 border-[0.5px] border-gray-200
|
||||
<div className={
|
||||
`shrink-0 flex items-center py-[7px] px-3 border-[0.5px] border-gray-200
|
||||
text-[13px] font-medium text-primary-600 bg-white
|
||||
shadow-xs rounded-lg cursor-pointer
|
||||
' onClick={() => setInviteModalVisible(true)}>
|
||||
shadow-xs rounded-lg ${isCurrentWorkspaceManager ? 'cursor-pointer' : 'grayscale opacity-50 cursor-default'}`
|
||||
} onClick={() => isCurrentWorkspaceManager && setInviteModalVisible(true)}>
|
||||
<UserPlusIcon className='w-4 h-4 mr-2 ' />
|
||||
{t('common.members.invite')}
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { Form, ValidateValue } from '../key-validator/declarations'
|
||||
import { updatePluginKey, validatePluginKey } from './utils'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import type { PluginProvider } from '@/models/common'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
|
||||
type SerpapiPluginProps = {
|
||||
plugin: PluginProvider
|
||||
@@ -16,6 +17,7 @@ const SerpapiPlugin = ({
|
||||
onUpdate,
|
||||
}: SerpapiPluginProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { isCurrentWorkspaceManager } = useAppContext()
|
||||
const { notify } = useToastContext()
|
||||
|
||||
const forms: Form[] = [{
|
||||
@@ -70,6 +72,7 @@ const SerpapiPlugin = ({
|
||||
link: 'https://serpapi.com/manage-api-key',
|
||||
}}
|
||||
onSave={handleSave}
|
||||
disabled={!isCurrentWorkspaceManager}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import Indicator from '../indicator'
|
||||
import type { AppDetailResponse } from '@/models/app'
|
||||
import NewAppDialog from '@/app/(commonLayout)/apps/NewAppDialog'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
|
||||
type IAppSelectorProps = {
|
||||
appItems: AppDetailResponse[]
|
||||
@@ -16,6 +17,7 @@ type IAppSelectorProps = {
|
||||
|
||||
export default function AppSelector({ appItems, curApp }: IAppSelectorProps) {
|
||||
const router = useRouter()
|
||||
const { isCurrentWorkspaceManager } = useAppContext()
|
||||
const [showNewAppDialog, setShowNewAppDialog] = useState(false)
|
||||
const { t } = useTranslation()
|
||||
|
||||
@@ -77,7 +79,7 @@ export default function AppSelector({ appItems, curApp }: IAppSelectorProps) {
|
||||
))
|
||||
}
|
||||
</div>)}
|
||||
<Menu.Item>
|
||||
{isCurrentWorkspaceManager && <Menu.Item>
|
||||
<div className='p-1' onClick={() => setShowNewAppDialog(true)}>
|
||||
<div
|
||||
className='flex items-center h-12 rounded-lg cursor-pointer hover:bg-gray-100'
|
||||
@@ -95,6 +97,7 @@ export default function AppSelector({ appItems, curApp }: IAppSelectorProps) {
|
||||
</div>
|
||||
</div>
|
||||
</Menu.Item>
|
||||
}
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import AccountDropdown from './account-dropdown'
|
||||
import AppNav from './app-nav'
|
||||
@@ -8,6 +10,7 @@ import GithubStar from './github-star'
|
||||
import PluginNav from './plugin-nav'
|
||||
import s from './index.module.css'
|
||||
import { WorkspaceProvider } from '@/context/workspace-context'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
|
||||
const navClassName = `
|
||||
flex items-center relative mr-3 px-3 h-8 rounded-xl
|
||||
@@ -16,6 +19,7 @@ const navClassName = `
|
||||
`
|
||||
|
||||
const Header = () => {
|
||||
const { isCurrentWorkspaceManager } = useAppContext()
|
||||
return (
|
||||
<>
|
||||
<div className='flex items-center'>
|
||||
@@ -29,7 +33,7 @@ const Header = () => {
|
||||
<ExploreNav className={navClassName} />
|
||||
<AppNav />
|
||||
<PluginNav className={navClassName} />
|
||||
<DatasetNav />
|
||||
{isCurrentWorkspaceManager && <DatasetNav />}
|
||||
</div>
|
||||
<div className='flex items-center flex-shrink-0'>
|
||||
<EnvNav />
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useRouter } from 'next/navigation'
|
||||
import { debounce } from 'lodash-es'
|
||||
import Indicator from '../../indicator'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
|
||||
type NavItem = {
|
||||
id: string
|
||||
@@ -29,6 +30,7 @@ const itemClassName = `
|
||||
|
||||
const NavSelector = ({ curNav, navs, createText, onCreate, onLoadmore }: INavSelectorProps) => {
|
||||
const router = useRouter()
|
||||
const { isCurrentWorkspaceManager } = useAppContext()
|
||||
|
||||
const handleScroll = useCallback(debounce((e) => {
|
||||
if (typeof onLoadmore === 'function') {
|
||||
@@ -81,7 +83,7 @@ const NavSelector = ({ curNav, navs, createText, onCreate, onLoadmore }: INavSel
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<Menu.Item>
|
||||
{isCurrentWorkspaceManager && <Menu.Item>
|
||||
<div className='p-1' onClick={onCreate}>
|
||||
<div
|
||||
className='flex items-center h-12 rounded-lg cursor-pointer hover:bg-gray-100'
|
||||
@@ -98,7 +100,7 @@ const NavSelector = ({ curNav, navs, createText, onCreate, onLoadmore }: INavSel
|
||||
<div className='font-normal text-[14px] text-gray-700'>{createText}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Menu.Item>
|
||||
</Menu.Item>}
|
||||
</Menu.Items>
|
||||
</Menu>
|
||||
</div>
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
'use client'
|
||||
|
||||
import { createRef, useEffect, useRef, useState } from 'react'
|
||||
import { createRef, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import useSWR from 'swr'
|
||||
import { createContext, useContext, useContextSelector } from 'use-context-selector'
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import { fetchAppList } from '@/service/apps'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { fetchLanggeniusVersion, fetchUserProfile } from '@/service/common'
|
||||
import { fetchCurrentWorkspace, fetchLanggeniusVersion, fetchUserProfile } from '@/service/common'
|
||||
import type { App } from '@/types/app'
|
||||
import type { LangGeniusVersionResponse, UserProfileResponse } from '@/models/common'
|
||||
import type { ICurrentWorkspace, LangGeniusVersionResponse, UserProfileResponse } from '@/models/common'
|
||||
|
||||
export type AppContextValue = {
|
||||
apps: App[]
|
||||
mutateApps: () => void
|
||||
mutateApps: VoidFunction
|
||||
userProfile: UserProfileResponse
|
||||
mutateUserProfile: () => void
|
||||
mutateUserProfile: VoidFunction
|
||||
currentWorkspace: ICurrentWorkspace
|
||||
isCurrentWorkspaceManager: boolean
|
||||
mutateCurrentWorkspace: VoidFunction
|
||||
pageContainerRef: React.RefObject<HTMLDivElement>
|
||||
langeniusVersionInfo: LangGeniusVersionResponse
|
||||
useSelector: typeof useSelector
|
||||
@@ -30,6 +33,17 @@ const initialLangeniusVersionInfo = {
|
||||
can_auto_update: false,
|
||||
}
|
||||
|
||||
const initialWorkspaceInfo: ICurrentWorkspace = {
|
||||
id: '',
|
||||
name: '',
|
||||
plan: '',
|
||||
status: '',
|
||||
created_at: 0,
|
||||
role: 'normal',
|
||||
providers: [],
|
||||
in_trail: true,
|
||||
}
|
||||
|
||||
const AppContext = createContext<AppContextValue>({
|
||||
apps: [],
|
||||
mutateApps: () => { },
|
||||
@@ -40,7 +54,10 @@ const AppContext = createContext<AppContextValue>({
|
||||
avatar: '',
|
||||
is_password_set: false,
|
||||
},
|
||||
currentWorkspace: initialWorkspaceInfo,
|
||||
isCurrentWorkspaceManager: false,
|
||||
mutateUserProfile: () => { },
|
||||
mutateCurrentWorkspace: () => { },
|
||||
pageContainerRef: createRef(),
|
||||
langeniusVersionInfo: initialLangeniusVersionInfo,
|
||||
useSelector,
|
||||
@@ -59,10 +76,14 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) =>
|
||||
|
||||
const { data: appList, mutate: mutateApps } = useSWR({ url: '/apps', params: { page: 1 } }, fetchAppList)
|
||||
const { data: userProfileResponse, mutate: mutateUserProfile } = useSWR({ url: '/account/profile', params: {} }, fetchUserProfile)
|
||||
const { data: currentWorkspaceResponse, mutate: mutateCurrentWorkspace } = useSWR({ url: '/workspaces/current', params: {} }, fetchCurrentWorkspace)
|
||||
|
||||
const [userProfile, setUserProfile] = useState<UserProfileResponse>()
|
||||
const [langeniusVersionInfo, setLangeniusVersionInfo] = useState<LangGeniusVersionResponse>(initialLangeniusVersionInfo)
|
||||
const updateUserProfileAndVersion = async () => {
|
||||
const [currentWorkspace, setCurrentWorkspace] = useState<ICurrentWorkspace>(initialWorkspaceInfo)
|
||||
const isCurrentWorkspaceManager = useMemo(() => ['owner', 'admin'].includes(currentWorkspace.role), [currentWorkspace.role])
|
||||
|
||||
const updateUserProfileAndVersion = useCallback(async () => {
|
||||
if (userProfileResponse && !userProfileResponse.bodyUsed) {
|
||||
const result = await userProfileResponse.json()
|
||||
setUserProfile(result)
|
||||
@@ -71,16 +92,33 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) =>
|
||||
const versionData = await fetchLanggeniusVersion({ url: '/version', params: { current_version } })
|
||||
setLangeniusVersionInfo({ ...versionData, current_version, latest_version: versionData.version, current_env })
|
||||
}
|
||||
}
|
||||
}, [userProfileResponse])
|
||||
|
||||
useEffect(() => {
|
||||
updateUserProfileAndVersion()
|
||||
}, [userProfileResponse])
|
||||
}, [updateUserProfileAndVersion, userProfileResponse])
|
||||
|
||||
useEffect(() => {
|
||||
if (currentWorkspaceResponse)
|
||||
setCurrentWorkspace(currentWorkspaceResponse)
|
||||
}, [currentWorkspaceResponse])
|
||||
|
||||
if (!appList || !userProfile)
|
||||
return <Loading type='app' />
|
||||
|
||||
return (
|
||||
<AppContext.Provider value={{ apps: appList.data, mutateApps, userProfile, mutateUserProfile, pageContainerRef, langeniusVersionInfo, useSelector }}>
|
||||
<AppContext.Provider value={{
|
||||
apps: appList.data,
|
||||
mutateApps,
|
||||
userProfile,
|
||||
mutateUserProfile,
|
||||
pageContainerRef,
|
||||
langeniusVersionInfo,
|
||||
useSelector,
|
||||
currentWorkspace,
|
||||
isCurrentWorkspaceManager,
|
||||
mutateCurrentWorkspace,
|
||||
}}>
|
||||
<div ref={pageContainerRef} className='relative flex flex-col h-full overflow-auto bg-gray-100'>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@ const translation = {
|
||||
apiKeyInfo: {
|
||||
cloud: {
|
||||
trial: {
|
||||
title: '您正在使用 {{providerName}} 试用配额。',
|
||||
title: '您正在使用 {{providerName}} 的试用配额。',
|
||||
description: '试用配额仅供您测试使用。 在试用配额用完之前,请自行设置模型提供商或购买额外配额。',
|
||||
},
|
||||
exhausted: {
|
||||
|
||||
@@ -118,6 +118,13 @@ export type IWorkspace = {
|
||||
current: boolean
|
||||
}
|
||||
|
||||
export type ICurrentWorkspace = Omit<IWorkspace, 'current'> & {
|
||||
role: 'normal' | 'admin' | 'owner'
|
||||
providers: Provider[]
|
||||
in_trail: boolean
|
||||
trial_end_reason?: string
|
||||
}
|
||||
|
||||
export type DataSourceNotionPage = {
|
||||
page_icon: null | {
|
||||
type: string | null
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dify-web",
|
||||
"version": "0.3.13",
|
||||
"version": "0.3.14",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Fetcher } from 'swr'
|
||||
import { del, get, patch, post, put } from './base'
|
||||
import type {
|
||||
AccountIntegrate, CommonResponse, DataSourceNotion,
|
||||
ICurrentWorkspace,
|
||||
IWorkspace, LangGeniusVersionResponse, Member,
|
||||
OauthResponse, PluginProvider, Provider, ProviderAnthropicToken, ProviderAzureToken,
|
||||
SetupStatusResponse, TenantInfoResponse, UserProfileOriginResponse,
|
||||
@@ -87,6 +88,10 @@ export const fetchFilePreview: Fetcher<{ content: string }, { fileID: string }>
|
||||
return get(`/files/${fileID}/preview`) as Promise<{ content: string }>
|
||||
}
|
||||
|
||||
export const fetchCurrentWorkspace: Fetcher<ICurrentWorkspace, { url: string; params: Record<string, any> }> = ({ url, params }) => {
|
||||
return get(url, { params }) as Promise<ICurrentWorkspace>
|
||||
}
|
||||
|
||||
export const fetchWorkspaces: Fetcher<{ workspaces: IWorkspace[] }, { url: string; params: Record<string, any> }> = ({ url, params }) => {
|
||||
return get(url, { params }) as Promise<{ workspaces: IWorkspace[] }>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user