)
})
diff --git a/web/app/(commonLayout)/apps/page.tsx b/web/app/(commonLayout)/apps/page.tsx
index ab9852e462..972aabc8bc 100644
--- a/web/app/(commonLayout)/apps/page.tsx
+++ b/web/app/(commonLayout)/apps/page.tsx
@@ -1,9 +1,10 @@
'use client'
import { useContextSelector } from 'use-context-selector'
import { useTranslation } from 'react-i18next'
+import { RiDiscordFill, RiGithubFill } from '@remixicon/react'
+import Link from 'next/link'
import style from '../list.module.css'
import Apps from './Apps'
-import classNames from '@/utils/classnames'
import AppContext from '@/context/app-context'
import { LicenseStatus } from '@/types/feature'
@@ -12,14 +13,18 @@ const AppList = () => {
const systemFeatures = useContextSelector(AppContext, v => v.systemFeatures)
return (
-
+
{systemFeatures.license.status === LicenseStatus.NONE &&
}
diff --git a/web/app/(commonLayout)/list.module.css b/web/app/(commonLayout)/list.module.css
index bb2aa8606c..2fc6469a6d 100644
--- a/web/app/(commonLayout)/list.module.css
+++ b/web/app/(commonLayout)/list.module.css
@@ -201,14 +201,6 @@
@apply block w-6 h-6 bg-center bg-contain;
}
-.githubIcon {
- background-image: url("./apps/assets/github.svg");
-}
-
-.discordIcon {
- background-image: url("./apps/assets/discord.svg");
-}
-
/* #region new app dialog */
.newItemCaption {
@apply inline-flex items-center mb-2 text-sm font-medium;
diff --git a/web/app/components/app-sidebar/app-info.tsx b/web/app/components/app-sidebar/app-info.tsx
index 12fe5cba46..12f9c59cd1 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 4da6b7cac4..032e4b8357 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 fbbea70612..d2189b4581 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 88ce23b9aa..d37593c784 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 8295df6e4d..9a496a1989 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 b4c4f8fc0d..6f7c32283a 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 0fb8bbc31e..83a64b980f 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 bf45136283..0000000000
--- 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 0558e29956..3ba35a7336 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 0000000000..c9354ce2e1
--- /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 0000000000..73b34c7d02
--- /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 13620a36f3..acc3650211 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 384e29831c..0000000000
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 6f45192b02..0000000000
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 971d5c39ab..0000000000
--- 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 7a3e1a83ec..0000000000
--- 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 9f80a6c440..0000000000
--- 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 144beda82c..0000000000
--- 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 496de58c19..eb78258cc8 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')}
-
-
-
- {t('app.newApp.completionDescription')}
+ 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')}
- }
- >
-
{
- setAppMode('completion')
- setShowChatBotType(false)
- }}
- >
-
-
{t('app.newApp.completeApp')}
-
-
-
{t('app.newApp.agentDescription')}
- }
- >
-
{
- setAppMode('agent-chat')
- setShowChatBotType(false)
- }}
- >
-
-
{t('app.types.agent')}
-
-
-
- {t('app.newApp.workflowDescription')}
+ }
+ onClick={() => {
+ setAppMode('chat')
+ }} />
+
+
+ }
+ onClick={() => {
+ setAppMode('agent-chat')
+ }} />
+
+
+ }
+ onClick={() => {
+ setAppMode('completion')
+ }} />
- }
- >
-
{
- 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.forAdvanced')}
+
+
}
+ onClick={() => {
+ setAppMode('advanced-chat')
+ }} />
+
+
+ }
+ onClick={() => {
+ setAppMode('workflow')
+ }} />
+
+
+
+
+
+
+
+
setName(e.target.value)}
+ placeholder={t('app.newApp.appNamePlaceholder') || ''}
+ />
-
{t('app.newApp.basicTip')}
+
{ setShowAppIconPicker(true) }}
+ />
+ {showAppIconPicker && {
+ setAppIcon(payload)
+ setShowAppIconPicker(false)
+ }}
+ onClose={() => {
+ setShowAppIconPicker(false)
+ }}
+ />}
-
{
- 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.optional')})
-
{t('app.newApp.advancedFor')}
+
+
+
+
+
{t('app.newApp.noIdeaTip')}
+
+
+
+
+
+
{t('app.newApp.Cancel')}
+
+ {t('app.newApp.Create')}
+
+
+
+
+
- )}
-
- {/* icon & name */}
-
-
{t('app.newApp.captionName')}
-
+
+
+
- {showAppIconPicker &&
{
- setAppIcon(payload)
- setShowAppIconPicker(false)
- }}
- onClose={() => {
- setShowAppIconPicker(false)
- }}
- />}
- {/* description */}
-
-
{t('app.newApp.captionDescription')}
-
- {isAppsFull && (
+
+ {
+ 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 9a503a8cdd..0000000000
--- 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 3fa13019f9..696cc19ba4 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 978e83737b..8f064c209e 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'
@@ -18,20 +17,16 @@ import { useShallow } from 'zustand/react/shallow'
import { useTranslation } from 'react-i18next'
import type { ChatItemInTree } from '../../base/chat/types'
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 +39,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)
@@ -75,15 +71,6 @@ 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 getFormattedChatList = (messages: ChatMessage[], conversationId: string, timezone: string, format: string) => {
const newChatList: IChatItem[] = []
messages.forEach((item: ChatMessage) => {
@@ -156,9 +143,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 +299,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 +310,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 +323,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)) && (
-
-
+
+
+ {(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}
- />
-
- : 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 && (
= ({ 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 a22856339e..3ae4bfb5c6 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 e10350acc4..ac868e6ee3 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 (
- = ({
)
}
- {(currentTab === 'RESULT' || !isWorkflow) && (
+ {((currentTab === 'RESULT' && workflowProcessData?.resultText) || !isWorkflow) && (
{
- if (data?.resultText)
+ if (data?.resultText || !!data?.files?.length)
switchTab('RESULT')
else
switchTab('DETAIL')
- }, [data?.resultText])
+ }, [data?.files?.length, data?.resultText])
return (
- {data?.resultText && (
+ {(data?.resultText || !!data?.files?.length) && (
{currentTab === 'RESULT' && (
<>
-
+ {data?.resultText &&
}
{!!data?.files?.length && (
-
+
+ {data?.files.map((item: any) => (
+
+ ))}
+
)}
>
)}
diff --git a/web/app/components/app/type-selector/index.tsx b/web/app/components/app/type-selector/index.tsx
index a09e189f50..9deb3e956f 100644
--- a/web/app/components/app/type-selector/index.tsx
+++ b/web/app/components/app/type-selector/index.tsx
@@ -1,23 +1,23 @@
import { useTranslation } from 'react-i18next'
import React, { useState } from 'react'
-import { RiArrowDownSLine } from '@remixicon/react'
+import { RiArrowDownSLine, RiCloseCircleFill, RiExchange2Fill, RiFilter3Line } from '@remixicon/react'
+import Checkbox from '../../base/checkbox'
import cn from '@/utils/classnames'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
-import { Check, DotsGrid } from '@/app/components/base/icons/src/vender/line/general'
-import { XCircle } from '@/app/components/base/icons/src/vender/solid/general'
-import { ChatBot, CuteRobot } from '@/app/components/base/icons/src/vender/solid/communication'
-import { Route } from '@/app/components/base/icons/src/vender/solid/mapsAndTravel'
+import { BubbleTextMod, ChatBot, ListSparkle, Logic } from '@/app/components/base/icons/src/vender/solid/communication'
+import { type AppMode } from '@/types/app'
export type AppSelectorProps = {
- value: string
- onChange: (value: string) => 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/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 ba0ef6b2b2..f26f5a1fcb 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'
-type UploaderProps = {
- className?: string
- onImageCropped?: (tempUrl: string, croppedAreaPixels: Area, fileName: string) => void
- onUpload?: (file?: File) => void
+export type OnImageInput = {
+ (isCropped: true, tempUrl: string, croppedAreaPixels: Area, fileName: string): void
+ (isCropped: false, file: File): void
}
-const Uploader: FC
= ({
+type UploaderProps = {
+ className?: string
+ onImageInput?: OnImageInput
+}
+
+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 8a10d28653..277e2fa1d0 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 99154d56da..f63b75eaa1 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 832ae64c1b..c195b7253d 100644
--- a/web/app/components/base/app-icon/index.tsx
+++ b/web/app/components/base/app-icon/index.tsx
@@ -3,14 +3,15 @@
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 Image from 'next/image'
+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 +21,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,23 +54,15 @@ const AppIcon: FC = ({
innerIcon,
onClick,
}) => {
- const wrapperClassName = classNames(
- 'flex items-center justify-center relative w-9 h-9 text-lg rounded-lg grow-0 shrink-0',
- size !== 'medium' && style[size],
- rounded && style.rounded,
- className ?? '',
- 'overflow-hidden',
- )
-
const isValidImageIcon = iconType === 'image' && imageUrl
return
{isValidImageIcon
- ?
+ ?
: (innerIcon || ((icon && icon !== '') ? : ))
}
diff --git a/web/app/components/base/chat/chat/answer/index.tsx b/web/app/components/base/chat/chat/answer/index.tsx
index 1ff390bd58..c6d14ddead 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/checkbox/assets/check.svg b/web/app/components/base/checkbox/assets/check.svg
deleted file mode 100644
index f1f635ed74..0000000000
--- 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
index 30c887801b..d675607b46 100644
--- a/web/app/components/base/checkbox/index.module.css
+++ b/web/app/components/base/checkbox/index.module.css
@@ -1,9 +1,3 @@
-.checked {
- background: var(--color-components-checkbox-bg) url(./assets/check.svg) center center no-repeat;
- background-size: 12px 12px;
- border: none;
-}
-
.mixed {
background: var(--color-components-checkbox-bg) url(./assets/mixed.svg) center center no-repeat;
background-size: 12px 12px;
diff --git a/web/app/components/base/checkbox/index.tsx b/web/app/components/base/checkbox/index.tsx
index 51d5fd9027..a2b059e027 100644
--- a/web/app/components/base/checkbox/index.tsx
+++ b/web/app/components/base/checkbox/index.tsx
@@ -1,5 +1,6 @@
-import s from './index.module.css'
+import { RiCheckLine } from '@remixicon/react'
import cn from '@/utils/classnames'
+import s from './index.module.css'
type CheckboxProps = {
checked?: boolean
@@ -10,13 +11,28 @@ type CheckboxProps = {
}
const Checkbox = ({ checked, onCheck, className, disabled, mixed }: CheckboxProps) => {
+ if (!checked) {
+ return (
+ {
+ if (disabled)
+ return
+ onCheck?.()
+ }}
+ >
+ )
+ }
return (
{
@@ -25,7 +41,9 @@ const Checkbox = ({ checked, onCheck, className, disabled, mixed }: CheckboxProp
onCheck?.()
}}
- />
+ >
+
+
)
}
diff --git a/web/app/components/base/copy-icon/index.tsx b/web/app/components/base/copy-icon/index.tsx
index 425a9ad293..9f886a1f32 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 894bea20d8..51402572b7 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/emoji-picker/Inner.tsx b/web/app/components/base/emoji-picker/Inner.tsx
index 36c146a2a0..5db223e3f4 100644
--- a/web/app/components/base/emoji-picker/Inner.tsx
+++ b/web/app/components/base/emoji-picker/Inner.tsx
@@ -68,7 +68,7 @@ const EmojiPickerInner: FC
= ({
}, [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 801f1348ee..b16c724175 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 8b3a0af240..c192fcbec4 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 2e08a99122..1df7253f4e 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 d68db9be73..3a9a34be43 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 21e48b3dd4..6b217b17c1 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 0000000000..9a1ed440e2
--- /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 0000000000..116db89864
--- /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 1a433dec5d..9d263225e1 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 = ({

{
- const [expanded, setExpanded] = useState(false)
+const FileListInLog = ({ fileList, isExpanded = false, noBorder = false, noPadding = false }: Props) => {
+ const { t } = useTranslation()
+ const [expanded, setExpanded] = useState(isExpanded)
+ const fullList = useMemo(() => {
+ return fileList.reduce((acc: FileEntity[], { list }) => {
+ return [...acc, ...list]
+ }, [])
+ }, [fileList])
if (!fileList.length)
return null
+
return (
-
+
{expanded && (
-
+
setExpanded(!expanded)}>{t('appLog.runDetail.fileListLabel')}
)}
{!expanded && (
-
- {fileList.map((file) => {
+
+ {fullList.map((file) => {
const { id, name, type, supportFileType, base64Url, url } = file
const isImageFile = supportFileType === SupportUploadFileTypes.image
return (
@@ -63,19 +77,25 @@ const FileListInLog = ({ fileList }: Props) => {
)}
setExpanded(!expanded)}>
- {!expanded &&
DETAIL
}
+ {!expanded &&
{t('appLog.runDetail.fileListDetail')}
}
{expanded && (
-
- {fileList.map(file => (
-
+
+ {fileList.map(item => (
+
+
{item.varName}
+ {item.list.map(file => (
+
+ ))}
+
))}
)}
diff --git a/web/app/components/base/file-uploader/file-uploader-in-attachment/file-item.tsx b/web/app/components/base/file-uploader/file-uploader-in-attachment/file-item.tsx
index 2a042bab40..722ef64a68 100644
--- a/web/app/components/base/file-uploader/file-uploader-in-attachment/file-item.tsx
+++ b/web/app/components/base/file-uploader/file-uploader-in-attachment/file-item.tsx
@@ -1,12 +1,15 @@
import {
memo,
+ useState,
} from 'react'
import {
RiDeleteBinLine,
RiDownloadLine,
+ RiEyeLine,
} from '@remixicon/react'
import FileTypeIcon from '../file-type-icon'
import {
+ downloadFile,
fileIsUploaded,
getFileAppearanceType,
getFileExtension,
@@ -19,6 +22,7 @@ import { formatFileSize } from '@/utils/format'
import cn from '@/utils/classnames'
import { ReplayLine } from '@/app/components/base/icons/src/vender/other'
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
+import ImagePreview from '@/app/components/base/image-uploader/image-preview'
type FileInAttachmentItemProps = {
file: FileEntity
@@ -26,6 +30,7 @@ type FileInAttachmentItemProps = {
showDownloadAction?: boolean
onRemove?: (fileId: string) => void
onReUpload?: (fileId: string) => void
+ canPreview?: boolean
}
const FileInAttachmentItem = ({
file,
@@ -33,96 +38,116 @@ const FileInAttachmentItem = ({
showDownloadAction = true,
onRemove,
onReUpload,
+ canPreview,
}: FileInAttachmentItemProps) => {
const { id, name, type, progress, supportFileType, base64Url, url, isRemote } = file
const ext = getFileExtension(name, type, isRemote)
const isImageFile = supportFileType === SupportUploadFileTypes.image
-
+ const [imagePreviewUrl, setImagePreviewUrl] = useState('')
return (
-
-
- {
- isImageFile && (
-
- )
- }
- {
- !isImageFile && (
-
- )
- }
-
-
-
-
{name}
+ <>
+
+
+ {
+ isImageFile && (
+
+ )
+ }
+ {
+ !isImageFile && (
+
+ )
+ }
-
+
+
+
+ {
+ ext && (
+ {ext.toLowerCase()}
+ )
+ }
+ {
+ ext && (
+ •
+ )
+ }
+ {
+ !!file.size && (
+ {formatFileSize(file.size)}
+ )
+ }
+
+
+
{
- ext && (
-
{ext.toLowerCase()}
+ progress >= 0 && !fileIsUploaded(file) && (
+
)
}
{
- ext && (
-
•
+ progress === -1 && (
+
onReUpload?.(id)}
+ >
+
+
)
}
{
- !!file.size && (
-
{formatFileSize(file.size)}
+ showDeleteAction && (
+
onRemove?.(id)}>
+
+
+ )
+ }
+ {
+ canPreview && isImageFile && (
+
setImagePreviewUrl(url || '')}>
+
+
+ )
+ }
+ {
+ showDownloadAction && (
+
{
+ e.stopPropagation()
+ downloadFile(url || base64Url || '', name)
+ }}>
+
+
)
}
-
- {
- progress >= 0 && !fileIsUploaded(file) && (
-
- )
- }
- {
- progress === -1 && (
-
onReUpload?.(id)}
- >
-
-
- )
- }
- {
- showDeleteAction && (
-
onRemove?.(id)}>
-
-
- )
- }
- {
- showDownloadAction && (
-
-
-
- )
- }
-
-
+ {
+ imagePreviewUrl && canPreview && (
+
setImagePreviewUrl('')}
+ />
+ )
+ }
+ >
)
}
diff --git a/web/app/components/base/file-uploader/file-uploader-in-chat-input/file-image-item.tsx b/web/app/components/base/file-uploader/file-uploader-in-chat-input/file-image-item.tsx
index 6e9ef489fc..2cc78b7fb9 100644
--- a/web/app/components/base/file-uploader/file-uploader-in-chat-input/file-image-item.tsx
+++ b/web/app/components/base/file-uploader/file-uploader-in-chat-input/file-image-item.tsx
@@ -37,7 +37,7 @@ const FileImageItem = ({
<>
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 dcf4082780..ddbe745141 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, isRemote } = file
+ 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)
+ }
+
{
- ext && (
- <>
- {ext}
-
·
- >
+ showDownloadAction && tmp_preview_url && (
+
{
+ e.stopPropagation()
+ downloadFile(tmp_preview_url || '', name)
+ }}
+ >
+
+
)
}
{
- !!file.size && formatFileSize(file.size)
+ progress >= 0 && !fileIsUploaded(file) && (
+
+ )
+ }
+ {
+ uploadError && (
+
onReUpload?.(id)}
+ />
+ )
}
- {
- showDownloadAction && url && (
-
{
- e.stopPropagation()
- downloadFile(url || '', 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 69204640e0..ba909040c3 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 0000000000..04947be0a4
--- /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 aa8625f221..e095d4aa93 100644
--- a/web/app/components/base/file-uploader/utils.ts
+++ b/web/app/components/base/file-uploader/utils.ts
@@ -1,5 +1,4 @@
import mime from 'mime'
-import { flatten } from 'lodash-es'
import { FileAppearanceTypeEnum } from './types'
import type { FileEntity } from './types'
import { upload } from '@/service/base'
@@ -85,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,12 +157,22 @@ export const isAllowedFileExtension = (fileName: string, fileMimetype: string, a
}
export const getFilesInLogs = (rawData: any) => {
- const originalFiles = flatten(Object.keys(rawData || {}).map((key) => {
- if (typeof rawData[key] === 'object' || Array.isArray(rawData[key]))
- return rawData[key]
+ const result = Object.keys(rawData || {}).map((key) => {
+ 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__')) {
+ return {
+ varName: key,
+ list: getProcessedFilesFromResponse(rawData[key]),
+ }
+ }
return undefined
- }).filter(Boolean)).filter(item => item?.model_identity === '__dify__file__')
- return getProcessedFilesFromResponse(originalFiles)
+ }).filter(Boolean)
+ return result
}
export const fileIsUploaded = (file: FileEntity) => {
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 0000000000..0378f37ddf
--- /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 0000000000..752a91cecc
--- /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 0000000000..7a37245066
--- /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 0000000000..dcda2ef346
--- /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 0000000000..4c019e32a8
--- /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/vender/solid/communication/BubbleTextMod.json b/web/app/components/base/icons/src/vender/solid/communication/BubbleTextMod.json
new file mode 100644
index 0000000000..fceddcc729
--- /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 0000000000..9440842786
--- /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 0000000000..2e348e4b8f
--- /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 0000000000..f616009bdd
--- /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 0000000000..57f86f4dd8
--- /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 0000000000..7aae28bef4
--- /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 673de27463..7d2a3a5a95 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 9ee5691f25..748e6ba8da 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(
{
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 727a9455d7..3787da28f4 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 9a80fc0486..5b8c4be4b8 100644
--- a/web/app/components/base/modal/index.tsx
+++ b/web/app/components/base/modal/index.tsx
@@ -29,7 +29,7 @@ export default function Modal({
}: IModal) {
return (
-