FEAT: NEW WORKFLOW ENGINE (#3160)

Co-authored-by: Joel <iamjoel007@gmail.com>
Co-authored-by: Yeuoly <admin@srmxy.cn>
Co-authored-by: JzoNg <jzongcode@gmail.com>
Co-authored-by: StyleZhang <jasonapring2015@outlook.com>
Co-authored-by: jyong <jyong@dify.ai>
Co-authored-by: nite-knite <nkCoding@gmail.com>
Co-authored-by: jyong <718720800@qq.com>
This commit is contained in:
takatost
2024-04-08 18:51:46 +08:00
committed by GitHub
parent 2fb9850af5
commit 7753ba2d37
1161 changed files with 103836 additions and 10327 deletions

View File

@ -24,7 +24,23 @@ done
if $api_modified; then
echo "Running Ruff linter on api module"
./dev/reformat
# python style checks rely on `ruff` in path
if ! command -v ruff &> /dev/null; then
echo "Installing Ruff ..."
pip install ruff
fi
ruff check ./api || status=$?
status=${status:-0}
if [ $status -ne 0 ]; then
echo "Ruff linter on api module error, exit code: $status"
echo "Please run 'dev/reformat' to fix the fixable linting errors."
exit 1
fi
fi
if $web_modified; then

View File

@ -6,11 +6,9 @@ export type IProps = {
params: { appId: string }
}
const Logs = async ({
params: { appId },
}: IProps) => {
const Logs = async () => {
return (
<Main pageType={PageType.annotation} appId={appId} />
<Main pageType={PageType.annotation} />
)
}

View File

@ -1,25 +1,20 @@
'use client'
import type { FC } from 'react'
import React, { useEffect, useMemo } from 'react'
import { useUnmount } from 'ahooks'
import React, { useCallback, useEffect, useState } from 'react'
import { usePathname, useRouter } from 'next/navigation'
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 { useStore } from '@/app/components/app/store'
import AppSideBar from '@/app/components/app-sidebar'
import type { NavIcon } from '@/app/components/app-sidebar/navLink'
import { fetchAppDetail } from '@/service/apps'
import { useAppContext } from '@/context/app-context'
import Loading from '@/app/components/base/loading'
import { BarChartSquare02, FileHeart02, PromptEngineering, TerminalSquare } from '@/app/components/base/icons/src/vender/line/development'
import { BarChartSquare02 as BarChartSquare02Solid, FileHeart02 as FileHeart02Solid, PromptEngineering as PromptEngineeringSolid, TerminalSquare as TerminalSquareSolid } from '@/app/components/base/icons/src/vender/solid/development'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
export type IAppDetailLayoutProps = {
children: React.ReactNode
@ -32,40 +27,103 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
params: { appId }, // get appId in path
} = props
const { t } = useTranslation()
const router = useRouter()
const pathname = usePathname()
const media = useBreakpoints()
const isMobile = media === MediaType.mobile
const { isCurrentWorkspaceManager } = useAppContext()
const detailParams = { url: '/apps', id: appId }
const { data: response } = useSWR(detailParams, fetchAppDetail)
const { appDetail, setAppDetail, setAppSiderbarExpand } = useStore()
const [navigation, setNavigation] = useState<Array<{
name: string
href: string
icon: NavIcon
selectedIcon: NavIcon
}>>([])
const navigation = useMemo(() => {
const getNavigations = useCallback((appId: string, isCurrentWorkspaceManager: boolean, mode: string) => {
const navs = [
...(isCurrentWorkspaceManager ? [{ name: t('common.appMenus.promptEng'), href: `/app/${appId}/configuration`, icon: Cog8ToothIcon, selectedIcon: Cog8ToothSolidIcon }] : []),
{ name: t('common.appMenus.overview'), href: `/app/${appId}/overview`, icon: ChartBarSquareIcon, selectedIcon: ChartBarSquareSolidIcon },
{ name: t('common.appMenus.apiAccess'), href: `/app/${appId}/develop`, icon: CommandLineIcon, selectedIcon: CommandLineSolidIcon },
{ name: t('common.appMenus.logAndAnn'), href: `/app/${appId}/logs`, icon: DocumentTextIcon, selectedIcon: DocumentTextSolidIcon },
...(isCurrentWorkspaceManager
? [{
name: t('common.appMenus.promptEng'),
href: `/app/${appId}/${(mode === 'workflow' || mode === 'advanced-chat') ? 'workflow' : 'configuration'}`,
icon: PromptEngineering,
selectedIcon: PromptEngineeringSolid,
}]
: []
),
{
name: t('common.appMenus.apiAccess'),
href: `/app/${appId}/develop`,
icon: TerminalSquare,
selectedIcon: TerminalSquareSolid,
},
{
name: mode !== 'workflow'
? t('common.appMenus.logAndAnn')
: t('common.appMenus.logs'),
href: `/app/${appId}/logs`,
icon: FileHeart02,
selectedIcon: FileHeart02Solid,
},
{
name: t('common.appMenus.overview'),
href: `/app/${appId}/overview`,
icon: BarChartSquare02,
selectedIcon: BarChartSquare02Solid,
},
]
return navs
}, [appId, isCurrentWorkspaceManager, t])
}, [t])
const appModeName = (() => {
if (response?.mode?.toUpperCase() === 'COMPLETION')
return t('app.newApp.completeApp')
const isAgent = !!response?.is_agent
if (isAgent)
return t('appDebug.assistantType.agentAssistant.name')
return t('appDebug.assistantType.chatAssistant.name')
})()
useEffect(() => {
if (response?.name)
document.title = `${(response.name || 'App')} - Dify`
}, [response])
if (!response)
return null
if (appDetail) {
document.title = `${(appDetail.name || 'App')} - Dify`
const localeMode = localStorage.getItem('app-detail-collapse-or-expand') || 'expand'
const mode = isMobile ? 'collapse' : 'expand'
setAppSiderbarExpand(isMobile ? mode : localeMode)
// TODO: consider screen size and mode
// if ((appDetail.mode === 'advanced-chat' || appDetail.mode === 'workflow') && (pathname).endsWith('workflow'))
// setAppSiderbarExpand('collapse')
}
}, [appDetail, isMobile])
useEffect(() => {
setAppDetail()
fetchAppDetail({ url: '/apps', id: appId }).then((res) => {
// redirections
if ((res.mode === 'workflow' || res.mode === 'advanced-chat') && (pathname).endsWith('configuration')) {
router.replace(`/app/${appId}/workflow`)
}
else if ((res.mode !== 'workflow' && res.mode !== 'advanced-chat') && (pathname).endsWith('workflow')) {
router.replace(`/app/${appId}/configuration`)
}
else {
setAppDetail(res)
setNavigation(getNavigations(appId, isCurrentWorkspaceManager, res.mode))
}
})
}, [appId, isCurrentWorkspaceManager])
useUnmount(() => {
setAppDetail()
})
if (!appDetail) {
return (
<div className='flex h-full items-center justify-center bg-white'>
<Loading />
</div>
)
}
return (
<div className={cn(s.app, 'flex', 'overflow-hidden')}>
<AppSideBar title={response.name} icon={response.icon} icon_background={response.icon_background} desc={appModeName} navigation={navigation} />
<div className="bg-white grow overflow-hidden">{children}</div>
{appDetail && (
<AppSideBar title={appDetail.name} icon={appDetail.icon} icon_background={appDetail.icon_background} desc={appDetail.mode} navigation={navigation} />
)}
<div className="bg-white grow overflow-hidden">
{children}
</div>
</div>
)
}

View File

@ -2,15 +2,9 @@ import React from 'react'
import Main from '@/app/components/app/log-annotation'
import { PageType } from '@/app/components/app/configuration/toolbox/annotation/type'
export type IProps = {
params: { appId: string }
}
const Logs = async ({
params: { appId },
}: IProps) => {
const Logs = async () => {
return (
<Main pageType={PageType.log} appId={appId} />
<Main pageType={PageType.log} />
)
}

View File

@ -3,7 +3,6 @@ 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'
@ -18,20 +17,22 @@ import type { UpdateAppSiteCodeResponse } from '@/models/app'
import { asyncRunSafe } from '@/utils'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import type { IAppCardProps } from '@/app/components/app/overview/appCard'
import { useStore as useAppStore } from '@/app/components/app/store'
export type ICardViewProps = {
appId: string
}
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()
const { notify } = useContext(ToastContext)
const { appDetail, setAppDetail } = useAppStore()
if (!response)
return <Loading />
const updateAppDetail = async () => {
fetchAppDetail({ url: '/apps', id: appId }).then((res) => {
setAppDetail(res)
})
}
const handleCallbackResult = (err: Error | null, message?: string) => {
const type = err ? 'error' : 'success'
@ -39,7 +40,7 @@ const CardView: FC<ICardViewProps> = ({ appId }) => {
message ||= (type === 'success' ? 'modifiedSuccessfully' : 'modifiedUnsuccessfully')
if (type === 'success')
mutate(detailParams)
updateAppDetail()
notify({
type,
@ -92,10 +93,13 @@ const CardView: FC<ICardViewProps> = ({ appId }) => {
handleCallbackResult(err, err ? 'generatedUnsuccessfully' : 'generatedSuccessfully')
}
if (!appDetail)
return <Loading />
return (
<div className="grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6">
<AppCard
appInfo={response}
appInfo={appDetail}
cardType="webapp"
onChangeStatus={onChangeSiteStatus}
onGenerateCode={onGenerateCode}
@ -103,7 +107,7 @@ const CardView: FC<ICardViewProps> = ({ appId }) => {
/>
<AppCard
cardType="api"
appInfo={response}
appInfo={appDetail}
onChangeStatus={onChangeApiStatus}
/>
</div>

View File

@ -3,13 +3,12 @@ import React, { useState } from 'react'
import dayjs from 'dayjs'
import quarterOfYear from 'dayjs/plugin/quarterOfYear'
import { useTranslation } from 'react-i18next'
import useSWR from 'swr'
import { fetchAppDetail } from '@/service/apps'
import type { PeriodParams } from '@/app/components/app/overview/appChart'
import { AvgResponseTime, AvgSessionInteractions, ConversationsChart, CostChart, EndUsersChart, TokenPerSecond, UserSatisfactionRate } from '@/app/components/app/overview/appChart'
import { AvgResponseTime, AvgSessionInteractions, AvgUserInteractions, ConversationsChart, CostChart, EndUsersChart, TokenPerSecond, UserSatisfactionRate, WorkflowCostChart, WorkflowDailyTerminalsChart, WorkflowMessagesChart } 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'
import { useStore as useAppStore } from '@/app/components/app/store'
dayjs.extend(quarterOfYear)
@ -22,10 +21,10 @@ export type IChartViewProps = {
}
export default function ChartView({ appId }: IChartViewProps) {
const detailParams = { url: '/apps', id: appId }
const { data: response } = useSWR(detailParams, fetchAppDetail)
const isChatApp = response?.mode === 'chat'
const { t } = useTranslation()
const { appDetail } = useAppStore()
const isChatApp = appDetail?.mode !== 'completion' && appDetail?.mode !== 'workflow'
const isWorkflow = appDetail?.mode === 'workflow'
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) => {
@ -42,7 +41,7 @@ export default function ChartView({ appId }: IChartViewProps) {
}
}
if (!response)
if (!appDetail)
return null
return (
@ -56,24 +55,42 @@ export default function ChartView({ appId }: IChartViewProps) {
defaultValue={7}
/>
</div>
<div className='grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6'>
<ConversationsChart period={period} id={appId} />
<EndUsersChart period={period} id={appId} />
</div>
<div className='grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6'>
{isChatApp
? (
<AvgSessionInteractions period={period} id={appId} />
)
: (
<AvgResponseTime period={period} id={appId} />
)}
<TokenPerSecond period={period} id={appId} />
</div>
<div className='grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6'>
<UserSatisfactionRate period={period} id={appId} />
<CostChart period={period} id={appId} />
</div>
{!isWorkflow && (
<div className='grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6'>
<ConversationsChart period={period} id={appId} />
<EndUsersChart period={period} id={appId} />
</div>
)}
{!isWorkflow && (
<div className='grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6'>
{isChatApp
? (
<AvgSessionInteractions period={period} id={appId} />
)
: (
<AvgResponseTime period={period} id={appId} />
)}
<TokenPerSecond period={period} id={appId} />
</div>
)}
{!isWorkflow && (
<div className='grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6'>
<UserSatisfactionRate period={period} id={appId} />
<CostChart period={period} id={appId} />
</div>
)}
{isWorkflow && (
<div className='grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6'>
<WorkflowMessagesChart period={period} id={appId} />
<WorkflowDailyTerminalsChart period={period} id={appId} />
</div>
)}
{isWorkflow && (
<div className='grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6'>
<WorkflowCostChart period={period} id={appId} />
<AvgUserInteractions period={period} id={appId} />
</div>
)}
</div>
)
}

View File

@ -0,0 +1,12 @@
'use client'
import Workflow from '@/app/components/workflow'
const Page = () => {
return (
<div className='w-full h-full overflow-x-auto'>
<Workflow />
</div>
)
}
export default Page

View File

@ -5,22 +5,26 @@ import { useRouter } from 'next/navigation'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import cn from 'classnames'
import style from '../list.module.css'
import AppModeLabel from './AppModeLabel'
import s from './style.module.css'
import SettingsModal from '@/app/components/app/overview/settings'
import type { ConfigParams } from '@/app/components/app/overview/settings'
import type { App } from '@/types/app'
import Confirm from '@/app/components/base/confirm'
import { ToastContext } from '@/app/components/base/toast'
import { deleteApp, fetchAppDetail, updateAppSiteConfig } from '@/service/apps'
import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps'
import DuplicateAppModal from '@/app/components/app/duplicate-modal'
import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-modal'
import AppIcon from '@/app/components/base/app-icon'
import AppsContext, { useAppContext } from '@/context/app-context'
import type { HtmlContentProps } from '@/app/components/base/popover'
import CustomPopover from '@/app/components/base/popover'
import Divider from '@/app/components/base/divider'
import { asyncRunSafe } from '@/utils'
import { getRedirection } from '@/utils/app-redirection'
import { useProviderContext } from '@/context/provider-context'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { AiText, ChatBot, CuteRobote } from '@/app/components/base/icons/src/vender/solid/communication'
import { Route } from '@/app/components/base/icons/src/vender/solid/mapsAndTravel'
import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal'
import EditAppModal from '@/app/components/explore/create-app-modal'
import SwitchAppModal from '@/app/components/app/switch-app-modal'
export type AppCardProps = {
app: App
@ -39,12 +43,10 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
state => state.mutateApps,
)
const [showEditModal, setShowEditModal] = useState(false)
const [showDuplicateModal, setShowDuplicateModal] = useState(false)
const [showSwitchModal, setShowSwitchModal] = useState<boolean>(false)
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
const [showSettingsModal, setShowSettingsModal] = useState(false)
const [detailState, setDetailState] = useState<{
loading: boolean
detail?: App
}>({ loading: false })
const onConfirmDelete = useCallback(async () => {
try {
@ -64,51 +66,105 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
setShowConfirmDelete(false)
}, [app.id])
const getAppDetail = async () => {
setDetailState({ loading: true })
const [err, res] = await asyncRunSafe(
fetchAppDetail({ url: '/apps', id: app.id }),
)
if (!err) {
setDetailState({ loading: false, detail: res })
setShowSettingsModal(true)
const onEdit: CreateAppModalProps['onConfirm'] = useCallback(async ({
name,
icon,
icon_background,
description,
}) => {
try {
await updateAppInfo({
appID: app.id,
name,
icon,
icon_background,
description,
})
setShowEditModal(false)
notify({
type: 'success',
message: t('app.editDone'),
})
if (onRefresh)
onRefresh()
mutateApps()
}
catch (e) {
notify({ type: 'error', message: t('app.editFailed') })
}
}, [app.id, mutateApps, notify, onRefresh, t])
const onCopy: DuplicateAppModalProps['onConfirm'] = async ({ name, icon, icon_background }) => {
try {
const newApp = await copyApp({
appID: app.id,
name,
icon,
icon_background,
mode: app.mode,
})
setShowDuplicateModal(false)
notify({
type: 'success',
message: t('app.newApp.appCreated'),
})
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
if (onRefresh)
onRefresh()
mutateApps()
onPlanInfoChanged()
getRedirection(isCurrentWorkspaceManager, newApp, push)
}
catch (e) {
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
}
else { setDetailState({ loading: false }) }
}
const onSaveSiteConfig = useCallback(
async (params: ConfigParams) => {
const [err] = await asyncRunSafe(
updateAppSiteConfig({
url: `/apps/${app.id}/site`,
body: params,
}),
)
if (!err) {
notify({
type: 'success',
message: t('common.actionMsg.modifiedSuccessfully'),
})
if (onRefresh)
onRefresh()
mutateApps()
}
else {
notify({
type: 'error',
message: t('common.actionMsg.modifiedUnsuccessfully'),
})
}
},
[app.id],
)
const onExport = async () => {
try {
const { data } = await exportAppConfig(app.id)
const a = document.createElement('a')
const file = new Blob([data], { type: 'application/yaml' })
a.href = URL.createObjectURL(file)
a.download = `${app.name}.yml`
a.click()
}
catch (e) {
notify({ type: 'error', message: t('app.exportFailed') })
}
}
const onSwitch = () => {
if (onRefresh)
onRefresh()
mutateApps()
setShowSwitchModal(false)
}
const Operations = (props: HtmlContentProps) => {
const onClickSettings = async (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
props.onClick?.()
e.preventDefault()
await getAppDetail()
setShowEditModal(true)
}
const onClickDuplicate = async (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
props.onClick?.()
e.preventDefault()
setShowDuplicateModal(true)
}
const onClickExport = async (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
props.onClick?.()
e.preventDefault()
onExport()
}
const onClickSwitch = async (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
props.onClick?.()
e.preventDefault()
setShowSwitchModal(true)
}
const onClickDelete = async (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
@ -117,11 +173,28 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
setShowConfirmDelete(true)
}
return (
<div className="w-full py-1">
<button className={s.actionItem} onClick={onClickSettings} disabled={detailState.loading}>
<span className={s.actionName}>{t('common.operation.settings')}</span>
<div className="relative w-full py-1">
<button className={s.actionItem} onClick={onClickSettings}>
<span className={s.actionName}>{t('app.editApp')}</span>
</button>
<Divider className="!my-1" />
<button className={s.actionItem} onClick={onClickDuplicate}>
<span className={s.actionName}>{t('app.duplicate')}</span>
</button>
<button className={s.actionItem} onClick={onClickExport}>
<span className={s.actionName}>{t('app.export')}</span>
</button>
{(app.mode === 'completion' || app.mode === 'chat') && (
<>
<Divider className="!my-1" />
<div
className='h-9 py-2 px-3 mx-1 flex items-center hover:bg-gray-50 rounded-lg cursor-pointer'
onClick={onClickSwitch}
>
<span className='text-gray-700 text-sm leading-5'>{t('app.switch')}</span>
</div>
</>
)}
<Divider className="!my-1" />
<div
className={cn(s.actionItem, s.deleteActionItem, 'group')}
@ -139,22 +212,47 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
<>
<div
onClick={(e) => {
if (showSettingsModal)
return
e.preventDefault()
push(`/app/${app.id}/${isCurrentWorkspaceManager ? 'configuration' : 'overview'}`)
getRedirection(isCurrentWorkspaceManager, app, push)
}}
className={style.listItem}
className='group flex col-span-1 bg-white border-2 border-solid border-transparent rounded-xl shadow-sm min-h-[160px] flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg'
>
<div className={style.listItemTitle}>
<AppIcon
size="small"
icon={app.icon}
background={app.icon_background}
/>
<div className={style.listItemHeading}>
<div className={style.listItemHeadingContent}>{app.name}</div>
<div className='flex pt-[14px] px-[14px] pb-3 h-[66px] items-center gap-3 grow-0 shrink-0'>
<div className='relative shrink-0'>
<AppIcon
size="large"
icon={app.icon}
background={app.icon_background}
/>
<span className='absolute bottom-[-3px] right-[-3px] w-4 h-4 p-0.5 bg-white rounded border-[0.5px] border-[rgba(0,0,0,0.02)] shadow-sm'>
{app.mode === 'advanced-chat' && (
<ChatBot className='w-3 h-3 text-[#1570EF]' />
)}
{app.mode === 'agent-chat' && (
<CuteRobote className='w-3 h-3 text-indigo-600' />
)}
{app.mode === 'chat' && (
<ChatBot className='w-3 h-3 text-[#1570EF]' />
)}
{app.mode === 'completion' && (
<AiText className='w-3 h-3 text-[#0E9384]' />
)}
{app.mode === 'workflow' && (
<Route className='w-3 h-3 text-[#f79009]' />
)}
</span>
</div>
<div className='grow w-0 py-[1px]'>
<div className='flex items-center text-sm leading-5 font-semibold text-gray-800'>
<div className='truncate' title={app.name}>{app.name}</div>
</div>
<div className='flex items-center text-[10px] leading-[18px] text-gray-500 font-medium'>
{app.mode === 'advanced-chat' && <div className='truncate'>{t('app.types.chatbot').toUpperCase()}</div>}
{app.mode === 'chat' && <div className='truncate'>{t('app.types.chatbot').toUpperCase()}</div>}
{app.mode === 'agent-chat' && <div className='truncate'>{t('app.types.agent').toUpperCase()}</div>}
{app.mode === 'workflow' && <div className='truncate'>{t('app.types.workflow').toUpperCase()}</div>}
{app.mode === 'completion' && <div className='truncate'>{t('app.types.completion').toUpperCase()}</div>}
</div>
</div>
{isCurrentWorkspaceManager && <CustomPopover
htmlContent={<Operations />}
@ -164,20 +262,49 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
btnClassName={open =>
cn(
open ? '!bg-gray-100 !shadow-none' : '!bg-transparent',
style.actionIconWrapper,
'!hidden h-8 w-8 !p-2 rounded-md border-none hover:!bg-gray-100 group-hover:!inline-flex',
)
}
className={'!w-[128px] h-fit !z-0'}
className={'!w-[128px] h-fit !z-20'}
popupClassName={
(app.mode === 'completion' || app.mode === 'chat')
? '!w-[238px] translate-x-[-110px]'
: ''
}
manualClose
/>}
</div>
<div className={style.listItemDescription}>
{app.model_config?.pre_prompt}
</div>
<div className={style.listItemFooter}>
<AppModeLabel mode={app.mode} isAgent={app.is_agent} />
</div>
<div className='mb-1 px-[14px] text-xs leading-normal text-gray-500 line-clamp-4'>{app.description}</div>
{showEditModal && (
<EditAppModal
isEditModal
appIcon={app.icon}
appIconBackground={app.icon_background}
appName={app.name}
appDescription={app.description}
show={showEditModal}
onConfirm={onEdit}
onHide={() => setShowEditModal(false)}
/>
)}
{showDuplicateModal && (
<DuplicateAppModal
appName={app.name}
icon={app.icon}
icon_background={app.icon_background}
show={showDuplicateModal}
onConfirm={onCopy}
onHide={() => setShowDuplicateModal(false)}
/>
)}
{showSwitchModal && (
<SwitchAppModal
show={showSwitchModal}
appDetail={app}
onClose={() => setShowSwitchModal(false)}
onSuccess={onSwitch}
/>
)}
{showConfirmDelete && (
<Confirm
title={t('app.deleteAppConfirmTitle')}
@ -188,14 +315,6 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
onCancel={() => setShowConfirmDelete(false)}
/>
)}
{showSettingsModal && detailState.detail && (
<SettingsModal
appInfo={detailState.detail}
isShow={showSettingsModal}
onClose={() => setShowSettingsModal(false)}
onSave={onSaveSiteConfig}
/>
)}
</div>
</>
)

View File

@ -1,54 +0,0 @@
'use client'
import { useTranslation } from 'react-i18next'
import { type AppMode } from '@/types/app'
import {
AiText,
CuteRobote,
} from '@/app/components/base/icons/src/vender/solid/communication'
import { BubbleText } from '@/app/components/base/icons/src/vender/solid/education'
export type AppModeLabelProps = {
mode: AppMode
isAgent?: boolean
className?: string
}
const AppModeLabel = ({
mode,
isAgent,
className,
}: AppModeLabelProps) => {
const { t } = useTranslation()
return (
<div className={`inline-flex items-center px-2 h-6 rounded-md border border-gray-100 text-xs text-gray-500 ${className}`}>
{
mode === 'completion' && (
<>
<AiText className='mr-1 w-3 h-3 text-gray-400' />
{t('app.newApp.completeApp')}
</>
)
}
{
mode === 'chat' && !isAgent && (
<>
<BubbleText className='mr-1 w-3 h-3 text-gray-400' />
{t('appDebug.assistantType.chatAssistant.name')}
</>
)
}
{
mode === 'chat' && isAgent && (
<>
<CuteRobote className='mr-1 w-3 h-3 text-gray-400' />
{t('appDebug.assistantType.agentAssistant.name')}
</>
)
}
</div>
)
}
export default AppModeLabel

View File

@ -11,10 +11,16 @@ import { fetchAppList } from '@/service/apps'
import { useAppContext } from '@/context/app-context'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { CheckModal } from '@/hooks/use-pay'
import TabSliderNew from '@/app/components/base/tab-slider-new'
import { useTabSearchParams } from '@/hooks/use-tab-searchparams'
import TabSlider from '@/app/components/base/tab-slider'
import { SearchLg } from '@/app/components/base/icons/src/vender/line/general'
import { DotsGrid, SearchLg } from '@/app/components/base/icons/src/vender/line/general'
import { XCircle } from '@/app/components/base/icons/src/vender/solid/general'
import {
// AiText,
ChatBot,
CuteRobot,
} from '@/app/components/base/icons/src/vender/line/communication'
import { Route } from '@/app/components/base/icons/src/vender/line/mapsAndTravel'
const getKey = (
pageIndex: number,
@ -27,6 +33,8 @@ const getKey = (
if (activeTab !== 'all')
params.params.mode = activeTab
else
delete params.params.mode
return params
}
@ -45,14 +53,16 @@ const Apps = () => {
const { data, isLoading, setSize, mutate } = useSWRInfinite(
(pageIndex: number, previousPageData: AppListResponse) => getKey(pageIndex, previousPageData, activeTab, searchKeywords),
fetchAppList,
{ revalidateFirstPage: false },
{ revalidateFirstPage: true },
)
const anchorRef = useRef<HTMLDivElement>(null)
const options = [
{ value: 'all', text: t('app.types.all') },
{ value: 'chat', text: t('app.types.assistant') },
{ value: 'completion', text: t('app.types.completion') },
{ value: 'all', text: t('app.types.all'), icon: <DotsGrid className='w-[14px] h-[14px] mr-1'/> },
{ value: 'chat', text: t('app.types.chatbot'), icon: <ChatBot className='w-[14px] h-[14px] mr-1'/> },
{ value: 'agent-chat', text: t('app.types.agent'), icon: <CuteRobot className='w-[14px] h-[14px] mr-1'/> },
// { value: 'completion', text: t('app.newApp.completeApp'), icon: <AiText className='w-[14px] h-[14px] mr-1'/> },
{ value: 'workflow', text: t('app.types.workflow'), icon: <Route className='w-[14px] h-[14px] mr-1'/> },
]
useEffect(() => {
@ -61,7 +71,7 @@ const Apps = () => {
localStorage.removeItem(NEED_REFRESH_APP_LIST_KEY)
mutate()
}
}, [mutate, t])
}, [])
useEffect(() => {
let observer: IntersectionObserver | undefined
@ -91,6 +101,11 @@ const Apps = () => {
return (
<>
<div className='sticky top-0 flex justify-between items-center pt-4 px-12 pb-2 leading-[56px] bg-gray-100 z-10 flex-wrap gap-y-2'>
<TabSliderNew
value={activeTab}
onChange={setActiveTab}
options={options}
/>
<div className="flex items-center px-2 w-[200px] h-8 rounded-lg bg-gray-200">
<div className="pointer-events-none shrink-0 flex items-center mr-1.5 justify-center w-4 h-4">
<SearchLg className="h-3.5 w-3.5 text-gray-500" aria-hidden="true" />
@ -117,12 +132,6 @@ const Apps = () => {
)
}
</div>
<TabSlider
value={activeTab}
onChange={setActiveTab}
options={options}
/>
</div>
<nav className='grid content-start grid-cols-1 gap-4 px-12 pt-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 grow shrink-0'>
{isCurrentWorkspaceManager

View File

@ -1,38 +1,77 @@
'use client'
import { forwardRef, useState } from 'react'
import classNames from 'classnames'
import { useTranslation } from 'react-i18next'
import style from '../list.module.css'
import NewAppDialog from './NewAppDialog'
import CreateAppTemplateDialog from '@/app/components/app/create-app-dialog'
import CreateAppModal from '@/app/components/app/create-app-modal'
import CreateFromDSLModal from '@/app/components/app/create-from-dsl-modal'
import { useProviderContext } from '@/context/provider-context'
import { FileArrow01, FilePlus01, FilePlus02 } from '@/app/components/base/icons/src/vender/line/files'
export type CreateAppCardProps = {
onSuccess?: () => void
}
// eslint-disable-next-line react/display-name
const CreateAppCard = forwardRef<HTMLAnchorElement, CreateAppCardProps>(({ onSuccess }, ref) => {
const { t } = useTranslation()
const { onPlanInfoChanged } = useProviderContext()
const [showNewAppDialog, setShowNewAppDialog] = useState(false)
const [showNewAppTemplateDialog, setShowNewAppTemplateDialog] = useState(false)
const [showNewAppModal, setShowNewAppModal] = useState(false)
const [showCreateFromDSLModal, setShowCreateFromDSLModal] = useState(false)
return (
<a ref={ref} 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')}
<a
ref={ref}
className='relative col-span-1 flex flex-col justify-between min-h-[160px] bg-gray-200 rounded-xl border-[0.5px] border-black/5'
>
<div className='grow p-2 rounded-t-xl'>
<div className='px-6 pt-2 pb-1 text-xs font-medium leading-[18px] text-gray-500'>{t('app.createApp')}</div>
<div className='flex items-center mb-1 px-6 py-[7px] rounded-lg text-[13px] font-medium leading-[18px] text-gray-600 cursor-pointer hover:text-primary-600 hover:bg-white' onClick={() => setShowNewAppModal(true)}>
<FilePlus01 className='shrink-0 mr-2 w-4 h-4' />
{t('app.newApp.startFromBlank')}
</div>
<div className='flex items-center px-6 py-[7px] rounded-lg text-[13px] font-medium leading-[18px] text-gray-600 cursor-pointer hover:text-primary-600 hover:bg-white' onClick={() => setShowNewAppTemplateDialog(true)}>
<FilePlus02 className='shrink-0 mr-2 w-4 h-4' />
{t('app.newApp.startFromTemplate')}
</div>
</div>
{/* <div className='text-xs text-gray-500'>{t('app.createFromConfigFile')}</div> */}
<NewAppDialog show={showNewAppDialog} onSuccess={
() => {
<div
className='p-2 border-t-[0.5px] border-black/5 rounded-b-xl'
onClick={() => setShowCreateFromDSLModal(true)}
>
<div className='flex items-center px-6 py-[7px] rounded-lg text-[13px] font-medium leading-[18px] text-gray-600 cursor-pointer hover:text-primary-600 hover:bg-white'>
<FileArrow01 className='shrink-0 mr-2 w-4 h-4' />
{t('app.importDSL')}
</div>
</div>
<CreateAppModal
show={showNewAppModal}
onClose={() => setShowNewAppModal(false)}
onSuccess={() => {
onPlanInfoChanged()
if (onSuccess)
onSuccess()
}} onClose={() => setShowNewAppDialog(false)} />
}}
/>
<CreateAppTemplateDialog
show={showNewAppTemplateDialog}
onClose={() => setShowNewAppTemplateDialog(false)}
onSuccess={() => {
onPlanInfoChanged()
if (onSuccess)
onSuccess()
}}
/>
<CreateFromDSLModal
show={showCreateFromDSLModal}
onClose={() => setShowCreateFromDSLModal(false)}
onSuccess={() => {
onPlanInfoChanged()
if (onSuccess)
onSuccess()
}}
/>
</a>
)
})

View File

@ -1,234 +0,0 @@
'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, { useAppContext } from '@/context/app-context'
import EmojiPicker from '@/app/components/base/emoji-picker'
import { useProviderContext } from '@/context/provider-context'
import AppsFull from '@/app/components/billing/apps-full-in-dialog'
import { AiText } from '@/app/components/base/icons/src/vender/solid/communication'
type NewAppDialogProps = {
show: boolean
onSuccess?: () => void
onClose?: () => void
}
const NewAppDialog = ({ show, onSuccess, onClose }: NewAppDialogProps) => {
const router = useRouter()
const { notify } = useContext(ToastContext)
const { isCurrentWorkspaceManager } = useAppContext()
const { t } = useTranslation()
const nameInputRef = useRef<HTMLInputElement>(null)
const [newAppMode, setNewAppMode] = useState<AppMode>()
const [isWithTemplate, setIsWithTemplate] = useState(false)
const [selectedTemplateIndex, setSelectedTemplateIndex] = useState<number>(-1)
// Emoji Picker
const [showEmojiPicker, setShowEmojiPicker] = useState(false)
const [emoji, setEmoji] = useState({ icon: '🤖', icon_background: '#FFEAD5' })
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)
}
}, [mutateTemplates, show])
const { plan, enableBilling } = useProviderContext()
const isAppsFull = (enableBilling && plan.usage.buildApps >= plan.total.buildApps)
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,
icon: emoji.icon,
icon_background: emoji.icon_background,
mode: isWithTemplate ? templates.data[selectedTemplateIndex].mode : newAppMode!,
config: isWithTemplate ? templates.data[selectedTemplateIndex].model_config : undefined,
})
if (onSuccess)
onSuccess()
if (onClose)
onClose()
notify({ type: 'success', message: t('app.newApp.appCreated') })
mutateApps()
router.push(`/app/${app.id}/${isCurrentWorkspaceManager ? 'configuration' : 'overview'}`)
}
catch (e) {
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
}
isCreatingRef.current = false
}, [isWithTemplate, newAppMode, notify, router, templates, selectedTemplateIndex, emoji])
return <>
{showEmojiPicker && <EmojiPicker
onSelect={(icon, icon_background) => {
setEmoji({ icon, icon_background })
setShowEmojiPicker(false)
}}
onClose={() => {
setEmoji({ icon: '🤖', icon_background: '#FFEAD5' })
setShowEmojiPicker(false)
}}
/>}
<Dialog
show={show}
title={t('app.newApp.startToCreate')}
footer={
<>
<Button onClick={onClose}>{t('app.newApp.Cancel')}</Button>
<Button disabled={isAppsFull} type="primary" onClick={onCreate}>{t('app.newApp.Create')}</Button>
</>
}
>
<div className='overflow-y-auto'>
<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-1 md: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 className='flex items-center h-[18px] border border-indigo-300 px-1 rounded-[5px] text-xs font-medium text-indigo-600 uppercase truncate'>{t('app.newApp.agentAssistant')}</div>
</div>
<div className={`${style.listItemDescription} ${style.noClip}`}>{t('app.newApp.chatAppIntro')}</div>
{/* <div className={classNames(style.listItemFooter, 'justify-end')}>
<a className={style.listItemLink} href='https://udify.app/chat/7CQBa5yyvYLSkZtx' target='_blank' rel='noopener noreferrer'>{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)} /> */}
<AiText className={classNames('w-5 h-5', newAppMode === 'completion' ? 'text-[#155EEF]' : 'text-gray-700')} />
</span>
<div className={style.listItemHeading}>
<div className={style.listItemHeadingContent}>{t('app.newApp.completeApp')}</div>
</div>
</div>
<div className={`${style.listItemDescription} ${style.noClip}`}>{t('app.newApp.completeAppIntro')}</div>
</li>
</ul>
</>
)
)}
{isWithTemplate && (
<ul className='grid grid-cols-1 md: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>
<div className='inline-block pl-3.5'>
<AppModeLabel mode={template.mode} isAgent={template.model_config.agent_mode.enabled} className='mt-2' />
</div>
</li>
))}
</ul>
)}
<div className='mt-8'>
<h3 className={style.newItemCaption}>{t('app.newApp.captionName')}</h3>
<div className='flex items-center justify-between gap-3'>
<AppIcon size='large' onClick={() => { setShowEmojiPicker(true) }} className='cursor-pointer' icon={emoji.icon} background={emoji.icon_background} />
<input ref={nameInputRef} className='h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg grow' placeholder={t('app.appNamePlaceholder') || ''} />
</div>
</div>
{
!isWithTemplate && (
<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>
{isAppsFull && <AppsFull loc='app-create' />}
</Dialog>
</>
}
export default NewAppDialog

View File

@ -10,7 +10,7 @@
mask-image: url(~@/assets/action.svg);
}
.actionItem {
@apply h-9 py-2 px-3 mx-1 flex items-center gap-2 hover:bg-gray-100 rounded-lg cursor-pointer;
@apply h-8 py-[6px] px-3 mx-1 flex items-center gap-2 hover:bg-gray-100 rounded-lg cursor-pointer;
width: calc(100% - 0.5rem);
}
.deleteActionItem {
@ -19,3 +19,11 @@
.actionName {
@apply text-gray-700 text-sm;
}
/* .completionPic {
background-image: url(~@/app/components/app-sidebar/completion.png)
}
.expertPic {
background-image: url(~@/app/components/app-sidebar/expert.png)
} */

View File

@ -28,7 +28,6 @@ import type { RelatedApp, RelatedAppResponse } from '@/models/datasets'
import { getLocaleOnClient } from '@/i18n'
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 FloatPopoverContainer from '@/app/components/base/float-popover-container'
@ -36,6 +35,9 @@ import DatasetDetailContext from '@/context/dataset-detail'
import { DataSourceType } from '@/models/datasets'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import { LanguagesSupported } from '@/i18n/language'
import { useStore } from '@/app/components/app/store'
import { AiText, ChatBot, CuteRobote } from '@/app/components/base/icons/src/vender/solid/communication'
import { Route } from '@/app/components/base/icons/src/vender/solid/mapsAndTravel'
export type IAppDetailLayoutProps = {
children: React.ReactNode
@ -51,18 +53,31 @@ type ILikedItemProps = {
const LikedItem = ({
type = 'app',
appStatus = true,
detail,
isMobile,
}: ILikedItemProps) => {
return (
<Link className={classNames(s.itemWrapper, 'px-0', isMobile && 'justify-center')} href={`/app/${detail?.id}/overview`}>
<Link className={classNames(s.itemWrapper, 'px-2', isMobile && 'justify-center')} href={`/app/${detail?.id}/overview`}>
<div className={classNames(s.iconWrapper, 'mr-0')}>
<AppIcon size='tiny' icon={detail?.icon} background={detail?.icon_background} />
{type === 'app' && (
<div className={s.statusPoint}>
<Indicator color={appStatus ? 'green' : 'gray'} />
</div>
<span className='absolute bottom-[-2px] right-[-2px] w-3.5 h-3.5 p-0.5 bg-white rounded border-[0.5px] border-[rgba(0,0,0,0.02)] shadow-sm'>
{detail.mode === 'advanced-chat' && (
<ChatBot className='w-2.5 h-2.5 text-[#1570EF]' />
)}
{detail.mode === 'agent-chat' && (
<CuteRobote className='w-2.5 h-2.5 text-indigo-600' />
)}
{detail.mode === 'chat' && (
<ChatBot className='w-2.5 h-2.5 text-[#1570EF]' />
)}
{detail.mode === 'completion' && (
<AiText className='w-2.5 h-2.5 text-[#0E9384]' />
)}
{detail.mode === 'workflow' && (
<Route className='w-2.5 h-2.5 text-[#f79009]' />
)}
</span>
)}
</div>
{!isMobile && <div className={classNames(s.appInfo, 'ml-2')}>{detail?.name || '--'}</div>}
@ -116,7 +131,7 @@ const ExtraInfo = ({ isMobile, relatedApps }: IExtraInfoProps) => {
<Divider className='mt-5' />
{(relatedApps?.data && relatedApps?.data?.length > 0) && (
<>
{!isMobile && <div className={s.subTitle}>{relatedApps?.total || '--'} {t('common.datasetMenus.relatedApp')}</div>}
{!isMobile && <div className='w-full px-2 pb-1 pt-4 uppercase text-xs text-gray-500 font-medium'>{relatedApps?.total || '--'} {t('common.datasetMenus.relatedApp')}</div>}
{isMobile && <div className={classNames(s.subTitle, 'flex items-center justify-center !px-0 gap-1')}>
{relatedApps?.total || '--'}
<PaperClipIcon className='h-4 w-4 text-gray-700' />
@ -136,7 +151,7 @@ const ExtraInfo = ({ isMobile, relatedApps }: IExtraInfoProps) => {
</div>
}
>
<div className={classNames('mt-5 p-3', isMobile && 'border-[0.5px] border-gray-200 shadow-lg rounded-lg bg-white w-[150px]')}>
<div className={classNames('mt-5 p-3', isMobile && 'border-[0.5px] border-gray-200 shadow-lg rounded-lg bg-white w-[160px]')}>
<div className='flex items-center justify-start gap-2'>
<div className={s.emptyIconDiv}>
<Squares2X2Icon className='w-3 h-3 text-gray-500' />
@ -198,6 +213,14 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
document.title = `${datasetRes.name || 'Dataset'} - Dify`
}, [datasetRes])
const { setAppSiderbarExpand } = useStore()
useEffect(() => {
const localeMode = localStorage.getItem('app-detail-collapse-or-expand') || 'expand'
const mode = isMobile ? 'collapse' : 'expand'
setAppSiderbarExpand(isMobile ? mode : localeMode)
}, [isMobile, setAppSiderbarExpand])
if (!datasetRes && !error)
return <Loading />

View File

@ -5,7 +5,7 @@
@apply truncate text-gray-700 text-sm font-normal;
}
.iconWrapper {
@apply relative w-6 h-6 bg-[#D5F5F6] rounded-md;
@apply relative w-6 h-6 rounded-lg;
}
.statusPoint {
@apply flex justify-center items-center absolute -right-0.5 -bottom-0.5 w-2.5 h-2.5 bg-white rounded;

View File

@ -10,7 +10,7 @@ import Datasets from './Datasets'
import DatasetFooter from './DatasetFooter'
import ApiServer from './ApiServer'
import Doc from './Doc'
import TabSlider from '@/app/components/base/tab-slider'
import TabSliderNew from '@/app/components/base/tab-slider-new'
// Services
import { fetchDatasetApiBaseUrl } from '@/service/datasets'
@ -35,7 +35,7 @@ const Container = () => {
return (
<div ref={containerRef} className='grow relative flex flex-col bg-gray-100 overflow-y-auto'>
<div className='sticky top-0 flex justify-between pt-4 px-12 pb-2 leading-[56px] bg-gray-100 z-10 flex-wrap gap-y-2'>
<TabSlider
<TabSliderNew
value={activeTab}
onChange={newActiveTab => setActiveTab(newActiveTab)}
options={options}

View File

@ -1,5 +1,5 @@
.listItem {
@apply col-span-1 bg-white border-2 border-solid border-transparent rounded-lg shadow-xs min-h-[160px] flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg;
@apply col-span-1 bg-white border-2 border-solid border-transparent rounded-xl shadow-xs min-h-[160px] flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg;
}
.listItem.newItemCard {

View File

@ -0,0 +1,13 @@
import type { FC } from 'react'
import React from 'react'
import type { IMainProps } from '@/app/components/share/text-generation'
import Main from '@/app/components/share/text-generation'
const TextGeneration: FC<IMainProps> = () => {
return (
<Main isWorkflow />
)
}
export default React.memo(TextGeneration)

View File

@ -0,0 +1,391 @@
import { useTranslation } from 'react-i18next'
import { useRouter } from 'next/navigation'
import { useContext, useContextSelector } from 'use-context-selector'
import cn from 'classnames'
import React, { useCallback, useState } from 'react'
import AppIcon from '../base/app-icon'
import SwitchAppModal from '../app/switch-app-modal'
import s from './style.module.css'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
import Divider from '@/app/components/base/divider'
import Confirm from '@/app/components/base/confirm'
import { useStore as useAppStore } from '@/app/components/app/store'
import { ToastContext } from '@/app/components/base/toast'
import AppsContext from '@/context/app-context'
import { useProviderContext } from '@/context/provider-context'
import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps'
import DuplicateAppModal from '@/app/components/app/duplicate-modal'
import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-modal'
import CreateAppModal from '@/app/components/explore/create-app-modal'
import { AiText, ChatBot, CuteRobote } from '@/app/components/base/icons/src/vender/solid/communication'
import { Route } from '@/app/components/base/icons/src/vender/solid/mapsAndTravel'
import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { getRedirection } from '@/utils/app-redirection'
export type IAppInfoProps = {
expand: boolean
}
const AppInfo = ({ expand }: IAppInfoProps) => {
const { t } = useTranslation()
const { notify } = useContext(ToastContext)
const { replace } = useRouter()
const { onPlanInfoChanged } = useProviderContext()
const appDetail = useAppStore(state => state.appDetail)
const setAppDetail = useAppStore(state => state.setAppDetail)
const [open, setOpen] = useState(false)
const [showEditModal, setShowEditModal] = useState(false)
const [showDuplicateModal, setShowDuplicateModal] = useState(false)
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
const [showSwitchTip, setShowSwitchTip] = useState<string>('')
const [showSwitchModal, setShowSwitchModal] = useState<boolean>(false)
const mutateApps = useContextSelector(
AppsContext,
state => state.mutateApps,
)
const onEdit: CreateAppModalProps['onConfirm'] = useCallback(async ({
name,
icon,
icon_background,
description,
}) => {
if (!appDetail)
return
try {
const app = await updateAppInfo({
appID: appDetail.id,
name,
icon,
icon_background,
description,
})
setShowEditModal(false)
notify({
type: 'success',
message: t('app.editDone'),
})
setAppDetail(app)
mutateApps()
}
catch (e) {
notify({ type: 'error', message: t('app.editFailed') })
}
}, [appDetail, mutateApps, notify, setAppDetail, t])
const onCopy: DuplicateAppModalProps['onConfirm'] = async ({ name, icon, icon_background }) => {
if (!appDetail)
return
try {
const newApp = await copyApp({
appID: appDetail.id,
name,
icon,
icon_background,
mode: appDetail.mode,
})
setShowDuplicateModal(false)
notify({
type: 'success',
message: t('app.newApp.appCreated'),
})
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
mutateApps()
onPlanInfoChanged()
getRedirection(true, newApp, replace)
}
catch (e) {
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
}
}
const onExport = async () => {
if (!appDetail)
return
try {
const { data } = await exportAppConfig(appDetail.id)
const a = document.createElement('a')
const file = new Blob([data], { type: 'application/yaml' })
a.href = URL.createObjectURL(file)
a.download = `${appDetail.name}.yml`
a.click()
}
catch (e) {
notify({ type: 'error', message: t('app.exportFailed') })
}
}
const onConfirmDelete = useCallback(async () => {
if (!appDetail)
return
try {
await deleteApp(appDetail.id)
notify({ type: 'success', message: t('app.appDeleted') })
mutateApps()
onPlanInfoChanged()
setAppDetail()
replace('/apps')
}
catch (e: any) {
notify({
type: 'error',
message: `${t('app.appDeleteFailed')}${'message' in e ? `: ${e.message}` : ''}`,
})
}
setShowConfirmDelete(false)
}, [appDetail, mutateApps, notify, onPlanInfoChanged, replace, t])
if (!appDetail)
return null
return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='bottom-start'
offset={4}
>
<div className='relative'>
<PortalToFollowElemTrigger
onClick={() => setOpen(v => !v)}
className='block'
>
<div className={cn('flex cursor-pointer p-1 rounded-lg hover:bg-gray-100', open && 'bg-gray-100')}>
<div className='relative shrink-0 mr-2'>
<AppIcon size={expand ? 'large' : 'small'} icon={appDetail.icon} background={appDetail.icon_background} />
<span className={cn(
'absolute bottom-[-3px] right-[-3px] w-4 h-4 p-0.5 bg-white rounded border-[0.5px] border-[rgba(0,0,0,0.02)] shadow-sm',
!expand && '!w-3.5 !h-3.5 !bottom-[-2px] !right-[-2px]',
)}>
{appDetail.mode === 'advanced-chat' && (
<ChatBot className={cn('w-3 h-3 text-[#1570EF]', !expand && '!w-2.5 !h-2.5')} />
)}
{appDetail.mode === 'agent-chat' && (
<CuteRobote className={cn('w-3 h-3 text-indigo-600', !expand && '!w-2.5 !h-2.5')} />
)}
{appDetail.mode === 'chat' && (
<ChatBot className={cn('w-3 h-3 text-[#1570EF]', !expand && '!w-2.5 !h-2.5')} />
)}
{appDetail.mode === 'completion' && (
<AiText className={cn('w-3 h-3 text-[#0E9384]', !expand && '!w-2.5 !h-2.5')} />
)}
{appDetail.mode === 'workflow' && (
<Route className={cn('w-3 h-3 text-[#f79009]', !expand && '!w-2.5 !h-2.5')} />
)}
</span>
</div>
{expand && (
<div className="grow w-0">
<div className='flex justify-between items-center text-sm leading-5 font-medium text-gray-900'>
<div className='truncate' title={appDetail.name}>{appDetail.name}</div>
<ChevronDown className='shrink-0 ml-[2px] w-3 h-3 text-gray-500' />
</div>
<div className='flex items-center text-[10px] leading-[18px] font-medium text-gray-500 gap-1'>
{appDetail.mode === 'advanced-chat' && (
<>
<div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.chatbot').toUpperCase()}</div>
<div title={t('app.newApp.advanced') || ''} className='px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.newApp.advanced').toUpperCase()}</div>
</>
)}
{appDetail.mode === 'agent-chat' && (
<div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.agent').toUpperCase()}</div>
)}
{appDetail.mode === 'chat' && (
<>
<div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.chatbot').toUpperCase()}</div>
<div title={t('app.newApp.basic') || ''} className='px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{(t('app.newApp.basic').toUpperCase())}</div>
</>
)}
{appDetail.mode === 'completion' && (
<>
<div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.completion').toUpperCase()}</div>
<div title={t('app.newApp.basic') || ''} className='px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{(t('app.newApp.basic').toUpperCase())}</div>
</>
)}
{appDetail.mode === 'workflow' && (
<div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.workflow').toUpperCase()}</div>
)}
</div>
</div>
)}
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1002]'>
<div className='relative w-[320px] bg-white rounded-2xl shadow-xl'>
{/* header */}
<div className={cn('flex pl-4 pt-3 pr-3', !appDetail.description && 'pb-2')}>
<div className='relative shrink-0 mr-2'>
<AppIcon size="large" icon={appDetail.icon} background={appDetail.icon_background} />
<span className='absolute bottom-[-3px] right-[-3px] w-4 h-4 p-0.5 bg-white rounded border-[0.5px] border-[rgba(0,0,0,0.02)] shadow-sm'>
{appDetail.mode === 'advanced-chat' && (
<ChatBot className='w-3 h-3 text-[#1570EF]' />
)}
{appDetail.mode === 'agent-chat' && (
<CuteRobote className='w-3 h-3 text-indigo-600' />
)}
{appDetail.mode === 'chat' && (
<ChatBot className='w-3 h-3 text-[#1570EF]' />
)}
{appDetail.mode === 'completion' && (
<AiText className='w-3 h-3 text-[#0E9384]' />
)}
{appDetail.mode === 'workflow' && (
<Route className='w-3 h-3 text-[#f79009]' />
)}
</span>
</div>
<div className='grow w-0'>
<div title={appDetail.name} className='flex justify-between items-center text-sm leading-5 font-medium text-gray-900 truncate'>{appDetail.name}</div>
<div className='flex items-center text-[10px] leading-[18px] font-medium text-gray-500 gap-1'>
{appDetail.mode === 'advanced-chat' && (
<>
<div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.chatbot').toUpperCase()}</div>
<div title={t('app.newApp.advanced') || ''} className='px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.newApp.advanced').toUpperCase()}</div>
</>
)}
{appDetail.mode === 'agent-chat' && (
<div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.agent').toUpperCase()}</div>
)}
{appDetail.mode === 'chat' && (
<>
<div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.chatbot').toUpperCase()}</div>
<div title={t('app.newApp.basic') || ''} className='px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{(t('app.newApp.basic').toUpperCase())}</div>
</>
)}
{appDetail.mode === 'completion' && (
<>
<div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.completion').toUpperCase()}</div>
<div title={t('app.newApp.basic') || ''} className='px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{(t('app.newApp.basic').toUpperCase())}</div>
</>
)}
{appDetail.mode === 'workflow' && (
<div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.workflow').toUpperCase()}</div>
)}
</div>
</div>
</div>
{/* desscription */}
{appDetail.description && (
<div className='px-4 py-2 text-gray-500 text-xs leading-[18px]'>{appDetail.description}</div>
)}
{/* operations */}
<Divider className="!my-1" />
<div className="w-full py-1">
<div className='h-9 py-2 px-3 mx-1 flex items-center hover:bg-gray-50 rounded-lg cursor-pointer' onClick={() => {
setOpen(false)
setShowEditModal(true)
}}>
<span className='text-gray-700 text-sm leading-5'>{t('app.editApp')}</span>
</div>
<div className='h-9 py-2 px-3 mx-1 flex items-center hover:bg-gray-50 rounded-lg cursor-pointer' onClick={() => {
setOpen(false)
setShowDuplicateModal(true)
}}>
<span className='text-gray-700 text-sm leading-5'>{t('app.duplicate')}</span>
</div>
<div className='h-9 py-2 px-3 mx-1 flex items-center hover:bg-gray-50 rounded-lg cursor-pointer' onClick={onExport}>
<span className='text-gray-700 text-sm leading-5'>{t('app.export')}</span>
</div>
{(appDetail.mode === 'completion' || appDetail.mode === 'chat') && (
<>
<Divider className="!my-1" />
<div
className='h-9 py-2 px-3 mx-1 flex items-center hover:bg-gray-50 rounded-lg cursor-pointer'
onMouseEnter={() => setShowSwitchTip(appDetail.mode)}
onMouseLeave={() => setShowSwitchTip('')}
onClick={() => {
setOpen(false)
setShowSwitchModal(true)
}}
>
<span className='text-gray-700 text-sm leading-5'>{t('app.switch')}</span>
</div>
</>
)}
<Divider className="!my-1" />
<div className='group h-9 py-2 px-3 mx-1 flex items-center hover:bg-red-50 rounded-lg cursor-pointer' onClick={() => {
setOpen(false)
setShowConfirmDelete(true)
}}>
<span className='text-gray-700 text-sm leading-5 group-hover:text-red-500'>
{t('common.operation.delete')}
</span>
</div>
</div>
{/* switch tip */}
<div
className={cn(
'hidden absolute left-[324px] top-0 w-[376px] rounded-xl bg-white border-[0.5px] border-[rgba(0,0,0,0.05)] shadow-lg',
showSwitchTip && '!block',
)}
>
<div className={cn(
'w-full h-[256px] bg-center bg-no-repeat bg-contain rounded-xl',
showSwitchTip === 'chat' && s.expertPic,
showSwitchTip === 'completion' && s.completionPic,
)}/>
<div className='px-4 pb-2'>
<div className='flex items-center gap-1 text-gray-700 text-md leading-6 font-semibold'>
{t('app.newApp.advanced')}
<span className='px-1 rounded-[5px] bg-white border border-black/8 text-gray-500 text-[10px] leading-[18px] font-medium'>BETA</span>
</div>
<div className='text-orange-500 text-xs leading-[18px] font-medium'>{t('app.newApp.advancedFor').toLocaleUpperCase()}</div>
<div className='mt-1 text-gray-500 text-sm leading-5'>{t('app.newApp.advancedDescription')}</div>
</div>
</div>
</div>
</PortalToFollowElemContent>
{showSwitchModal && (
<SwitchAppModal
inAppDetail
show={showSwitchModal}
appDetail={appDetail}
onClose={() => setShowSwitchModal(false)}
onSuccess={() => setShowSwitchModal(false)}
/>
)}
{showEditModal && (
<CreateAppModal
isEditModal
appIcon={appDetail.icon}
appIconBackground={appDetail.icon_background}
appName={appDetail.name}
appDescription={appDetail.description}
show={showEditModal}
onConfirm={onEdit}
onHide={() => setShowEditModal(false)}
/>
)}
{showDuplicateModal && (
<DuplicateAppModal
appName={appDetail.name}
icon={appDetail.icon}
icon_background={appDetail.icon_background}
show={showDuplicateModal}
onConfirm={onCopy}
onHide={() => setShowDuplicateModal(false)}
/>
)}
{showConfirmDelete && (
<Confirm
title={t('app.deleteAppConfirmTitle')}
content={t('app.deleteAppConfirmContent')}
isShow={showConfirmDelete}
onClose={() => setShowConfirmDelete(false)}
onConfirm={onConfirmDelete}
onCancel={() => setShowConfirmDelete(false)}
/>
)}
</div>
</PortalToFollowElem>
)
}
export default React.memo(AppInfo)

View File

@ -58,7 +58,7 @@ const ICON_MAP = {
export default function AppBasic({ icon, icon_background, name, type, hoverTip, textStyle, mode = 'expand', iconType = 'app' }: IAppBasicProps) {
return (
<div className="flex items-start">
<div className="flex items-start p-1">
{icon && icon_background && iconType === 'app' && (
<div className='flex-shrink-0 mr-3'>
<AppIcon icon={icon} background={icon_background} />

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

View File

@ -1,14 +1,14 @@
import React, { useCallback, useState } from 'react'
import React, { useEffect, useState } from 'react'
import NavLink from './navLink'
import type { NavIcon } from './navLink'
import AppBasic from './basic'
import AppInfo from './app-info'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import {
AlignLeft01,
AlignRight01,
} from '@/app/components/base/icons/src/vender/line/layout'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import { APP_SIDEBAR_SHOULD_COLLAPSE } from '@/app/components/app/configuration/debug/types'
import { useStore as useAppStore } from '@/app/components/app/store'
export type IAppDetailNavProps = {
iconType?: 'app' | 'dataset' | 'notion'
@ -26,28 +26,22 @@ export type IAppDetailNavProps = {
}
const AppDetailNav = ({ title, desc, icon, icon_background, navigation, extraInfo, iconType = 'app' }: IAppDetailNavProps) => {
const localeMode = localStorage.getItem('app-detail-collapse-or-expand') || 'expand'
const { appSidebarExpand, setAppSiderbarExpand } = useAppStore()
const media = useBreakpoints()
const isMobile = media === MediaType.mobile
const mode = isMobile ? 'collapse' : 'expand'
const [modeState, setModeState] = useState(isMobile ? mode : localeMode)
const [modeState, setModeState] = useState(appSidebarExpand)
const expand = modeState === 'expand'
const handleToggle = useCallback(() => {
setModeState((prev) => {
const next = prev === 'expand' ? 'collapse' : 'expand'
localStorage.setItem('app-detail-collapse-or-expand', next)
return next
})
}, [])
const handleToggle = (state: string) => {
setAppSiderbarExpand(state === 'expand' ? 'collapse' : 'expand')
}
const { eventEmitter } = useEventEmitterContextContext()
eventEmitter?.useSubscription((v: any) => {
if (v.type === APP_SIDEBAR_SHOULD_COLLAPSE) {
setModeState('collapse')
localStorage.setItem('app-detail-collapse-or-expand', 'collapse')
useEffect(() => {
if (appSidebarExpand) {
localStorage.setItem('app-detail-collapse-or-expand', appSidebarExpand)
setModeState(appSidebarExpand)
}
})
}, [appSidebarExpand])
return (
<div
@ -59,18 +53,26 @@ const AppDetailNav = ({ title, desc, icon, icon_background, navigation, extraInf
<div
className={`
shrink-0
${expand ? 'p-4' : 'p-2'}
${expand ? 'p-3' : 'p-2'}
`}
>
<AppBasic
mode={modeState}
iconType={iconType}
icon={icon}
icon_background={icon_background}
name={title}
type={desc}
/>
{iconType === 'app' && (
<AppInfo expand={expand}/>
)}
{iconType !== 'app' && (
<AppBasic
mode={modeState}
iconType={iconType}
icon={icon}
icon_background={icon_background}
name={title}
type={desc}
/>
)}
</div>
{!expand && (
<div className='mt-1 mx-auto w-6 h-[1px] bg-gray-100'/>
)}
<nav
className={`
grow space-y-1 bg-white
@ -94,7 +96,7 @@ const AppDetailNav = ({ title, desc, icon, icon_background, navigation, extraInf
>
<div
className='flex items-center justify-center w-6 h-6 text-gray-500 cursor-pointer'
onClick={handleToggle}
onClick={() => handleToggle(modeState)}
>
{
expand

View File

@ -1,3 +1,11 @@
.sidebar {
border-right: 1px solid #F3F4F6;
}
.completionPic {
background-image: url('./completion.png')
}
.expertPic {
background-image: url('./expert.png')
}

View File

@ -24,14 +24,14 @@ import { sleep } from '@/utils'
import { useProviderContext } from '@/context/provider-context'
import AnnotationFullModal from '@/app/components/billing/annotation-full/modal'
import { Settings04 } from '@/app/components/base/icons/src/vender/line/general'
import { fetchAppDetail } from '@/service/apps'
import type { App } from '@/types/app'
type Props = {
appId: string
appDetail: App
}
const Annotation: FC<Props> = ({
appId,
appDetail,
}) => {
const { t } = useTranslation()
const [isShowEdit, setIsShowEdit] = React.useState(false)
@ -39,16 +39,14 @@ const Annotation: FC<Props> = ({
const [isChatApp, setIsChatApp] = useState(false)
const fetchAnnotationConfig = async () => {
const res = await doFetchAnnotationConfig(appId)
const res = await doFetchAnnotationConfig(appDetail.id)
setAnnotationConfig(res as AnnotationReplyConfig)
}
useEffect(() => {
fetchAppDetail({ url: '/apps', id: appId }).then(async (res: any) => {
const isChatApp = res.mode === 'chat'
setIsChatApp(isChatApp)
if (isChatApp)
fetchAnnotationConfig()
})
const isChatApp = appDetail.mode !== 'completion'
setIsChatApp(isChatApp)
if (isChatApp)
fetchAnnotationConfig()
}, [])
const [controlRefreshSwitch, setControlRefreshSwitch] = useState(Date.now())
const { plan, enableBilling } = useProviderContext()
@ -57,7 +55,7 @@ const Annotation: FC<Props> = ({
const ensureJobCompleted = async (jobId: string, status: AnnotationEnableStatus) => {
let isCompleted = false
while (!isCompleted) {
const res: any = await queryAnnotationJobStatus(appId, status, jobId)
const res: any = await queryAnnotationJobStatus(appDetail.id, status, jobId)
isCompleted = res.job_status === JobStatus.completed
if (isCompleted)
break
@ -81,7 +79,7 @@ const Annotation: FC<Props> = ({
const fetchList = async (page = 1) => {
setIsLoading(true)
try {
const { data, total }: any = await fetchAnnotationList(appId, {
const { data, total }: any = await fetchAnnotationList(appDetail.id, {
...query,
page,
})
@ -104,7 +102,7 @@ const Annotation: FC<Props> = ({
}, [queryParams])
const handleAdd = async (payload: AnnotationItemBasic) => {
await addAnnotation(appId, {
await addAnnotation(appDetail.id, {
...payload,
})
Toast.notify({
@ -116,7 +114,7 @@ const Annotation: FC<Props> = ({
}
const handleRemove = async (id: string) => {
await delAnnotation(appId, id)
await delAnnotation(appDetail.id, id)
Toast.notify({
message: t('common.api.actionSuccess'),
type: 'success',
@ -137,7 +135,7 @@ const Annotation: FC<Props> = ({
}
const handleSave = async (question: string, answer: string) => {
await editAnnotation(appId, (currItem as AnnotationItem).id, {
await editAnnotation(appDetail.id, (currItem as AnnotationItem).id, {
question,
answer,
})
@ -153,7 +151,7 @@ const Annotation: FC<Props> = ({
<div className='flex flex-col h-full'>
<p className='flex text-sm font-normal text-gray-500'>{t('appLog.description')}</p>
<div className='grow flex flex-col py-4 '>
<Filter appId={appId} queryParams={queryParams} setQueryParams={setQueryParams}>
<Filter appId={appDetail.id} queryParams={queryParams} setQueryParams={setQueryParams}>
<div className='flex items-center space-x-2'>
{isChatApp && (
<>
@ -173,7 +171,7 @@ const Annotation: FC<Props> = ({
setIsShowEdit(true)
}
else {
const { job_id: jobId }: any = await updateAnnotationStatus(appId, AnnotationEnableStatus.disable, annotationConfig?.embedding_model, annotationConfig?.score_threshold)
const { job_id: jobId }: any = await updateAnnotationStatus(appDetail.id, AnnotationEnableStatus.disable, annotationConfig?.embedding_model, annotationConfig?.score_threshold)
await ensureJobCompleted(jobId, AnnotationEnableStatus.disable)
await fetchAnnotationConfig()
Toast.notify({
@ -205,7 +203,7 @@ const Annotation: FC<Props> = ({
)}
<HeaderOpts
appId={appId}
appId={appDetail.id}
controlUpdateList={controlUpdateList}
onAdd={handleAdd}
onAdded={() => {
@ -260,7 +258,7 @@ const Annotation: FC<Props> = ({
{isShowViewModal && (
<ViewAnnotationModal
appId={appId}
appId={appDetail.id}
isShow={isShowViewModal}
onHide={() => setIsShowViewModal(false)}
onRemove={async () => {
@ -272,7 +270,7 @@ const Annotation: FC<Props> = ({
)}
{isShowEdit && (
<ConfigParamModal
appId={appId}
appId={appDetail.id}
isShow
isInit={!annotationConfig?.enabled}
onHide={() => {
@ -283,12 +281,12 @@ const Annotation: FC<Props> = ({
embeddingModel.embedding_model_name !== annotationConfig?.embedding_model?.embedding_model_name
&& embeddingModel.embedding_provider_name !== annotationConfig?.embedding_model?.embedding_provider_name
) {
const { job_id: jobId }: any = await updateAnnotationStatus(appId, AnnotationEnableStatus.enable, embeddingModel, score)
const { job_id: jobId }: any = await updateAnnotationStatus(appDetail.id, AnnotationEnableStatus.enable, embeddingModel, score)
await ensureJobCompleted(jobId, AnnotationEnableStatus.enable)
}
if (score !== annotationConfig?.score_threshold)
await updateAnnotationScore(appId, annotationConfig?.id || '', score)
await updateAnnotationScore(appDetail.id, annotationConfig?.id || '', score)
await fetchAnnotationConfig()
Toast.notify({

View File

@ -1,265 +0,0 @@
import type { AnnotationItem, HitHistoryItem } from './type'
const list: AnnotationItem[] = [
// create some mock data
{
id: '1',
question: 'What is the capital of the United States?What is the capital of the United States?What is the capital of the United States?What is the capital of the United States?What is the capital of the United States?What is the capital of the United States?What is the capital of the United States?',
answer: 'What is the capital of the United States?What is the capital of the United States?What is the capital of the United States?What is the capital of the United States?What is the capital of the United States?What is the capital of the United States?What is the capital of the United States?',
created_at: '2020-01-01T00:00:00Z',
hit_count: 1,
},
{
id: '2',
question: 'What is the capital of Canada?',
answer: 'Ottawa',
created_at: '2020-01-02T00:00:00Z',
hit_count: 2,
},
{
id: '3',
question: 'What is the capital of Mexico?',
answer: 'Mexico City',
created_at: '2020-01-03T00:00:00Z',
hit_count: 3,
},
{
id: '4',
question: 'What is the capital of Brazil?',
answer: 'Brasilia',
created_at: '2020-01-04T00:00:00Z',
hit_count: 4,
},
{
id: '5',
question: 'What is the capital of Argentina?',
answer: 'Buenos Aires',
created_at: '2020-01-05T00:00:00Z',
hit_count: 5,
},
{
id: '6',
question: 'What is the capital of Chile?',
answer: 'Santiago',
created_at: '2020-01-06T00:00:00Z',
hit_count: 6,
},
{
id: '7',
question: 'What is the capital of Peru?',
answer: 'Lima',
created_at: '2020-01-07T00:00:00Z',
hit_count: 7,
},
{
id: '8',
question: 'What is the capital of Ecuador?',
answer: 'Quito',
created_at: '2020-01-08T00:00:00Z',
hit_count: 8,
},
{
id: '9',
question: 'What is the capital of Colombia?',
answer: 'Bogota',
created_at: '2020-01-09T00:00:00Z',
hit_count: 9,
},
]
export const hitHistoryList: HitHistoryItem[] = [
// create some mock data. source can only be: API/Webapp/Explore/Debug
{
id: '1',
question: 'What is the capital of the United States?What is the capital of the United States?What is the capital of the United States?What is the capital of the United States?What is the capital of the United States?What is the capital of the United States?What is the capital of the United States?',
source: 'API',
score: 0.9,
created_at: '2020-01-01T00:00:00Z',
},
{
id: '2',
question: 'What is the capital of Canada?',
source: 'Webapp',
score: 0.8,
created_at: '2020-01-02T00:00:00Z',
},
{
id: '3',
question: 'What is the capital of Mexico?',
source: 'Explore',
score: 0.7,
created_at: '2020-01-03T00:00:00Z',
},
{
id: '4',
question: 'What is the capital of Brazil?',
source: 'Debug',
score: 0.6,
created_at: '2020-01-04T00:00:00Z',
},
{
id: '5',
question: 'What is the capital of Argentina?',
source: 'API',
score: 0.5,
created_at: '2020-01-05T00:00:00Z',
},
{
id: '6',
question: 'What is the capital of Chile?',
source: 'Webapp',
score: 0.4,
created_at: '2020-01-06T00:00:00Z',
},
{
id: '7',
question: 'What is the capital of Peru?',
source: 'Explore',
score: 0.3,
created_at: '2020-01-07T00:00:00Z',
},
{
id: '8',
question: 'What is the capital of Ecuador?',
source: 'Debug',
score: 0.2,
created_at: '2020-01-08T00:00:00Z',
},
{
id: '9',
question: 'What is the capital of Colombia?',
source: 'API',
score: 0.1,
created_at: '2020-01-09T00:00:00Z',
},
// make more mock data
{
id: '10',
question: 'What is the capital of the United States?',
source: 'API',
score: 0.9,
created_at: '2020-01-01T00:00:00Z',
},
{
id: '11',
question: 'What is the capital of Canada?',
source: 'Webapp',
score: 0.8,
created_at: '2020-01-02T00:00:00Z',
},
{
id: '12',
question: 'What is the capital of Mexico?',
source: 'Explore',
score: 0.7,
created_at: '2020-01-03T00:00:00Z',
},
{
id: '13',
question: 'What is the capital of Brazil?',
source: 'Debug',
score: 0.6,
created_at: '2020-01-04T00:00:00Z',
},
{
id: '14',
question: 'What is the capital of Argentina?',
source: 'API',
score: 0.5,
created_at: '2020-01-05T00:00:00Z',
},
{
id: '15',
question: 'What is the capital of Chile?',
source: 'Webapp',
score: 0.4,
created_at: '2020-01-06T00:00:00Z',
},
{
id: '16',
question: 'What is the capital of Peru?',
source: 'Explore',
score: 0.3,
created_at: '2020-01-07T00:00:00Z',
},
{
id: '17',
question: 'What is the capital of Ecuador?',
source: 'Debug',
score: 0.2,
created_at: '2020-01-08T00:00:00Z',
},
{
id: '18',
question: 'What is the capital of Colombia?',
source: 'API',
score: 0.1,
created_at: '2020-01-09T00:00:00Z',
},
// make more mock data
{
id: '19',
question: 'What is the capital of the United States?',
source: 'API',
score: 0.9,
created_at: '2020-01-01T00:00:00Z',
},
{
id: '20',
question: 'What is the capital of Canada?',
source: 'Webapp',
score: 0.8,
created_at: '2020-01-02T00:00:00Z',
},
{
id: '21',
question: 'What is the capital of Mexico?',
source: 'Explore',
score: 0.7,
created_at: '2020-01-03T00:00:00Z',
},
{
id: '22',
question: 'What is the capital of Brazil?',
source: 'Debug',
score: 0.6,
created_at: '2020-01-04T00:00:00Z',
},
{
id: '23',
question: 'What is the capital of Argentina?',
source: 'API',
score: 0.5,
created_at: '2020-01-05T00:00:00Z',
},
{
id: '24',
question: 'What is the capital of Chile?',
source: 'Webapp',
score: 0.4,
created_at: '2020-01-06T00:00:00Z',
},
{
id: '25',
question: 'What is the capital of Peru?',
source: 'Explore',
score: 0.3,
created_at: '2020-01-07T00:00:00Z',
},
{
id: '26',
question: 'What is the capital of Ecuador?',
source: 'Debug',
score: 0.2,
created_at: '2020-01-08T00:00:00Z',
},
{
id: '27',
question: 'What is the capital of Colombia?',
source: 'API',
score: 0.1,
created_at: '2020-01-09T00:00:00Z',
},
]
export default list

View File

@ -0,0 +1,219 @@
import {
memo,
useCallback,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import dayjs from 'dayjs'
import classNames from 'classnames'
import type { ModelAndParameter } from '../configuration/debug/types'
import SuggestedAction from './suggested-action'
import PublishWithMultipleModel from './publish-with-multiple-model'
import Button from '@/app/components/base/button'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import EmbeddedModal from '@/app/components/app/overview/embedded'
import { useStore as useAppStore } from '@/app/components/app/store'
import { useGetLanguage } from '@/context/i18n'
import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
import { PlayCircle } from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
import { CodeBrowser } from '@/app/components/base/icons/src/vender/line/development'
import { LeftIndent02 } from '@/app/components/base/icons/src/vender/line/editor'
import { FileText } from '@/app/components/base/icons/src/vender/line/files'
export type AppPublisherProps = {
disabled?: boolean
publishDisabled?: boolean
publishedAt?: number
/** only needed in workflow / chatflow mode */
draftUpdatedAt?: number
debugWithMultipleModel?: boolean
multipleModelConfigs?: ModelAndParameter[]
/** modelAndParameter is passed when debugWithMultipleModel is true */
onPublish?: (modelAndParameter?: ModelAndParameter) => Promise<any> | any
onRestore?: () => Promise<any> | any
onToggle?: (state: boolean) => void
crossAxisOffset?: number
}
const AppPublisher = ({
disabled = false,
publishDisabled = false,
publishedAt,
draftUpdatedAt,
debugWithMultipleModel = false,
multipleModelConfigs = [],
onPublish,
onRestore,
onToggle,
crossAxisOffset = 0,
}: AppPublisherProps) => {
const { t } = useTranslation()
const [published, setPublished] = useState(false)
const [open, setOpen] = useState(false)
const appDetail = useAppStore(state => state.appDetail)
const { app_base_url: appBaseURL = '', access_token: accessToken = '' } = appDetail?.site ?? {}
const appMode = (appDetail?.mode !== 'completion' && appDetail?.mode !== 'workflow') ? 'chat' : appDetail.mode
const appURL = `${appBaseURL}/${appMode}/${accessToken}`
const language = useGetLanguage()
const formatTimeFromNow = useCallback((time: number) => {
return dayjs(time).locale(language === 'zh_Hans' ? 'zh-cn' : language.replace('_', '-')).fromNow()
}, [language])
const handlePublish = async (modelAndParameter?: ModelAndParameter) => {
try {
await onPublish?.(modelAndParameter)
setPublished(true)
}
catch (e) {
setPublished(false)
}
}
const handleRestore = useCallback(async () => {
try {
await onRestore?.()
setOpen(false)
}
catch (e) { }
}, [onRestore])
const handleTrigger = useCallback(() => {
const state = !open
if (disabled) {
setOpen(false)
return
}
onToggle?.(state)
setOpen(state)
if (state)
setPublished(false)
}, [disabled, onToggle, open])
const [embeddingModalOpen, setEmbeddingModalOpen] = useState(false)
return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='bottom-end'
offset={{
mainAxis: 4,
crossAxis: crossAxisOffset,
}}
>
<PortalToFollowElemTrigger onClick={handleTrigger}>
<Button
type='primary'
className={`
pl-3 pr-2 py-0 h-8 text-[13px] font-medium
${disabled && 'cursor-not-allowed opacity-50'}
`}
>
{t('workflow.common.publish')}
<ChevronDown className='ml-0.5' />
</Button>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[11]'>
<div className='w-[320px] bg-white rounded-2xl border-[0.5px] border-gray-200 shadow-xl'>
<div className='p-4 pt-3'>
<div className='flex items-center h-6 text-xs font-medium text-gray-500 uppercase'>
{publishedAt ? t('workflow.common.latestPublished') : t('workflow.common.currentDraftUnpublished')}
</div>
{publishedAt
? (
<div className='flex justify-between items-center h-[18px]'>
<div className='flex items-center mt-[3px] mb-[3px] leading-[18px] text-[13px] font-medium text-gray-700'>
{t('workflow.common.publishedAt')} {formatTimeFromNow(publishedAt)}
</div>
<Button
className={`
ml-2 px-2 py-0 h-6 shadow-xs rounded-md text-xs font-medium text-primary-600 border-[0.5px] bg-white border-gray-200
${published && 'text-primary-300 border-gray-100'}
`}
onClick={handleRestore}
disabled={published}
>
{t('workflow.common.restore')}
</Button>
</div>
)
: (
<div className='flex items-center h-[18px] leading-[18px] text-[13px] font-medium text-gray-700'>
{t('workflow.common.autoSaved')} · {Boolean(draftUpdatedAt) && formatTimeFromNow(draftUpdatedAt!)}
</div>
)}
{debugWithMultipleModel
? (
<PublishWithMultipleModel
multipleModelConfigs={multipleModelConfigs}
onSelect={item => handlePublish(item)}
// textGenerationModelList={textGenerationModelList}
/>
)
: (
<Button
type='primary'
className={classNames(
'mt-3 px-3 py-0 w-full h-8 border-[0.5px] border-primary-700 rounded-lg text-[13px] font-medium',
(publishDisabled || published) && 'border-transparent',
)}
onClick={() => handlePublish()}
disabled={publishDisabled || published}
>
{
published
? t('workflow.common.published')
: publishedAt ? t('workflow.common.update') : t('workflow.common.publish')
}
</Button>
)
}
</div>
<div className='p-4 pt-3 border-t-[0.5px] border-t-black/5'>
<SuggestedAction disabled={!publishedAt} link={appURL} icon={<PlayCircle />}>{t('workflow.common.runApp')}</SuggestedAction>
{appDetail?.mode === 'workflow'
? (
<SuggestedAction
disabled={!publishedAt}
link={`${appURL}${appURL.includes('?') ? '&' : '?'}mode=batch`}
icon={<LeftIndent02 className='w-4 h-4' />}
>
{t('workflow.common.batchRunApp')}
</SuggestedAction>
)
: (
<SuggestedAction
onClick={() => {
setEmbeddingModalOpen(true)
handleTrigger()
}}
disabled={!publishedAt}
icon={<CodeBrowser className='w-4 h-4' />}
>
{t('workflow.common.embedIntoSite')}
</SuggestedAction>
)}
<SuggestedAction disabled={!publishedAt} link='./develop' icon={<FileText className='w-4 h-4' />}>{t('workflow.common.accessAPIReference')}</SuggestedAction>
</div>
</div>
</PortalToFollowElemContent>
<EmbeddedModal
isShow={embeddingModalOpen}
onClose={() => setEmbeddingModalOpen(false)}
appBaseUrl={appBaseURL}
accessToken={accessToken}
className='z-50'
/>
</PortalToFollowElem >
)
}
export default memo(AppPublisher)

View File

@ -1,7 +1,8 @@
import type { FC } from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import type { ModelAndParameter } from '../types'
import type { ModelAndParameter } from '../configuration/debug/types'
import ModelIcon from '../../header/account-setting/model-provider-page/model-icon'
import Button from '@/app/components/base/button'
import {
PortalToFollowElem,
@ -10,15 +11,17 @@ import {
} from '@/app/components/base/portal-to-follow-elem'
import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
import { useProviderContext } from '@/context/provider-context'
import type { ModelItem } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { Model, ModelItem } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
type PublishWithMultipleModelProps = {
multipleModelConfigs: ModelAndParameter[]
// textGenerationModelList?: Model[]
onSelect: (v: ModelAndParameter) => void
}
const PublishWithMultipleModel: FC<PublishWithMultipleModelProps> = ({
multipleModelConfigs,
// textGenerationModelList = [],
onSelect,
}) => {
const { t } = useTranslation()
@ -26,7 +29,7 @@ const PublishWithMultipleModel: FC<PublishWithMultipleModelProps> = ({
const { textGenerationModelList } = useProviderContext()
const [open, setOpen] = useState(false)
const validModelConfigs: (ModelAndParameter & { modelItem: ModelItem })[] = []
const validModelConfigs: (ModelAndParameter & { modelItem: ModelItem; providerItem: Model })[] = []
multipleModelConfigs.forEach((item) => {
const provider = textGenerationModelList.find(model => model.provider === item.provider)
@ -40,6 +43,7 @@ const PublishWithMultipleModel: FC<PublishWithMultipleModelProps> = ({
model: item.model,
provider: item.provider,
modelItem: model,
providerItem: provider,
parameters: item.parameters,
})
}
@ -62,18 +66,18 @@ const PublishWithMultipleModel: FC<PublishWithMultipleModelProps> = ({
onOpenChange={setOpen}
placement='bottom-end'
>
<PortalToFollowElemTrigger onClick={handleToggle}>
<PortalToFollowElemTrigger className='w-full' onClick={handleToggle}>
<Button
type='primary'
disabled={!validModelConfigs.length}
className='pl-3 pr-2 h-8 text-[13px]'
className='mt-3 px-3 py-0 w-full h-8 border-[0.5px] border-primary-700 rounded-lg text-[13px] font-medium'
>
{t('appDebug.operation.applyConfig')}
<ChevronDown className='ml-0.5 w-3 h-3' />
</Button>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent>
<div className='p-1 w-[168px] rounded-lg border-[0.5px] border-gray-200 shadow-lg bg-white'>
<PortalToFollowElemContent className='mt-1 w-[288px] z-50'>
<div className='p-1 rounded-lg border-[0.5px] border-gray-200 shadow-lg bg-white'>
<div className='flex items-center px-3 h-[22px] text-xs font-medium text-gray-500'>
{t('appDebug.publishAs')}
</div>
@ -81,10 +85,11 @@ const PublishWithMultipleModel: FC<PublishWithMultipleModelProps> = ({
validModelConfigs.map((item, index) => (
<div
key={item.id}
className='flex items-center px-3 h-8 rounded-lg hover:bg-gray-100 cursor-pointer text-sm text-gray-500'
className='flex items-center h-8 px-3 text-sm text-gray-500 rounded-lg cursor-pointer hover:bg-gray-100'
onClick={() => handleSelect(item)}
>
#{index + 1}
<span className='italic min-w-[18px]'>#{index + 1}</span>
<ModelIcon modelName={item.model} provider={item.providerItem} className='ml-2' />
<div
className='ml-1 text-gray-700 truncate'
title={item.modelItem.label[language]}

View File

@ -0,0 +1,29 @@
import type { HTMLProps, PropsWithChildren } from 'react'
import classNames from 'classnames'
import { ArrowUpRight } from '@/app/components/base/icons/src/vender/line/arrows'
export type SuggestedActionProps = PropsWithChildren<HTMLProps<HTMLAnchorElement> & {
icon?: React.ReactNode
link?: string
disabled?: boolean
}>
const SuggestedAction = ({ icon, link, disabled, children, className, ...props }: SuggestedActionProps) => (
<a
href={disabled ? undefined : link}
target='_blank'
rel='noreferrer'
className={classNames(
'flex justify-start items-center gap-2 h-[34px] px-2.5 bg-gray-100 rounded-lg transition-colors [&:not(:first-child)]:mt-1',
disabled ? 'shadow-xs opacity-30 cursor-not-allowed' : 'hover:bg-primary-50 hover:text-primary-600 cursor-pointer',
className,
)}
{...props}
>
<div className='relative w-4 h-4'>{icon}</div>
<div className='grow shrink basis-0 text-[13px] font-medium leading-[18px]'>{children}</div>
<ArrowUpRight />
</a>
)
export default SuggestedAction

View File

@ -1,13 +1,13 @@
'use client'
import type { FC, ReactNode } from 'react'
import React, { useState } from 'react'
import React, { useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { UserCircleIcon } from '@heroicons/react/24/solid'
import cn from 'classnames'
import type { CitationItem, DisplayScene, FeedbackFunc, Feedbacktype, IChatItem } from '../type'
import OperationBtn from '../operation'
import LoadingAnim from '../loading-anim'
import { EditIconSolid, RatingIcon } from '../icon-component'
import { RatingIcon } from '../icon-component'
import s from '../style.module.css'
import MoreInfo from '../more-info'
import CopyBtn from '../copy-btn'
@ -26,16 +26,8 @@ import { MessageFast } from '@/app/components/base/icons/src/vender/solid/commun
import type { Emoji } from '@/app/components/tools/types'
import type { VisionFile } from '@/types/app'
import ImageGallery from '@/app/components/base/image-gallery'
import Log from '@/app/components/app/chat/log'
const Divider: FC<{ name: string }> = ({ name }) => {
const { t } = useTranslation()
return <div className='flex items-center my-2'>
<span className='text-xs text-gray-500 inline-flex items-center mr-2'>
<EditIconSolid className='mr-1' />{t('appLog.detail.annotationTip', { user: name })}
</span>
<div className='h-[1px] bg-gray-200 flex-1'></div>
</div>
}
const IconWrapper: FC<{ children: React.ReactNode | string }> = ({ children }) => {
return <div className={'rounded-lg h-6 w-6 flex items-center justify-center hover:bg-gray-100'}>
{children}
@ -64,6 +56,7 @@ export type IAnswerProps = {
onAnnotationAdded?: (annotationId: string, authorName: string, question: string, answer: string, index: number) => void
onAnnotationRemoved?: (index: number) => void
allToolIcons?: Record<string, string | Emoji>
isShowPromptLog?: boolean
}
// The component needs to maintain its own state to control whether to display input component
const Answer: FC<IAnswerProps> = ({
@ -87,12 +80,11 @@ const Answer: FC<IAnswerProps> = ({
onAnnotationAdded,
onAnnotationRemoved,
allToolIcons,
isShowPromptLog,
}) => {
const { id, content, more, feedback, adminFeedback, annotation, agent_thoughts } = item
const isAgentMode = !!agent_thoughts && agent_thoughts.length > 0
const hasAnnotation = !!annotation?.id
const [showEdit, setShowEdit] = useState(false)
const [loading, setLoading] = useState(false)
const hasAnnotation = useMemo(() => !!annotation, [annotation])
// const [annotation, setAnnotation] = useState<Annotation | undefined | null>(initAnnotation)
// const [inputValue, setInputValue] = useState<string>(initAnnotation?.content ?? '')
const [localAdminFeedback, setLocalAdminFeedback] = useState<Feedbacktype | undefined | null>(adminFeedback)
@ -144,19 +136,6 @@ const Answer: FC<IAnswerProps> = ({
)
}
const renderHasAnnotationBtn = () => {
return (
<div
className={cn(s.hasAnnotationBtn, 'relative box-border flex items-center justify-center h-7 w-7 p-0.5 rounded-lg bg-white cursor-pointer text-[#444CE7]')}
style={{ boxShadow: '0px 4px 6px -1px rgba(0, 0, 0, 0.1), 0px 2px 4px -2px rgba(0, 0, 0, 0.05)' }}
>
<div className='p-1 rounded-lg bg-[#EEF4FF] '>
<MessageFast className='w-4 h-4' />
</div>
</div>
)
}
/**
* Different scenarios have different operation items.
* @param isWebScene Whether it is web scene
@ -241,6 +220,50 @@ const Answer: FC<IAnswerProps> = ({
</div>
)
const [containerWidth, setContainerWidth] = useState(0)
const [contentWidth, setContentWidth] = useState(0)
const containerRef = useRef<HTMLDivElement>(null)
const contentRef = useRef<HTMLDivElement>(null)
const getContainerWidth = () => {
if (containerRef.current)
setContainerWidth(containerRef.current?.clientWidth + 24)
}
const getContentWidth = () => {
if (contentRef.current)
setContentWidth(contentRef.current?.clientWidth)
}
useEffect(() => {
getContainerWidth()
}, [])
useEffect(() => {
if (!isResponding)
getContentWidth()
}, [isResponding])
const operationWidth = useMemo(() => {
let width = 0
if (!item.isOpeningStatement)
width += 28
if (!item.isOpeningStatement && isShowPromptLog)
width += 102 + 8
if (!item.isOpeningStatement && isShowTextToSpeech)
width += 33
if (!item.isOpeningStatement && supportAnnotation)
width += 96 + 8
if (!feedbackDisabled && !item.feedbackDisabled)
width += 60 + 8
if (!feedbackDisabled && localAdminFeedback?.rating && !item.isOpeningStatement)
width += 60 + 8
if (!feedbackDisabled && feedback?.rating && !item.isOpeningStatement)
width += 28 + 8
return width
}, [item.isOpeningStatement, item.feedbackDisabled, isShowPromptLog, isShowTextToSpeech, supportAnnotation, feedbackDisabled, localAdminFeedback?.rating, feedback?.rating])
const positionRight = useMemo(() => operationWidth < containerWidth - contentWidth - 4, [operationWidth, containerWidth, contentWidth])
return (
// data-id for debug the item message is right
<div key={id} data-id={id}>
@ -256,9 +279,9 @@ const Answer: FC<IAnswerProps> = ({
</div>
)
}
<div className={cn(s.answerWrapWrap, 'chat-answer-container group')}>
<div className={`${s.answerWrap} ${showEdit ? 'w-full' : ''}`}>
<div className={`${s.answer} relative text-sm text-gray-900`}>
<div ref={containerRef} className={cn(s.answerWrapWrap, 'chat-answer-container')}>
<div className={cn(s.answerWrap, 'group')}>
<div ref={contentRef} className={`${s.answer} relative text-sm text-gray-900`}>
<div className={'ml-2 py-3 px-4 bg-gray-100 rounded-tr-2xl rounded-b-2xl'}>
{(isResponding && (isAgentMode ? (!content && (agent_thoughts || []).filter(item => !!item.thought || !!item.tool).length === 0) : !content))
? (
@ -295,7 +318,7 @@ const Answer: FC<IAnswerProps> = ({
</div>
{(hasAnnotation && !annotation?.logAnnotation) && (
<EditTitle className='mt-1' title={t('appAnnotation.editBy', {
author: annotation.authorName,
author: annotation?.authorName,
})} />
)}
{item.isOpeningStatement && item.suggestedQuestions && item.suggestedQuestions.filter(q => !!q && q.trim()).length > 0 && (
@ -319,25 +342,51 @@ const Answer: FC<IAnswerProps> = ({
)
}
</div>
<div className='absolute top-[-14px] right-[-14px] flex flex-row justify-end gap-1'>
{hasAnnotation && (
<div
className={cn(s.hasAnnotationBtn, 'absolute -top-3.5 -right-3.5 box-border flex items-center justify-center h-7 w-7 p-0.5 rounded-lg bg-white cursor-pointer text-[#444CE7]')}
style={{ boxShadow: '0px 4px 6px -1px rgba(0, 0, 0, 0.1), 0px 2px 4px -2px rgba(0, 0, 0, 0.05)' }}
>
<div className='p-1 rounded-lg bg-[#EEF4FF] '>
<MessageFast className='w-4 h-4' />
</div>
</div>
)}
<div
className={cn(
'absolute -top-3.5 flex justify-end gap-1',
positionRight ? '!top-[9px]' : '-right-3.5',
)}
style={positionRight ? { left: contentWidth + 8 } : {}}
>
{!item.isOpeningStatement && (
<CopyBtn
value={content}
className={cn(s.copyBtn, 'mr-1')}
/>
)}
{!item.isOpeningStatement && isShowTextToSpeech && (
<AudioBtn
value={content}
className={cn(s.playBtn, 'mr-1')}
/>
{((isShowPromptLog && !isResponding) || (!item.isOpeningStatement && isShowTextToSpeech)) && (
<div className='hidden group-hover:flex items-center w-max h-[28px] p-0.5 rounded-lg bg-white border-[0.5px] border-gray-100 shadow-md shrink-0'>
{isShowPromptLog && !isResponding && (
<Log logItem={item} />
)}
{!item.isOpeningStatement && isShowTextToSpeech && (
<>
<div className='mx-1 w-[1px] h-[14px] bg-gray-200'/>
<AudioBtn
value={content}
className={cn(s.playBtn)}
/>
</>
)}
</div>
)}
{(!item.isOpeningStatement && supportAnnotation) && (
<AnnotationCtrlBtn
appId={appId!}
messageId={id}
annotationId={annotation?.id || ''}
className={cn(s.annotationBtn, 'ml-1')}
className={cn(s.annotationBtn, 'ml-1 shrink-0')}
cached={hasAnnotation}
query={question}
answer={content}
@ -360,7 +409,6 @@ const Answer: FC<IAnswerProps> = ({
createdAt={annotation?.created_at}
onRemove={() => { }}
/>
{hasAnnotation && renderHasAnnotationBtn()}
{!feedbackDisabled && !item.feedbackDisabled && renderItemOperation(displayScene !== 'console')}
{/* Admin feedback is displayed only in the background. */}
@ -369,7 +417,6 @@ const Answer: FC<IAnswerProps> = ({
{!feedbackDisabled && renderFeedbackRating(feedback?.rating, !isHideFeedbackEdit, displayScene !== 'console')}
</div>
</div>
{more && <MoreInfo className='invisible group-hover:visible' more={more} isQuestion={false} />}
</div>
</div>

View File

@ -303,6 +303,7 @@ const Chat: FC<IChatProps> = ({
onAnnotationAdded={handleAnnotationAdded}
onAnnotationRemoved={handleAnnotationRemoved}
allToolIcons={allToolIcons}
isShowPromptLog={isShowPromptLog}
/>
}
return (
@ -319,140 +320,132 @@ const Chat: FC<IChatProps> = ({
)
})}
</div>
{
!isHideSendInput && (
<div className={cn(!feedbackDisabled && '!left-3.5 !right-3.5', 'absolute z-10 bottom-0 left-0 right-0')}>
{/* Thinking is sync and can not be stopped */}
{(isResponding && canStopResponding && ((!!chatList[chatList.length - 1]?.content) || (chatList[chatList.length - 1]?.agent_thoughts && chatList[chatList.length - 1].agent_thoughts!.length > 0))) && (
<div className='flex justify-center mb-4'>
<Button className='flex items-center space-x-1 bg-white' onClick={() => abortResponding?.()}>
{stopIcon}
<span className='text-xs text-gray-500 font-normal'>{t('appDebug.operation.stopResponding')}</span>
</Button>
{!isHideSendInput && (
<div className={cn(!feedbackDisabled && '!left-3.5 !right-3.5', 'absolute z-10 bottom-0 left-0 right-0')}>
{/* Thinking is sync and can not be stopped */}
{(isResponding && canStopResponding && ((!!chatList[chatList.length - 1]?.content) || (chatList[chatList.length - 1]?.agent_thoughts && chatList[chatList.length - 1].agent_thoughts!.length > 0))) && (
<div className='flex justify-center mb-4'>
<Button className='flex items-center space-x-1 bg-white' onClick={() => abortResponding?.()}>
{stopIcon}
<span className='text-xs text-gray-500 font-normal'>{t('appDebug.operation.stopResponding')}</span>
</Button>
</div>
)}
{isShowSuggestion && (
<div className='pt-2'>
<div className='flex items-center justify-center mb-2.5'>
<div className='grow h-[1px]'
style={{
background: 'linear-gradient(270deg, #F3F4F6 0%, rgba(243, 244, 246, 0) 100%)',
}}></div>
<div className='shrink-0 flex items-center px-3 space-x-1'>
{TryToAskIcon}
<span className='text-xs text-gray-500 font-medium'>{t('appDebug.feature.suggestedQuestionsAfterAnswer.tryToAsk')}</span>
</div>
<div className='grow h-[1px]'
style={{
background: 'linear-gradient(270deg, rgba(243, 244, 246, 0) 0%, #F3F4F6 100%)',
}}></div>
</div>
{/* has scrollbar would hide part of first item */}
<div ref={suggestionListRef} className={cn(!hasScrollbar && 'justify-center', 'flex overflow-x-auto pb-2')}>
{suggestionList?.map((item, index) => (
<div key={item} className='shrink-0 flex justify-center mr-2'>
<Button
key={index}
onClick={() => onQueryChange(item)}
>
<span className='text-primary-600 text-xs font-medium'>{item}</span>
</Button>
</div>
))}
</div>
</div>
)}
<div className={cn('p-[5.5px] max-h-[150px] bg-white border-[1.5px] border-gray-200 rounded-xl overflow-y-auto', isDragActive && 'border-primary-600')}>
{visionConfig?.enabled && (
<>
<div className='absolute bottom-2 left-2 flex items-center'>
<ChatImageUploader
settings={visionConfig}
onUpload={onUpload}
disabled={files.length >= visionConfig.number_limits}
/>
<div className='mx-1 w-[1px] h-4 bg-black/5' />
</div>
<div className='pl-[52px]'>
<ImageList
list={files}
onRemove={onRemove}
onReUpload={onReUpload}
onImageLinkLoadSuccess={onImageLinkLoadSuccess}
onImageLinkLoadError={onImageLinkLoadError}
/>
</div>
</>
)}
{
isShowSuggestion && (
<div className='pt-2'>
<div className='flex items-center justify-center mb-2.5'>
<div className='grow h-[1px]'
style={{
background: 'linear-gradient(270deg, #F3F4F6 0%, rgba(243, 244, 246, 0) 100%)',
}}></div>
<div className='shrink-0 flex items-center px-3 space-x-1'>
{TryToAskIcon}
<span className='text-xs text-gray-500 font-medium'>{t('appDebug.feature.suggestedQuestionsAfterAnswer.tryToAsk')}</span>
</div>
<div className='grow h-[1px]'
style={{
background: 'linear-gradient(270deg, rgba(243, 244, 246, 0) 0%, #F3F4F6 100%)',
}}></div>
</div>
{/* has scrollbar would hide part of first item */}
<div ref={suggestionListRef} className={cn(!hasScrollbar && 'justify-center', 'flex overflow-x-auto pb-2')}>
{suggestionList?.map((item, index) => (
<div key={item} className='shrink-0 flex justify-center mr-2'>
<Button
key={index}
onClick={() => onQueryChange(item)}
>
<span className='text-primary-600 text-xs font-medium'>{item}</span>
</Button>
</div>
))}
</div>
</div>)
}
<div className={cn('p-[5.5px] max-h-[150px] bg-white border-[1.5px] border-gray-200 rounded-xl overflow-y-auto', isDragActive && 'border-primary-600')}>
<Textarea
className={`
block w-full px-2 pr-[118px] py-[7px] leading-5 max-h-none text-sm text-gray-700 outline-none appearance-none resize-none
${visionConfig?.enabled && 'pl-12'}
`}
value={query}
onChange={handleContentChange}
onKeyUp={handleKeyUp}
onKeyDown={handleKeyDown}
onPaste={onPaste}
onDragEnter={onDragEnter}
onDragLeave={onDragLeave}
onDragOver={onDragOver}
onDrop={onDrop}
autoSize
/>
<div className="absolute bottom-2 right-2 flex items-center h-8">
<div className={`${s.count} mr-4 h-5 leading-5 text-sm bg-gray-50 text-gray-500`}>{query.trim().length}</div>
{
visionConfig?.enabled && (
<>
<div className='absolute bottom-2 left-2 flex items-center'>
<ChatImageUploader
settings={visionConfig}
onUpload={onUpload}
disabled={files.length >= visionConfig.number_limits}
/>
<div className='mx-1 w-[1px] h-4 bg-black/5' />
query
? (
<div className='flex justify-center items-center w-8 h-8 cursor-pointer hover:bg-gray-100 rounded-lg' onClick={() => onQueryChange('')}>
<XCircle className='w-4 h-4 text-[#98A2B3]' />
</div>
<div className='pl-[52px]'>
<ImageList
list={files}
onRemove={onRemove}
onReUpload={onReUpload}
onImageLinkLoadSuccess={onImageLinkLoadSuccess}
onImageLinkLoadError={onImageLinkLoadError}
/>
</div>
</>
)
}
<Textarea
className={`
block w-full px-2 pr-[118px] py-[7px] leading-5 max-h-none text-sm text-gray-700 outline-none appearance-none resize-none
${visionConfig?.enabled && 'pl-12'}
`}
value={query}
onChange={handleContentChange}
onKeyUp={handleKeyUp}
onKeyDown={handleKeyDown}
onPaste={onPaste}
onDragEnter={onDragEnter}
onDragLeave={onDragLeave}
onDragOver={onDragOver}
onDrop={onDrop}
autoSize
/>
<div className="absolute bottom-2 right-2 flex items-center h-8">
<div className={`${s.count} mr-4 h-5 leading-5 text-sm bg-gray-50 text-gray-500`}>{query.trim().length}</div>
{
query
)
: isShowSpeechToText
? (
<div className='flex justify-center items-center w-8 h-8 cursor-pointer hover:bg-gray-100 rounded-lg' onClick={() => onQueryChange('')}>
<XCircle className='w-4 h-4 text-[#98A2B3]' />
<div
className='group flex justify-center items-center w-8 h-8 hover:bg-primary-50 rounded-lg cursor-pointer'
onClick={handleVoiceInputShow}
>
<Microphone01 className='block w-4 h-4 text-gray-500 group-hover:hidden' />
<Microphone01Solid className='hidden w-4 h-4 text-primary-600 group-hover:block' />
</div>
)
: isShowSpeechToText
? (
<div
className='group flex justify-center items-center w-8 h-8 hover:bg-primary-50 rounded-lg cursor-pointer'
onClick={handleVoiceInputShow}
>
<Microphone01 className='block w-4 h-4 text-gray-500 group-hover:hidden' />
<Microphone01Solid className='hidden w-4 h-4 text-primary-600 group-hover:block' />
</div>
)
: null
}
<div className='mx-2 w-[1px] h-4 bg-black opacity-5' />
{isMobile
? sendBtn
: (
<TooltipPlus
popupContent={
<div>
<div>{t('common.operation.send')} Enter</div>
<div>{t('common.operation.lineBreak')} Shift Enter</div>
</div>
}
>
{sendBtn}
</TooltipPlus>
)}
</div>
{
voiceInputShow && (
<VoiceInput
onCancel={() => setVoiceInputShow(false)}
onConverted={text => onQueryChange(text)}
/>
)
: null
}
<div className='mx-2 w-[1px] h-4 bg-black opacity-5' />
{isMobile
? sendBtn
: (
<TooltipPlus
popupContent={
<div>
<div>{t('common.operation.send')} Enter</div>
<div>{t('common.operation.lineBreak')} Shift Enter</div>
</div>
}
>
{sendBtn}
</TooltipPlus>
)}
</div>
{voiceInputShow && (
<VoiceInput
onCancel={() => setVoiceInputShow(false)}
onConverted={text => onQueryChange(text)}
/>
)}
</div>
)
}
</div>
)}
</div>
)
}

View File

@ -1,74 +1,35 @@
import type { Dispatch, FC, ReactNode, RefObject, SetStateAction } from 'react'
import { useEffect, useState } from 'react'
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { uniqueId } from 'lodash-es'
import { File02 } from '@/app/components/base/icons/src/vender/line/files'
import PromptLogModal from '@/app/components/base/prompt-log-modal'
import Tooltip from '@/app/components/base/tooltip'
export type LogData = {
role: string
text: string
}
import type { IChatItem } from '@/app/components/app/chat/type'
import { useStore as useAppStore } from '@/app/components/app/store'
type LogProps = {
containerRef: RefObject<HTMLElement>
log: LogData[]
children?: (v: Dispatch<SetStateAction<boolean>>) => ReactNode
logItem: IChatItem
}
const Log: FC<LogProps> = ({
containerRef,
children,
log,
logItem,
}) => {
const { t } = useTranslation()
const [showModal, setShowModal] = useState(false)
const [width, setWidth] = useState(0)
const adjustModalWidth = () => {
if (containerRef.current)
setWidth(document.body.clientWidth - (containerRef.current?.clientWidth + 56 + 16))
}
useEffect(() => {
adjustModalWidth()
}, [])
const { setCurrentLogItem, setShowPromptLogModal, setShowMessageLogModal } = useAppStore()
const { workflow_run_id: runID } = logItem
return (
<>
{
children
? children(setShowModal)
: (
<Tooltip selector={`prompt-log-modal-trigger-${uniqueId()}`} content={t('common.operation.log') || ''}>
<div className={`
hidden absolute -left-[14px] -top-[14px] group-hover:block w-7 h-7
p-0.5 rounded-lg border-[0.5px] border-gray-100 bg-white shadow-md cursor-pointer
`}>
<div
className='flex items-center justify-center rounded-md w-full h-full hover:bg-gray-100'
onClick={(e) => {
e.stopPropagation()
setShowModal(true)
}
}
>
<File02 className='w-4 h-4 text-gray-500' />
</div>
</div>
</Tooltip>
)
}
{
showModal && (
<PromptLogModal
width={width}
log={log}
onCancel={() => setShowModal(false)}
/>
)
}
</>
<div
className='shrink-0 p-1 flex items-center justify-center rounded-[6px] font-medium text-gray-500 hover:bg-gray-50 cursor-pointer hover:text-gray-700'
onClick={(e) => {
e.stopPropagation()
e.nativeEvent.stopImmediatePropagation()
setCurrentLogItem(logItem)
if (runID)
setShowMessageLogModal(true)
else
setShowPromptLogModal(true)
}}
>
<File02 className='mr-1 w-4 h-4' />
<div className='text-xs leading-4'>{runID ? t('appLog.viewLog') : t('appLog.promptLog')}</div>
</div>
)
}

View File

@ -4,7 +4,6 @@ import React, { useRef } from 'react'
import { useContext } from 'use-context-selector'
import s from '../style.module.css'
import type { IChatItem } from '../type'
import Log from '../log'
import MoreInfo from '../more-info'
import AppContext from '@/context/app-context'
import { Markdown } from '@/app/components/base/markdown'
@ -16,7 +15,7 @@ type IQuestionProps = Pick<IChatItem, 'id' | 'content' | 'more' | 'useCurrentUse
isResponding?: boolean
}
const Question: FC<IQuestionProps> = ({ id, content, more, useCurrentUserAvatar, isShowPromptLog, item, isResponding }) => {
const Question: FC<IQuestionProps> = ({ id, content, more, useCurrentUserAvatar, isShowPromptLog, item }) => {
const { userProfile } = useContext(AppContext)
const userName = userProfile?.name
const ref = useRef(null)
@ -27,11 +26,6 @@ const Question: FC<IQuestionProps> = ({ id, content, more, useCurrentUserAvatar,
<div className={s.questionWrapWrap}>
<div className={`${s.question} group relative text-sm text-gray-900`}>
{
isShowPromptLog && !isResponding && (
<Log log={item.log!} containerRef={ref} />
)
}
<div
className={'mr-2 py-3 px-4 bg-blue-500 rounded-tl-2xl rounded-b-2xl'}
>

View File

@ -79,9 +79,10 @@ export type IChatItem = {
useCurrentUserAvatar?: boolean
isOpeningStatement?: boolean
suggestedQuestions?: string[]
log?: { role: string; text: string }[]
log?: { role: string; text: string; files?: VisionFile[] }[]
agent_thoughts?: ThoughtItem[]
message_files?: VisionFile[]
workflow_run_id?: string
}
export type MessageEnd = {

View File

@ -6,15 +6,17 @@ import s from './style.module.css'
export type IVarHighlightProps = {
name: string
className?: string
}
const VarHighlight: FC<IVarHighlightProps> = ({
name,
className = '',
}) => {
return (
<div
key={name}
className={`${s.item} flex mb-2 items-center justify-center rounded-md px-1 h-5 text-xs font-medium text-primary-600`}
className={`${s.item} ${className} flex mb-2 items-center justify-center rounded-md px-1 h-5 text-xs font-medium text-primary-600`}
>
<span className='opacity-60'>{'{{'}</span>
<span>{name}</span>
@ -23,8 +25,8 @@ const VarHighlight: FC<IVarHighlightProps> = ({
)
}
export const varHighlightHTML = ({ name }: IVarHighlightProps) => {
const html = `<div class="${s.item} inline-flex mb-2 items-center justify-center px-1 rounded-md h-5 text-xs font-medium text-primary-600">
export const varHighlightHTML = ({ name, className = '' }: IVarHighlightProps) => {
const html = `<div class="${s.item} ${className} inline-flex mb-2 items-center justify-center px-1 rounded-md h-5 text-xs font-medium text-primary-600">
<span class='opacity-60'>{{</span>
<span>${name}</span>
<span class='opacity-60'>}}</span>

View File

@ -92,7 +92,7 @@ const AdvancedPromptInput: FC<Props> = ({
},
})
}
const isChatApp = mode === AppType.chat
const isChatApp = mode !== AppType.completion
const [isCopied, setIsCopied] = React.useState(false)
const promptVariablesObj = (() => {
@ -216,10 +216,13 @@ const AdvancedPromptInput: FC<Props> = ({
onAddContext: showSelectDataSet,
}}
variableBlock={{
show: true,
variables: modelConfig.configs.prompt_variables.filter(item => item.type !== 'api').map(item => ({
name: item.name,
value: item.key,
})),
}}
externalToolBlock={{
externalTools: modelConfig.configs.prompt_variables.filter(item => item.type === 'api').map(item => ({
name: item.name,
variableName: item.key,

View File

@ -70,7 +70,7 @@ const Prompt: FC<IPromptProps> = ({
}])
return
}
const lastMessageType = currentAdvancedPromptList[currentAdvancedPromptList.length - 1].role
const lastMessageType = currentAdvancedPromptList[currentAdvancedPromptList.length - 1]?.role
const appendMessage = {
role: lastMessageType === PromptRole.user ? PromptRole.assistant : PromptRole.user,
text: '',

View File

@ -11,6 +11,7 @@ type Props = {
onHeightChange: (height: number) => void
children: JSX.Element
footer?: JSX.Element
hideResize?: boolean
}
const PromptEditorHeightResizeWrap: FC<Props> = ({
@ -20,6 +21,7 @@ const PromptEditorHeightResizeWrap: FC<Props> = ({
onHeightChange,
children,
footer,
hideResize,
}) => {
const [clientY, setClientY] = useState(0)
const [isResizing, setIsResizing] = useState(false)
@ -80,11 +82,13 @@ const PromptEditorHeightResizeWrap: FC<Props> = ({
</div>
{/* resize handler */}
{footer}
<div
className='absolute bottom-0 left-0 w-full flex justify-center h-2 cursor-row-resize'
onMouseDown={handleStartResize}>
<div className='w-5 h-[3px] rounded-sm bg-gray-300'></div>
</div>
{!hideResize && (
<div
className='absolute bottom-0 left-0 w-full flex justify-center h-2 cursor-row-resize'
onMouseDown={handleStartResize}>
<div className='w-5 h-[3px] rounded-sm bg-gray-300'></div>
</div>
)}
</div>
)
}

View File

@ -9,7 +9,7 @@ import { useContext } from 'use-context-selector'
import ConfirmAddVar from './confirm-add-var'
import s from './style.module.css'
import PromptEditorHeightResizeWrap from './prompt-editor-height-resize-wrap'
import { PromptMode, type PromptVariable } from '@/models/debug'
import { type PromptVariable } from '@/models/debug'
import Tooltip from '@/app/components/base/tooltip'
import { AppType } from '@/types/app'
import { getNewVar, getVars } from '@/utils/var'
@ -22,7 +22,6 @@ import ConfigContext from '@/context/debug-configuration'
import { useModalContext } from '@/context/modal-context'
import type { ExternalDataTool } from '@/models/common'
import { useToastContext } from '@/app/components/base/toast'
import { ArrowNarrowRight } from '@/app/components/base/icons/src/vender/line/arrows'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import { ADD_EXTERNAL_DATA_TOOL } from '@/app/components/app/configuration/config-var'
import { INSERT_VARIABLE_VALUE_BLOCK_COMMAND } from '@/app/components/base/prompt-editor/plugins/variable-block'
@ -54,9 +53,7 @@ const Prompt: FC<ISimplePromptInput> = ({
hasSetBlockStatus,
showSelectDataSet,
externalDataToolsConfig,
isAdvancedMode,
isAgent,
setPromptMode,
} = useContext(ConfigContext)
const { notify } = useToastContext()
const { setShowExternalDataToolModal } = useModalContext()
@ -123,7 +120,7 @@ const Prompt: FC<ISimplePromptInput> = ({
})
setModelConfig(newModelConfig)
setPrevPromptConfig(modelConfig.configs)
if (mode === AppType.chat)
if (mode !== AppType.completion)
setIntroduction(res.opening_statement)
showAutomaticFalse()
eventEmitter?.emit({
@ -139,10 +136,7 @@ const Prompt: FC<ISimplePromptInput> = ({
<div className='rounded-xl bg-[#EEF4FF]'>
<div className="flex justify-between items-center h-11 px-3">
<div className="flex items-center space-x-1">
<svg width="14" height="13" viewBox="0 0 14 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M3.00001 0.100098C3.21218 0.100098 3.41566 0.184383 3.56569 0.334412C3.71572 0.484441 3.80001 0.687924 3.80001 0.900098V1.7001H4.60001C4.81218 1.7001 5.01566 1.78438 5.16569 1.93441C5.31572 2.08444 5.40001 2.28792 5.40001 2.5001C5.40001 2.71227 5.31572 2.91575 5.16569 3.06578C5.01566 3.21581 4.81218 3.3001 4.60001 3.3001H3.80001V4.1001C3.80001 4.31227 3.71572 4.51575 3.56569 4.66578C3.41566 4.81581 3.21218 4.9001 3.00001 4.9001C2.78783 4.9001 2.58435 4.81581 2.43432 4.66578C2.28429 4.51575 2.20001 4.31227 2.20001 4.1001V3.3001H1.40001C1.18783 3.3001 0.98435 3.21581 0.834321 3.06578C0.684292 2.91575 0.600006 2.71227 0.600006 2.5001C0.600006 2.28792 0.684292 2.08444 0.834321 1.93441C0.98435 1.78438 1.18783 1.7001 1.40001 1.7001H2.20001V0.900098C2.20001 0.687924 2.28429 0.484441 2.43432 0.334412C2.58435 0.184383 2.78783 0.100098 3.00001 0.100098ZM3.00001 8.1001C3.21218 8.1001 3.41566 8.18438 3.56569 8.33441C3.71572 8.48444 3.80001 8.68792 3.80001 8.9001V9.7001H4.60001C4.81218 9.7001 5.01566 9.78438 5.16569 9.93441C5.31572 10.0844 5.40001 10.2879 5.40001 10.5001C5.40001 10.7123 5.31572 10.9158 5.16569 11.0658C5.01566 11.2158 4.81218 11.3001 4.60001 11.3001H3.80001V12.1001C3.80001 12.3123 3.71572 12.5158 3.56569 12.6658C3.41566 12.8158 3.21218 12.9001 3.00001 12.9001C2.78783 12.9001 2.58435 12.8158 2.43432 12.6658C2.28429 12.5158 2.20001 12.3123 2.20001 12.1001V11.3001H1.40001C1.18783 11.3001 0.98435 11.2158 0.834321 11.0658C0.684292 10.9158 0.600006 10.7123 0.600006 10.5001C0.600006 10.2879 0.684292 10.0844 0.834321 9.93441C0.98435 9.78438 1.18783 9.7001 1.40001 9.7001H2.20001V8.9001C2.20001 8.68792 2.28429 8.48444 2.43432 8.33441C2.58435 8.18438 2.78783 8.1001 3.00001 8.1001ZM8.60001 0.100098C8.77656 0.100041 8.94817 0.158388 9.0881 0.266047C9.22802 0.373706 9.32841 0.52463 9.37361 0.695298L10.3168 4.2601L13 5.8073C13.1216 5.87751 13.2226 5.9785 13.2928 6.10011C13.363 6.22173 13.4 6.35967 13.4 6.5001C13.4 6.64052 13.363 6.77847 13.2928 6.90008C13.2226 7.02169 13.1216 7.12268 13 7.1929L10.3168 8.7409L9.37281 12.3049C9.32753 12.4754 9.22716 12.6262 9.08732 12.7337C8.94748 12.8413 8.77602 12.8996 8.59961 12.8996C8.42319 12.8996 8.25173 12.8413 8.11189 12.7337C7.97205 12.6262 7.87169 12.4754 7.82641 12.3049L6.88321 8.7401L4.20001 7.1929C4.0784 7.12268 3.97742 7.02169 3.90721 6.90008C3.837 6.77847 3.80004 6.64052 3.80004 6.5001C3.80004 6.35967 3.837 6.22173 3.90721 6.10011C3.97742 5.9785 4.0784 5.87751 4.20001 5.8073L6.88321 4.2593L7.82721 0.695298C7.87237 0.524762 7.97263 0.373937 8.1124 0.266291C8.25216 0.158646 8.42359 0.100217 8.60001 0.100098Z" fill="#5850EC" />
</svg>
<div className='h2'>{mode === AppType.chat ? t('appDebug.chatSubTitle') : t('appDebug.completionSubTitle')}</div>
<div className='h2'>{mode !== AppType.completion ? t('appDebug.chatSubTitle') : t('appDebug.completionSubTitle')}</div>
{!readonly && (
<Tooltip
htmlContent={<div className='w-[180px]'>
@ -154,29 +148,25 @@ const Prompt: FC<ISimplePromptInput> = ({
)}
</div>
<div className='flex items-center'>
<AutomaticBtn onClick={showAutomaticTrue} />
{!isAgent && !isAdvancedMode && (
<>
<div className='mx-1 w-px h-3.5 bg-black/5'></div>
<div
className='flex items-center px-2 space-x-1 text-xs font-semibold text-[#444CE7] cursor-pointer'
onClick={() => setPromptMode(PromptMode.advanced)}
>
<div>{t('appDebug.promptMode.advanced')}</div>
<ArrowNarrowRight className='w-3 h-3'></ArrowNarrowRight>
</div>
</>
{!isAgent && (
<AutomaticBtn onClick={showAutomaticTrue} />
)}
</div>
</div>
<PromptEditorHeightResizeWrap
className='px-4 py-2 min-h-[228px] bg-white rounded-xl text-sm text-gray-700'
className='px-4 pt-2 min-h-[228px] bg-white rounded-t-xl text-sm text-gray-700'
height={editorHeight}
minHeight={minHeight}
onHeightChange={setEditorHeight}
footer={(
<div className='pl-4 pb-2 flex bg-white rounded-b-xl'>
<div className="h-[18px] leading-[18px] px-1 rounded-md bg-gray-100 text-xs text-gray-500">{promptTemplate.length}</div>
</div>
)}
>
<PromptEditor
className='min-h-[210px]'
compact
value={promptTemplate}
contextBlock={{
show: false,
@ -189,10 +179,14 @@ const Prompt: FC<ISimplePromptInput> = ({
onAddContext: showSelectDataSet,
}}
variableBlock={{
show: true,
variables: modelConfig.configs.prompt_variables.filter(item => item.type !== 'api').map(item => ({
name: item.name,
value: item.key,
})),
}}
externalToolBlock={{
show: true,
externalTools: modelConfig.configs.prompt_variables.filter(item => item.type === 'api').map(item => ({
name: item.name,
variableName: item.key,

View File

@ -0,0 +1,21 @@
'use client'
import type { FC } from 'react'
import React from 'react'
type Props = {
title: string
children: JSX.Element
}
const Field: FC<Props> = ({
title,
children,
}) => {
return (
<div>
<div className='leading-8 text-[13px] font-medium text-gray-700'>{title}</div>
<div>{children}</div>
</div>
)
}
export default React.memo(Field)

View File

@ -1,30 +1,37 @@
'use client'
import type { FC } from 'react'
import React, { useEffect, useState } from 'react'
import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import ModalFoot from '../modal-foot'
import type { Options } from '../config-select'
import ConfigSelect from '../config-select'
import ConfigString from '../config-string'
import SelectTypeItem from '../select-type-item'
import s from './style.module.css'
import Field from './field'
import Toast from '@/app/components/base/toast'
import type { PromptVariable } from '@/models/debug'
import { getNewVar } from '@/utils/var'
import { checkKeys, getNewVarInWorkflow } from '@/utils/var'
import ConfigContext from '@/context/debug-configuration'
import type { InputVar, MoreInfo } from '@/app/components/workflow/types'
import Modal from '@/app/components/base/modal'
import Switch from '@/app/components/base/switch'
import { ChangeType, InputVarType } from '@/app/components/workflow/types'
const TEXT_MAX_LENGTH = 256
const PARAGRAPH_MAX_LENGTH = 1024
export type IConfigModalProps = {
payload: PromptVariable
type?: string
isCreate?: boolean
payload?: InputVar
isShow: boolean
varKeys?: string[]
onClose: () => void
onConfirm: (newValue: { type: string; value: any }) => void
onConfirm: (newValue: InputVar, moreInfo?: MoreInfo) => void
}
const inputClassName = 'w-full px-3 text-sm leading-9 text-gray-900 border-0 rounded-lg grow h-9 bg-gray-100 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200'
const ConfigModal: FC<IConfigModalProps> = ({
isCreate,
payload,
isShow,
onClose,
@ -32,45 +39,80 @@ const ConfigModal: FC<IConfigModalProps> = ({
}) => {
const { modelConfig } = useContext(ConfigContext)
const { t } = useTranslation()
const { type, name, key, options, max_length } = payload || getNewVar('')
const [tempPayload, setTempPayload] = useState<InputVar>(payload || getNewVarInWorkflow('') as any)
const { type, label, variable, options, max_length } = tempPayload
const [tempType, setTempType] = useState(type)
useEffect(() => {
setTempType(type)
}, [type])
const handleTypeChange = (type: string) => {
return () => {
setTempType(type)
const isStringInput = type === InputVarType.textInput || type === InputVarType.paragraph
const handlePayloadChange = useCallback((key: string) => {
return (value: any) => {
if (key === 'variable') {
const { isValid, errorKey, errorMessageKey } = checkKeys([value], true)
if (!isValid) {
Toast.notify({
type: 'error',
message: t(`appDebug.varKeyError.${errorMessageKey}`, { key: errorKey }),
})
return
}
}
setTempPayload((prev) => {
const newPayload = {
...prev,
[key]: value,
}
return newPayload
})
}
}
}, [t])
const isStringInput = tempType === 'string' || tempType === 'paragraph'
const title = isStringInput ? t('appDebug.variableConig.maxLength') : t('appDebug.variableConig.options')
const handleVarKeyBlur = useCallback((e: any) => {
if (tempPayload.label)
return
// string type
const [tempMaxLength, setTempMaxValue] = useState(max_length)
useEffect(() => {
setTempMaxValue(max_length)
}, [max_length])
// select type
const [tempOptions, setTempOptions] = useState<Options>(options || [])
useEffect(() => {
setTempOptions(options || [])
}, [options])
setTempPayload((prev) => {
return {
...prev,
label: e.target.value,
}
})
}, [tempPayload])
const handleConfirm = () => {
if (isStringInput) {
onConfirm({ type: tempType, value: tempMaxLength })
const moreInfo = tempPayload.variable === payload?.variable
? undefined
: {
type: ChangeType.changeVarName,
payload: { beforeKey: payload?.variable || '', afterKey: tempPayload.variable },
}
if (!tempPayload.variable) {
Toast.notify({ type: 'error', message: t('appDebug.variableConig.errorMsg.varNameRequired') })
return
}
// TODO: check if key already exists. should the consider the edit case
// if (varKeys.map(key => key?.trim()).includes(tempPayload.variable.trim())) {
// Toast.notify({
// type: 'error',
// message: t('appDebug.varKeyError.keyAlreadyExists', { key: tempPayload.variable }),
// })
// return
// }
if (!tempPayload.label) {
Toast.notify({ type: 'error', message: t('appDebug.variableConig.errorMsg.labelNameRequired') })
return
}
if (isStringInput || type === InputVarType.number) {
onConfirm(tempPayload, moreInfo)
}
else {
if (tempOptions.length === 0) {
Toast.notify({ type: 'error', message: 'At least one option requied' })
if (options?.length === 0) {
Toast.notify({ type: 'error', message: t('appDebug.variableConig.errorMsg.atLeastOneOption') })
return
}
const obj: Record<string, boolean> = {}
let hasRepeatedItem = false
tempOptions.forEach((o) => {
options?.forEach((o) => {
if (obj[o]) {
hasRepeatedItem = true
return
@ -78,42 +120,68 @@ const ConfigModal: FC<IConfigModalProps> = ({
obj[o] = true
})
if (hasRepeatedItem) {
Toast.notify({ type: 'error', message: 'Has repeat items' })
Toast.notify({ type: 'error', message: t('appDebug.variableConig.errorMsg.optionRepeat') })
return
}
onConfirm({ type: tempType, value: tempOptions })
onConfirm(tempPayload, moreInfo)
}
}
return (
<Modal
title={t('appDebug.variableConig.modalTitle')}
title={t(`appDebug.variableConig.${isCreate ? 'addModalTitle' : 'editModalTitle'}`)}
isShow={isShow}
onClose={onClose}
wrapperClassName='!z-[100]'
>
<div className='mb-8'>
<div className='mt-2 mb-8 text-sm text-gray-500'>{t('appDebug.variableConig.description', { varName: `{{${name || key}}}` })}</div>
<div className='mb-2'>
<div className={s.title}>{t('appDebug.variableConig.fieldType')}</div>
<div className='flex space-x-2'>
<SelectTypeItem type='string' selected={tempType === 'string'} onClick={handleTypeChange('string')} />
<SelectTypeItem type='paragraph' selected={tempType === 'paragraph'} onClick={handleTypeChange('paragraph')} />
<SelectTypeItem type='select' selected={tempType === 'select'} onClick={handleTypeChange('select')} />
</div>
</div>
<div className='space-y-2'>
{tempType !== 'paragraph' && (
<div className='mt-6'>
<div className={s.title}>{title}</div>
{isStringInput
? (
<ConfigString modelId={modelConfig.model_id} value={tempMaxLength} onChange={setTempMaxValue} />
)
: (
<ConfigSelect options={tempOptions} onChange={setTempOptions} />
)}
</div>
)}
<Field title={t('appDebug.variableConig.fieldType')}>
<div className='flex space-x-2'>
<SelectTypeItem type={InputVarType.textInput} selected={type === InputVarType.textInput} onClick={() => handlePayloadChange('type')(InputVarType.textInput)} />
<SelectTypeItem type={InputVarType.paragraph} selected={type === InputVarType.paragraph} onClick={() => handlePayloadChange('type')(InputVarType.paragraph)} />
<SelectTypeItem type={InputVarType.select} selected={type === InputVarType.select} onClick={() => handlePayloadChange('type')(InputVarType.select)} />
<SelectTypeItem type={InputVarType.number} selected={type === InputVarType.number} onClick={() => handlePayloadChange('type')(InputVarType.number)} />
</div>
</Field>
<Field title={t('appDebug.variableConig.varName')}>
<input
type='text'
className={inputClassName}
value={variable}
onChange={e => handlePayloadChange('variable')(e.target.value)}
onBlur={handleVarKeyBlur}
placeholder={t('appDebug.variableConig.inputPlaceholder')!}
/>
</Field>
<Field title={t('appDebug.variableConig.labelName')}>
<input
type='text'
className={inputClassName}
value={label as string}
onChange={e => handlePayloadChange('label')(e.target.value)}
placeholder={t('appDebug.variableConig.inputPlaceholder')!}
/>
</Field>
{isStringInput && (
<Field title={t('appDebug.variableConig.maxLength')}>
<ConfigString maxLength={type === InputVarType.textInput ? TEXT_MAX_LENGTH : PARAGRAPH_MAX_LENGTH} modelId={modelConfig.model_id} value={max_length} onChange={handlePayloadChange('max_length')} />
</Field>
)}
{type === InputVarType.select && (
<Field title={t('appDebug.variableConig.options')}>
<ConfigSelect options={options || []} onChange={handlePayloadChange('options')} />
</Field>
)}
<Field title={t('appDebug.variableConig.required')}>
<Switch defaultValue={tempPayload.required} onChange={handlePayloadChange('required')} />
</Field>
</div>
</div>
<ModalFoot
onConfirm={handleConfirm}

View File

@ -1,8 +0,0 @@
.title {
margin-bottom: 8px;
font-size: 13px;
line-height: 18px;
font-weight: 500;
color: #101828;
text-transform: capitalize;
}

View File

@ -4,39 +4,39 @@ import React, { useEffect } from 'react'
export type IConfigStringProps = {
value: number | undefined
maxLength: number
modelId: string
onChange: (value: number | undefined) => void
}
const MAX_LENGTH = 256
const ConfigString: FC<IConfigStringProps> = ({
value,
onChange,
maxLength,
}) => {
useEffect(() => {
if (value && value > MAX_LENGTH)
onChange(MAX_LENGTH)
}, [value, MAX_LENGTH])
if (value && value > maxLength)
onChange(maxLength)
}, [value, maxLength, onChange])
return (
<div>
<input
type="number"
max={MAX_LENGTH}
max={maxLength}
min={1}
value={value || ''}
onChange={(e) => {
let value = parseInt(e.target.value, 10)
if (value > MAX_LENGTH)
value = MAX_LENGTH
if (value > maxLength)
value = maxLength
else if (value < 1)
value = 1
onChange(value)
}}
className="w-full px-3 text-sm leading-9 text-gray-900 border-0 rounded-lg grow h-9 bg-gray-50 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200"
className="w-full px-3 text-sm leading-9 text-gray-900 border-0 rounded-lg grow h-9 bg-gray-100 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200"
/>
</div>
)

View File

@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks'
import type { Timeout } from 'ahooks/lib/useRequest/src/types'
import { useContext } from 'use-context-selector'
import produce from 'immer'
import Panel from '../base/feature-panel'
import EditModal from './config-modal'
import IconTypeIcon from './input-type-icon'
@ -25,6 +26,8 @@ import { AppType } from '@/types/app'
import type { ExternalDataTool } from '@/models/common'
import { useModalContext } from '@/context/modal-context'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import type { InputVar } from '@/app/components/workflow/types'
import { InputVarType } from '@/app/components/workflow/types'
export const ADD_EXTERNAL_DATA_TOOL = 'ADD_EXTERNAL_DATA_TOOL'
@ -51,7 +54,6 @@ const ConfigVar: FC<IConfigVarProps> = ({ promptVariables, readonly, onPromptVar
const {
mode,
dataSets,
externalDataToolsConfig,
} = useContext(ConfigContext)
const { eventEmitter } = useEventEmitterContextContext()
@ -69,22 +71,35 @@ const ConfigVar: FC<IConfigVarProps> = ({ promptVariables, readonly, onPromptVar
})
onPromptVariablesChange?.(newPromptVariables)
}
const [currIndex, setCurrIndex] = useState<number>(-1)
const currItem = currIndex !== -1 ? promptVariables[currIndex] : null
const currItemToEdit: InputVar | null = (() => {
if (!currItem)
return null
const batchUpdatePromptVariable = (key: string, updateKeys: string[], newValues: any[], isParagraph?: boolean) => {
const newPromptVariables = promptVariables.map((item) => {
if (item.key === key) {
const newItem: any = { ...item }
updateKeys.forEach((updateKey, i) => {
newItem[updateKey] = newValues[i]
})
if (isParagraph) {
delete newItem.max_length
delete newItem.options
}
return newItem
return {
...currItem,
label: currItem.name,
variable: currItem.key,
type: currItem.type === 'string' ? InputVarType.textInput : currItem.type,
} as InputVar
})()
const updatePromptVariableItem = (payload: InputVar) => {
console.log(payload)
const newPromptVariables = produce(promptVariables, (draft) => {
const { variable, label, type, ...rest } = payload
draft[currIndex] = {
...rest,
type: type === InputVarType.textInput ? 'string' : type,
key: variable,
name: label,
}
return item
if (payload.type === InputVarType.textInput)
draft[currIndex].max_length = draft[currIndex].max_length || DEFAULT_VALUE_MAX_LEN
if (payload.type !== InputVarType.select)
delete draft[currIndex].options
})
onPromptVariablesChange?.(newPromptVariables)
@ -111,7 +126,7 @@ const ConfigVar: FC<IConfigVarProps> = ({ promptVariables, readonly, onPromptVar
})
conflictTimer = setTimeout(() => {
const isKeyExists = promptVariables.some(item => item.key.trim() === newKey.trim())
const isKeyExists = promptVariables.some(item => item.key?.trim() === newKey.trim())
if (isKeyExists) {
Toast.notify({
type: 'error',
@ -240,13 +255,13 @@ const ConfigVar: FC<IConfigVarProps> = ({ promptVariables, readonly, onPromptVar
didRemoveVar(index)
}
const [currKey, setCurrKey] = useState<string | null>(null)
const currItem = currKey ? promptVariables.find(item => item.key === currKey) : null
// const [currKey, setCurrKey] = useState<string | null>(null)
const [isShowEditModal, { setTrue: showEditModal, setFalse: hideEditModal }] = useBoolean(false)
const handleConfig = ({ key, type, index, name, config, icon, icon_background }: ExternalDataToolParams) => {
setCurrKey(key)
if (type !== 'string' && type !== 'paragraph' && type !== 'select') {
// setCurrKey(key)
setCurrIndex(index)
if (type !== 'string' && type !== 'paragraph' && type !== 'select' && type !== 'number') {
handleOpenExternalDataToolModal({ key, type, index, name, config, icon, icon_background }, promptVariables)
return
}
@ -358,17 +373,14 @@ const ConfigVar: FC<IConfigVarProps> = ({ promptVariables, readonly, onPromptVar
{isShowEditModal && (
<EditModal
payload={currItem as PromptVariable}
payload={currItemToEdit!}
isShow={isShowEditModal}
onClose={hideEditModal}
onConfirm={({ type, value }) => {
if (type === 'string')
batchUpdatePromptVariable(currKey as string, ['type', 'max_length'], [type, value || DEFAULT_VALUE_MAX_LEN])
else
batchUpdatePromptVariable(currKey as string, ['type', 'options'], [type, value || []], type === 'paragraph')
onConfirm={(item) => {
updatePromptVariableItem(item)
hideEditModal()
}}
varKeys={promptVariables.map(v => v.key)}
/>
)}

View File

@ -1,9 +1,9 @@
'use client'
import React from 'react'
import type { FC } from 'react'
import { Paragraph, TypeSquare } from '@/app/components/base/icons/src/vender/solid/editor'
import { CheckDone01 } from '@/app/components/base/icons/src/vender/solid/general'
import { ApiConnection } from '@/app/components/base/icons/src/vender/solid/development'
import InputVarTypeIcon from '@/app/components/workflow/nodes/_base/components/input-var-type-icon'
import { InputVarType } from '@/app/components/workflow/types'
export type IInputTypeIconProps = {
type: 'string' | 'select'
@ -14,13 +14,16 @@ const IconMap = (type: IInputTypeIconProps['type'], className: string) => {
const classNames = `w-3.5 h-3.5 ${className}`
const icons = {
string: (
<TypeSquare className={classNames} />
<InputVarTypeIcon type={InputVarType.textInput} className={classNames} />
),
paragraph: (
<Paragraph className={classNames} />
<InputVarTypeIcon type={InputVarType.paragraph} className={classNames} />
),
select: (
<CheckDone01 className={classNames} />
<InputVarTypeIcon type={InputVarType.select} className={classNames} />
),
number: (
<InputVarTypeIcon type={InputVarType.number} className={classNames} />
),
api: (
<ApiConnection className={classNames} />

View File

@ -3,71 +3,15 @@ import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import cn from 'classnames'
import s from './style.module.css'
import type { InputVarType } from '@/app/components/workflow/types'
import InputVarTypeIcon from '@/app/components/workflow/nodes/_base/components/input-var-type-icon'
export type ISelectTypeItemProps = {
type: string
type: InputVarType
selected: boolean
onClick: () => void
}
const Icon = ({ type, selected }: Partial<ISelectTypeItemProps>) => {
switch (type) {
case 'select':
return selected
? (
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M8.89233 4.66669H3.77428C3.42285 4.66668 3.11966 4.66667 2.86995 4.68707C2.60639 4.7086 2.34424 4.75615 2.09199 4.88468C1.71567 5.07642 1.4097 5.38238 1.21796 5.75871C1.08943 6.01096 1.04188 6.27311 1.02035 6.53667C0.999949 6.78638 0.999959 7.08954 0.99997 7.44096V12.5591C0.999959 12.9105 0.999949 13.2137 1.02035 13.4634C1.04188 13.7269 1.08943 13.9891 1.21796 14.2413C1.4097 14.6177 1.71567 14.9236 2.09199 15.1154C2.34424 15.2439 2.60639 15.2914 2.86995 15.313C3.11965 15.3334 3.4228 15.3334 3.77421 15.3334H8.89232C9.24372 15.3334 9.54696 15.3334 9.79666 15.313C10.0602 15.2914 10.3224 15.2439 10.5746 15.1154C10.9509 14.9236 11.2569 14.6177 11.4487 14.2413C11.5772 13.9891 11.6247 13.7269 11.6463 13.4634C11.6667 13.2137 11.6667 12.9105 11.6666 12.559V7.44101C11.6667 7.08957 11.6667 6.78639 11.6463 6.53667C11.6247 6.27311 11.5772 6.01096 11.4487 5.75871C11.2569 5.38238 10.9509 5.07642 10.5746 4.88468C10.3224 4.75615 10.0602 4.7086 9.79666 4.68707C9.54695 4.66667 9.24376 4.66668 8.89233 4.66669ZM9.13804 8.80476C9.39839 8.54441 9.39839 8.1223 9.13804 7.86195C8.87769 7.6016 8.45558 7.6016 8.19523 7.86195L5.66664 10.3905L4.80471 9.52862C4.54436 9.26827 4.12225 9.26827 3.8619 9.52862C3.60155 9.78897 3.60155 10.2111 3.8619 10.4714L5.19523 11.8048C5.45558 12.0651 5.87769 12.0651 6.13804 11.8048L9.13804 8.80476Z" fill="#155EEF" />
<path d="M12.8923 0.666688H7.77427C7.42285 0.666676 7.11966 0.666666 6.86995 0.687068C6.60639 0.708602 6.34424 0.756146 6.09199 0.884676C5.71567 1.07642 5.40971 1.38238 5.21796 1.75871C5.08943 2.01096 5.04188 2.27311 5.02035 2.53667C5.00206 2.76053 5.00018 3.02734 4.99999 3.33337L8.92055 3.33336C9.2463 3.33329 9.59951 3.3332 9.90523 3.35818C10.2512 3.38645 10.7084 3.45642 11.1799 3.69668C11.8071 4.01626 12.3171 4.5262 12.6367 5.1534C12.8769 5.62495 12.9469 6.08209 12.9752 6.42811C13.0001 6.73384 13.0001 7.08704 13 7.4128L13 11.3333C13.306 11.3332 13.5728 11.3313 13.7967 11.313C14.0602 11.2914 14.3224 11.2439 14.5746 11.1154C14.9509 10.9236 15.2569 10.6177 15.4487 10.2413C15.5772 9.98908 15.6247 9.72694 15.6463 9.46338C15.6667 9.21368 15.6666 8.91052 15.6666 8.55912V3.44101C15.6666 3.0896 15.6667 2.78637 15.6463 2.53667C15.6247 2.27311 15.5772 2.01096 15.4487 1.75871C15.2569 1.38238 14.9509 1.07642 14.5746 0.884676C14.3224 0.756146 14.0602 0.708602 13.7967 0.687068C13.5469 0.666666 13.2438 0.666676 12.8923 0.666688Z" fill="#155EEF" />
</svg>
)
: (
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M8.89233 4.66667H3.77428C3.42285 4.66666 3.11966 4.66665 2.86995 4.68705C2.60639 4.70859 2.34424 4.75613 2.09199 4.88466C1.71567 5.07641 1.4097 5.38237 1.21796 5.75869C1.08943 6.01095 1.04188 6.27309 1.02035 6.53665C0.999949 6.78636 0.999959 7.08953 0.99997 7.44095V12.559C0.999959 12.9105 0.999949 13.2137 1.02035 13.4634C1.04188 13.7269 1.08943 13.9891 1.21796 14.2413C1.4097 14.6176 1.71567 14.9236 2.09199 15.1154C2.34424 15.2439 2.60639 15.2914 2.86995 15.313C3.11965 15.3334 3.4228 15.3334 3.77421 15.3333H8.89232C9.24372 15.3334 9.54696 15.3334 9.79666 15.313C10.0602 15.2914 10.3224 15.2439 10.5746 15.1154C10.9509 14.9236 11.2569 14.6176 11.4487 14.2413C11.5772 13.9891 11.6247 13.7269 11.6463 13.4634C11.6667 13.2136 11.6667 12.9105 11.6666 12.559V7.44099C11.6667 7.08955 11.6667 6.78637 11.6463 6.53665C11.6247 6.27309 11.5772 6.01095 11.4487 5.75869C11.2569 5.38237 10.9509 5.07641 10.5746 4.88466C10.3224 4.75613 10.0602 4.70859 9.79666 4.68705C9.54695 4.66665 9.24376 4.66666 8.89233 4.66667ZM9.13804 8.80474C9.39839 8.54439 9.39839 8.12228 9.13804 7.86193C8.87769 7.60159 8.45558 7.60159 8.19523 7.86193L5.66664 10.3905L4.80471 9.5286C4.54436 9.26825 4.12225 9.26825 3.8619 9.5286C3.60155 9.78895 3.60155 10.2111 3.8619 10.4714L5.19523 11.8047C5.45558 12.0651 5.87769 12.0651 6.13804 11.8047L9.13804 8.80474Z" fill="#667085" />
<path d="M12.8923 0.666672H7.77427C7.42285 0.666661 7.11966 0.666651 6.86995 0.687053C6.60639 0.708587 6.34424 0.756131 6.09199 0.884661C5.71567 1.07641 5.40971 1.38237 5.21796 1.75869C5.08943 2.01095 5.04188 2.27309 5.02035 2.53665C5.00206 2.76051 5.00018 3.02733 4.99999 3.33336L8.92055 3.33335C9.2463 3.33327 9.59951 3.33319 9.90523 3.35816C10.2512 3.38644 10.7084 3.4564 11.1799 3.69667C11.8071 4.01625 12.3171 4.52618 12.6367 5.15339C12.8769 5.62493 12.9469 6.08208 12.9752 6.42809C13.0001 6.73382 13.0001 7.08702 13 7.41279L13 11.3333C13.306 11.3331 13.5728 11.3313 13.7967 11.313C14.0602 11.2914 14.3224 11.2439 14.5746 11.1154C14.9509 10.9236 15.2569 10.6176 15.4487 10.2413C15.5772 9.98907 15.6247 9.72692 15.6463 9.46336C15.6667 9.21366 15.6666 8.91051 15.6666 8.5591V3.44099C15.6666 3.08959 15.6667 2.78635 15.6463 2.53665C15.6247 2.27309 15.5772 2.01095 15.4487 1.75869C15.2569 1.38237 14.9509 1.07641 14.5746 0.884661C14.3224 0.756131 14.0602 0.708587 13.7967 0.687053C13.5469 0.666651 13.2438 0.666661 12.8923 0.666672Z" fill="#667085" />
</svg>
)
case 'paragraph':
return selected
? (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="align-left">
<g id="Solid">
<path fillRule="evenodd" clipRule="evenodd" d="M1.33334 6.66665C1.33334 6.29846 1.63182 5.99998 2.00001 5.99998H10.6667C11.0349 5.99998 11.3333 6.29846 11.3333 6.66665C11.3333 7.03484 11.0349 7.33331 10.6667 7.33331H2.00001C1.63182 7.33331 1.33334 7.03484 1.33334 6.66665Z" fill="#155EEF"/>
<path fillRule="evenodd" clipRule="evenodd" d="M1.33334 3.99998C1.33334 3.63179 1.63182 3.33331 2.00001 3.33331H13.3333C13.7015 3.33331 14 3.63179 14 3.99998C14 4.36817 13.7015 4.66665 13.3333 4.66665H2.00001C1.63182 4.66665 1.33334 4.36817 1.33334 3.99998Z" fill="#155EEF"/>
<path fillRule="evenodd" clipRule="evenodd" d="M1.33334 9.33331C1.33334 8.96512 1.63182 8.66665 2.00001 8.66665H13.3333C13.7015 8.66665 14 8.96512 14 9.33331C14 9.7015 13.7015 9.99998 13.3333 9.99998H2.00001C1.63182 9.99998 1.33334 9.7015 1.33334 9.33331Z" fill="#155EEF"/>
<path fillRule="evenodd" clipRule="evenodd" d="M1.33334 12C1.33334 11.6318 1.63182 11.3333 2.00001 11.3333H10.6667C11.0349 11.3333 11.3333 11.6318 11.3333 12C11.3333 12.3682 11.0349 12.6666 10.6667 12.6666H2.00001C1.63182 12.6666 1.33334 12.3682 1.33334 12Z" fill="#155EEF"/>
</g>
</g>
</svg>
)
: (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="align-left">
<g id="Solid">
<path fillRule="evenodd" clipRule="evenodd" d="M1.33334 6.66666C1.33334 6.29847 1.63182 5.99999 2.00001 5.99999H10.6667C11.0349 5.99999 11.3333 6.29847 11.3333 6.66666C11.3333 7.03485 11.0349 7.33333 10.6667 7.33333H2.00001C1.63182 7.33333 1.33334 7.03485 1.33334 6.66666Z" fill="#667085"/>
<path fillRule="evenodd" clipRule="evenodd" d="M1.33334 3.99999C1.33334 3.63181 1.63182 3.33333 2.00001 3.33333H13.3333C13.7015 3.33333 14 3.63181 14 3.99999C14 4.36818 13.7015 4.66666 13.3333 4.66666H2.00001C1.63182 4.66666 1.33334 4.36818 1.33334 3.99999Z" fill="#667085"/>
<path fillRule="evenodd" clipRule="evenodd" d="M1.33334 9.33333C1.33334 8.96514 1.63182 8.66666 2.00001 8.66666H13.3333C13.7015 8.66666 14 8.96514 14 9.33333C14 9.70152 13.7015 10 13.3333 10H2.00001C1.63182 10 1.33334 9.70152 1.33334 9.33333Z" fill="#667085"/>
<path fillRule="evenodd" clipRule="evenodd" d="M1.33334 12C1.33334 11.6318 1.63182 11.3333 2.00001 11.3333H10.6667C11.0349 11.3333 11.3333 11.6318 11.3333 12C11.3333 12.3682 11.0349 12.6667 10.6667 12.6667H2.00001C1.63182 12.6667 1.33334 12.3682 1.33334 12Z" fill="#667085"/>
</g>
</g>
</svg>
)
case 'string':
default:
return selected
? (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M5.17246 1.33333H10.8275C11.3642 1.33332 11.8071 1.33331 12.1679 1.36279C12.5426 1.39341 12.8871 1.45912 13.2106 1.62398C13.7124 1.87964 14.1203 2.28759 14.376 2.78935C14.5409 3.11291 14.6066 3.45738 14.6372 3.83211C14.6667 4.19291 14.6667 4.63581 14.6667 5.17245V10.8275C14.6667 11.3642 14.6667 11.8071 14.6372 12.1679C14.6066 12.5426 14.5409 12.8871 14.376 13.2106C14.1203 13.7124 13.7124 14.1203 13.2106 14.376C12.8871 14.5409 12.5426 14.6066 12.1679 14.6372C11.8071 14.6667 11.3642 14.6667 10.8275 14.6667H5.17245C4.63581 14.6667 4.1929 14.6667 3.83211 14.6372C3.45738 14.6066 3.11291 14.5409 2.78935 14.376C2.28759 14.1203 1.87964 13.7124 1.62398 13.2106C1.45912 12.8871 1.39341 12.5426 1.36279 12.1679C1.33331 11.8071 1.33332 11.3642 1.33333 10.8275V5.17245C1.33332 4.63581 1.33331 4.1929 1.36279 3.83211C1.39341 3.45738 1.45912 3.11291 1.62398 2.78935C1.87964 2.28759 2.28759 1.87964 2.78935 1.62398C3.11291 1.45912 3.45738 1.39341 3.83211 1.36279C4.1929 1.33331 4.63583 1.33332 5.17246 1.33333ZM4.66666 4.66666C4.66666 4.29847 4.96514 3.99999 5.33333 3.99999H10.6667C11.0349 3.99999 11.3333 4.29847 11.3333 4.66666C11.3333 5.03485 11.0349 5.33333 10.6667 5.33333H8.66666V11.3333C8.66666 11.7015 8.36818 12 7.99999 12C7.6318 12 7.33333 11.7015 7.33333 11.3333V5.33333H5.33333C4.96514 5.33333 4.66666 5.03485 4.66666 4.66666Z" fill="#155EEF" />
</svg>
)
: (<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M5.17246 1.33331H10.8275C11.3642 1.33331 11.8071 1.3333 12.1679 1.36278C12.5426 1.39339 12.8871 1.4591 13.2106 1.62396C13.7124 1.87963 14.1203 2.28757 14.376 2.78934C14.5409 3.1129 14.6066 3.45737 14.6372 3.8321C14.6667 4.19289 14.6667 4.63579 14.6667 5.17243V10.8275C14.6667 11.3642 14.6667 11.8071 14.6372 12.1679C14.6066 12.5426 14.5409 12.8871 14.376 13.2106C14.1203 13.7124 13.7124 14.1203 13.2106 14.376C12.8871 14.5409 12.5426 14.6066 12.1679 14.6372C11.8071 14.6667 11.3642 14.6667 10.8275 14.6666H5.17245C4.63581 14.6667 4.1929 14.6667 3.83211 14.6372C3.45738 14.6066 3.11291 14.5409 2.78935 14.376C2.28759 14.1203 1.87964 13.7124 1.62398 13.2106C1.45912 12.8871 1.39341 12.5426 1.36279 12.1679C1.33331 11.8071 1.33332 11.3642 1.33333 10.8275V5.17244C1.33332 4.6358 1.33331 4.19289 1.36279 3.8321C1.39341 3.45737 1.45912 3.1129 1.62398 2.78934C1.87964 2.28757 2.28759 1.87963 2.78935 1.62396C3.11291 1.4591 3.45738 1.39339 3.83211 1.36278C4.1929 1.3333 4.63583 1.33331 5.17246 1.33331ZM4.66666 4.66665C4.66666 4.29846 4.96514 3.99998 5.33333 3.99998H10.6667C11.0349 3.99998 11.3333 4.29846 11.3333 4.66665C11.3333 5.03484 11.0349 5.33331 10.6667 5.33331H8.66666V11.3333C8.66666 11.7015 8.36818 12 7.99999 12C7.6318 12 7.33333 11.7015 7.33333 11.3333V5.33331H5.33333C4.96514 5.33331 4.66666 5.03484 4.66666 4.66665Z" fill="#667085" />
</svg>)
}
}
const SelectTypeItem: FC<ISelectTypeItemProps> = ({
type,
selected,
@ -82,7 +26,7 @@ const SelectTypeItem: FC<ISelectTypeItemProps> = ({
onClick={onClick}
>
<div className='shrink-0'>
<Icon type={type} selected={selected} />
<InputVarTypeIcon type={type} className='w-5 h-5' />
</div>
<span className={cn(s.text)}>{typeName}</span>
</div>

View File

@ -19,6 +19,7 @@
}
.item.selected {
color: #155EEF;
border-color: #528BFF;
background-color: #F5F8FF;
box-shadow: 0px 1px 3px rgba(16, 24, 40, 0.1), 0px 1px 2px rgba(16, 24, 40, 0.06);
@ -30,7 +31,7 @@
font-weight: 500;
}
.item.selected.text {
.item.selected .text {
color: #155EEF;
}

View File

@ -9,9 +9,10 @@ import {
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import { Paragraph, TypeSquare } from '@/app/components/base/icons/src/vender/solid/editor'
import { CheckDone01 } from '@/app/components/base/icons/src/vender/solid/general'
import { ApiConnection } from '@/app/components/base/icons/src/vender/solid/development'
import InputVarTypeIcon from '@/app/components/workflow/nodes/_base/components/input-var-type-icon'
import { InputVarType } from '@/app/components/workflow/types'
type Props = {
onChange: (value: string) => void
}
@ -19,17 +20,18 @@ type Props = {
type ItemProps = {
text: string
value: string
Icon: any
Icon?: any
type?: InputVarType
onClick: (value: string) => void
}
const SelectItem: FC<ItemProps> = ({ text, value, Icon, onClick }) => {
const SelectItem: FC<ItemProps> = ({ text, type, value, Icon, onClick }) => {
return (
<div
className='flex items-center px-3 h-8 rounded-lg hover:bg-gray-50 cursor-pointer'
onClick={() => onClick(value)}
>
<Icon className='w-4 h-4 text-gray-500' />
{Icon ? <Icon className='w-4 h-4 text-gray-500' /> : <InputVarTypeIcon type={type!} className='w-4 h-4 text-gray-500' />}
<div className='ml-2 text-xs text-gray-600 truncate'>{text}</div>
</div>
)
@ -60,9 +62,10 @@ const SelectVarType: FC<Props> = ({
<PortalToFollowElemContent style={{ zIndex: 1000 }}>
<div className='bg-white border border-gray-200 shadow-lg rounded-lg min-w-[192px]'>
<div className='p-1'>
<SelectItem Icon={TypeSquare} value='string' text={t('appDebug.variableConig.string')} onClick={handleChange}></SelectItem>
<SelectItem Icon={Paragraph} value='paragraph' text={t('appDebug.variableConig.paragraph')} onClick={handleChange}></SelectItem>
<SelectItem Icon={CheckDone01} value='select' text={t('appDebug.variableConig.select')} onClick={handleChange}></SelectItem>
<SelectItem type={InputVarType.textInput} value='string' text={t('appDebug.variableConig.string')} onClick={handleChange}></SelectItem>
<SelectItem type={InputVarType.paragraph} value='paragraph' text={t('appDebug.variableConig.paragraph')} onClick={handleChange}></SelectItem>
<SelectItem type={InputVarType.select} value='select' text={t('appDebug.variableConig.select')} onClick={handleChange}></SelectItem>
<SelectItem type={InputVarType.number} value='number' text={t('appDebug.variableConig.number')} onClick={handleChange}></SelectItem>
</div>
<div className='h-[1px] bg-gray-100'></div>
<div className='p-1'>

View File

@ -0,0 +1,47 @@
'use client'
import type { FC } from 'react'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import AgentSetting from './agent/agent-setting'
import Button from '@/app/components/base/button'
import { Settings01 } from '@/app/components/base/icons/src/vender/line/general'
import type { AgentConfig } from '@/models/debug'
type Props = {
isFunctionCall: boolean
isChatModel: boolean
agentConfig?: AgentConfig
onAgentSettingChange: (payload: AgentConfig) => void
}
const AgentSettingButton: FC<Props> = ({
onAgentSettingChange,
isFunctionCall,
isChatModel,
agentConfig,
}) => {
const { t } = useTranslation()
const [isShowAgentSetting, setIsShowAgentSetting] = useState(false)
return (
<>
<Button onClick={() => setIsShowAgentSetting(true)} className='shrink-0 mr-2 !px-3 !h-8 !text-[13px] font-medium text-gray-700'>
<Settings01 className='mr-1 w-4 h-4 text-gray-500' />
{t('appDebug.agent.setting.name')}
</Button>
{isShowAgentSetting && (
<AgentSetting
isFunctionCall={isFunctionCall}
payload={agentConfig as AgentConfig}
isChatModel={isChatModel}
onSave={(payloadNew) => {
onAgentSettingChange(payloadNew)
setIsShowAgentSetting(false)
}}
onCancel={() => setIsShowAgentSetting(false)}
/>
)}
</>
)
}
export default React.memo(AgentSettingButton)

View File

@ -102,10 +102,14 @@ const Editor: FC<Props> = ({
onAddContext: showSelectDataSet,
}}
variableBlock={{
show: true,
variables: modelConfig.configs.prompt_variables.map(item => ({
name: item.name,
value: item.key,
})),
}}
externalToolBlock={{
show: true,
externalTools: externalDataToolsConfig.map(item => ({
name: item.label!,
variableName: item.variable!,

View File

@ -186,7 +186,7 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
)
: ''}
{(mode === AppType.chat && res?.opening_statement) && (
{(mode !== AppType.completion && res?.opening_statement) && (
<div className='mt-7'>
<GroupName name={t('appDebug.feature.groupChat.title')} />
<OpeningStatement

View File

@ -16,11 +16,10 @@ import AddFeatureBtn from './feature/add-feature-btn'
import ChooseFeature from './feature/choose-feature'
import useFeature from './feature/use-feature'
import AgentTools from './agent/agent-tools'
import AdvancedModeWaring from '@/app/components/app/configuration/prompt-mode/advanced-mode-waring'
import ConfigContext from '@/context/debug-configuration'
import ConfigPrompt from '@/app/components/app/configuration/config-prompt'
import ConfigVar from '@/app/components/app/configuration/config-var'
import { type CitationConfig, type ModelConfig, type ModerationConfig, type MoreLikeThisConfig, PromptMode, type PromptVariable, type SpeechToTextConfig, type SuggestedQuestionsAfterAnswerConfig, type TextToSpeechConfig } from '@/models/debug'
import { type CitationConfig, type ModelConfig, type ModerationConfig, type MoreLikeThisConfig, type PromptVariable, type SpeechToTextConfig, type SuggestedQuestionsAfterAnswerConfig, type TextToSpeechConfig } from '@/models/debug'
import { AppType, ModelModeType } from '@/types/app'
import { useModalContext } from '@/context/modal-context'
import ConfigParamModal from '@/app/components/app/configuration/toolbox/annotation/config-param-modal'
@ -35,8 +34,8 @@ const Config: FC = () => {
isAdvancedMode,
modelModeType,
isAgent,
canReturnToSimpleMode,
setPromptMode,
// canReturnToSimpleMode,
// setPromptMode,
hasSetBlockStatus,
showHistoryModal,
introduction,
@ -210,11 +209,6 @@ const Config: FC = () => {
className="grow h-0 relative px-6 pb-[50px] overflow-y-auto"
>
<AddFeatureBtn toBottomHeight={toBottomHeight} onClick={showChooseFeatureTrue} />
{
(isAdvancedMode && canReturnToSimpleMode && !isAgent) && (
<AdvancedModeWaring onReturnToSimpleMode={() => setPromptMode(PromptMode.simple)} />
)
}
{showChooseFeature && (
<ChooseFeature
isShow={showChooseFeature}
@ -245,7 +239,7 @@ const Config: FC = () => {
<DatasetConfig />
{/* Tools */}
{(isAgent && isChatApp) && (
{isAgent && (
<AgentTools />
)}

View File

@ -0,0 +1,181 @@
'use client'
import React from 'react'
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import TopKItem from '@/app/components/base/param-item/top-k-item'
import ScoreThresholdItem from '@/app/components/base/param-item/score-threshold-item'
import RadioCard from '@/app/components/base/radio-card/simple'
import { RETRIEVE_TYPE } from '@/types/app'
import {
MultiPathRetrieval,
NTo1Retrieval,
} from '@/app/components/base/icons/src/public/common'
import type {
DatasetConfigs,
} from '@/models/debug'
import ModelSelector from '@/app/components/header/account-setting/model-provider-page/model-selector'
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
import type { ModelConfig } from '@/app/components/workflow/types'
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
import TooltipPlus from '@/app/components/base/tooltip-plus'
import { HelpCircle } from '@/app/components/base/icons/src/vender/line/general'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
type Props = {
datasetConfigs: DatasetConfigs
onChange: (configs: DatasetConfigs, isRetrievalModeChange?: boolean) => void
isInWorkflow?: boolean
singleRetrievalModelConfig?: ModelConfig
onSingleRetrievalModelChange?: (config: ModelConfig) => void
onSingleRetrievalModelParamsChange?: (config: ModelConfig) => void
}
const ConfigContent: FC<Props> = ({
datasetConfigs,
onChange,
isInWorkflow,
singleRetrievalModelConfig: singleRetrievalConfig = {} as ModelConfig,
onSingleRetrievalModelChange = () => { },
onSingleRetrievalModelParamsChange = () => { },
}) => {
const { t } = useTranslation()
const type = datasetConfigs.retrieval_model
const setType = (value: RETRIEVE_TYPE) => {
onChange({
...datasetConfigs,
retrieval_model: value,
}, true)
}
const {
modelList: rerankModelList,
defaultModel: rerankDefaultModel,
} = useModelListAndDefaultModelAndCurrentProviderAndModel(ModelTypeEnum.rerank)
const rerankModel = (() => {
if (datasetConfigs.reranking_model) {
return {
provider_name: datasetConfigs.reranking_model.reranking_provider_name,
model_name: datasetConfigs.reranking_model.reranking_model_name,
}
}
else if (rerankDefaultModel) {
return {
provider_name: rerankDefaultModel.provider.provider,
model_name: rerankDefaultModel.model,
}
}
})()
const handleParamChange = (key: string, value: number) => {
if (key === 'top_k') {
onChange({
...datasetConfigs,
top_k: value,
})
}
else if (key === 'score_threshold') {
onChange({
...datasetConfigs,
score_threshold: value,
})
}
}
const handleSwitch = (key: string, enable: boolean) => {
if (key === 'top_k')
return
onChange({
...datasetConfigs,
score_threshold_enabled: enable,
})
}
const model = singleRetrievalConfig
return (
<div>
<div className='mt-2 space-y-3'>
<RadioCard
icon={<NTo1Retrieval className='shrink-0 mr-3 w-9 h-9 rounded-lg' />}
title={t('appDebug.datasetConfig.retrieveOneWay.title')}
description={t('appDebug.datasetConfig.retrieveOneWay.description')}
isChosen={type === RETRIEVE_TYPE.oneWay}
onChosen={() => { setType(RETRIEVE_TYPE.oneWay) }}
/>
<RadioCard
icon={<MultiPathRetrieval className='shrink-0 mr-3 w-9 h-9 rounded-lg' />}
title={t('appDebug.datasetConfig.retrieveMultiWay.title')}
description={t('appDebug.datasetConfig.retrieveMultiWay.description')}
isChosen={type === RETRIEVE_TYPE.multiWay}
onChosen={() => { setType(RETRIEVE_TYPE.multiWay) }}
/>
</div>
{type === RETRIEVE_TYPE.multiWay && (
<>
<div className='mt-6'>
<div className='leading-[32px] text-[13px] font-medium text-gray-900'>{t('common.modelProvider.rerankModel.key')}</div>
<div>
<ModelSelector
defaultModel={rerankModel && { provider: rerankModel?.provider_name, model: rerankModel?.model_name }}
onSelect={(v) => {
onChange({
...datasetConfigs,
reranking_model: {
reranking_provider_name: v.provider,
reranking_model_name: v.model,
},
})
}}
modelList={rerankModelList}
/>
</div>
</div>
<div className='mt-4 space-y-4'>
<TopKItem
value={datasetConfigs.top_k}
onChange={handleParamChange}
enable={true}
/>
<ScoreThresholdItem
value={datasetConfigs.score_threshold as number}
onChange={handleParamChange}
enable={datasetConfigs.score_threshold_enabled}
hasSwitch={true}
onSwitchChange={handleSwitch}
/>
</div>
</>
)}
{isInWorkflow && type === RETRIEVE_TYPE.oneWay && (
<div className='mt-6'>
<div className='flex items-center space-x-0.5'>
<div className='leading-[32px] text-[13px] font-medium text-gray-900'>{t('common.modelProvider.systemReasoningModel.key')}</div>
<TooltipPlus
popupContent={t('common.modelProvider.systemReasoningModel.tip')}
>
<HelpCircle className='w-3.5 h-4.5 text-gray-400' />
</TooltipPlus>
</div>
<ModelParameterModal
isInWorkflow={isInWorkflow}
popupClassName='!w-[387px]'
portalToFollowElemContentClassName='!z-[1002]'
isAdvancedMode={true}
mode={model?.mode}
provider={model?.provider}
completionParams={model?.completion_params}
modelId={model?.name}
setModel={onSingleRetrievalModelChange as any}
onCompletionParamsChange={onSingleRetrievalModelParamsChange as any}
hideDebugWithMultipleModel
debugWithMultipleModel={false}
/>
</div>
)
}
</div >
)
}
export default React.memo(ConfigContent)

View File

@ -4,21 +4,14 @@ import { memo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import cn from 'classnames'
import ConfigContent from './config-content'
import { Settings04 } from '@/app/components/base/icons/src/vender/line/general'
import ConfigContext from '@/context/debug-configuration'
import TopKItem from '@/app/components/base/param-item/top-k-item'
import ScoreThresholdItem from '@/app/components/base/param-item/score-threshold-item'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import RadioCard from '@/app/components/base/radio-card/simple'
import { RETRIEVE_TYPE } from '@/types/app'
import Toast from '@/app/components/base/toast'
import { DATASET_DEFAULT } from '@/config'
import {
MultiPathRetrieval,
NTo1Retrieval,
} from '@/app/components/base/icons/src/public/common'
import ModelSelector from '@/app/components/header/account-setting/model-provider-page/model-selector'
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
@ -31,58 +24,11 @@ const ParamsConfig: FC = () => {
} = useContext(ConfigContext)
const [tempDataSetConfigs, setTempDataSetConfigs] = useState(datasetConfigs)
const type = tempDataSetConfigs.retrieval_model
const setType = (value: RETRIEVE_TYPE) => {
setTempDataSetConfigs({
...tempDataSetConfigs,
retrieval_model: value,
})
}
const {
modelList: rerankModelList,
defaultModel: rerankDefaultModel,
currentModel: isRerankDefaultModelVaild,
} = useModelListAndDefaultModelAndCurrentProviderAndModel(ModelTypeEnum.rerank)
const rerankModel = (() => {
if (tempDataSetConfigs.reranking_model) {
return {
provider_name: tempDataSetConfigs.reranking_model.reranking_provider_name,
model_name: tempDataSetConfigs.reranking_model.reranking_model_name,
}
}
else if (rerankDefaultModel) {
return {
provider_name: rerankDefaultModel.provider.provider,
model_name: rerankDefaultModel.model,
}
}
})()
const handleParamChange = (key: string, value: number) => {
if (key === 'top_k') {
setTempDataSetConfigs({
...tempDataSetConfigs,
top_k: value,
})
}
else if (key === 'score_threshold') {
setTempDataSetConfigs({
...tempDataSetConfigs,
score_threshold: value,
})
}
}
const handleSwitch = (key: string, enable: boolean) => {
if (key === 'top_k')
return
setTempDataSetConfigs({
...tempDataSetConfigs,
score_threshold_enabled: enable,
})
}
const isValid = () => {
let errMsg = ''
if (tempDataSetConfigs.retrieval_model === RETRIEVE_TYPE.multiWay) {
@ -141,58 +87,11 @@ const ParamsConfig: FC = () => {
wrapperClassName='z-50'
title={t('appDebug.datasetConfig.settingTitle')}
>
<div className='mt-2 space-y-3'>
<RadioCard
icon={<NTo1Retrieval className='shrink-0 mr-3 w-9 h-9 rounded-lg' />}
title={t('appDebug.datasetConfig.retrieveOneWay.title')}
description={t('appDebug.datasetConfig.retrieveOneWay.description')}
isChosen={type === RETRIEVE_TYPE.oneWay}
onChosen={() => { setType(RETRIEVE_TYPE.oneWay) }}
/>
<RadioCard
icon={<MultiPathRetrieval className='shrink-0 mr-3 w-9 h-9 rounded-lg' />}
title={t('appDebug.datasetConfig.retrieveMultiWay.title')}
description={t('appDebug.datasetConfig.retrieveMultiWay.description')}
isChosen={type === RETRIEVE_TYPE.multiWay}
onChosen={() => { setType(RETRIEVE_TYPE.multiWay) }}
/>
</div>
{type === RETRIEVE_TYPE.multiWay && (
<>
<div className='mt-6'>
<div className='leading-[32px] text-[13px] font-medium text-gray-900'>{t('common.modelProvider.rerankModel.key')}</div>
<div>
<ModelSelector
defaultModel={rerankModel && { provider: rerankModel?.provider_name, model: rerankModel?.model_name }}
onSelect={(v) => {
setTempDataSetConfigs({
...tempDataSetConfigs,
reranking_model: {
reranking_provider_name: v.provider,
reranking_model_name: v.model,
},
})
}}
modelList={rerankModelList}
/>
</div>
</div>
<div className='mt-4 space-y-4'>
<TopKItem
value={tempDataSetConfigs.top_k}
onChange={handleParamChange}
enable={true}
/>
<ScoreThresholdItem
value={tempDataSetConfigs.score_threshold}
onChange={handleParamChange}
enable={tempDataSetConfigs.score_threshold_enabled}
hasSwitch={true}
onSwitchChange={handleSwitch}
/>
</div>
</>
)}
<ConfigContent
datasetConfigs={tempDataSetConfigs}
onChange={setTempDataSetConfigs}
/>
<div className='mt-6 flex justify-end'>
<Button className='mr-2 flex-shrink-0' onClick={() => {
setOpen(false)

View File

@ -2,7 +2,7 @@
import type { FC } from 'react'
import useSWR from 'swr'
import { useTranslation } from 'react-i18next'
import React, { useCallback, useEffect, useState } from 'react'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { setAutoFreeze } from 'immer'
import { useBoolean } from 'ahooks'
import { useContext } from 'use-context-selector'
@ -36,6 +36,8 @@ import type { ModelParameterModalProps } from '@/app/components/header/account-s
import { Plus } from '@/app/components/base/icons/src/vender/line/general'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import { useProviderContext } from '@/context/provider-context'
import PromptLogModal from '@/app/components/base/prompt-log-modal'
import { useStore as useAppStore } from '@/app/components/app/store'
type IDebug = {
hasSetAPIKEY: boolean
@ -135,7 +137,7 @@ const Debug: FC<IDebug> = ({
const [completionFiles, setCompletionFiles] = useState<VisionFile[]>([])
const checkCanSend = useCallback(() => {
if (isAdvancedMode && mode === AppType.chat) {
if (isAdvancedMode && mode !== AppType.completion) {
if (modelModeType === ModelModeType.completion) {
if (!hasSetBlockStatus.history) {
notify({ type: 'error', message: t('appDebug.otherError.historyNoBeEmpty'), duration: 3000 })
@ -365,6 +367,19 @@ const Debug: FC<IDebug> = ({
handleVisionConfigInMultipleModel()
}, [multipleModelConfigs, mode])
const { currentLogItem, setCurrentLogItem, showPromptLogModal, setShowPromptLogModal } = useAppStore()
const [width, setWidth] = useState(0)
const ref = useRef<HTMLDivElement>(null)
const adjustModalWidth = () => {
if (ref.current)
setWidth(document.body.clientWidth - (ref.current?.clientWidth + 16) - 8)
}
useEffect(() => {
adjustModalWidth()
}, [])
return (
<>
<div className="shrink-0 pt-4 px-6">
@ -391,7 +406,7 @@ const Debug: FC<IDebug> = ({
)
: null
}
{mode === 'chat' && (
{mode !== AppType.completion && (
<Button className='flex items-center gap-1 !h-8 !bg-white' onClick={clearConversation}>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.66663 2.66629V5.99963H3.05463M3.05463 5.99963C3.49719 4.90505 4.29041 3.98823 5.30998 3.39287C6.32954 2.7975 7.51783 2.55724 8.68861 2.70972C9.85938 2.8622 10.9465 3.39882 11.7795 4.23548C12.6126 5.07213 13.1445 6.16154 13.292 7.33296M3.05463 5.99963H5.99996M13.3333 13.333V9.99963H12.946M12.946 9.99963C12.5028 11.0936 11.7093 12.0097 10.6898 12.6045C9.67038 13.1993 8.48245 13.4393 7.31203 13.2869C6.1416 13.1344 5.05476 12.5982 4.22165 11.7621C3.38854 10.926 2.8562 9.83726 2.70796 8.66629M12.946 9.99963H9.99996" stroke="#1C64F2" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
@ -426,9 +441,9 @@ const Debug: FC<IDebug> = ({
}
{
!debugWithMultipleModel && (
<div className="flex flex-col grow">
<div className="flex flex-col grow" ref={ref}>
{/* Chat */}
{mode === AppType.chat && (
{mode !== AppType.completion && (
<div className='grow h-0 overflow-hidden'>
<DebugWithSingleModel
ref={debugWithSingleModelRef}
@ -458,6 +473,16 @@ const Debug: FC<IDebug> = ({
)}
</div>
)}
{showPromptLogModal && (
<PromptLogModal
width={width}
currentLogItem={currentLogItem}
onCancel={() => {
setCurrentLogItem()
setShowPromptLogModal(false)
}}
/>
)}
{isShowCannotQueryDataset && (
<CannotQueryDataset
onConfirm={() => setShowCannotQueryDataset(false)}

View File

@ -15,5 +15,4 @@ export type DebugWithSingleOrMultipleModelConfigs = {
}
export const APP_CHAT_WITH_MULTIPLE_MODEL = 'APP_CHAT_WITH_MULTIPLE_MODEL'
export const APP_CHAT_WITH_MULTIPLE_MODEL_RESTART = 'APP_CHAT_WITH_MULTIPLE_MODEL_RESTART'
export const APP_SIDEBAR_SHOULD_COLLAPSE = 'APP_SIDEBAR_SHOULD_COLLAPSE'
export const ORCHESTRATE_CHANGED = 'ORCHESTRATE_CHANGED'

View File

@ -88,7 +88,7 @@ const useAdvancedPromptConfig = ({
}
}
else {
const prompt = completionPromptConfig.prompt.text
const prompt = completionPromptConfig.prompt?.text
return {
context: checkHasContextBlock(prompt),
history: checkHasHistoryBlock(prompt),
@ -146,11 +146,11 @@ const useAdvancedPromptConfig = ({
if (toModelModeType === ModelModeType.completion) {
const newPromptConfig = produce(completion_prompt_config, (draft) => {
if (!completionPromptConfig.prompt.text)
if (!completionPromptConfig.prompt?.text)
draft.prompt.text = draft.prompt.text.replace(PRE_PROMPT_PLACEHOLDER_TEXT, toReplacePrePrompt)
else
draft.prompt.text = completionPromptConfig.prompt.text.replace(PRE_PROMPT_PLACEHOLDER_TEXT, toReplacePrePrompt)
draft.prompt.text = completionPromptConfig.prompt?.text.replace(PRE_PROMPT_PLACEHOLDER_TEXT, toReplacePrePrompt)
if (appMode === AppType.chat && completionPromptConfig.conversation_histories_role.assistant_prefix && completionPromptConfig.conversation_histories_role.user_prefix)
draft.conversation_histories_role = completionPromptConfig.conversation_histories_role

View File

@ -1,16 +1,17 @@
'use client'
import type { FC } from 'react'
import React, { useEffect, useRef, useState } from 'react'
import React, { useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import { usePathname } from 'next/navigation'
import produce from 'immer'
import { useBoolean, useGetState } from 'ahooks'
import cn from 'classnames'
import { clone, isEqual } from 'lodash-es'
import { CodeBracketIcon } from '@heroicons/react/20/solid'
import Button from '../../base/button'
import Loading from '../../base/loading'
import AppPublisher from '../app-publisher'
import AgentSettingButton from './config/agent-setting-button'
import useAdvancedPromptConfig from './hooks/use-advanced-prompt-config'
import EditHistoryModal from './config-prompt/conversation-histroy/edit-modal'
import {
@ -18,9 +19,6 @@ import {
useFormattingChangedDispatcher,
} from './debug/hooks'
import type { ModelAndParameter } from './debug/types'
import { APP_SIDEBAR_SHOULD_COLLAPSE } from './debug/types'
import PublishWithMultipleModel from './debug/debug-with-multiple-model/publish-with-multiple-model'
import AssistantTypePicker from './config/assistant-type-picker'
import type {
AnnotationReplyConfig,
DatasetConfigs,
@ -57,7 +55,7 @@ import type { FormValue } from '@/app/components/header/account-setting/model-pr
import { useTextGenerationCurrentProviderAndModelAndModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { fetchCollectionList } from '@/service/tools'
import { type Collection } from '@/app/components/tools/types'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import { useStore as useAppStore } from '@/app/components/app/store'
type PublichConfig = {
modelConfig: ModelConfig
@ -67,6 +65,7 @@ type PublichConfig = {
const Configuration: FC = () => {
const { t } = useTranslation()
const { notify } = useContext(ToastContext)
const { appDetail, setAppSiderbarExpand } = useAppStore()
const [formattingChanged, setFormattingChanged] = useState(false)
const { setShowAccountSettingModal } = useModalContext()
const [hasFetchedDetail, setHasFetchedDetail] = useState(false)
@ -77,6 +76,7 @@ const Configuration: FC = () => {
const [mode, setMode] = useState('')
const [publishedConfig, setPublishedConfig] = useState<PublichConfig | null>(null)
const modalConfig = useMemo(() => appDetail?.model_config || {} as BackendModelConfig, [appDetail])
const [conversationId, setConversationId] = useState<string | null>('')
const media = useBreakpoints()
@ -130,7 +130,7 @@ const Configuration: FC = () => {
const [inputs, setInputs] = useState<Inputs>({})
const [query, setQuery] = useState('')
const [completionParams, doSetCompletionParams] = useState<FormValue>({})
const [tempStop, setTempStop, getTempStop] = useGetState<string[]>([])
const [_, setTempStop, getTempStop] = useGetState<string[]>([])
const setCompletionParams = (value: FormValue) => {
const params = { ...value }
@ -161,14 +161,8 @@ const Configuration: FC = () => {
agentConfig: DEFAULT_AGENT_SETTING,
})
const isChatApp = mode === AppType.chat
const isAgent = modelConfig.agentConfig?.enabled
const setIsAgent = (value: boolean) => {
const newModelConfig = produce(modelConfig, (draft: ModelConfig) => {
draft.agentConfig.enabled = value
})
doSetModelConfig(newModelConfig)
}
const isAgent = mode === 'agent-chat'
const isOpenAI = modelConfig.provider === 'openai'
const [collectionList, setCollectionList] = useState<Collection[]>([])
@ -352,12 +346,12 @@ const Configuration: FC = () => {
const appMode = mode
if (modeMode === ModelModeType.completion) {
if (appMode === AppType.chat) {
if (!completionPromptConfig.prompt.text || !completionPromptConfig.conversation_histories_role.assistant_prefix || !completionPromptConfig.conversation_histories_role.user_prefix)
if (appMode !== AppType.completion) {
if (!completionPromptConfig.prompt?.text || !completionPromptConfig.conversation_histories_role.assistant_prefix || !completionPromptConfig.conversation_histories_role.user_prefix)
await migrateToDefaultPrompt(true, ModelModeType.completion)
}
else {
if (!completionPromptConfig.prompt.text)
if (!completionPromptConfig.prompt?.text)
await migrateToDefaultPrompt(true, ModelModeType.completion)
}
}
@ -447,7 +441,7 @@ const Configuration: FC = () => {
model_id: model.name,
mode: model.mode,
configs: {
prompt_template: modelConfig.pre_prompt,
prompt_template: modelConfig.pre_prompt || '',
prompt_variables: userInputsFormToPromptVariables(
[
...modelConfig.user_input_form,
@ -483,7 +477,7 @@ const Configuration: FC = () => {
external_data_tools: modelConfig.external_data_tools,
dataSets: datasets || [],
// eslint-disable-next-line multiline-ternary
agentConfig: res.is_agent ? {
agentConfig: res.mode === 'agent-chat' ? {
max_iteration: DEFAULT_AGENT_SETTING.max_iteration,
...modelConfig.agent_mode,
// remove dataset
@ -514,10 +508,11 @@ const Configuration: FC = () => {
setHasFetchedDetail(true)
})
})()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [appId])
const promptEmpty = (() => {
if (mode === AppType.chat)
if (mode !== AppType.completion)
return false
if (isAdvancedMode) {
@ -525,13 +520,13 @@ const Configuration: FC = () => {
return chatPromptConfig.prompt.every(({ text }: any) => !text)
else
return !completionPromptConfig.prompt.text
return !completionPromptConfig.prompt?.text
}
else { return !modelConfig.configs.prompt_template }
})()
const cannotPublish = (() => {
if (mode === AppType.chat) {
if (mode !== AppType.completion) {
if (!isAdvancedMode)
return false
@ -547,7 +542,7 @@ const Configuration: FC = () => {
else { return promptEmpty }
})()
const contextVarEmpty = mode === AppType.completion && dataSets.length > 0 && !hasSetContextVar
const handlePublish = async (isSilence?: boolean, modelAndParameter?: ModelAndParameter) => {
const onPublish = async (modelAndParameter?: ModelAndParameter) => {
const modelId = modelAndParameter?.model || modelConfig.model_id
const promptTemplate = modelConfig.configs.prompt_template
const promptVariables = modelConfig.configs.prompt_variables
@ -556,7 +551,7 @@ const Configuration: FC = () => {
notify({ type: 'error', message: t('appDebug.otherError.promptNoBeEmpty'), duration: 3000 })
return
}
if (isAdvancedMode && mode === AppType.chat) {
if (isAdvancedMode && mode !== AppType.completion) {
if (modelModeType === ModelModeType.completion) {
if (!hasSetBlockStatus.history) {
notify({ type: 'error', message: t('appDebug.otherError.historyNoBeEmpty'), duration: 3000 })
@ -636,22 +631,20 @@ const Configuration: FC = () => {
modelConfig: newModelConfig,
completionParams,
})
if (!isSilence)
notify({ type: 'success', message: t('common.api.success'), duration: 3000 })
notify({ type: 'success', message: t('common.api.success'), duration: 3000 })
setCanReturnToSimpleMode(false)
return true
}
const [showConfirm, setShowConfirm] = useState(false)
const [restoreConfirmOpen, setRestoreConfirmOpen] = useState(false)
const resetAppConfig = () => {
syncToPublishedConfig(publishedConfig!)
setShowConfirm(false)
setRestoreConfirmOpen(false)
}
const [showUseGPT4Confirm, setShowUseGPT4Confirm] = useState(false)
const { eventEmitter } = useEventEmitterContextContext()
const {
debugWithMultipleModel,
multipleModelConfigs,
@ -666,13 +659,11 @@ const Configuration: FC = () => {
{ id: `${Date.now()}-no-repeat`, model: '', provider: '', parameters: {} },
],
)
eventEmitter?.emit({
type: APP_SIDEBAR_SHOULD_COLLAPSE,
} as any)
setAppSiderbarExpand('collapse')
}
if (isLoading) {
return <div className='flex h-full items-center justify-center'>
return <div className='flex items-center justify-center h-full'>
<Loading type='area' />
</div>
}
@ -750,47 +741,36 @@ const Configuration: FC = () => {
>
<>
<div className="flex flex-col h-full">
<div className='flex grow h-[200px]'>
<div className={`w-full sm:w-1/2 shrink-0 flex flex-col h-full ${debugWithMultipleModel && 'max-w-[560px]'}`}>
{/* Header Left */}
<div className='flex justify-between items-center px-6 h-14'>
<div className='relative flex grow h-[200px] pt-14'>
{/* Header */}
<div className='absolute top-0 left-0 w-full bg-white h-14'>
<div className='flex items-center justify-between px-6 h-14'>
<div className='flex items-center'>
<div className='leading-6 text-base font-semibold text-gray-900'>{t('appDebug.orchestrate')}</div>
<div className='text-base font-semibold leading-6 text-gray-900'>{t('appDebug.orchestrate')}</div>
<div className='flex items-center h-[14px] space-x-1 text-xs'>
{isAdvancedMode && (
<div className='ml-1 flex items-center h-5 px-1.5 border border-gray-100 rounded-md text-[11px] font-medium text-gray-500 uppercase'>{t('appDebug.promptMode.advanced')}</div>
)}
</div>
</div>
{isChatApp && (
<AssistantTypePicker
value={isAgent ? 'agent' : 'assistant'}
disabled={isAdvancedMode && !canReturnToSimpleMode}
onChange={(value: string) => {
setIsAgent(value === 'agent')
if (value === 'agent')
setPromptMode(PromptMode.simple)
}}
isFunctionCall={isFunctionCall}
isChatModel={modelConfig.mode === ModelModeType.chat}
agentConfig={modelConfig.agentConfig}
onAgentSettingChange={(config) => {
const nextConfig = produce(modelConfig, (draft: ModelConfig) => {
draft.agentConfig = config
})
setModelConfig(nextConfig)
}}
/>
)}
</div>
<Config />
</div>
{!isMobile && <div className="grow relative w-1/2 h-full overflow-y-auto flex flex-col " style={{ borderColor: 'rgba(0, 0, 0, 0.02)' }}>
{/* Header Right */}
<div className='flex justify-end items-center flex-wrap px-6 h-14 space-x-2'>
{/* Model and Parameters */}
{
!debugWithMultipleModel && (
<div className='flex items-center'>
{/* Agent Setting */}
{isAgent && (
<AgentSettingButton
isChatModel={modelConfig.mode === ModelModeType.chat}
agentConfig={modelConfig.agentConfig}
isFunctionCall={isFunctionCall}
onAgentSettingChange={(config) => {
const nextConfig = produce(modelConfig, (draft: ModelConfig) => {
draft.agentConfig = config
})
setModelConfig(nextConfig)
}}
/>
)}
{/* Model and Parameters */}
{!debugWithMultipleModel && (
<>
<ModelParameterModal
isAdvancedMode={isAdvancedMode}
@ -805,37 +785,31 @@ const Configuration: FC = () => {
debugWithMultipleModel={debugWithMultipleModel}
onDebugWithMultipleModelChange={handleDebugWithMultipleModelChange}
/>
<div className='w-[1px] h-[14px] bg-gray-200'></div>
<div className='mx-2 w-[1px] h-[14px] bg-gray-200'></div>
</>
)
}
<Button onClick={() => setShowConfirm(true)} className='shrink-0 mr-2 w-[70px] !h-8 !text-[13px] font-medium'>{t('appDebug.operation.resetConfig')}</Button>
{isMobile && (
<Button className='!h-8 !text-[13px] font-medium' onClick={showDebugPanel}>
<span className='mr-1'>{t('appDebug.operation.debugConfig')}</span>
<CodeBracketIcon className="h-4 w-4 text-gray-500" />
</Button>
)}
{
debugWithMultipleModel
? (
<PublishWithMultipleModel
multipleModelConfigs={multipleModelConfigs}
onSelect={item => handlePublish(false, item)}
/>
)
: (
<Button
type='primary'
onClick={() => handlePublish(false)}
className={cn(cannotPublish && '!bg-primary-200 !cursor-not-allowed', 'shrink-0 w-[70px] !h-8 !text-[13px] font-medium')}
>
{t('appDebug.operation.applyConfig')}
</Button>
)
}
)}
{isMobile && (
<Button className='!h-8 !text-[13px] font-medium' onClick={showDebugPanel}>
<span className='mr-1'>{t('appDebug.operation.debugConfig')}</span>
<CodeBracketIcon className="w-4 h-4 text-gray-500" />
</Button>
)}
<AppPublisher {...{
publishDisabled: cannotPublish,
publishedAt: (modalConfig.created_at || 0) * 1000,
debugWithMultipleModel,
multipleModelConfigs,
onPublish,
onRestore: () => setRestoreConfirmOpen(true),
}} />
</div>
</div>
<div className='flex flex-col grow h-0 rounded-tl-2xl border-t border-l bg-gray-50 '>
</div>
<div className={`w-full sm:w-1/2 shrink-0 flex flex-col h-full ${debugWithMultipleModel && 'max-w-[560px]'}`}>
<Config />
</div>
{!isMobile && <div className="relative flex flex-col w-1/2 h-full overflow-y-auto grow " style={{ borderColor: 'rgba(0, 0, 0, 0.02)' }}>
<div className='flex flex-col h-0 border-t border-l grow rounded-tl-2xl bg-gray-50 '>
<Debug
hasSetAPIKEY={hasSettedApiKey}
onSetting={() => setShowAccountSettingModal({ payload: 'provider' })}
@ -852,14 +826,14 @@ const Configuration: FC = () => {
</div>}
</div>
</div>
{showConfirm && (
{restoreConfirmOpen && (
<Confirm
title={t('appDebug.resetConfig.title')}
content={t('appDebug.resetConfig.message')}
isShow={showConfirm}
onClose={() => setShowConfirm(false)}
isShow={restoreConfirmOpen}
onClose={() => setRestoreConfirmOpen(false)}
onConfirm={resetAppConfig}
onCancel={() => setShowConfirm(false)}
onCancel={() => setRestoreConfirmOpen(false)}
/>
)}
{showUseGPT4Confirm && (

View File

@ -54,7 +54,7 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
if (isAdvancedMode) {
if (modelModeType === ModelModeType.chat)
return chatPromptConfig.prompt.every(({ text }) => !text)
return !completionPromptConfig.prompt.text
return !completionPromptConfig.prompt?.text
}
else { return !modelConfig.configs.prompt_template }
@ -149,7 +149,15 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
onChange={(e) => { handleInputValueChange(key, e.target.value) }}
/>
)}
{type === 'number' && (
<input
className="w-full px-3 text-sm leading-9 text-gray-900 border-0 rounded-lg grow h-9 bg-gray-50 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200"
placeholder={`${name}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
type="number"
value={inputs[key] ? `${inputs[key]}` : ''}
onChange={(e) => { handleInputValueChange(key, e.target.value) }}
/>
)}
</div>
))}
</div>

View File

@ -0,0 +1,37 @@
'use client'
import { useTranslation } from 'react-i18next'
import NewAppDialog from './newAppDialog'
import AppList, { PageType } from '@/app/components/explore/app-list'
import { XClose } from '@/app/components/base/icons/src/vender/line/general'
type CreateAppDialogProps = {
show: boolean
onSuccess: () => void
onClose: () => void
}
const CreateAppTemplateDialog = ({ show, onSuccess, onClose }: CreateAppDialogProps) => {
const { t } = useTranslation()
return (
<NewAppDialog
className='flex'
show={show}
onClose={() => {}}
>
{/* template list */}
<div className='grow flex flex-col h-full bg-gray-100'>
<div className='shrink-0 pl-8 pr-6 pt-6 pb-3 bg-gray-100 rounded-se-xl text-xl leading-[30px] font-semibold text-gray-900 z-10'>{t('app.newApp.startFromTemplate')}</div>
<AppList onSuccess={() => {
onSuccess()
onClose()
}} pageType={PageType.CREATE} />
</div>
<div className='absolute right-6 top-6 p-2 cursor-pointer z-20' onClick={onClose}>
<XClose className='w-4 h-4 text-gray-500' />
</div>
</NewAppDialog>
)
}
export default CreateAppTemplateDialog

View File

@ -0,0 +1,57 @@
import { Fragment, useCallback } from 'react'
import type { ReactNode } from 'react'
import { Dialog, Transition } from '@headlessui/react'
import cn from 'classnames'
type DialogProps = {
className?: string
children: ReactNode
show: boolean
onClose?: () => void
}
const NewAppDialog = ({
className,
children,
show,
onClose,
}: DialogProps) => {
const close = useCallback(() => onClose?.(), [onClose])
return (
<Transition appear show={show} as={Fragment}>
<Dialog as="div" className="relative z-40" onClose={close}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-black bg-opacity-25" />
</Transition.Child>
<div className="fixed inset-0">
<div className="flex flex-col items-center justify-center min-h-full pt-[56px]">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className={cn('grow relative w-full h-[calc(100vh-56px)] p-0 overflow-hidden text-left align-middle transition-all transform bg-white shadow-xl rounded-t-xl', className)}>
{children}
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition >
)
}
export default NewAppDialog

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@ -0,0 +1,16 @@
<svg width="125" height="74" viewBox="0 0 125 74" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Grid BG">
<mask id="mask0_3169_30051" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="125" height="74">
<rect id="Mask" width="125" height="74" fill="url(#paint0_linear_3169_30051)"/>
</mask>
<g mask="url(#mask0_3169_30051)">
<path id="Grid" d="M7 -3H-1V5M7 -3V5M7 -3H15M7 5H-1M7 5H15M7 5V13M-1 5V13M15 -3V5M15 -3H23M15 5H23M15 5V13M23 -3V5M23 -3H31M23 5H31M23 5V13M31 -3V5M31 -3H39M31 5H39M31 5V13M39 -3V5M39 -3H47M39 5H47M39 5V13M47 -3V5M47 -3H55M47 5H55M47 5V13M55 -3V5M55 -3H63M55 5H63M55 5V13M63 -3V5M63 -3H71M63 5H71M63 5V13M71 -3V5M71 -3H79M71 5H79M71 5V13M79 -3V5M79 -3H87M79 5H87M79 5V13M87 -3V5M87 -3H95M87 5H95M87 5V13M95 -3V5M95 -3H103M95 5H103M95 5V13M103 -3V5M103 -3H111M103 5H111M103 5V13M111 -3V5M111 -3H119M111 5H119M111 5V13M119 -3V5M119 -3H127V5M119 5H127M119 5V13M127 5V13M7 13H-1M7 13H15M7 13V21M-1 13V21M15 13H23M15 13V21M23 13H31M23 13V21M31 13H39M31 13V21M39 13H47M39 13V21M47 13H55M47 13V21M55 13H63M55 13V21M63 13H71M63 13V21M71 13H79M71 13V21M79 13H87M79 13V21M87 13H95M87 13V21M95 13H103M95 13V21M103 13H111M103 13V21M111 13H119M111 13V21M119 13H127M119 13V21M127 13V21M7 21H-1M7 21H15M7 21V29M-1 21V29M15 21H23M15 21V29M23 21H31M23 21V29M31 21H39M31 21V29M39 21H47M39 21V29M47 21H55M47 21V29M55 21H63M55 21V29M63 21H71M63 21V29M71 21H79M71 21V29M79 21H87M79 21V29M87 21H95M87 21V29M95 21H103M95 21V29M103 21H111M103 21V29M111 21H119M111 21V29M119 21H127M119 21V29M127 21V29M7 29H-1M7 29H15M7 29V37M-1 29V37M15 29H23M15 29V37M23 29H31M23 29V37M31 29H39M31 29V37M39 29H47M39 29V37M47 29H55M47 29V37M55 29H63M55 29V37M63 29H71M63 29V37M71 29H79M71 29V37M79 29H87M79 29V37M87 29H95M87 29V37M95 29H103M95 29V37M103 29H111M103 29V37M111 29H119M111 29V37M119 29H127M119 29V37M127 29V37M7 37H-1M7 37H15M7 37V45M-1 37V45M15 37H23M15 37V45M23 37H31M23 37V45M31 37H39M31 37V45M39 37H47M39 37V45M47 37H55M47 37V45M55 37H63M55 37V45M63 37H71M63 37V45M71 37H79M71 37V45M79 37H87M79 37V45M87 37H95M87 37V45M95 37H103M95 37V45M103 37H111M103 37V45M111 37H119M111 37V45M119 37H127M119 37V45M127 37V45M7 45H-1M7 45H15M7 45V53M-1 45V53M15 45H23M15 45V53M23 45H31M23 45V53M31 45H39M31 45V53M39 45H47M39 45V53M47 45H55M47 45V53M55 45H63M55 45V53M63 45H71M63 45V53M71 45H79M71 45V53M79 45H87M79 45V53M87 45H95M87 45V53M95 45H103M95 45V53M103 45H111M103 45V53M111 45H119M111 45V53M119 45H127M119 45V53M127 45V53M7 53H-1M7 53H15M7 53V61M-1 53V61M15 53H23M15 53V61M23 53H31M23 53V61M31 53H39M31 53V61M39 53H47M39 53V61M47 53H55M47 53V61M55 53H63M55 53V61M63 53H71M63 53V61M71 53H79M71 53V61M79 53H87M79 53V61M87 53H95M87 53V61M95 53H103M95 53V61M103 53H111M103 53V61M111 53H119M111 53V61M119 53H127M119 53V61M127 53V61M7 61H-1M7 61H15M7 61V69M-1 61V69M15 61H23M15 61V69M23 61H31M23 61V69M31 61H39M31 61V69M39 61H47M39 61V69M47 61H55M47 61V69M55 61H63M55 61V69M63 61H71M63 61V69M71 61H79M71 61V69M79 61H87M79 61V69M87 61H95M87 61V69M95 61H103M95 61V69M103 61H111M103 61V69M111 61H119M111 61V69M119 61H127M119 61V69M127 61V69M7 69H-1M7 69H15M7 69V77M-1 69V77H7M15 69H23M15 69V77M23 69H31M23 69V77M31 69H39M31 69V77M39 69H47M39 69V77M47 69H55M47 69V77M55 69H63M55 69V77M63 69H71M63 69V77M71 69H79M71 69V77M79 69H87M79 69V77M87 69H95M87 69V77M95 69H103M95 69V77M103 69H111M103 69V77M111 69H119M111 69V77M119 69H127M119 69V77M127 69V77H119M7 77H15M15 77H23M23 77H31M31 77H39M39 77H47M47 77H55M55 77H63M63 77H71M71 77H79M79 77H87M87 77H95M95 77H103M103 77H111M111 77H119" stroke="#1570EF" stroke-width="0.5"/>
</g>
</g>
<defs>
<linearGradient id="paint0_linear_3169_30051" x1="62.5" y1="0" x2="62.5" y2="74" gradientUnits="userSpaceOnUse">
<stop stop-color="#D9D9D9" stop-opacity="0.08"/>
<stop offset="1" stop-color="#737373" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@ -0,0 +1,315 @@
'use client'
import type { MouseEventHandler } from 'react'
import { useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import cn from 'classnames'
import { useRouter } from 'next/navigation'
import { useContext, useContextSelector } from 'use-context-selector'
import s from './style.module.css'
import AppsContext, { useAppContext } from '@/context/app-context'
import { useProviderContext } from '@/context/provider-context'
import { ToastContext } from '@/app/components/base/toast'
import type { AppMode } from '@/types/app'
import { createApp } from '@/service/apps'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import AppIcon from '@/app/components/base/app-icon'
import EmojiPicker from '@/app/components/base/emoji-picker'
import AppsFull from '@/app/components/billing/apps-full-in-dialog'
import { AiText, ChatBot, CuteRobote } from '@/app/components/base/icons/src/vender/solid/communication'
import { HelpCircle, XClose } from '@/app/components/base/icons/src/vender/line/general'
import { Route } from '@/app/components/base/icons/src/vender/solid/mapsAndTravel'
import TooltipPlus from '@/app/components/base/tooltip-plus'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { getRedirection } from '@/utils/app-redirection'
type CreateAppDialogProps = {
show: boolean
onSuccess: () => void
onClose: () => void
}
const CreateAppModal = ({ show, onSuccess, onClose }: CreateAppDialogProps) => {
const { t } = useTranslation()
const { push } = useRouter()
const { notify } = useContext(ToastContext)
const mutateApps = useContextSelector(AppsContext, state => state.mutateApps)
const [appMode, setAppMode] = useState<AppMode>('chat')
const [showChatBotType, setShowChatBotType] = useState<boolean>(true)
const [emoji, setEmoji] = useState({ icon: '🤖', icon_background: '#FFEAD5' })
const [showEmojiPicker, setShowEmojiPicker] = useState(false)
const [name, setName] = useState('')
const [description, setDescription] = useState('')
const { plan, enableBilling } = useProviderContext()
const isAppsFull = (enableBilling && plan.usage.buildApps >= plan.total.buildApps)
const { isCurrentWorkspaceManager } = useAppContext()
const isCreatingRef = useRef(false)
const onCreate: MouseEventHandler = useCallback(async () => {
if (!appMode) {
notify({ type: 'error', message: t('app.newApp.appTypeRequired') })
return
}
if (!name.trim()) {
notify({ type: 'error', message: t('app.newApp.nameNotEmpty') })
return
}
if (isCreatingRef.current)
return
isCreatingRef.current = true
try {
const app = await createApp({
name,
description,
icon: emoji.icon,
icon_background: emoji.icon_background,
mode: appMode,
})
notify({ type: 'success', message: t('app.newApp.appCreated') })
onSuccess()
onClose()
mutateApps()
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
getRedirection(isCurrentWorkspaceManager, app, push)
}
catch (e) {
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
}
isCreatingRef.current = false
}, [name, notify, t, appMode, emoji.icon, emoji.icon_background, description, onSuccess, onClose, mutateApps, push, isCurrentWorkspaceManager])
return (
<Modal
overflowVisible
wrapperClassName='z-20'
className='!p-0 !max-w-[720px] !w-[720px] rounded-xl'
isShow={show}
onClose={() => {}}
>
{/* Heading */}
<div className='shrink-0 flex flex-col h-full bg-white rounded-t-xl'>
<div className='shrink-0 pl-8 pr-6 pt-6 pb-3 bg-white text-xl rounded-t-xl leading-[30px] font-semibold text-gray-900 z-10'>{t('app.newApp.startFromBlank')}</div>
</div>
{/* app type */}
<div className='py-2 px-8'>
<div className='py-2 text-sm leading-[20px] font-medium text-gray-900'>{t('app.newApp.captionAppType')}</div>
<div className='flex'>
<TooltipPlus
hideArrow
popupContent={
<div className='max-w-[280px] leading-[18px] text-xs text-gray-700'>{t('app.newApp.chatbotDescription')}</div>
}
>
<div
className={cn(
'relative grow box-border w-[158px] mr-2 px-0.5 pt-3 pb-2 flex flex-col items-center justify-center gap-1 rounded-lg border border-gray-100 bg-white text-gray-700 cursor-pointer shadow-xs hover:border-gray-300',
showChatBotType && 'border-[1.5px] border-primary-400 hover:border-[1.5px] hover:border-primary-400',
s['grid-bg-chat'],
)}
onClick={() => {
setAppMode('chat')
setShowChatBotType(true)
}}
>
<ChatBot className='w-6 h-6 text-[#1570EF]' />
<div className='h-5 text-[13px] font-medium leading-[18px]'>{t('app.types.chatbot')}</div>
</div>
</TooltipPlus>
<TooltipPlus
hideArrow
popupContent={
<div className='flex flex-col max-w-[320px] leading-[18px] text-xs'>
<div className='text-gray-700'>{t('app.newApp.completionDescription')}</div>
</div>
}
>
<div
className={cn(
'relative grow box-border w-[158px] mr-2 px-0.5 pt-3 pb-2 flex flex-col items-center justify-center gap-1 rounded-lg border border-gray-100 text-gray-700 cursor-pointer bg-white shadow-xs hover:border-gray-300',
s['grid-bg-completion'],
appMode === 'completion' && 'border-[1.5px] border-primary-400 hover:border-[1.5px] hover:border-primary-400',
)}
onClick={() => {
setAppMode('completion')
setShowChatBotType(false)
}}
>
<AiText className='w-6 h-6 text-[#0E9384]' />
<div className='h-5 text-[13px] font-medium leading-[18px]'>{t('app.newApp.completeApp')}</div>
</div>
</TooltipPlus>
<TooltipPlus
hideArrow
popupContent={
<div className='max-w-[280px] leading-[18px] text-xs text-gray-700'>{t('app.newApp.agentDescription')}</div>
}
>
<div
className={cn(
'relative grow box-border w-[158px] mr-2 px-0.5 pt-3 pb-2 flex flex-col items-center justify-center gap-1 rounded-lg border border-gray-100 text-gray-700 cursor-pointer bg-white shadow-xs hover:border-gray-300',
s['grid-bg-agent-chat'],
appMode === 'agent-chat' && 'border-[1.5px] border-primary-400 hover:border-[1.5px] hover:border-primary-400',
)}
onClick={() => {
setAppMode('agent-chat')
setShowChatBotType(false)
}}
>
<CuteRobote className='w-6 h-6 text-indigo-600' />
<div className='h-5 text-[13px] font-medium leading-[18px]'>{t('app.types.agent')}</div>
</div>
</TooltipPlus>
<TooltipPlus
hideArrow
popupContent={
<div className='flex flex-col max-w-[320px] leading-[18px] text-xs'>
<div className='text-gray-700'>{t('app.newApp.workflowDescription')}</div>
</div>
}
>
<div
className={cn(
'relative grow box-border w-[158px] px-0.5 pt-3 pb-2 flex flex-col items-center justify-center gap-1 rounded-lg border border-gray-100 text-gray-700 cursor-pointer bg-white shadow-xs hover:border-gray-300',
s['grid-bg-workflow'],
appMode === 'workflow' && 'border-[1.5px] border-primary-400 hover:border-[1.5px] hover:border-primary-400',
)}
onClick={() => {
setAppMode('workflow')
setShowChatBotType(false)
}}
>
<Route className='w-6 h-6 text-[#f79009]' />
<div className='h-5 text-[13px] font-medium leading-[18px]'>{t('app.types.workflow')}</div>
<span className='absolute top-[-3px] right-[-3px] px-1 rounded-[5px] bg-white border border-black/8 text-gray-500 text-[10px] leading-[18px] font-medium'>BETA</span>
</div>
</TooltipPlus>
</div>
</div>
{showChatBotType && (
<div className='py-2 px-8'>
<div className='py-2 text-sm leading-[20px] font-medium text-gray-900'>{t('app.newApp.chatbotType')}</div>
<div className='flex gap-2'>
<div
className={cn(
'relative grow flex-[50%] pl-4 py-[10px] pr-[10px] rounded-lg border border-gray-100 bg-gray-25 text-gray-700 cursor-pointer hover:bg-white hover:shadow-xs hover:border-gray-300',
appMode === 'chat' && 'bg-white shadow-xs border-[1.5px] border-primary-400 hover:border-[1.5px] hover:border-primary-400',
)}
onClick={() => {
setAppMode('chat')
}}
>
<div className='flex items-center justify-between'>
<div className='h-5 text-sm font-medium leading-5'>{t('app.newApp.basic')}</div>
<div className='group'>
<HelpCircle className='w-[14px] h-[14px] text-gray-400 hover:text-gray-500' />
<div
className={cn(
'hidden z-20 absolute left-[327px] top-[-158px] w-[376px] rounded-xl bg-white border-[0.5px] border-[rgba(0,0,0,0.05)] shadow-lg group-hover:block',
)}
>
<div className={cn('w-full h-[256px] bg-center bg-no-repeat bg-contain rounded-xl', s.basicPic)}/>
<div className='px-4 pb-2'>
<div className='flex items-center justify-between'>
<div className='text-gray-700 text-md leading-6 font-semibold'>{t('app.newApp.basic')}</div>
<div className='text-orange-500 text-xs leading-[18px] font-medium'>{t('app.newApp.basicFor')}</div>
</div>
<div className='mt-1 text-gray-500 text-sm leading-5'>{t('app.newApp.basicDescription')}</div>
</div>
</div>
</div>
</div>
<div className='mt-[2px] text-gray-500 text-xs leading-[18px]'>{t('app.newApp.basicTip')}</div>
</div>
<div
className={cn(
'relative grow flex-[50%] pl-3 py-2 pr-2 rounded-lg border border-gray-100 bg-gray-25 text-gray-700 cursor-pointer hover:bg-white hover:shadow-xs hover:border-gray-300',
appMode === 'advanced-chat' && 'bg-white shadow-xs border-[1.5px] border-primary-400 hover:border-[1.5px] hover:border-primary-400',
)}
onClick={() => {
setAppMode('advanced-chat')
}}
>
<div className='flex items-center justify-between'>
<div className='flex items-center'>
<div className='mr-1 h-5 text-sm font-medium leading-5'>{t('app.newApp.advanced')}</div>
<span className='px-1 rounded-[5px] bg-white border border-black/8 text-gray-500 text-[10px] leading-[18px] font-medium'>BETA</span>
</div>
<div className='group'>
<HelpCircle className='w-[14px] h-[14px] text-gray-400 hover:text-gray-500' />
<div
className={cn(
'hidden z-20 absolute right-[26px] top-[-158px] w-[376px] rounded-xl bg-white border-[0.5px] border-[rgba(0,0,0,0.05)] shadow-lg group-hover:block',
)}
>
<div className={cn('w-full h-[256px] bg-center bg-no-repeat bg-contain rounded-xl', s.basicPic)}/>
<div className='px-4 pb-2'>
<div className='flex items-center justify-between'>
<div className='flex items-center'>
<div className='mr-1 text-gray-700 text-md leading-6 font-semibold'>{t('app.newApp.advanced')}</div>
<span className='px-1 rounded-[5px] bg-white border border-black/8 text-gray-500 text-[10px] leading-[18px] font-medium'>BETA</span>
</div>
<div className='text-orange-500 text-xs leading-[18px] font-medium'>{t('app.newApp.advancedFor').toLocaleUpperCase()}</div>
</div>
<div className='mt-1 text-gray-500 text-sm leading-5'>{t('app.newApp.advancedDescription')}</div>
</div>
</div>
</div>
</div>
<div className='mt-[2px] text-gray-500 text-xs leading-[18px]'>{t('app.newApp.advancedFor')}</div>
</div>
</div>
</div>
)}
{/* icon & name */}
<div className='pt-2 px-8'>
<div className='py-2 text-sm font-medium leading-[20px] text-gray-900'>{t('app.newApp.captionName')}</div>
<div className='flex items-center justify-between space-x-2'>
<AppIcon size='large' onClick={() => { setShowEmojiPicker(true) }} className='cursor-pointer' icon={emoji.icon} background={emoji.icon_background} />
<input
value={name}
onChange={e => setName(e.target.value)}
placeholder={t('app.newApp.appNamePlaceholder') || ''}
className='grow h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg border border-transparent outline-none appearance-none caret-primary-600 placeholder:text-gray-400 hover:bg-gray-50 hover:border hover:border-gray-300 focus:bg-gray-50 focus:border focus:border-gray-300 focus:shadow-xs'
/>
</div>
{showEmojiPicker && <EmojiPicker
onSelect={(icon, icon_background) => {
setEmoji({ icon, icon_background })
setShowEmojiPicker(false)
}}
onClose={() => {
setEmoji({ icon: '🤖', icon_background: '#FFEAD5' })
setShowEmojiPicker(false)
}}
/>}
</div>
{/* description */}
<div className='pt-2 px-8'>
<div className='py-2 text-sm font-medium leading-[20px] text-gray-900'>{t('app.newApp.captionDescription')}</div>
<textarea
className='w-full h-10 px-3 py-2 text-sm font-normal bg-gray-100 rounded-lg border border-transparent outline-none appearance-none caret-primary-600 placeholder:text-gray-400 hover:bg-gray-50 hover:border hover:border-gray-300 focus:bg-gray-50 focus:border focus:border-gray-300 focus:shadow-xs h-[80px] resize-none'
placeholder={t('app.newApp.appDescriptionPlaceholder') || ''}
value={description}
onChange={e => setDescription(e.target.value)}
/>
</div>
{isAppsFull && (
<div className='px-8 py-2'>
<AppsFull loc='app-create' />
</div>
)}
<div className='px-8 py-6 flex justify-end'>
<Button className='mr-2 text-gray-700 text-sm font-medium' onClick={onClose}>{t('app.newApp.Cancel')}</Button>
<Button className='text-sm font-medium' disabled={isAppsFull || !name} type="primary" onClick={onCreate}>{t('app.newApp.Create')}</Button>
</div>
<div className='absolute right-6 top-6 p-2 cursor-pointer z-20' onClick={onClose}>
<XClose className='w-4 h-4 text-gray-500' />
</div>
</Modal>
)
}
export default CreateAppModal

View File

@ -0,0 +1,23 @@
.grid-bg-chat {
background-image: url('./grid-bg-chat.svg');
background-repeat: repeat-x;
}
.grid-bg-completion {
background-image: url('./grid-bg-completion.svg');
background-repeat: repeat-x;
}
.grid-bg-agent-chat {
background-image: url('./grid-bg-agent-chat.svg');
background-repeat: repeat-x;
}
.grid-bg-workflow {
background-image: url('./grid-bg-workflow.svg');
background-repeat: repeat-x;
}
.basicPic {
background-image: url('./basic.png')
}
.advancedPic {
background-image: url('./advanced.png')
}

View File

@ -0,0 +1,103 @@
'use client'
import type { MouseEventHandler } from 'react'
import { useRef, useState } from 'react'
import { useRouter } from 'next/navigation'
import { useContext } from 'use-context-selector'
import { useTranslation } from 'react-i18next'
import Uploader from './uploader'
import Button from '@/app/components/base/button'
import Modal from '@/app/components/base/modal'
import { ToastContext } from '@/app/components/base/toast'
import { importApp } from '@/service/apps'
import { useAppContext } from '@/context/app-context'
import { useProviderContext } from '@/context/provider-context'
import AppsFull from '@/app/components/billing/apps-full-in-dialog'
import { XClose } from '@/app/components/base/icons/src/vender/line/general'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { getRedirection } from '@/utils/app-redirection'
type CreateFromDSLModalProps = {
show: boolean
onSuccess?: () => void
onClose: () => void
}
const CreateFromDSLModal = ({ show, onSuccess, onClose }: CreateFromDSLModalProps) => {
const { push } = useRouter()
const { t } = useTranslation()
const { notify } = useContext(ToastContext)
const [currentFile, setDSLFile] = useState<File>()
const [fileContent, setFileContent] = useState<string>()
const readFile = (file: File) => {
const reader = new FileReader()
reader.onload = function (event) {
const content = event.target?.result
setFileContent(content as string)
}
reader.readAsText(file)
}
const handleFile = (file?: File) => {
setDSLFile(file)
if (file)
readFile(file)
if (!file)
setFileContent('')
}
const { isCurrentWorkspaceManager } = useAppContext()
const { plan, enableBilling } = useProviderContext()
const isAppsFull = (enableBilling && plan.usage.buildApps >= plan.total.buildApps)
const isCreatingRef = useRef(false)
const onCreate: MouseEventHandler = async () => {
if (isCreatingRef.current)
return
isCreatingRef.current = true
if (!currentFile)
return
try {
const app = await importApp({
data: fileContent || '',
})
if (onSuccess)
onSuccess()
if (onClose)
onClose()
notify({ type: 'success', message: t('app.newApp.appCreated') })
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
getRedirection(isCurrentWorkspaceManager, app, push)
}
catch (e) {
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
}
isCreatingRef.current = false
}
return (
<Modal
wrapperClassName='z-20'
className='px-8 py-6 max-w-[520px] w-[520px] rounded-xl'
isShow={show}
onClose={() => {}}
>
<div className='relative pb-2 text-xl font-medium leading-[30px] text-gray-900'>{t('app.createFromConfigFile')}</div>
<div className='absolute right-4 top-4 p-2 cursor-pointer' onClick={onClose}>
<XClose className='w-4 h-4 text-gray-500' />
</div>
<Uploader
file={currentFile}
updateFile={handleFile}
/>
{isAppsFull && <AppsFull loc='app-create-dsl' />}
<div className='pt-6 flex justify-end'>
<Button className='mr-2 text-gray-700 text-sm font-medium' onClick={onClose}>{t('app.newApp.Cancel')}</Button>
<Button className='text-sm font-medium' disabled={isAppsFull || !currentFile} type="primary" onClick={onCreate}>{t('app.newApp.Create')}</Button>
</div>
</Modal>
)
}
export default CreateFromDSLModal

View File

@ -0,0 +1,126 @@
'use client'
import type { FC } from 'react'
import React, { useEffect, useRef, useState } from 'react'
import cn from 'classnames'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import { Yaml as YamlIcon } from '@/app/components/base/icons/src/public/files'
import { ToastContext } from '@/app/components/base/toast'
import { Trash03, UploadCloud01 } from '@/app/components/base/icons/src/vender/line/general'
import Button from '@/app/components/base/button'
export type Props = {
file: File | undefined
updateFile: (file?: File) => void
}
const Uploader: FC<Props> = ({
file,
updateFile,
}) => {
const { t } = useTranslation()
const { notify } = useContext(ToastContext)
const [dragging, setDragging] = useState(false)
const dropRef = useRef<HTMLDivElement>(null)
const dragRef = useRef<HTMLDivElement>(null)
const fileUploader = useRef<HTMLInputElement>(null)
const handleDragEnter = (e: DragEvent) => {
e.preventDefault()
e.stopPropagation()
e.target !== dragRef.current && setDragging(true)
}
const handleDragOver = (e: DragEvent) => {
e.preventDefault()
e.stopPropagation()
}
const handleDragLeave = (e: DragEvent) => {
e.preventDefault()
e.stopPropagation()
e.target === dragRef.current && setDragging(false)
}
const handleDrop = (e: DragEvent) => {
e.preventDefault()
e.stopPropagation()
setDragging(false)
if (!e.dataTransfer)
return
const files = [...e.dataTransfer.files]
if (files.length > 1) {
notify({ type: 'error', message: t('datasetCreation.stepOne.uploader.validation.count') })
return
}
updateFile(files[0])
}
const selectHandle = () => {
if (fileUploader.current)
fileUploader.current.click()
}
const removeFile = () => {
if (fileUploader.current)
fileUploader.current.value = ''
updateFile()
}
const fileChangeHandle = (e: React.ChangeEvent<HTMLInputElement>) => {
const currentFile = e.target.files?.[0]
updateFile(currentFile)
}
useEffect(() => {
dropRef.current?.addEventListener('dragenter', handleDragEnter)
dropRef.current?.addEventListener('dragover', handleDragOver)
dropRef.current?.addEventListener('dragleave', handleDragLeave)
dropRef.current?.addEventListener('drop', handleDrop)
return () => {
dropRef.current?.removeEventListener('dragenter', handleDragEnter)
dropRef.current?.removeEventListener('dragover', handleDragOver)
dropRef.current?.removeEventListener('dragleave', handleDragLeave)
dropRef.current?.removeEventListener('drop', handleDrop)
}
}, [])
return (
<div className='mt-6'>
<input
ref={fileUploader}
style={{ display: 'none' }}
type="file"
id="fileUploader"
accept='.yml'
onChange={fileChangeHandle}
/>
<div ref={dropRef}>
{!file && (
<div className={cn('flex items-center h-20 rounded-xl bg-gray-50 border border-dashed border-gray-200 text-sm font-normal', dragging && 'bg-[#F5F8FF] border border-[#B2CCFF]')}>
<div className='w-full flex items-center justify-center space-x-2'>
<UploadCloud01 className='w-6 h-6 mr-2'/>
<div className='text-gray-500'>
{t('datasetCreation.stepOne.uploader.button')}
<span className='pl-1 text-[#155eef] cursor-pointer' onClick={selectHandle}>{t('datasetDocuments.list.batchModal.browse')}</span>
</div>
</div>
{dragging && <div ref={dragRef} className='absolute w-full h-full top-0 left-0'/>}
</div>
)}
{file && (
<div className={cn('flex items-center h-20 px-6 rounded-xl bg-gray-50 border border-gray-200 text-sm font-normal group', 'hover:bg-[#F5F8FF] hover:border-[#B2CCFF]')}>
<YamlIcon className="shrink-0" />
<div className='flex ml-2 w-0 grow'>
<span className='max-w-[calc(100%_-_30px)] text-ellipsis whitespace-nowrap overflow-hidden text-gray-800'>{file.name.replace(/(.yaml|.yml)$/, '')}</span>
<span className='shrink-0 text-gray-500'>.yml</span>
</div>
<div className='hidden group-hover:flex items-center'>
<Button className='!h-8 !px-3 !py-[6px] bg-white !text-[13px] !leading-[18px] text-gray-700' onClick={selectHandle}>{t('datasetCreation.stepOne.uploader.change')}</Button>
<div className='mx-2 w-px h-4 bg-gray-200' />
<div className='p-2 cursor-pointer' onClick={removeFile}>
<Trash03 className='w-4 h-4 text-gray-500' />
</div>
</div>
</div>
)}
</div>
</div>
)
}
export default React.memo(Uploader)

View File

@ -0,0 +1,97 @@
'use client'
import React, { useState } from 'react'
import cn from 'classnames'
import { useTranslation } from 'react-i18next'
import s from './style.module.css'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import Toast from '@/app/components/base/toast'
import AppIcon from '@/app/components/base/app-icon'
import EmojiPicker from '@/app/components/base/emoji-picker'
import { useProviderContext } from '@/context/provider-context'
import AppsFull from '@/app/components/billing/apps-full-in-dialog'
export type DuplicateAppModalProps = {
appName: string
icon: string
icon_background: string
show: boolean
onConfirm: (info: {
name: string
icon: string
icon_background: string
}) => Promise<void>
onHide: () => void
}
const DuplicateAppModal = ({
appName,
icon,
icon_background,
show = false,
onConfirm,
onHide,
}: DuplicateAppModalProps) => {
const { t } = useTranslation()
const [name, setName] = React.useState(appName)
const [showEmojiPicker, setShowEmojiPicker] = useState(false)
const [emoji, setEmoji] = useState({ icon, icon_background })
const { plan, enableBilling } = useProviderContext()
const isAppsFull = (enableBilling && plan.usage.buildApps >= plan.total.buildApps)
const submit = () => {
if (!name.trim()) {
Toast.notify({ type: 'error', message: t('explore.appCustomize.nameRequired') })
return
}
onConfirm({
name,
...emoji,
})
onHide()
}
return (
<>
<Modal
isShow={show}
onClose={() => { }}
className={cn(s.modal, '!max-w-[480px]', 'px-8')}
>
<span className={s.close} onClick={onHide} />
<div className={s.title}>{t('app.duplicateTitle')}</div>
<div className={s.content}>
<div className={s.subTitle}>{t('explore.appCustomize.subTitle')}</div>
<div className='flex items-center justify-between space-x-2'>
<AppIcon size='large' onClick={() => { setShowEmojiPicker(true) }} className='cursor-pointer' icon={emoji.icon} background={emoji.icon_background} />
<input
value={name}
onChange={e => setName(e.target.value)}
className='h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg grow'
/>
</div>
{isAppsFull && <AppsFull loc='app-duplicate-create' />}
</div>
<div className='flex flex-row-reverse'>
<Button disabled={isAppsFull} className='w-24 ml-2' type='primary' onClick={submit}>{t('app.duplicate')}</Button>
<Button className='w-24' onClick={onHide}>{t('common.operation.cancel')}</Button>
</div>
</Modal>
{showEmojiPicker && <EmojiPicker
onSelect={(icon, icon_background) => {
setEmoji({ icon, icon_background })
setShowEmojiPicker(false)
}}
onClose={() => {
setEmoji({ icon, icon_background })
setShowEmojiPicker(false)
}}
/>}
</>
)
}
export default DuplicateAppModal

View File

@ -1,43 +1,57 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import cn from 'classnames'
import { useTranslation } from 'react-i18next'
import { useRouter } from 'next/navigation'
import Log from '@/app/components/app/log'
import WorkflowLog from '@/app/components/app/workflow-log'
import Annotation from '@/app/components/app/annotation'
import Loading from '@/app/components/base/loading'
import { PageType } from '@/app/components/app/configuration/toolbox/annotation/type'
import TabSlider from '@/app/components/base/tab-slider-plain'
import { useStore as useAppStore } from '@/app/components/app/store'
type Props = {
pageType: PageType
appId: string
}
const LogAnnotation: FC<Props> = ({
pageType,
appId,
}) => {
const { t } = useTranslation()
const router = useRouter()
const { appDetail } = useAppStore()
const options = [
{ value: PageType.log, text: t('appLog.title') },
{ value: PageType.annotation, text: t('appAnnotation.title') },
]
if (!appDetail) {
return (
<div className='flex h-full items-center justify-center bg-white'>
<Loading />
</div>
)
}
return (
<div className='pt-4 px-6 h-full flex flex-col'>
<TabSlider
className='shrink-0'
value={pageType}
onChange={(value) => {
router.push(`/app/${appId}/${value === PageType.log ? 'logs' : 'annotations'}`)
}}
options={options}
/>
<div className='mt-3 grow'>
{pageType === PageType.log && (<Log appId={appId} />)}
{pageType === PageType.annotation && (<Annotation appId={appId} />)}
{appDetail.mode !== 'workflow' && (
<TabSlider
className='shrink-0'
value={pageType}
onChange={(value) => {
router.push(`/app/${appDetail.id}/${value === PageType.log ? 'logs' : 'annotations'}`)
}}
options={options}
/>
)}
<div className={cn('grow', appDetail.mode !== 'workflow' && 'mt-3')}>
{pageType === PageType.log && appDetail.mode !== 'workflow' && (<Log appDetail={appDetail} />)}
{pageType === PageType.annotation && (<Annotation appDetail={appDetail} />)}
{pageType === PageType.log && appDetail.mode === 'workflow' && (<WorkflowLog appDetail={appDetail} />)}
</div>
</div>
)

View File

@ -14,10 +14,10 @@ import Filter from './filter'
import s from './style.module.css'
import Loading from '@/app/components/base/loading'
import { fetchChatConversations, fetchCompletionConversations } from '@/service/log'
import { fetchAppDetail } from '@/service/apps'
import { APP_PAGE_LIMIT } from '@/config'
import type { App, AppMode } from '@/types/app'
export type ILogsProps = {
appId: string
appDetail: App
}
export type QueryParam = {
@ -50,7 +50,7 @@ const EmptyElement: FC<{ appUrl: string }> = ({ appUrl }) => {
</div>
}
const Logs: FC<ILogsProps> = ({ appId }) => {
const Logs: FC<ILogsProps> = ({ appDetail }) => {
const { t } = useTranslation()
const [queryParams, setQueryParams] = useState<QueryParam>({ period: 7, annotation_status: 'all' })
const [currPage, setCurrPage] = React.useState<number>(0)
@ -67,21 +67,26 @@ const Logs: FC<ILogsProps> = ({ appId }) => {
...omit(queryParams, ['period']),
}
const getWebAppType = (appType: AppMode) => {
if (appType !== 'completion' && appType !== 'workflow')
return 'chat'
return appType
}
// Get the app type first
const { data: appDetail } = useSWR({ url: '/apps', id: appId }, fetchAppDetail)
const isChatMode = appDetail?.mode === 'chat'
const isChatMode = appDetail.mode !== 'completion'
// When the details are obtained, proceed to the next request
const { data: chatConversations, mutate: mutateChatList } = useSWR(() => isChatMode
? {
url: `/apps/${appId}/chat-conversations`,
url: `/apps/${appDetail.id}/chat-conversations`,
params: query,
}
: null, fetchChatConversations)
const { data: completionConversations, mutate: mutateCompletionList } = useSWR(() => !isChatMode
? {
url: `/apps/${appId}/completion-conversations`,
url: `/apps/${appDetail.id}/completion-conversations`,
params: query,
}
: null, fetchCompletionConversations)
@ -92,12 +97,12 @@ const Logs: FC<ILogsProps> = ({ appId }) => {
<div className='flex flex-col h-full'>
<p className='flex text-sm font-normal text-gray-500'>{t('appLog.description')}</p>
<div className='flex flex-col py-4 flex-1'>
<Filter appId={appId} queryParams={queryParams} setQueryParams={setQueryParams} />
<Filter appId={appDetail.id} queryParams={queryParams} setQueryParams={setQueryParams} />
{total === undefined
? <Loading type='app' />
: total > 0
? <List logs={isChatMode ? chatConversations : completionConversations} appDetail={appDetail} onRefresh={isChatMode ? mutateChatList : mutateCompletionList} />
: <EmptyElement appUrl={`${appDetail?.site.app_base_url}/${appDetail?.mode}/${appDetail?.site.access_token}`} />
: <EmptyElement appUrl={`${appDetail.site.app_base_url}/${getWebAppType(appDetail.mode)}/${appDetail.site.access_token}`} />
}
{/* Show Pagination only if the total is more than the limit */}
{(total && total > APP_PAGE_LIMIT)

View File

@ -1,6 +1,6 @@
'use client'
import type { FC } from 'react'
import React, { useEffect, useState } from 'react'
import React, { useEffect, useRef, useState } from 'react'
import useSWR from 'swr'
import {
HandThumbDownIcon,
@ -35,10 +35,13 @@ import ModelName from '@/app/components/header/account-setting/model-provider-pa
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import TextGeneration from '@/app/components/app/text-generate/item'
import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils'
import PromptLogModal from '@/app/components/base/prompt-log-modal'
import MessageLogModal from '@/app/components/base/message-log-modal'
import { useStore as useAppStore } from '@/app/components/app/store'
type IConversationList = {
logs?: ChatConversationsResponse | CompletionConversationsResponse
appDetail?: App
appDetail: App
onRefresh: () => void
}
@ -80,7 +83,6 @@ const getFormattedChatList = (messages: ChatMessage[]) => {
id: `question-${item.id}`,
content: item.inputs.query || item.inputs.default_input || item.query, // text generation: item.inputs.query; chat: item.query
isAnswer: false,
log: item.message as any,
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'user') || [],
})
newChatList.push({
@ -92,6 +94,19 @@ const getFormattedChatList = (messages: ChatMessage[]) => {
feedbackDisabled: false,
isAnswer: true,
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
log: [
...item.message,
...(item.message[item.message.length - 1]?.role !== 'assistant'
? [
{
role: 'assistant',
text: item.answer,
files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
},
]
: []),
],
workflow_run_id: item.workflow_run_id,
more: {
time: dayjs.unix(item.created_at).format('hh:mm A'),
tokens: item.answer_tokens + item.message_tokens,
@ -133,6 +148,7 @@ type IDetailPanel<T> = {
function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionConversationFullDetailResponse>({ detail, onFeedback }: IDetailPanel<T>) {
const { onClose, appDetail } = useContext(DrawerContext)
const { currentLogItem, setCurrentLogItem, showPromptLogModal, setShowPromptLogModal, showMessageLogModal, setShowMessageLogModal } = useAppStore()
const { t } = useTranslation()
const [items, setItems] = React.useState<IChatItem[]>([])
const [hasMore, setHasMore] = useState(true)
@ -175,11 +191,12 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
}
useEffect(() => {
if (appDetail?.id && detail.id && appDetail?.mode === 'chat')
if (appDetail?.id && detail.id && appDetail?.mode !== 'completion')
fetchData()
}, [appDetail?.id, detail.id, appDetail?.mode])
const isChatMode = appDetail?.mode === 'chat'
const isChatMode = appDetail?.mode !== 'completion'
const isAdvanced = appDetail?.mode === 'advanced-chat'
const targetTone = TONE_LIST.find((item: any) => {
let res = true
@ -189,21 +206,21 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
return res
})?.name ?? 'custom'
const modelName = (detail.model_config as any).model.name
const provideName = (detail.model_config as any).model.provider as any
const modelName = (detail.model_config as any).model?.name
const provideName = (detail.model_config as any).model?.provider as any
const {
currentModel,
currentProvider,
} = useTextGenerationCurrentProviderAndModelAndModelList(
{ provider: provideName, model: modelName },
)
const varList = (detail.model_config as any).user_input_form.map((item: any) => {
const varList = (detail.model_config as any).user_input_form?.map((item: any) => {
const itemContent = item[Object.keys(item)[0]]
return {
label: itemContent.variable,
value: varValues[itemContent.variable] || detail.message?.inputs?.[itemContent.variable],
}
})
}) || []
const message_files = (!isChatMode && detail.message.message_files && detail.message.message_files.length > 0)
? detail.message.message_files.map((item: any) => item.url)
: []
@ -220,144 +237,182 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
return value
}
return (<div className='rounded-xl border-[0.5px] border-gray-200 h-full flex flex-col overflow-auto'>
{/* Panel Header */}
<div className='border-b border-gray-100 py-4 px-6 flex items-center justify-between'>
<div>
<div className='text-gray-500 text-[10px] leading-[14px]'>{isChatMode ? t('appLog.detail.conversationId') : t('appLog.detail.time')}</div>
<div className='text-gray-700 text-[13px] leading-[18px]'>{isChatMode ? detail.id?.split('-').slice(-1)[0] : dayjs.unix(detail.created_at).format(t('appLog.dateTimeFormat') as string)}</div>
</div>
<div className='flex items-center flex-wrap gap-y-1 justify-end'>
<div
className={cn('mr-2 flex items-center border h-8 px-2 space-x-2 rounded-lg bg-indigo-25 border-[#2A87F5]')}
>
<ModelIcon
className='!w-5 !h-5'
provider={currentProvider}
modelName={currentModel?.model}
/>
<ModelName
modelItem={currentModel!}
showMode
/>
const [width, setWidth] = useState(0)
const ref = useRef<HTMLDivElement>(null)
const adjustModalWidth = () => {
if (ref.current)
setWidth(document.body.clientWidth - (ref.current?.clientWidth + 16) - 8)
}
useEffect(() => {
adjustModalWidth()
}, [])
return (
<div ref={ref} className='rounded-xl border-[0.5px] border-gray-200 h-full flex flex-col overflow-auto'>
{/* Panel Header */}
<div className='border-b border-gray-100 py-4 px-6 flex items-center justify-between'>
<div>
<div className='text-gray-500 text-[10px] leading-[14px]'>{isChatMode ? t('appLog.detail.conversationId') : t('appLog.detail.time')}</div>
<div className='text-gray-700 text-[13px] leading-[18px]'>{isChatMode ? detail.id?.split('-').slice(-1)[0] : dayjs.unix(detail.created_at).format(t('appLog.dateTimeFormat') as string)}</div>
</div>
<Popover
position='br'
className='!w-[280px]'
btnClassName='mr-4 !bg-gray-50 !py-1.5 !px-2.5 border-none font-normal'
btnElement={<>
<span className='text-[13px]'>{targetTone}</span>
<InformationCircleIcon className='h-4 w-4 text-gray-800 ml-1.5' />
</>}
htmlContent={<div className='w-[280px]'>
<div className='flex justify-between py-2 px-4 font-medium text-sm text-gray-700'>
<span>Tone of responses</span>
<div>{targetTone}</div>
</div>
{['temperature', 'top_p', 'presence_penalty', 'max_tokens', 'stop'].map((param: string, index: number) => {
return <div className='flex justify-between py-2 px-4 bg-gray-50' key={index}>
<span className='text-xs text-gray-700'>{PARAM_MAP[param as keyof typeof PARAM_MAP]}</span>
<span className='text-gray-800 font-medium text-xs'>{getParamValue(param)}</span>
<div className='flex items-center flex-wrap gap-y-1 justify-end'>
{!isAdvanced && (
<>
<div
className={cn('mr-2 flex items-center border h-8 px-2 space-x-2 rounded-lg bg-indigo-25 border-[#2A87F5]')}
>
<ModelIcon
className='!w-5 !h-5'
provider={currentProvider}
modelName={currentModel?.model}
/>
<ModelName
modelItem={currentModel!}
showMode
/>
</div>
})}
</div>}
/>
<div className='w-6 h-6 rounded-lg flex items-center justify-center hover:cursor-pointer hover:bg-gray-100'>
<XMarkIcon className='w-4 h-4 text-gray-500' onClick={onClose} />
<Popover
position='br'
className='!w-[280px]'
btnClassName='mr-4 !bg-gray-50 !py-1.5 !px-2.5 border-none font-normal'
btnElement={<>
<span className='text-[13px]'>{targetTone}</span>
<InformationCircleIcon className='h-4 w-4 text-gray-800 ml-1.5' />
</>}
htmlContent={<div className='w-[280px]'>
<div className='flex justify-between py-2 px-4 font-medium text-sm text-gray-700'>
<span>Tone of responses</span>
<div>{targetTone}</div>
</div>
{['temperature', 'top_p', 'presence_penalty', 'max_tokens', 'stop'].map((param: string, index: number) => {
return <div className='flex justify-between py-2 px-4 bg-gray-50' key={index}>
<span className='text-xs text-gray-700'>{PARAM_MAP[param as keyof typeof PARAM_MAP]}</span>
<span className='text-gray-800 font-medium text-xs'>{getParamValue(param)}</span>
</div>
})}
</div>}
/>
</>
)}
<div className='w-6 h-6 rounded-lg flex items-center justify-center hover:cursor-pointer hover:bg-gray-100'>
<XMarkIcon className='w-4 h-4 text-gray-500' onClick={onClose} />
</div>
</div>
</div>
</div>
{/* Panel Body */}
{(varList.length > 0 || (!isChatMode && message_files.length > 0)) && (
<div className='px-6 pt-4 pb-2'>
<VarPanel
varList={varList}
message_files={message_files}
/>
</div>
)}
{!isChatMode
? <div className="px-6 py-4">
<div className='flex h-[18px] items-center space-x-3'>
<div className='leading-[18px] text-xs font-semibold text-gray-500 uppercase'>{t('appLog.table.header.output')}</div>
<div className='grow h-[1px]' style={{
background: 'linear-gradient(270deg, rgba(243, 244, 246, 0) 0%, rgb(243, 244, 246) 100%)',
}}></div>
{/* Panel Body */}
{(varList.length > 0 || (!isChatMode && message_files.length > 0)) && (
<div className='px-6 pt-4 pb-2'>
<VarPanel
varList={varList}
message_files={message_files}
/>
</div>
<TextGeneration
className='mt-2'
content={detail.message.answer}
messageId={detail.message.id}
isError={false}
onRetry={() => { }}
isInstalledApp={false}
supportFeedback
feedback={detail.message.feedbacks.find((item: any) => item.from_source === 'admin')}
onFeedback={feedback => onFeedback(detail.message.id, feedback)}
supportAnnotation
isShowTextToSpeech
appId={appDetail?.id}
varList={varList}
/>
</div>
: items.length < 8
? <div className="px-2.5 pt-4 mb-4">
<Chat
chatList={items}
isHideSendInput={true}
onFeedback={onFeedback}
displayScene='console'
isShowPromptLog
)}
{!isChatMode
? <div className="px-6 py-4">
<div className='flex h-[18px] items-center space-x-3'>
<div className='leading-[18px] text-xs font-semibold text-gray-500 uppercase'>{t('appLog.table.header.output')}</div>
<div className='grow h-[1px]' style={{
background: 'linear-gradient(270deg, rgba(243, 244, 246, 0) 0%, rgb(243, 244, 246) 100%)',
}}></div>
</div>
<TextGeneration
className='mt-2'
content={detail.message.answer}
messageId={detail.message.id}
isError={false}
onRetry={() => { }}
isInstalledApp={false}
supportFeedback
feedback={detail.message.feedbacks.find((item: any) => item.from_source === 'admin')}
onFeedback={feedback => onFeedback(detail.message.id, feedback)}
supportAnnotation
isShowTextToSpeech
appId={appDetail?.id}
onChatListChange={setItems}
varList={varList}
/>
</div>
: <div
className="px-2.5 py-4"
id="scrollableDiv"
style={{
height: 1000, // Specify a value
overflow: 'auto',
display: 'flex',
flexDirection: 'column-reverse',
}}>
{/* Put the scroll bar always on the bottom */}
<InfiniteScroll
scrollableTarget="scrollableDiv"
dataLength={items.length}
next={fetchData}
hasMore={hasMore}
loader={<div className='text-center text-gray-400 text-xs'>{t('appLog.detail.loading')}...</div>}
// endMessage={<div className='text-center'>Nothing more to show</div>}
// below props only if you need pull down functionality
refreshFunction={fetchData}
pullDownToRefresh
pullDownToRefreshThreshold={50}
// pullDownToRefreshContent={
// <div className='text-center'>Pull down to refresh</div>
// }
// releaseToRefreshContent={
// <div className='text-center'>Release to refresh</div>
// }
// To put endMessage and loader to the top.
style={{ display: 'flex', flexDirection: 'column-reverse' }}
inverse={true}
>
: items.length < 8
? <div className="px-2.5 pt-4 mb-4">
<Chat
chatList={items}
isHideSendInput={true}
onFeedback={onFeedback}
displayScene='console'
isShowPromptLog
supportAnnotation
isShowTextToSpeech
appId={appDetail?.id}
onChatListChange={setItems}
/>
</InfiniteScroll>
</div>
}
</div>)
</div>
: <div
className="px-2.5 py-4"
id="scrollableDiv"
style={{
height: 1000, // Specify a value
overflow: 'auto',
display: 'flex',
flexDirection: 'column-reverse',
}}>
{/* Put the scroll bar always on the bottom */}
<InfiniteScroll
scrollableTarget="scrollableDiv"
dataLength={items.length}
next={fetchData}
hasMore={hasMore}
loader={<div className='text-center text-gray-400 text-xs'>{t('appLog.detail.loading')}...</div>}
// endMessage={<div className='text-center'>Nothing more to show</div>}
// below props only if you need pull down functionality
refreshFunction={fetchData}
pullDownToRefresh
pullDownToRefreshThreshold={50}
// pullDownToRefreshContent={
// <div className='text-center'>Pull down to refresh</div>
// }
// releaseToRefreshContent={
// <div className='text-center'>Release to refresh</div>
// }
// To put endMessage and loader to the top.
style={{ display: 'flex', flexDirection: 'column-reverse' }}
inverse={true}
>
<Chat
chatList={items}
isHideSendInput={true}
onFeedback={onFeedback}
displayScene='console'
isShowPromptLog
/>
</InfiniteScroll>
</div>
}
{showPromptLogModal && (
<PromptLogModal
width={width}
currentLogItem={currentLogItem}
onCancel={() => {
setCurrentLogItem()
setShowPromptLogModal(false)
}}
/>
)}
{showMessageLogModal && (
<MessageLogModal
width={width}
currentLogItem={currentLogItem}
onCancel={() => {
setCurrentLogItem()
setShowMessageLogModal(false)
}}
/>
)}
</div>
)
}
/**
@ -460,7 +515,7 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh })
const [showDrawer, setShowDrawer] = useState<boolean>(false) // Whether to display the chat details drawer
const [currentConversation, setCurrentConversation] = useState<ChatConversationGeneralDetail | CompletionConversationGeneralDetail | undefined>() // Currently selected conversation
const isChatMode = appDetail?.mode === 'chat' // Whether the app is a chat app
const isChatMode = appDetail.mode !== 'completion' // Whether the app is a chat app
// Annotated data needs to be highlighted
const renderTdValue = (value: string | number | null, isEmptyStyle: boolean, isHighlight = false, annotation?: LogAnnotation) => {
@ -559,8 +614,8 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh })
appDetail,
}}>
{isChatMode
? <ChatConversationDetailComp appId={appDetail?.id} conversationId={currentConversation?.id} />
: <CompletionConversationDetailComp appId={appDetail?.id} conversationId={currentConversation?.id} />
? <ChatConversationDetailComp appId={appDetail.id} conversationId={currentConversation?.id} />
: <CompletionConversationDetailComp appId={appDetail.id} conversationId={currentConversation?.id} />
}
</DrawerContext.Provider>
</Drawer>

View File

@ -25,7 +25,6 @@ import CopyFeedback from '@/app/components/base/copy-feedback'
import ShareQRCode from '@/app/components/base/qrcode'
import SecretKeyButton from '@/app/components/develop/secret-key/secret-key-button'
import type { AppDetailResponse } from '@/models/app'
import { AppType } from '@/types/app'
import { useAppContext } from '@/context/app-context'
export type IAppCardProps = {
@ -69,7 +68,7 @@ function AppCard({
api: [{ opName: t('appOverview.overview.apiInfo.doc'), opIcon: DocumentTextIcon }],
app: [],
}
if (appInfo.mode === AppType.chat)
if (appInfo.mode !== 'completion' && appInfo.mode !== 'workflow')
operationsMap.webapp.push({ opName: t('appOverview.overview.appInfo.embedded.entry'), opIcon: EmbedIcon })
if (isCurrentWorkspaceManager)
@ -84,7 +83,8 @@ function AppCard({
: t('appOverview.overview.apiInfo.title')
const runningStatus = isApp ? appInfo.enable_site : appInfo.enable_api
const { app_base_url, access_token } = appInfo.site ?? {}
const appUrl = `${app_base_url}/${appInfo.mode}/${access_token}`
const appMode = (appInfo.mode !== 'completion' && appInfo.mode !== 'workflow') ? 'chat' : appInfo.mode
const appUrl = `${app_base_url}/${appMode}/${access_token}`
const apiUrl = appInfo?.api_base_url
let bgColor = 'bg-primary-50 bg-opacity-40'

View File

@ -11,7 +11,7 @@ import { formatNumber } from '@/utils/format'
import Basic from '@/app/components/app-sidebar/basic'
import Loading from '@/app/components/base/loading'
import type { AppDailyConversationsResponse, AppDailyEndUsersResponse, AppTokenCostsResponse } from '@/models/app'
import { getAppDailyConversations, getAppDailyEndUsers, getAppStatistics, getAppTokenCosts } from '@/service/apps'
import { getAppDailyConversations, getAppDailyEndUsers, getAppStatistics, getAppTokenCosts, getWorkflowDailyConversations } from '@/service/apps'
const valueFormatter = (v: string | number) => v
const COLOR_TYPE_MAP = {
@ -36,7 +36,7 @@ const COMMON_COLOR_MAP = {
}
type IColorType = 'green' | 'orange' | 'blue'
type IChartType = 'conversations' | 'endUsers' | 'costs'
type IChartType = 'conversations' | 'endUsers' | 'costs' | 'workflowCosts'
type IChartConfigType = { colorType: IColorType; showTokens?: boolean }
const commonDateFormat = 'MMM D, YYYY'
@ -52,6 +52,9 @@ const CHART_TYPE_CONFIG: Record<string, IChartConfigType> = {
colorType: 'blue',
showTokens: true,
},
workflowCosts: {
colorType: 'blue',
},
}
const sum = (arr: number[]): number => {
@ -366,4 +369,65 @@ export const CostChart: FC<IBizChartProps> = ({ id, period }) => {
/>
}
export const WorkflowMessagesChart: FC<IBizChartProps> = ({ id, period }) => {
const { t } = useTranslation()
const { data: response } = useSWR({ url: `/apps/${id}/workflow/statistics/daily-conversations`, params: period.query }, getWorkflowDailyConversations)
if (!response)
return <Loading />
const noDataFlag = !response.data || response.data.length === 0
return <Chart
basicInfo={{ title: t('appOverview.analysis.totalMessages.title'), explanation: t('appOverview.analysis.totalMessages.explanation'), timePeriod: period.name }}
chartData={!noDataFlag ? response : { data: getDefaultChartData({ ...(period.query ?? defaultPeriod), key: 'runs' }) }}
chartType='conversations'
valueKey='runs'
{...(noDataFlag && { yMax: 500 })}
/>
}
export const WorkflowDailyTerminalsChart: FC<IBizChartProps> = ({ id, period }) => {
const { t } = useTranslation()
const { data: response } = useSWR({ url: `/apps/${id}/workflow/statistics/daily-terminals`, id, params: period.query }, getAppDailyEndUsers)
if (!response)
return <Loading />
const noDataFlag = !response.data || response.data.length === 0
return <Chart
basicInfo={{ title: t('appOverview.analysis.activeUsers.title'), explanation: t('appOverview.analysis.activeUsers.explanation'), timePeriod: period.name }}
chartData={!noDataFlag ? response : { data: getDefaultChartData(period.query ?? defaultPeriod) }}
chartType='endUsers'
{...(noDataFlag && { yMax: 500 })}
/>
}
export const WorkflowCostChart: FC<IBizChartProps> = ({ id, period }) => {
const { t } = useTranslation()
const { data: response } = useSWR({ url: `/apps/${id}/workflow/statistics/token-costs`, params: period.query }, getAppTokenCosts)
if (!response)
return <Loading />
const noDataFlag = !response.data || response.data.length === 0
return <Chart
basicInfo={{ title: t('appOverview.analysis.tokenUsage.title'), explanation: t('appOverview.analysis.tokenUsage.explanation'), timePeriod: period.name }}
chartData={!noDataFlag ? response : { data: getDefaultChartData(period.query ?? defaultPeriod) }}
chartType='workflowCosts'
{...(noDataFlag && { yMax: 100 })}
/>
}
export const AvgUserInteractions: FC<IBizChartProps> = ({ id, period }) => {
const { t } = useTranslation()
const { data: response } = useSWR({ url: `/apps/${id}/workflow/statistics/average-app-interactions`, params: period.query }, getAppStatistics)
if (!response)
return <Loading />
const noDataFlag = !response.data || response.data.length === 0
return <Chart
basicInfo={{ title: t('appOverview.analysis.avgUserInteractions.title'), explanation: t('appOverview.analysis.avgUserInteractions.explanation'), timePeriod: period.name }}
chartData={!noDataFlag ? response : { data: getDefaultChartData({ ...(period.query ?? defaultPeriod), key: 'interactions' }) } as any}
chartType='conversations'
valueKey='interactions'
isAvg
{...(noDataFlag && { yMax: 500 })}
/>
}
export default Chart

View File

@ -14,6 +14,7 @@ type Props = {
onClose: () => void
accessToken: string
appBaseUrl: string
className?: string
}
const OPTION_MAP = {
@ -22,19 +23,19 @@ const OPTION_MAP = {
`<iframe
src="${url}/chatbot/${token}"
style="width: 100%; height: 100%; min-height: 700px"
frameborder="0"
frameborder="0"
allow="microphone">
</iframe>`,
},
scripts: {
getContent: (url: string, token: string, isTestEnv?: boolean) =>
`<script>
window.difyChatbotConfig = {
window.difyChatbotConfig = {
token: '${token}'${isTestEnv
? `,
? `,
isDev: true`
: ''}${IS_CE_EDITION
? `,
? `,
baseUrl: '${url}'`
: ''}
}
@ -59,7 +60,7 @@ type OptionStatus = {
chromePlugin: boolean
}
const Embedded = ({ isShow, onClose, appBaseUrl, accessToken }: Props) => {
const Embedded = ({ isShow, onClose, appBaseUrl, accessToken, className }: Props) => {
const { t } = useTranslation()
const [option, setOption] = useState<Option>('iframe')
const [isCopied, setIsCopied] = useState<OptionStatus>({ iframe: false, scripts: false, chromePlugin: false })
@ -101,12 +102,13 @@ const Embedded = ({ isShow, onClose, appBaseUrl, accessToken }: Props) => {
isShow={isShow}
onClose={onClose}
className="!max-w-2xl w-[640px]"
wrapperClassName={className}
closable={true}
>
<div className="mb-4 mt-8 text-gray-900 text-[14px] font-medium leading-tight">
{t(`${prefixEmbedded}.explanation`)}
</div>
<div className="flex items-center justify-between flex-wrap gap-y-2">
<div className="flex flex-wrap items-center justify-between gap-y-2">
{Object.keys(OPTION_MAP).map((v, index) => {
return (
<div
@ -125,7 +127,7 @@ const Embedded = ({ isShow, onClose, appBaseUrl, accessToken }: Props) => {
})}
</div>
{option === 'chromePlugin' && (
<div className="mt-6 w-full">
<div className="w-full mt-6">
<div className={cn('gap-2 py-3 justify-center items-center inline-flex w-full rounded-lg',
'bg-primary-600 hover:bg-primary-600/75 hover:shadow-md cursor-pointer text-white hover:shadow-sm flex-shrink-0')}>
<div className={`w-4 h-4 relative ${style.pluginInstallIcon}`}></div>
@ -135,22 +137,22 @@ const Embedded = ({ isShow, onClose, appBaseUrl, accessToken }: Props) => {
)}
<div className={cn('w-full bg-gray-100 rounded-lg flex-col justify-start items-start inline-flex',
'mt-6')}>
<div className="self-stretch pl-3 pr-1 py-1 bg-gray-50 rounded-tl-lg rounded-tr-lg border border-black border-opacity-5 justify-start items-center gap-2 inline-flex">
<div className="inline-flex items-center self-stretch justify-start gap-2 py-1 pl-3 pr-1 border border-black rounded-tl-lg rounded-tr-lg bg-gray-50 border-opacity-5">
<div className="grow shrink basis-0 text-slate-700 text-[13px] font-medium leading-none">
{t(`${prefixEmbedded}.${option}`)}
</div>
<div className="p-2 rounded-lg justify-center items-center gap-1 flex">
<div className="flex items-center justify-center gap-1 p-2 rounded-lg">
<Tooltip
selector={'code-copy-feedback'}
content={(isCopied[option] ? t(`${prefixEmbedded}.copied`) : t(`${prefixEmbedded}.copy`)) || ''}
>
<div className="w-8 h-8 cursor-pointer hover:bg-gray-100 rounded-lg">
<div className="w-8 h-8 rounded-lg cursor-pointer hover:bg-gray-100">
<div onClick={onClickCopy} className={`w-full h-full ${copyStyle.copyIcon} ${isCopied[option] ? copyStyle.copied : ''}`}></div>
</div>
</Tooltip>
</div>
</div>
<div className="p-3 justify-start items-start gap-2 flex overflow-x-auto w-full">
<div className="flex items-start justify-start w-full gap-2 p-3 overflow-x-auto">
<div className="grow shrink basis-0 text-slate-700 text-[13px] leading-tight font-mono">
<pre className='select-text'>{OPTION_MAP[option].getContent(appBaseUrl, accessToken, isTestEnv)}</pre>
</div>

View File

@ -0,0 +1,32 @@
import { create } from 'zustand'
import type { App } from '@/types/app'
import type { IChatItem } from '@/app/components/app/chat/type'
type State = {
appDetail?: App
appSidebarExpand: string
currentLogItem?: IChatItem
showPromptLogModal: boolean
showMessageLogModal: boolean
}
type Action = {
setAppDetail: (appDetail?: App) => void
setAppSiderbarExpand: (state: string) => void
setCurrentLogItem: (item?: IChatItem) => void
setShowPromptLogModal: (showPromptLogModal: boolean) => void
setShowMessageLogModal: (showMessageLogModal: boolean) => void
}
export const useStore = create<State & Action>(set => ({
appDetail: undefined,
setAppDetail: appDetail => set(() => ({ appDetail })),
appSidebarExpand: '',
setAppSiderbarExpand: appSidebarExpand => set(() => ({ appSidebarExpand })),
currentLogItem: undefined,
setCurrentLogItem: currentLogItem => set(() => ({ currentLogItem })),
showPromptLogModal: false,
setShowPromptLogModal: showPromptLogModal => set(() => ({ showPromptLogModal })),
showMessageLogModal: false,
setShowMessageLogModal: showMessageLogModal => set(() => ({ showMessageLogModal })),
}))

View File

@ -0,0 +1,161 @@
'use client'
import { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation'
import { useContext } from 'use-context-selector'
import { useTranslation } from 'react-i18next'
import cn from 'classnames'
import s from './style.module.css'
import Button from '@/app/components/base/button'
import Modal from '@/app/components/base/modal'
import Confirm from '@/app/components/base/confirm'
import { ToastContext } from '@/app/components/base/toast'
import { deleteApp, switchApp } from '@/service/apps'
import { useAppContext } from '@/context/app-context'
import { useProviderContext } from '@/context/provider-context'
import AppsFull from '@/app/components/billing/apps-full-in-dialog'
import EmojiPicker from '@/app/components/base/emoji-picker'
import { XClose } from '@/app/components/base/icons/src/vender/line/general'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { getRedirection } from '@/utils/app-redirection'
import type { App } from '@/types/app'
import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
import AppIcon from '@/app/components/base/app-icon'
import { useStore as useAppStore } from '@/app/components/app/store'
type SwitchAppModalProps = {
show: boolean
appDetail: App
onSuccess?: () => void
onClose: () => void
inAppDetail?: boolean
}
const SwitchAppModal = ({ show, appDetail, inAppDetail = false, onSuccess, onClose }: SwitchAppModalProps) => {
const { push, replace } = useRouter()
const { t } = useTranslation()
const { notify } = useContext(ToastContext)
const { setAppDetail } = useAppStore()
const { isCurrentWorkspaceManager } = useAppContext()
const { plan, enableBilling } = useProviderContext()
const isAppsFull = (enableBilling && plan.usage.buildApps >= plan.total.buildApps)
const [emoji, setEmoji] = useState({ icon: appDetail.icon, icon_background: appDetail.icon_background })
const [showEmojiPicker, setShowEmojiPicker] = useState(false)
const [name, setName] = useState(`${appDetail.name}(copy)`)
const [removeOriginal, setRemoveOriginal] = useState<boolean>(false)
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
const goStart = async () => {
try {
const { new_app_id: newAppID } = await switchApp({
appID: appDetail.id,
name,
icon: emoji.icon,
icon_background: emoji.icon_background,
})
if (onSuccess)
onSuccess()
if (onClose)
onClose()
notify({ type: 'success', message: t('app.newApp.appCreated') })
if (inAppDetail)
setAppDetail()
if (removeOriginal)
await deleteApp(appDetail.id)
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
getRedirection(
isCurrentWorkspaceManager,
{
id: newAppID,
mode: appDetail.mode === 'completion' ? 'workflow' : 'advanced-chat',
},
removeOriginal ? replace : push,
)
}
catch (e) {
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
}
}
useEffect(() => {
if (removeOriginal)
setShowConfirmDelete(true)
}, [removeOriginal])
return (
<>
<Modal
wrapperClassName='z-20'
className={cn('p-8 max-w-[600px] w-[600px]', s.bg)}
isShow={show}
onClose={() => {}}
>
<div className='absolute right-4 top-4 p-2 cursor-pointer' onClick={onClose}>
<XClose className='w-4 h-4 text-gray-500' />
</div>
<div className='w-12 h-12 p-3 bg-white rounded-xl border-[0.5px] border-gray-100 shadow-xl'>
<AlertTriangle className='w-6 h-6 text-[rgb(247,144,9)]' />
</div>
<div className='relative mt-3 text-xl font-semibold leading-[30px] text-gray-900'>{t('app.switch')}</div>
<div className='my-1 text-gray-500 text-sm leading-5'>
<span>{t('app.switchTipStart')}</span>
<span className='text-gray-700 font-medium'>{t('app.switchTip')}</span>
<span>{t('app.switchTipEnd')}</span>
</div>
<div className='pb-4'>
<div className='py-2 text-sm font-medium leading-[20px] text-gray-900'>{t('app.switchLabel')}</div>
<div className='flex items-center justify-between space-x-2'>
<AppIcon size='large' onClick={() => { setShowEmojiPicker(true) }} className='cursor-pointer' icon={emoji.icon} background={emoji.icon_background} />
<input
value={name}
onChange={e => setName(e.target.value)}
placeholder={t('app.newApp.appNamePlaceholder') || ''}
className='grow h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg border border-transparent outline-none appearance-none caret-primary-600 placeholder:text-gray-400 hover:bg-gray-50 hover:border hover:border-gray-300 focus:bg-gray-50 focus:border focus:border-gray-300 focus:shadow-xs'
/>
</div>
{showEmojiPicker && <EmojiPicker
onSelect={(icon, icon_background) => {
setEmoji({ icon, icon_background })
setShowEmojiPicker(false)
}}
onClose={() => {
setEmoji({ icon: appDetail.icon, icon_background: appDetail.icon_background })
setShowEmojiPicker(false)
}}
/>}
</div>
{isAppsFull && <AppsFull loc='app-switch' />}
<div className='pt-6 flex justify-between items-center'>
<div className='flex items-center'>
<input id="removeOriginal" type="checkbox" checked={removeOriginal} onChange={() => setRemoveOriginal(!removeOriginal)} className="w-4 h-4 rounded border-gray-300 text-blue-700 cursor-pointer focus:ring-blue-700" />
<label htmlFor="removeOriginal" className="ml-2 text-sm leading-5 text-gray-700 cursor-pointer">{t('app.removeOriginal')}</label>
</div>
<div className='flex items-center'>
<Button className='mr-2 text-gray-700 text-sm font-medium' onClick={onClose}>{t('app.newApp.Cancel')}</Button>
<Button className='text-sm font-medium border-red-700 border-[0.5px]' disabled={isAppsFull || !name} type="warning" onClick={goStart}>{t('app.switchStart')}</Button>
</div>
</div>
</Modal>
{showConfirmDelete && (
<Confirm
title={t('app.deleteAppConfirmTitle')}
content={t('app.deleteAppConfirmContent')}
isShow={showConfirmDelete}
onConfirm={() => setShowConfirmDelete(false)}
onCancel={() => {
setShowConfirmDelete(false)
setRemoveOriginal(false)
}}
onClose={() => {
setShowConfirmDelete(false)
setRemoveOriginal(false)
}}
/>
)}
</>
)
}
export default SwitchAppModal

View File

@ -0,0 +1,3 @@
.bg {
background: linear-gradient(180deg, rgba(247, 144, 9, 0.05) 0%, rgba(247, 144, 9, 0.00) 24.41%), #F9FAFB;
}

View File

@ -1,5 +1,5 @@
'use client'
import type { Dispatch, FC, SetStateAction } from 'react'
import type { FC } from 'react'
import React, { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import cn from 'classnames'
@ -8,8 +8,9 @@ import { useParams } from 'next/navigation'
import { HandThumbDownIcon, HandThumbUpIcon } from '@heroicons/react/24/outline'
import { useBoolean } from 'ahooks'
import { HashtagIcon } from '@heroicons/react/24/solid'
import PromptLog from '@/app/components/app/chat/log'
// import PromptLog from '@/app/components/app/chat/log'
import { Markdown } from '@/app/components/base/markdown'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import Loading from '@/app/components/base/loading'
import Toast from '@/app/components/base/toast'
import AudioBtn from '@/app/components/base/audio-btn'
@ -22,13 +23,20 @@ import { RefreshCcw01 } from '@/app/components/base/icons/src/vender/line/arrows
import { fetchTextGenerationMessge } from '@/service/debug'
import AnnotationCtrlBtn from '@/app/components/app/configuration/toolbox/annotation/annotation-ctrl-btn'
import EditReplyModal from '@/app/components/app/annotation/edit-annotation-modal'
import { useStore as useAppStore } from '@/app/components/app/store'
import WorkflowProcessItem from '@/app/components/base/chat/chat/answer/workflow-process'
import type { WorkflowProcess } from '@/app/components/base/chat/types'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
const MAX_DEPTH = 3
export type IGenerationItemProps = {
isWorkflow?: boolean
workflowProcessData?: WorkflowProcess
className?: string
isError: boolean
onRetry: () => void
content: string
content: any
messageId?: string | null
conversationId?: string
isLoading?: boolean
@ -75,6 +83,8 @@ export const copyIcon = (
)
const GenerationItem: FC<IGenerationItemProps> = ({
isWorkflow,
workflowProcessData,
className,
isError,
onRetry,
@ -111,7 +121,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
const [childFeedback, setChildFeedback] = useState<Feedbacktype>({
rating: null,
})
const [promptLog, setPromptLog] = useState<{ role: string; text: string }[]>([])
const { setCurrentLogItem, setShowPromptLogModal } = useAppStore()
const handleFeedback = async (childFeedback: Feedbacktype) => {
await updateFeedback({ url: `/messages/${childMessageId}/feedbacks`, body: { rating: childFeedback.rating } }, isInstalledApp, installedAppId)
@ -137,6 +147,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
isInstalledApp,
installedAppId,
controlClearMoreLikeThis,
isWorkflow,
}
const handleMoreLikeThis = async () => {
@ -180,18 +191,33 @@ const GenerationItem: FC<IGenerationItemProps> = ({
setChildMessageId(null)
}, [isLoading])
const handleOpenLogModal = async (setModal: Dispatch<SetStateAction<boolean>>) => {
const handleOpenLogModal = async () => {
const data = await fetchTextGenerationMessge({
appId: params.appId as string,
messageId: messageId!,
})
setPromptLog(data.message as any || [])
setModal(true)
const logItem = {
...data,
log: [
...data.message,
...(data.message[data.message.length - 1].role !== 'assistant'
? [
{
role: 'assistant',
text: data.answer,
files: data.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
},
]
: []),
],
}
setCurrentLogItem(logItem)
setShowPromptLogModal(true)
}
const ratingContent = (
<>
{!isError && messageId && !feedback?.rating && (
{!isWorkflow && !isError && messageId && !feedback?.rating && (
<SimpleBtn className="!px-0">
<>
<div
@ -215,7 +241,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
</>
</SimpleBtn>
)}
{!isError && messageId && feedback?.rating === 'like' && (
{!isWorkflow && !isError && messageId && feedback?.rating === 'like' && (
<div
onClick={() => {
onFeedback?.({
@ -226,7 +252,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
<HandThumbUpIcon width={16} height={16} />
</div>
)}
{!isError && messageId && feedback?.rating === 'dislike' && (
{!isWorkflow && !isError && messageId && feedback?.rating === 'dislike' && (
<div
onClick={() => {
onFeedback?.({
@ -265,12 +291,24 @@ const GenerationItem: FC<IGenerationItemProps> = ({
}
<div className={`flex ${contentClassName}`}>
<div className='grow w-0'>
{isError
? <div className='text-gray-400 text-sm'>{t('share.generation.batchFailed.outputPlaceholder')}</div>
: (
<Markdown content={content} />
)}
{workflowProcessData && (
<WorkflowProcessItem grayBg data={workflowProcessData} expand={workflowProcessData.expand} />
)}
{isError && (
<div className='text-gray-400 text-sm'>{t('share.generation.batchFailed.outputPlaceholder')}</div>
)}
{!isError && (typeof content === 'string') && (
<Markdown content={content} />
)}
{!isError && (typeof content !== 'string') && (
<CodeEditor
readOnly
title={<div/>}
language={CodeLanguage.json}
value={content}
isJSONStringifyBeauty
/>
)}
</div>
</div>
@ -278,29 +316,23 @@ const GenerationItem: FC<IGenerationItemProps> = ({
<div className='flex items-center'>
{
!isInWebApp && !isInstalledApp && !isResponding && (
<PromptLog
log={promptLog}
containerRef={ref}
>
{
showModal => (
<SimpleBtn
isDisabled={isError || !messageId}
className={cn(isMobile && '!px-1.5', 'space-x-1 mr-1')}
onClick={() => handleOpenLogModal(showModal)}>
<File02 className='w-3.5 h-3.5' />
{!isMobile && <div>{t('common.operation.log')}</div>}
</SimpleBtn>
)
}
</PromptLog>
<SimpleBtn
isDisabled={isError || !messageId}
className={cn(isMobile && '!px-1.5', 'space-x-1 mr-1')}
onClick={handleOpenLogModal}>
<File02 className='w-3.5 h-3.5' />
{!isMobile && <div>{t('common.operation.log')}</div>}
</SimpleBtn>
)
}
<SimpleBtn
isDisabled={isError || !messageId}
className={cn(isMobile && '!px-1.5', 'space-x-1')}
onClick={() => {
copy(content)
if (typeof content === 'string')
copy(content)
else
copy(JSON.stringify(content))
Toast.notify({ type: 'success', message: t('common.actionMsg.copySuccessfully') })
}}>
<Clipboard className='w-3.5 h-3.5' />
@ -308,14 +340,16 @@ const GenerationItem: FC<IGenerationItemProps> = ({
</SimpleBtn>
{isInWebApp && (
<>
<SimpleBtn
isDisabled={isError || !messageId}
className={cn(isMobile && '!px-1.5', 'ml-2 space-x-1')}
onClick={() => { onSave?.(messageId as string) }}
>
<Bookmark className='w-3.5 h-3.5' />
{!isMobile && <div>{t('common.operation.save')}</div>}
</SimpleBtn>
{!isWorkflow && (
<SimpleBtn
isDisabled={isError || !messageId}
className={cn(isMobile && '!px-1.5', 'ml-2 space-x-1')}
onClick={() => { onSave?.(messageId as string) }}
>
<Bookmark className='w-3.5 h-3.5' />
{!isMobile && <div>{t('common.operation.save')}</div>}
</SimpleBtn>
)}
{(moreLikeThis && depth < MAX_DEPTH) && (
<SimpleBtn
isDisabled={isError || !messageId}
@ -324,15 +358,20 @@ const GenerationItem: FC<IGenerationItemProps> = ({
>
<Stars02 className='w-3.5 h-3.5' />
{!isMobile && <div>{t('appDebug.feature.moreLikeThis.title')}</div>}
</SimpleBtn>)}
{isError && <SimpleBtn
onClick={onRetry}
className={cn(isMobile && '!px-1.5', 'ml-2 space-x-1')}
>
<RefreshCcw01 className='w-3.5 h-3.5' />
{!isMobile && <div>{t('share.generation.batchFailed.retry')}</div>}
</SimpleBtn>}
{!isError && messageId && <div className="mx-3 w-[1px] h-[14px] bg-gray-200"></div>}
</SimpleBtn>
)}
{isError && (
<SimpleBtn
onClick={onRetry}
className={cn(isMobile && '!px-1.5', 'ml-2 space-x-1')}
>
<RefreshCcw01 className='w-3.5 h-3.5' />
{!isMobile && <div>{t('share.generation.batchFailed.retry')}</div>}
</SimpleBtn>
)}
{!isError && messageId && !isWorkflow && (
<div className="mx-3 w-[1px] h-[14px] bg-gray-200"></div>
)}
{ratingContent}
</>
)}

View File

@ -0,0 +1,128 @@
import { useTranslation } from 'react-i18next'
import React, { useState } from 'react'
import cn from 'classnames'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
import { Check, DotsGrid } from '@/app/components/base/icons/src/vender/line/general'
import { XCircle } from '@/app/components/base/icons/src/vender/solid/general'
import { ChatBot, CuteRobote } from '@/app/components/base/icons/src/vender/solid/communication'
import { Route } from '@/app/components/base/icons/src/vender/solid/mapsAndTravel'
export type AppSelectorProps = {
value: string
onChange: (value: string) => void
}
const AppTypeSelector = ({ value, onChange }: AppSelectorProps) => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='bottom-start'
offset={4}
>
<div className='relative'>
<PortalToFollowElemTrigger
onClick={() => setOpen(v => !v)}
className='block'
>
<div className={cn(
'flex items-center gap-1 h-8 text-gray-700 text-[13px] leading-[18px] cursor-pointer px-2 rounded-lg bg-white shadow-xs hover:bg-gray-200',
open && !value && '!bg-gray-200 hover:!bg-gray-200',
!!value && '!bg-white hover:!bg-white',
)}>
{!value && (
<>
<div className='w-4 h-4 p-[1px]'>
<DotsGrid className='w-3.5 h-3.5' />
</div>
<div className=''>{t('app.typeSelector.all')}</div>
<div className='w-4 h-4 p-[1px]'>
<ChevronDown className='w-3.5 h-3.5' />
</div>
</>
)}
{value === 'chatbot' && (
<>
<div className='w-4 h-4 p-[1px]'>
<ChatBot className='w-3.5 h-3.5 text-[#1570EF]' />
</div>
<div className=''>{t('app.typeSelector.chatbot')}</div>
<div className='w-4 h-4 p-[1px]' onClick={(e) => {
e.stopPropagation()
onChange('')
}}>
<XCircle className='w-3.5 h-3.5 text-gray-400 cursor-pointer hover:text-gray-600' />
</div>
</>
)}
{value === 'agent' && (
<>
<div className='w-4 h-4 p-[1px]'>
<CuteRobote className='w-3.5 h-3.5 text-indigo-600' />
</div>
<div className=''>{t('app.typeSelector.agent')}</div>
<div className='w-4 h-4 p-[1px]' onClick={(e) => {
e.stopPropagation()
onChange('')
}}>
<XCircle className='w-3.5 h-3.5 text-gray-400 cursor-pointer hover:text-gray-600' />
</div>
</>
)}
{value === 'workflow' && (
<>
<div className='w-4 h-4 p-[1px]'>
<Route className='w-3.5 h-3.5 text-[#F79009]' />
</div>
<div className=''>{t('app.typeSelector.workflow')}</div>
<div className='w-4 h-4 p-[1px]' onClick={(e) => {
e.stopPropagation()
onChange('')
}}>
<XCircle className='w-3.5 h-3.5 text-gray-400 cursor-pointer hover:text-gray-600' />
</div>
</>
)}
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1002]'>
<div className='relative p-1 w-[180px] bg-white rounded-lg shadow-xl'>
<div className='flex items-center pl-3 py-[6px] pr-2 rounded-lg cursor-pointer hover:bg-gray-50' onClick={() => {
onChange('chatbot')
setOpen(false)
}}>
<ChatBot className='mr-2 w-4 h-4 text-[#1570EF]' />
<div className='grow text-gray-700 text-[13px] font-medium leading-[18px]'>{t('app.typeSelector.chatbot')}</div>
{value === 'chatbot' && <Check className='w-4 h-4 text-primary-600'/>}
</div>
<div className='flex items-center pl-3 py-[6px] pr-2 rounded-lg cursor-pointer hover:bg-gray-50' onClick={() => {
onChange('agent')
setOpen(false)
}}>
<CuteRobote className='mr-2 w-4 h-4 text-indigo-600' />
<div className='grow text-gray-700 text-[13px] font-medium leading-[18px]'>{t('app.typeSelector.agent')}</div>
{value === 'agent' && <Check className='w-4 h-4 text-primary-600'/>}
</div>
<div className='flex items-center pl-3 py-[6px] pr-2 rounded-lg cursor-pointer hover:bg-gray-50' onClick={() => {
onChange('workflow')
setOpen(false)
}}>
<Route className='mr-2 w-4 h-4 text-[#F79009]' />
<div className='grow text-gray-700 text-[13px] font-medium leading-[18px]'>{t('app.typeSelector.workflow')}</div>
{value === 'workflow' && <Check className='w-4 h-4 text-primary-600'/>}
</div>
</div>
</PortalToFollowElemContent>
</div>
</PortalToFollowElem>
)
}
export default React.memo(AppTypeSelector)

View File

@ -0,0 +1,26 @@
'use client'
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import Run from '@/app/components/workflow/run'
import { XClose } from '@/app/components/base/icons/src/vender/line/general'
type ILogDetail = {
runID: string
onClose: () => void
}
const DetailPanel: FC<ILogDetail> = ({ runID, onClose }) => {
const { t } = useTranslation()
return (
<div className='grow relative flex flex-col py-3'>
<span className='absolute right-3 top-4 p-1 cursor-pointer z-20' onClick={onClose}>
<XClose className='w-4 h-4 text-gray-500' />
</span>
<h1 className='shrink-0 px-4 py-1 text-md font-semibold text-gray-900'>{t('appLog.runDetail.workflowTitle')}</h1>
<Run runID={runID}/>
</div>
)
}
export default DetailPanel

View File

@ -0,0 +1,55 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import {
MagnifyingGlassIcon,
} from '@heroicons/react/24/solid'
import type { QueryParam } from './index'
import { SimpleSelect } from '@/app/components/base/select'
type IFilterProps = {
queryParams: QueryParam
setQueryParams: (v: QueryParam) => void
}
const Filter: FC<IFilterProps> = ({ queryParams, setQueryParams }: IFilterProps) => {
const { t } = useTranslation()
return (
<div className='flex flex-row flex-wrap gap-y-2 gap-x-4 items-center mb-4 text-gray-900 text-base'>
<div className="relative rounded-md">
<SimpleSelect
defaultValue={'all'}
className='!min-w-[100px]'
onSelect={
(item) => {
setQueryParams({ ...queryParams, status: item.value as string })
}
}
items={[{ value: 'all', name: 'All' },
{ value: 'succeeded', name: 'Success' },
{ value: 'failed', name: 'Fail' },
{ value: 'stopped', name: 'Stop' },
]}
/>
</div>
<div className="relative">
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<MagnifyingGlassIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
</div>
<input
type="text"
name="query"
className="block w-[240px] bg-gray-100 shadow-sm rounded-md border-0 py-1.5 pl-10 text-gray-900 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-gray-200 focus-visible:outline-none sm:text-sm sm:leading-6"
placeholder={t('common.operation.search')!}
value={queryParams.keyword}
onChange={(e) => {
setQueryParams({ ...queryParams, keyword: e.target.value })
}}
/>
</div>
</div>
)
}
export default Filter

View File

@ -0,0 +1,125 @@
'use client'
import type { FC, SVGProps } from 'react'
import React, { useState } from 'react'
import useSWR from 'swr'
import { usePathname } from 'next/navigation'
import { Pagination } from 'react-headless-pagination'
import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/24/outline'
import { Trans, useTranslation } from 'react-i18next'
import Link from 'next/link'
import List from './list'
import Filter from './filter'
import s from './style.module.css'
import Loading from '@/app/components/base/loading'
import { fetchWorkflowLogs } from '@/service/log'
import { APP_PAGE_LIMIT } from '@/config'
import type { App, AppMode } from '@/types/app'
export type ILogsProps = {
appDetail: App
}
export type QueryParam = {
status?: string
keyword?: string
}
const ThreeDotsIcon = ({ className }: SVGProps<SVGElement>) => {
return <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
<path d="M5 6.5V5M8.93934 7.56066L10 6.5M10.0103 11.5H11.5103" stroke="#374151" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
}
const EmptyElement: FC<{ appUrl: string }> = ({ appUrl }) => {
const { t } = useTranslation()
const pathname = usePathname()
const pathSegments = pathname.split('/')
pathSegments.pop()
return <div className='flex items-center justify-center h-full'>
<div className='bg-gray-50 w-[560px] h-fit box-border px-5 py-4 rounded-2xl'>
<span className='text-gray-700 font-semibold'>{t('appLog.table.empty.element.title')}<ThreeDotsIcon className='inline relative -top-3 -left-1.5' /></span>
<div className='mt-2 text-gray-500 text-sm font-normal'>
<Trans
i18nKey="appLog.table.empty.element.content"
components={{ shareLink: <Link href={`${pathSegments.join('/')}/overview`} className='text-primary-600' />, testLink: <Link href={appUrl} className='text-primary-600' target='_blank' rel='noopener noreferrer' /> }}
/>
</div>
</div>
</div>
}
const Logs: FC<ILogsProps> = ({ appDetail }) => {
const { t } = useTranslation()
const [queryParams, setQueryParams] = useState<QueryParam>({ status: 'all' })
const [currPage, setCurrPage] = React.useState<number>(0)
const query = {
page: currPage + 1,
limit: APP_PAGE_LIMIT,
...(queryParams.status !== 'all' ? { status: queryParams.status } : {}),
...(queryParams.keyword ? { keyword: queryParams.keyword } : {}),
}
const getWebAppType = (appType: AppMode) => {
if (appType !== 'completion' && appType !== 'workflow')
return 'chat'
return appType
}
const { data: workflowLogs, mutate } = useSWR({
url: `/apps/${appDetail.id}/workflow-app-logs`,
params: query,
}, fetchWorkflowLogs)
const total = workflowLogs?.total
return (
<div className='flex flex-col h-full'>
<h1 className='text-md font-semibold text-gray-900'>{t('appLog.workflowTitle')}</h1>
<p className='flex text-sm font-normal text-gray-500'>{t('appLog.workflowSubtitle')}</p>
<div className='flex flex-col py-4 flex-1'>
<Filter queryParams={queryParams} setQueryParams={setQueryParams} />
{/* workflow log */}
{total === undefined
? <Loading type='app' />
: total > 0
? <List logs={workflowLogs} appDetail={appDetail} onRefresh={mutate} />
: <EmptyElement appUrl={`${appDetail.site.app_base_url}/${getWebAppType(appDetail.mode)}/${appDetail.site.access_token}`} />
}
{/* Show Pagination only if the total is more than the limit */}
{(total && total > APP_PAGE_LIMIT)
? <Pagination
className="flex items-center w-full h-10 text-sm select-none mt-8"
currentPage={currPage}
edgePageCount={2}
middlePagesSiblingCount={1}
setCurrentPage={setCurrPage}
totalPages={Math.ceil(total / APP_PAGE_LIMIT)}
truncableClassName="w-8 px-0.5 text-center"
truncableText="..."
>
<Pagination.PrevButton
disabled={currPage === 0}
className={`flex items-center mr-2 text-gray-500 focus:outline-none ${currPage === 0 ? 'cursor-not-allowed opacity-50' : 'cursor-pointer hover:text-gray-600 dark:hover:text-gray-200'}`} >
<ArrowLeftIcon className="mr-3 h-3 w-3" />
{t('appLog.table.pagination.previous')}
</Pagination.PrevButton>
<div className={`flex items-center justify-center flex-grow ${s.pagination}`}>
<Pagination.PageButton
activeClassName="bg-primary-50 dark:bg-opacity-0 text-primary-600 dark:text-white"
className="flex items-center justify-center h-8 w-8 rounded-full cursor-pointer"
inactiveClassName="text-gray-500"
/>
</div>
<Pagination.NextButton
disabled={currPage === Math.ceil(total / APP_PAGE_LIMIT) - 1}
className={`flex items-center mr-2 text-gray-500 focus:outline-none ${currPage === Math.ceil(total / APP_PAGE_LIMIT) - 1 ? 'cursor-not-allowed opacity-50' : 'cursor-pointer hover:text-gray-600 dark:hover:text-gray-200'}`} >
{t('appLog.table.pagination.next')}
<ArrowRightIcon className="ml-3 h-3 w-3" />
</Pagination.NextButton>
</Pagination>
: null}
</div>
</div>
)
}
export default Logs

View File

@ -0,0 +1,133 @@
'use client'
import type { FC } from 'react'
import React, { useState } from 'react'
import dayjs from 'dayjs'
import { useTranslation } from 'react-i18next'
import cn from 'classnames'
import s from './style.module.css'
import DetailPanel from './detail'
import type { WorkflowAppLogDetail, WorkflowLogsResponse } from '@/models/log'
import type { App } from '@/types/app'
import Loading from '@/app/components/base/loading'
import Drawer from '@/app/components/base/drawer'
import Indicator from '@/app/components/header/indicator'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
type ILogs = {
logs?: WorkflowLogsResponse
appDetail?: App
onRefresh: () => void
}
const defaultValue = 'N/A'
const WorkflowAppLogList: FC<ILogs> = ({ logs, appDetail, onRefresh }) => {
const { t } = useTranslation()
const media = useBreakpoints()
const isMobile = media === MediaType.mobile
const [showDrawer, setShowDrawer] = useState<boolean>(false)
const [currentLog, setCurrentLog] = useState<WorkflowAppLogDetail | undefined>()
const statusTdRender = (status: string) => {
if (status === 'succeeded') {
return (
<div className='inline-flex items-center gap-1'>
<Indicator color={'green'} />
<span>Success</span>
</div>
)
}
if (status === 'failed') {
return (
<div className='inline-flex items-center gap-1'>
<Indicator color={'red'} />
<span className='text-red-600'>Fail</span>
</div>
)
}
if (status === 'stopped') {
return (
<div className='inline-flex items-center gap-1'>
<Indicator color={'yellow'} />
<span>Stop</span>
</div>
)
}
if (status === 'running') {
return (
<div className='inline-flex items-center gap-1'>
<Indicator color={'blue'} />
<span className='text-primary-600'>Running</span>
</div>
)
}
}
const onCloseDrawer = () => {
onRefresh()
setShowDrawer(false)
setCurrentLog(undefined)
}
if (!logs || !appDetail)
return <Loading />
return (
<div className='overflow-x-auto'>
<table className={`w-full min-w-[440px] border-collapse border-0 text-sm mt-3 ${s.logTable}`}>
<thead className="h-8 !pl-3 py-2 leading-[18px] border-b border-gray-200 text-xs text-gray-500 font-medium">
<tr>
<td className='w-[1.375rem] whitespace-nowrap'></td>
<td className='whitespace-nowrap'>{t('appLog.table.header.startTime')}</td>
<td className='whitespace-nowrap'>{t('appLog.table.header.status')}</td>
<td className='whitespace-nowrap'>{t('appLog.table.header.runtime')}</td>
<td className='whitespace-nowrap'>{t('appLog.table.header.tokens')}</td>
<td className='whitespace-nowrap'>{t('appLog.table.header.user')}</td>
{/* <td className='whitespace-nowrap'>{t('appLog.table.header.version')}</td> */}
</tr>
</thead>
<tbody className="text-gray-700 text-[13px]">
{logs.data.map((log: WorkflowAppLogDetail) => {
const endUser = log.created_by_end_user ? log.created_by_end_user.session_id : defaultValue
return <tr
key={log.id}
className={`border-b border-gray-200 h-8 hover:bg-gray-50 cursor-pointer ${currentLog?.id !== log.id ? '' : 'bg-gray-50'}`}
onClick={() => {
setCurrentLog(log)
setShowDrawer(true)
}}>
<td className='text-center align-middle'>{!log.read_at && <span className='inline-block bg-[#3F83F8] h-1.5 w-1.5 rounded'></span>}</td>
<td className='w-[160px]'>{dayjs.unix(log.created_at).format(t('appLog.dateTimeFormat') as string)}</td>
<td>{statusTdRender(log.workflow_run.status)}</td>
<td>
<div className={cn(
log.workflow_run.elapsed_time === 0 && 'text-gray-400',
)}>{`${log.workflow_run.elapsed_time.toFixed(3)}s`}</div>
</td>
<td>{log.workflow_run.total_tokens}</td>
<td>
<div className={cn(endUser === defaultValue ? 'text-gray-400' : 'text-gray-700', 'text-sm overflow-hidden text-ellipsis whitespace-nowrap')}>
{endUser}
</div>
</td>
{/* <td>VERSION</td> */}
</tr>
})}
</tbody>
</table>
<Drawer
isOpen={showDrawer}
onClose={onCloseDrawer}
mask={isMobile}
footer={null}
panelClassname='mt-16 mx-2 sm:mr-2 mb-3 !p-0 !max-w-[600px] rounded-xl border border-gray-200'
>
<DetailPanel onClose={onCloseDrawer} runID={currentLog?.workflow_run.id || ''} />
</Drawer>
</div>
)
}
export default WorkflowAppLogList

View File

@ -0,0 +1,9 @@
.logTable td {
padding: 7px 8px;
box-sizing: border-box;
max-width: 200px;
}
.pagination li {
list-style: none;
}

View File

@ -115,10 +115,9 @@ const AudioBtn = ({
className='z-10'
>
<div
className={`box-border p-0.5 flex items-center justify-center cursor-pointer ${isAudition || 'rounded-md bg-white'}`}
style={{ boxShadow: !isAudition ? '0px 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06)' : '' }}
className={`box-border p-0.5 flex items-center justify-center cursor-pointer ${isAudition || '!p-0 rounded-md bg-white'}`}
onClick={togglePlayPause}>
<div className={`w-6 h-6 rounded-md ${!isAudition ? 'hover:bg-gray-200' : 'hover:bg-gray-50'} ${(isPlaying && !hasEnded) ? s.pauseIcon : s.playIcon}`}></div>
<div className={`w-6 h-6 rounded-md ${!isAudition ? 'w-4 h-4 hover:bg-gray-50' : 'hover:bg-gray-50'} ${(isPlaying && !hasEnded) ? s.pauseIcon : s.playIcon}`}></div>
</div>
</Tooltip>
</div>

View File

@ -0,0 +1,22 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import cn from 'classnames'
import { Plus } from '../icons/src/vender/line/general'
type Props = {
className?: string
onClick: () => void
}
const AddButton: FC<Props> = ({
className,
onClick,
}) => {
return (
<div className={cn(className, 'p-1 rounded-md cursor-pointer hover:bg-gray-200 select-none')} onClick={onClick}>
<Plus className='w-4 h-4 text-gray-500' />
</div>
)
}
export default React.memo(AddButton)

View File

@ -17,7 +17,7 @@ const BasicContent: FC<BasicContentProps> = ({
if (annotation?.logAnnotation)
return <Markdown content={annotation?.logAnnotation.content || ''} />
return <Markdown content={content} />
return <Markdown content={content} className={`${item.isError && '!text-[#F04438]'}`} />
}
export default memo(BasicContent)

View File

@ -2,7 +2,7 @@ import type {
FC,
ReactNode,
} from 'react'
import { memo } from 'react'
import { memo, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import type {
ChatConfig,
@ -13,7 +13,9 @@ import AgentContent from './agent-content'
import BasicContent from './basic-content'
import SuggestedQuestions from './suggested-questions'
import More from './more'
import WorkflowProcess from './workflow-process'
import { AnswerTriangle } from '@/app/components/base/icons/src/vender/solid/general'
import { MessageFast } from '@/app/components/base/icons/src/vender/solid/communication'
import LoadingAnim from '@/app/components/app/chat/loading-anim'
import Citation from '@/app/components/app/chat/citation'
import { EditTitle } from '@/app/components/app/annotation/edit-annotation-modal/edit-item'
@ -27,6 +29,8 @@ type AnswerProps = {
answerIcon?: ReactNode
responding?: boolean
allToolIcons?: Record<string, string | Emoji>
showPromptLog?: boolean
chatAnswerContainerInner?: string
}
const Answer: FC<AnswerProps> = ({
item,
@ -36,6 +40,8 @@ const Answer: FC<AnswerProps> = ({
answerIcon,
responding,
allToolIcons,
showPromptLog,
chatAnswerContainerInner,
}) => {
const { t } = useTranslation()
const {
@ -44,9 +50,33 @@ const Answer: FC<AnswerProps> = ({
agent_thoughts,
more,
annotation,
workflowProcess,
} = item
const hasAgentThoughts = !!agent_thoughts?.length
const [containerWidth, setContainerWidth] = useState(0)
const [contentWidth, setContentWidth] = useState(0)
const containerRef = useRef<HTMLDivElement>(null)
const contentRef = useRef<HTMLDivElement>(null)
const getContainerWidth = () => {
if (containerRef.current)
setContainerWidth(containerRef.current?.clientWidth + 16)
}
const getContentWidth = () => {
if (contentRef.current)
setContentWidth(contentRef.current?.clientWidth)
}
useEffect(() => {
getContainerWidth()
}, [])
useEffect(() => {
if (!responding)
getContentWidth()
}, [responding])
return (
<div className='flex mb-2 last:mb-0'>
<div className='shrink-0 relative w-10 h-10'>
@ -65,19 +95,43 @@ const Answer: FC<AnswerProps> = ({
)
}
</div>
<div className='chat-answer-container grow w-0 group ml-4'>
<div className='relative pr-10'>
<div className='chat-answer-container grow w-0 ml-4' ref={containerRef}>
<div className={`group relative pr-10 ${chatAnswerContainerInner}`}>
<AnswerTriangle className='absolute -left-2 top-0 w-2 h-3 text-gray-100' />
<div className='group relative inline-block px-4 py-3 max-w-full bg-gray-100 rounded-b-2xl rounded-tr-2xl text-sm text-gray-900'>
<div
ref={contentRef}
className={`
relative inline-block px-4 py-3 max-w-full bg-gray-100 rounded-b-2xl rounded-tr-2xl text-sm text-gray-900
${workflowProcess && 'w-full'}
`}
>
{annotation?.id && (
<div
className='absolute -top-3.5 -right-3.5 box-border flex items-center justify-center h-7 w-7 p-0.5 rounded-lg bg-white cursor-pointer text-[#444CE7] shadow-md group-hover:hidden'
>
<div className='p-1 rounded-lg bg-[#EEF4FF] '>
<MessageFast className='w-4 h-4' />
</div>
</div>
)}
{
!responding && (
<Operation
hasWorkflowProcess={!!workflowProcess}
maxSize={containerWidth - contentWidth - 4}
contentWidth={contentWidth}
item={item}
question={question}
index={index}
showPromptLog={showPromptLog}
/>
)
}
{
workflowProcess && (
<WorkflowProcess data={workflowProcess} hideInfo />
)
}
{
responding && !content && !hasAgentThoughts && (
<div className='flex items-center justify-center w-6 h-5'>

View File

@ -4,6 +4,7 @@ import {
useMemo,
useState,
} from 'react'
import cn from 'classnames'
import { useTranslation } from 'react-i18next'
import type { ChatItem } from '../../types'
import { useChatContext } from '../context'
@ -17,16 +18,25 @@ import {
ThumbsUp,
} from '@/app/components/base/icons/src/vender/line/alertsAndFeedback'
import TooltipPlus from '@/app/components/base/tooltip-plus'
import Log from '@/app/components/app/chat/log'
type OperationProps = {
item: ChatItem
question: string
index: number
showPromptLog?: boolean
maxSize: number
contentWidth: number
hasWorkflowProcess: boolean
}
const Operation: FC<OperationProps> = ({
item,
question,
index,
showPromptLog,
maxSize,
contentWidth,
hasWorkflowProcess,
}) => {
const { t } = useTranslation()
const {
@ -63,39 +73,134 @@ const Operation: FC<OperationProps> = ({
setLocalFeedback({ rating })
}
const operationWidth = useMemo(() => {
let width = 0
if (!isOpeningStatement)
width += 28
if (!isOpeningStatement && showPromptLog)
width += 102 + 8
if (!isOpeningStatement && config?.text_to_speech?.enabled)
width += 33
if (!isOpeningStatement && config?.supportAnnotation && config?.annotation_reply?.enabled)
width += 56 + 8
if (config?.supportFeedback && !localFeedback?.rating && onFeedback && !isOpeningStatement)
width += 60 + 8
if (config?.supportFeedback && localFeedback?.rating && onFeedback && !isOpeningStatement)
width += 28 + 8
return width
}, [isOpeningStatement, showPromptLog, config?.text_to_speech?.enabled, config?.supportAnnotation, config?.annotation_reply?.enabled, config?.supportFeedback, localFeedback?.rating, onFeedback])
const positionRight = useMemo(() => operationWidth < maxSize, [operationWidth, maxSize])
return (
<div className='absolute top-[-14px] right-[-14px] flex justify-end gap-1'>
{
!isOpeningStatement && (
<>
<div
className={cn(
'absolute flex justify-end gap-1',
hasWorkflowProcess && '-top-3.5 -right-3.5',
!positionRight && '-top-3.5 -right-3.5',
!hasWorkflowProcess && positionRight && '!top-[9px]',
)}
style={(!hasWorkflowProcess && positionRight) ? { left: contentWidth + 8 } : {}}
>
{!isOpeningStatement && (
<CopyBtn
value={content}
className='hidden group-hover:block'
/>
)
}
)}
{(!isOpeningStatement && config?.text_to_speech?.enabled) && (
<AudioBtn
value={content}
voice={config?.text_to_speech?.voice}
className='hidden group-hover:block'
/>
)}
{(!isOpeningStatement && config?.supportAnnotation && config.annotation_reply?.enabled) && (
<AnnotationCtrlBtn
appId={config?.appId || ''}
messageId={id}
annotationId={annotation?.id || ''}
className='hidden group-hover:block ml-1 shrink-0'
cached={hasAnnotation}
query={question}
answer={content}
onAdded={(id, authorName) => onAnnotationAdded?.(id, authorName, question, content, index)}
onEdit={() => setIsShowReplyModal(true)}
onRemoved={() => onAnnotationRemoved?.(index)}
/>
)}
{!isOpeningStatement && (showPromptLog || config?.text_to_speech?.enabled) && (
<div className='hidden group-hover:flex items-center w-max h-[28px] p-0.5 rounded-lg bg-white border-[0.5px] border-gray-100 shadow-md shrink-0'>
{showPromptLog && (
<Log logItem={item} />
)}
{(config?.text_to_speech?.enabled) && (
<>
<div className='mx-1 w-[1px] h-[14px] bg-gray-200'/>
<AudioBtn
value={content}
voice={config?.text_to_speech?.voice}
className='hidden group-hover:block'
/>
</>
)}
</div>
)}
{(!isOpeningStatement && config?.supportAnnotation && config.annotation_reply?.enabled) && (
<AnnotationCtrlBtn
appId={config?.appId || ''}
messageId={id}
annotationId={annotation?.id || ''}
className='hidden group-hover:block ml-1 shrink-0'
cached={hasAnnotation}
query={question}
answer={content}
onAdded={(id, authorName) => onAnnotationAdded?.(id, authorName, question, content, index)}
onEdit={() => setIsShowReplyModal(true)}
onRemoved={() => onAnnotationRemoved?.(index)}
/>
)}
{
!positionRight && annotation?.id && (
<div
className='relative box-border flex items-center justify-center h-7 w-7 p-0.5 rounded-lg bg-white cursor-pointer text-[#444CE7] shadow-md group-hover:hidden'
>
<div className='p-1 rounded-lg bg-[#EEF4FF] '>
<MessageFast className='w-4 h-4' />
</div>
</div>
)
}
{
config?.supportFeedback && !localFeedback?.rating && onFeedback && !isOpeningStatement && (
<div className='hidden group-hover:flex ml-1 shrink-0 items-center px-0.5 bg-white border-[0.5px] border-gray-100 shadow-md text-gray-500 rounded-lg'>
<TooltipPlus popupContent={t('appDebug.operation.agree')}>
<div
className='flex items-center justify-center mr-0.5 w-6 h-6 rounded-md hover:bg-black/5 hover:text-gray-800 cursor-pointer'
onClick={() => handleFeedback('like')}
>
<ThumbsUp className='w-4 h-4' />
</div>
</TooltipPlus>
<TooltipPlus popupContent={t('appDebug.operation.disagree')}>
<div
className='flex items-center justify-center w-6 h-6 rounded-md hover:bg-black/5 hover:text-gray-800 cursor-pointer'
onClick={() => handleFeedback('dislike')}
>
<ThumbsDown className='w-4 h-4' />
</div>
</TooltipPlus>
</div>
)
}
{
config?.supportFeedback && localFeedback?.rating && onFeedback && !isOpeningStatement && (
<TooltipPlus popupContent={localFeedback.rating === 'like' ? t('appDebug.operation.cancelAgree') : t('appDebug.operation.cancelDisagree')}>
<div
className={`
flex items-center justify-center w-7 h-7 rounded-[10px] border-[2px] border-white cursor-pointer
${localFeedback.rating === 'like' && 'bg-blue-50 text-blue-600'}
${localFeedback.rating === 'dislike' && 'bg-red-100 text-red-600'}
`}
onClick={() => handleFeedback(null)}
>
{
localFeedback.rating === 'like' && (
<ThumbsUp className='w-4 h-4' />
)
}
{
localFeedback.rating === 'dislike' && (
<ThumbsDown className='w-4 h-4' />
)
}
</div>
</TooltipPlus>
)
}
</div>
<EditReplyModal
isShow={isShowReplyModal}
onHide={() => setIsShowReplyModal(false)}
@ -109,65 +214,7 @@ const Operation: FC<OperationProps> = ({
createdAt={annotation?.created_at}
onRemove={() => onAnnotationRemoved?.(index)}
/>
{
annotation?.id && (
<div
className='relative box-border flex items-center justify-center h-7 w-7 p-0.5 rounded-lg bg-white cursor-pointer text-[#444CE7] shadow-md'
>
<div className='p-1 rounded-lg bg-[#EEF4FF] '>
<MessageFast className='w-4 h-4' />
</div>
</div>
)
}
{
config?.supportFeedback && !localFeedback?.rating && onFeedback && !isOpeningStatement && (
<div className='hidden group-hover:flex ml-1 shrink-0 items-center px-0.5 bg-white border-[0.5px] border-gray-100 shadow-md text-gray-500 rounded-lg'>
<TooltipPlus popupContent={t('appDebug.operation.agree')}>
<div
className='flex items-center justify-center mr-0.5 w-6 h-6 rounded-md hover:bg-black/5 hover:text-gray-800 cursor-pointer'
onClick={() => handleFeedback('like')}
>
<ThumbsUp className='w-4 h-4' />
</div>
</TooltipPlus>
<TooltipPlus popupContent={t('appDebug.operation.disagree')}>
<div
className='flex items-center justify-center w-6 h-6 rounded-md hover:bg-black/5 hover:text-gray-800 cursor-pointer'
onClick={() => handleFeedback('dislike')}
>
<ThumbsDown className='w-4 h-4' />
</div>
</TooltipPlus>
</div>
)
}
{
config?.supportFeedback && localFeedback?.rating && onFeedback && !isOpeningStatement && (
<TooltipPlus popupContent={localFeedback.rating === 'like' ? t('appDebug.operation.cancelAgree') : t('appDebug.operation.cancelDisagree')}>
<div
className={`
flex items-center justify-center w-7 h-7 rounded-[10px] border-[2px] border-white cursor-pointer
${localFeedback.rating === 'like' && 'bg-blue-50 text-blue-600'}
${localFeedback.rating === 'dislike' && 'bg-red-100 text-red-600'}
`}
onClick={() => handleFeedback(null)}
>
{
localFeedback.rating === 'like' && (
<ThumbsUp className='w-4 h-4' />
)
}
{
localFeedback.rating === 'dislike' && (
<ThumbsDown className='w-4 h-4' />
)
}
</div>
</TooltipPlus>
)
}
</div>
</>
)
}

View File

@ -0,0 +1,108 @@
import {
useEffect,
useMemo,
useState,
} from 'react'
import cn from 'classnames'
import { useTranslation } from 'react-i18next'
import type { WorkflowProcess } from '../../types'
import { CheckCircle } from '@/app/components/base/icons/src/vender/solid/general'
import { AlertCircle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
import { Loading02 } from '@/app/components/base/icons/src/vender/line/general'
import { ChevronRight } from '@/app/components/base/icons/src/vender/line/arrows'
import { WorkflowRunningStatus } from '@/app/components/workflow/types'
import NodePanel from '@/app/components/workflow/run/node'
type WorkflowProcessProps = {
data: WorkflowProcess
grayBg?: boolean
expand?: boolean
hideInfo?: boolean
}
const WorkflowProcessItem = ({
data,
grayBg,
expand = false,
hideInfo = false,
}: WorkflowProcessProps) => {
const { t } = useTranslation()
const [collapse, setCollapse] = useState(!expand)
const running = data.status === WorkflowRunningStatus.Running
const succeeded = data.status === WorkflowRunningStatus.Succeeded
const failed = data.status === WorkflowRunningStatus.Failed || data.status === WorkflowRunningStatus.Stopped
const background = useMemo(() => {
if (running && !collapse)
return 'linear-gradient(180deg, #E1E4EA 0%, #EAECF0 100%)'
if (succeeded && !collapse)
return 'linear-gradient(180deg, #ECFDF3 0%, #F6FEF9 100%)'
if (failed && !collapse)
return 'linear-gradient(180deg, #FEE4E2 0%, #FEF3F2 100%)'
}, [running, succeeded, failed, collapse])
useEffect(() => {
setCollapse(!expand)
}, [expand])
return (
<div
className={cn(
'mb-2 rounded-xl border-[0.5px] border-black/[0.08]',
collapse ? 'py-[7px]' : hideInfo ? 'pt-2 pb-1' : 'py-2',
collapse && (!grayBg ? 'bg-white' : 'bg-gray-50'),
hideInfo ? 'mx-[-8px] px-1' : 'w-full px-3',
)}
style={{
background,
}}
>
<div
className={cn(
'flex items-center h-[18px] cursor-pointer',
hideInfo && 'px-[6px]',
)}
onClick={() => setCollapse(!collapse)}
>
{
running && (
<Loading02 className='shrink-0 mr-1 w-3 h-3 text-[#667085] animate-spin' />
)
}
{
succeeded && (
<CheckCircle className='shrink-0 mr-1 w-3 h-3 text-[#12B76A]' />
)
}
{
failed && (
<AlertCircle className='shrink-0 mr-1 w-3 h-3 text-[#F04438]' />
)
}
<div className='grow text-xs font-medium text-gray-700'>
{t('workflow.common.workflowProcess')}
</div>
<ChevronRight className={`'ml-1 w-3 h-3 text-gray-500' ${collapse ? '' : 'rotate-90'}`} />
</div>
{
!collapse && (
<div className='mt-1.5'>
{
data.tracing.map(node => (
<div key={node.id} className='mb-1 last-of-type:mb-0'>
<NodePanel
nodeInfo={node}
hideInfo={hideInfo}
/>
</div>
))
}
</div>
)
}
</div>
)
}
export default WorkflowProcessItem

View File

@ -19,6 +19,7 @@ import { useToastContext } from '@/app/components/base/toast'
import { ssePost } from '@/service/base'
import { replaceStringWithValues } from '@/app/components/app/configuration/prompt-value-panel'
import type { Annotation } from '@/models/log'
import { WorkflowRunningStatus } from '@/app/components/workflow/types'
type GetAbortController = (abortController: AbortController) => void
type SendCallback = {
@ -318,10 +319,21 @@ export const useChat = (
const requestion = draft[index - 1]
draft[index - 1] = {
...requestion,
log: newResponseItem.message,
}
draft[index] = {
...draft[index],
log: [
...newResponseItem.message,
...(newResponseItem.message[newResponseItem.message.length - 1].role !== 'assistant'
? [
{
role: 'assistant',
text: newResponseItem.answer,
files: newResponseItem.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
},
]
: []),
],
more: {
time: dayjs.unix(newResponseItem.created_at).format('hh:mm A'),
tokens: newResponseItem.answer_tokens + newResponseItem.message_tokens,
@ -422,6 +434,52 @@ export const useChat = (
})
handleUpdateChatList(newChatList)
},
onWorkflowStarted: ({ workflow_run_id, task_id }) => {
taskIdRef.current = task_id
responseItem.workflow_run_id = workflow_run_id
responseItem.workflowProcess = {
status: WorkflowRunningStatus.Running,
tracing: [],
}
handleUpdateChatList(produce(chatListRef.current, (draft) => {
const currentIndex = draft.findIndex(item => item.id === responseItem.id)
draft[currentIndex] = {
...draft[currentIndex],
...responseItem,
}
}))
},
onWorkflowFinished: ({ data }) => {
responseItem.workflowProcess!.status = data.status as WorkflowRunningStatus
handleUpdateChatList(produce(chatListRef.current, (draft) => {
const currentIndex = draft.findIndex(item => item.id === responseItem.id)
draft[currentIndex] = {
...draft[currentIndex],
...responseItem,
}
}))
},
onNodeStarted: ({ data }) => {
responseItem.workflowProcess!.tracing!.push(data as any)
handleUpdateChatList(produce(chatListRef.current, (draft) => {
const currentIndex = draft.findIndex(item => item.id === responseItem.id)
draft[currentIndex] = {
...draft[currentIndex],
...responseItem,
}
}))
},
onNodeFinished: ({ data }) => {
const currentIndex = responseItem.workflowProcess!.tracing!.findIndex(item => item.node_id === data.node_id)
responseItem.workflowProcess!.tracing[currentIndex] = data as any
handleUpdateChatList(produce(chatListRef.current, (draft) => {
const currentIndex = draft.findIndex(item => item.id === responseItem.id)
draft[currentIndex] = {
...draft[currentIndex],
...responseItem,
}
}))
},
})
return true
}, [

View File

@ -7,6 +7,7 @@ import {
useCallback,
useEffect,
useRef,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import { debounce } from 'lodash-es'
@ -24,6 +25,8 @@ import { ChatContextProvider } from './context'
import type { Emoji } from '@/app/components/tools/types'
import Button from '@/app/components/base/button'
import { StopCircle } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
import PromptLogModal from '@/app/components/base/prompt-log-modal'
import { useStore as useAppStore } from '@/app/components/app/store'
export type ChatProps = {
chatList: ChatItem[]
@ -47,6 +50,7 @@ export type ChatProps = {
onAnnotationRemoved?: (index: number) => void
chatNode?: ReactNode
onFeedback?: (messageId: string, feedback: Feedback) => void
chatAnswerContainerInner?: string
}
const Chat: FC<ChatProps> = ({
config,
@ -70,8 +74,11 @@ const Chat: FC<ChatProps> = ({
onAnnotationRemoved,
chatNode,
onFeedback,
chatAnswerContainerInner,
}) => {
const { t } = useTranslation()
const { currentLogItem, setCurrentLogItem, showPromptLogModal, setShowPromptLogModal } = useAppStore()
const [width, setWidth] = useState(0)
const chatContainerRef = useRef<HTMLDivElement>(null)
const chatContainerInnerRef = useRef<HTMLDivElement>(null)
const chatFooterRef = useRef<HTMLDivElement>(null)
@ -84,6 +91,9 @@ const Chat: FC<ChatProps> = ({
}, [])
const handleWindowResize = useCallback(() => {
if (chatContainerRef.current)
setWidth(document.body.clientWidth - (chatContainerRef.current?.clientWidth + 16) - 8)
if (chatContainerRef.current && chatFooterRef.current)
chatFooterRef.current.style.width = `${chatContainerRef.current.clientWidth}px`
@ -182,6 +192,8 @@ const Chat: FC<ChatProps> = ({
answerIcon={answerIcon}
responding={isLast && isResponding}
allToolIcons={allToolIcons}
showPromptLog={showPromptLog}
chatAnswerContainerInner={chatAnswerContainerInner}
/>
)
}
@ -189,9 +201,7 @@ const Chat: FC<ChatProps> = ({
<Question
key={item.id}
item={item}
showPromptLog={showPromptLog}
questionIcon={questionIcon}
isResponding={isResponding}
/>
)
})
@ -238,6 +248,16 @@ const Chat: FC<ChatProps> = ({
}
</div>
</div>
{showPromptLogModal && (
<PromptLogModal
width={width}
currentLogItem={currentLogItem}
onCancel={() => {
setCurrentLogItem()
setShowPromptLogModal(false)
}}
/>
)}
</div>
</ChatContextProvider>
)

View File

@ -4,28 +4,21 @@ import type {
} from 'react'
import {
memo,
useRef,
} from 'react'
import type { ChatItem } from '../types'
import { QuestionTriangle } from '@/app/components/base/icons/src/vender/solid/general'
import { User } from '@/app/components/base/icons/src/public/avatar'
import Log from '@/app/components/app/chat/log'
import { Markdown } from '@/app/components/base/markdown'
import ImageGallery from '@/app/components/base/image-gallery'
type QuestionProps = {
item: ChatItem
showPromptLog?: boolean
questionIcon?: ReactNode
isResponding?: boolean
}
const Question: FC<QuestionProps> = ({
item,
showPromptLog,
isResponding,
questionIcon,
}) => {
const ref = useRef(null)
const {
content,
message_files,
@ -34,14 +27,9 @@ const Question: FC<QuestionProps> = ({
const imgSrcs = message_files?.length ? message_files.map(item => item.url) : []
return (
<div className='flex justify-end mb-2 last:mb-0 pl-10' ref={ref}>
<div className='flex justify-end mb-2 last:mb-0 pl-10'>
<div className='group relative mr-4'>
<QuestionTriangle className='absolute -right-2 top-0 w-2 h-3 text-[#D1E9FF]/50' />
{
showPromptLog && !isResponding && (
<Log log={item.log!} containerRef={ref} />
)
}
<div className='px-4 py-3 bg-[#D1E9FF]/50 rounded-b-2xl rounded-tl-2xl text-sm text-gray-900'>
{
!!imgSrcs.length && (

View File

@ -4,6 +4,8 @@ import type {
VisionSettings,
} from '@/types/app'
import type { IChatItem } from '@/app/components/app/chat/type'
import type { NodeTracing } from '@/types/workflow'
import type { WorkflowRunningStatus } from '@/app/components/workflow/types'
export type { VisionFile } from '@/types/app'
export { TransferMethod } from '@/types/app'
@ -48,7 +50,16 @@ export type ChatConfig = Omit<ModelConfig, 'model'> & {
supportCitationHitInfo?: boolean
}
export type ChatItem = IChatItem
export type WorkflowProcess = {
status: WorkflowRunningStatus
tracing: NodeTracing[]
expand?: boolean // for UI
}
export type ChatItem = IChatItem & {
isError?: boolean
workflowProcess?: WorkflowProcess
}
export type OnSend = (message: string, files?: VisionFile[]) => void

View File

@ -2,6 +2,7 @@
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import Button from '../button'
export type IConfirmUIProps = {
type: 'info' | 'warning'
@ -41,8 +42,8 @@ const ConfirmUI: FC<IConfirmUIProps> = ({
</div>
<div className='flex gap-3 mt-4 ml-12'>
<div onClick={onConfirm} className='w-20 leading-9 text-center text-white border rounded-lg cursor-pointer h-9 border-color-primary-700 bg-primary-700'>{confirmText || t('common.operation.confirm')}</div>
<div onClick={onCancel} className='w-20 leading-9 text-center text-gray-500 border rounded-lg cursor-pointer h-9 border-color-gray-200'>{cancelText || t('common.operation.cancel')}</div>
<Button type='primary' onClick={onConfirm} className='flex items-center justify-center w-20 text-center text-white rounded-lg cursor-pointer h-9 '>{confirmText || t('common.operation.confirm')}</Button>
<Button onClick={onCancel} className='flex items-center justify-center w-20 text-center text-gray-500 border rounded-lg cursor-pointer h-9 border-color-gray-200'>{cancelText || t('common.operation.cancel')}</Button>
</div>
</div>

Some files were not shown because too many files have changed in this diff Show More