+
{children}
diff --git a/web/app/components/app-sidebar/app-info.tsx b/web/app/components/app-sidebar/app-info.tsx
index 12fe5cba468df3..12f9c59cd16251 100644
--- a/web/app/components/app-sidebar/app-info.tsx
+++ b/web/app/components/app-sidebar/app-info.tsx
@@ -237,7 +237,7 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
{appDetail.mode === 'advanced-chat' && (
<>
{t('app.types.chatbot').toUpperCase()}
-
{t('app.newApp.advanced').toUpperCase()}
+
{t('app.types.advanced').toUpperCase()}
>
)}
{appDetail.mode === 'agent-chat' && (
@@ -246,13 +246,13 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
{appDetail.mode === 'chat' && (
<>
{t('app.types.chatbot').toUpperCase()}
-
{(t('app.newApp.basic').toUpperCase())}
+
{(t('app.types.basic').toUpperCase())}
>
)}
{appDetail.mode === 'completion' && (
<>
{t('app.types.completion').toUpperCase()}
-
{(t('app.newApp.basic').toUpperCase())}
+
{(t('app.types.basic').toUpperCase())}
>
)}
{appDetail.mode === 'workflow' && (
@@ -299,7 +299,7 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
{appDetail.mode === 'advanced-chat' && (
<>
{t('app.types.chatbot').toUpperCase()}
-
{t('app.newApp.advanced').toUpperCase()}
+
{t('app.types.advanced').toUpperCase()}
>
)}
{appDetail.mode === 'agent-chat' && (
@@ -308,13 +308,13 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
{appDetail.mode === 'chat' && (
<>
{t('app.types.chatbot').toUpperCase()}
-
{(t('app.newApp.basic').toUpperCase())}
+
{(t('app.types.basic').toUpperCase())}
>
)}
{appDetail.mode === 'completion' && (
<>
{t('app.types.completion').toUpperCase()}
-
{(t('app.newApp.basic').toUpperCase())}
+
{(t('app.types.basic').toUpperCase())}
>
)}
{appDetail.mode === 'workflow' && (
@@ -398,7 +398,7 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
)} />
- {showSwitchTip === 'chat' ? t('app.newApp.advanced') : t('app.types.workflow')}
+ {showSwitchTip === 'chat' ? t('app.types.advanced') : t('app.types.workflow')}
BETA
{t('app.newApp.advancedFor').toLocaleUpperCase()}
diff --git a/web/app/components/app/annotation/add-annotation-modal/edit-item/index.tsx b/web/app/components/app/annotation/add-annotation-modal/edit-item/index.tsx
index 4da6b7cac4d0b4..032e4b83576adf 100644
--- a/web/app/components/app/annotation/add-annotation-modal/edit-item/index.tsx
+++ b/web/app/components/app/annotation/add-annotation-modal/edit-item/index.tsx
@@ -2,7 +2,7 @@
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
-import Textarea from 'rc-textarea'
+import Textarea from '@/app/components/base/textarea'
import { Robot, User } from '@/app/components/base/icons/src/public/avatar'
export enum EditItemType {
@@ -31,12 +31,10 @@ const EditItem: FC
= ({
{avatar}
)}
-
+
-
setIsCreateNext(!isCreateNext)} className="w-4 h-4 rounded border-gray-300 text-blue-700 focus:ring-blue-700" />
+
setIsCreateNext(!isCreateNext)} />
{t('appAnnotation.addModal.createNext')}
diff --git a/web/app/components/app/annotation/batch-add-annotation-modal/csv-downloader.tsx b/web/app/components/app/annotation/batch-add-annotation-modal/csv-downloader.tsx
index fbbea706122c42..d2189b4581c853 100644
--- a/web/app/components/app/annotation/batch-add-annotation-modal/csv-downloader.tsx
+++ b/web/app/components/app/annotation/batch-add-annotation-modal/csv-downloader.tsx
@@ -33,19 +33,19 @@ const CSVDownload: FC = () => {
return (
-
{t('share.generation.csvStructureTitle')}
+
{t('share.generation.csvStructureTitle')}
-
-
+
+
- {t('appAnnotation.batchModal.question')} |
- {t('appAnnotation.batchModal.answer')} |
+ {t('appAnnotation.batchModal.question')} |
+ {t('appAnnotation.batchModal.answer')} |
- {t('appAnnotation.batchModal.question')} 1 |
- {t('appAnnotation.batchModal.answer')} 1 |
+ {t('appAnnotation.batchModal.question')} 1 |
+ {t('appAnnotation.batchModal.answer')} 1 |
{t('appAnnotation.batchModal.question')} 2 |
@@ -61,7 +61,7 @@ const CSVDownload: FC = () => {
bom={true}
data={getTemplate()}
>
-
+
{t('appAnnotation.batchModal.template')}
diff --git a/web/app/components/app/annotation/batch-add-annotation-modal/csv-uploader.tsx b/web/app/components/app/annotation/batch-add-annotation-modal/csv-uploader.tsx
index 88ce23b9aa9072..d37593c784b6bb 100644
--- a/web/app/components/app/annotation/batch-add-annotation-modal/csv-uploader.tsx
+++ b/web/app/components/app/annotation/batch-add-annotation-modal/csv-uploader.tsx
@@ -91,29 +91,29 @@ const CSVUploader: FC
= ({
/>
{!file && (
-
+
-
+
{t('appAnnotation.batchModal.csvUploadTitle')}
- {t('appAnnotation.batchModal.browse')}
+ {t('appAnnotation.batchModal.browse')}
{dragging &&
}
)}
{file && (
-
+
- {file.name.replace(/.csv$/, '')}
- .csv
+ {file.name.replace(/.csv$/, '')}
+ .csv
-
-
+
+
-
+
diff --git a/web/app/components/app/annotation/batch-add-annotation-modal/index.tsx b/web/app/components/app/annotation/batch-add-annotation-modal/index.tsx
index 8295df6e4dfd91..9a496a1989da2e 100644
--- a/web/app/components/app/annotation/batch-add-annotation-modal/index.tsx
+++ b/web/app/components/app/annotation/batch-add-annotation-modal/index.tsx
@@ -88,9 +88,9 @@ const BatchModal: FC
= ({
return (
{ }} className='px-8 py-6 !max-w-[520px] !rounded-xl'>
- {t('appAnnotation.batchModal.title')}
+ {t('appAnnotation.batchModal.title')}
-
+
= ({
)}
-
diff --git a/web/app/components/app/annotation/view-annotation-modal/hit-history-no-data.tsx b/web/app/components/app/annotation/view-annotation-modal/hit-history-no-data.tsx
index b4c4f8fc0d7de7..6f7c32283a4866 100644
--- a/web/app/components/app/annotation/view-annotation-modal/hit-history-no-data.tsx
+++ b/web/app/components/app/annotation/view-annotation-modal/hit-history-no-data.tsx
@@ -7,11 +7,11 @@ import { ClockFastForward } from '@/app/components/base/icons/src/vender/line/ti
const HitHistoryNoData: FC = () => {
const { t } = useTranslation()
return (
-
-
-
+
+
+
-
{t('appAnnotation.viewModal.noHitHistory')}
+
{t('appAnnotation.viewModal.noHitHistory')}
)
}
diff --git a/web/app/components/app/annotation/view-annotation-modal/index.tsx b/web/app/components/app/annotation/view-annotation-modal/index.tsx
index 0fb8bbc31e3e40..83a64b980f68b7 100644
--- a/web/app/components/app/annotation/view-annotation-modal/index.tsx
+++ b/web/app/components/app/annotation/view-annotation-modal/index.tsx
@@ -4,17 +4,17 @@ import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import EditItem, { EditItemType } from '../edit-annotation-modal/edit-item'
import type { AnnotationItem, HitHistoryItem } from '../type'
-import s from './style.module.css'
import HitHistoryNoData from './hit-history-no-data'
-import cn from '@/utils/classnames'
-import Pagination from '@/app/components/base/pagination'
+import Badge from '@/app/components/base/badge'
import Drawer from '@/app/components/base/drawer-plus'
+import Pagination from '@/app/components/base/pagination'
import { MessageCheckRemove } from '@/app/components/base/icons/src/vender/line/communication'
import Confirm from '@/app/components/base/confirm'
import TabSlider from '@/app/components/base/tab-slider-plain'
import { fetchHitHistoryList } from '@/service/annotation'
import { APP_PAGE_LIMIT } from '@/config'
import useTimestamp from '@/hooks/use-timestamp'
+import cn from '@/utils/classnames'
type Props = {
appId: string
@@ -72,7 +72,9 @@ const ViewAnnotationModal: FC
= ({
? (
{t('appAnnotation.viewModal.hitHistory')}
-
{total} {t(`appAnnotation.viewModal.hit${hitHistoryList.length > 1 ? 's' : ''}`)}
+
1 ? 's' : ''}`)}`}
+ />
)
: t('appAnnotation.viewModal.hitHistory')
@@ -111,44 +113,45 @@ const ViewAnnotationModal: FC = ({
? ()
: (
-
-
-
- {t('appAnnotation.hitHistoryTable.query')} |
- {t('appAnnotation.hitHistoryTable.match')} |
- {t('appAnnotation.hitHistoryTable.response')} |
- {t('appAnnotation.hitHistoryTable.source')} |
- {t('appAnnotation.hitHistoryTable.score')} |
- {t('appAnnotation.hitHistoryTable.time')} |
+
+
+
+ {t('appAnnotation.hitHistoryTable.query')} |
+ {t('appAnnotation.hitHistoryTable.match')} |
+ {t('appAnnotation.hitHistoryTable.response')} |
+ {t('appAnnotation.hitHistoryTable.source')} |
+ {t('appAnnotation.hitHistoryTable.score')} |
+ {t('appAnnotation.hitHistoryTable.time')} |
-
+
{hitHistoryList.map(item => (
{item.question} |
{item.match} |
{item.response} |
- {item.source} |
- {item.score ? item.score.toFixed(2) : '-'} |
- {formatTime(item.created_at, t('appLog.dateTimeFormat') as string)} |
+ {item.source} |
+ {item.score ? item.score.toFixed(2) : '-'} |
+ {formatTime(item.created_at, t('appLog.dateTimeFormat') as string)} |
))}
{(total && total > APP_PAGE_LIMIT)
? = ({
isShow={isShow}
onHide={onHide}
maxWidthClassName='!max-w-[800px]'
- // t('appAnnotation.editModal.title') as string
title={
= ({
)}
foot={id
? (
-
+
setShowModal(true)}
diff --git a/web/app/components/app/annotation/view-annotation-modal/style.module.css b/web/app/components/app/annotation/view-annotation-modal/style.module.css
deleted file mode 100644
index bf45136283590c..00000000000000
--- a/web/app/components/app/annotation/view-annotation-modal/style.module.css
+++ /dev/null
@@ -1,9 +0,0 @@
-.table td {
- padding: 7px 8px;
- box-sizing: border-box;
- max-width: 200px;
-}
-
-.pagination li {
- list-style: none;
-}
\ No newline at end of file
diff --git a/web/app/components/app/app-publisher/index.tsx b/web/app/components/app/app-publisher/index.tsx
index 0558e299560396..3ba35a73369d37 100644
--- a/web/app/components/app/app-publisher/index.tsx
+++ b/web/app/components/app/app-publisher/index.tsx
@@ -5,7 +5,8 @@ import {
} from 'react'
import { useTranslation } from 'react-i18next'
import dayjs from 'dayjs'
-import { RiArrowDownSLine } from '@remixicon/react'
+import { RiArrowDownSLine, RiPlanetLine } from '@remixicon/react'
+import Toast from '../../base/toast'
import type { ModelAndParameter } from '../configuration/debug/types'
import SuggestedAction from './suggested-action'
import PublishWithMultipleModel from './publish-with-multiple-model'
@@ -15,6 +16,7 @@ import {
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
+import { fetchInstalledAppList } from '@/service/explore'
import EmbeddedModal from '@/app/components/app/overview/embedded'
import { useStore as useAppStore } from '@/app/components/app/store'
import { useGetLanguage } from '@/context/i18n'
@@ -105,6 +107,19 @@ const AppPublisher = ({
setPublished(false)
}, [disabled, onToggle, open])
+ const handleOpenInExplore = useCallback(async () => {
+ try {
+ const { installed_apps }: any = await fetchInstalledAppList(appDetail?.id) || {}
+ if (installed_apps?.length > 0)
+ window.open(`/explore/installed/${installed_apps[0].id}`, '_blank')
+ else
+ throw new Error('No app found in Explore')
+ }
+ catch (e: any) {
+ Toast.notify({ type: 'error', message: `${e.message || e}` })
+ }
+ }, [appDetail?.id])
+
const [embeddingModalOpen, setEmbeddingModalOpen] = useState(false)
return (
@@ -205,6 +220,15 @@ const AppPublisher = ({
{t('workflow.common.embedIntoSite')}
)}
+
{
+ handleOpenInExplore()
+ }}
+ disabled={!publishedAt}
+ icon={}
+ >
+ {t('workflow.common.openInExplore')}
+
}>{t('workflow.common.accessAPIReference')}
{appDetail?.mode === 'workflow' && (
void
+}
+
+const AppCard = ({
+ app,
+ onCreate,
+}: AppCardProps) => {
+ const { t } = useTranslation()
+ const { app: appBasicInfo } = app
+ return (
+
+
+
+
+
+ {appBasicInfo.name}
+
+
+
+
+
+
+ {app.description}
+
+
+
+
+
onCreate()}>
+
+ {t('app.newApp.useTemplate')}
+
+
+
+
+ )
+}
+
+export default AppCard
diff --git a/web/app/components/app/create-app-dialog/app-list/index.tsx b/web/app/components/app/create-app-dialog/app-list/index.tsx
new file mode 100644
index 00000000000000..c9354ce2e146af
--- /dev/null
+++ b/web/app/components/app/create-app-dialog/app-list/index.tsx
@@ -0,0 +1,247 @@
+'use client'
+
+import React, { useMemo, useState } from 'react'
+import { useRouter } from 'next/navigation'
+import { useTranslation } from 'react-i18next'
+import { useContext } from 'use-context-selector'
+import useSWR from 'swr'
+import { useDebounceFn } from 'ahooks'
+import { RiRobot2Line } from '@remixicon/react'
+import AppCard from '../app-card'
+import Sidebar, { AppCategories, AppCategoryLabel } from './sidebar'
+import Toast from '@/app/components/base/toast'
+import Divider from '@/app/components/base/divider'
+import cn from '@/utils/classnames'
+import ExploreContext from '@/context/explore-context'
+import type { App } from '@/models/explore'
+import { fetchAppDetail, fetchAppList } from '@/service/explore'
+import { importDSL } from '@/service/apps'
+import { useTabSearchParams } from '@/hooks/use-tab-searchparams'
+import CreateAppModal from '@/app/components/explore/create-app-modal'
+import AppTypeSelector from '@/app/components/app/type-selector'
+import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal'
+import Loading from '@/app/components/base/loading'
+import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
+import { useAppContext } from '@/context/app-context'
+import { getRedirection } from '@/utils/app-redirection'
+import Input from '@/app/components/base/input'
+import type { AppMode } from '@/types/app'
+import { DSLImportMode } from '@/models/app'
+
+type AppsProps = {
+ onSuccess?: () => void
+ onCreateFromBlank?: () => void
+}
+
+// export enum PageType {
+// EXPLORE = 'explore',
+// CREATE = 'create',
+// }
+
+const Apps = ({
+ onSuccess,
+ onCreateFromBlank,
+}: AppsProps) => {
+ const { t } = useTranslation()
+ const { isCurrentWorkspaceEditor } = useAppContext()
+ const { push } = useRouter()
+ const { hasEditPermission } = useContext(ExploreContext)
+ const allCategoriesEn = AppCategories.RECOMMENDED
+
+ const [keywords, setKeywords] = useState('')
+ const [searchKeywords, setSearchKeywords] = useState('')
+
+ const { run: handleSearch } = useDebounceFn(() => {
+ setSearchKeywords(keywords)
+ }, { wait: 500 })
+
+ const handleKeywordsChange = (value: string) => {
+ setKeywords(value)
+ handleSearch()
+ }
+
+ const [currentType, setCurrentType] = useState([])
+ const [currCategory, setCurrCategory] = useTabSearchParams({
+ defaultTab: allCategoriesEn,
+ disableSearchParams: true,
+ })
+
+ const {
+ data: { categories, allList },
+ } = useSWR(
+ ['/explore/apps'],
+ () =>
+ fetchAppList().then(({ categories, recommended_apps }) => ({
+ categories,
+ allList: recommended_apps.sort((a, b) => a.position - b.position),
+ })),
+ {
+ fallbackData: {
+ categories: [],
+ allList: [],
+ },
+ },
+ )
+
+ const filteredList = useMemo(() => {
+ const filteredByCategory = allList.filter((item) => {
+ if (currCategory === allCategoriesEn)
+ return true
+ return item.category === currCategory
+ })
+ if (currentType.length === 0)
+ return filteredByCategory
+ return filteredByCategory.filter((item) => {
+ if (currentType.includes('chat') && item.app.mode === 'chat')
+ return true
+ if (currentType.includes('advanced-chat') && item.app.mode === 'advanced-chat')
+ return true
+ if (currentType.includes('agent-chat') && item.app.mode === 'agent-chat')
+ return true
+ if (currentType.includes('completion') && item.app.mode === 'completion')
+ return true
+ if (currentType.includes('workflow') && item.app.mode === 'workflow')
+ return true
+ return false
+ })
+ }, [currentType, currCategory, allCategoriesEn, allList])
+
+ const searchFilteredList = useMemo(() => {
+ if (!searchKeywords || !filteredList || filteredList.length === 0)
+ return filteredList
+
+ const lowerCaseSearchKeywords = searchKeywords.toLowerCase()
+
+ return filteredList.filter(item =>
+ item.app && item.app.name && item.app.name.toLowerCase().includes(lowerCaseSearchKeywords),
+ )
+ }, [searchKeywords, filteredList])
+
+ const [currApp, setCurrApp] = React.useState(null)
+ const [isShowCreateModal, setIsShowCreateModal] = React.useState(false)
+ const onCreate: CreateAppModalProps['onConfirm'] = async ({
+ name,
+ icon_type,
+ icon,
+ icon_background,
+ description,
+ }) => {
+ const { export_data } = await fetchAppDetail(
+ currApp?.app.id as string,
+ )
+ try {
+ const app = await importDSL({
+ mode: DSLImportMode.YAML_CONTENT,
+ yaml_content: export_data,
+ name,
+ icon_type,
+ icon,
+ icon_background,
+ description,
+ })
+ setIsShowCreateModal(false)
+ Toast.notify({
+ type: 'success',
+ message: t('app.newApp.appCreated'),
+ })
+ if (onSuccess)
+ onSuccess()
+ localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
+ getRedirection(isCurrentWorkspaceEditor, app, push)
+ }
+ catch (e) {
+ Toast.notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
+ }
+ }
+
+ if (!categories || categories.length === 0) {
+ return (
+
+
+
+ )
+ }
+
+ return (
+
+
+
+ {t('app.newApp.startFromTemplate')}
+
+
+
+
+
handleKeywordsChange(e.target.value)}
+ onClear={() => handleKeywordsChange('')}
+ />
+
+
+
+
+ {!searchKeywords &&
+ { setCurrCategory(category) }} onCreateFromBlank={onCreateFromBlank} />
+
}
+
+ {searchFilteredList && searchFilteredList.length > 0 && <>
+
+ {searchKeywords
+ ?
{searchFilteredList.length > 1 ? t('app.newApp.foundResults', { count: searchFilteredList.length }) : t('app.newApp.foundResult', { count: searchFilteredList.length })}
+ :
}
+
+
+ {searchFilteredList.map(app => (
+
{
+ setCurrApp(app)
+ setIsShowCreateModal(true)
+ }}
+ />
+ ))}
+
+ >}
+ {(!searchFilteredList || searchFilteredList.length === 0) &&
}
+
+
+ {isShowCreateModal && (
+
setIsShowCreateModal(false)}
+ />
+ )}
+
+ )
+}
+
+export default React.memo(Apps)
+
+function NoTemplateFound() {
+ const { t } = useTranslation()
+ return
+
+
+
+
{t('app.newApp.noTemplateFound')}
+
{t('app.newApp.noTemplateFoundTip')}
+
+}
diff --git a/web/app/components/app/create-app-dialog/app-list/sidebar.tsx b/web/app/components/app/create-app-dialog/app-list/sidebar.tsx
new file mode 100644
index 00000000000000..73b34c7d02461c
--- /dev/null
+++ b/web/app/components/app/create-app-dialog/app-list/sidebar.tsx
@@ -0,0 +1,91 @@
+'use client'
+import { RiAppsFill, RiChatSmileAiFill, RiExchange2Fill, RiPassPendingFill, RiQuillPenAiFill, RiSpeakAiFill, RiStickyNoteAddLine, RiTerminalBoxFill, RiThumbUpFill } from '@remixicon/react'
+import { useTranslation } from 'react-i18next'
+import classNames from '@/utils/classnames'
+import Divider from '@/app/components/base/divider'
+
+export enum AppCategories {
+ RECOMMENDED = 'Recommended',
+ ASSISTANT = 'Assistant',
+ AGENT = 'Agent',
+ HR = 'HR',
+ PROGRAMMING = 'Programming',
+ WORKFLOW = 'Workflow',
+ WRITING = 'Writing',
+}
+
+type SidebarProps = {
+ current: AppCategories
+ onClick?: (category: AppCategories) => void
+ onCreateFromBlank?: () => void
+}
+
+export default function Sidebar({ current, onClick, onCreateFromBlank }: SidebarProps) {
+ const { t } = useTranslation()
+ return
+
+
{t('app.newAppFromTemplate.byCategories')}
+
+
+
+
+ {t('app.newApp.startFromBlank')}
+
+
+}
+
+type CategoryItemProps = {
+ active: boolean
+ category: AppCategories
+ onClick?: (category: AppCategories) => void
+}
+function CategoryItem({ category, active, onClick }: CategoryItemProps) {
+ return { onClick?.(category) }}>
+
+
+
+}
+
+type AppCategoryLabelProps = {
+ category: AppCategories
+ className?: string
+}
+export function AppCategoryLabel({ category, className }: AppCategoryLabelProps) {
+ const { t } = useTranslation()
+ return {t(`app.newAppFromTemplate.sidebar.${category}`)}
+}
+
+type AppCategoryIconProps = {
+ category: AppCategories
+}
+function AppCategoryIcon({ category }: AppCategoryIconProps) {
+ if (category === AppCategories.AGENT)
+ return
+ if (category === AppCategories.ASSISTANT)
+ return
+ if (category === AppCategories.HR)
+ return
+ if (category === AppCategories.PROGRAMMING)
+ return
+ if (category === AppCategories.RECOMMENDED)
+ return
+ if (category === AppCategories.WRITING)
+ return
+ if (category === AppCategories.WORKFLOW)
+ return
+ return
+}
diff --git a/web/app/components/app/create-app-dialog/index.tsx b/web/app/components/app/create-app-dialog/index.tsx
index 13620a36f3c539..acc3650211d390 100644
--- a/web/app/components/app/create-app-dialog/index.tsx
+++ b/web/app/components/app/create-app-dialog/index.tsx
@@ -1,36 +1,26 @@
'use client'
-import { useTranslation } from 'react-i18next'
-import { RiCloseLine } from '@remixicon/react'
-import NewAppDialog from './newAppDialog'
-import AppList, { PageType } from '@/app/components/explore/app-list'
+import AppList from './app-list'
+import FullScreenModal from '@/app/components/base/fullscreen-modal'
type CreateAppDialogProps = {
show: boolean
onSuccess: () => void
onClose: () => void
+ onCreateFromBlank?: () => void
}
-const CreateAppTemplateDialog = ({ show, onSuccess, onClose }: CreateAppDialogProps) => {
- const { t } = useTranslation()
-
+const CreateAppTemplateDialog = ({ show, onSuccess, onClose, onCreateFromBlank }: CreateAppDialogProps) => {
return (
- {}}
+
- {/* template list */}
-
-
{t('app.newApp.startFromTemplate')}
-
{
- onSuccess()
- onClose()
- }} pageType={PageType.CREATE} />
-
-
-
-
-
+ {
+ onSuccess()
+ onClose()
+ }} />
+
)
}
diff --git a/web/app/components/app/create-app-modal/advanced.png b/web/app/components/app/create-app-modal/advanced.png
deleted file mode 100644
index 384e29831c5530..00000000000000
Binary files a/web/app/components/app/create-app-modal/advanced.png and /dev/null differ
diff --git a/web/app/components/app/create-app-modal/basic.png b/web/app/components/app/create-app-modal/basic.png
deleted file mode 100644
index 6f45192b02e10b..00000000000000
Binary files a/web/app/components/app/create-app-modal/basic.png and /dev/null differ
diff --git a/web/app/components/app/create-app-modal/grid-bg-agent-chat.svg b/web/app/components/app/create-app-modal/grid-bg-agent-chat.svg
deleted file mode 100644
index 971d5c39ab11ed..00000000000000
--- a/web/app/components/app/create-app-modal/grid-bg-agent-chat.svg
+++ /dev/null
@@ -1,16 +0,0 @@
-
diff --git a/web/app/components/app/create-app-modal/grid-bg-chat.svg b/web/app/components/app/create-app-modal/grid-bg-chat.svg
deleted file mode 100644
index 7a3e1a83ec5584..00000000000000
--- a/web/app/components/app/create-app-modal/grid-bg-chat.svg
+++ /dev/null
@@ -1,16 +0,0 @@
-
diff --git a/web/app/components/app/create-app-modal/grid-bg-completion.svg b/web/app/components/app/create-app-modal/grid-bg-completion.svg
deleted file mode 100644
index 9f80a6c440ae4d..00000000000000
--- a/web/app/components/app/create-app-modal/grid-bg-completion.svg
+++ /dev/null
@@ -1,16 +0,0 @@
-
diff --git a/web/app/components/app/create-app-modal/grid-bg-workflow.svg b/web/app/components/app/create-app-modal/grid-bg-workflow.svg
deleted file mode 100644
index 144beda82c4c27..00000000000000
--- a/web/app/components/app/create-app-modal/grid-bg-workflow.svg
+++ /dev/null
@@ -1,16 +0,0 @@
-
diff --git a/web/app/components/app/create-app-modal/index.tsx b/web/app/components/app/create-app-modal/index.tsx
index 496de58c19e6f0..eb78258cc86622 100644
--- a/web/app/components/app/create-app-modal/index.tsx
+++ b/web/app/components/app/create-app-modal/index.tsx
@@ -1,48 +1,46 @@
'use client'
-import type { MouseEventHandler } from 'react'
+
import { useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
-import {
- RiCloseLine,
- RiQuestionLine,
-} from '@remixicon/react'
+
import { useRouter } from 'next/navigation'
import { useContext, useContextSelector } from 'use-context-selector'
+import { RiArrowRightLine, RiCommandLine, RiCornerDownLeftLine, RiExchange2Fill } from '@remixicon/react'
+import Link from 'next/link'
+import { useDebounceFn, useKeyPress } from 'ahooks'
+import Image from 'next/image'
import AppIconPicker from '../../base/app-icon-picker'
import type { AppIconSelection } from '../../base/app-icon-picker'
-import s from './style.module.css'
+import Button from '@/app/components/base/button'
+import Divider from '@/app/components/base/divider'
import cn from '@/utils/classnames'
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 Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea'
import AppIcon from '@/app/components/base/app-icon'
import AppsFull from '@/app/components/billing/apps-full-in-dialog'
-import { AiText, ChatBot, CuteRobot } from '@/app/components/base/icons/src/vender/solid/communication'
-import { Route } from '@/app/components/base/icons/src/vender/solid/mapsAndTravel'
-import Tooltip from '@/app/components/base/tooltip'
+import { BubbleTextMod, ChatBot, ListSparkle, Logic } from '@/app/components/base/icons/src/vender/solid/communication'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { getRedirection } from '@/utils/app-redirection'
+import FullScreenModal from '@/app/components/base/fullscreen-modal'
-type CreateAppDialogProps = {
- show: boolean
+type CreateAppProps = {
onSuccess: () => void
onClose: () => void
+ onCreateFromTemplate?: () => void
}
-const CreateAppModal = ({ show, onSuccess, onClose }: CreateAppDialogProps) => {
+function CreateApp({ onClose, onSuccess, onCreateFromTemplate }: CreateAppProps) {
const { t } = useTranslation()
const { push } = useRouter()
const { notify } = useContext(ToastContext)
const mutateApps = useContextSelector(AppsContext, state => state.mutateApps)
const [appMode, setAppMode] = useState('chat')
- const [showChatBotType, setShowChatBotType] = useState(true)
const [appIcon, setAppIcon] = useState({ type: 'emoji', icon: '🤖', background: '#FFEAD5' })
const [showAppIconPicker, setShowAppIconPicker] = useState(false)
const [name, setName] = useState('')
@@ -53,7 +51,8 @@ const CreateAppModal = ({ show, onSuccess, onClose }: CreateAppDialogProps) => {
const { isCurrentWorkspaceEditor } = useAppContext()
const isCreatingRef = useRef(false)
- const onCreate: MouseEventHandler = useCallback(async () => {
+
+ const onCreate = useCallback(async () => {
if (!appMode) {
notify({ type: 'error', message: t('app.newApp.appTypeRequired') })
return
@@ -87,237 +86,281 @@ const CreateAppModal = ({ show, onSuccess, onClose }: CreateAppDialogProps) => {
isCreatingRef.current = false
}, [name, notify, t, appMode, appIcon, description, onSuccess, onClose, mutateApps, push, isCurrentWorkspaceEditor])
- return (
- { }}
- >
- {/* Heading */}
-
-
{t('app.newApp.startFromBlank')}
-
- {/* app type */}
-
-
{t('app.newApp.captionAppType')}
-
- {t('app.newApp.chatbotDescription')}
- }
- >
-
{
- setAppMode('chat')
- setShowChatBotType(true)
- }}
- >
-
-
{t('app.types.chatbot')}
+ const { run: handleCreateApp } = useDebounceFn(onCreate, { wait: 300 })
+ useKeyPress(['meta.enter', 'ctrl.enter'], () => {
+ if (isAppsFull)
+ return
+ handleCreateApp()
+ })
+ return <>
+
+
+
+
+
+ {t('app.newApp.startFromBlank')}
+
+
+ {t('app.newApp.chooseAppType')}
+
+
+
+
+ {t('app.newApp.forBeginners')}
+
+
}
+ onClick={() => {
+ setAppMode('chat')
+ }} />
+
+
+ }
+ onClick={() => {
+ setAppMode('agent-chat')
+ }} />
+
+
+ }
+ onClick={() => {
+ setAppMode('completion')
+ }} />
+
-
-
- {t('app.newApp.completionDescription')}
+
+
+ {t('app.newApp.forAdvanced')}
+
+
}
+ onClick={() => {
+ setAppMode('advanced-chat')
+ }} />
+
+
+ }
+ onClick={() => {
+ setAppMode('workflow')
+ }} />
- }
- >
-
{
- setAppMode('completion')
- setShowChatBotType(false)
- }}
- >
-
-
{t('app.newApp.completeApp')}
-
-
{t('app.newApp.agentDescription')}
- }
- >
-
{
- setAppMode('agent-chat')
- setShowChatBotType(false)
- }}
- >
-
-
{t('app.types.agent')}
+
+
+
+
+
+
+
setName(e.target.value)}
+ placeholder={t('app.newApp.appNamePlaceholder') || ''}
+ />
+
+
{ setShowAppIconPicker(true) }}
+ />
+ {showAppIconPicker && {
+ setAppIcon(payload)
+ setShowAppIconPicker(false)
+ }}
+ onClose={() => {
+ setShowAppIconPicker(false)
+ }}
+ />}
-
-
- {t('app.newApp.workflowDescription')}
+
+
+
+ ({t('app.newApp.optional')})
- }
- >
-
{
- setAppMode('workflow')
- setShowChatBotType(false)
- }}
- >
-
-
{t('app.types.workflow')}
-
BETA
+
-
-
-
- {showChatBotType && (
-
-
{t('app.newApp.chatbotType')}
-
-
{
- setAppMode('chat')
- }}
- >
-
-
{t('app.newApp.basic')}
-
-
-
-
-
-
-
{t('app.newApp.basic')}
-
{t('app.newApp.basicFor')}
-
-
{t('app.newApp.basicDescription')}
-
-
-
+
+
+
+
{t('app.newApp.noIdeaTip')}
+
+
-
{t('app.newApp.basicTip')}
-
{
- setAppMode('advanced-chat')
- }}
- >
-
-
-
{t('app.newApp.advanced')}
-
BETA
-
-
-
-
-
-
-
-
-
{t('app.newApp.advanced')}
-
BETA
-
-
{t('app.newApp.advancedFor').toLocaleUpperCase()}
-
-
{t('app.newApp.advancedDescription')}
-
-
+
+
{t('app.newApp.Cancel')}
+
+ {t('app.newApp.Create')}
+
+
+
-
-
{t('app.newApp.advancedFor')}
+
- )}
-
- {/* icon & name */}
-
-
{t('app.newApp.captionName')}
-
- {showAppIconPicker &&
{
- setAppIcon(payload)
- setShowAppIconPicker(false)
- }}
- onClose={() => {
- setShowAppIconPicker(false)
- }}
- />}
- {/* description */}
-
-
{t('app.newApp.captionDescription')}
-
+ {
+ isAppsFull && (
- )}
-
- {t('app.newApp.Cancel')}
- {t('app.newApp.Create')}
-
-
-
-
-
+ )
+ }
+ >
+}
+type CreateAppDialogProps = CreateAppProps & {
+ show: boolean
+}
+const CreateAppModal = ({ show, onClose, onSuccess, onCreateFromTemplate }: CreateAppDialogProps) => {
+ return (
+
+
+
)
}
export default CreateAppModal
+
+type AppTypeCardProps = {
+ icon: JSX.Element
+ beta?: boolean
+ title: string
+ description: string
+ active: boolean
+ onClick: () => void
+}
+function AppTypeCard({ icon, title, beta = false, description, active, onClick }: AppTypeCardProps) {
+ const { t } = useTranslation()
+ return
+ {beta &&
{t('common.menus.status')}
}
+ {icon}
+
{title}
+
{description}
+
+}
+
+function AppPreview({ mode }: { mode: AppMode }) {
+ const { t } = useTranslation()
+ const modeToPreviewInfoMap = {
+ 'chat': {
+ title: t('app.types.chatbot'),
+ description: t('app.newApp.chatbotUserDescription'),
+ link: 'https://docs.dify.ai/guides/application-orchestrate/conversation-application?fallback=true',
+ },
+ 'advanced-chat': {
+ title: t('app.types.advanced'),
+ description: t('app.newApp.advancedUserDescription'),
+ link: 'https://docs.dify.ai/guides/workflow',
+ },
+ 'agent-chat': {
+ title: t('app.types.agent'),
+ description: t('app.newApp.agentUserDescription'),
+ link: 'https://docs.dify.ai/guides/application-orchestrate/agent',
+ },
+ 'completion': {
+ title: t('app.newApp.completeApp'),
+ description: t('app.newApp.completionUserDescription'),
+ link: null,
+ },
+ 'workflow': {
+ title: t('app.types.workflow'),
+ description: t('app.newApp.workflowUserDescription'),
+ link: 'https://docs.dify.ai/guides/workflow',
+ },
+ }
+ const previewInfo = modeToPreviewInfoMap[mode]
+ return
+
{previewInfo.title}
+
+ {previewInfo.description}
+ {previewInfo.link && {t('app.newApp.learnMore')}}
+
+
+}
+
+function AppScreenShot({ mode, show }: { mode: AppMode; show: boolean }) {
+ const theme = useContextSelector(AppsContext, state => state.theme)
+ const modeToImageMap = {
+ 'chat': 'Chatbot',
+ 'advanced-chat': 'Chatflow',
+ 'agent-chat': 'Agent',
+ 'completion': 'TextGenerator',
+ 'workflow': 'Workflow',
+ }
+ return
+}
diff --git a/web/app/components/app/create-app-modal/style.module.css b/web/app/components/app/create-app-modal/style.module.css
deleted file mode 100644
index 9a503a8cdd734f..00000000000000
--- a/web/app/components/app/create-app-modal/style.module.css
+++ /dev/null
@@ -1,23 +0,0 @@
-.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')
-}
diff --git a/web/app/components/app/log-annotation/index.tsx b/web/app/components/app/log-annotation/index.tsx
index 3fa13019f944c2..696cc19ba4054f 100644
--- a/web/app/components/app/log-annotation/index.tsx
+++ b/web/app/components/app/log-annotation/index.tsx
@@ -34,7 +34,7 @@ const LogAnnotation: FC
= ({
if (!appDetail) {
return (
-
+
)
diff --git a/web/app/components/app/log/list.tsx b/web/app/components/app/log/list.tsx
index 978e83737b13e1..383aeb1492c769 100644
--- a/web/app/components/app/log/list.tsx
+++ b/web/app/components/app/log/list.tsx
@@ -5,9 +5,8 @@ import useSWR from 'swr'
import {
HandThumbDownIcon,
HandThumbUpIcon,
- XMarkIcon,
} from '@heroicons/react/24/outline'
-import { RiEditFill, RiQuestionLine } from '@remixicon/react'
+import { RiCloseLine, RiEditFill } from '@remixicon/react'
import { get } from 'lodash-es'
import InfiniteScroll from 'react-infinite-scroll-component'
import dayjs from 'dayjs'
@@ -17,21 +16,18 @@ import { createContext, useContext } from 'use-context-selector'
import { useShallow } from 'zustand/react/shallow'
import { useTranslation } from 'react-i18next'
import type { ChatItemInTree } from '../../base/chat/types'
+import Indicator from '../../header/indicator'
import VarPanel from './var-panel'
-import cn from '@/utils/classnames'
import type { FeedbackFunc, FeedbackType, IChatItem, SubmitAnnotationFunc } from '@/app/components/base/chat/chat/type'
import type { Annotation, ChatConversationGeneralDetail, ChatConversationsResponse, ChatMessage, ChatMessagesRequest, CompletionConversationGeneralDetail, CompletionConversationsResponse, LogAnnotation } from '@/models/log'
import type { App } from '@/types/app'
+import ActionButton from '@/app/components/base/action-button'
import Loading from '@/app/components/base/loading'
import Drawer from '@/app/components/base/drawer'
-import Popover from '@/app/components/base/popover'
import Chat from '@/app/components/base/chat/chat'
import { ToastContext } from '@/app/components/base/toast'
import { fetchChatConversationDetail, fetchChatMessages, fetchCompletionConversationDetail, updateLogMessageAnnotations, updateLogMessageFeedbacks } from '@/service/log'
-import { TONE_LIST } from '@/config'
-import ModelIcon from '@/app/components/header/account-setting/model-provider-page/model-icon'
-import { useTextGenerationCurrentProviderAndModelAndModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
-import ModelName from '@/app/components/header/account-setting/model-provider-page/model-name'
+import ModelInfo from '@/app/components/app/log/model-info'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import TextGeneration from '@/app/components/app/text-generate/item'
import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils'
@@ -44,6 +40,7 @@ import Tooltip from '@/app/components/base/tooltip'
import { CopyIcon } from '@/app/components/base/copy-icon'
import { buildChatItemTree, getThreadMessages } from '@/app/components/base/chat/utils'
import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
+import cn from '@/utils/classnames'
dayjs.extend(utc)
dayjs.extend(timezone)
@@ -61,6 +58,12 @@ type IDrawerContext = {
appDetail?: App
}
+type StatusCount = {
+ success: number
+ failed: number
+ partial_success: number
+}
+
const DrawerContext = createContext
({} as IDrawerContext)
/**
@@ -75,13 +78,31 @@ const HandThumbIconWithCount: FC<{ count: number; iconType: 'up' | 'down' }> = (
}
-const PARAM_MAP = {
- temperature: 'Temperature',
- top_p: 'Top P',
- presence_penalty: 'Presence Penalty',
- max_tokens: 'Max Token',
- stop: 'Stop',
- frequency_penalty: 'Frequency Penalty',
+const statusTdRender = (statusCount: StatusCount) => {
+ if (statusCount.partial_success + statusCount.failed === 0) {
+ return (
+
+
+ Success
+
+ )
+ }
+ else if (statusCount.failed === 0) {
+ return (
+
+
+ Partial Success
+
+ )
+ }
+ else {
+ return (
+
+
+ {statusCount.failed} {`${statusCount.failed > 1 ? 'Failures' : 'Failure'}`}
+
+ )
+ }
}
const getFormattedChatList = (messages: ChatMessage[], conversationId: string, timezone: string, format: string) => {
@@ -156,9 +177,6 @@ const getFormattedChatList = (messages: ChatMessage[], conversationId: string, t
return newChatList
}
-// const displayedParams = CompletionParams.slice(0, -2)
-const validatedParams = ['temperature', 'top_p', 'presence_penalty', 'frequency_penalty']
-
type IDetailPanel = {
detail: any
onFeedback: FeedbackFunc
@@ -315,22 +333,6 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
const isChatMode = appDetail?.mode !== 'completion'
const isAdvanced = appDetail?.mode === 'advanced-chat'
- const targetTone = TONE_LIST.find((item: any) => {
- let res = true
- validatedParams.forEach((param) => {
- res = item.config?.[param] === detail?.model_config.model?.completion_params?.[param]
- })
- 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 {
- currentModel,
- currentProvider,
- } = useTextGenerationCurrentProviderAndModelAndModelList(
- { provider: provideName, model: modelName },
- )
const varList = (detail.model_config as any).user_input_form?.map((item: any) => {
const itemContent = item[Object.keys(item)[0]]
return {
@@ -342,18 +344,6 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
? detail.message.message_files.map((item: any) => item.url)
: []
- const getParamValue = (param: string) => {
- const value = detail?.model_config.model?.completion_params?.[param] || '-'
- if (param === 'stop') {
- if (Array.isArray(value))
- return value.join(',')
- else
- return '-'
- }
-
- return value
- }
-
const [width, setWidth] = useState(0)
const ref = useRef(null)
@@ -367,162 +357,71 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
}, [])
return (
-
+
{/* Panel Header */}
-
-
-
{isChatMode ? t('appLog.detail.conversationId') : t('appLog.detail.time')}
+
+
+
{isChatMode ? t('appLog.detail.conversationId') : t('appLog.detail.time')}
{isChatMode && (
-
+
- {detail.id}
+ {detail.id}
)}
{!isChatMode && (
-
{formatTime(detail.created_at, t('appLog.dateTimeFormat') as string)}
+
{formatTime(detail.created_at, t('appLog.dateTimeFormat') as string)}
)}
-
- {!isAdvanced && (
- <>
-
-
-
-
-
- {targetTone}
-
- >}
- htmlContent={
-
-
Tone of responses
-
{targetTone}
-
- {['temperature', 'top_p', 'presence_penalty', 'max_tokens', 'stop'].map((param: string, index: number) => {
- return
- {PARAM_MAP[param as keyof typeof PARAM_MAP]}
- {getParamValue(param)}
-
- })}
-
}
- />
- >
- )}
-
-
-
+
+ {!isAdvanced && }
-
+
+
+
{/* Panel Body */}
- {(varList.length > 0 || (!isChatMode && message_files.length > 0)) && (
-
-
-
- )}
-
- {!isChatMode
- ?
-
-
{t('appLog.table.header.output')}
-
-
-
{ }}
- 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}
- siteInfo={null}
- />
+
+
+ {(varList.length > 0 || (!isChatMode && message_files.length > 0)) && (
+
+ )}
- : threadChatItems.length < 8
- ?
-
+
+ {!isChatMode
+ ?
+
+
{t('appLog.table.header.output')}
+
+
+
{ }}
+ 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}
+ siteInfo={null}
/>
- :
- {/* Put the scroll bar always on the bottom */}
- {t('appLog.detail.loading')}...
}
- // endMessage={
Nothing more to show
}
- // below props only if you need pull down functionality
- refreshFunction={fetchData}
- pullDownToRefresh
- pullDownToRefreshThreshold={50}
- // pullDownToRefreshContent={
- //
Pull down to refresh
- // }
- // releaseToRefreshContent={
- //
Release to refresh
- // }
- // To put endMessage and loader to the top.
- style={{ display: 'flex', flexDirection: 'column-reverse' }}
- inverse={true}
- >
+ : threadChatItems.length < 8
+ ?
-
-
- }
+
+ :
+ {/* Put the scroll bar always on the bottom */}
+ {t('appLog.detail.loading')}...
}
+ // endMessage={Nothing more to show
}
+ // below props only if you need pull down functionality
+ refreshFunction={fetchData}
+ pullDownToRefresh
+ pullDownToRefreshThreshold={50}
+ // pullDownToRefreshContent={
+ // Pull down to refresh
+ // }
+ // releaseToRefreshContent={
+ // Release to refresh
+ // }
+ // To put endMessage and loader to the top.
+ style={{ display: 'flex', flexDirection: 'column-reverse' }}
+ inverse={true}
+ >
+
+
+
+ }
+
{showMessageLogModal && (
= ({ appId, conversationId }) => {
// Text Generator App Session Details Including Message List
const detailParams = ({ url: `/apps/${appId}/completion-conversations/${conversationId}` })
@@ -621,8 +576,8 @@ const CompletionConversationDetailComp: FC<{ appId?: string; conversationId?: st
}
/**
- * Chat App Conversation Detail Component
- */
+ * Chat App Conversation Detail Component
+ */
const ChatConversationDetailComp: FC<{ appId?: string; conversationId?: string }> = ({ appId, conversationId }) => {
const detailParams = { url: `/apps/${appId}/chat-conversations/${conversationId}` }
const { data: conversationDetail } = useSWR(() => (appId && conversationId) ? detailParams : null, fetchChatConversationDetail)
@@ -664,8 +619,8 @@ const ChatConversationDetailComp: FC<{ appId?: string; conversationId?: string }
}
/**
- * Conversation list component including basic information
- */
+ * Conversation list component including basic information
+ */
const ConversationList: FC = ({ logs, appDetail, onRefresh }) => {
const { t } = useTranslation()
const { formatTime } = useTimestamp()
@@ -676,6 +631,7 @@ const ConversationList: FC = ({ logs, appDetail, onRefresh })
const [showDrawer, setShowDrawer] = useState(false) // Whether to display the chat details drawer
const [currentConversation, setCurrentConversation] = useState() // Currently selected conversation
const isChatMode = appDetail.mode !== 'completion' // Whether the app is a chat app
+ const isChatflow = appDetail.mode === 'advanced-chat' // Whether the app is a chatflow app
const { setShowPromptLogModal, setShowAgentLogModal } = useAppStore(useShallow(state => ({
setShowPromptLogModal: state.setShowPromptLogModal,
setShowAgentLogModal: state.setShowAgentLogModal,
@@ -718,6 +674,7 @@ const ConversationList: FC = ({ logs, appDetail, onRefresh })
|
{isChatMode ? t('appLog.table.header.summary') : t('appLog.table.header.input')} |
{t('appLog.table.header.endUser')} |
+ {isChatflow && {t('appLog.table.header.status')} | }
{isChatMode ? t('appLog.table.header.messageCount') : t('appLog.table.header.output')} |
{t('appLog.table.header.userRate')} |
{t('appLog.table.header.adminRate')} |
@@ -748,6 +705,9 @@ const ConversationList: FC = ({ logs, appDetail, onRefresh })
{renderTdValue(leftValue || t('appLog.table.empty.noChat'), !leftValue, isChatMode && log.annotated)}
{renderTdValue(endUser || defaultValue, !endUser)} |
+ {isChatflow &&
+ {statusTdRender(log.status_count)}
+ | }
{renderTdValue(rightValue === 0 ? 0 : (rightValue || t('appLog.table.empty.noOutput')), !rightValue, !isChatMode && !!log.annotation?.content, log.annotation)}
|
@@ -780,7 +740,7 @@ const ConversationList: FC = ({ logs, appDetail, onRefresh })
onClose={onCloseDrawer}
mask={isMobile}
footer={null}
- panelClassname='mt-16 mx-2 sm:mr-2 mb-4 !p-0 !max-w-[640px] rounded-xl bg-background-gradient-bg-fill-chat-bg-1'
+ panelClassname='mt-16 mx-2 sm:mr-2 mb-4 !p-0 !max-w-[640px] rounded-xl bg-components-panel-bg'
>
= ({
+ model,
+}) => {
+ const { t } = useTranslation()
+ const modelName = model.name
+ const provideName = model.provider as any
+ const {
+ currentModel,
+ currentProvider,
+ } = useTextGenerationCurrentProviderAndModelAndModelList(
+ { provider: provideName, model: modelName },
+ )
+
+ const [open, setOpen] = React.useState(false)
+
+ const getParamValue = (param: string) => {
+ const value = model.completion_params?.[param] || '-'
+ if (param === 'stop') {
+ if (Array.isArray(value))
+ return value.join(',')
+ else
+ return '-'
+ }
+
+ return value
+ }
+
+ return (
+
+
+
+
+
+
+
+
setOpen(v => !v)}
+ className='block'
+ >
+
+
+
+
+
+
+
{t('appLog.detail.modelParams')}
+
+ {['temperature', 'top_p', 'presence_penalty', 'max_tokens', 'stop'].map((param: string, index: number) => {
+ return
+ {PARAM_MAP[param as keyof typeof PARAM_MAP]}
+ {getParamValue(param)}
+
+ })}
+
+
+
+
+
+
+ )
+}
+export default React.memo(ModelInfo)
diff --git a/web/app/components/app/log/var-panel.tsx b/web/app/components/app/log/var-panel.tsx
index a22856339e5d31..3ae4bfb5c63a58 100644
--- a/web/app/components/app/log/var-panel.tsx
+++ b/web/app/components/app/log/var-panel.tsx
@@ -7,7 +7,9 @@ import {
RiArrowDownSLine,
RiArrowRightSLine,
} from '@remixicon/react'
+import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
import ImagePreview from '@/app/components/base/image-uploader/image-preview'
+import cn from '@/utils/classnames'
type Props = {
varList: { label: string; value: string }[]
@@ -23,34 +25,35 @@ const VarPanel: FC = ({
const [imagePreviewUrl, setImagePreviewUrl] = useState('')
return (
-
+
+
+
{t('appLog.detail.variables')}
{
isCollapse
- ?
- :
+ ?
+ :
}
-
{t('appLog.detail.variables')}
{!isCollapse && (
-
+
{varList.map(({ label, value }, index) => (
-
-
+
+
{'{{'}
{label}
{'}}'}
-
{value}
+
{value}
))}
{message_files.length > 0 && (
-
{t('appLog.detail.uploadImages')}
+
{t('appLog.detail.uploadImages')}
{message_files.map((url, index) => (
= ({
imagePreviewUrl && (
setImagePreviewUrl('')}
/>
)
diff --git a/web/app/components/app/text-generate/item/index.tsx b/web/app/components/app/text-generate/item/index.tsx
index 0c4f62282ef579..ac868e6ee3c745 100644
--- a/web/app/components/app/text-generate/item/index.tsx
+++ b/web/app/components/app/text-generate/item/index.tsx
@@ -282,7 +282,7 @@ const GenerationItem: FC = ({
const [currentTab, setCurrentTab] = useState('DETAIL')
return (
- void
+ value: Array
+ onChange: (value: AppSelectorProps['value']) => void
}
+const allTypes: AppMode[] = ['chat', 'agent-chat', 'completion', 'advanced-chat', 'workflow']
+
const AppTypeSelector = ({ value, onChange }: AppSelectorProps) => {
- const { t } = useTranslation()
const [open, setOpen] = useState(false)
return (
@@ -33,96 +33,136 @@ const AppTypeSelector = ({ value, onChange }: AppSelectorProps) => {
className='block'
>
- {!value && (
- <>
-
-
-
-
{t('app.typeSelector.all')}
-
-
-
- >
- )}
- {value === 'chatbot' && (
- <>
-
-
-
-
{t('app.typeSelector.chatbot')}
-
{
- e.stopPropagation()
- onChange('')
- }}>
-
-
- >
- )}
- {value === 'agent' && (
- <>
-
-
-
-
{t('app.typeSelector.agent')}
-
{
- e.stopPropagation()
- onChange('')
- }}>
-
-
- >
- )}
- {value === 'workflow' && (
- <>
-
-
-
-
{t('app.typeSelector.workflow')}
-
{
- e.stopPropagation()
- onChange('')
- }}>
-
-
- >
- )}
+
+ {value && value.length > 0 &&
{
+ e.stopPropagation()
+ onChange([])
+ }}>
+
+
}
-
-
{
- onChange('chatbot')
- setOpen(false)
- }}>
-
-
{t('app.typeSelector.chatbot')}
- {value === 'chatbot' &&
}
-
-
{
- onChange('agent')
- setOpen(false)
- }}>
-
-
{t('app.typeSelector.agent')}
- {value === 'agent' &&
}
-
-
{
- onChange('workflow')
- setOpen(false)
- }}>
-
-
{t('app.typeSelector.workflow')}
- {value === 'workflow' &&
}
-
-
+
+ {allTypes.map(mode => (
+ 0 && value?.indexOf(mode) !== -1)}
+ onClick={() => {
+ if (value?.indexOf(mode) !== -1)
+ onChange(value?.filter(v => v !== mode) ?? [])
+ else
+ onChange([...(value || []), mode])
+ }} />
+ ))}
+
-
-
+
+
)
}
-export default React.memo(AppTypeSelector)
+export default AppTypeSelector
+
+function AppTypeSelectTrigger({ values }: { values: AppSelectorProps['value'] }) {
+ const { t } = useTranslation()
+ if (!values || values.length === 0) {
+ return
+
+
{t('app.typeSelector.all')}
+
+
+ }
+ if (values.length === 1) {
+ return
+ }
+ return
+ {values.map((mode, index) => (
))}
+
+}
+
+type AppTypeSelectorItemProps = {
+ checked: boolean
+ type: AppMode
+ onClick: () => void
+}
+function AppTypeSelectorItem({ checked, type, onClick }: AppTypeSelectorItemProps) {
+ return
+
+
+
+
+}
+
+type AppTypeIconProps = {
+ type: AppMode
+ style?: React.CSSProperties
+ className?: string
+ wrapperClassName?: string
+}
+
+export function AppTypeIcon({ type, className, wrapperClassName, style }: AppTypeIconProps) {
+ const wrapperClassNames = cn('w-5 h-5 inline-flex items-center justify-center rounded-md border border-divider-regular', wrapperClassName)
+ const iconClassNames = cn('w-3.5 h-3.5 text-components-avatar-shape-fill-stop-100', className)
+ if (type === 'chat') {
+ return
+
+
+ }
+ if (type === 'agent-chat') {
+ return
+
+
+ }
+ if (type === 'advanced-chat') {
+ return
+
+
+ }
+ if (type === 'workflow') {
+ return
+
+
+ }
+ if (type === 'completion') {
+ return
+
+
+ }
+ return null
+}
+
+type AppTypeLabelProps = {
+ type: AppMode
+ className?: string
+}
+export function AppTypeLabel({ type, className }: AppTypeLabelProps) {
+ const { t } = useTranslation()
+ let label = ''
+ if (type === 'chat')
+ label = t('app.typeSelector.chatbot')
+ if (type === 'agent-chat')
+ label = t('app.typeSelector.agent')
+ if (type === 'completion')
+ label = t('app.typeSelector.completion')
+ if (type === 'advanced-chat')
+ label = t('app.typeSelector.advanced')
+ if (type === 'workflow')
+ label = t('app.typeSelector.workflow')
+
+ return
{label}
+}
diff --git a/web/app/components/app/workflow-log/list.tsx b/web/app/components/app/workflow-log/list.tsx
index e3de4a957f22e4..41db9b5d466822 100644
--- a/web/app/components/app/workflow-log/list.tsx
+++ b/web/app/components/app/workflow-log/list.tsx
@@ -63,6 +63,14 @@ const WorkflowAppLogList: FC
= ({ logs, appDetail, onRefresh }) => {
)
}
+ if (status === 'partial-succeeded') {
+ return (
+
+
+ Partial Success
+
+ )
+ }
}
const onCloseDrawer = () => {
diff --git a/web/app/components/base/app-icon-picker/Uploader.tsx b/web/app/components/base/app-icon-picker/ImageInput.tsx
similarity index 89%
rename from web/app/components/base/app-icon-picker/Uploader.tsx
rename to web/app/components/base/app-icon-picker/ImageInput.tsx
index ba0ef6b2b2178a..f26f5a1fcb91db 100644
--- a/web/app/components/base/app-icon-picker/Uploader.tsx
+++ b/web/app/components/base/app-icon-picker/ImageInput.tsx
@@ -11,16 +11,19 @@ import { useDraggableUploader } from './hooks'
import { checkIsAnimatedImage } from './utils'
import { ALLOW_FILE_EXTENSIONS } from '@/types/app'
+export type OnImageInput = {
+ (isCropped: true, tempUrl: string, croppedAreaPixels: Area, fileName: string): void
+ (isCropped: false, file: File): void
+}
+
type UploaderProps = {
className?: string
- onImageCropped?: (tempUrl: string, croppedAreaPixels: Area, fileName: string) => void
- onUpload?: (file?: File) => void
+ onImageInput?: OnImageInput
}
-const Uploader: FC
= ({
+const ImageInput: FC = ({
className,
- onImageCropped,
- onUpload,
+ onImageInput,
}) => {
const [inputImage, setInputImage] = useState<{ file: File; url: string }>()
const [isAnimatedImage, setIsAnimatedImage] = useState(false)
@@ -37,8 +40,7 @@ const Uploader: FC = ({
const onCropComplete = async (_: Area, croppedAreaPixels: Area) => {
if (!inputImage)
return
- onImageCropped?.(inputImage.url, croppedAreaPixels, inputImage.file.name)
- onUpload?.(undefined)
+ onImageInput?.(true, inputImage.url, croppedAreaPixels, inputImage.file.name)
}
const handleLocalFileInput = (e: ChangeEvent) => {
@@ -48,7 +50,7 @@ const Uploader: FC = ({
checkIsAnimatedImage(file).then((isAnimatedImage) => {
setIsAnimatedImage(!!isAnimatedImage)
if (isAnimatedImage)
- onUpload?.(file)
+ onImageInput?.(false, file)
})
}
}
@@ -117,4 +119,4 @@ const Uploader: FC = ({
)
}
-export default Uploader
+export default ImageInput
diff --git a/web/app/components/base/app-icon-picker/index.tsx b/web/app/components/base/app-icon-picker/index.tsx
index 8a10d28653dcfa..277e2fa1d0862f 100644
--- a/web/app/components/base/app-icon-picker/index.tsx
+++ b/web/app/components/base/app-icon-picker/index.tsx
@@ -8,12 +8,14 @@ import Button from '../button'
import { ImagePlus } from '../icons/src/vender/line/images'
import { useLocalFileUploader } from '../image-uploader/hooks'
import EmojiPickerInner from '../emoji-picker/Inner'
-import Uploader from './Uploader'
+import type { OnImageInput } from './ImageInput'
+import ImageInput from './ImageInput'
import s from './style.module.css'
import getCroppedImg from './utils'
import type { AppIconType, ImageFile } from '@/types/app'
import cn from '@/utils/classnames'
import { DISABLE_UPLOAD_IMAGE_AS_ICON } from '@/config'
+
export type AppIconEmojiSelection = {
type: 'emoji'
icon: string
@@ -69,14 +71,15 @@ const AppIconPicker: FC = ({
},
})
- const [imageCropInfo, setImageCropInfo] = useState<{ tempUrl: string; croppedAreaPixels: Area; fileName: string }>()
- const handleImageCropped = async (tempUrl: string, croppedAreaPixels: Area, fileName: string) => {
- setImageCropInfo({ tempUrl, croppedAreaPixels, fileName })
- }
+ type InputImageInfo = { file: File } | { tempUrl: string; croppedAreaPixels: Area; fileName: string }
+ const [inputImageInfo, setInputImageInfo] = useState()
- const [uploadImageInfo, setUploadImageInfo] = useState<{ file?: File }>()
- const handleUpload = async (file?: File) => {
- setUploadImageInfo({ file })
+ const handleImageInput: OnImageInput = async (isCropped: boolean, fileOrTempUrl: string | File, croppedAreaPixels?: Area, fileName?: string) => {
+ setInputImageInfo(
+ isCropped
+ ? { tempUrl: fileOrTempUrl as string, croppedAreaPixels: croppedAreaPixels!, fileName: fileName! }
+ : { file: fileOrTempUrl as File },
+ )
}
const handleSelect = async () => {
@@ -90,15 +93,15 @@ const AppIconPicker: FC = ({
}
}
else {
- if (!imageCropInfo && !uploadImageInfo)
+ if (!inputImageInfo)
return
setUploading(true)
- if (imageCropInfo.file) {
- handleLocalFileUpload(imageCropInfo.file)
+ if ('file' in inputImageInfo) {
+ handleLocalFileUpload(inputImageInfo.file)
return
}
- const blob = await getCroppedImg(imageCropInfo.tempUrl, imageCropInfo.croppedAreaPixels, imageCropInfo.fileName)
- const file = new File([blob], imageCropInfo.fileName, { type: blob.type })
+ const blob = await getCroppedImg(inputImageInfo.tempUrl, inputImageInfo.croppedAreaPixels, inputImageInfo.fileName)
+ const file = new File([blob], inputImageInfo.fileName, { type: blob.type })
handleLocalFileUpload(file)
}
}
@@ -127,10 +130,8 @@ const AppIconPicker: FC = ({
}
-
-
-
-
+
+
diff --git a/web/app/components/base/app-icon-picker/utils.ts b/web/app/components/base/app-icon-picker/utils.ts
index 99154d56da3097..f63b75eaa10572 100644
--- a/web/app/components/base/app-icon-picker/utils.ts
+++ b/web/app/components/base/app-icon-picker/utils.ts
@@ -116,12 +116,12 @@ export default async function getCroppedImg(
})
}
-export function checkIsAnimatedImage(file) {
+export function checkIsAnimatedImage(file: File): Promise
{
return new Promise((resolve, reject) => {
const fileReader = new FileReader()
fileReader.onload = function (e) {
- const arr = new Uint8Array(e.target.result)
+ const arr = new Uint8Array(e.target?.result as ArrayBuffer)
// Check file extension
const fileName = file.name.toLowerCase()
@@ -148,7 +148,7 @@ export function checkIsAnimatedImage(file) {
}
// Function to check for WebP signature
-function isWebP(arr) {
+function isWebP(arr: Uint8Array) {
return (
arr[0] === 0x52 && arr[1] === 0x49 && arr[2] === 0x46 && arr[3] === 0x46
&& arr[8] === 0x57 && arr[9] === 0x45 && arr[10] === 0x42 && arr[11] === 0x50
@@ -156,7 +156,7 @@ function isWebP(arr) {
}
// Function to check if the WebP is animated (contains ANIM chunk)
-function checkWebPAnimation(arr) {
+function checkWebPAnimation(arr: Uint8Array) {
// Search for the ANIM chunk in WebP to determine if it's animated
for (let i = 12; i < arr.length - 4; i++) {
if (arr[i] === 0x41 && arr[i + 1] === 0x4E && arr[i + 2] === 0x49 && arr[i + 3] === 0x4D)
diff --git a/web/app/components/base/app-icon/index.tsx b/web/app/components/base/app-icon/index.tsx
index 5e7378c08734aa..1938c42d3ef93b 100644
--- a/web/app/components/base/app-icon/index.tsx
+++ b/web/app/components/base/app-icon/index.tsx
@@ -3,14 +3,14 @@
import type { FC } from 'react'
import { init } from 'emoji-mart'
import data from '@emoji-mart/data'
-import style from './style.module.css'
-import classNames from '@/utils/classnames'
+import { cva } from 'class-variance-authority'
import type { AppIconType } from '@/types/app'
+import classNames from '@/utils/classnames'
init({ data })
export type AppIconProps = {
- size?: 'xs' | 'tiny' | 'small' | 'medium' | 'large'
+ size?: 'xs' | 'tiny' | 'small' | 'medium' | 'large' | 'xl' | 'xxl'
rounded?: boolean
iconType?: AppIconType | null
icon?: string
@@ -20,7 +20,28 @@ export type AppIconProps = {
innerIcon?: React.ReactNode
onClick?: () => void
}
-
+const appIconVariants = cva(
+ 'flex items-center justify-center relative text-lg rounded-lg grow-0 shrink-0 overflow-hidden leading-none',
+ {
+ variants: {
+ size: {
+ xs: 'w-4 h-4 text-xs',
+ tiny: 'w-6 h-6 text-base',
+ small: 'w-8 h-8 text-xl',
+ medium: 'w-9 h-9 text-[22px]',
+ large: 'w-10 h-10 text-[24px]',
+ xl: 'w-12 h-12 text-[28px]',
+ xxl: 'w-14 h-14 text-[32px]',
+ },
+ rounded: {
+ true: 'rounded-full',
+ },
+ },
+ defaultVariants: {
+ size: 'medium',
+ rounded: false,
+ },
+ })
const AppIcon: FC = ({
size = 'medium',
rounded = false,
@@ -32,22 +53,15 @@ const AppIcon: FC = ({
innerIcon,
onClick,
}) => {
- const wrapperClassName = classNames(
- style.appIcon,
- size !== 'medium' && style[size],
- rounded && style.rounded,
- className ?? '',
- 'overflow-hidden',
- )
-
const isValidImageIcon = iconType === 'image' && imageUrl
return
{isValidImageIcon
+ // eslint-disable-next-line @next/next/no-img-element
?
: (innerIcon || ((icon && icon !== '') ? : ))
}
diff --git a/web/app/components/base/app-icon/style.module.css b/web/app/components/base/app-icon/style.module.css
deleted file mode 100644
index 06a2478d41e380..00000000000000
--- a/web/app/components/base/app-icon/style.module.css
+++ /dev/null
@@ -1,23 +0,0 @@
-.appIcon {
- @apply flex items-center justify-center relative w-9 h-9 text-lg rounded-lg grow-0 shrink-0;
-}
-
-.appIcon.large {
- @apply w-10 h-10;
-}
-
-.appIcon.small {
- @apply w-8 h-8;
-}
-
-.appIcon.tiny {
- @apply w-6 h-6 text-base;
-}
-
-.appIcon.xs {
- @apply w-3 h-3 text-base;
-}
-
-.appIcon.rounded {
- @apply rounded-full;
-}
\ No newline at end of file
diff --git a/web/app/components/base/chat/chat/answer/index.tsx b/web/app/components/base/chat/chat/answer/index.tsx
index 1ff390bd58dc08..c6d14ddeade9e1 100644
--- a/web/app/components/base/chat/chat/answer/index.tsx
+++ b/web/app/components/base/chat/chat/answer/index.tsx
@@ -114,7 +114,7 @@ const Answer: FC = ({
{
!responding && (
@@ -212,15 +212,15 @@ const Answer: FC
= ({
disabled={!item.prevSibling}
onClick={() => item.prevSibling && switchSibling?.(item.prevSibling)}
>
-
+
- {item.siblingIndex + 1} / {item.siblingCount}
+ {item.siblingIndex + 1} / {item.siblingCount}
item.nextSibling && switchSibling?.(item.nextSibling)}
>
-
+
}
diff --git a/web/app/components/base/chat/chat/answer/workflow-process.tsx b/web/app/components/base/chat/chat/answer/workflow-process.tsx
index 4a09e27d9818f6..bb9abdb6fcf9b9 100644
--- a/web/app/components/base/chat/chat/answer/workflow-process.tsx
+++ b/web/app/components/base/chat/chat/answer/workflow-process.tsx
@@ -64,6 +64,12 @@ const WorkflowProcessItem = ({
setShowMessageLogModal(true)
}, [item, setCurrentLogItem, setCurrentLogModalActiveTab, setShowMessageLogModal])
+ const showRetryDetail = useCallback(() => {
+ setCurrentLogItem(item)
+ setCurrentLogModalActiveTab('TRACING')
+ setShowMessageLogModal(true)
+ }, [item, setCurrentLogItem, setCurrentLogModalActiveTab, setShowMessageLogModal])
+
return (
diff --git a/web/app/components/base/checkbox/assets/check.svg b/web/app/components/base/checkbox/assets/check.svg
deleted file mode 100644
index f1f635ed74f237..00000000000000
--- a/web/app/components/base/checkbox/assets/check.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-
diff --git a/web/app/components/base/checkbox/index.module.css b/web/app/components/base/checkbox/index.module.css
deleted file mode 100644
index 5fe4172f13ff87..00000000000000
--- a/web/app/components/base/checkbox/index.module.css
+++ /dev/null
@@ -1,14 +0,0 @@
-.wrapper {
- border-color: #d0d5dd;
-}
-
-.checked {
- background: #155eef url(./assets/check.svg) center center no-repeat;
- background-size: 12px 12px;
- border-color: #155eef;
-}
-
-.checked.disabled {
- background-color: #d0d5dd;
- border-color: #d0d5dd;
-}
\ No newline at end of file
diff --git a/web/app/components/base/checkbox/index.tsx b/web/app/components/base/checkbox/index.tsx
index fe95155b3caaca..c2b43ba4b29d7a 100644
--- a/web/app/components/base/checkbox/index.tsx
+++ b/web/app/components/base/checkbox/index.tsx
@@ -1,4 +1,4 @@
-import s from './index.module.css'
+import { RiCheckLine } from '@remixicon/react'
import cn from '@/utils/classnames'
type CheckboxProps = {
@@ -9,13 +9,27 @@ type CheckboxProps = {
}
const Checkbox = ({ checked, onCheck, className, disabled }: CheckboxProps) => {
+ if (!checked) {
+ return (
+ {
+ if (disabled)
+ return
+ onCheck?.()
+ }}
+ >
+ )
+ }
return (
{
@@ -24,7 +38,9 @@ const Checkbox = ({ checked, onCheck, className, disabled }: CheckboxProps) => {
onCheck?.()
}}
- />
+ >
+
+
)
}
diff --git a/web/app/components/base/copy-icon/index.tsx b/web/app/components/base/copy-icon/index.tsx
index 425a9ad293b284..9f886a1f320eaa 100644
--- a/web/app/components/base/copy-icon/index.tsx
+++ b/web/app/components/base/copy-icon/index.tsx
@@ -39,10 +39,10 @@ export const CopyIcon = ({ content }: Props) => {
{!isCopied
? (
-
+
)
: (
-
+
)
}
diff --git a/web/app/components/base/drawer-plus/index.tsx b/web/app/components/base/drawer-plus/index.tsx
index 894bea20d89e92..51402572b78739 100644
--- a/web/app/components/base/drawer-plus/index.tsx
+++ b/web/app/components/base/drawer-plus/index.tsx
@@ -58,15 +58,15 @@ const DrawerPlus: FC = ({
panelClassname={cn('mt-16 mx-2 sm:mr-2 mb-3 !p-0 rounded-xl', panelClassName, maxWidthClassName)}
>
-
+
-
+
{title}
@@ -74,12 +74,12 @@ const DrawerPlus: FC
= ({
onClick={onHide}
className='flex justify-center items-center w-6 h-6 cursor-pointer'
>
-
+
{titleDescription && (
-
+
{titleDescription}
)}
diff --git a/web/app/components/base/drawer/index.tsx b/web/app/components/base/drawer/index.tsx
index c2285b5c53ff14..d9df7cc0537108 100644
--- a/web/app/components/base/drawer/index.tsx
+++ b/web/app/components/base/drawer/index.tsx
@@ -49,7 +49,7 @@ export default function Drawer({
-
+
<>
{title &&
= ({
}, [onSelect, selectedEmoji, selectedBackground])
return
-
+
diff --git a/web/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal.tsx b/web/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal.tsx
index 801f1348ee240a..b16c7241754d59 100644
--- a/web/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal.tsx
+++ b/web/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal.tsx
@@ -77,9 +77,9 @@ const ConfigParamModal: FC
= ({
-
+
{t(`appAnnotation.initSetup.${isInit ? 'title' : 'configTitle'}`)}
diff --git a/web/app/components/base/features/new-feature-panel/annotation-reply/config-param.tsx b/web/app/components/base/features/new-feature-panel/annotation-reply/config-param.tsx
index 8b3a0af2403810..c192fcbec46cd3 100644
--- a/web/app/components/base/features/new-feature-panel/annotation-reply/config-param.tsx
+++ b/web/app/components/base/features/new-feature-panel/annotation-reply/config-param.tsx
@@ -10,11 +10,11 @@ export const Item: FC<{ title: string; tooltip: string; children: JSX.Element }>
}) => {
return (
-
-
{title}
+
+
{tooltip}
}
/>
diff --git a/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/base-slider/index.tsx b/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/base-slider/index.tsx
index 2e08a991226097..1df7253f4e2f0a 100644
--- a/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/base-slider/index.tsx
+++ b/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/base-slider/index.tsx
@@ -26,7 +26,7 @@ const Slider: React.FC
= ({ className, max, min, step, value, disa
renderThumb={(props, state) => (
-
+
{(state.valueNow / 100).toFixed(2)}
diff --git a/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/index.tsx b/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/index.tsx
index d68db9be736e88..3a9a34be43c421 100644
--- a/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/index.tsx
+++ b/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/index.tsx
@@ -28,13 +28,13 @@ const ScoreSlider: FC
= ({
onChange={onChange}
/>
-
-
+
+
0.8
·
{t('appDebug.feature.annotation.scoreThreshold.easyMatch')}
-
+
1.0
·
{t('appDebug.feature.annotation.scoreThreshold.accurateMatch')}
diff --git a/web/app/components/base/file-icon/index.tsx b/web/app/components/base/file-icon/index.tsx
index 21e48b3dd41fbe..6b217b17c114eb 100644
--- a/web/app/components/base/file-icon/index.tsx
+++ b/web/app/components/base/file-icon/index.tsx
@@ -36,6 +36,7 @@ const FileIcon: FC
= ({
return
case 'md':
case 'markdown':
+ case 'mdx':
return
case 'pdf':
return
diff --git a/web/app/components/base/file-uploader/audio-preview.tsx b/web/app/components/base/file-uploader/audio-preview.tsx
new file mode 100644
index 00000000000000..9a1ed440e25f08
--- /dev/null
+++ b/web/app/components/base/file-uploader/audio-preview.tsx
@@ -0,0 +1,47 @@
+import type { FC } from 'react'
+import { createPortal } from 'react-dom'
+import { RiCloseLine } from '@remixicon/react'
+import React from 'react'
+
+import { useHotkeys } from 'react-hotkeys-hook'
+
+type AudioPreviewProps = {
+ url: string
+ title: string
+ onCancel: () => void
+}
+const AudioPreview: FC = ({
+ url,
+ title,
+ onCancel,
+}) => {
+ useHotkeys('esc', onCancel)
+
+ return createPortal(
+ e.stopPropagation()}
+ tabIndex={-1}
+ >
+
+
+
+
+
+ ,
+ document.body,
+ )
+}
+
+export default AudioPreview
diff --git a/web/app/components/base/file-uploader/dynamic-pdf-preview.tsx b/web/app/components/base/file-uploader/dynamic-pdf-preview.tsx
new file mode 100644
index 00000000000000..116db89864c865
--- /dev/null
+++ b/web/app/components/base/file-uploader/dynamic-pdf-preview.tsx
@@ -0,0 +1,17 @@
+'use client'
+
+import dynamic from 'next/dynamic'
+
+type DynamicPdfPreviewProps = {
+ url: string
+ onCancel: () => void
+}
+const DynamicPdfPreview = dynamic(
+ (() => {
+ if (typeof window !== 'undefined')
+ return import('./pdf-preview')
+ }) as any,
+ { ssr: false }, // This will prevent the module from being loaded on the server-side
+)
+
+export default DynamicPdfPreview
diff --git a/web/app/components/base/file-uploader/file-image-render.tsx b/web/app/components/base/file-uploader/file-image-render.tsx
index 1a433dec5d10a1..9d263225e1ac5d 100644
--- a/web/app/components/base/file-uploader/file-image-render.tsx
+++ b/web/app/components/base/file-uploader/file-image-render.tsx
@@ -20,7 +20,7 @@ const FileImageRender = ({
canPreview && setImagePreviewUrl(url || '')}
+ onClick={() => canPreview && setImagePreviewUrl(base64Url || url || '')}
>
{
showDeleteAction && (
diff --git a/web/app/components/base/file-uploader/file-uploader-in-chat-input/file-item.tsx b/web/app/components/base/file-uploader/file-uploader-in-chat-input/file-item.tsx
index b6ecc276dbabea..ddbe745141b4b3 100644
--- a/web/app/components/base/file-uploader/file-uploader-in-chat-input/file-item.tsx
+++ b/web/app/components/base/file-uploader/file-uploader-in-chat-input/file-item.tsx
@@ -2,6 +2,7 @@ import {
RiCloseLine,
RiDownloadLine,
} from '@remixicon/react'
+import { useState } from 'react'
import {
downloadFile,
fileIsUploaded,
@@ -16,11 +17,15 @@ import ProgressCircle from '@/app/components/base/progress-bar/progress-circle'
import { ReplayLine } from '@/app/components/base/icons/src/vender/other'
import ActionButton from '@/app/components/base/action-button'
import Button from '@/app/components/base/button'
+import PdfPreview from '@/app/components/base/file-uploader/dynamic-pdf-preview'
+import AudioPreview from '@/app/components/base/file-uploader/audio-preview'
+import VideoPreview from '@/app/components/base/file-uploader/video-preview'
type FileItemProps = {
file: FileEntity
showDeleteAction?: boolean
showDownloadAction?: boolean
+ canPreview?: boolean
onRemove?: (fileId: string) => void
onReUpload?: (fileId: string) => void
}
@@ -30,88 +35,120 @@ const FileItem = ({
showDownloadAction = true,
onRemove,
onReUpload,
+ canPreview,
}: FileItemProps) => {
const { id, name, type, progress, url, base64Url, isRemote } = file
+ const [previewUrl, setPreviewUrl] = useState('')
const ext = getFileExtension(name, type, isRemote)
const uploadError = progress === -1
+ let tmp_preview_url = url || base64Url
+ if (!tmp_preview_url && file?.originalFile)
+ tmp_preview_url = URL.createObjectURL(file.originalFile.slice()).toString()
+
return (
-
- {
- showDeleteAction && (
-
onRemove?.(id)}
- >
-
-
- )
- }
+ <>
- {name}
-
-
-
-
+ {
+ showDeleteAction && (
+
onRemove?.(id)}
+ >
+
+
+ )
+ }
+
canPreview && setPreviewUrl(tmp_preview_url || '')}
+ >
+ {name}
+
+
+
+
+ {
+ ext && (
+ <>
+ {ext}
+
·
+ >
+ )
+ }
+ {
+ !!file.size && formatFileSize(file.size)
+ }
+
+ {
+ showDownloadAction && tmp_preview_url && (
+
{
+ e.stopPropagation()
+ downloadFile(tmp_preview_url || '', name)
+ }}
+ >
+
+
+ )
+ }
{
- ext && (
- <>
- {ext}
-
·
- >
+ progress >= 0 && !fileIsUploaded(file) && (
+
)
}
{
- !!file.size && formatFileSize(file.size)
+ uploadError && (
+
onReUpload?.(id)}
+ />
+ )
}
- {
- showDownloadAction && url && (
-
{
- e.stopPropagation()
- downloadFile(url || base64Url || '', name)
- }}
- >
-
-
- )
- }
- {
- progress >= 0 && !fileIsUploaded(file) && (
-
- )
- }
- {
- uploadError && (
-
onReUpload?.(id)}
- />
- )
- }
-
+ {
+ type.split('/')[0] === 'audio' && canPreview && previewUrl && (
+
setPreviewUrl('')}
+ />
+ )
+ }
+ {
+ type.split('/')[0] === 'video' && canPreview && previewUrl && (
+ setPreviewUrl('')}
+ />
+ )
+ }
+ {
+ type.split('/')[1] === 'pdf' && canPreview && previewUrl && (
+ { setPreviewUrl('') }} />
+ )
+ }
+ >
)
}
diff --git a/web/app/components/base/file-uploader/file-uploader-in-chat-input/file-list.tsx b/web/app/components/base/file-uploader/file-uploader-in-chat-input/file-list.tsx
index 69204640e096bf..ba909040c33352 100644
--- a/web/app/components/base/file-uploader/file-uploader-in-chat-input/file-list.tsx
+++ b/web/app/components/base/file-uploader/file-uploader-in-chat-input/file-list.tsx
@@ -23,7 +23,7 @@ export const FileList = ({
onRemove,
showDeleteAction = true,
showDownloadAction = false,
- canPreview,
+ canPreview = true,
}: FileListProps) => {
return (
@@ -51,6 +51,7 @@ export const FileList = ({
showDownloadAction={showDownloadAction}
onRemove={onRemove}
onReUpload={onReUpload}
+ canPreview={canPreview}
/>
)
})
diff --git a/web/app/components/base/file-uploader/pdf-preview.tsx b/web/app/components/base/file-uploader/pdf-preview.tsx
new file mode 100644
index 00000000000000..04947be0a4af8c
--- /dev/null
+++ b/web/app/components/base/file-uploader/pdf-preview.tsx
@@ -0,0 +1,102 @@
+import type { FC } from 'react'
+import { createPortal } from 'react-dom'
+import 'react-pdf-highlighter/dist/style.css'
+import { PdfHighlighter, PdfLoader } from 'react-pdf-highlighter'
+import { t } from 'i18next'
+import { RiCloseLine, RiZoomInLine, RiZoomOutLine } from '@remixicon/react'
+import React, { useState } from 'react'
+import { useHotkeys } from 'react-hotkeys-hook'
+import Loading from '@/app/components/base/loading'
+import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
+import Tooltip from '@/app/components/base/tooltip'
+
+type PdfPreviewProps = {
+ url: string
+ onCancel: () => void
+}
+
+const PdfPreview: FC
= ({
+ url,
+ onCancel,
+}) => {
+ const media = useBreakpoints()
+ const [scale, setScale] = useState(1)
+ const [position, setPosition] = useState({ x: 0, y: 0 })
+ const isMobile = media === MediaType.mobile
+
+ const zoomIn = () => {
+ setScale(prevScale => Math.min(prevScale * 1.2, 15))
+ setPosition({ x: position.x - 50, y: position.y - 50 })
+ }
+
+ const zoomOut = () => {
+ setScale((prevScale) => {
+ const newScale = Math.max(prevScale / 1.2, 0.5)
+ if (newScale === 1)
+ setPosition({ x: 0, y: 0 })
+ else
+ setPosition({ x: position.x + 50, y: position.y + 50 })
+
+ return newScale
+ })
+ }
+
+ useHotkeys('esc', onCancel)
+ useHotkeys('up', zoomIn)
+ useHotkeys('down', zoomOut)
+
+ return createPortal(
+ e.stopPropagation()}
+ tabIndex={-1}
+ >
+
}
+ >
+ {(pdfDocument) => {
+ return (
+
event.altKey}
+ scrollRef={() => { }}
+ onScrollChange={() => { }}
+ onSelectionFinished={() => null}
+ highlightTransform={() => { return }}
+ highlights={[]}
+ />
+ )
+ }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ,
+ document.body,
+ )
+}
+
+export default PdfPreview
diff --git a/web/app/components/base/file-uploader/utils.ts b/web/app/components/base/file-uploader/utils.ts
index 8c752fde8ab858..e095d4aa930291 100644
--- a/web/app/components/base/file-uploader/utils.ts
+++ b/web/app/components/base/file-uploader/utils.ts
@@ -84,7 +84,7 @@ export const getFileAppearanceType = (fileName: string, fileMimetype: string) =>
if (extension === 'pdf')
return FileAppearanceTypeEnum.pdf
- if (extension === 'md' || extension === 'markdown')
+ if (extension === 'md' || extension === 'markdown' || extension === 'mdx')
return FileAppearanceTypeEnum.markdown
if (extension === 'xlsx' || extension === 'xls')
@@ -158,13 +158,13 @@ export const isAllowedFileExtension = (fileName: string, fileMimetype: string, a
export const getFilesInLogs = (rawData: any) => {
const result = Object.keys(rawData || {}).map((key) => {
- if (typeof rawData[key] === 'object' && rawData[key].dify_model_identity === '__dify__file__') {
+ if (typeof rawData[key] === 'object' && rawData[key]?.dify_model_identity === '__dify__file__') {
return {
varName: key,
list: getProcessedFilesFromResponse([rawData[key]]),
}
}
- if (Array.isArray(rawData[key]) && rawData[key].some(item => item.dify_model_identity === '__dify__file__')) {
+ if (Array.isArray(rawData[key]) && rawData[key].some(item => item?.dify_model_identity === '__dify__file__')) {
return {
varName: key,
list: getProcessedFilesFromResponse(rawData[key]),
diff --git a/web/app/components/base/file-uploader/video-preview.tsx b/web/app/components/base/file-uploader/video-preview.tsx
new file mode 100644
index 00000000000000..0378f37ddf8070
--- /dev/null
+++ b/web/app/components/base/file-uploader/video-preview.tsx
@@ -0,0 +1,45 @@
+import type { FC } from 'react'
+import { createPortal } from 'react-dom'
+import { RiCloseLine } from '@remixicon/react'
+import React from 'react'
+import { useHotkeys } from 'react-hotkeys-hook'
+
+type VideoPreviewProps = {
+ url: string
+ title: string
+ onCancel: () => void
+}
+const VideoPreview: FC = ({
+ url,
+ title,
+ onCancel,
+}) => {
+ useHotkeys('esc', onCancel)
+
+ return createPortal(
+ e.stopPropagation()}
+ tabIndex={-1}
+ >
+
+
+
+
+
+
+
+ , document.body,
+ )
+}
+
+export default VideoPreview
diff --git a/web/app/components/base/fullscreen-modal/index.tsx b/web/app/components/base/fullscreen-modal/index.tsx
new file mode 100644
index 00000000000000..752a91cecc88ab
--- /dev/null
+++ b/web/app/components/base/fullscreen-modal/index.tsx
@@ -0,0 +1,82 @@
+import { Dialog, Transition } from '@headlessui/react'
+import { Fragment } from 'react'
+import { RiCloseLargeLine } from '@remixicon/react'
+import classNames from '@/utils/classnames'
+
+type IModal = {
+ className?: string
+ wrapperClassName?: string
+ open: boolean
+ onClose?: () => void
+ title?: React.ReactNode
+ description?: React.ReactNode
+ children?: React.ReactNode
+ closable?: boolean
+ overflowVisible?: boolean
+}
+
+export default function FullScreenModal({
+ className,
+ wrapperClassName,
+ open,
+ onClose = () => { },
+ children,
+ closable = false,
+ overflowVisible = false,
+}: IModal) {
+ return (
+
+
+
+ )
+}
diff --git a/web/app/components/base/icons/assets/vender/solid/communication/bubble-text-mod.svg b/web/app/components/base/icons/assets/vender/solid/communication/bubble-text-mod.svg
new file mode 100644
index 00000000000000..7a37245066784c
--- /dev/null
+++ b/web/app/components/base/icons/assets/vender/solid/communication/bubble-text-mod.svg
@@ -0,0 +1,3 @@
+
diff --git a/web/app/components/base/icons/assets/vender/solid/communication/list-sparkle.svg b/web/app/components/base/icons/assets/vender/solid/communication/list-sparkle.svg
new file mode 100644
index 00000000000000..dcda2ef3464543
--- /dev/null
+++ b/web/app/components/base/icons/assets/vender/solid/communication/list-sparkle.svg
@@ -0,0 +1,6 @@
+
diff --git a/web/app/components/base/icons/assets/vender/solid/communication/logic.svg b/web/app/components/base/icons/assets/vender/solid/communication/logic.svg
new file mode 100644
index 00000000000000..4c019e32a84076
--- /dev/null
+++ b/web/app/components/base/icons/assets/vender/solid/communication/logic.svg
@@ -0,0 +1,8 @@
+
diff --git a/web/app/components/base/icons/src/public/common/Lock.json b/web/app/components/base/icons/src/public/common/Lock.json
new file mode 100644
index 00000000000000..24af41a73dc5fb
--- /dev/null
+++ b/web/app/components/base/icons/src/public/common/Lock.json
@@ -0,0 +1,38 @@
+{
+ "icon": {
+ "type": "element",
+ "isRootNode": true,
+ "name": "svg",
+ "attributes": {
+ "width": "16",
+ "height": "16",
+ "viewBox": "0 0 16 16",
+ "fill": "none",
+ "xmlns": "http://www.w3.org/2000/svg"
+ },
+ "children": [
+ {
+ "type": "element",
+ "name": "g",
+ "attributes": {
+ "id": "lock"
+ },
+ "children": [
+ {
+ "type": "element",
+ "name": "path",
+ "attributes": {
+ "id": "Vector",
+ "fill-rule": "evenodd",
+ "clip-rule": "evenodd",
+ "d": "M8 1.75C6.27411 1.75 4.875 3.14911 4.875 4.875V6.125C3.83947 6.125 3 6.96444 3 8V12.375C3 13.4106 3.83947 14.25 4.875 14.25H11.125C12.1606 14.25 13 13.4106 13 12.375V8C13 6.96444 12.1606 6.125 11.125 6.125V4.875C11.125 3.14911 9.72587 1.75 8 1.75ZM9.875 6.125V4.875C9.875 3.83947 9.03556 3 8 3C6.96444 3 6.125 3.83947 6.125 4.875V6.125H9.875ZM8 8.625C8.34519 8.625 8.625 8.90481 8.625 9.25V11.125C8.625 11.4702 8.34519 11.75 8 11.75C7.65481 11.75 7.375 11.4702 7.375 11.125V9.25C7.375 8.90481 7.65481 8.625 8 8.625Z",
+ "fill": "#155AEF"
+ },
+ "children": []
+ }
+ ]
+ }
+ ]
+ },
+ "name": "Lock"
+}
\ No newline at end of file
diff --git a/web/app/components/base/icons/src/public/common/Lock.tsx b/web/app/components/base/icons/src/public/common/Lock.tsx
new file mode 100644
index 00000000000000..3a2ed3857403bf
--- /dev/null
+++ b/web/app/components/base/icons/src/public/common/Lock.tsx
@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './Lock.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef, Omit>((
+ props,
+ ref,
+) => )
+
+Icon.displayName = 'Lock'
+
+export default Icon
diff --git a/web/app/components/base/icons/src/public/common/index.ts b/web/app/components/base/icons/src/public/common/index.ts
index b20668175c1c4a..e581cff7510b9e 100644
--- a/web/app/components/base/icons/src/public/common/index.ts
+++ b/web/app/components/base/icons/src/public/common/index.ts
@@ -3,6 +3,7 @@ export { default as DiagonalDividingLine } from './DiagonalDividingLine'
export { default as Dify } from './Dify'
export { default as Github } from './Github'
export { default as Line3 } from './Line3'
+export { default as Lock } from './Lock'
export { default as MessageChatSquare } from './MessageChatSquare'
export { default as MultiPathRetrieval } from './MultiPathRetrieval'
export { default as NTo1Retrieval } from './NTo1Retrieval'
diff --git a/web/app/components/base/icons/src/vender/solid/communication/BubbleTextMod.json b/web/app/components/base/icons/src/vender/solid/communication/BubbleTextMod.json
new file mode 100644
index 00000000000000..fceddcc72989dd
--- /dev/null
+++ b/web/app/components/base/icons/src/vender/solid/communication/BubbleTextMod.json
@@ -0,0 +1,28 @@
+{
+ "icon": {
+ "type": "element",
+ "isRootNode": true,
+ "name": "svg",
+ "attributes": {
+ "width": "24",
+ "height": "24",
+ "viewBox": "0 0 24 24",
+ "fill": "none",
+ "xmlns": "http://www.w3.org/2000/svg"
+ },
+ "children": [
+ {
+ "type": "element",
+ "name": "path",
+ "attributes": {
+ "fill-rule": "evenodd",
+ "clip-rule": "evenodd",
+ "d": "M2 9C2 5.68629 4.68629 3 8 3H16C19.3137 3 22 5.68629 22 9V15C22 18.3137 19.3137 21 16 21H3C2.44772 21 2 20.5523 2 20V9ZM9 9C8.44772 9 8 9.44772 8 10C8 10.5523 8.44772 11 9 11H15C15.5523 11 16 10.5523 16 10C16 9.44772 15.5523 9 15 9H9ZM9 13C8.44772 13 8 13.4477 8 14C8 14.5523 8.44772 15 9 15H12C12.5523 15 13 14.5523 13 14C13 13.4477 12.5523 13 12 13H9Z",
+ "fill": "currentColor"
+ },
+ "children": []
+ }
+ ]
+ },
+ "name": "BubbleTextMod"
+}
\ No newline at end of file
diff --git a/web/app/components/base/icons/src/vender/solid/communication/BubbleTextMod.tsx b/web/app/components/base/icons/src/vender/solid/communication/BubbleTextMod.tsx
new file mode 100644
index 00000000000000..944084278693f8
--- /dev/null
+++ b/web/app/components/base/icons/src/vender/solid/communication/BubbleTextMod.tsx
@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './BubbleTextMod.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef, Omit>((
+ props,
+ ref,
+) => )
+
+Icon.displayName = 'BubbleTextMod'
+
+export default Icon
diff --git a/web/app/components/base/icons/src/vender/solid/communication/ListSparkle.json b/web/app/components/base/icons/src/vender/solid/communication/ListSparkle.json
new file mode 100644
index 00000000000000..2e348e4b8fbb94
--- /dev/null
+++ b/web/app/components/base/icons/src/vender/solid/communication/ListSparkle.json
@@ -0,0 +1,53 @@
+{
+ "icon": {
+ "type": "element",
+ "isRootNode": true,
+ "name": "svg",
+ "attributes": {
+ "width": "24",
+ "height": "24",
+ "viewBox": "0 0 24 24",
+ "fill": "none",
+ "xmlns": "http://www.w3.org/2000/svg"
+ },
+ "children": [
+ {
+ "type": "element",
+ "name": "path",
+ "attributes": {
+ "d": "M4 5C3.44772 5 3 5.44772 3 6C3 6.55228 3.44772 7 4 7H20C20.5523 7 21 6.55228 21 6C21 5.44772 20.5523 5 20 5H4Z",
+ "fill": "currentColor"
+ },
+ "children": []
+ },
+ {
+ "type": "element",
+ "name": "path",
+ "attributes": {
+ "d": "M17.9191 9.60608C17.7616 9.2384 17.4 9 17 9C16.6 9 16.2384 9.2384 16.0809 9.60608L14.7384 12.7384L11.6061 14.0809C11.2384 14.2384 11 14.6 11 15C11 15.4 11.2384 15.7616 11.6061 15.9191L14.7384 17.2616L16.0809 20.3939C16.2384 20.7616 16.6 21 17 21C17.4 21 17.7616 20.7616 17.9191 20.3939L19.2616 17.2616L22.3939 15.9191C22.7616 15.7616 23 15.4 23 15C23 14.6 22.7616 14.2384 22.3939 14.0809L19.2616 12.7384L17.9191 9.60608Z",
+ "fill": "currentColor"
+ },
+ "children": []
+ },
+ {
+ "type": "element",
+ "name": "path",
+ "attributes": {
+ "d": "M4 11C3.44772 11 3 11.4477 3 12C3 12.5523 3.44772 13 4 13H9C9.55228 13 10 12.5523 10 12C10 11.4477 9.55228 11 9 11H4Z",
+ "fill": "currentColor"
+ },
+ "children": []
+ },
+ {
+ "type": "element",
+ "name": "path",
+ "attributes": {
+ "d": "M4 17C3.44772 17 3 17.4477 3 18C3 18.5523 3.44772 19 4 19H7C7.55228 19 8 18.5523 8 18C8 17.4477 7.55228 17 7 17H4Z",
+ "fill": "currentColor"
+ },
+ "children": []
+ }
+ ]
+ },
+ "name": "ListSparkle"
+}
\ No newline at end of file
diff --git a/web/app/components/base/icons/src/vender/solid/communication/ListSparkle.tsx b/web/app/components/base/icons/src/vender/solid/communication/ListSparkle.tsx
new file mode 100644
index 00000000000000..f616009bddae39
--- /dev/null
+++ b/web/app/components/base/icons/src/vender/solid/communication/ListSparkle.tsx
@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './ListSparkle.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef, Omit>((
+ props,
+ ref,
+) => )
+
+Icon.displayName = 'ListSparkle'
+
+export default Icon
diff --git a/web/app/components/base/icons/src/vender/solid/communication/Logic.json b/web/app/components/base/icons/src/vender/solid/communication/Logic.json
new file mode 100644
index 00000000000000..57f86f4dd84389
--- /dev/null
+++ b/web/app/components/base/icons/src/vender/solid/communication/Logic.json
@@ -0,0 +1,53 @@
+{
+ "icon": {
+ "type": "element",
+ "isRootNode": true,
+ "name": "svg",
+ "attributes": {
+ "width": "24",
+ "height": "24",
+ "viewBox": "0 0 24 24",
+ "fill": "none",
+ "xmlns": "http://www.w3.org/2000/svg"
+ },
+ "children": [
+ {
+ "type": "element",
+ "name": "g",
+ "attributes": {
+ "id": "logic"
+ },
+ "children": [
+ {
+ "type": "element",
+ "name": "g",
+ "attributes": {
+ "id": "Vector"
+ },
+ "children": [
+ {
+ "type": "element",
+ "name": "path",
+ "attributes": {
+ "d": "M12.9089 11.9999C13.913 11.9999 14.727 11.186 14.727 10.1819C14.727 9.17775 13.913 8.36376 12.9089 8.36376C11.9048 8.36376 11.0908 9.17775 11.0908 10.1819C11.0908 11.186 11.9048 11.9999 12.9089 11.9999Z",
+ "fill": "currentColor"
+ },
+ "children": []
+ },
+ {
+ "type": "element",
+ "name": "path",
+ "attributes": {
+ "d": "M12.2871 1.11229C9.95219 1.3228 7.78275 2.40696 6.21264 4.14796C4.64254 5.88897 3.78749 8.15849 3.81849 10.5027V10.8763L2.09676 14.3207C2.04261 14.4277 2.01016 14.5444 2.00129 14.6639C1.99241 14.7835 2.00729 14.9037 2.04506 15.0175C2.08283 15.1313 2.14275 15.2366 2.22136 15.3271C2.29997 15.4177 2.39573 15.4918 2.50311 15.5452L3.81849 16.1979V18.3632C3.81849 19.0865 4.10581 19.7802 4.61725 20.2916C5.12869 20.803 5.82234 21.0904 6.54562 21.0904H9.27276V22.9084H19.2722V16.6606C20.5995 15.3604 21.496 13.6844 21.8409 11.8588C22.1858 10.0331 21.9625 8.14562 21.2012 6.45084C20.4398 4.75606 19.1769 3.33556 17.583 2.38094C15.989 1.42633 14.1406 0.983541 12.2871 1.11229ZM17.4542 11.0909H16.416C16.3316 11.4163 16.2016 11.7282 16.0297 12.0172L16.7651 12.7526C16.8519 12.8365 16.9212 12.9368 16.9688 13.0477C17.0165 13.1586 17.0415 13.2779 17.0426 13.3986C17.0436 13.5193 17.0206 13.639 16.9749 13.7507C16.9292 13.8624 16.8617 13.9639 16.7764 14.0493C16.691 14.1346 16.5895 14.2021 16.4778 14.2478C16.3661 14.2935 16.2464 14.3165 16.1257 14.3155C16.005 14.3144 15.8857 14.2893 15.7748 14.2417C15.6639 14.1941 15.5636 14.1248 15.4797 14.038L14.7443 13.3026C14.4553 13.4745 14.1434 13.6045 13.818 13.6889V14.727C13.818 14.9681 13.7222 15.1994 13.5517 15.3698C13.3812 15.5403 13.15 15.6361 12.9089 15.6361C12.6678 15.6361 12.4366 15.5403 12.2661 15.3698C12.0957 15.1994 11.9999 14.9681 11.9999 14.727V13.6889C11.6744 13.6045 11.3625 13.4745 11.0736 13.3026L10.3382 14.038C10.1667 14.2036 9.93708 14.2952 9.69873 14.2931C9.46038 14.2911 9.23239 14.1955 9.06384 14.0269C8.8953 13.8584 8.79969 13.6304 8.79762 13.392C8.79555 13.1537 8.88718 12.924 9.05277 12.7526L9.78818 12.0172C9.61629 11.7282 9.48622 11.4163 9.40184 11.0909H8.36371C8.12262 11.0909 7.8914 10.9951 7.72092 10.8246C7.55044 10.6541 7.45467 10.4229 7.45467 10.1818C7.45467 9.94073 7.55044 9.70951 7.72092 9.53903C7.8914 9.36855 8.12262 9.27278 8.36371 9.27278H9.40184C9.48622 8.94731 9.61629 8.63544 9.78818 8.34647L9.05277 7.61105C8.88718 7.4396 8.79555 7.20997 8.79762 6.97163C8.79969 6.73328 8.8953 6.50528 9.06384 6.33673C9.23239 6.16819 9.46038 6.07259 9.69873 6.07052C9.93708 6.06844 10.1667 6.16007 10.3382 6.32566L11.0736 7.06108C11.3625 6.88918 11.6744 6.75911 11.9999 6.67473V5.63661C11.9999 5.39551 12.0957 5.16429 12.2661 4.99381C12.4366 4.82334 12.6678 4.72756 12.9089 4.72756C13.15 4.72756 13.3812 4.82334 13.5517 4.99381C13.7222 5.16429 13.818 5.39551 13.818 5.63661V6.67473C14.1434 6.75911 14.4553 6.88918 14.7443 7.06108L15.4797 6.32566C15.5636 6.23884 15.6639 6.16958 15.7748 6.12194C15.8857 6.0743 16.005 6.04922 16.1257 6.04817C16.2464 6.04713 16.3661 6.07013 16.4778 6.11583C16.5895 6.16154 16.691 6.22904 16.7764 6.31439C16.8617 6.39975 16.9292 6.50124 16.9749 6.61296C17.0206 6.72468 17.0436 6.84438 17.0426 6.96508C17.0415 7.08578 17.0165 7.20507 16.9688 7.31598C16.9212 7.42688 16.8519 7.52719 16.7651 7.61105L16.0297 8.34647C16.2016 8.63544 16.3316 8.94731 16.416 9.27278H17.4542C17.6952 9.27278 17.9265 9.36855 18.0969 9.53903C18.2674 9.70951 18.3632 9.94073 18.3632 10.1818C18.3632 10.4229 18.2674 10.6541 18.0969 10.8246C17.9265 10.9951 17.6952 11.0909 17.4542 11.0909Z",
+ "fill": "currentColor"
+ },
+ "children": []
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "name": "Logic"
+}
\ No newline at end of file
diff --git a/web/app/components/base/icons/src/vender/solid/communication/Logic.tsx b/web/app/components/base/icons/src/vender/solid/communication/Logic.tsx
new file mode 100644
index 00000000000000..7aae28bef438ec
--- /dev/null
+++ b/web/app/components/base/icons/src/vender/solid/communication/Logic.tsx
@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './Logic.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef, Omit>((
+ props,
+ ref,
+) => )
+
+Icon.displayName = 'Logic'
+
+export default Icon
diff --git a/web/app/components/base/icons/src/vender/solid/communication/index.ts b/web/app/components/base/icons/src/vender/solid/communication/index.ts
index 673de27463149e..7d2a3a5a95c32d 100644
--- a/web/app/components/base/icons/src/vender/solid/communication/index.ts
+++ b/web/app/components/base/icons/src/vender/solid/communication/index.ts
@@ -1,7 +1,10 @@
export { default as AiText } from './AiText'
+export { default as BubbleTextMod } from './BubbleTextMod'
export { default as ChatBot } from './ChatBot'
export { default as CuteRobot } from './CuteRobot'
export { default as EditList } from './EditList'
+export { default as ListSparkle } from './ListSparkle'
+export { default as Logic } from './Logic'
export { default as MessageDotsCircle } from './MessageDotsCircle'
export { default as MessageFast } from './MessageFast'
export { default as MessageHeartCircle } from './MessageHeartCircle'
diff --git a/web/app/components/base/image-uploader/image-preview.tsx b/web/app/components/base/image-uploader/image-preview.tsx
index 9ee5691f25c7dc..748e6ba8daa088 100644
--- a/web/app/components/base/image-uploader/image-preview.tsx
+++ b/web/app/components/base/image-uploader/image-preview.tsx
@@ -3,6 +3,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'
import { t } from 'i18next'
import { createPortal } from 'react-dom'
import { RiAddBoxLine, RiCloseLine, RiDownloadCloud2Line, RiFileCopyLine, RiZoomInLine, RiZoomOutLine } from '@remixicon/react'
+import { useHotkeys } from 'react-hotkeys-hook'
import Tooltip from '@/app/components/base/tooltip'
import Toast from '@/app/components/base/toast'
@@ -10,6 +11,8 @@ type ImagePreviewProps = {
url: string
title: string
onCancel: () => void
+ onPrev?: () => void
+ onNext?: () => void
}
const isBase64 = (str: string): boolean => {
@@ -25,6 +28,8 @@ const ImagePreview: FC = ({
url,
title,
onCancel,
+ onPrev,
+ onNext,
}) => {
const [scale, setScale] = useState(1)
const [position, setPosition] = useState({ x: 0, y: 0 })
@@ -32,7 +37,6 @@ const ImagePreview: FC = ({
const imgRef = useRef(null)
const dragStartRef = useRef({ x: 0, y: 0 })
const [isCopied, setIsCopied] = useState(false)
- const containerRef = useRef(null)
const openInNewTab = () => {
// Open in a new window, considering the case when the page is inside an iframe
@@ -51,6 +55,7 @@ const ImagePreview: FC = ({
})
}
}
+
const downloadImage = () => {
// Open in a new window, considering the case when the page is inside an iframe
if (url.startsWith('http') || url.startsWith('https')) {
@@ -188,23 +193,11 @@ const ImagePreview: FC = ({
}
}, [handleMouseUp])
- useEffect(() => {
- const handleKeyDown = (event: KeyboardEvent) => {
- if (event.key === 'Escape')
- onCancel()
- }
-
- window.addEventListener('keydown', handleKeyDown)
-
- // Set focus to the container element
- if (containerRef.current)
- containerRef.current.focus()
-
- // Cleanup function
- return () => {
- window.removeEventListener('keydown', handleKeyDown)
- }
- }, [onCancel])
+ useHotkeys('esc', onCancel)
+ useHotkeys('up', zoomIn)
+ useHotkeys('down', zoomOut)
+ useHotkeys('left', onPrev || (() => {}))
+ useHotkeys('right', onNext || (() => {}))
return createPortal(
& VariantProps
const Input = ({
@@ -43,6 +44,7 @@ const Input = ({
value,
placeholder,
onChange,
+ unit,
...props
}: InputProps) => {
const { t } = useTranslation()
@@ -80,6 +82,13 @@ const Input = ({
{destructive && (
)}
+ {
+ unit && (
+
+ {unit}
+
+ )
+ }
)
}
diff --git a/web/app/components/base/mermaid/index.tsx b/web/app/components/base/mermaid/index.tsx
index dc01338a8c9b16..bcc30ca9393623 100644
--- a/web/app/components/base/mermaid/index.tsx
+++ b/web/app/components/base/mermaid/index.tsx
@@ -1,30 +1,17 @@
-import React, { useEffect, useRef, useState } from 'react'
+import React, { useCallback, useEffect, useRef, useState } from 'react'
import mermaid from 'mermaid'
import { usePrevious } from 'ahooks'
-import CryptoJS from 'crypto-js'
+import { useTranslation } from 'react-i18next'
import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'
import LoadingAnim from '@/app/components/base/chat/chat/loading-anim'
+import cn from '@/utils/classnames'
+import ImagePreview from '@/app/components/base/image-uploader/image-preview'
let mermaidAPI: any
mermaidAPI = null
-if (typeof window !== 'undefined') {
- mermaid.initialize({
- startOnLoad: true,
- theme: 'default',
- flowchart: {
- htmlLabels: true,
- useMaxWidth: true,
- },
- })
+if (typeof window !== 'undefined')
mermaidAPI = mermaid.mermaidAPI
-}
-
-const style = {
- minWidth: '480px',
- height: 'auto',
- overflow: 'auto',
-}
const svgToBase64 = (svgGraph: string) => {
const svgBytes = new TextEncoder().encode(svgGraph)
@@ -40,22 +27,26 @@ const svgToBase64 = (svgGraph: string) => {
const Flowchart = React.forwardRef((props: {
PrimitiveCode: string
}, ref) => {
+ const { t } = useTranslation()
const [svgCode, setSvgCode] = useState(null)
- const chartId = useRef(`flowchart_${CryptoJS.MD5(props.PrimitiveCode).toString()}`)
+ const [look, setLook] = useState<'classic' | 'handDrawn'>('classic')
+
const prevPrimitiveCode = usePrevious(props.PrimitiveCode)
const [isLoading, setIsLoading] = useState(true)
const timeRef = useRef()
const [errMsg, setErrMsg] = useState('')
+ const [imagePreviewUrl, setImagePreviewUrl] = useState('')
+
+ const renderFlowchart = useCallback(async (PrimitiveCode: string) => {
+ setSvgCode(null)
+ setIsLoading(true)
- const renderFlowchart = async (PrimitiveCode: string) => {
try {
if (typeof window !== 'undefined' && mermaidAPI) {
- const svgGraph = await mermaidAPI.render(chartId.current, PrimitiveCode)
+ const svgGraph = await mermaidAPI.render('flowchart', PrimitiveCode)
const base64Svg: any = await svgToBase64(svgGraph.svg)
setSvgCode(base64Svg)
setIsLoading(false)
- if (chartId.current && base64Svg)
- localStorage.setItem(chartId.current, base64Svg)
}
}
catch (error) {
@@ -64,15 +55,25 @@ const Flowchart = React.forwardRef((props: {
setErrMsg((error as Error).message)
}
}
- }
+ }, [props.PrimitiveCode])
useEffect(() => {
- const cachedSvg: any = localStorage.getItem(chartId.current)
- if (cachedSvg) {
- setSvgCode(cachedSvg)
- setIsLoading(false)
- return
+ if (typeof window !== 'undefined') {
+ mermaid.initialize({
+ startOnLoad: true,
+ theme: 'neutral',
+ look,
+ flowchart: {
+ htmlLabels: true,
+ useMaxWidth: true,
+ },
+ })
+
+ renderFlowchart(props.PrimitiveCode)
}
+ }, [look])
+
+ useEffect(() => {
if (timeRef.current)
clearTimeout(timeRef.current)
@@ -85,24 +86,51 @@ const Flowchart = React.forwardRef((props: {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
+
+
+
+
+
{
svgCode
- &&
- {svgCode &&
}
-
+ &&
setImagePreviewUrl(svgCode)}>
+ {svgCode &&
}
+
}
{isLoading
- &&
-
-
+ &&
+
+
}
{
errMsg
- &&
-
-
- {errMsg}
-
+ &&
+
+
+ {errMsg}
+
+ }
+ {
+ imagePreviewUrl && (
setImagePreviewUrl('')} />)
}
)
diff --git a/web/app/components/base/modal/index.css b/web/app/components/base/modal/index.css
index 727a9455d7fbcf..3787da28f4c2ae 100644
--- a/web/app/components/base/modal/index.css
+++ b/web/app/components/base/modal/index.css
@@ -3,5 +3,5 @@
}
.modal-panel {
- @apply w-full max-w-[480px] transform rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all;
+ @apply w-full max-w-[480px] transform rounded-2xl bg-components-panel-bg p-6 text-left align-middle shadow-xl transition-all;
}
diff --git a/web/app/components/base/modal/index.tsx b/web/app/components/base/modal/index.tsx
index 9a80fc0486923c..26cde5fce3dd6a 100644
--- a/web/app/components/base/modal/index.tsx
+++ b/web/app/components/base/modal/index.tsx
@@ -1,6 +1,6 @@
import { Dialog, Transition } from '@headlessui/react'
import { Fragment } from 'react'
-import { XMarkIcon } from '@heroicons/react/24/outline'
+import { RiCloseLine } from '@remixicon/react'
import classNames from '@/utils/classnames'
// https://headlessui.com/react/dialog
@@ -29,7 +29,7 @@ export default function Modal({
}: IModal) {
return (
-