Initial commit
85
web/app/(commonLayout)/_layout-client.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { usePathname, useRouter, useSelectedLayoutSegments } from 'next/navigation'
|
||||
import useSWR, { SWRConfig } from 'swr'
|
||||
import Header from '../components/header'
|
||||
import { fetchAppList } from '@/service/apps'
|
||||
import { fetchDatasets } from '@/service/datasets'
|
||||
import { fetchLanggeniusVersion, fetchUserProfile, logout } from '@/service/common'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import AppContext from '@/context/app-context'
|
||||
import DatasetsContext from '@/context/datasets-context'
|
||||
import type { LangGeniusVersionResponse, UserProfileResponse } from '@/models/common'
|
||||
|
||||
export type ICommonLayoutProps = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const CommonLayout: FC<ICommonLayoutProps> = ({ children }) => {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const segments = useSelectedLayoutSegments()
|
||||
const pattern = pathname.replace(/.*\/app\//, '')
|
||||
const [idOrMethod] = pattern.split('/')
|
||||
const isNotDetailPage = idOrMethod === 'list'
|
||||
|
||||
const appId = isNotDetailPage ? '' : idOrMethod
|
||||
|
||||
const { data: appList, mutate: mutateApps } = useSWR({ url: '/apps', params: { page: 1 } }, fetchAppList)
|
||||
const { data: datasetList, mutate: mutateDatasets } = useSWR(segments[0] === 'datasets' ? { url: '/datasets', params: { page: 1 } } : null, fetchDatasets)
|
||||
const { data: userProfileResponse, mutate: mutateUserProfile } = useSWR({ url: '/account/profile', params: {} }, fetchUserProfile)
|
||||
|
||||
const [userProfile, setUserProfile] = useState<UserProfileResponse>()
|
||||
const [langeniusVersionInfo, setLangeniusVersionInfo] = useState<LangGeniusVersionResponse>()
|
||||
const updateUserProfileAndVersion = async () => {
|
||||
if (userProfileResponse && !userProfileResponse.bodyUsed) {
|
||||
const result = await userProfileResponse.json()
|
||||
setUserProfile(result)
|
||||
const current_version = userProfileResponse.headers.get('x-version')
|
||||
const current_env = userProfileResponse.headers.get('x-env')
|
||||
const versionData = await fetchLanggeniusVersion({ url: '/version', params: { current_version } })
|
||||
setLangeniusVersionInfo({ ...versionData, current_version, latest_version: versionData.version, current_env })
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
updateUserProfileAndVersion()
|
||||
}, [userProfileResponse])
|
||||
|
||||
if (!appList || !userProfile || !langeniusVersionInfo)
|
||||
return <Loading type='app' />
|
||||
|
||||
const curApp = appList?.data.find(opt => opt.id === appId)
|
||||
const currentDatasetId = segments[0] === 'datasets' && segments[2]
|
||||
const currentDataset = datasetList?.data?.find(opt => opt.id === currentDatasetId)
|
||||
|
||||
// if (!isNotDetailPage && !curApp) {
|
||||
// alert('app not found') // TODO: use toast. Now can not get toast context here.
|
||||
// // notify({ type: 'error', message: 'App not found' })
|
||||
// router.push('/apps')
|
||||
// }
|
||||
|
||||
const onLogout = async () => {
|
||||
await logout({
|
||||
url: '/logout',
|
||||
params: {},
|
||||
})
|
||||
router.push('/signin')
|
||||
}
|
||||
|
||||
return (
|
||||
<SWRConfig value={{
|
||||
shouldRetryOnError: false
|
||||
}}>
|
||||
<AppContext.Provider value={{ apps: appList.data, mutateApps, userProfile, mutateUserProfile }}>
|
||||
<DatasetsContext.Provider value={{ datasets: datasetList?.data || [], mutateDatasets, currentDataset }}>
|
||||
<div className='relative flex flex-col h-full overflow-scroll bg-gray-100'>
|
||||
<Header isBordered={['/apps', '/datasets'].includes(pathname)} curApp={curApp as any} appItems={appList.data} userProfile={userProfile} onLogout={onLogout} langeniusVersionInfo={langeniusVersionInfo} />
|
||||
{children}
|
||||
</div>
|
||||
</DatasetsContext.Provider>
|
||||
</AppContext.Provider>
|
||||
</SWRConfig>
|
||||
)
|
||||
}
|
||||
export default React.memo(CommonLayout)
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from 'react'
|
||||
import Configuration from '@/app/components/app/configuration'
|
||||
|
||||
const IConfiguration = async () => {
|
||||
return (
|
||||
<Configuration />
|
||||
)
|
||||
}
|
||||
|
||||
export default IConfiguration
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
16
web/app/(commonLayout)/app/(appDetailLayout)/layout.tsx
Normal 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)
|
||||
76
web/app/(commonLayout)/apps/AppCard.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
'use client'
|
||||
|
||||
import { useContext, useContextSelector } from 'use-context-selector'
|
||||
import Link from 'next/link'
|
||||
import type { MouseEventHandler } from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import style from '../list.module.css'
|
||||
import AppModeLabel from './AppModeLabel'
|
||||
import type { App } from '@/types/app'
|
||||
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'
|
||||
|
||||
export type AppCardProps = {
|
||||
app: App
|
||||
}
|
||||
|
||||
const AppCard = ({
|
||||
app,
|
||||
}: AppCardProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
|
||||
const mutateApps = useContextSelector(AppsContext, state => state.mutateApps)
|
||||
|
||||
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
|
||||
const onDeleteClick: MouseEventHandler = useCallback((e) => {
|
||||
e.preventDefault()
|
||||
setShowConfirmDelete(true)
|
||||
}, [])
|
||||
const onConfirmDelete = useCallback(async () => {
|
||||
try {
|
||||
await deleteApp(app.id)
|
||||
notify({ type: 'success', message: t('app.appDeleted') })
|
||||
mutateApps()
|
||||
}
|
||||
catch (e: any) {
|
||||
notify({ type: 'error', message: `${t('app.appDeleteFailed')}${'message' in e ? `: ${e.message}` : ''}` })
|
||||
}
|
||||
setShowConfirmDelete(false)
|
||||
}, [app.id])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Link href={`/app/${app.id}/overview`} className={style.listItem}>
|
||||
<div className={style.listItemTitle}>
|
||||
<AppIcon size='small' />
|
||||
<div className={style.listItemHeading}>
|
||||
<div className={style.listItemHeadingContent}>{app.name}</div>
|
||||
</div>
|
||||
<span className={style.deleteAppIcon} onClick={onDeleteClick} />
|
||||
</div>
|
||||
<div className={style.listItemDescription}>{app.model_config?.pre_prompt}</div>
|
||||
<div className={style.listItemFooter}>
|
||||
<AppModeLabel mode={app.mode} />
|
||||
</div>
|
||||
|
||||
{showConfirmDelete && (
|
||||
<Confirm
|
||||
title={t('app.deleteAppConfirmTitle')}
|
||||
content={t('app.deleteAppConfirmContent')}
|
||||
isShow={showConfirmDelete}
|
||||
onClose={() => setShowConfirmDelete(false)}
|
||||
onConfirm={onConfirmDelete}
|
||||
onCancel={() => setShowConfirmDelete(false)}
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default AppCard
|
||||
26
web/app/(commonLayout)/apps/AppModeLabel.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
'use client'
|
||||
|
||||
import classNames from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { type AppMode } from '@/types/app'
|
||||
import style from '../list.module.css'
|
||||
|
||||
export type AppModeLabelProps = {
|
||||
mode: AppMode
|
||||
className?: string
|
||||
}
|
||||
|
||||
const AppModeLabel = ({
|
||||
mode,
|
||||
className,
|
||||
}: AppModeLabelProps) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<span className={classNames('flex items-center w-fit h-6 gap-1 px-2 text-gray-500 text-xs border border-gray-100 rounded', className)}>
|
||||
<span className={classNames(style.listItemFooterIcon, mode === 'chat' && style.solidChatIcon, mode === 'completion' && style.solidCompletionIcon)} />
|
||||
{t(`app.modes.${mode}`)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export default AppModeLabel
|
||||
23
web/app/(commonLayout)/apps/Apps.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import AppCard from './AppCard'
|
||||
import NewAppCard from './NewAppCard'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
|
||||
const Apps = () => {
|
||||
const { apps, mutateApps } = useAppContext()
|
||||
|
||||
useEffect(() => {
|
||||
mutateApps()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<nav className='grid content-start grid-cols-1 gap-4 px-12 pt-8 sm:grid-cols-2 lg:grid-cols-4 grow shrink-0'>
|
||||
{apps.map(app => (<AppCard key={app.id} app={app} />))}
|
||||
<NewAppCard />
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
export default Apps
|
||||
29
web/app/(commonLayout)/apps/NewAppCard.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import classNames from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import style from '../list.module.css'
|
||||
import NewAppDialog from './NewAppDialog'
|
||||
|
||||
const CreateAppCard = () => {
|
||||
const { t } = useTranslation()
|
||||
const [showNewAppDialog, setShowNewAppDialog] = useState(false)
|
||||
|
||||
return (
|
||||
<a className={classNames(style.listItem, style.newItemCard)} onClick={() => setShowNewAppDialog(true)}>
|
||||
<div className={style.listItemTitle}>
|
||||
<span className={style.newItemIcon}>
|
||||
<span className={classNames(style.newItemIconImage, style.newItemIconAdd)} />
|
||||
</span>
|
||||
<div className={classNames(style.listItemHeading, style.newItemCardHeading)}>
|
||||
{t('app.createApp')}
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className='text-xs text-gray-500'>{t('app.createFromConfigFile')}</div> */}
|
||||
<NewAppDialog show={showNewAppDialog} onClose={() => setShowNewAppDialog(false)} />
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreateAppCard
|
||||
193
web/app/(commonLayout)/apps/NewAppDialog.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
'use client'
|
||||
|
||||
import type { MouseEventHandler } from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import useSWR from 'swr'
|
||||
import classNames from 'classnames'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useContext, useContextSelector } from 'use-context-selector'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import style from '../list.module.css'
|
||||
import AppModeLabel from './AppModeLabel'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Dialog from '@/app/components/base/dialog'
|
||||
import type { AppMode } from '@/types/app'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { createApp, fetchAppTemplates } from '@/service/apps'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import AppsContext from '@/context/app-context'
|
||||
|
||||
type NewAppDialogProps = {
|
||||
show: boolean
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
const NewAppDialog = ({ show, onClose }: NewAppDialogProps) => {
|
||||
const router = useRouter()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const { t } = useTranslation()
|
||||
|
||||
const nameInputRef = useRef<HTMLInputElement>(null)
|
||||
const [newAppMode, setNewAppMode] = useState<AppMode>()
|
||||
const [isWithTemplate, setIsWithTemplate] = useState(false)
|
||||
const [selectedTemplateIndex, setSelectedTemplateIndex] = useState<number>(-1)
|
||||
const mutateApps = useContextSelector(AppsContext, state => state.mutateApps)
|
||||
|
||||
const { data: templates, mutate } = useSWR({ url: '/app-templates' }, fetchAppTemplates)
|
||||
const mutateTemplates = useCallback(
|
||||
() => mutate(),
|
||||
[],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (show) {
|
||||
mutateTemplates()
|
||||
setIsWithTemplate(false)
|
||||
}
|
||||
}, [show])
|
||||
|
||||
const isCreatingRef = useRef(false)
|
||||
const onCreate: MouseEventHandler = useCallback(async () => {
|
||||
const name = nameInputRef.current?.value
|
||||
if (!name) {
|
||||
notify({ type: 'error', message: t('app.newApp.nameNotEmpty') })
|
||||
return
|
||||
}
|
||||
if (!templates || (isWithTemplate && !(selectedTemplateIndex > -1))) {
|
||||
notify({ type: 'error', message: t('app.newApp.appTemplateNotSelected') })
|
||||
return
|
||||
}
|
||||
if (!isWithTemplate && !newAppMode) {
|
||||
notify({ type: 'error', message: t('app.newApp.appTypeRequired') })
|
||||
return
|
||||
}
|
||||
if (isCreatingRef.current)
|
||||
return
|
||||
isCreatingRef.current = true
|
||||
try {
|
||||
const app = await createApp({
|
||||
name,
|
||||
mode: isWithTemplate ? templates.data[selectedTemplateIndex].mode : newAppMode!,
|
||||
config: isWithTemplate ? templates.data[selectedTemplateIndex].model_config : undefined,
|
||||
})
|
||||
if (onClose)
|
||||
onClose()
|
||||
notify({ type: 'success', message: t('app.newApp.appCreated') })
|
||||
mutateApps()
|
||||
router.push(`/app/${app.id}/overview`)
|
||||
}
|
||||
catch (e) {
|
||||
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
|
||||
}
|
||||
isCreatingRef.current = false
|
||||
}, [isWithTemplate, newAppMode, notify, router, templates, selectedTemplateIndex])
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
show={show}
|
||||
title={t('app.newApp.startToCreate')}
|
||||
footer={
|
||||
<>
|
||||
<Button onClick={onClose}>{t('app.newApp.Cancel')}</Button>
|
||||
<Button type="primary" onClick={onCreate}>{t('app.newApp.Create')}</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<h3 className={style.newItemCaption}>{t('app.newApp.captionName')}</h3>
|
||||
|
||||
<div className='flex items-center justify-between gap-3 mb-8'>
|
||||
<AppIcon size='large' />
|
||||
<input ref={nameInputRef} className='h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg grow' />
|
||||
</div>
|
||||
|
||||
<div className='h-[247px]'>
|
||||
<div className={style.newItemCaption}>
|
||||
<h3 className='inline'>{t('app.newApp.captionAppType')}</h3>
|
||||
{isWithTemplate && (
|
||||
<>
|
||||
<span className='block ml-[9px] mr-[9px] w-[1px] h-[13px] bg-gray-200' />
|
||||
<span
|
||||
className='inline-flex items-center gap-1 text-xs font-medium cursor-pointer text-primary-600'
|
||||
onClick={() => setIsWithTemplate(false)}
|
||||
>
|
||||
{t('app.newApp.hideTemplates')}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{isWithTemplate
|
||||
? (
|
||||
<ul className='grid grid-cols-2 gap-4'>
|
||||
{templates?.data?.map((template, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className={classNames(style.listItem, style.selectable, selectedTemplateIndex === index && style.selected)}
|
||||
onClick={() => setSelectedTemplateIndex(index)}
|
||||
>
|
||||
<div className={style.listItemTitle}>
|
||||
<AppIcon size='small' />
|
||||
<div className={style.listItemHeading}>
|
||||
<div className={style.listItemHeadingContent}>{template.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.listItemDescription}>{template.model_config?.pre_prompt}</div>
|
||||
<AppModeLabel mode={template.mode} className='mt-2' />
|
||||
{/* <AppModeLabel mode='chat' className='mt-2' /> */}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<ul className='grid grid-cols-2 gap-4'>
|
||||
<li
|
||||
className={classNames(style.listItem, style.selectable, newAppMode === 'chat' && style.selected)}
|
||||
onClick={() => setNewAppMode('chat')}
|
||||
>
|
||||
<div className={style.listItemTitle}>
|
||||
<span className={style.newItemIcon}>
|
||||
<span className={classNames(style.newItemIconImage, style.newItemIconChat)} />
|
||||
</span>
|
||||
<div className={style.listItemHeading}>
|
||||
<div className={style.listItemHeadingContent}>{t('app.newApp.chatApp')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.listItemDescription}>{t('app.newApp.chatAppIntro')}</div>
|
||||
<div className={classNames(style.listItemFooter, 'justify-end')}>
|
||||
<a className={style.listItemLink} href='https://udify.app/chat/7CQBa5yyvYLSkZtx' target='_blank'>{t('app.newApp.previewDemo')}<span className={classNames(style.linkIcon, style.grayLinkIcon)} /></a>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
className={classNames(style.listItem, style.selectable, newAppMode === 'completion' && style.selected)}
|
||||
onClick={() => setNewAppMode('completion')}
|
||||
>
|
||||
<div className={style.listItemTitle}>
|
||||
<span className={style.newItemIcon}>
|
||||
<span className={classNames(style.newItemIconImage, style.newItemIconComplete)} />
|
||||
</span>
|
||||
<div className={style.listItemHeading}>
|
||||
<div className={style.listItemHeadingContent}>{t('app.newApp.completeApp')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.listItemDescription}>{t('app.newApp.completeAppIntro')}</div>
|
||||
<div className={classNames(style.listItemFooter, 'justify-end')}>
|
||||
<a className={style.listItemLink} href='https://udify.app/completion/aeFTj0VCb3Ok3TUE' target='_blank'>{t('app.newApp.previewDemo')}<span className={classNames(style.linkIcon, style.grayLinkIcon)} /></a>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div className='flex items-center h-[34px] mt-2'>
|
||||
<span
|
||||
className='inline-flex items-center gap-1 text-xs font-medium cursor-pointer text-primary-600'
|
||||
onClick={() => setIsWithTemplate(true)}
|
||||
>
|
||||
{t('app.newApp.showTemplates')}<span className={style.rightIcon} />
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default NewAppDialog
|
||||
3
web/app/(commonLayout)/apps/assets/add.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 4V8M8 8V12M8 8H12M8 8H4" stroke="#6B7280" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 206 B |
4
web/app/(commonLayout)/apps/assets/chat-solid.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.631586 8.25C0.631586 6.46656 2.04586 5 3.8158 5C5.58573 5 7.00001 6.46656 7.00001 8.25C7.00001 10.0334 5.58573 11.5 3.8158 11.5C3.45197 11.5 3.10149 11.4375 2.77474 11.3222C2.72073 11.3031 2.68723 11.2913 2.66266 11.2832C2.65821 11.2817 2.65456 11.2806 2.65164 11.2796L2.64892 11.2799C2.63177 11.2818 2.60839 11.285 2.56507 11.2909L1.06766 11.4954C0.905637 11.5175 0.743029 11.459 0.632239 11.3387C0.521449 11.2185 0.476481 11.0516 0.511825 10.8919L0.817497 9.51109C0.828118 9.46311 0.833802 9.43722 0.837453 9.41817C0.83766 9.4171 0.838022 9.41517 0.838022 9.41517C0.837114 9.412 0.835963 9.40808 0.834525 9.40332C0.826292 9.37605 0.814183 9.33888 0.794499 9.27863C0.688657 8.95463 0.631586 8.60857 0.631586 8.25Z" fill="#98A2B3"/>
|
||||
<path d="M2.57377 4.1863C2.96256 4.06535 3.37698 4 3.80894 4C6.16566 4 8.00006 5.94534 8.00006 8.24999C8.00006 8.65682 7.9429 9.05245 7.8358 9.42816C8.10681 9.37948 8.36964 9.30678 8.6219 9.21229C8.65748 9.19897 8.69298 9.18534 8.72893 9.17304C8.75795 9.17641 8.78684 9.18093 8.81574 9.18517L10.4222 9.42065C10.498 9.43179 10.5841 9.44444 10.6591 9.4487C10.7422 9.45343 10.8713 9.45292 11.0081 9.39408C11.1789 9.32061 11.3164 9.18628 11.3938 9.01716C11.4558 8.88174 11.4593 8.75269 11.4564 8.66955C11.4539 8.59442 11.4433 8.5081 11.4339 8.43202L11.2309 6.78307C11.2256 6.7402 11.2229 6.71768 11.2213 6.70118C11.23 6.66505 11.2466 6.6301 11.2598 6.59546C11.4492 6.09896 11.5526 5.56093 11.5526 5C11.5526 2.51163 9.52304 0.5 7.02632 0.5C4.80843 0.5 2.95915 2.08742 2.57377 4.1863Z" fill="#98A2B3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
3
web/app/(commonLayout)/apps/assets/chat.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.1667 6.66634H15.8333C16.2754 6.66634 16.6993 6.84194 17.0118 7.1545C17.3244 7.46706 17.5 7.89098 17.5 8.33301V13.333C17.5 13.775 17.3244 14.199 17.0118 14.5115C16.6993 14.8241 16.2754 14.9997 15.8333 14.9997H14.1667V18.333L10.8333 14.9997H7.5C7.28111 14.9999 7.06433 14.9569 6.86211 14.8731C6.6599 14.7893 6.47623 14.6663 6.32167 14.5113M6.32167 14.5113L9.16667 11.6663H12.5C12.942 11.6663 13.366 11.4907 13.6785 11.1782C13.9911 10.8656 14.1667 10.4417 14.1667 9.99967V4.99967C14.1667 4.55765 13.9911 4.13372 13.6785 3.82116C13.366 3.5086 12.942 3.33301 12.5 3.33301H4.16667C3.72464 3.33301 3.30072 3.5086 2.98816 3.82116C2.67559 4.13372 2.5 4.55765 2.5 4.99967V9.99967C2.5 10.4417 2.67559 10.8656 2.98816 11.1782C3.30072 11.4907 3.72464 11.6663 4.16667 11.6663H5.83333V14.9997L6.32167 14.5113Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1002 B |
4
web/app/(commonLayout)/apps/assets/completion-solid.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.5 1.00779C6.5 0.994638 6.5 0.988062 6.49943 0.976137C6.48764 0.729248 6.27052 0.51224 6.02363 0.50056C6.01171 0.499996 6.0078 0.499998 6.00001 0.5H4.37933C3.97686 0.499995 3.64468 0.49999 3.37409 0.522098C3.09304 0.545061 2.83469 0.594343 2.59202 0.717989C2.2157 0.909735 1.90973 1.2157 1.71799 1.59202C1.59434 1.83469 1.54506 2.09304 1.5221 2.37409C1.49999 2.64468 1.49999 2.97686 1.5 3.37934V8.62066C1.49999 9.02313 1.49999 9.35532 1.5221 9.62591C1.54506 9.90696 1.59434 10.1653 1.71799 10.408C1.90973 10.7843 2.2157 11.0903 2.59202 11.282C2.83469 11.4057 3.09304 11.4549 3.37409 11.4779C3.64468 11.5 3.97686 11.5 4.37934 11.5H7.62066C8.02314 11.5 8.35532 11.5 8.62591 11.4779C8.90696 11.4549 9.16531 11.4057 9.40798 11.282C9.78431 11.0903 10.0903 10.7843 10.282 10.408C10.4057 10.1653 10.4549 9.90696 10.4779 9.62591C10.5 9.35532 10.5 9.02314 10.5 8.62066V4.99997C10.5 4.9922 10.5 4.98832 10.4994 4.97641C10.4878 4.72949 10.2707 4.51236 10.0238 4.50057C10.0119 4.50001 10.0054 4.50001 9.99225 4.50001L7.78404 4.50001C7.65786 4.50002 7.53496 4.50004 7.43089 4.49153C7.31659 4.48219 7.18172 4.46016 7.04601 4.39101C6.85785 4.29514 6.70487 4.14216 6.609 3.954C6.53985 3.81828 6.51781 3.68342 6.50848 3.56912C6.49997 3.46504 6.49999 3.34215 6.5 3.21596L6.5 1.00779ZM4 6.5C3.72386 6.5 3.5 6.72386 3.5 7C3.5 7.27614 3.72386 7.5 4 7.5H8C8.27614 7.5 8.5 7.27614 8.5 7C8.5 6.72386 8.27614 6.5 8 6.5H4ZM4 8.5C3.72386 8.5 3.5 8.72386 3.5 9C3.5 9.27614 3.72386 9.5 4 9.5H7C7.27614 9.5 7.5 9.27614 7.5 9C7.5 8.72386 7.27614 8.5 7 8.5H4Z" fill="#98A2B3"/>
|
||||
<path d="M9.45398 3.5C9.60079 3.5 9.67419 3.5 9.73432 3.46314C9.81925 3.41107 9.87002 3.28842 9.84674 3.19157C9.83025 3.12299 9.78238 3.07516 9.68665 2.97952L8.02049 1.31336C7.92484 1.21762 7.87701 1.16975 7.80843 1.15326C7.71158 1.12998 7.58893 1.18075 7.53687 1.26567C7.5 1.3258 7.5 1.39921 7.5 1.54602L7.5 3.09998C7.5 3.23999 7.5 3.30999 7.52725 3.36347C7.55122 3.41051 7.58946 3.44876 7.6365 3.47272C7.68998 3.49997 7.75998 3.49997 7.9 3.49998L9.45398 3.5Z" fill="#98A2B3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
3
web/app/(commonLayout)/apps/assets/completion.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.25 11.875V9.6875C16.25 8.1342 14.9908 6.875 13.4375 6.875H12.1875C11.6697 6.875 11.25 6.45527 11.25 5.9375V4.6875C11.25 3.1342 9.9908 1.875 8.4375 1.875H6.875M6.875 12.5H13.125M6.875 15H10M8.75 1.875H4.6875C4.16973 1.875 3.75 2.29473 3.75 2.8125V17.1875C3.75 17.7053 4.16973 18.125 4.6875 18.125H15.3125C15.8303 18.125 16.25 17.7053 16.25 17.1875V9.375C16.25 5.23286 12.8921 1.875 8.75 1.875Z" stroke="#1F2A37" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 595 B |
3
web/app/(commonLayout)/apps/assets/delete.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.6665 7.33333V11.3333M9.33317 7.33333V11.3333M2.6665 4.66667H13.3332M12.6665 4.66667L12.0885 12.7613C12.0646 13.0977 11.914 13.4125 11.6672 13.6424C11.4205 13.8722 11.0957 14 10.7585 14H5.24117C4.90393 14 4.57922 13.8722 4.33243 13.6424C4.08564 13.4125 3.93511 13.0977 3.91117 12.7613L3.33317 4.66667H12.6665ZM9.99984 4.66667V2.66667C9.99984 2.48986 9.9296 2.32029 9.80457 2.19526C9.67955 2.07024 9.50998 2 9.33317 2H6.6665C6.48969 2 6.32012 2.07024 6.1951 2.19526C6.07008 2.32029 5.99984 2.48986 5.99984 2.66667V4.66667H9.99984Z" stroke="#1F2A37" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 711 B |
3
web/app/(commonLayout)/apps/assets/discord.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="26" height="26" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M22.0101 4.50191C20.3529 3.74154 18.5759 3.18133 16.7179 2.86048C16.6841 2.85428 16.6503 2.86976 16.6328 2.90071C16.4043 3.30719 16.1511 3.83748 15.9738 4.25429C13.9754 3.95511 11.9873 3.95511 10.0298 4.25429C9.85253 3.82822 9.59019 3.30719 9.36062 2.90071C9.34319 2.87079 9.30939 2.85532 9.27555 2.86048C7.41857 3.18031 5.64152 3.74051 3.98335 4.50191C3.96899 4.5081 3.95669 4.51843 3.94852 4.53183C0.577841 9.56755 -0.345529 14.4795 0.107445 19.3306C0.109495 19.3543 0.122817 19.377 0.141265 19.3914C2.36514 21.0246 4.51935 22.0161 6.63355 22.6732C6.66739 22.6836 6.70324 22.6712 6.72477 22.6433C7.22489 21.9604 7.6707 21.2402 8.05293 20.4829C8.07549 20.4386 8.05396 20.386 8.00785 20.3684C7.30073 20.1002 6.6274 19.7731 5.97971 19.4017C5.92848 19.3718 5.92437 19.2985 5.9715 19.2635C6.1078 19.1613 6.24414 19.0551 6.37428 18.9478C6.39783 18.9282 6.43064 18.924 6.45833 18.9364C10.7134 20.8791 15.32 20.8791 19.5249 18.9364C19.5525 18.923 19.5854 18.9272 19.6099 18.9467C19.7401 19.054 19.8764 19.1613 20.0137 19.2635C20.0609 19.2985 20.0578 19.3718 20.0066 19.4017C19.3589 19.7804 18.6855 20.1002 17.9774 20.3674C17.9313 20.3849 17.9108 20.4386 17.9333 20.4829C18.3238 21.2392 18.7696 21.9593 19.2605 22.6423C19.281 22.6712 19.3179 22.6836 19.3517 22.6732C21.4761 22.0161 23.6303 21.0246 25.8542 19.3914C25.8737 19.377 25.886 19.3553 25.8881 19.3316C26.4302 13.7232 24.98 8.85156 22.0439 4.53286C22.0367 4.51843 22.0245 4.5081 22.0101 4.50191ZM8.68836 16.3768C7.40729 16.3768 6.35173 15.2007 6.35173 13.7563C6.35173 12.3119 7.38682 11.1358 8.68836 11.1358C10.0001 11.1358 11.0455 12.3222 11.025 13.7563C11.025 15.2007 9.98986 16.3768 8.68836 16.3768ZM17.3276 16.3768C16.0466 16.3768 14.991 15.2007 14.991 13.7563C14.991 12.3119 16.0261 11.1358 17.3276 11.1358C18.6394 11.1358 19.6847 12.3222 19.6643 13.7563C19.6643 15.2007 18.6394 16.3768 17.3276 16.3768Z" fill="#5865F2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
17
web/app/(commonLayout)/apps/assets/github.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_131_1011)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.0003 0.5C9.15149 0.501478 6.39613 1.51046 4.22687 3.34652C2.05761 5.18259 0.615903 7.72601 0.159545 10.522C-0.296814 13.318 0.261927 16.1842 1.73587 18.6082C3.20981 21.0321 5.50284 22.8558 8.20493 23.753C8.80105 23.8636 9.0256 23.4941 9.0256 23.18C9.0256 22.8658 9.01367 21.955 9.0097 20.9592C5.6714 21.6804 4.96599 19.5505 4.96599 19.5505C4.42152 18.1674 3.63464 17.8039 3.63464 17.8039C2.54571 17.065 3.71611 17.0788 3.71611 17.0788C4.92227 17.1637 5.55616 18.3097 5.55616 18.3097C6.62521 20.1333 8.36389 19.6058 9.04745 19.2976C9.15475 18.5251 9.46673 17.9995 9.8105 17.7012C7.14383 17.4008 4.34204 16.3774 4.34204 11.8054C4.32551 10.6197 4.76802 9.47305 5.57801 8.60268C5.45481 8.30236 5.04348 7.08923 5.69524 5.44143C5.69524 5.44143 6.7027 5.12135 8.9958 6.66444C10.9627 6.12962 13.0379 6.12962 15.0047 6.66444C17.2958 5.12135 18.3013 5.44143 18.3013 5.44143C18.9551 7.08528 18.5437 8.29841 18.4205 8.60268C19.2331 9.47319 19.6765 10.6218 19.6585 11.8094C19.6585 16.3912 16.8507 17.4008 14.1801 17.6952C14.6093 18.0667 14.9928 18.7918 14.9928 19.9061C14.9928 21.5026 14.9789 22.7868 14.9789 23.18C14.9789 23.4981 15.1955 23.8695 15.8035 23.753C18.5059 22.8557 20.7992 21.0317 22.2731 18.6073C23.747 16.183 24.3055 13.3163 23.8486 10.5201C23.3917 7.7238 21.9493 5.18035 19.7793 3.34461C17.6093 1.50886 14.8533 0.500541 12.0042 0.5H12.0003Z" fill="#191717"/>
|
||||
<path d="M4.54444 17.6321C4.5186 17.6914 4.42322 17.7092 4.34573 17.6677C4.26823 17.6262 4.21061 17.5491 4.23843 17.4879C4.26625 17.4266 4.35964 17.4108 4.43714 17.4523C4.51463 17.4938 4.57424 17.5729 4.54444 17.6321Z" fill="#191717"/>
|
||||
<path d="M5.03123 18.1714C4.99008 18.192 4.943 18.1978 4.89805 18.1877C4.8531 18.1776 4.81308 18.1523 4.78483 18.1161C4.70734 18.0331 4.69143 17.9185 4.75104 17.8671C4.81066 17.8157 4.91797 17.8395 4.99546 17.9224C5.07296 18.0054 5.09084 18.12 5.03123 18.1714Z" fill="#191717"/>
|
||||
<path d="M5.50425 18.857C5.43072 18.9084 5.30553 18.857 5.23598 18.7543C5.21675 18.7359 5.20146 18.7138 5.19101 18.6893C5.18056 18.6649 5.17517 18.6386 5.17517 18.612C5.17517 18.5855 5.18056 18.5592 5.19101 18.5347C5.20146 18.5103 5.21675 18.4882 5.23598 18.4698C5.3095 18.4204 5.4347 18.4698 5.50425 18.5705C5.57379 18.6713 5.57578 18.8057 5.50425 18.857V18.857Z" fill="#191717"/>
|
||||
<path d="M6.14612 19.5207C6.08054 19.5939 5.94741 19.5741 5.83812 19.4753C5.72883 19.3765 5.70299 19.2422 5.76857 19.171C5.83414 19.0999 5.96727 19.1197 6.08054 19.2165C6.1938 19.3133 6.21566 19.4496 6.14612 19.5207V19.5207Z" fill="#191717"/>
|
||||
<path d="M7.04617 19.9081C7.01637 20.001 6.88124 20.0425 6.74612 20.003C6.611 19.9635 6.52158 19.8528 6.54741 19.758C6.57325 19.6631 6.71036 19.6197 6.84747 19.6631C6.98457 19.7066 7.07201 19.8113 7.04617 19.9081Z" fill="#191717"/>
|
||||
<path d="M8.02783 19.9752C8.02783 20.072 7.91656 20.155 7.77349 20.1569C7.63042 20.1589 7.51318 20.0799 7.51318 19.9831C7.51318 19.8863 7.62445 19.8033 7.76752 19.8013C7.91059 19.7993 8.02783 19.8764 8.02783 19.9752Z" fill="#191717"/>
|
||||
<path d="M8.9419 19.8232C8.95978 19.92 8.86042 20.0207 8.71735 20.0445C8.57428 20.0682 8.4491 20.0109 8.43121 19.916C8.41333 19.8212 8.51666 19.7185 8.65576 19.6928C8.79485 19.6671 8.92401 19.7264 8.9419 19.8232Z" fill="#191717"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_131_1011">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
3
web/app/(commonLayout)/apps/assets/link-gray.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="13" height="14" viewBox="0 0 13 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.41663 3.75033H3.24996C2.96264 3.75033 2.68709 3.86446 2.48393 4.06763C2.28076 4.27079 2.16663 4.54634 2.16663 4.83366V10.2503C2.16663 10.5376 2.28076 10.8132 2.48393 11.0164C2.68709 11.2195 2.96264 11.3337 3.24996 11.3337H8.66663C8.95394 11.3337 9.22949 11.2195 9.43266 11.0164C9.63582 10.8132 9.74996 10.5376 9.74996 10.2503V8.08366M7.58329 2.66699H10.8333M10.8333 2.66699V5.91699M10.8333 2.66699L5.41663 8.08366" stroke="#9CA3AF" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 596 B |
3
web/app/(commonLayout)/apps/assets/link.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="13" height="14" viewBox="0 0 13 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.41663 3.75008H3.24996C2.96264 3.75008 2.68709 3.86422 2.48393 4.06738C2.28076 4.27055 2.16663 4.5461 2.16663 4.83341V10.2501C2.16663 10.5374 2.28076 10.8129 2.48393 11.0161C2.68709 11.2193 2.96264 11.3334 3.24996 11.3334H8.66663C8.95394 11.3334 9.22949 11.2193 9.43266 11.0161C9.63582 10.8129 9.74996 10.5374 9.74996 10.2501V8.08341M7.58329 2.66675H10.8333M10.8333 2.66675V5.91675M10.8333 2.66675L5.41663 8.08341" stroke="#1C64F2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 595 B |
3
web/app/(commonLayout)/apps/assets/right-arrow.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 2.5L10.5 6M10.5 6L7 9.5M10.5 6H1.5" stroke="#1C64F2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 217 B |
36
web/app/(commonLayout)/apps/page.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import classNames from 'classnames'
|
||||
import style from '../list.module.css'
|
||||
import Apps from './Apps'
|
||||
import { getLocaleOnServer } from '@/i18n/server'
|
||||
import { useTranslation } from '@/i18n/i18next-serverside-config'
|
||||
|
||||
const AppList = async () => {
|
||||
const locale = getLocaleOnServer()
|
||||
const { t } = await useTranslation(locale, 'app')
|
||||
|
||||
return (
|
||||
<div className='flex flex-col overflow-auto bg-gray-100 shrink-0 grow'>
|
||||
<Apps />
|
||||
<footer className='px-12 py-6 grow-0 shrink-0'>
|
||||
<h3 className='text-xl font-semibold leading-tight text-gradient'>{t('join')}</h3>
|
||||
<p className='mt-1 text-sm font-normal leading-tight text-gray-700'>{t('communityIntro')}</p>
|
||||
{/*<p className='mt-3 text-sm'>*/}
|
||||
{/* <a className='inline-flex items-center gap-1 link' target='_blank' href={`https://docs.dify.ai${locale === 'en' ? '' : '/v/zh-hans'}/community/product-roadmap`}>*/}
|
||||
{/* {t('roadmap')}*/}
|
||||
{/* <span className={style.linkIcon} />*/}
|
||||
{/* </a>*/}
|
||||
{/*</p>*/}
|
||||
<div className='flex items-center gap-2 mt-3'>
|
||||
<a className={style.socialMediaLink} target='_blank' href='https://github.com/langgenius'><span className={classNames(style.socialMediaIcon, style.githubIcon)} /></a>
|
||||
<a className={style.socialMediaLink} target='_blank' href='https://discord.gg/AhzKf7dNgk'><span className={classNames(style.socialMediaIcon, style.discordIcon)} /></a>
|
||||
</div>
|
||||
</footer>
|
||||
</div >
|
||||
)
|
||||
}
|
||||
|
||||
export const metadata = {
|
||||
title: 'Apps - Dify',
|
||||
}
|
||||
|
||||
export default AppList
|
||||
@@ -0,0 +1,11 @@
|
||||
import React from 'react'
|
||||
|
||||
type Props = {}
|
||||
|
||||
const page = (props: Props) => {
|
||||
return (
|
||||
<div>dataset detail api</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default page
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from 'react'
|
||||
import MainDetail from '@/app/components/datasets/documents/detail'
|
||||
|
||||
export type IDocumentDetailProps = {
|
||||
params: { datasetId: string; documentId: string }
|
||||
}
|
||||
|
||||
const DocumentDetail = async ({
|
||||
params: { datasetId, documentId },
|
||||
}: IDocumentDetailProps) => {
|
||||
return (
|
||||
<MainDetail datasetId={datasetId} documentId={documentId} />
|
||||
)
|
||||
}
|
||||
|
||||
export default DocumentDetail
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from 'react'
|
||||
import DatasetUpdateForm from '@/app/components/datasets/create'
|
||||
|
||||
export type IProps = {
|
||||
params: { datasetId: string }
|
||||
}
|
||||
|
||||
const Create = async ({
|
||||
params: { datasetId },
|
||||
}: IProps) => {
|
||||
return (
|
||||
<DatasetUpdateForm datasetId={datasetId} />
|
||||
)
|
||||
}
|
||||
|
||||
export default Create
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from 'react'
|
||||
import Main from '@/app/components/datasets/documents'
|
||||
|
||||
export type IProps = {
|
||||
params: { datasetId: string }
|
||||
}
|
||||
|
||||
const Documents = async ({
|
||||
params: { datasetId },
|
||||
}: IProps) => {
|
||||
return (
|
||||
<Main datasetId={datasetId} />
|
||||
)
|
||||
}
|
||||
|
||||
export default Documents
|
||||
@@ -0,0 +1,9 @@
|
||||
.logTable td {
|
||||
padding: 7px 8px;
|
||||
box-sizing: border-box;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.pagination li {
|
||||
list-style: none;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from 'react'
|
||||
import Main from '@/app/components/datasets/hit-testing'
|
||||
|
||||
type Props = {
|
||||
params: { datasetId: string }
|
||||
}
|
||||
|
||||
const HitTesting = ({
|
||||
params: { datasetId },
|
||||
}: Props) => {
|
||||
return (
|
||||
<Main datasetId={datasetId} />
|
||||
)
|
||||
}
|
||||
|
||||
export default HitTesting
|
||||
@@ -0,0 +1,169 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect } from 'react'
|
||||
import { usePathname, useSelectedLayoutSegments } from 'next/navigation'
|
||||
import useSWR from 'swr'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { getLocaleOnClient } from '@/i18n/client'
|
||||
import {
|
||||
Cog8ToothIcon,
|
||||
// CommandLineIcon,
|
||||
Squares2X2Icon,
|
||||
PuzzlePieceIcon,
|
||||
DocumentTextIcon,
|
||||
} from '@heroicons/react/24/outline'
|
||||
import {
|
||||
Cog8ToothIcon as Cog8ToothSolidIcon,
|
||||
// CommandLineIcon as CommandLineSolidIcon,
|
||||
DocumentTextIcon as DocumentTextSolidIcon,
|
||||
} from '@heroicons/react/24/solid'
|
||||
import Link from 'next/link'
|
||||
import { fetchDataDetail, fetchDatasetRelatedApps } from '@/service/datasets'
|
||||
import type { RelatedApp } from '@/models/datasets'
|
||||
import s from './style.module.css'
|
||||
import AppSideBar from '@/app/components/app-sidebar'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import DatasetDetailContext from '@/context/dataset-detail'
|
||||
|
||||
// import { fetchDatasetDetail } from '@/service/datasets'
|
||||
|
||||
export type IAppDetailLayoutProps = {
|
||||
children: React.ReactNode
|
||||
params: { datasetId: string }
|
||||
}
|
||||
|
||||
const LikedItem: FC<{ type?: 'plugin' | 'app'; appStatus?: boolean; detail: RelatedApp }> = ({
|
||||
type = 'app',
|
||||
appStatus = true,
|
||||
detail
|
||||
}) => {
|
||||
return (
|
||||
<Link prefetch className={s.itemWrapper} href={`/app/${detail?.id}/overview`}>
|
||||
<div className={s.iconWrapper}>
|
||||
<AppIcon size='tiny' />
|
||||
{type === 'app' && (
|
||||
<div className={s.statusPoint}>
|
||||
<Indicator color={appStatus ? 'green' : 'gray'} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={s.appInfo}>{detail?.name || '--'}</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const TargetIcon: FC<{ className?: string }> = ({ className }) => {
|
||||
return <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
|
||||
<g clip-path="url(#clip0_4610_6951)">
|
||||
<path d="M10.6666 5.33325V3.33325L12.6666 1.33325L13.3332 2.66659L14.6666 3.33325L12.6666 5.33325H10.6666ZM10.6666 5.33325L7.9999 7.99988M14.6666 7.99992C14.6666 11.6818 11.6818 14.6666 7.99992 14.6666C4.31802 14.6666 1.33325 11.6818 1.33325 7.99992C1.33325 4.31802 4.31802 1.33325 7.99992 1.33325M11.3333 7.99992C11.3333 9.84087 9.84087 11.3333 7.99992 11.3333C6.15897 11.3333 4.66659 9.84087 4.66659 7.99992C4.66659 6.15897 6.15897 4.66659 7.99992 4.66659" stroke="#344054" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_4610_6951">
|
||||
<rect width="16" height="16" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
}
|
||||
|
||||
const TargetSolidIcon: FC<{ className?: string }> = ({ className }) => {
|
||||
return <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.7733 0.67512C12.9848 0.709447 13.1669 0.843364 13.2627 1.03504L13.83 2.16961L14.9646 2.73689C15.1563 2.83273 15.2902 3.01486 15.3245 3.22639C15.3588 3.43792 15.2894 3.65305 15.1379 3.80458L13.1379 5.80458C13.0128 5.92961 12.8433 5.99985 12.6665 5.99985H10.9426L8.47124 8.47124C8.21089 8.73159 7.78878 8.73159 7.52843 8.47124C7.26808 8.21089 7.26808 7.78878 7.52843 7.52843L9.9998 5.05707V3.33318C9.9998 3.15637 10.07 2.9868 10.1951 2.86177L12.1951 0.861774C12.3466 0.710244 12.5617 0.640794 12.7733 0.67512Z" fill="#155EEF" />
|
||||
<path d="M1.99984 7.99984C1.99984 4.68613 4.68613 1.99984 7.99984 1.99984C8.36803 1.99984 8.6665 1.70136 8.6665 1.33317C8.6665 0.964981 8.36803 0.666504 7.99984 0.666504C3.94975 0.666504 0.666504 3.94975 0.666504 7.99984C0.666504 12.0499 3.94975 15.3332 7.99984 15.3332C12.0499 15.3332 15.3332 12.0499 15.3332 7.99984C15.3332 7.63165 15.0347 7.33317 14.6665 7.33317C14.2983 7.33317 13.9998 7.63165 13.9998 7.99984C13.9998 11.3135 11.3135 13.9998 7.99984 13.9998C4.68613 13.9998 1.99984 11.3135 1.99984 7.99984Z" fill="#155EEF" />
|
||||
<path d="M5.33317 7.99984C5.33317 6.52708 6.52708 5.33317 7.99984 5.33317C8.36803 5.33317 8.6665 5.03469 8.6665 4.6665C8.6665 4.29831 8.36803 3.99984 7.99984 3.99984C5.7907 3.99984 3.99984 5.7907 3.99984 7.99984C3.99984 10.209 5.7907 11.9998 7.99984 11.9998C10.209 11.9998 11.9998 10.209 11.9998 7.99984C11.9998 7.63165 11.7014 7.33317 11.3332 7.33317C10.965 7.33317 10.6665 7.63165 10.6665 7.99984C10.6665 9.4726 9.4726 10.6665 7.99984 10.6665C6.52708 10.6665 5.33317 9.4726 5.33317 7.99984Z" fill="#155EEF" />
|
||||
</svg>
|
||||
}
|
||||
|
||||
const BookOpenIcon: FC<{ className?: string }> = ({ className }) => {
|
||||
return <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
|
||||
<path opacity="0.12" d="M1 3.1C1 2.53995 1 2.25992 1.10899 2.04601C1.20487 1.85785 1.35785 1.70487 1.54601 1.60899C1.75992 1.5 2.03995 1.5 2.6 1.5H2.8C3.9201 1.5 4.48016 1.5 4.90798 1.71799C5.28431 1.90973 5.59027 2.21569 5.78201 2.59202C6 3.01984 6 3.5799 6 4.7V10.5L5.94997 10.425C5.60265 9.90398 5.42899 9.64349 5.19955 9.45491C4.99643 9.28796 4.76238 9.1627 4.5108 9.0863C4.22663 9 3.91355 9 3.28741 9H2.6C2.03995 9 1.75992 9 1.54601 8.89101C1.35785 8.79513 1.20487 8.64215 1.10899 8.45399C1 8.24008 1 7.96005 1 7.4V3.1Z" fill="#155EEF" />
|
||||
<path d="M6 10.5L5.94997 10.425C5.60265 9.90398 5.42899 9.64349 5.19955 9.45491C4.99643 9.28796 4.76238 9.1627 4.5108 9.0863C4.22663 9 3.91355 9 3.28741 9H2.6C2.03995 9 1.75992 9 1.54601 8.89101C1.35785 8.79513 1.20487 8.64215 1.10899 8.45399C1 8.24008 1 7.96005 1 7.4V3.1C1 2.53995 1 2.25992 1.10899 2.04601C1.20487 1.85785 1.35785 1.70487 1.54601 1.60899C1.75992 1.5 2.03995 1.5 2.6 1.5H2.8C3.9201 1.5 4.48016 1.5 4.90798 1.71799C5.28431 1.90973 5.59027 2.21569 5.78201 2.59202C6 3.01984 6 3.5799 6 4.7M6 10.5V4.7M6 10.5L6.05003 10.425C6.39735 9.90398 6.57101 9.64349 6.80045 9.45491C7.00357 9.28796 7.23762 9.1627 7.4892 9.0863C7.77337 9 8.08645 9 8.71259 9H9.4C9.96005 9 10.2401 9 10.454 8.89101C10.6422 8.79513 10.7951 8.64215 10.891 8.45399C11 8.24008 11 7.96005 11 7.4V3.1C11 2.53995 11 2.25992 10.891 2.04601C10.7951 1.85785 10.6422 1.70487 10.454 1.60899C10.2401 1.5 9.96005 1.5 9.4 1.5H9.2C8.07989 1.5 7.51984 1.5 7.09202 1.71799C6.71569 1.90973 6.40973 2.21569 6.21799 2.59202C6 3.01984 6 3.5799 6 4.7" stroke="#155EEF" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
}
|
||||
|
||||
const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
||||
const {
|
||||
children,
|
||||
params: { datasetId },
|
||||
} = props
|
||||
const pathname = usePathname()
|
||||
const hideSideBar = /documents\/create$/.test(pathname)
|
||||
const { t } = useTranslation()
|
||||
const { data: datasetRes, error } = useSWR({
|
||||
action: 'fetchDataDetail',
|
||||
datasetId,
|
||||
}, apiParams => fetchDataDetail(apiParams.datasetId))
|
||||
|
||||
const { data: relatedApps } = useSWR({
|
||||
action: 'fetchDatasetRelatedApps',
|
||||
datasetId,
|
||||
}, apiParams => fetchDatasetRelatedApps(apiParams.datasetId))
|
||||
|
||||
const navigation = [
|
||||
{ name: t('common.datasetMenus.documents'), href: `/datasets/${datasetId}/documents`, icon: DocumentTextIcon, selectedIcon: DocumentTextSolidIcon },
|
||||
{ name: t('common.datasetMenus.hitTesting'), href: `/datasets/${datasetId}/hitTesting`, icon: TargetIcon, selectedIcon: TargetSolidIcon },
|
||||
// { name: 'api & webhook', href: `/datasets/${datasetId}/api`, icon: CommandLineIcon, selectedIcon: CommandLineSolidIcon },
|
||||
{ name: t('common.datasetMenus.settings'), href: `/datasets/${datasetId}/settings`, icon: Cog8ToothIcon, selectedIcon: Cog8ToothSolidIcon },
|
||||
]
|
||||
|
||||
useEffect(() => {
|
||||
if (datasetRes) {
|
||||
document.title = `${datasetRes.name || 'Dataset'} - Dify`
|
||||
}
|
||||
}, [datasetRes])
|
||||
|
||||
const ExtraInfo: FC = () => {
|
||||
const locale = getLocaleOnClient()
|
||||
|
||||
return <div className='w-full'>
|
||||
<Divider className='mt-5' />
|
||||
{relatedApps?.data?.length ? (
|
||||
<>
|
||||
<div className={s.subTitle}>{relatedApps?.total || '--'} {t('common.datasetMenus.relatedApp')}</div>
|
||||
{relatedApps?.data?.map((item) => (<LikedItem detail={item} />))}
|
||||
</>
|
||||
) : (
|
||||
<div className='mt-5 p-3'>
|
||||
<div className='flex items-center justify-start gap-2'>
|
||||
<div className={s.emptyIconDiv}>
|
||||
<Squares2X2Icon className='w-3 h-3 text-gray-500' />
|
||||
</div>
|
||||
<div className={s.emptyIconDiv}>
|
||||
<PuzzlePieceIcon className='w-3 h-3 text-gray-500' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='text-xs text-gray-500 mt-2'>{t('common.datasetMenus.emptyTip')}</div>
|
||||
<a
|
||||
className='inline-flex items-center text-xs text-primary-600 mt-2 cursor-pointer'
|
||||
href={`https://docs.dify.ai/${locale === 'en' ? '' : 'v/zh-hans'}/application/prompt-engineering`}
|
||||
target='_blank'
|
||||
>
|
||||
<BookOpenIcon className='mr-1' />
|
||||
{t('common.datasetMenus.viewDoc')}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
|
||||
if (!datasetRes && !error)
|
||||
return <Loading />
|
||||
|
||||
return (
|
||||
<div className='flex' style={{ height: 'calc(100vh - 56px)' }}>
|
||||
{!hideSideBar && <AppSideBar
|
||||
title={datasetRes?.name || '--'}
|
||||
desc={datasetRes?.description || '--'}
|
||||
navigation={navigation}
|
||||
extraInfo={<ExtraInfo />}
|
||||
iconType='dataset'
|
||||
/>}
|
||||
<DatasetDetailContext.Provider value={{ indexingTechnique: datasetRes?.indexing_technique }}>
|
||||
<div className="bg-white grow">{children}</div>
|
||||
</DatasetDetailContext.Provider>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(DatasetDetailLayout)
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react'
|
||||
import { getLocaleOnServer } from '@/i18n/server'
|
||||
import { useTranslation } from '@/i18n/i18next-serverside-config'
|
||||
import Form from '@/app/components/datasets/settings/form'
|
||||
|
||||
const Settings = async () => {
|
||||
const locale = getLocaleOnServer()
|
||||
const { t } = await useTranslation(locale, 'dataset-settings')
|
||||
|
||||
return (
|
||||
<div className='bg-white h-full'>
|
||||
<div className='px-6 py-3'>
|
||||
<div className='mb-1 text-lg font-semibold text-gray-900'>{t('title')}</div>
|
||||
<div className='text-sm text-gray-500'>{t('desc')}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Form />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Settings
|
||||
@@ -0,0 +1,18 @@
|
||||
.itemWrapper {
|
||||
@apply flex items-center w-full h-10 px-3 rounded-lg hover:bg-gray-50 cursor-pointer;
|
||||
}
|
||||
.appInfo {
|
||||
@apply truncate text-gray-700 text-sm font-normal;
|
||||
}
|
||||
.iconWrapper {
|
||||
@apply relative w-6 h-6 mr-2 bg-[#D5F5F6] rounded-md;
|
||||
}
|
||||
.statusPoint {
|
||||
@apply flex justify-center items-center absolute -right-0.5 -bottom-0.5 w-2.5 h-2.5 bg-white rounded;
|
||||
}
|
||||
.subTitle {
|
||||
@apply uppercase text-xs text-gray-500 font-medium px-3 pb-2 pt-4;
|
||||
}
|
||||
.emptyIconDiv {
|
||||
@apply h-7 w-7 bg-gray-50 border border-[#EAECF5] inline-flex justify-center items-center rounded-lg;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
|
||||
export type IDatasetDetail = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const AppDetail: FC<IDatasetDetail> = ({ children }) => {
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(AppDetail)
|
||||
89
web/app/(commonLayout)/datasets/DatasetCard.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
'use client'
|
||||
|
||||
import { useContext, useContextSelector } from 'use-context-selector'
|
||||
import Link from 'next/link'
|
||||
import useSWR from 'swr'
|
||||
import type { MouseEventHandler } from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import style from '../list.module.css'
|
||||
import type { App } from '@/types/app'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { deleteDataset, fetchDatasets } from '@/service/datasets'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import AppsContext from '@/context/app-context'
|
||||
import { DataSet } from '@/models/datasets'
|
||||
import classNames from 'classnames'
|
||||
|
||||
export type DatasetCardProps = {
|
||||
dataset: DataSet
|
||||
}
|
||||
|
||||
const DatasetCard = ({
|
||||
dataset,
|
||||
}: DatasetCardProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
|
||||
const { mutate: mutateDatasets } = useSWR({ url: '/datasets', params: { page: 1 } }, fetchDatasets)
|
||||
|
||||
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
|
||||
const onDeleteClick: MouseEventHandler = useCallback((e) => {
|
||||
e.preventDefault()
|
||||
setShowConfirmDelete(true)
|
||||
}, [])
|
||||
const onConfirmDelete = useCallback(async () => {
|
||||
try {
|
||||
await deleteDataset(dataset.id)
|
||||
notify({ type: 'success', message: t('dataset.datasetDeleted') })
|
||||
mutateDatasets()
|
||||
}
|
||||
catch (e: any) {
|
||||
notify({ type: 'error', message: `${t('dataset.datasetDeleteFailed')}${'message' in e ? `: ${e.message}` : ''}` })
|
||||
}
|
||||
setShowConfirmDelete(false)
|
||||
}, [dataset.id])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Link href={`/datasets/${dataset.id}/documents`} className={style.listItem}>
|
||||
<div className={style.listItemTitle}>
|
||||
<AppIcon size='small' />
|
||||
<div className={style.listItemHeading}>
|
||||
<div className={style.listItemHeadingContent}>{dataset.name}</div>
|
||||
</div>
|
||||
<span className={style.deleteAppIcon} onClick={onDeleteClick} />
|
||||
</div>
|
||||
<div className={style.listItemDescription}>{dataset.description}</div>
|
||||
<div className={classNames(style.listItemFooter, style.datasetCardFooter)}>
|
||||
<span className={style.listItemStats}>
|
||||
<span className={classNames(style.listItemFooterIcon, style.docIcon)} />
|
||||
{dataset.document_count}{t('dataset.documentCount')}
|
||||
</span>
|
||||
<span className={style.listItemStats}>
|
||||
<span className={classNames(style.listItemFooterIcon, style.textIcon)} />
|
||||
{Math.round(dataset.word_count / 1000)}{t('dataset.wordCount')}
|
||||
</span>
|
||||
<span className={style.listItemStats}>
|
||||
<span className={classNames(style.listItemFooterIcon, style.applicationIcon)} />
|
||||
{dataset.app_count}{t('dataset.appCount')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{showConfirmDelete && (
|
||||
<Confirm
|
||||
title={t('dataset.deleteDatasetConfirmTitle')}
|
||||
content={t('dataset.deleteDatasetConfirmContent')}
|
||||
isShow={showConfirmDelete}
|
||||
onClose={() => setShowConfirmDelete(false)}
|
||||
onConfirm={onConfirmDelete}
|
||||
onCancel={() => setShowConfirmDelete(false)}
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default DatasetCard
|
||||
19
web/app/(commonLayout)/datasets/DatasetFooter.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
'use client'
|
||||
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
const DatasetFooter = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<footer className='px-12 py-6 grow-0 shrink-0'>
|
||||
<h3 className='text-xl font-semibold leading-tight text-gradient'>{t('dataset.didYouKnow')}</h3>
|
||||
<p className='mt-1 text-sm font-normal leading-tight text-gray-700'>
|
||||
{t('dataset.intro1')}<a className='inline-flex items-center gap-1 link' target='_blank' href='/'>{t('dataset.intro2')}</a>{t('dataset.intro3')}<br />
|
||||
{t('dataset.intro4')}<a className='inline-flex items-center gap-1 link' target='_blank' href='/'>{t('dataset.intro5')}</a>{t('dataset.intro6')}
|
||||
</p>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
|
||||
export default DatasetFooter
|
||||
27
web/app/(commonLayout)/datasets/Datasets.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import useSWR from 'swr'
|
||||
import { DataSet } from '@/models/datasets';
|
||||
import NewDatasetCard from './NewDatasetCard'
|
||||
import DatasetCard from './DatasetCard';
|
||||
import { fetchDatasets } from '@/service/datasets';
|
||||
|
||||
const Datasets = () => {
|
||||
// const { datasets, mutateDatasets } = useAppContext()
|
||||
const { data: datasetList, mutate: mutateDatasets } = useSWR({ url: '/datasets', params: { page: 1 } }, fetchDatasets)
|
||||
|
||||
useEffect(() => {
|
||||
mutateDatasets()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<nav className='grid content-start grid-cols-1 gap-4 px-12 pt-8 sm:grid-cols-2 lg:grid-cols-4 grow shrink-0'>
|
||||
{datasetList?.data.map(dataset => (<DatasetCard key={dataset.id} dataset={dataset} />))}
|
||||
<NewDatasetCard />
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
export default Datasets
|
||||
|
||||
28
web/app/(commonLayout)/datasets/NewDatasetCard.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import classNames from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import style from '../list.module.css'
|
||||
|
||||
const CreateAppCard = () => {
|
||||
const { t } = useTranslation()
|
||||
const [showNewAppDialog, setShowNewAppDialog] = useState(false)
|
||||
|
||||
return (
|
||||
<a className={classNames(style.listItem, style.newItemCard)} href='/datasets/create'>
|
||||
<div className={style.listItemTitle}>
|
||||
<span className={style.newItemIcon}>
|
||||
<span className={classNames(style.newItemIconImage, style.newItemIconAdd)} />
|
||||
</span>
|
||||
<div className={classNames(style.listItemHeading, style.newItemCardHeading)}>
|
||||
{t('dataset.createDataset')}
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.listItemDescription}>{t('dataset.createDatasetIntro')}</div>
|
||||
{/* <div className='text-xs text-gray-500'>{t('app.createFromConfigFile')}</div> */}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreateAppCard
|
||||
6
web/app/(commonLayout)/datasets/assets/application.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.2 1.5H2.3C2.01997 1.5 1.87996 1.5 1.773 1.5545C1.67892 1.60243 1.60243 1.67892 1.5545 1.773C1.5 1.87996 1.5 2.01997 1.5 2.3V4.2C1.5 4.48003 1.5 4.62004 1.5545 4.727C1.60243 4.82108 1.67892 4.89757 1.773 4.9455C1.87996 5 2.01997 5 2.3 5H4.2C4.48003 5 4.62004 5 4.727 4.9455C4.82108 4.89757 4.89757 4.82108 4.9455 4.727C5 4.62004 5 4.48003 5 4.2V2.3C5 2.01997 5 1.87996 4.9455 1.773C4.89757 1.67892 4.82108 1.60243 4.727 1.5545C4.62004 1.5 4.48003 1.5 4.2 1.5Z" stroke="#98A2B3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.7 1.5H7.8C7.51997 1.5 7.37996 1.5 7.273 1.5545C7.17892 1.60243 7.10243 1.67892 7.0545 1.773C7 1.87996 7 2.01997 7 2.3V4.2C7 4.48003 7 4.62004 7.0545 4.727C7.10243 4.82108 7.17892 4.89757 7.273 4.9455C7.37996 5 7.51997 5 7.8 5H9.7C9.98003 5 10.12 5 10.227 4.9455C10.3211 4.89757 10.3976 4.82108 10.4455 4.727C10.5 4.62004 10.5 4.48003 10.5 4.2V2.3C10.5 2.01997 10.5 1.87996 10.4455 1.773C10.3976 1.67892 10.3211 1.60243 10.227 1.5545C10.12 1.5 9.98003 1.5 9.7 1.5Z" stroke="#98A2B3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.7 7H7.8C7.51997 7 7.37996 7 7.273 7.0545C7.17892 7.10243 7.10243 7.17892 7.0545 7.273C7 7.37996 7 7.51997 7 7.8V9.7C7 9.98003 7 10.12 7.0545 10.227C7.10243 10.3211 7.17892 10.3976 7.273 10.4455C7.37996 10.5 7.51997 10.5 7.8 10.5H9.7C9.98003 10.5 10.12 10.5 10.227 10.4455C10.3211 10.3976 10.3976 10.3211 10.4455 10.227C10.5 10.12 10.5 9.98003 10.5 9.7V7.8C10.5 7.51997 10.5 7.37996 10.4455 7.273C10.3976 7.17892 10.3211 7.10243 10.227 7.0545C10.12 7 9.98003 7 9.7 7Z" stroke="#98A2B3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4.2 7H2.3C2.01997 7 1.87996 7 1.773 7.0545C1.67892 7.10243 1.60243 7.17892 1.5545 7.273C1.5 7.37996 1.5 7.51997 1.5 7.8V9.7C1.5 9.98003 1.5 10.12 1.5545 10.227C1.60243 10.3211 1.67892 10.3976 1.773 10.4455C1.87996 10.5 2.01997 10.5 2.3 10.5H4.2C4.48003 10.5 4.62004 10.5 4.727 10.4455C4.82108 10.3976 4.89757 10.3211 4.9455 10.227C5 10.12 5 9.98003 5 9.7V7.8C5 7.51997 5 7.37996 4.9455 7.273C4.89757 7.17892 4.82108 7.10243 4.727 7.0545C4.62004 7 4.48003 7 4.2 7Z" stroke="#98A2B3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
3
web/app/(commonLayout)/datasets/assets/doc.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 1H3C2.73478 1 2.48043 1.10536 2.29289 1.29289C2.10536 1.48043 2 1.73478 2 2V10C2 10.2652 2.10536 10.5196 2.29289 10.7071C2.48043 10.8946 2.73478 11 3 11H9C9.26522 11 9.51957 10.8946 9.70711 10.7071C9.89464 10.5196 10 10.2652 10 10V4M7 1L10 4M7 1V4H10M8 6.5H4M8 8.5H4M5 4.5H4" stroke="#98A2B3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 457 B |
3
web/app/(commonLayout)/datasets/assets/text.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 1H3C2.73478 1 2.48043 1.10536 2.29289 1.29289C2.10536 1.48043 2 1.73478 2 2V10C2 10.2652 2.10536 10.5196 2.29289 10.7071C2.48043 10.8946 2.73478 11 3 11H9C9.26522 11 9.51957 10.8946 9.70711 10.7071C9.89464 10.5196 10 10.2652 10 10V4M7 1L10 4M7 1V4H10M8 6.5H4M8 8.5H4M5 4.5H4" stroke="#98A2B3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 457 B |
12
web/app/(commonLayout)/datasets/create/page.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import React from 'react'
|
||||
import DatasetUpdateForm from '@/app/components/datasets/create'
|
||||
|
||||
type Props = {}
|
||||
|
||||
const DatasetCreation = async (props: Props) => {
|
||||
return (
|
||||
<DatasetUpdateForm />
|
||||
)
|
||||
}
|
||||
|
||||
export default DatasetCreation
|
||||
23
web/app/(commonLayout)/datasets/page.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import classNames from 'classnames'
|
||||
import { getLocaleOnServer } from '@/i18n/server'
|
||||
import { useTranslation } from '@/i18n/i18next-serverside-config'
|
||||
import Datasets from './Datasets'
|
||||
import DatasetFooter from './DatasetFooter'
|
||||
|
||||
const AppList = async () => {
|
||||
const locale = getLocaleOnServer()
|
||||
const { t } = await useTranslation(locale, 'dataset')
|
||||
|
||||
return (
|
||||
<div className='flex flex-col overflow-auto bg-gray-100 shrink-0 grow'>
|
||||
<Datasets />
|
||||
<DatasetFooter />
|
||||
</div >
|
||||
)
|
||||
}
|
||||
|
||||
export const metadata = {
|
||||
title: 'Datasets - Dify',
|
||||
}
|
||||
|
||||
export default AppList
|
||||
19
web/app/(commonLayout)/layout.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
import type { FC } from 'react'
|
||||
import LayoutClient, { ICommonLayoutProps } from "./_layout-client";
|
||||
import GA, { GaType } from '@/app/components/base/ga'
|
||||
|
||||
const Layout: FC<ICommonLayoutProps> = ({ children }) => {
|
||||
return (
|
||||
<>
|
||||
<GA gaType={GaType.admin} />
|
||||
<LayoutClient children={children}></LayoutClient>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const metadata = {
|
||||
title: 'Dify',
|
||||
}
|
||||
|
||||
export default Layout
|
||||
183
web/app/(commonLayout)/list.module.css
Normal file
@@ -0,0 +1,183 @@
|
||||
.listItem {
|
||||
@apply col-span-1 bg-white border-2 border-solid border-transparent rounded-lg shadow-sm min-h-[160px] flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg;
|
||||
}
|
||||
|
||||
.listItem.newItemCard {
|
||||
@apply outline outline-1 outline-gray-200 -outline-offset-1 hover:shadow-sm hover:bg-white;
|
||||
background-color: rgba(229, 231, 235, 0.5);
|
||||
}
|
||||
|
||||
.listItem.selectable {
|
||||
@apply relative bg-gray-50 outline outline-1 outline-gray-200 -outline-offset-1 shadow-none hover:bg-none hover:shadow-none hover:outline-primary-200 transition-colors;
|
||||
}
|
||||
.listItem.selectable * {
|
||||
@apply relative;
|
||||
}
|
||||
.listItem.selectable::before {
|
||||
content: '';
|
||||
@apply absolute top-0 left-0 block w-full h-full rounded-lg pointer-events-none opacity-0 transition-opacity duration-200 ease-in-out hover:opacity-100;
|
||||
background: linear-gradient(0deg, rgba(235, 245, 255, 0.5), rgba(235, 245, 255, 0.5)), #FFFFFF;
|
||||
}
|
||||
.listItem.selectable:hover::before {
|
||||
@apply opacity-100;
|
||||
}
|
||||
|
||||
.listItem.selected {
|
||||
@apply border-primary-600 hover:border-primary-600 border-2;
|
||||
}
|
||||
.listItem.selected::before {
|
||||
@apply opacity-100;
|
||||
}
|
||||
|
||||
.appIcon {
|
||||
@apply flex items-center justify-center w-8 h-8 bg-pink-100 rounded-lg grow-0 shrink-0;
|
||||
}
|
||||
.appIcon.medium {
|
||||
@apply w-9 h-9;
|
||||
}
|
||||
.appIcon.large {
|
||||
@apply w-10 h-10;
|
||||
}
|
||||
|
||||
.newItemIcon {
|
||||
@apply flex items-center justify-center w-8 h-8 transition-colors duration-200 ease-in-out border border-gray-200 rounded-lg hover:bg-white grow-0 shrink-0;
|
||||
}
|
||||
.listItem:hover .newItemIcon {
|
||||
@apply bg-gray-50 border-primary-100;
|
||||
}
|
||||
.newItemCard .newItemIcon {
|
||||
@apply bg-gray-100;
|
||||
}
|
||||
.newItemCard:hover .newItemIcon {
|
||||
@apply bg-white;
|
||||
}
|
||||
.selectable .newItemIcon {
|
||||
@apply bg-gray-50;
|
||||
}
|
||||
.selectable:hover .newItemIcon {
|
||||
@apply bg-primary-50;
|
||||
}
|
||||
.newItemIconImage {
|
||||
@apply grow-0 shrink-0 block w-4 h-4 bg-center bg-contain transition-colors duration-200 ease-in-out;
|
||||
color: #1f2a37;
|
||||
}
|
||||
.listItem:hover .newIconImage {
|
||||
@apply text-primary-600;
|
||||
}
|
||||
.newItemIconAdd {
|
||||
background-image: url('./apps/assets/add.svg');
|
||||
}
|
||||
.newItemIconChat {
|
||||
background-image: url('./apps/assets/chat.svg');
|
||||
}
|
||||
.newItemIconComplete {
|
||||
background-image: url('./apps/assets/completion.svg');
|
||||
}
|
||||
|
||||
.listItemTitle {
|
||||
@apply flex pt-[14px] px-[14px] pb-3 h-[66px] items-center gap-3 grow-0 shrink-0;
|
||||
}
|
||||
|
||||
.listItemHeading {
|
||||
@apply relative h-8 text-sm font-medium leading-8 grow;
|
||||
}
|
||||
|
||||
.listItemHeadingContent {
|
||||
@apply absolute top-0 left-0 w-full h-full overflow-hidden text-ellipsis whitespace-nowrap;
|
||||
}
|
||||
|
||||
.deleteAppIcon {
|
||||
@apply hidden grow-0 shrink-0 basis-8 w-8 h-8 rounded-lg transition-colors duration-200 ease-in-out bg-white border border-gray-200 hover:bg-gray-100 bg-center bg-no-repeat;
|
||||
background-size: 16px;
|
||||
background-image: url('./apps/assets/delete.svg');
|
||||
}
|
||||
.listItem:hover .deleteAppIcon {
|
||||
@apply block;
|
||||
}
|
||||
|
||||
.listItemDescription {
|
||||
@apply mb-3 px-[14px] h-9 text-xs leading-normal text-gray-500 line-clamp-2;
|
||||
}
|
||||
|
||||
.listItemFooter {
|
||||
@apply flex items-center flex-wrap min-h-[42px] px-[14px] pt-2 pb-[10px];
|
||||
}
|
||||
.listItemFooter.datasetCardFooter {
|
||||
@apply flex items-center gap-4 text-xs text-gray-500;
|
||||
}
|
||||
|
||||
.listItemStats {
|
||||
@apply flex items-center gap-1;
|
||||
}
|
||||
|
||||
.listItemFooterIcon {
|
||||
@apply block w-3 h-3 bg-center bg-contain;
|
||||
}
|
||||
.solidChatIcon {
|
||||
background-image: url('./apps/assets/chat-solid.svg');
|
||||
}
|
||||
.solidCompletionIcon {
|
||||
background-image: url('./apps/assets/completion-solid.svg');
|
||||
}
|
||||
.docIcon {
|
||||
background-image: url('./datasets/assets/doc.svg');
|
||||
}
|
||||
.textIcon {
|
||||
background-image: url('./datasets/assets/text.svg');
|
||||
}
|
||||
.applicationIcon {
|
||||
background-image: url('./datasets/assets/application.svg');
|
||||
}
|
||||
|
||||
.newItemCardHeading {
|
||||
@apply transition-colors duration-200 ease-in-out;
|
||||
}
|
||||
.listItem:hover .newItemCardHeading {
|
||||
@apply text-primary-600;
|
||||
}
|
||||
|
||||
.listItemLink {
|
||||
@apply inline-flex items-center gap-1 text-xs text-gray-400 transition-colors duration-200 ease-in-out;
|
||||
}
|
||||
.listItem:hover .listItemLink {
|
||||
@apply text-primary-600
|
||||
}
|
||||
|
||||
.linkIcon {
|
||||
@apply block w-[13px] h-[13px] bg-center bg-contain;
|
||||
background-image: url('./apps/assets/link.svg');
|
||||
}
|
||||
|
||||
.linkIcon.grayLinkIcon {
|
||||
background-image: url('./apps/assets/link-gray.svg');
|
||||
}
|
||||
.listItem:hover .grayLinkIcon {
|
||||
background-image: url('./apps/assets/link.svg');
|
||||
}
|
||||
|
||||
.rightIcon {
|
||||
@apply block w-[13px] h-[13px] bg-center bg-contain;
|
||||
background-image: url('./apps/assets/right-arrow.svg');
|
||||
}
|
||||
|
||||
.socialMediaLink {
|
||||
@apply flex items-center justify-center w-8 h-8 cursor-pointer hover:opacity-80 transition-opacity duration-200 ease-in-out;
|
||||
}
|
||||
|
||||
.socialMediaIcon {
|
||||
@apply block w-6 h-6 bg-center bg-contain;
|
||||
}
|
||||
|
||||
.githubIcon {
|
||||
background-image: url('./apps/assets/github.svg');
|
||||
}
|
||||
|
||||
.discordIcon {
|
||||
background-image: url('./apps/assets/discord.svg');
|
||||
}
|
||||
|
||||
/* #region new app dialog */
|
||||
.newItemCaption {
|
||||
@apply inline-flex items-center mb-2 text-sm font-medium;
|
||||
}
|
||||
/* #endregion new app dialog */
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
BIN
web/app/(commonLayout)/plugins-coming-soon/assets/plugins-bg.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
32
web/app/(commonLayout)/plugins-coming-soon/page.module.css
Normal file
@@ -0,0 +1,32 @@
|
||||
.bg {
|
||||
position: relative;
|
||||
width: 750px;
|
||||
height: 450px;
|
||||
background: #fff url(./assets/plugins-bg.png) center center no-repeat;
|
||||
background-size: contain;
|
||||
box-shadow: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.text {
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
left: 48px;
|
||||
width: 526px;
|
||||
background: linear-gradient(91.92deg, #104AE1 -1.74%, #0098EE 75.74%);
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
position: absolute;
|
||||
width: 116.74px;
|
||||
height: 69.3px;
|
||||
left: -18.37px;
|
||||
top: -11.48px;
|
||||
background: url(./assets/coming-soon.png) center center no-repeat;
|
||||
background-size: contain;
|
||||
}
|
||||
19
web/app/(commonLayout)/plugins-coming-soon/page.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import s from './page.module.css'
|
||||
import { getLocaleOnServer } from '@/i18n/server'
|
||||
import { useTranslation } from '@/i18n/i18next-serverside-config'
|
||||
|
||||
const PluginsComingSoon = async () => {
|
||||
const locale = getLocaleOnServer()
|
||||
const { t } = await useTranslation(locale, 'common')
|
||||
|
||||
return (
|
||||
<div className='flex justify-center items-center w-full h-full bg-gray-100'>
|
||||
<div className={s.bg}>
|
||||
<div className={s.tag} />
|
||||
<div className={s.text}>{t('menus.pluginsTips')}</div>
|
||||
</div>
|
||||
</div >
|
||||
)
|
||||
}
|
||||
|
||||
export default PluginsComingSoon
|
||||