| from flask_login import current_user | |||||
| from extensions.ext_database import db | from extensions.ext_database import db | ||||
| from models.account import Tenant | |||||
| from models.account import Tenant, TenantAccountJoin | |||||
| from models.provider import Provider | from models.provider import Provider | ||||
| class WorkspaceService: | class WorkspaceService: | ||||
| @classmethod | @classmethod | ||||
| def get_tenant_info(cls, tenant: Tenant): | def get_tenant_info(cls, tenant: Tenant): | ||||
| if not tenant: | |||||
| return None | |||||
| tenant_info = { | tenant_info = { | ||||
| 'id': tenant.id, | 'id': tenant.id, | ||||
| 'name': tenant.name, | 'name': tenant.name, | ||||
| 'status': tenant.status, | 'status': tenant.status, | ||||
| 'created_at': tenant.created_at, | 'created_at': tenant.created_at, | ||||
| 'providers': [], | '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 | # Get providers | ||||
| providers = db.session.query(Provider).filter( | providers = db.session.query(Provider).filter( | ||||
| Provider.tenant_id == tenant.id | Provider.tenant_id == tenant.id |
| 'use client' | 'use client' | ||||
| import type { FC } from 'react' | import type { FC } from 'react' | ||||
| import React, { useEffect } from 'react' | |||||
| import React, { useEffect, useMemo } from 'react' | |||||
| import cn from 'classnames' | import cn from 'classnames' | ||||
| import useSWR from 'swr' | import useSWR from 'swr' | ||||
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||
| import s from './style.module.css' | import s from './style.module.css' | ||||
| import AppSideBar from '@/app/components/app-sidebar' | import AppSideBar from '@/app/components/app-sidebar' | ||||
| import { fetchAppDetail } from '@/service/apps' | import { fetchAppDetail } from '@/service/apps' | ||||
| import { useAppContext } from '@/context/app-context' | |||||
| export type IAppDetailLayoutProps = { | export type IAppDetailLayoutProps = { | ||||
| children: React.ReactNode | children: React.ReactNode | ||||
| params: { appId }, // get appId in path | params: { appId }, // get appId in path | ||||
| } = props | } = props | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| const { isCurrentWorkspaceManager } = useAppContext() | |||||
| const detailParams = { url: '/apps', id: appId } | const detailParams = { url: '/apps', id: appId } | ||||
| const { data: response } = useSWR(detailParams, fetchAppDetail) | 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') | const appModeName = response?.mode?.toUpperCase() === 'COMPLETION' ? t('common.appModes.completionApp') : t('common.appModes.chatApp') | ||||
| useEffect(() => { | useEffect(() => { | ||||
| if (response?.name) | if (response?.name) |
| import { ToastContext } from '@/app/components/base/toast' | import { ToastContext } from '@/app/components/base/toast' | ||||
| import { deleteApp } from '@/service/apps' | import { deleteApp } from '@/service/apps' | ||||
| import AppIcon from '@/app/components/base/app-icon' | import AppIcon from '@/app/components/base/app-icon' | ||||
| import AppsContext from '@/context/app-context' | |||||
| import AppsContext, { useAppContext } from '@/context/app-context' | |||||
| export type AppCardProps = { | export type AppCardProps = { | ||||
| app: App | app: App | ||||
| }: AppCardProps) => { | }: AppCardProps) => { | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| const { notify } = useContext(ToastContext) | const { notify } = useContext(ToastContext) | ||||
| const { isCurrentWorkspaceManager } = useAppContext() | |||||
| const mutateApps = useContextSelector(AppsContext, state => state.mutateApps) | const mutateApps = useContextSelector(AppsContext, state => state.mutateApps) | ||||
| <div className={style.listItemHeading}> | <div className={style.listItemHeading}> | ||||
| <div className={style.listItemHeadingContent}>{app.name}</div> | <div className={style.listItemHeadingContent}>{app.name}</div> | ||||
| </div> | </div> | ||||
| <span className={style.deleteAppIcon} onClick={onDeleteClick} /> | |||||
| { isCurrentWorkspaceManager | |||||
| && <span className={style.deleteAppIcon} onClick={onDeleteClick} />} | |||||
| </div> | </div> | ||||
| <div className={style.listItemDescription}>{app.model_config?.pre_prompt}</div> | <div className={style.listItemDescription}>{app.model_config?.pre_prompt}</div> | ||||
| <div className={style.listItemFooter}> | <div className={style.listItemFooter}> |
| import NewAppCard from './NewAppCard' | import NewAppCard from './NewAppCard' | ||||
| import type { AppListResponse } from '@/models/app' | import type { AppListResponse } from '@/models/app' | ||||
| import { fetchAppList } from '@/service/apps' | 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' | import { NEED_REFRESH_APP_LIST_KEY } from '@/config' | ||||
| const getKey = (pageIndex: number, previousPageData: AppListResponse) => { | const getKey = (pageIndex: number, previousPageData: AppListResponse) => { | ||||
| const Apps = () => { | const Apps = () => { | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| const { isCurrentWorkspaceManager } = useAppContext() | |||||
| const { data, isLoading, setSize, mutate } = useSWRInfinite(getKey, fetchAppList, { revalidateFirstPage: false }) | const { data, isLoading, setSize, mutate } = useSWRInfinite(getKey, fetchAppList, { revalidateFirstPage: false }) | ||||
| const loadingStateRef = useRef(false) | const loadingStateRef = useRef(false) | ||||
| const pageContainerRef = useSelector(state => state.pageContainerRef) | const pageContainerRef = useSelector(state => state.pageContainerRef) | ||||
| {data?.map(({ data: apps }) => apps.map(app => ( | {data?.map(({ data: apps }) => apps.map(app => ( | ||||
| <AppCard key={app.id} app={app} onDelete={mutate} /> | <AppCard key={app.id} app={app} onDelete={mutate} /> | ||||
| )))} | )))} | ||||
| <NewAppCard ref={anchorRef} onSuccess={mutate} /> | |||||
| { isCurrentWorkspaceManager | |||||
| && <NewAppCard ref={anchorRef} onSuccess={mutate} />} | |||||
| </nav> | </nav> | ||||
| ) | ) | ||||
| } | } |
| import DatasetCard from './DatasetCard' | import DatasetCard from './DatasetCard' | ||||
| import type { DataSetListResponse } from '@/models/datasets' | import type { DataSetListResponse } from '@/models/datasets' | ||||
| import { fetchDatasets } from '@/service/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) => { | const getKey = (pageIndex: number, previousPageData: DataSetListResponse) => { | ||||
| if (!pageIndex || previousPageData.has_more) | if (!pageIndex || previousPageData.has_more) | ||||
| } | } | ||||
| const Datasets = () => { | const Datasets = () => { | ||||
| const { isCurrentWorkspaceManager } = useAppContext() | |||||
| const { data, isLoading, setSize, mutate } = useSWRInfinite(getKey, fetchDatasets, { revalidateFirstPage: false }) | const { data, isLoading, setSize, mutate } = useSWRInfinite(getKey, fetchDatasets, { revalidateFirstPage: false }) | ||||
| const loadingStateRef = useRef(false) | const loadingStateRef = useRef(false) | ||||
| const pageContainerRef = useSelector(state => state.pageContainerRef) | const pageContainerRef = useSelector(state => state.pageContainerRef) | ||||
| {data?.map(({ data: datasets }) => datasets.map(dataset => ( | {data?.map(({ data: datasets }) => datasets.map(dataset => ( | ||||
| <DatasetCard key={dataset.id} dataset={dataset} onDelete={mutate} />), | <DatasetCard key={dataset.id} dataset={dataset} onDelete={mutate} />), | ||||
| ))} | ))} | ||||
| <NewDatasetCard ref={anchorRef} /> | |||||
| { isCurrentWorkspaceManager && <NewDatasetCard ref={anchorRef} /> } | |||||
| </nav> | </nav> | ||||
| ) | ) | ||||
| } | } |
| 'use client' | 'use client' | ||||
| import type { FC } from 'react' | import type { FC } from 'react' | ||||
| import React, { useState } from 'react' | |||||
| import React, { useMemo, useState } from 'react' | |||||
| import { | import { | ||||
| Cog8ToothIcon, | Cog8ToothIcon, | ||||
| DocumentTextIcon, | DocumentTextIcon, | ||||
| import type { AppDetailResponse } from '@/models/app' | import type { AppDetailResponse } from '@/models/app' | ||||
| import './style.css' | import './style.css' | ||||
| import { AppType } from '@/types/app' | import { AppType } from '@/types/app' | ||||
| import { useAppContext } from '@/context/app-context' | |||||
| export type IAppCardProps = { | export type IAppCardProps = { | ||||
| className?: string | className?: string | ||||
| }: IAppCardProps) { | }: IAppCardProps) { | ||||
| const router = useRouter() | const router = useRouter() | ||||
| const pathname = usePathname() | const pathname = usePathname() | ||||
| const { currentWorkspace, isCurrentWorkspaceManager } = useAppContext() | |||||
| const [showSettingsModal, setShowSettingsModal] = useState(false) | const [showSettingsModal, setShowSettingsModal] = useState(false) | ||||
| const [showShareModal, setShowShareModal] = useState(false) | const [showShareModal, setShowShareModal] = useState(false) | ||||
| const [showEmbedded, setShowEmbedded] = useState(false) | const [showEmbedded, setShowEmbedded] = useState(false) | ||||
| const [showCustomizeModal, setShowCustomizeModal] = useState(false) | const [showCustomizeModal, setShowCustomizeModal] = useState(false) | ||||
| const { t } = useTranslation() | 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 isApp = cardType === 'app' || cardType === 'webapp' | ||||
| const basicName = isApp ? appInfo?.site?.title : t('appOverview.overview.apiInfo.title') | const basicName = isApp ? appInfo?.site?.title : t('appOverview.overview.apiInfo.title') | ||||
| <Tag className="mr-2" color={runningStatus ? 'green' : 'yellow'}> | <Tag className="mr-2" color={runningStatus ? 'green' : 'yellow'}> | ||||
| {runningStatus ? t('appOverview.overview.status.running') : t('appOverview.overview.status.disable')} | {runningStatus ? t('appOverview.overview.status.running') : t('appOverview.overview.status.disable')} | ||||
| </Tag> | </Tag> | ||||
| <Switch defaultValue={runningStatus} onChange={onChangeStatus} /> | |||||
| <Switch defaultValue={runningStatus} onChange={onChangeStatus} disabled={currentWorkspace?.role === 'normal'} /> | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| <div className="flex flex-col justify-center py-2"> | <div className="flex flex-col justify-center py-2"> | ||||
| onClose={() => setShowShareModal(false)} | onClose={() => setShowShareModal(false)} | ||||
| linkUrl={appUrl} | linkUrl={appUrl} | ||||
| onGenerateCode={onGenerateCode} | onGenerateCode={onGenerateCode} | ||||
| regeneratable={isCurrentWorkspaceManager} | |||||
| /> | /> | ||||
| <SettingsModal | <SettingsModal | ||||
| appInfo={appInfo} | appInfo={appInfo} |
| onClose: () => void | onClose: () => void | ||||
| onGenerateCode: () => Promise<void> | onGenerateCode: () => Promise<void> | ||||
| linkUrl: string | linkUrl: string | ||||
| regeneratable?: boolean | |||||
| } | } | ||||
| const prefixShare = 'appOverview.overview.appInfo.share' | const prefixShare = 'appOverview.overview.appInfo.share' | ||||
| isShow, | isShow, | ||||
| onClose, | onClose, | ||||
| onGenerateCode, | onGenerateCode, | ||||
| regeneratable, | |||||
| }) => { | }) => { | ||||
| const [genLoading, setGenLoading] = useState(false) | const [genLoading, setGenLoading] = useState(false) | ||||
| const [isCopied, setIsCopied] = useState(false) | const [isCopied, setIsCopied] = useState(false) | ||||
| <LinkIcon className='w-4 h-4 mr-2' /> | <LinkIcon className='w-4 h-4 mr-2' /> | ||||
| { t(`${prefixShare}.${isCopied ? 'linkCopied' : 'copyLink'}`) } | { t(`${prefixShare}.${isCopied ? 'linkCopied' : 'copyLink'}`) } | ||||
| </Button> | </Button> | ||||
| <Button className='w-32 !px-0' onClick={async () => { | |||||
| {regeneratable && <Button className='w-32 !px-0' onClick={async () => { | |||||
| setGenLoading(true) | setGenLoading(true) | ||||
| await onGenerateCode() | await onGenerateCode() | ||||
| setGenLoading(false) | setGenLoading(false) | ||||
| }}> | }}> | ||||
| <ArrowPathIcon className={`w-4 h-4 mr-2 ${genLoading ? 'generateLogo' : ''}`} /> | <ArrowPathIcon className={`w-4 h-4 mr-2 ${genLoading ? 'generateLogo' : ''}`} /> | ||||
| {t(`${prefixShare}.regenerate`)} | {t(`${prefixShare}.regenerate`)} | ||||
| </Button> | |||||
| </Button>} | |||||
| </div> | </div> | ||||
| </Modal> | </Modal> | ||||
| } | } |
| import Loading from '@/app/components/base/loading' | import Loading from '@/app/components/base/loading' | ||||
| import Confirm from '@/app/components/base/confirm' | import Confirm from '@/app/components/base/confirm' | ||||
| import I18n from '@/context/i18n' | import I18n from '@/context/i18n' | ||||
| import { useAppContext } from '@/context/app-context' | |||||
| type ISecretKeyModalProps = { | type ISecretKeyModalProps = { | ||||
| isShow: boolean | isShow: boolean | ||||
| onClose, | onClose, | ||||
| }: ISecretKeyModalProps) => { | }: ISecretKeyModalProps) => { | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| const { currentWorkspace, isCurrentWorkspaceManager } = useAppContext() | |||||
| const [showConfirmDelete, setShowConfirmDelete] = useState(false) | const [showConfirmDelete, setShowConfirmDelete] = useState(false) | ||||
| const [isVisible, setVisible] = useState(false) | const [isVisible, setVisible] = useState(false) | ||||
| const [newKey, setNewKey] = useState<CreateApiKeyResponse | undefined>(undefined) | const [newKey, setNewKey] = useState<CreateApiKeyResponse | undefined>(undefined) | ||||
| setCopyValue(api.token) | setCopyValue(api.token) | ||||
| }}></div> | }}></div> | ||||
| </Tooltip> | </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> | ||||
| </div> | </div> | ||||
| ))} | ))} | ||||
| ) | ) | ||||
| } | } | ||||
| <div className='flex'> | <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' /> | <PlusIcon className='flex flex-shrink-0 w-4 h-4' /> | ||||
| <div className='text-xs font-medium text-gray-800'>{t('appApi.apiKeyModal.createNewSecretKey')}</div> | <div className='text-xs font-medium text-gray-800'>{t('appApi.apiKeyModal.createNewSecretKey')}</div> | ||||
| </Button> | </Button> |
| import NotionIcon from '@/app/components/base/notion-icon' | import NotionIcon from '@/app/components/base/notion-icon' | ||||
| import { apiPrefix } from '@/config' | import { apiPrefix } from '@/config' | ||||
| import type { DataSourceNotion as TDataSourceNotion } from '@/models/common' | import type { DataSourceNotion as TDataSourceNotion } from '@/models/common' | ||||
| import { useAppContext } from '@/context/app-context' | |||||
| type DataSourceNotionProps = { | type DataSourceNotionProps = { | ||||
| workspaces: TDataSourceNotion[] | workspaces: TDataSourceNotion[] | ||||
| workspaces, | workspaces, | ||||
| }: DataSourceNotionProps) => { | }: DataSourceNotionProps) => { | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| const { isCurrentWorkspaceManager } = useAppContext() | |||||
| const connected = !!workspaces.length | const connected = !!workspaces.length | ||||
| return ( | return ( | ||||
| } | } | ||||
| </div> | </div> | ||||
| { | { | ||||
| !connected | |||||
| connected | |||||
| ? ( | ? ( | ||||
| <Link | <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')} | {t('common.dataSource.connect')} | ||||
| </Link> | </Link> | ||||
| ) | ) | ||||
| : ( | : ( | ||||
| <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]' /> | <PlusIcon className='w-[14px] h-[14px] mr-[5px]' /> | ||||
| {t('common.dataSource.notion.addWorkspace')} | {t('common.dataSource.notion.addWorkspace')} | ||||
| </Link> | </Link> |
| type OperateProps = { | type OperateProps = { | ||||
| isOpen: boolean | isOpen: boolean | ||||
| status: Status | status: Status | ||||
| disabled?: boolean | |||||
| onCancel: () => void | onCancel: () => void | ||||
| onSave: () => void | onSave: () => void | ||||
| onAdd: () => void | onAdd: () => void | ||||
| const Operate = ({ | const Operate = ({ | ||||
| isOpen, | isOpen, | ||||
| status, | status, | ||||
| disabled, | |||||
| onCancel, | onCancel, | ||||
| onSave, | onSave, | ||||
| onAdd, | onAdd, | ||||
| if (status === 'add') { | if (status === 'add') { | ||||
| return ( | 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')} | {t('common.provider.addKey')} | ||||
| </div> | </div> | ||||
| ) | ) | ||||
| <Indicator color='green' className='mr-4' /> | <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')} | {t('common.provider.editKey')} | ||||
| </div> | </div> | ||||
| </div> | </div> |
| forms: Form[] | forms: Form[] | ||||
| keyFrom: KeyFrom | keyFrom: KeyFrom | ||||
| onSave: (v: ValidateValue) => Promise<boolean | undefined> | onSave: (v: ValidateValue) => Promise<boolean | undefined> | ||||
| disabled?: boolean | |||||
| } | } | ||||
| const KeyValidator = ({ | const KeyValidator = ({ | ||||
| forms, | forms, | ||||
| keyFrom, | keyFrom, | ||||
| onSave, | onSave, | ||||
| disabled, | |||||
| }: KeyValidatorProps) => { | }: KeyValidatorProps) => { | ||||
| const triggerKey = `plugins/${type}` | const triggerKey = `plugins/${type}` | ||||
| const { eventEmitter } = useEventEmitterContextContext() | const { eventEmitter } = useEventEmitterContextContext() | ||||
| onSave={handleSave} | onSave={handleSave} | ||||
| onAdd={handleAdd} | onAdd={handleAdd} | ||||
| onEdit={handleEdit} | onEdit={handleEdit} | ||||
| disabled={disabled} | |||||
| /> | /> | ||||
| </div> | </div> | ||||
| { | { | ||||
| isOpen && ( | |||||
| isOpen && !disabled && ( | |||||
| <div className='px-4 py-3'> | <div className='px-4 py-3'> | ||||
| { | { | ||||
| forms.map(form => ( | forms.map(form => ( |
| import I18n from '@/context/i18n' | import I18n from '@/context/i18n' | ||||
| import { useAppContext } from '@/context/app-context' | import { useAppContext } from '@/context/app-context' | ||||
| import Avatar from '@/app/components/base/avatar' | import Avatar from '@/app/components/base/avatar' | ||||
| import { useWorkspacesContext } from '@/context/workspace-context' | |||||
| dayjs.extend(relativeTime) | dayjs.extend(relativeTime) | ||||
| const MembersPage = () => { | const MembersPage = () => { | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| const RoleMap = { | const RoleMap = { | ||||
| normal: t('common.members.normal'), | normal: t('common.members.normal'), | ||||
| } | } | ||||
| const { locale } = useContext(I18n) | const { locale } = useContext(I18n) | ||||
| const { userProfile } = useAppContext() | |||||
| const { userProfile, currentWorkspace, isCurrentWorkspaceManager } = useAppContext() | |||||
| const { data, mutate } = useSWR({ url: '/workspaces/current/members' }, fetchMembers) | const { data, mutate } = useSWR({ url: '/workspaces/current/members' }, fetchMembers) | ||||
| const [inviteModalVisible, setInviteModalVisible] = useState(false) | const [inviteModalVisible, setInviteModalVisible] = useState(false) | ||||
| const [invitationLink, setInvitationLink] = useState('') | const [invitationLink, setInvitationLink] = useState('') | ||||
| const [invitedModalVisible, setInvitedModalVisible] = useState(false) | const [invitedModalVisible, setInvitedModalVisible] = useState(false) | ||||
| const accounts = data?.accounts || [] | const accounts = data?.accounts || [] | ||||
| const owner = accounts.filter(account => account.role === 'owner')?.[0]?.email === userProfile.email | const owner = accounts.filter(account => account.role === 'owner')?.[0]?.email === userProfile.email | ||||
| const { workspaces } = useWorkspacesContext() | |||||
| const currentWrokspace = workspaces.filter(item => item.current)?.[0] | |||||
| return ( | return ( | ||||
| <> | <> | ||||
| <div className='flex items-center mb-4 p-3 bg-gray-50 rounded-2xl'> | <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={cn(s['logo-icon'], 'shrink-0')}></div> | ||||
| <div className='grow mx-2'> | <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 className='text-xs text-gray-500'>{t('common.userProfile.workspace')}</div> | ||||
| </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 | 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 ' /> | <UserPlusIcon className='w-4 h-4 mr-2 ' /> | ||||
| {t('common.members.invite')} | {t('common.members.invite')} | ||||
| </div> | </div> |
| import { updatePluginKey, validatePluginKey } from './utils' | import { updatePluginKey, validatePluginKey } from './utils' | ||||
| import { useToastContext } from '@/app/components/base/toast' | import { useToastContext } from '@/app/components/base/toast' | ||||
| import type { PluginProvider } from '@/models/common' | import type { PluginProvider } from '@/models/common' | ||||
| import { useAppContext } from '@/context/app-context' | |||||
| type SerpapiPluginProps = { | type SerpapiPluginProps = { | ||||
| plugin: PluginProvider | plugin: PluginProvider | ||||
| onUpdate, | onUpdate, | ||||
| }: SerpapiPluginProps) => { | }: SerpapiPluginProps) => { | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| const { isCurrentWorkspaceManager } = useAppContext() | |||||
| const { notify } = useToastContext() | const { notify } = useToastContext() | ||||
| const forms: Form[] = [{ | const forms: Form[] = [{ | ||||
| link: 'https://serpapi.com/manage-api-key', | link: 'https://serpapi.com/manage-api-key', | ||||
| }} | }} | ||||
| onSave={handleSave} | onSave={handleSave} | ||||
| disabled={!isCurrentWorkspaceManager} | |||||
| /> | /> | ||||
| ) | ) | ||||
| } | } |
| import type { AppDetailResponse } from '@/models/app' | import type { AppDetailResponse } from '@/models/app' | ||||
| import NewAppDialog from '@/app/(commonLayout)/apps/NewAppDialog' | import NewAppDialog from '@/app/(commonLayout)/apps/NewAppDialog' | ||||
| import AppIcon from '@/app/components/base/app-icon' | import AppIcon from '@/app/components/base/app-icon' | ||||
| import { useAppContext } from '@/context/app-context' | |||||
| type IAppSelectorProps = { | type IAppSelectorProps = { | ||||
| appItems: AppDetailResponse[] | appItems: AppDetailResponse[] | ||||
| export default function AppSelector({ appItems, curApp }: IAppSelectorProps) { | export default function AppSelector({ appItems, curApp }: IAppSelectorProps) { | ||||
| const router = useRouter() | const router = useRouter() | ||||
| const { isCurrentWorkspaceManager } = useAppContext() | |||||
| const [showNewAppDialog, setShowNewAppDialog] = useState(false) | const [showNewAppDialog, setShowNewAppDialog] = useState(false) | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| )) | )) | ||||
| } | } | ||||
| </div>)} | </div>)} | ||||
| <Menu.Item> | |||||
| {isCurrentWorkspaceManager && <Menu.Item> | |||||
| <div className='p-1' onClick={() => setShowNewAppDialog(true)}> | <div className='p-1' onClick={() => setShowNewAppDialog(true)}> | ||||
| <div | <div | ||||
| className='flex items-center h-12 rounded-lg cursor-pointer hover:bg-gray-100' | className='flex items-center h-12 rounded-lg cursor-pointer hover:bg-gray-100' | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| </Menu.Item> | </Menu.Item> | ||||
| } | |||||
| </Menu.Items> | </Menu.Items> | ||||
| </Transition> | </Transition> | ||||
| </Menu> | </Menu> |
| 'use client' | |||||
| import Link from 'next/link' | import Link from 'next/link' | ||||
| import AccountDropdown from './account-dropdown' | import AccountDropdown from './account-dropdown' | ||||
| import AppNav from './app-nav' | import AppNav from './app-nav' | ||||
| import PluginNav from './plugin-nav' | import PluginNav from './plugin-nav' | ||||
| import s from './index.module.css' | import s from './index.module.css' | ||||
| import { WorkspaceProvider } from '@/context/workspace-context' | import { WorkspaceProvider } from '@/context/workspace-context' | ||||
| import { useAppContext } from '@/context/app-context' | |||||
| const navClassName = ` | const navClassName = ` | ||||
| flex items-center relative mr-3 px-3 h-8 rounded-xl | flex items-center relative mr-3 px-3 h-8 rounded-xl | ||||
| ` | ` | ||||
| const Header = () => { | const Header = () => { | ||||
| const { isCurrentWorkspaceManager } = useAppContext() | |||||
| return ( | return ( | ||||
| <> | <> | ||||
| <div className='flex items-center'> | <div className='flex items-center'> | ||||
| <ExploreNav className={navClassName} /> | <ExploreNav className={navClassName} /> | ||||
| <AppNav /> | <AppNav /> | ||||
| <PluginNav className={navClassName} /> | <PluginNav className={navClassName} /> | ||||
| <DatasetNav /> | |||||
| {isCurrentWorkspaceManager && <DatasetNav />} | |||||
| </div> | </div> | ||||
| <div className='flex items-center flex-shrink-0'> | <div className='flex items-center flex-shrink-0'> | ||||
| <EnvNav /> | <EnvNav /> |
| import { debounce } from 'lodash-es' | import { debounce } from 'lodash-es' | ||||
| import Indicator from '../../indicator' | import Indicator from '../../indicator' | ||||
| import AppIcon from '@/app/components/base/app-icon' | import AppIcon from '@/app/components/base/app-icon' | ||||
| import { useAppContext } from '@/context/app-context' | |||||
| type NavItem = { | type NavItem = { | ||||
| id: string | id: string | ||||
| const NavSelector = ({ curNav, navs, createText, onCreate, onLoadmore }: INavSelectorProps) => { | const NavSelector = ({ curNav, navs, createText, onCreate, onLoadmore }: INavSelectorProps) => { | ||||
| const router = useRouter() | const router = useRouter() | ||||
| const { isCurrentWorkspaceManager } = useAppContext() | |||||
| const handleScroll = useCallback(debounce((e) => { | const handleScroll = useCallback(debounce((e) => { | ||||
| if (typeof onLoadmore === 'function') { | if (typeof onLoadmore === 'function') { | ||||
| )) | )) | ||||
| } | } | ||||
| </div> | </div> | ||||
| <Menu.Item> | |||||
| {isCurrentWorkspaceManager && <Menu.Item> | |||||
| <div className='p-1' onClick={onCreate}> | <div className='p-1' onClick={onCreate}> | ||||
| <div | <div | ||||
| className='flex items-center h-12 rounded-lg cursor-pointer hover:bg-gray-100' | className='flex items-center h-12 rounded-lg cursor-pointer hover:bg-gray-100' | ||||
| <div className='font-normal text-[14px] text-gray-700'>{createText}</div> | <div className='font-normal text-[14px] text-gray-700'>{createText}</div> | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| </Menu.Item> | |||||
| </Menu.Item>} | |||||
| </Menu.Items> | </Menu.Items> | ||||
| </Menu> | </Menu> | ||||
| </div> | </div> |
| 'use client' | 'use client' | ||||
| import { createRef, useEffect, useRef, useState } from 'react' | |||||
| import { createRef, useCallback, useEffect, useMemo, useRef, useState } from 'react' | |||||
| import useSWR from 'swr' | import useSWR from 'swr' | ||||
| import { createContext, useContext, useContextSelector } from 'use-context-selector' | import { createContext, useContext, useContextSelector } from 'use-context-selector' | ||||
| import type { FC, ReactNode } from 'react' | import type { FC, ReactNode } from 'react' | ||||
| import { fetchAppList } from '@/service/apps' | import { fetchAppList } from '@/service/apps' | ||||
| import Loading from '@/app/components/base/loading' | 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 { App } from '@/types/app' | ||||
| import type { LangGeniusVersionResponse, UserProfileResponse } from '@/models/common' | |||||
| import type { ICurrentWorkspace, LangGeniusVersionResponse, UserProfileResponse } from '@/models/common' | |||||
| export type AppContextValue = { | export type AppContextValue = { | ||||
| apps: App[] | apps: App[] | ||||
| mutateApps: () => void | |||||
| mutateApps: VoidFunction | |||||
| userProfile: UserProfileResponse | userProfile: UserProfileResponse | ||||
| mutateUserProfile: () => void | |||||
| mutateUserProfile: VoidFunction | |||||
| currentWorkspace: ICurrentWorkspace | |||||
| isCurrentWorkspaceManager: boolean | |||||
| mutateCurrentWorkspace: VoidFunction | |||||
| pageContainerRef: React.RefObject<HTMLDivElement> | pageContainerRef: React.RefObject<HTMLDivElement> | ||||
| langeniusVersionInfo: LangGeniusVersionResponse | langeniusVersionInfo: LangGeniusVersionResponse | ||||
| useSelector: typeof useSelector | useSelector: typeof useSelector | ||||
| can_auto_update: false, | can_auto_update: false, | ||||
| } | } | ||||
| const initialWorkspaceInfo: ICurrentWorkspace = { | |||||
| id: '', | |||||
| name: '', | |||||
| plan: '', | |||||
| status: '', | |||||
| created_at: 0, | |||||
| role: 'normal', | |||||
| providers: [], | |||||
| in_trail: true, | |||||
| } | |||||
| const AppContext = createContext<AppContextValue>({ | const AppContext = createContext<AppContextValue>({ | ||||
| apps: [], | apps: [], | ||||
| mutateApps: () => { }, | mutateApps: () => { }, | ||||
| avatar: '', | avatar: '', | ||||
| is_password_set: false, | is_password_set: false, | ||||
| }, | }, | ||||
| currentWorkspace: initialWorkspaceInfo, | |||||
| isCurrentWorkspaceManager: false, | |||||
| mutateUserProfile: () => { }, | mutateUserProfile: () => { }, | ||||
| mutateCurrentWorkspace: () => { }, | |||||
| pageContainerRef: createRef(), | pageContainerRef: createRef(), | ||||
| langeniusVersionInfo: initialLangeniusVersionInfo, | langeniusVersionInfo: initialLangeniusVersionInfo, | ||||
| useSelector, | useSelector, | ||||
| const { data: appList, mutate: mutateApps } = useSWR({ url: '/apps', params: { page: 1 } }, fetchAppList) | 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: 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 [userProfile, setUserProfile] = useState<UserProfileResponse>() | ||||
| const [langeniusVersionInfo, setLangeniusVersionInfo] = useState<LangGeniusVersionResponse>(initialLangeniusVersionInfo) | 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) { | if (userProfileResponse && !userProfileResponse.bodyUsed) { | ||||
| const result = await userProfileResponse.json() | const result = await userProfileResponse.json() | ||||
| setUserProfile(result) | setUserProfile(result) | ||||
| const versionData = await fetchLanggeniusVersion({ url: '/version', params: { current_version } }) | const versionData = await fetchLanggeniusVersion({ url: '/version', params: { current_version } }) | ||||
| setLangeniusVersionInfo({ ...versionData, current_version, latest_version: versionData.version, current_env }) | setLangeniusVersionInfo({ ...versionData, current_version, latest_version: versionData.version, current_env }) | ||||
| } | } | ||||
| } | |||||
| }, [userProfileResponse]) | |||||
| useEffect(() => { | useEffect(() => { | ||||
| updateUserProfileAndVersion() | updateUserProfileAndVersion() | ||||
| }, [userProfileResponse]) | |||||
| }, [updateUserProfileAndVersion, userProfileResponse]) | |||||
| useEffect(() => { | |||||
| if (currentWorkspaceResponse) | |||||
| setCurrentWorkspace(currentWorkspaceResponse) | |||||
| }, [currentWorkspaceResponse]) | |||||
| if (!appList || !userProfile) | if (!appList || !userProfile) | ||||
| return <Loading type='app' /> | return <Loading type='app' /> | ||||
| return ( | 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'> | <div ref={pageContainerRef} className='relative flex flex-col h-full overflow-auto bg-gray-100'> | ||||
| {children} | {children} | ||||
| </div> | </div> |
| current: boolean | current: boolean | ||||
| } | } | ||||
| export type ICurrentWorkspace = Omit<IWorkspace, 'current'> & { | |||||
| role: 'normal' | 'admin' | 'owner' | |||||
| providers: Provider[] | |||||
| in_trail: boolean | |||||
| trial_end_reason?: string | |||||
| } | |||||
| export type DataSourceNotionPage = { | export type DataSourceNotionPage = { | ||||
| page_icon: null | { | page_icon: null | { | ||||
| type: string | null | type: string | null |
| import { del, get, patch, post, put } from './base' | import { del, get, patch, post, put } from './base' | ||||
| import type { | import type { | ||||
| AccountIntegrate, CommonResponse, DataSourceNotion, | AccountIntegrate, CommonResponse, DataSourceNotion, | ||||
| ICurrentWorkspace, | |||||
| IWorkspace, LangGeniusVersionResponse, Member, | IWorkspace, LangGeniusVersionResponse, Member, | ||||
| OauthResponse, PluginProvider, Provider, ProviderAnthropicToken, ProviderAzureToken, | OauthResponse, PluginProvider, Provider, ProviderAnthropicToken, ProviderAzureToken, | ||||
| SetupStatusResponse, TenantInfoResponse, UserProfileOriginResponse, | SetupStatusResponse, TenantInfoResponse, UserProfileOriginResponse, | ||||
| return get(`/files/${fileID}/preview`) as Promise<{ content: 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 }) => { | export const fetchWorkspaces: Fetcher<{ workspaces: IWorkspace[] }, { url: string; params: Record<string, any> }> = ({ url, params }) => { | ||||
| return get(url, { params }) as Promise<{ workspaces: IWorkspace[] }> | return get(url, { params }) as Promise<{ workspaces: IWorkspace[] }> | ||||
| } | } |