Initial commit

This commit is contained in:
John Wang
2023-05-15 08:51:32 +08:00
commit db896255d6
744 changed files with 56028 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
import React from 'react'
import Configuration from '@/app/components/app/configuration'
const IConfiguration = async () => {
return (
<Configuration />
)
}
export default IConfiguration

View File

@@ -0,0 +1,18 @@
import React from 'react'
import { getDictionary } from '@/i18n/server'
import { type Locale } from '@/i18n'
import DevelopMain from '@/app/components/develop'
export type IDevelopProps = {
params: { locale: Locale; appId: string }
}
const Develop = async ({
params: { locale, appId },
}: IDevelopProps) => {
const dictionary = await getDictionary(locale)
return <DevelopMain appId={appId} dictionary={dictionary} />
}
export default Develop

View File

@@ -0,0 +1,57 @@
'use client'
import type { FC } from 'react'
import React, { useEffect } from 'react'
import cn from 'classnames'
import useSWR from 'swr'
import { useTranslation } from 'react-i18next'
import {
ChartBarSquareIcon,
Cog8ToothIcon,
CommandLineIcon,
DocumentTextIcon,
} from '@heroicons/react/24/outline'
import {
ChartBarSquareIcon as ChartBarSquareSolidIcon,
Cog8ToothIcon as Cog8ToothSolidIcon,
CommandLineIcon as CommandLineSolidIcon,
DocumentTextIcon as DocumentTextSolidIcon,
} from '@heroicons/react/24/solid'
import s from './style.module.css'
import AppSideBar from '@/app/components/app-sidebar'
import { fetchAppDetail } from '@/service/apps'
export type IAppDetailLayoutProps = {
children: React.ReactNode
params: { appId: string }
}
const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
const {
children,
params: { appId }, // get appId in path
} = props
const { t } = useTranslation()
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 appModeName = response?.mode?.toUpperCase() === 'COMPLETION' ? t('common.appModes.completionApp') : t('common.appModes.chatApp')
useEffect(() => {
if (response?.name)
document.title = `${(response.name || 'App')} - Dify`
}, [response])
if (!response)
return null
return (
<div className={cn(s.app, 'flex', 'overflow-hidden')}>
<AppSideBar title={response.name} desc={appModeName} navigation={navigation} />
<div className="bg-white grow">{children}</div>
</div>
)
}
export default React.memo(AppDetailLayout)

View File

@@ -0,0 +1,16 @@
import React from 'react'
import Main from '@/app/components/app/log'
export type IProps = {
params: { appId: string }
}
const Logs = async ({
params: { appId },
}: IProps) => {
return (
<Main appId={appId} />
)
}
export default Logs

View File

@@ -0,0 +1,86 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import useSWR, { useSWRConfig } from 'swr'
import AppCard from '@/app/components/app/overview/appCard'
import Loading from '@/app/components/base/loading'
import { ToastContext } from '@/app/components/base/toast'
import { fetchAppDetail, updateAppApiStatus, updateAppSiteAccessToken, updateAppSiteConfig, updateAppSiteStatus } from '@/service/apps'
import type { IToastProps } from '@/app/components/base/toast'
import type { App } from '@/types/app'
export type ICardViewProps = {
appId: string
}
type IParams = {
url: string
body?: Record<string, any>
}
export async function asyncRunSafe<T>(func: (val: IParams) => Promise<T>, params: IParams, callback: (props: IToastProps) => void, dict?: any): Promise<[string?, T?]> {
try {
const res = await func(params)
callback && callback({ type: 'success', message: dict('common.actionMsg.modifiedSuccessfully') })
return [undefined, res]
}
catch (err) {
callback && callback({ type: 'error', message: dict('common.actionMsg.modificationFailed') })
return [(err as Error).message, undefined]
}
}
const CardView: FC<ICardViewProps> = ({ appId }) => {
const detailParams = { url: '/apps', id: appId }
const { data: response } = useSWR(detailParams, fetchAppDetail)
const { mutate } = useSWRConfig()
const { notify } = useContext(ToastContext)
const { t } = useTranslation()
if (!response)
return <Loading />
const onChangeSiteStatus = async (value: boolean) => {
const [err] = await asyncRunSafe<App>(updateAppSiteStatus as any, { url: `/apps/${appId}/site-enable`, body: { enable_site: value } }, notify, t)
if (!err)
mutate(detailParams)
}
const onChangeApiStatus = async (value: boolean) => {
const [err] = await asyncRunSafe<App>(updateAppApiStatus as any, { url: `/apps/${appId}/api-enable`, body: { enable_api: value } }, notify, t)
if (!err)
mutate(detailParams)
}
const onSaveSiteConfig = async (params: any) => {
const [err] = await asyncRunSafe<App>(updateAppSiteConfig as any, { url: `/apps/${appId}/site`, body: params }, notify, t)
if (!err)
mutate(detailParams)
}
const onGenerateCode = async () => {
const [err] = await asyncRunSafe<App>(updateAppSiteAccessToken as any, { url: `/apps/${appId}/site/access-token-reset` }, notify, t)
if (!err)
mutate(detailParams)
}
return (
<div className='flex flex-row justify-between w-full mb-6'>
<AppCard
className='mr-3 flex-1'
appInfo={response}
onChangeStatus={onChangeSiteStatus}
onGenerateCode={onGenerateCode}
onSaveSiteConfig={onSaveSiteConfig} />
<AppCard
className='ml-3 flex-1'
cardType='api'
appInfo={response}
onChangeStatus={onChangeApiStatus} />
</div>
)
}
export default CardView

View File

@@ -0,0 +1,52 @@
'use client'
import React, { useState } from 'react'
import dayjs from 'dayjs'
import quarterOfYear from 'dayjs/plugin/quarterOfYear'
import { useTranslation } from 'react-i18next'
import type { PeriodParams } from '@/app/components/app/overview/appChart'
import { ConversationsChart, CostChart, EndUsersChart } from '@/app/components/app/overview/appChart'
import type { Item } from '@/app/components/base/select'
import { SimpleSelect } from '@/app/components/base/select'
import { TIME_PERIOD_LIST } from '@/app/components/app/log/filter'
dayjs.extend(quarterOfYear)
const today = dayjs()
const queryDateFormat = 'YYYY-MM-DD HH:mm'
export type IChartViewProps = {
appId: string
}
export default function ChartView({ appId }: IChartViewProps) {
const { t } = useTranslation()
const [period, setPeriod] = useState<PeriodParams>({ name: t('appLog.filter.period.last7days'), query: { start: today.subtract(7, 'day').format(queryDateFormat), end: today.format(queryDateFormat) } })
const onSelect = (item: Item) => {
setPeriod({ name: item.name, query: { start: today.subtract(item.value as number, 'day').format(queryDateFormat), end: today.format(queryDateFormat) } })
}
return (
<div>
<div className='flex flex-row items-center mt-8 mb-4 text-gray-900 text-base'>
<span className='mr-3'>{t('appOverview.analysis.title')}</span>
<SimpleSelect
items={TIME_PERIOD_LIST.map(item => ({ value: item.value, name: t(`appLog.filter.period.${item.name}`) }))}
className='mt-0 !w-40'
onSelect={onSelect}
defaultValue={7}
/>
</div>
<div className='flex flex-row w-full mb-6'>
<div className='flex-1 mr-3'>
<ConversationsChart period={period} id={appId} />
</div>
<div className='flex-1 ml-3'>
<EndUsersChart period={period} id={appId} />
</div>
</div>
<CostChart period={period} id={appId} />
</div>
)
}

View File

@@ -0,0 +1,30 @@
import React from 'react'
import WelcomeBanner, { EditKeyPopover } from './welcome-banner'
import ChartView from './chartView'
import CardView from './cardView'
import { getLocaleOnServer } from '@/i18n/server'
import { useTranslation } from '@/i18n/i18next-serverside-config'
export type IDevelopProps = {
params: { appId: string }
}
const Overview = async ({
params: { appId },
}: IDevelopProps) => {
const locale = getLocaleOnServer()
const { t } = await useTranslation(locale, 'app-overview')
return (
<div className="h-full px-16 py-6 overflow-scroll">
<WelcomeBanner />
<div className='flex flex-row items-center justify-between mb-4 text-xl text-gray-900'>
{t('overview.title')}
<EditKeyPopover />
</div>
<CardView appId={appId} />
<ChartView appId={appId} />
</div>
)
}
export default Overview

View File

@@ -0,0 +1,200 @@
'use client'
import type { FC } from 'react'
import React, { useState } from 'react'
import { useContext } from 'use-context-selector'
import { useTranslation } from 'react-i18next'
import Link from 'next/link'
import useSWR, { useSWRConfig } from 'swr'
import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline'
import { ExclamationCircleIcon } from '@heroicons/react/24/solid'
import { debounce } from 'lodash-es'
import Popover from '@/app/components/base/popover'
import Button from '@/app/components/base/button'
import Tag from '@/app/components/base/tag'
import { ToastContext } from '@/app/components/base/toast'
import { updateOpenAIKey, validateOpenAIKey } from '@/service/apps'
import { fetchTenantInfo } from '@/service/common'
import I18n from '@/context/i18n'
type IStatusType = 'normal' | 'verified' | 'error' | 'error-api-key-exceed-bill'
const STATUS_COLOR_MAP = {
normal: { color: '', bgColor: 'bg-primary-50', borderColor: 'border-primary-100' },
error: { color: 'text-red-600', bgColor: 'bg-red-50', borderColor: 'border-red-100' },
verified: { color: '', bgColor: 'bg-green-50', borderColor: 'border-green-100' },
'error-api-key-exceed-bill': { color: 'text-red-600', bgColor: 'bg-red-50', borderColor: 'border-red-100' },
}
const CheckCircleIcon: FC<{ className?: string }> = ({ className }) => {
return <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
<rect width="20" height="20" rx="10" fill="#DEF7EC" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.6947 6.70495C14.8259 6.83622 14.8996 7.01424 14.8996 7.19985C14.8996 7.38547 14.8259 7.56348 14.6947 7.69475L9.0947 13.2948C8.96343 13.426 8.78541 13.4997 8.5998 13.4997C8.41418 13.4997 8.23617 13.426 8.1049 13.2948L5.3049 10.4948C5.17739 10.3627 5.10683 10.1859 5.10842 10.0024C5.11002 9.81883 5.18364 9.64326 5.31342 9.51348C5.44321 9.38369 5.61878 9.31007 5.80232 9.30848C5.98585 9.30688 6.16268 9.37744 6.2947 9.50495L8.5998 11.8101L13.7049 6.70495C13.8362 6.57372 14.0142 6.5 14.1998 6.5C14.3854 6.5 14.5634 6.57372 14.6947 6.70495Z" fill="#046C4E" />
</svg>
}
type IEditKeyDiv = {
className?: string
showInPopover?: boolean
onClose?: () => void
getTenantInfo?: () => void
}
const EditKeyDiv: FC<IEditKeyDiv> = ({ className = '', showInPopover = false, onClose, getTenantInfo }) => {
const [inputValue, setInputValue] = useState<string | undefined>()
const [editStatus, setEditStatus] = useState<IStatusType>('normal')
const [loading, setLoading] = useState(false)
const [validating, setValidating] = useState(false)
const { notify } = useContext(ToastContext)
const { t } = useTranslation()
const { locale } = useContext(I18n)
// Hide the pop-up window and need to get the latest key again
// If the key is valid, the edit button will be hidden later
const onClosePanel = () => {
getTenantInfo && getTenantInfo()
onClose && onClose()
}
const onSaveKey = async () => {
if (editStatus === 'verified') {
setLoading(true)
try {
await updateOpenAIKey({ url: '/providers/openai/token', body: { token: inputValue ?? '' } })
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
onClosePanel()
}
catch (err) {
notify({ type: 'error', message: t('common.actionMsg.modificationFailed') })
}
finally {
setLoading(false)
}
}
}
const validateKey = async (value: string) => {
try {
setValidating(true)
const res = await validateOpenAIKey({ url: '/providers/openai/token-validate', body: { token: value ?? '' } })
setEditStatus(res.result === 'success' ? 'verified' : 'error')
}
catch (err: any) {
if (err.status === 400) {
err.json().then(({ code }: any) => {
if (code === 'provider_request_failed') {
setEditStatus('error-api-key-exceed-bill')
}
})
} else {
setEditStatus('error')
}
}
finally {
setValidating(false)
}
}
const renderErrorMessage = () => {
if (validating) {
return (
<div className={`text-primary-600 mt-2 text-xs`}>
{t('common.provider.validating')}
</div>
)
}
if (editStatus === 'error-api-key-exceed-bill') {
return (
<div className={`text-[#D92D20] mt-2 text-xs`}>
{t('common.provider.apiKeyExceedBill')}
{locale === 'en' ? ' ' : ''}
<Link
className='underline'
href="https://platform.openai.com/account/api-keys"
target={'_blank'}>
{locale === 'en' ? 'this link' : '这篇文档'}
</Link>
</div>
)
}
if (editStatus === 'error') {
return (
<div className={`text-[#D92D20] mt-2 text-xs`}>
{t('common.provider.invalidKey')}
</div>
)
}
return null
}
return (
<div className={`flex flex-col w-full rounded-lg px-8 py-6 border-solid border-[0.5px] ${className} ${Object.values(STATUS_COLOR_MAP[editStatus]).join(' ')}`}>
{!showInPopover && <p className='text-xl font-medium text-gray-800'>{t('appOverview.welcome.firstStepTip')}</p>}
<p className={`${showInPopover ? 'text-sm' : 'text-xl'} font-medium text-gray-800`}>{t('appOverview.welcome.enterKeyTip')} {showInPopover ? '' : '👇'}</p>
<div className='relative mt-2'>
<input type="text"
className={`h-9 w-96 max-w-full py-2 pl-2 text-gray-900 rounded-lg bg-white sm:text-xs focus:ring-blue-500 focus:border-blue-500 shadow-sm ${editStatus === 'normal' ? 'pr-2' : 'pr-8'}`}
placeholder={t('appOverview.welcome.placeholder') || ''}
onChange={debounce((e) => {
setInputValue(e.target.value)
if (!e.target.value) {
setEditStatus('normal')
return
}
validateKey(e.target.value)
}, 300)}
/>
{editStatus === 'verified' && <div className="absolute inset-y-0 right-0 flex flex-row-reverse items-center pr-6 pointer-events-none">
<CheckCircleIcon className="rounded-lg" />
</div>}
{(editStatus === 'error' || editStatus === 'error-api-key-exceed-bill') && <div className="absolute inset-y-0 right-0 flex flex-row-reverse items-center pr-6 pointer-events-none">
<ExclamationCircleIcon className="w-5 h-5 text-red-800" />
</div>}
{showInPopover ? null : <Button type='primary' onClick={onSaveKey} className='!h-9 !inline-block ml-2' loading={loading} disabled={editStatus !== 'verified'}>{t('common.operation.save')}</Button>}
</div>
{renderErrorMessage()}
<Link className="inline-flex items-center mt-2 text-xs font-normal cursor-pointer text-primary-600 w-fit" href="https://platform.openai.com/account/api-keys" target={'_blank'}>
{t('appOverview.welcome.getKeyTip')}
<ArrowTopRightOnSquareIcon className='w-3 h-3 ml-1 text-primary-600' aria-hidden="true" />
</Link>
{showInPopover && <div className='flex justify-end mt-6'>
<Button className='flex-shrink-0 mr-2' onClick={onClosePanel}>{t('common.operation.cancel')}</Button>
<Button type='primary' className='flex-shrink-0' onClick={onSaveKey} loading={loading} disabled={editStatus !== 'verified'}>{t('common.operation.save')}</Button>
</div>}
</div>
)
}
const WelcomeBanner: FC = () => {
const { data: userInfo } = useSWR({ url: '/info' }, fetchTenantInfo)
if (!userInfo)
return null
return userInfo?.providers?.find(({ token_is_set }) => token_is_set) ? null : <EditKeyDiv className='mb-8' />
}
export const EditKeyPopover: FC = () => {
const { data: userInfo } = useSWR({ url: '/info' }, fetchTenantInfo)
const { mutate } = useSWRConfig()
if (!userInfo)
return null
const getTenantInfo = () => {
mutate({ url: '/info' })
}
// In this case, the edit button is displayed
const targetProvider = userInfo?.providers?.some(({ token_is_set, is_valid }) => token_is_set && is_valid)
return (
!targetProvider
? <div className='flex items-center'>
<Tag className='mr-2 h-fit' color='red'><ExclamationCircleIcon className='h-3.5 w-3.5 mr-2' />OpenAI API key invalid</Tag>
<Popover
htmlContent={<EditKeyDiv className='!border-0' showInPopover={true} getTenantInfo={getTenantInfo} />}
trigger='click'
position='br'
btnElement='Edit'
btnClassName='text-primary-600 !text-xs px-3 py-1.5'
className='!p-0 !w-[464px] h-[200px]'
/>
</div>
: null)
}
export default WelcomeBanner

View File

@@ -0,0 +1,5 @@
.app {
height: calc(100vh - 56px);
border-radius: 16px 16px 0px 0px;
box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.05), 0px 0px 2px -1px rgba(0, 0, 0, 0.03);
}

View File

@@ -0,0 +1,16 @@
import type { FC } from 'react'
import React from 'react'
export type IAppDetail = {
children: React.ReactNode
}
const AppDetail: FC<IAppDetail> = ({ children }) => {
return (
<>
{children}
</>
)
}
export default React.memo(AppDetail)