From b49a4eab6224c4ac0718964963e0d4bd748c0c91 Mon Sep 17 00:00:00 2001 From: Joel Date: Fri, 24 Oct 2025 18:33:54 +0800 Subject: [PATCH 1/6] feat: add app list context --- .../app/create-app-dialog/app-card/index.tsx | 23 +++++++++++++++-- web/app/components/apps/index.tsx | 25 ++++++++++++++++--- web/context/app-list-context.ts | 17 +++++++++++++ 3 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 web/context/app-list-context.ts diff --git a/web/app/components/app/create-app-dialog/app-card/index.tsx b/web/app/components/app/create-app-dialog/app-card/index.tsx index 7f7ede0065..82f10a2d6f 100644 --- a/web/app/components/app/create-app-dialog/app-card/index.tsx +++ b/web/app/components/app/create-app-dialog/app-card/index.tsx @@ -6,6 +6,11 @@ import Button from '@/app/components/base/button' import cn from '@/utils/classnames' import type { App } from '@/models/explore' import AppIcon from '@/app/components/base/app-icon' +import { useGlobalPublicStore } from '@/context/global-public-context' +import { RiInformation2Line } from '@remixicon/react' +import { useCallback } from 'react' +import AppListContext from '@/context/app-list-context' +import { useContextSelector } from 'use-context-selector' export type AppCardProps = { app: App @@ -19,6 +24,14 @@ const AppCard = ({ }: AppCardProps) => { const { t } = useTranslation() const { app: appBasicInfo } = app + const { systemFeatures } = useGlobalPublicStore() + const isTrialApp = app.can_trial && systemFeatures.enable_trial_app + const setShowTryAppPanel = useContextSelector(AppListContext, ctx => ctx.setShowTryAppPanel) + const showTryAPPPanel = useCallback((appId: string) => { + return () => { + setShowTryAppPanel?.(true, { appId, app }) + } + }, [setShowTryAppPanel, app.category]) return (
@@ -46,11 +59,17 @@ const AppCard = ({
diff --git a/web/app/components/apps/index.tsx b/web/app/components/apps/index.tsx index 6d21800421..c02741705f 100644 --- a/web/app/components/apps/index.tsx +++ b/web/app/components/apps/index.tsx @@ -3,6 +3,9 @@ import { useEducationInit } from '@/app/education-apply/hooks' import List from './list' import useDocumentTitle from '@/hooks/use-document-title' import { useTranslation } from 'react-i18next' +import AppListContext from '@/context/app-list-context' +import { useState } from 'react' +import type { CurrentTryAppParams } from '@/context/explore-context' const Apps = () => { const { t } = useTranslation() @@ -10,10 +13,26 @@ const Apps = () => { useDocumentTitle(t('common.menus.apps')) useEducationInit() + const [currentTryAppParams, setCurrentTryAppParams] = useState(undefined) + const [isShowTryAppPanel, setIsShowTryAppPanel] = useState(false) + const setShowTryAppPanel = (showTryAppPanel: boolean, params?: CurrentTryAppParams) => { + if (showTryAppPanel) + setCurrentTryAppParams(params) + else + setCurrentTryAppParams(undefined) + setIsShowTryAppPanel(showTryAppPanel) + } + return ( -
- -
+ +
+ +
+
) } diff --git a/web/context/app-list-context.ts b/web/context/app-list-context.ts new file mode 100644 index 0000000000..bdd1766bb2 --- /dev/null +++ b/web/context/app-list-context.ts @@ -0,0 +1,17 @@ +import { createContext } from 'use-context-selector' +import { noop } from 'lodash-es' +import type { CurrentTryAppParams } from './explore-context' + +type Props = { + currentApp?: CurrentTryAppParams + isShowTryAppPanel: boolean + setShowTryAppPanel: (showTryAppPanel: boolean, params?: CurrentTryAppParams) => void +} + +const AppListContext = createContext({ + isShowTryAppPanel: false, + setShowTryAppPanel: noop, + currentApp: undefined, +}) + +export default AppListContext From 8786ebdbcaa483c1b3f88c78b52bf1a27e65f9a9 Mon Sep 17 00:00:00 2001 From: Joel Date: Mon, 27 Oct 2025 10:58:57 +0800 Subject: [PATCH 2/6] feat: support use tempalte in create app --- web/app/components/apps/index.tsx | 107 +++++++++++++++++- web/app/components/apps/list.tsx | 13 ++- web/app/components/apps/new-app-card.tsx | 10 +- web/app/components/explore/app-card/index.tsx | 2 +- web/context/app-list-context.ts | 2 + 5 files changed, 129 insertions(+), 5 deletions(-) diff --git a/web/app/components/apps/index.tsx b/web/app/components/apps/index.tsx index c02741705f..ba611a597b 100644 --- a/web/app/components/apps/index.tsx +++ b/web/app/components/apps/index.tsx @@ -4,8 +4,15 @@ import List from './list' import useDocumentTitle from '@/hooks/use-document-title' import { useTranslation } from 'react-i18next' import AppListContext from '@/context/app-list-context' -import { useState } from 'react' +import { useCallback, useState } from 'react' import type { CurrentTryAppParams } from '@/context/explore-context' +import TryApp from '../explore/try-app' +import type { CreateAppModalProps } from '../explore/create-app-modal' +import CreateAppModal from '../explore/create-app-modal' +import { fetchAppDetail } from '@/service/explore' +import { DSLImportMode } from '@/models/app' +import { useImportDSL } from '@/hooks/use-import-dsl' +import DSLConfirmModal from '../app/create-from-dsl-modal/dsl-confirm-modal' const Apps = () => { const { t } = useTranslation() @@ -14,7 +21,11 @@ const Apps = () => { useEducationInit() const [currentTryAppParams, setCurrentTryAppParams] = useState(undefined) + const currApp = currentTryAppParams?.app const [isShowTryAppPanel, setIsShowTryAppPanel] = useState(false) + const hideTryAppPanel = useCallback(() => { + setIsShowTryAppPanel(false) + }, []) const setShowTryAppPanel = (showTryAppPanel: boolean, params?: CurrentTryAppParams) => { if (showTryAppPanel) setCurrentTryAppParams(params) @@ -22,15 +33,107 @@ const Apps = () => { setCurrentTryAppParams(undefined) setIsShowTryAppPanel(showTryAppPanel) } + const [isShowCreateModal, setIsShowCreateModal] = useState(false) + + const handleShowFromTryApp = useCallback(() => { + setIsShowCreateModal(true) + }, []) + + const [controlRefreshList, setControlRefreshList] = useState(0) + const [controlHideCreateFromTemplatePanel, setControlHideCreateFromTemplatePanel] = useState(0) + const onSuccess = useCallback(() => { + setControlRefreshList(prev => prev + 1) + setControlHideCreateFromTemplatePanel(prev => prev + 1) + }, []) + + const [showDSLConfirmModal, setShowDSLConfirmModal] = useState(false) + + const { + handleImportDSL, + handleImportDSLConfirm, + versions, + isFetching, + } = useImportDSL() + + const onConfirmDSL = useCallback(async () => { + await handleImportDSLConfirm({ + onSuccess, + }) + }, [handleImportDSLConfirm, onSuccess]) + + const onCreate: CreateAppModalProps['onConfirm'] = async ({ + name, + icon_type, + icon, + icon_background, + description, + }) => { + hideTryAppPanel() + + const { export_data } = await fetchAppDetail( + currApp?.app.id as string, + ) + const payload = { + mode: DSLImportMode.YAML_CONTENT, + yaml_content: export_data, + name, + icon_type, + icon, + icon_background, + description, + } + await handleImportDSL(payload, { + onSuccess: () => { + setIsShowCreateModal(false) + }, + onPending: () => { + setShowDSLConfirmModal(true) + }, + }) + } return (
- + + {isShowTryAppPanel && ( + + )} + + { + showDSLConfirmModal && ( + setShowDSLConfirmModal(false)} + onConfirm={onConfirmDSL} + confirmDisabled={isFetching} + /> + ) + } + + {isShowCreateModal && ( + setIsShowCreateModal(false)} + /> + )}
) diff --git a/web/app/components/apps/list.tsx b/web/app/components/apps/list.tsx index 4ee9a6d6d5..49cba78c5b 100644 --- a/web/app/components/apps/list.tsx +++ b/web/app/components/apps/list.tsx @@ -1,5 +1,6 @@ 'use client' +import type { FC } from 'react' import { useCallback, useEffect, useRef, useState } from 'react' import { useRouter, @@ -66,7 +67,12 @@ const getKey = ( return null } -const List = () => { +type Props = { + controlRefreshList: number +} +const List: FC = ({ + controlRefreshList, +}) => { const { t } = useTranslation() const { systemFeatures } = useGlobalPublicStore() const router = useRouter() @@ -112,6 +118,11 @@ const List = () => { }, ) + useEffect(() => { + if (controlRefreshList > 0) + mutate() + }, [controlRefreshList]) + const anchorRef = useRef(null) const options = [ { value: 'all', text: t('app.types.all'), icon: }, diff --git a/web/app/components/apps/new-app-card.tsx b/web/app/components/apps/new-app-card.tsx index 7a10bc8527..8d051e9ecf 100644 --- a/web/app/components/apps/new-app-card.tsx +++ b/web/app/components/apps/new-app-card.tsx @@ -1,6 +1,6 @@ 'use client' -import React, { useMemo, useState } from 'react' +import React, { useEffect, useMemo, useState } from 'react' import { useRouter, useSearchParams, @@ -11,6 +11,8 @@ import { useProviderContext } from '@/context/provider-context' import { FileArrow01, FilePlus01, FilePlus02 } from '@/app/components/base/icons/src/vender/line/files' import cn from '@/utils/classnames' import dynamic from 'next/dynamic' +import AppListContext from '@/context/app-list-context' +import { useContextSelector } from 'use-context-selector' const CreateAppModal = dynamic(() => import('@/app/components/app/create-app-modal'), { ssr: false, @@ -52,6 +54,12 @@ const CreateAppCard = ({ return undefined }, [dslUrl]) + const controlHideCreateFromTemplatePanel = useContextSelector(AppListContext, ctx => ctx.controlHideCreateFromTemplatePanel) + useEffect(() => { + if (controlHideCreateFromTemplatePanel > 0) + setShowNewAppTemplateDialog(false) + }, [controlHideCreateFromTemplatePanel]) + return ( ) - }, [appData?.site.icon, appData?.site.icon_background, appData?.site.icon_type, appData?.site.icon_url, chatList, collapsed, currentConversationId, inputsForms.length, respondingState, allInputsHidden]) + }, [appData?.site, chatList, collapsed, currentConversationId, inputsForms.length, respondingState, allInputsHidden]) const answerIcon = isDify() ? diff --git a/web/app/components/base/chat/embedded-chatbot/hooks.tsx b/web/app/components/base/chat/embedded-chatbot/hooks.tsx index 698f212693..2f7cf23191 100644 --- a/web/app/components/base/chat/embedded-chatbot/hooks.tsx +++ b/web/app/components/base/chat/embedded-chatbot/hooks.tsx @@ -72,8 +72,12 @@ export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: stri const isInstalledApp = false // just can be webapp and try app const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) const isTryApp = appSourceType === AppSourceType.tryApp - const { data: appInfo, isLoading: appInfoLoading, error: appInfoError } = useSWR('appInfo', isTryApp ? () => fetchTryAppInfo(tryAppId) : fetchAppInfo) - const appId = useMemo(() => isTryApp ? tryAppId : appInfo?.app_id, [appInfo]) + const { data: appInfo, isLoading: appInfoLoading, error: appInfoError } = useSWR('appInfo', () => { + return isTryApp ? () => fetchTryAppInfo(tryAppId!) : fetchAppInfo + }) + const appId = useMemo(() => { + return isTryApp ? tryAppId : (appInfo as any)?.app_id + }, [appInfo]) const [userId, setUserId] = useState() const [conversationId, setConversationId] = useState() @@ -116,9 +120,16 @@ export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: stri const [conversationIdInfo, setConversationIdInfo] = useLocalStorageState>>(CONVERSATION_ID_INFO, { defaultValue: {}, }) + const removeConversationIdInfo = useCallback((appId: string) => { + setConversationIdInfo((prev) => { + const newInfo = { ...prev } + delete newInfo[appId] + return newInfo + }) + }, [setConversationIdInfo]) const allowResetChat = !conversationId - const currentConversationId = useMemo(() => isTryApp ? '' : conversationIdInfo?.[appId || '']?.[userId || 'DEFAULT'] || conversationId || '', - [isTryApp, appId, conversationIdInfo, userId, conversationId]) + const currentConversationId = useMemo(() => conversationIdInfo?.[appId || '']?.[userId || 'DEFAULT'] || conversationId || '', + [appId, conversationIdInfo, userId, conversationId]) const handleConversationIdInfoChange = useCallback((changeConversationId: string) => { if (appId) { let prevValue = conversationIdInfo?.[appId || ''] @@ -146,7 +157,7 @@ export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: stri const { data: appMeta } = useSWR(isTryApp ? null : ['appMeta', appSourceType, appId], () => fetchAppMeta(appSourceType, appId)) const { data: appPinnedConversationData } = useSWR(isTryApp ? null : ['appConversationData', appSourceType, appId, true], () => fetchConversations(appSourceType, appId, undefined, true, 100)) const { data: appConversationData, isLoading: appConversationDataLoading, mutate: mutateAppConversationData } = useSWR(isTryApp ? null : ['appConversationData', appSourceType, appId, false], () => fetchConversations(appSourceType, appId, undefined, false, 100)) - const { data: appChatListData, isLoading: appChatListDataLoading } = useSWR(chatShouldReloadKey ? ['appChatList', chatShouldReloadKey, appSourceType, appId] : null, () => fetchChatList(chatShouldReloadKey, appSourceType, appId)) + const { data: appChatListData, isLoading: appChatListDataLoading } = useSWR((chatShouldReloadKey && !isTryApp) ? ['appChatList', chatShouldReloadKey, appSourceType, appId] : null, () => fetchChatList(chatShouldReloadKey, appSourceType, appId)) const [clearChatList, setClearChatList] = useState(false) const [isResponding, setIsResponding] = useState(false) @@ -406,6 +417,7 @@ export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: stri appId, currentConversationId, currentConversationItem, + removeConversationIdInfo, handleConversationIdInfoChange, appData: appInfo, appParams: appParams || {} as ChatConfig, diff --git a/web/app/components/explore/try-app/app/chat.tsx b/web/app/components/explore/try-app/app/chat.tsx index 54f167391d..501b275535 100644 --- a/web/app/components/explore/try-app/app/chat.tsx +++ b/web/app/components/explore/try-app/app/chat.tsx @@ -1,6 +1,6 @@ 'use client' import type { FC } from 'react' -import React from 'react' +import React, { useEffect } from 'react' import ChatWrapper from '@/app/components/base/chat/embedded-chatbot/chat-wrapper' import { useThemeContext } from '../../../base/chat/embedded-chatbot/theme/theme-context' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' @@ -33,7 +33,12 @@ const TryApp: FC = ({ const media = useBreakpoints() const isMobile = media === MediaType.mobile const themeBuilder = useThemeContext() - const chatData = useEmbeddedChatbot(AppSourceType.tryApp, appId) + const { removeConversationIdInfo, ...chatData } = useEmbeddedChatbot(AppSourceType.tryApp, appId) + + useEffect(() => { + if (appId) + removeConversationIdInfo(appId) + }, [appId]) const [isHideTryNotice, { setTrue: hideTryNotice, }] = useBoolean(false) From b1ebeb67a767c0e9b6df75f175691fa8966efea7 Mon Sep 17 00:00:00 2001 From: Joel Date: Mon, 27 Oct 2025 13:50:36 +0800 Subject: [PATCH 4/6] feat: support new chat --- .../base/chat/embedded-chatbot/hooks.tsx | 7 ++++- .../components/explore/try-app/app/chat.tsx | 26 ++++++++++++++++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/web/app/components/base/chat/embedded-chatbot/hooks.tsx b/web/app/components/base/chat/embedded-chatbot/hooks.tsx index 2f7cf23191..9fde01e037 100644 --- a/web/app/components/base/chat/embedded-chatbot/hooks.tsx +++ b/web/app/components/base/chat/embedded-chatbot/hooks.tsx @@ -388,12 +388,17 @@ export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: stri setClearChatList(false) }, [handleConversationIdInfoChange, setClearChatList]) const handleNewConversation = useCallback(async () => { + if (isTryApp) { + setClearChatList(true) + return + } + currentChatInstanceRef.current.handleStop() setShowNewConversationItemInList(true) handleChangeConversation('') handleNewConversationInputsChange(await getProcessedInputsFromUrlParams()) setClearChatList(true) - }, [handleChangeConversation, setShowNewConversationItemInList, handleNewConversationInputsChange, setClearChatList]) + }, [isTryApp, setShowNewConversationItemInList, handleNewConversationInputsChange, setClearChatList]) const handleNewConversationCompleted = useCallback((newConversationId: string) => { setNewConversationId(newConversationId) diff --git a/web/app/components/explore/try-app/app/chat.tsx b/web/app/components/explore/try-app/app/chat.tsx index 501b275535..745cc0bc62 100644 --- a/web/app/components/explore/try-app/app/chat.tsx +++ b/web/app/components/explore/try-app/app/chat.tsx @@ -17,6 +17,10 @@ import { useTranslation } from 'react-i18next' import { useBoolean } from 'ahooks' import type { TryAppInfo } from '@/service/try-app' import AppIcon from '@/app/components/base/app-icon' +import Tooltip from '@/app/components/base/tooltip' +import ActionButton from '@/app/components/base/action-button' +import { RiResetLeftLine } from '@remixicon/react' +import ViewFormDropdown from '@/app/components/base/chat/embedded-chatbot/inputs-form/view-form-dropdown' type Props = { appId: string @@ -34,7 +38,8 @@ const TryApp: FC = ({ const isMobile = media === MediaType.mobile const themeBuilder = useThemeContext() const { removeConversationIdInfo, ...chatData } = useEmbeddedChatbot(AppSourceType.tryApp, appId) - + const currentConversationId = chatData.currentConversationId + const inputsForms = chatData.inputsForms useEffect(() => { if (appId) removeConversationIdInfo(appId) @@ -42,6 +47,11 @@ const TryApp: FC = ({ const [isHideTryNotice, { setTrue: hideTryNotice, }] = useBoolean(false) + + const handleNewConversation = () => { + removeConversationIdInfo(appId) + chatData.handleNewConversation() + } return ( = ({ />
{appDetail.name}
+
+ {currentConversationId && ( + + + + + + )} + {currentConversationId && inputsForms.length > 0 && ( + + )} +
{!isHideTryNotice && ( From a0e1eeb3f1ec4b34197cd85bcc64d4aa9617fdd0 Mon Sep 17 00:00:00 2001 From: Joel Date: Mon, 27 Oct 2025 13:57:16 +0800 Subject: [PATCH 5/6] chore: reset form --- .../chat/embedded-chatbot/inputs-form/view-form-dropdown.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/app/components/base/chat/embedded-chatbot/inputs-form/view-form-dropdown.tsx b/web/app/components/base/chat/embedded-chatbot/inputs-form/view-form-dropdown.tsx index d6c89864d9..c8b6c406fc 100644 --- a/web/app/components/base/chat/embedded-chatbot/inputs-form/view-form-dropdown.tsx +++ b/web/app/components/base/chat/embedded-chatbot/inputs-form/view-form-dropdown.tsx @@ -33,7 +33,7 @@ const ViewFormDropdown = ({ iconColor }: Props) => { - +
From ab814e3eacd4985419dffc956c1f90b61f075d8b Mon Sep 17 00:00:00 2001 From: Joel Date: Mon, 27 Oct 2025 14:08:32 +0800 Subject: [PATCH 6/6] fix: inputs overwrite by curr item --- web/app/components/base/chat/embedded-chatbot/hooks.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/app/components/base/chat/embedded-chatbot/hooks.tsx b/web/app/components/base/chat/embedded-chatbot/hooks.tsx index 9fde01e037..500860e740 100644 --- a/web/app/components/base/chat/embedded-chatbot/hooks.tsx +++ b/web/app/components/base/chat/embedded-chatbot/hooks.tsx @@ -328,7 +328,7 @@ export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: stri }, [appChatListData, currentConversationId]) const [currentConversationInputs, setCurrentConversationInputs] = useState>(currentConversationLatestInputs || {}) useEffect(() => { - if (currentConversationItem) + if (currentConversationItem && !isTryApp) setCurrentConversationInputs(currentConversationLatestInputs || {}) }, [currentConversationItem, currentConversationLatestInputs])