Commit a4b13382 authored by StyleZhang's avatar StyleZhang

Merge branch 'feat/refact-common-layout' into deploy/dev

parents 73820c43 c70168bb
import math import math
import re
from typing import Mapping, List, Dict, Any, Optional from typing import Mapping, List, Dict, Any, Optional
from langchain import PromptTemplate from langchain import PromptTemplate
...@@ -178,13 +179,20 @@ class MultiDatasetRouterChain(Chain): ...@@ -178,13 +179,20 @@ class MultiDatasetRouterChain(Chain):
route = self.router_chain.route(inputs) route = self.router_chain.route(inputs)
if not route.destination: destination = ''
if route.destination:
pattern = r'\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b'
match = re.search(pattern, route.destination, re.IGNORECASE)
if match:
destination = match.group()
if not destination:
return {"text": ''} return {"text": ''}
elif route.destination in self.dataset_tools: elif destination in self.dataset_tools:
return {"text": self.dataset_tools[route.destination].run( return {"text": self.dataset_tools[destination].run(
route.next_inputs['input'] route.next_inputs['input']
)} )}
else: else:
raise ValueError( raise ValueError(
f"Received invalid destination chain name '{route.destination}'" f"Received invalid destination chain name '{destination}'"
) )
'use client'
import type { FC } from 'react'
import React, { useEffect, useRef, useState } from 'react'
import { usePathname, useRouter, useSelectedLayoutSegments } from 'next/navigation'
import useSWR, { SWRConfig } from 'swr'
import * as Sentry from '@sentry/react'
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 { AppContextProvider } from '@/context/app-context'
import DatasetsContext from '@/context/datasets-context'
import type { LangGeniusVersionResponse, UserProfileResponse } from '@/models/common'
const isDevelopment = process.env.NODE_ENV === 'development'
export type ICommonLayoutProps = {
children: React.ReactNode
}
const CommonLayout: FC<ICommonLayoutProps> = ({ children }) => {
useEffect(() => {
const SENTRY_DSN = document?.body?.getAttribute('data-public-sentry-dsn')
if (!isDevelopment && SENTRY_DSN) {
Sentry.init({
dsn: SENTRY_DSN,
integrations: [
new Sentry.BrowserTracing({
}),
new Sentry.Replay(),
],
tracesSampleRate: 0.1,
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
})
}
}, [])
const router = useRouter()
const pathname = usePathname()
const segments = useSelectedLayoutSegments()
const pattern = pathname.replace(/.*\/app\//, '')
const [idOrMethod] = pattern.split('/')
const isNotDetailPage = idOrMethod === 'list'
const pageContainerRef = useRef<HTMLDivElement>(null)
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 curAppId = segments[0] === 'app' && segments[2]
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,
}}>
<AppContextProvider value={{ apps: appList.data, mutateApps, userProfile, mutateUserProfile, pageContainerRef }}>
<DatasetsContext.Provider value={{ datasets: datasetList?.data || [], mutateDatasets, currentDataset }}>
<div ref={pageContainerRef} className='relative flex flex-col h-full overflow-auto bg-gray-100'>
<Header
isBordered={['/apps', '/datasets'].includes(pathname)}
curAppId={curAppId || ''}
userProfile={userProfile}
onLogout={onLogout}
langeniusVersionInfo={langeniusVersionInfo}
/>
{children}
</div>
</DatasetsContext.Provider>
</AppContextProvider>
</SWRConfig>
)
}
export default React.memo(CommonLayout)
import React from "react"; import React from 'react'
import type { FC } from 'react' import type { ReactNode } from 'react'
import LayoutClient, { ICommonLayoutProps } from "./_layout-client"; import SwrInitor from '@/app/components/swr-initor'
import { AppContextProvider } from '@/context/app-context'
import GA, { GaType } from '@/app/components/base/ga' import GA, { GaType } from '@/app/components/base/ga'
import Header from '@/app/components/header'
const Layout: FC<ICommonLayoutProps> = ({ children }) => { const Layout = ({ children }: { children: ReactNode }) => {
return ( return (
<> <>
<GA gaType={GaType.admin} /> <GA gaType={GaType.admin} />
<LayoutClient children={children}></LayoutClient> <SwrInitor>
<AppContextProvider>
<Header />
{children}
</AppContextProvider>
</SwrInitor>
</> </>
) )
} }
...@@ -16,4 +23,4 @@ export const metadata = { ...@@ -16,4 +23,4 @@ export const metadata = {
title: 'Dify', title: 'Dify',
} }
export default Layout export default Layout
\ No newline at end of file
'use client' 'use client'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Fragment, useState } from 'react' import { Fragment, useState } from 'react'
import { useRouter } from 'next/navigation'
import { useContext } from 'use-context-selector' import { useContext } from 'use-context-selector'
import classNames from 'classnames' import classNames from 'classnames'
import Link from 'next/link' import Link from 'next/link'
...@@ -13,24 +14,33 @@ import WorkplaceSelector from './workplace-selector' ...@@ -13,24 +14,33 @@ import WorkplaceSelector from './workplace-selector'
import type { LangGeniusVersionResponse, UserProfileResponse } from '@/models/common' import type { LangGeniusVersionResponse, UserProfileResponse } from '@/models/common'
import I18n from '@/context/i18n' import I18n from '@/context/i18n'
import Avatar from '@/app/components/base/avatar' import Avatar from '@/app/components/base/avatar'
import { logout } from '@/service/common'
type IAppSelectorProps = { type IAppSelectorProps = {
userProfile: UserProfileResponse userProfile: UserProfileResponse
onLogout: () => void
langeniusVersionInfo: LangGeniusVersionResponse langeniusVersionInfo: LangGeniusVersionResponse
} }
export default function AppSelector({ userProfile, onLogout, langeniusVersionInfo }: IAppSelectorProps) { export default function AppSelector({ userProfile, langeniusVersionInfo }: IAppSelectorProps) {
const itemClassName = ` const itemClassName = `
flex items-center w-full h-10 px-3 text-gray-700 text-[14px] flex items-center w-full h-10 px-3 text-gray-700 text-[14px]
rounded-lg font-normal hover:bg-gray-100 cursor-pointer rounded-lg font-normal hover:bg-gray-100 cursor-pointer
` `
const router = useRouter()
const [settingVisible, setSettingVisible] = useState(false) const [settingVisible, setSettingVisible] = useState(false)
const [aboutVisible, setAboutVisible] = useState(false) const [aboutVisible, setAboutVisible] = useState(false)
const { locale } = useContext(I18n) const { locale } = useContext(I18n)
const { t } = useTranslation() const { t } = useTranslation()
const handleLogout = async () => {
await logout({
url: '/logout',
params: {},
})
router.push('/signin')
}
return ( return (
<div className=""> <div className="">
<Menu as="div" className="relative inline-block text-left"> <Menu as="div" className="relative inline-block text-left">
...@@ -107,7 +117,7 @@ export default function AppSelector({ userProfile, onLogout, langeniusVersionInf ...@@ -107,7 +117,7 @@ export default function AppSelector({ userProfile, onLogout, langeniusVersionInf
</Menu.Item> </Menu.Item>
</div> </div>
<Menu.Item> <Menu.Item>
<div className='p-1' onClick={() => onLogout()}> <div className='p-1' onClick={() => handleLogout()}>
<div <div
className='flex items-center justify-between h-12 px-3 rounded-lg cursor-pointer group hover:bg-gray-100' className='flex items-center justify-between h-12 px-3 rounded-lg cursor-pointer group hover:bg-gray-100'
> >
......
'use client' 'use client'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import type { FC } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useSelectedLayoutSegment } from 'next/navigation' import { usePathname, useSelectedLayoutSegment } from 'next/navigation'
import classNames from 'classnames' import classNames from 'classnames'
import { CommandLineIcon } from '@heroicons/react/24/solid' import { CommandLineIcon } from '@heroicons/react/24/solid'
import Link from 'next/link' import Link from 'next/link'
...@@ -10,19 +9,14 @@ import AccountDropdown from './account-dropdown' ...@@ -10,19 +9,14 @@ import AccountDropdown from './account-dropdown'
import AppNav from './app-nav' import AppNav from './app-nav'
import DatasetNav from './dataset-nav' import DatasetNav from './dataset-nav'
import s from './index.module.css' import s from './index.module.css'
import type { GithubRepo, LangGeniusVersionResponse, UserProfileResponse } from '@/models/common' import type { GithubRepo } from '@/models/common'
import { WorkspaceProvider } from '@/context/workspace-context' import { WorkspaceProvider } from '@/context/workspace-context'
import { useAppContext } from '@/context/app-context'
import { Grid01 } from '@/app/components/base/icons/src/vender/line/layout' import { Grid01 } from '@/app/components/base/icons/src/vender/line/layout'
import { Grid01 as Grid01Solid } from '@/app/components/base/icons/src/vender/solid/layout' import { Grid01 as Grid01Solid } from '@/app/components/base/icons/src/vender/solid/layout'
import { PuzzlePiece01 } from '@/app/components/base/icons/src/vender/line/development' import { PuzzlePiece01 } from '@/app/components/base/icons/src/vender/line/development'
import { PuzzlePiece01 as PuzzlePiece01Solid } from '@/app/components/base/icons/src/vender/solid/development' import { PuzzlePiece01 as PuzzlePiece01Solid } from '@/app/components/base/icons/src/vender/solid/development'
export type IHeaderProps = {
userProfile: UserProfileResponse
onLogout: () => void
langeniusVersionInfo: LangGeniusVersionResponse
isBordered: boolean
}
const navClassName = ` const navClassName = `
flex items-center relative mr-3 px-3 h-8 rounded-xl flex items-center relative mr-3 px-3 h-8 rounded-xl
font-medium text-sm font-medium text-sm
...@@ -32,18 +26,16 @@ const headerEnvClassName: { [k: string]: string } = { ...@@ -32,18 +26,16 @@ const headerEnvClassName: { [k: string]: string } = {
DEVELOPMENT: 'bg-[#FEC84B] border-[#FDB022] text-[#93370D]', DEVELOPMENT: 'bg-[#FEC84B] border-[#FDB022] text-[#93370D]',
TESTING: 'bg-[#A5F0FC] border-[#67E3F9] text-[#164C63]', TESTING: 'bg-[#A5F0FC] border-[#67E3F9] text-[#164C63]',
} }
const Header: FC<IHeaderProps> = ({ const Header = () => {
userProfile,
onLogout,
langeniusVersionInfo,
isBordered,
}) => {
const { t } = useTranslation() const { t } = useTranslation()
const pathname = usePathname()
const { userProfile, langeniusVersionInfo } = useAppContext()
const showEnvTag = langeniusVersionInfo.current_env === 'TESTING' || langeniusVersionInfo.current_env === 'DEVELOPMENT' const showEnvTag = langeniusVersionInfo.current_env === 'TESTING' || langeniusVersionInfo.current_env === 'DEVELOPMENT'
const selectedSegment = useSelectedLayoutSegment() const selectedSegment = useSelectedLayoutSegment()
const isPluginsComingSoon = selectedSegment === 'plugins-coming-soon' const isPluginsComingSoon = selectedSegment === 'plugins-coming-soon'
const isExplore = selectedSegment === 'explore' const isExplore = selectedSegment === 'explore'
const [starCount, setStarCount] = useState(0) const [starCount, setStarCount] = useState(0)
const isBordered = ['/apps', '/datasets'].includes(pathname)
useEffect(() => { useEffect(() => {
globalThis.fetch('https://api.github.com/repos/langgenius/dify').then(res => res.json()).then((data: GithubRepo) => { globalThis.fetch('https://api.github.com/repos/langgenius/dify').then(res => res.json()).then((data: GithubRepo) => {
...@@ -136,7 +128,7 @@ const Header: FC<IHeaderProps> = ({ ...@@ -136,7 +128,7 @@ const Header: FC<IHeaderProps> = ({
) )
} }
<WorkspaceProvider> <WorkspaceProvider>
<AccountDropdown userProfile={userProfile} onLogout={onLogout} langeniusVersionInfo={langeniusVersionInfo} /> <AccountDropdown userProfile={userProfile} langeniusVersionInfo={langeniusVersionInfo} />
</WorkspaceProvider> </WorkspaceProvider>
</div> </div>
</div> </div>
......
'use client'
import { useEffect } from 'react'
import * as Sentry from '@sentry/react'
const isDevelopment = process.env.NODE_ENV === 'development'
const SentryInit = ({
children,
}: { children: React.ReactElement }) => {
useEffect(() => {
const SENTRY_DSN = document?.body?.getAttribute('data-public-sentry-dsn')
if (!isDevelopment && SENTRY_DSN) {
Sentry.init({
dsn: SENTRY_DSN,
integrations: [
new Sentry.BrowserTracing({
}),
new Sentry.Replay(),
],
tracesSampleRate: 0.1,
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
})
}
}, [])
return children
}
export default SentryInit
'use client'
import { SWRConfig } from 'swr'
import type { ReactNode } from 'react'
type SwrInitorProps = {
children: ReactNode
}
const SwrInitor = ({
children,
}: SwrInitorProps) => {
return (
<SWRConfig value={{
shouldRetryOnError: false,
}}>
{children}
</SWRConfig>
)
}
export default SwrInitor
import I18nServer from './components/i18n-server' import I18nServer from './components/i18n-server'
import SentryInitor from './components/sentry-initor'
import { getLocaleOnServer } from '@/i18n/server' import { getLocaleOnServer } from '@/i18n/server'
import './styles/globals.css' import './styles/globals.css'
...@@ -14,6 +15,7 @@ const LocaleLayout = ({ ...@@ -14,6 +15,7 @@ const LocaleLayout = ({
children: React.ReactNode children: React.ReactNode
}) => { }) => {
const locale = getLocaleOnServer() const locale = getLocaleOnServer()
return ( return (
<html lang={locale ?? 'en'} className="h-full"> <html lang={locale ?? 'en'} className="h-full">
<body <body
...@@ -23,8 +25,10 @@ const LocaleLayout = ({ ...@@ -23,8 +25,10 @@ const LocaleLayout = ({
data-public-edition={process.env.NEXT_PUBLIC_EDITION} data-public-edition={process.env.NEXT_PUBLIC_EDITION}
data-public-sentry-dsn={process.env.NEXT_PUBLIC_SENTRY_DSN} data-public-sentry-dsn={process.env.NEXT_PUBLIC_SENTRY_DSN}
> >
{/* @ts-expect-error Async Server Component */} <SentryInitor>
<I18nServer locale={locale}>{children}</I18nServer> {/* @ts-expect-error Async Server Component */}
<I18nServer locale={locale}>{children}</I18nServer>
</SentryInitor>
</body> </body>
</html> </html>
) )
......
'use client' 'use client'
import { createRef, useEffect, useRef, useState } from 'react'
import useSWR from 'swr'
import { createContext, useContext, useContextSelector } from 'use-context-selector' import { createContext, useContext, useContextSelector } from 'use-context-selector'
import type { FC, ReactNode } from 'react'
import { fetchAppList } from '@/service/apps'
import Loading from '@/app/components/base/loading'
import { fetchLanggeniusVersion, fetchUserProfile } from '@/service/common'
import type { App } from '@/types/app' import type { App } from '@/types/app'
import type { UserProfileResponse } from '@/models/common' import type { LangGeniusVersionResponse, UserProfileResponse } from '@/models/common'
import { createRef, FC, PropsWithChildren } from 'react'
export const useSelector = <T extends any>(selector: (value: AppContextValue) => T): T =>
useContextSelector(AppContext, selector);
export type AppContextValue = { export type AppContextValue = {
apps: App[] apps: App[]
mutateApps: () => void mutateApps: () => void
userProfile: UserProfileResponse userProfile: UserProfileResponse
mutateUserProfile: () => void mutateUserProfile: () => void
pageContainerRef: React.RefObject<HTMLDivElement>, pageContainerRef: React.RefObject<HTMLDivElement>
useSelector: typeof useSelector, langeniusVersionInfo: LangGeniusVersionResponse
useSelector: typeof useSelector
} }
const AppContext = createContext<AppContextValue>({ const AppContext = createContext<AppContextValue>({
...@@ -27,18 +30,59 @@ const AppContext = createContext<AppContextValue>({ ...@@ -27,18 +30,59 @@ const AppContext = createContext<AppContextValue>({
}, },
mutateUserProfile: () => { }, mutateUserProfile: () => { },
pageContainerRef: createRef(), pageContainerRef: createRef(),
langeniusVersionInfo: {
current_env: '',
current_version: '',
latest_version: '',
release_date: '',
release_notes: '',
version: '',
can_auto_update: false,
},
useSelector, useSelector,
}) })
export type AppContextProviderProps = PropsWithChildren<{ export function useSelector<T>(selector: (value: AppContextValue) => T): T {
value: Omit<AppContextValue, 'useSelector'> return useContextSelector(AppContext, selector)
}> }
export type AppContextProviderProps = {
children: ReactNode
}
export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) => {
const pageContainerRef = useRef<HTMLDivElement>(null)
const { data: appList, mutate: mutateApps } = useSWR({ url: '/apps', params: { page: 1 } }, fetchAppList)
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])
export const AppContextProvider: FC<AppContextProviderProps> = ({ value, children }) => ( if (!appList || !userProfile || !langeniusVersionInfo)
<AppContext.Provider value={{ ...value, useSelector }}> return <Loading type='app' />
{children}
</AppContext.Provider> return (
) <AppContext.Provider value={{ apps: appList.data, mutateApps, userProfile, mutateUserProfile, pageContainerRef, langeniusVersionInfo, useSelector }}>
<div ref={pageContainerRef} className='relative flex flex-col h-full overflow-auto bg-gray-100'>
{children}
</div>
</AppContext.Provider>
)
}
export const useAppContext = () => useContext(AppContext) export const useAppContext = () => useContext(AppContext)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment