diff --git a/web/app/components/app/configuration/config-var/index.tsx b/web/app/components/app/configuration/config-var/index.tsx index 0e453d5171..cad7262650 100644 --- a/web/app/components/app/configuration/config-var/index.tsx +++ b/web/app/components/app/configuration/config-var/index.tsx @@ -258,9 +258,9 @@ const ConfigVar: FC = ({ promptVariables, readonly, onPromptVar )} {hasVar && ( -
+
{ onPromptVariablesChange?.(list.map(item => item.variable)) }} handle='.handle' diff --git a/web/app/components/app/configuration/config-var/var-item.tsx b/web/app/components/app/configuration/config-var/var-item.tsx index 88cd5d7843..90cfd13788 100644 --- a/web/app/components/app/configuration/config-var/var-item.tsx +++ b/web/app/components/app/configuration/config-var/var-item.tsx @@ -38,7 +38,7 @@ const VarItem: FC = ({ const [isDeleting, setIsDeleting] = useState(false) return ( -
+
{canDrag && ( diff --git a/web/app/components/app/configuration/config-vision/index.tsx b/web/app/components/app/configuration/config-vision/index.tsx index bbe322ee7e..2057976a83 100644 --- a/web/app/components/app/configuration/config-vision/index.tsx +++ b/web/app/components/app/configuration/config-vision/index.tsx @@ -13,10 +13,14 @@ import ConfigContext from '@/context/debug-configuration' import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks' import Switch from '@/app/components/base/switch' import { SupportUploadFileTypes } from '@/app/components/workflow/types' +import OptionCard from '@/app/components/workflow/nodes/_base/components/option-card' +import { Resolution } from '@/types/app' +import { noop } from 'lodash-es' +import cn from '@/utils/classnames' const ConfigVision: FC = () => { const { t } = useTranslation() - const { isShowVisionConfig, isAllowVideoUpload } = useContext(ConfigContext) + const { isShowVisionConfig, isAllowVideoUpload, readonly } = useContext(ConfigContext) const file = useFeatures(s => s.features.file) const featuresStore = useFeaturesStore() @@ -53,7 +57,7 @@ const ConfigVision: FC = () => { setFeatures(newFeatures) }, [featuresStore, isAllowVideoUpload]) - if (!isShowVisionConfig) + if (!isShowVisionConfig || (readonly && !isImageEnabled)) return null return ( @@ -74,37 +78,49 @@ const ConfigVision: FC = () => { />
- {/*
-
{t('appDebug.vision.visionSettings.resolution')}
- - {t('appDebug.vision.visionSettings.resolutionTooltip').split('\n').map(item => ( -
{item}
- ))} -
- } + {readonly ? (<> +
+
{t('appDebug.vision.visionSettings.resolution')}
+ + {t('appDebug.vision.visionSettings.resolutionTooltip').split('\n').map(item => ( +
{item}
+ ))} +
+ } + /> +
+
+ + +
+ ) : <> + +
+ -
*/} - {/*
- handleChange(Resolution.high)} - /> - handleChange(Resolution.low)} - /> -
*/} - -
- + } +
) diff --git a/web/app/components/app/configuration/config/agent/agent-tools/index.tsx b/web/app/components/app/configuration/config/agent/agent-tools/index.tsx index f2b9c105fc..3ad5b72f8f 100644 --- a/web/app/components/app/configuration/config/agent/agent-tools/index.tsx +++ b/web/app/components/app/configuration/config/agent/agent-tools/index.tsx @@ -37,7 +37,7 @@ type AgentToolWithMoreInfo = AgentTool & { icon: any; collection?: Collection } const AgentTools: FC = () => { const { t } = useTranslation() const [isShowChooseTool, setIsShowChooseTool] = useState(false) - const { modelConfig, setModelConfig } = useContext(ConfigContext) + const { readonly, modelConfig, setModelConfig } = useContext(ConfigContext) const { data: buildInTools } = useAllBuiltInTools() const { data: customTools } = useAllCustomTools() const { data: workflowTools } = useAllWorkflowTools() @@ -158,7 +158,7 @@ const AgentTools: FC = () => { headerRight={
{tools.filter(item => !!item.enabled).length}/{tools.length} {t('appDebug.agent.tools.enabled')}
- {tools.length < MAX_TOOLS_NUM && ( + {tools.length < MAX_TOOLS_NUM && !readonly && ( <>
{
} > -
+
{tools.map((item: AgentTool & { icon: any; collection?: Collection }, index) => (
{ > {getProviderShowName(item)} {item.tool_label} - {!item.isDeleted && ( + {!item.isDeleted && !readonly && ( @@ -212,7 +212,7 @@ const AgentTools: FC = () => { } >
-
+
@@ -246,8 +246,8 @@ const AgentTools: FC = () => {
)} - {!item.isDeleted && ( -
+ {!item.isDeleted && !readonly && ( + -
-
- -
+ {!readonly && ( +
+
+ +
+ )}
) } diff --git a/web/app/components/app/configuration/config/config-document.tsx b/web/app/components/app/configuration/config/config-document.tsx index c0e8cc3a2d..5ff76cac6a 100644 --- a/web/app/components/app/configuration/config/config-document.tsx +++ b/web/app/components/app/configuration/config/config-document.tsx @@ -16,7 +16,7 @@ const ConfigDocument: FC = () => { const { t } = useTranslation() const file = useFeatures(s => s.features.file) const featuresStore = useFeaturesStore() - const { isShowDocumentConfig } = useContext(ConfigContext) + const { isShowDocumentConfig, readonly } = useContext(ConfigContext) const isDocumentEnabled = file?.allowed_file_types?.includes(SupportUploadFileTypes.document) ?? false @@ -44,7 +44,7 @@ const ConfigDocument: FC = () => { setFeatures(newFeatures) }, [featuresStore]) - if (!isShowDocumentConfig) + if (!isShowDocumentConfig || (readonly && !isDocumentEnabled)) return null return ( @@ -64,14 +64,16 @@ const ConfigDocument: FC = () => { } />
-
-
- -
+ {!readonly && ( +
+
+ +
+ )}
) } diff --git a/web/app/components/app/configuration/config/index.tsx b/web/app/components/app/configuration/config/index.tsx index 7e130a4e95..38b72f09cc 100644 --- a/web/app/components/app/configuration/config/index.tsx +++ b/web/app/components/app/configuration/config/index.tsx @@ -19,6 +19,7 @@ import { ModelModeType } from '@/types/app' const Config: FC = () => { const { + readonly, mode, isAdvancedMode, modelModeType, @@ -28,6 +29,7 @@ const Config: FC = () => { modelConfig, setModelConfig, setPrevPromptConfig, + dataSets, } = useContext(ConfigContext) const isChatApp = ['advanced-chat', 'agent-chat', 'chat'].includes(mode) const formattingChangedDispatcher = useFormattingChangedDispatcher() @@ -66,19 +68,28 @@ const Config: FC = () => { promptTemplate={promptTemplate} promptVariables={promptVariables} onChange={handlePromptChange} + readonly={readonly} /> {/* Variables */} - + {!(readonly && promptVariables.length === 0) && ( + + )} {/* Dataset */} - - + {!(readonly && dataSets.length === 0) && ( + + ) + } {/* Tools */} - {isAgent && ( + {isAgent && !(readonly && modelConfig.agentConfig.tools.length === 0) && ( )} @@ -89,7 +100,7 @@ const Config: FC = () => { {/* Chat History */} - {isAdvancedMode && isChatApp && modelModeType === ModelModeType.completion && ( + {!readonly && isAdvancedMode && isChatApp && modelModeType === ModelModeType.completion && ( = ({ config, onSave, onRemove, + readonly = false, editable = true, }) => { const media = useBreakpoints() @@ -55,6 +56,7 @@ const Item: FC = ({
= ({
{ - editable && { e.stopPropagation() setShowSettingsModal(true) @@ -77,14 +79,18 @@ const Item: FC = ({ } - onRemove(config.id)} - state={isDeleting ? ActionButtonState.Destructive : ActionButtonState.Default} - onMouseEnter={() => setIsDeleting(true)} - onMouseLeave={() => setIsDeleting(false)} - > - - + { + !readonly && ( + onRemove(config.id)} + state={isDeleting ? ActionButtonState.Destructive : ActionButtonState.Default} + onMouseEnter={() => setIsDeleting(true)} + onMouseLeave={() => setIsDeleting(false)} + > + + + ) + }
{ config.indexing_technique && = ({ text={t('dataset.externalTag') as string} /> } - setShowSettingsModal(false)} footer={null} mask={isMobile} panelClassName='mt-16 mx-2 sm:mr-2 mb-3 !p-0 !max-w-[640px] rounded-xl'> - setShowSettingsModal(false)} - onSave={handleSave} - /> - + {showSettingsModal && ( + setShowSettingsModal(false)} footer={null} mask={isMobile} panelClassName='mt-16 mx-2 sm:mr-2 mb-3 !p-0 !max-w-[640px] rounded-xl'> + setShowSettingsModal(false)} + onSave={handleSave} + /> + + )}
) } diff --git a/web/app/components/app/configuration/dataset-config/index.tsx b/web/app/components/app/configuration/dataset-config/index.tsx index 0c1b9349ae..78bd0b7f13 100644 --- a/web/app/components/app/configuration/dataset-config/index.tsx +++ b/web/app/components/app/configuration/dataset-config/index.tsx @@ -36,8 +36,13 @@ import { LogicalOperator, MetadataFilteringVariableType, } from '@/app/components/workflow/nodes/knowledge-retrieval/types' +import cn from '@/utils/classnames' -const DatasetConfig: FC = () => { +type Props = { + readonly?: boolean + hideMetadataFilter?: boolean +} +const DatasetConfig: FC = ({ readonly, hideMetadataFilter }) => { const { t } = useTranslation() const userProfile = useAppContextSelector(s => s.userProfile) const { @@ -254,24 +259,25 @@ const DatasetConfig: FC = () => { className='mt-2' title={t('appDebug.feature.dataSet.title')} headerRight={ -
+ !readonly && (
{!isAgent && } -
+
) } hasHeaderBottomBorder={!hasData} noBodySpacing > {hasData ? ( -
+
{formattedDataset.map(item => ( ))}
@@ -282,27 +288,29 @@ const DatasetConfig: FC = () => {
)} -
- item.type === MetadataFilteringVariableType.string || item.type === MetadataFilteringVariableType.select)} - availableCommonNumberVars={promptVariablesToSelect.filter(item => item.type === MetadataFilteringVariableType.number)} - /> -
+ {!hideMetadataFilter && ( +
+ item.type === MetadataFilteringVariableType.string || item.type === MetadataFilteringVariableType.select)} + availableCommonNumberVars={promptVariablesToSelect.filter(item => item.type === MetadataFilteringVariableType.number)} + /> +
+ )} - {mode === AppType.completion && dataSet.length > 0 && ( + {!readonly && mode === AppType.completion && dataSet.length > 0 && ( { const { t } = useTranslation() - const { modelConfig, setInputs } = useContext(ConfigContext) + const { modelConfig, setInputs, readonly } = useContext(ConfigContext) const promptVariables = modelConfig.configs.prompt_variables.filter(({ key, name }) => { return key && key?.trim() && name && name?.trim() @@ -70,6 +70,7 @@ const ChatUserInput = ({ placeholder={name} autoFocus={index === 0} maxLength={max_length || DEFAULT_VALUE_MAX_LEN} + readOnly={readonly} /> )} {type === 'paragraph' && ( @@ -78,6 +79,7 @@ const ChatUserInput = ({ placeholder={name} value={inputs[key] ? `${inputs[key]}` : ''} onChange={(e) => { handleInputValueChange(key, e.target.value) }} + readOnly={readonly} /> )} {type === 'select' && ( @@ -87,6 +89,7 @@ const ChatUserInput = ({ onSelect={(i) => { handleInputValueChange(key, i.value as string) }} items={(options || []).map(i => ({ name: i, value: i }))} allowSearch={false} + disabled={readonly} /> )} {type === 'number' && ( @@ -97,6 +100,7 @@ const ChatUserInput = ({ placeholder={name} autoFocus={index === 0} maxLength={max_length || DEFAULT_VALUE_MAX_LEN} + readOnly={readonly} /> )} {type === 'checkbox' && ( @@ -105,6 +109,7 @@ const ChatUserInput = ({ value={!!inputs[key]} required={required} onChange={(value) => { handleInputValueChange(key, value) }} + readonly={readonly} /> )} diff --git a/web/app/components/app/configuration/debug/debug-with-single-model/index.tsx b/web/app/components/app/configuration/debug/debug-with-single-model/index.tsx index d439b00939..503aefc4fb 100644 --- a/web/app/components/app/configuration/debug/debug-with-single-model/index.tsx +++ b/web/app/components/app/configuration/debug/debug-with-single-model/index.tsx @@ -39,6 +39,7 @@ const DebugWithSingleModel = ( ) => { const { userProfile } = useAppContext() const { + readonly, modelConfig, appId, inputs, @@ -154,6 +155,7 @@ const DebugWithSingleModel = ( return ( = ({ }) => { const { t } = useTranslation() const { + readonly, appId, mode, modelModeType, @@ -413,19 +414,23 @@ const Debug: FC = ({ } {mode !== AppType.completion && ( <> - - - - - + {!readonly && ( + + + + + + + )} + {varList.length > 0 && (
- setExpanded(!expanded)}> + !readonly && setExpanded(!expanded)}> @@ -553,7 +558,7 @@ const Debug: FC = ({ onCancel={handleCancel} /> )} - {!isAPIKeySet && ()} + {!isAPIKeySet && !readonly && ()} ) } diff --git a/web/app/components/app/configuration/prompt-value-panel/index.tsx b/web/app/components/app/configuration/prompt-value-panel/index.tsx index 43c836132f..9b9b5fd66e 100644 --- a/web/app/components/app/configuration/prompt-value-panel/index.tsx +++ b/web/app/components/app/configuration/prompt-value-panel/index.tsx @@ -40,7 +40,7 @@ const PromptValuePanel: FC = ({ onVisionFilesChange, }) => { const { t } = useTranslation() - const { modelModeType, modelConfig, setInputs, mode, isAdvancedMode, completionPromptConfig, chatPromptConfig } = useContext(ConfigContext) + const { readonly, modelModeType, modelConfig, setInputs, mode, isAdvancedMode, completionPromptConfig, chatPromptConfig } = useContext(ConfigContext) const [userInputFieldCollapse, setUserInputFieldCollapse] = useState(false) const promptVariables = modelConfig.configs.prompt_variables.filter(({ key, name }) => { return key && key?.trim() && name && name?.trim() @@ -60,12 +60,12 @@ const PromptValuePanel: FC = ({ if (isAdvancedMode) { if (modelModeType === ModelModeType.chat) - return chatPromptConfig.prompt.every(({ text }) => !text) + return chatPromptConfig?.prompt.every(({ text }) => !text) return !completionPromptConfig.prompt?.text } else { return !modelConfig.configs.prompt_template } - }, [chatPromptConfig.prompt, completionPromptConfig.prompt?.text, isAdvancedMode, mode, modelConfig.configs.prompt_template, modelModeType]) + }, [chatPromptConfig?.prompt, completionPromptConfig.prompt?.text, isAdvancedMode, mode, modelConfig.configs.prompt_template, modelModeType]) const handleInputValueChange = (key: string, value: string | boolean) => { if (!(key in promptVariableObj)) @@ -124,6 +124,7 @@ const PromptValuePanel: FC = ({ placeholder={name} autoFocus={index === 0} maxLength={max_length || DEFAULT_VALUE_MAX_LEN} + readOnly={readonly} /> )} {type === 'paragraph' && ( @@ -132,6 +133,7 @@ const PromptValuePanel: FC = ({ placeholder={name} value={inputs[key] ? `${inputs[key]}` : ''} onChange={(e) => { handleInputValueChange(key, e.target.value) }} + readOnly={readonly} /> )} {type === 'select' && ( @@ -142,6 +144,7 @@ const PromptValuePanel: FC = ({ items={(options || []).map(i => ({ name: i, value: i }))} allowSearch={false} bgClassName='bg-gray-50' + disabled={readonly} /> )} {type === 'number' && ( @@ -152,6 +155,7 @@ const PromptValuePanel: FC = ({ placeholder={name} autoFocus={index === 0} maxLength={max_length || DEFAULT_VALUE_MAX_LEN} + readOnly={readonly} /> )} {type === 'checkbox' && ( @@ -160,6 +164,7 @@ const PromptValuePanel: FC = ({ value={!!inputs[key]} required={required} onChange={(value) => { handleInputValueChange(key, value) }} + readonly={readonly} /> )}
@@ -178,6 +183,7 @@ const PromptValuePanel: FC = ({ url: fileItem.url, upload_file_id: fileItem.fileId, })))} + disabled={readonly} /> @@ -186,12 +192,12 @@ const PromptValuePanel: FC = ({ )} {!userInputFieldCollapse && (
- + {canNotRun && (
) diff --git a/web/app/components/app/text-generate/item/index.tsx b/web/app/components/app/text-generate/item/index.tsx index 92d86351e0..0a715c4b68 100644 --- a/web/app/components/app/text-generate/item/index.tsx +++ b/web/app/components/app/text-generate/item/index.tsx @@ -21,7 +21,7 @@ import { Markdown } from '@/app/components/base/markdown' import Loading from '@/app/components/base/loading' import Toast from '@/app/components/base/toast' import type { FeedbackType } from '@/app/components/base/chat/chat/type' -import { fetchMoreLikeThis, updateFeedback } from '@/service/share' +import { AppSourceType, fetchMoreLikeThis, updateFeedback } from '@/service/share' import { fetchTextGenerationMessage } from '@/service/debug' import { useStore as useAppStore } from '@/app/components/app/store' import WorkflowProcessItem from '@/app/components/base/chat/chat/answer/workflow-process' @@ -52,7 +52,7 @@ export type IGenerationItemProps = { onFeedback?: (feedback: FeedbackType) => void onSave?: (messageId: string) => void isMobile?: boolean - isInstalledApp: boolean + appSourceType: AppSourceType installedAppId?: string taskId?: string controlClearMoreLikeThis?: number @@ -86,7 +86,7 @@ const GenerationItem: FC = ({ onSave, depth = 1, isMobile, - isInstalledApp, + appSourceType, installedAppId, taskId, controlClearMoreLikeThis, @@ -99,6 +99,7 @@ const GenerationItem: FC = ({ const { t } = useTranslation() const params = useParams() const isTop = depth === 1 + const isTryApp = appSourceType === AppSourceType.tryApp const [completionRes, setCompletionRes] = useState('') const [childMessageId, setChildMessageId] = useState(null) const [childFeedback, setChildFeedback] = useState({ @@ -112,7 +113,7 @@ const GenerationItem: FC = ({ const setShowPromptLogModal = useAppStore(s => s.setShowPromptLogModal) const handleFeedback = async (childFeedback: FeedbackType) => { - await updateFeedback({ url: `/messages/${childMessageId}/feedbacks`, body: { rating: childFeedback.rating } }, isInstalledApp, installedAppId) + await updateFeedback({ url: `/messages/${childMessageId}/feedbacks`, body: { rating: childFeedback.rating } }, appSourceType, installedAppId) setChildFeedback(childFeedback) } @@ -130,7 +131,7 @@ const GenerationItem: FC = ({ onSave, isShowTextToSpeech, isMobile, - isInstalledApp, + appSourceType, installedAppId, controlClearMoreLikeThis, isWorkflow, @@ -144,7 +145,7 @@ const GenerationItem: FC = ({ return } startQuerying() - const res: any = await fetchMoreLikeThis(messageId as string, isInstalledApp, installedAppId) + const res: any = await fetchMoreLikeThis(messageId as string, appSourceType, installedAppId) setCompletionRes(res.answer) setChildFeedback({ rating: null, @@ -292,7 +293,7 @@ const GenerationItem: FC = ({ {!isWorkflow && {content?.length} {t('common.unit.char')}} {/* action buttons */}
- {!isInWebApp && !isInstalledApp && !isResponding && ( + {!isInWebApp && (appSourceType !== AppSourceType.installedApp) && !isResponding && (
@@ -329,13 +330,13 @@ const GenerationItem: FC = ({ )} - {isInWebApp && !isWorkflow && ( + {isInWebApp && !isWorkflow && !isTryApp && ( { onSave?.(messageId as string) }}> )}
- {(supportFeedback || isInWebApp) && !isWorkflow && !isError && messageId && ( + {(supportFeedback || isInWebApp) && !isWorkflow && !isTryApp && !isError && messageId && (
{!feedback?.rating && ( <> diff --git a/web/app/components/base/action-button/index.tsx b/web/app/components/base/action-button/index.tsx index f70bfb4448..2aefef94f0 100644 --- a/web/app/components/base/action-button/index.tsx +++ b/web/app/components/base/action-button/index.tsx @@ -50,13 +50,14 @@ function getActionButtonState(state: ActionButtonState) { } } -const ActionButton = ({ className, size, state = ActionButtonState.Default, styleCss, children, ref, ...props }: ActionButtonProps) => { +const ActionButton = ({ className, size, state = ActionButtonState.Default, styleCss, children, ref, disabled, ...props }: ActionButtonProps) => { return ( + ) + }, +) +CarouselPrevious.displayName = 'CarouselPrevious' + +const CarouselNext = React.forwardRef( + ({ children, ...props }, ref) => { + const { scrollNext, canScrollNext } = useCarousel() + + return ( + + ) + }, +) +CarouselNext.displayName = 'CarouselNext' + +const CarouselDot = React.forwardRef( + ({ children, ...props }, ref) => { + const { api, selectedIndex } = useCarousel() + + return api?.slideNodes().map((_, index) => { + return ( + + ) + }) + }, +) +CarouselDot.displayName = 'CarouselDot' + +const CarouselPlugins = { + Autoplay, +} + +Carousel.Content = CarouselContent +Carousel.Item = CarouselItem +Carousel.Previous = CarouselPrevious +Carousel.Next = CarouselNext +Carousel.Dot = CarouselDot +Carousel.Plugin = CarouselPlugins + +export { Carousel, useCarousel } diff --git a/web/app/components/base/chat/chat-with-history/chat-wrapper.tsx b/web/app/components/base/chat/chat-with-history/chat-wrapper.tsx index 29b27a60ad..e8ed5dfc95 100644 --- a/web/app/components/base/chat/chat-with-history/chat-wrapper.tsx +++ b/web/app/components/base/chat/chat-with-history/chat-wrapper.tsx @@ -13,6 +13,7 @@ import { InputVarType } from '@/app/components/workflow/types' import { TransferMethod } from '@/types/app' import InputsForm from '@/app/components/base/chat/chat-with-history/inputs-form' import { + AppSourceType, fetchSuggestedQuestions, getUrl, stopChatMessageResponding, @@ -53,6 +54,11 @@ const ChatWrapper = () => { initUserVariables, } = useChatWithHistoryContext() + const appSourceType = isInstalledApp ? AppSourceType.installedApp : AppSourceType.webApp + + // Semantic variable for better code readability + const isHistoryConversation = !!currentConversationId + const appConfig = useMemo(() => { const config = appParams || {} @@ -80,7 +86,7 @@ const ChatWrapper = () => { inputsForm: inputsForms, }, appPrevChatTree, - taskId => stopChatMessageResponding('', taskId, isInstalledApp, appId), + taskId => stopChatMessageResponding('', taskId, appSourceType, appId), clearChatList, setClearChatList, ) @@ -139,11 +145,11 @@ const ChatWrapper = () => { } handleSend( - getUrl('chat-messages', isInstalledApp, appId || ''), + getUrl('chat-messages', appSourceType, appId || ''), data, { - onGetSuggestedQuestions: responseItemId => fetchSuggestedQuestions(responseItemId, isInstalledApp, appId), - onConversationComplete: currentConversationId ? undefined : handleNewConversationCompleted, + onGetSuggestedQuestions: responseItemId => fetchSuggestedQuestions(responseItemId, appSourceType, appId), + onConversationComplete: isHistoryConversation ? undefined : handleNewConversationCompleted, isPublicAPI: !isInstalledApp, }, ) @@ -179,13 +185,13 @@ const ChatWrapper = () => { else { return } - }, - [ + }, [ inputsForms.length, isMobile, currentConversationId, collapsed, allInputsHidden, - ]) + ], + ) const welcome = useMemo(() => { const welcomeMessage = chatList.find(item => item.isOpeningStatement) @@ -232,8 +238,7 @@ const ChatWrapper = () => {
) - }, - [ + }, [ appData?.site.icon, appData?.site.icon_background, appData?.site.icon_type, diff --git a/web/app/components/base/chat/chat-with-history/hooks.tsx b/web/app/components/base/chat/chat-with-history/hooks.tsx index 9b0f67e6b2..36f04dfdf6 100644 --- a/web/app/components/base/chat/chat-with-history/hooks.tsx +++ b/web/app/components/base/chat/chat-with-history/hooks.tsx @@ -20,6 +20,7 @@ import { buildChatItemTree, getProcessedSystemVariablesFromUrlParams, getRawInpu import { addFileInfos, sortAgentSorts } from '../../../tools/utils' import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils' import { + AppSourceType, delConversation, fetchChatList, fetchConversations, @@ -70,6 +71,7 @@ function getFormattedChatList(messages: any[]) { export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { const isInstalledApp = useMemo(() => !!installedAppInfo, [installedAppInfo]) + const appSourceType = isInstalledApp ? AppSourceType.installedApp : AppSourceType.webApp const appInfo = useWebAppStore(s => s.appInfo) const appParams = useWebAppStore(s => s.appParams) const appMeta = useWebAppStore(s => s.appMeta) @@ -176,17 +178,17 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { const { data: appPinnedConversationData, mutate: mutateAppPinnedConversationData } = useSWR( appId ? ['appConversationData', isInstalledApp, appId, true] : null, - () => fetchConversations(isInstalledApp, appId, undefined, true, 100), + () => fetchConversations(appSourceType, appId, undefined, true, 100), { revalidateOnFocus: false, revalidateOnReconnect: false }, ) const { data: appConversationData, isLoading: appConversationDataLoading, mutate: mutateAppConversationData } = useSWR( appId ? ['appConversationData', isInstalledApp, appId, false] : null, - () => fetchConversations(isInstalledApp, appId, undefined, false, 100), + () => fetchConversations(appSourceType, appId, undefined, false, 100), { revalidateOnFocus: false, revalidateOnReconnect: false }, ) const { data: appChatListData, isLoading: appChatListDataLoading } = useSWR( - chatShouldReloadKey ? ['appChatList', chatShouldReloadKey, isInstalledApp, appId] : null, - () => fetchChatList(chatShouldReloadKey, isInstalledApp, appId), + chatShouldReloadKey ? ['appChatList', chatShouldReloadKey, appSourceType, appId] : null, + () => fetchChatList(chatShouldReloadKey, appSourceType, appId), { revalidateOnFocus: false, revalidateOnReconnect: false }, ) @@ -309,7 +311,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { handleNewConversationInputsChange(conversationInputs) }, [handleNewConversationInputsChange, inputsForms]) - const { data: newConversation } = useSWR(newConversationId ? [isInstalledApp, appId, newConversationId] : null, () => generationConversationName(isInstalledApp, appId, newConversationId), { revalidateOnFocus: false }) + const { data: newConversation } = useSWR(newConversationId ? [isInstalledApp, appId, newConversationId] : null, () => generationConversationName(appSourceType, appId, newConversationId), { revalidateOnFocus: false }) const [originConversationList, setOriginConversationList] = useState([]) useEffect(() => { if (appConversationData?.data && !appConversationDataLoading) @@ -434,16 +436,16 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { }, [mutateAppConversationData, mutateAppPinnedConversationData]) const handlePinConversation = useCallback(async (conversationId: string) => { - await pinConversation(isInstalledApp, appId, conversationId) + await pinConversation(appSourceType, appId, conversationId) notify({ type: 'success', message: t('common.api.success') }) handleUpdateConversationList() - }, [isInstalledApp, appId, notify, t, handleUpdateConversationList]) + }, [appSourceType, appId, notify, t, handleUpdateConversationList]) const handleUnpinConversation = useCallback(async (conversationId: string) => { - await unpinConversation(isInstalledApp, appId, conversationId) + await unpinConversation(appSourceType, appId, conversationId) notify({ type: 'success', message: t('common.api.success') }) handleUpdateConversationList() - }, [isInstalledApp, appId, notify, t, handleUpdateConversationList]) + }, [appSourceType, appId, notify, t, handleUpdateConversationList]) const [conversationDeleting, setConversationDeleting] = useState(false) const handleDeleteConversation = useCallback(async ( @@ -457,7 +459,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { try { setConversationDeleting(true) - await delConversation(isInstalledApp, appId, conversationId) + await delConversation(appSourceType, appId, conversationId) notify({ type: 'success', message: t('common.api.success') }) onSuccess() } @@ -492,7 +494,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { setConversationRenaming(true) try { - await renameConversation(isInstalledApp, appId, conversationId, newName) + await renameConversation(appSourceType, appId, conversationId, newName) notify({ type: 'success', @@ -522,9 +524,9 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { }, [mutateAppConversationData, handleConversationIdInfoChange]) const handleFeedback = useCallback(async (messageId: string, feedback: Feedback) => { - await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating, content: feedback.content } }, isInstalledApp, appId) + await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating, content: feedback.content } }, appSourceType, appId) notify({ type: 'success', message: t('common.api.success') }) - }, [isInstalledApp, appId, t, notify]) + }, [appSourceType, appId, t, notify]) return { isInstalledApp, diff --git a/web/app/components/base/chat/chat/answer/operation.tsx b/web/app/components/base/chat/chat/answer/operation.tsx index 7ffb21c6d8..f86801f86b 100644 --- a/web/app/components/base/chat/chat/answer/operation.tsx +++ b/web/app/components/base/chat/chat/answer/operation.tsx @@ -51,6 +51,7 @@ const Operation: FC = ({ onAnnotationAdded, onAnnotationEdited, onAnnotationRemoved, + disableFeedback, onFeedback, onRegenerate, } = useChatContext() @@ -166,7 +167,7 @@ const Operation: FC = ({ )} )} - {!isOpeningStatement && config?.supportFeedback && !localFeedback?.rating && onFeedback && ( + {!disableFeedback && !isOpeningStatement && config?.supportFeedback && !localFeedback?.rating && onFeedback && (
{!localFeedback?.rating && ( <> @@ -180,7 +181,7 @@ const Operation: FC = ({ )}
)} - {!isOpeningStatement && config?.supportFeedback && localFeedback?.rating && onFeedback && ( + {!disableFeedback && !isOpeningStatement && config?.supportFeedback && localFeedback?.rating && onFeedback && (
{localFeedback?.rating === 'like' && ( handleFeedback(null)}> diff --git a/web/app/components/base/chat/chat/chat-input-area/index.tsx b/web/app/components/base/chat/chat/chat-input-area/index.tsx index a1144d5537..d44aa455ca 100644 --- a/web/app/components/base/chat/chat/chat-input-area/index.tsx +++ b/web/app/components/base/chat/chat/chat-input-area/index.tsx @@ -27,8 +27,10 @@ import { useToastContext } from '@/app/components/base/toast' import FeatureBar from '@/app/components/base/features/new-feature-panel/feature-bar' import type { FileUpload } from '@/app/components/base/features/types' import { TransferMethod } from '@/types/app' +import { noop } from 'lodash-es' type ChatInputAreaProps = { + readonly?: boolean botName?: string showFeatureBar?: boolean showFileUpload?: boolean @@ -44,6 +46,7 @@ type ChatInputAreaProps = { disabled?: boolean } const ChatInputArea = ({ + readonly, botName, showFeatureBar, showFileUpload, @@ -168,6 +171,7 @@ const ChatInputArea = ({ const operation = (
{ @@ -237,7 +242,12 @@ const ChatInputArea = ({ ) } - {showFeatureBar && } + {showFeatureBar && } ) } diff --git a/web/app/components/base/chat/chat/chat-input-area/operation.tsx b/web/app/components/base/chat/chat/chat-input-area/operation.tsx index 014ca6651f..f080157345 100644 --- a/web/app/components/base/chat/chat/chat-input-area/operation.tsx +++ b/web/app/components/base/chat/chat/chat-input-area/operation.tsx @@ -13,8 +13,10 @@ import ActionButton from '@/app/components/base/action-button' import { FileUploaderInChatInput } from '@/app/components/base/file-uploader' import type { FileUpload } from '@/app/components/base/features/types' import cn from '@/utils/classnames' +import { noop } from 'lodash-es' type OperationProps = { + readonly?: boolean fileConfig?: FileUpload speechToTextConfig?: EnableType onShowVoiceInput?: () => void @@ -23,6 +25,7 @@ type OperationProps = { ref?: Ref; } const Operation: FC = ({ + readonly, ref, fileConfig, speechToTextConfig, @@ -41,12 +44,13 @@ const Operation: FC = ({ ref={ref} >
- {fileConfig?.enabled && } + {fileConfig?.enabled && } { speechToTextConfig?.enabled && ( @@ -56,7 +60,8 @@ const Operation: FC = ({ + { + !hideEditEntrance && ( + + ) + }
)} diff --git a/web/app/components/base/file-uploader/file-uploader-in-chat-input/index.tsx b/web/app/components/base/file-uploader/file-uploader-in-chat-input/index.tsx index 7e6e190ddb..91d6f22ef2 100644 --- a/web/app/components/base/file-uploader/file-uploader-in-chat-input/index.tsx +++ b/web/app/components/base/file-uploader/file-uploader-in-chat-input/index.tsx @@ -13,21 +13,27 @@ import { TransferMethod } from '@/types/app' type FileUploaderInChatInputProps = { fileConfig: FileUpload + readonly?: boolean } const FileUploaderInChatInput = ({ fileConfig, + readonly, }: FileUploaderInChatInputProps) => { const renderTrigger = useCallback((open: boolean) => { return ( ) }, []) + if(readonly) + return renderTrigger(false) + return ( = ({ type TextGenerationImageUploaderProps = { settings: VisionSettings onFilesChange: (files: ImageFile[]) => void + disabled?: boolean } const TextGenerationImageUploader: FC = ({ settings, onFilesChange, + disabled, }) => { const { t } = useTranslation() @@ -92,7 +94,7 @@ const TextGenerationImageUploader: FC = ({ const localUpload = ( = settings.number_limits} + disabled={files.length >= settings.number_limits || disabled} limit={+settings.image_file_size_limit!} > { @@ -113,7 +115,7 @@ const TextGenerationImageUploader: FC = ({ const urlUpload = ( = settings.number_limits} + disabled={files.length >= settings.number_limits || disabled} /> ) diff --git a/web/app/components/base/tab-header/index.tsx b/web/app/components/base/tab-header/index.tsx index 846277e5db..c208612d30 100644 --- a/web/app/components/base/tab-header/index.tsx +++ b/web/app/components/base/tab-header/index.tsx @@ -16,6 +16,8 @@ export type ITabHeaderProps = { items: Item[] value: string itemClassName?: string + itemWrapClassName?: string + activeItemClassName?: string onChange: (value: string) => void } @@ -23,6 +25,8 @@ const TabHeader: FC = ({ items, value, itemClassName, + itemWrapClassName, + activeItemClassName, onChange, }) => { const renderItem = ({ id, name, icon, extra, disabled }: Item) => ( @@ -30,8 +34,9 @@ const TabHeader: FC = ({ key={id} className={cn( 'system-md-semibold relative flex cursor-pointer items-center border-b-2 border-transparent pb-2 pt-2.5', - id === value ? 'border-components-tab-active text-text-primary' : 'text-text-tertiary', + id === value ? cn('border-components-tab-active text-text-primary', activeItemClassName) : 'text-text-tertiary', disabled && 'cursor-not-allowed opacity-30', + itemWrapClassName, )} onClick={() => !disabled && onChange(id)} > diff --git a/web/app/components/base/voice-input/index.tsx b/web/app/components/base/voice-input/index.tsx index 6587a61217..c6506464b4 100644 --- a/web/app/components/base/voice-input/index.tsx +++ b/web/app/components/base/voice-input/index.tsx @@ -11,7 +11,7 @@ import { convertToMp3 } from './utils' import s from './index.module.css' import cn from '@/utils/classnames' import { StopCircle } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices' -import { audioToText } from '@/service/share' +import { AppSourceType, audioToText } from '@/service/share' type VoiceInputTypes = { onConverted: (text: string) => void @@ -108,7 +108,7 @@ const VoiceInput = ({ } try { - const audioResponse = await audioToText(url, isPublic, formData) + const audioResponse = await audioToText(url, isPublic ? AppSourceType.webApp : AppSourceType.installedApp, formData) onConverted(audioResponse.text) onCancel() } diff --git a/web/app/components/explore/app-card/index.tsx b/web/app/components/explore/app-card/index.tsx index 0d6a9b4ad4..dda12083c3 100644 --- a/web/app/components/explore/app-card/index.tsx +++ b/web/app/components/explore/app-card/index.tsx @@ -6,6 +6,12 @@ import cn from '@/utils/classnames' import type { App } from '@/models/explore' import AppIcon from '@/app/components/base/app-icon' import { AppTypeIcon } from '../../app/type-selector' +import { useGlobalPublicStore } from '@/context/global-public-context' +import { RiInformation2Line } from '@remixicon/react' +import { useCallback } from 'react' +import ExploreContext from '@/context/explore-context' +import { useContextSelector } from 'use-context-selector' + export type AppCardProps = { app: App canCreate: boolean @@ -21,8 +27,17 @@ 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(ExploreContext, ctx => ctx.setShowTryAppPanel) + const showTryAPPPanel = useCallback((appId: string) => { + return () => { + setShowTryAppPanel?.(true, { appId, app }) + } + }, [setShowTryAppPanel, app.category]) + return ( -
+
- {isExplore && canCreate && ( - )}
diff --git a/web/app/components/explore/app-list/index.tsx b/web/app/components/explore/app-list/index.tsx index 252a102d80..6b66667486 100644 --- a/web/app/components/explore/app-list/index.tsx +++ b/web/app/components/explore/app-list/index.tsx @@ -22,6 +22,11 @@ import { } from '@/models/app' import { useImportDSL } from '@/hooks/use-import-dsl' import DSLConfirmModal from '@/app/components/app/create-from-dsl-modal/dsl-confirm-modal' +import Banner from '@/app/components/explore/banner/banner' +import { useGlobalPublicStore } from '@/context/global-public-context' +import Button from '@/app/components/base/button' +import { useContextSelector } from 'use-context-selector' +import TryApp from '../try-app' type AppsProps = { onSuccess?: () => void @@ -36,12 +41,19 @@ const Apps = ({ onSuccess, }: AppsProps) => { const { t } = useTranslation() + const { systemFeatures } = useGlobalPublicStore() const { hasEditPermission } = useContext(ExploreContext) const allCategoriesEn = t('explore.apps.allCategories', { lng: 'en' }) const [keywords, setKeywords] = useState('') const [searchKeywords, setSearchKeywords] = useState('') + const hasFilterCondition = !!keywords + const handleResetFilter = useCallback(() => { + setKeywords('') + setSearchKeywords('') + }, []) + const { run: handleSearch } = useDebounceFn(() => { setSearchKeywords(keywords) }, { wait: 500 }) @@ -96,6 +108,18 @@ const Apps = ({ isFetching, } = useImportDSL() const [showDSLConfirmModal, setShowDSLConfirmModal] = useState(false) + + const isShowTryAppPanel = useContextSelector(ExploreContext, ctx => ctx.isShowTryAppPanel) + const setShowTryAppPanel = useContextSelector(ExploreContext, ctx => ctx.setShowTryAppPanel) + const hideTryAppPanel = useCallback(() => { + setShowTryAppPanel(false) + }, [setShowTryAppPanel]) + const appParams = useContextSelector(ExploreContext, ctx => ctx.currentApp) + const handleShowFromTryApp = useCallback(() => { + setCurrApp(appParams?.app || null) + setIsShowCreateModal(true) + }, [appParams?.app]) + const onCreate: CreateAppModalProps['onConfirm'] = async ({ name, icon_type, @@ -103,6 +127,8 @@ const Apps = ({ icon_background, description, }) => { + hideTryAppPanel() + const { export_data } = await fetchAppDetail( currApp?.app.id as string, ) @@ -141,23 +167,25 @@ const Apps = ({ return (
- -
-
{t('explore.apps.title')}
-
{t('explore.apps.description')}
-
- + {systemFeatures.enable_explore_banner && ( +
+ +
+ )}
- +
+
{!hasFilterCondition ? t('explore.apps.title') : t('explore.apps.resultNum', { num: searchFilteredList.length })}
+ {hasFilterCondition && ( + <> +
+ + + )} +
+
+ +
+
@@ -214,6 +251,14 @@ const Apps = ({ /> ) } + + {isShowTryAppPanel && ( + + )}
) } diff --git a/web/app/components/explore/banner/banner-item.tsx b/web/app/components/explore/banner/banner-item.tsx new file mode 100644 index 0000000000..9645a307b2 --- /dev/null +++ b/web/app/components/explore/banner/banner-item.tsx @@ -0,0 +1,196 @@ +import type { FC } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { RiArrowRightLine } from '@remixicon/react' +import { useCarousel } from '@/app/components/base/carousel' +import { IndicatorButton } from './indicator-button' +import { useTranslation } from 'react-i18next' +import cn from '@/utils/classnames' + +export type BannerData = { + id: string + content: { + 'category': string + 'title': string + 'description': string + 'img-src': string + } + status: 'enabled' | 'disabled' + link: string + created_at: number +} + +type BannerItemProps = { + banner: BannerData + autoplayDelay: number + isPaused?: boolean +} + +const RESPONSIVE_BREAKPOINT = 1200 +const MAX_RESPONSIVE_WIDTH = 600 +const INDICATOR_WIDTH = 20 +const INDICATOR_GAP = 8 +const MIN_VIEW_MORE_WIDTH = 480 + +export const BannerItem: FC = ({ banner, autoplayDelay, isPaused = false }) => { + const { t } = useTranslation() + const { api, selectedIndex } = useCarousel() + const { category, title, description, 'img-src': imgSrc } = banner.content + + const [resetKey, setResetKey] = useState(0) + const textAreaRef = useRef(null) + const [maxWidth, setMaxWidth] = useState(undefined) + + const slideInfo = useMemo(() => { + const slides = api?.slideNodes() ?? [] + const totalSlides = slides.length + const nextIndex = totalSlides > 0 ? (selectedIndex + 1) % totalSlides : 0 + return { slides, totalSlides, nextIndex } + }, [api, selectedIndex]) + + const indicatorsWidth = useMemo(() => { + const count = slideInfo.totalSlides + if (count === 0) return 0 + // Calculate: indicator buttons + gaps + extra spacing (3 * 20px for divider and padding) + return (count + 2) * INDICATOR_WIDTH + (count - 1) * INDICATOR_GAP + }, [slideInfo.totalSlides]) + + const viewMoreStyle = useMemo(() => { + if (!maxWidth) return undefined + return { + maxWidth: `${maxWidth}px`, + minWidth: indicatorsWidth ? `${Math.min(maxWidth - indicatorsWidth, MIN_VIEW_MORE_WIDTH)}px` : undefined, + } + }, [maxWidth, indicatorsWidth]) + + const responsiveStyle = useMemo( + () => (maxWidth !== undefined ? { maxWidth: `${maxWidth}px` } : undefined), + [maxWidth], + ) + + const incrementResetKey = useCallback(() => setResetKey(prev => prev + 1), []) + + useEffect(() => { + const updateMaxWidth = () => { + if (window.innerWidth < RESPONSIVE_BREAKPOINT && textAreaRef.current) { + const textAreaWidth = textAreaRef.current.offsetWidth + setMaxWidth(Math.min(textAreaWidth, MAX_RESPONSIVE_WIDTH)) + } + else { + setMaxWidth(undefined) + } + } + + updateMaxWidth() + + const resizeObserver = new ResizeObserver(updateMaxWidth) + if (textAreaRef.current) + resizeObserver.observe(textAreaRef.current) + + window.addEventListener('resize', updateMaxWidth) + + return () => { + resizeObserver.disconnect() + window.removeEventListener('resize', updateMaxWidth) + } + }, []) + + useEffect(() => { + incrementResetKey() + }, [selectedIndex, incrementResetKey]) + + const handleBannerClick = useCallback(() => { + incrementResetKey() + if (banner.link) + window.open(banner.link, '_blank', 'noopener,noreferrer') + }, [banner.link, incrementResetKey]) + + const handleIndicatorClick = useCallback((index: number) => { + incrementResetKey() + api?.scrollTo(index) + }, [api, incrementResetKey]) + + return ( +
+ {/* Left content area */} +
+
+ {/* Text section */} +
+ {/* Title area */} +
+

+ {category} +

+

+ {title} +

+
+ {/* Description area */} +
+

+ {description} +

+
+
+ + {/* Actions section */} +
+ {/* View more button */} +
+
+ +
+ + {t('explore.banner.viewMore')} + +
+ +
+ {/* Slide navigation indicators */} +
+ {slideInfo.slides.map((_: unknown, index: number) => ( + handleIndicatorClick(index)} + /> + ))} +
+
+
+
+
+
+ + {/* Right image area */} +
+ {title} +
+
+ ) +} diff --git a/web/app/components/explore/banner/banner.tsx b/web/app/components/explore/banner/banner.tsx new file mode 100644 index 0000000000..bc81e4b213 --- /dev/null +++ b/web/app/components/explore/banner/banner.tsx @@ -0,0 +1,93 @@ +import type { FC } from 'react' +import React, { useEffect, useMemo, useRef, useState } from 'react' +import { Carousel } from '@/app/components/base/carousel' +import { useGetBanners } from '@/service/use-explore' +import Loading from '../../base/loading' +import { type BannerData, BannerItem } from './banner-item' +import { useI18N } from '@/context/i18n' + +const AUTOPLAY_DELAY = 5000 +const MIN_LOADING_HEIGHT = 168 +const RESIZE_DEBOUNCE_DELAY = 50 + +const LoadingState: FC = () => ( +
+ +
+) + +const Banner: FC = () => { + const { locale } = useI18N() + const { data: banners, isLoading, isError } = useGetBanners(locale) + const [isHovered, setIsHovered] = useState(false) + const [isResizing, setIsResizing] = useState(false) + const resizeTimerRef = useRef(null) + + const enabledBanners = useMemo( + () => banners?.filter((banner: BannerData) => banner.status === 'enabled') ?? [], + [banners], + ) + + const isPaused = isHovered || isResizing + + // Handle window resize to pause animation + useEffect(() => { + const handleResize = () => { + setIsResizing(true) + + if (resizeTimerRef.current) + clearTimeout(resizeTimerRef.current) + + resizeTimerRef.current = setTimeout(() => { + setIsResizing(false) + }, RESIZE_DEBOUNCE_DELAY) + } + + window.addEventListener('resize', handleResize) + + return () => { + window.removeEventListener('resize', handleResize) + if (resizeTimerRef.current) + clearTimeout(resizeTimerRef.current) + } + }, []) + + if (isLoading) + return + + if (isError || enabledBanners.length === 0) + return null + + return ( + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + + {enabledBanners.map((banner: BannerData) => ( + + + + ))} + + + ) +} + +export default React.memo(Banner) diff --git a/web/app/components/explore/banner/indicator-button.tsx b/web/app/components/explore/banner/indicator-button.tsx new file mode 100644 index 0000000000..5214fd7826 --- /dev/null +++ b/web/app/components/explore/banner/indicator-button.tsx @@ -0,0 +1,111 @@ +import type { FC } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' +import cn from '@/utils/classnames' + +type IndicatorButtonProps = { + index: number + selectedIndex: number + isNextSlide: boolean + autoplayDelay: number + resetKey: number + isPaused?: boolean + onClick: () => void +} + +const PROGRESS_MAX = 100 +const DEGREES_PER_PERCENT = 3.6 + +export const IndicatorButton: FC = ({ + index, + selectedIndex, + isNextSlide, + autoplayDelay, + resetKey, + isPaused = false, + onClick, +}) => { + const [progress, setProgress] = useState(0) + const frameIdRef = useRef(undefined) + const startTimeRef = useRef(0) + + const isActive = index === selectedIndex + const shouldAnimate = !document.hidden && !isPaused + + useEffect(() => { + if (!isNextSlide) { + setProgress(0) + if (frameIdRef.current) + cancelAnimationFrame(frameIdRef.current) + return + } + + setProgress(0) + startTimeRef.current = Date.now() + + const animate = () => { + if (!document.hidden && !isPaused) { + const elapsed = Date.now() - startTimeRef.current + const newProgress = Math.min((elapsed / autoplayDelay) * PROGRESS_MAX, PROGRESS_MAX) + setProgress(newProgress) + + if (newProgress < PROGRESS_MAX) + frameIdRef.current = requestAnimationFrame(animate) + } + else { + frameIdRef.current = requestAnimationFrame(animate) + } + } + + if (shouldAnimate) + frameIdRef.current = requestAnimationFrame(animate) + + return () => { + if (frameIdRef.current) + cancelAnimationFrame(frameIdRef.current) + } + }, [isNextSlide, autoplayDelay, resetKey, isPaused]) + + const handleClick = useCallback((e: React.MouseEvent) => { + e.stopPropagation() + onClick() + }, [onClick]) + + const progressDegrees = progress * DEGREES_PER_PERCENT + + return ( + + ) +} diff --git a/web/app/components/explore/category.tsx b/web/app/components/explore/category.tsx index a36c91a73d..66b63c68da 100644 --- a/web/app/components/explore/category.tsx +++ b/web/app/components/explore/category.tsx @@ -5,7 +5,6 @@ import { useTranslation } from 'react-i18next' import cn from '@/utils/classnames' import exploreI18n from '@/i18n/en-US/explore' import type { AppCategory } from '@/models/explore' -import { ThumbsUp } from '@/app/components/base/icons/src/vender/line/alertsAndFeedback' const categoryI18n = exploreI18n.category @@ -31,7 +30,7 @@ const Category: FC = ({ const isAllCategories = !list.includes(value as AppCategory) || value === allCategoriesEn const itemClassName = (isSelected: boolean) => cn( - 'flex h-[32px] cursor-pointer items-center rounded-lg border-[0.5px] border-transparent px-3 py-[7px] font-medium leading-[18px] text-text-tertiary hover:bg-components-main-nav-nav-button-bg-active', + 'system-sm-medium flex h-7 cursor-pointer items-center rounded-lg border border-transparent px-3 text-text-tertiary hover:bg-components-main-nav-nav-button-bg-active', isSelected && 'border-components-main-nav-nav-button-border bg-components-main-nav-nav-button-bg-active text-components-main-nav-nav-button-text-active shadow-xs', ) @@ -41,7 +40,6 @@ const Category: FC = ({ className={itemClassName(isAllCategories)} onClick={() => onChange(allCategoriesEn)} > - {t('explore.apps.allCategories')}
{list.filter(name => name !== allCategoriesEn).map(name => ( diff --git a/web/app/components/explore/index.tsx b/web/app/components/explore/index.tsx index e716de96f1..beafc8b6d1 100644 --- a/web/app/components/explore/index.tsx +++ b/web/app/components/explore/index.tsx @@ -2,6 +2,7 @@ import type { FC } from 'react' import React, { useEffect, useState } from 'react' import { useRouter } from 'next/navigation' +import type { CurrentTryAppParams } from '@/context/explore-context' import ExploreContext from '@/context/explore-context' import Sidebar from '@/app/components/explore/sidebar' import { useAppContext } from '@/context/app-context' @@ -42,6 +43,16 @@ const Explore: FC = ({ return router.replace('/datasets') }, [isCurrentWorkspaceDatasetOperator]) + 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 (
= ({ setInstalledApps, isFetchingInstalledApps, setIsFetchingInstalledApps, + currentApp: currentTryAppParams, + isShowTryAppPanel, + setShowTryAppPanel, } } > diff --git a/web/app/components/explore/installed-app/index.tsx b/web/app/components/explore/installed-app/index.tsx index 8032e173c6..b4321d6336 100644 --- a/web/app/components/explore/installed-app/index.tsx +++ b/web/app/components/explore/installed-app/index.tsx @@ -12,6 +12,7 @@ import AppUnavailable from '../../base/app-unavailable' import { useGetUserCanAccessApp } from '@/service/access-control' import { useGetInstalledAppAccessModeByAppId, useGetInstalledAppMeta, useGetInstalledAppParams } from '@/service/use-explore' import type { AppData } from '@/models/share' +import type { AccessMode } from '@/models/access-control' export type IInstalledAppProps = { id: string @@ -61,8 +62,8 @@ const InstalledApp: FC = ({ if (appMeta) updateWebAppMeta(appMeta) if (webAppAccessMode) - updateWebAppAccessMode(webAppAccessMode.accessMode) - updateUserCanAccessApp(Boolean(userCanAccessApp && userCanAccessApp?.result)) + updateWebAppAccessMode((webAppAccessMode as { accessMode: AccessMode }).accessMode) + updateUserCanAccessApp(Boolean(userCanAccessApp && (userCanAccessApp as { result: boolean })?.result)) }, [installedApp, appMeta, appParams, updateAppInfo, updateAppParams, updateUserCanAccessApp, updateWebAppMeta, userCanAccessApp, webAppAccessMode, updateWebAppAccessMode]) if (appParamsError) { diff --git a/web/app/components/explore/item-operation/index.tsx b/web/app/components/explore/item-operation/index.tsx index 6fd11fd084..3959f4710e 100644 --- a/web/app/components/explore/item-operation/index.tsx +++ b/web/app/components/explore/item-operation/index.tsx @@ -52,7 +52,7 @@ const ItemOperation: FC = ({ setOpen(v => !v)} > -
+
-
{name}
+
{name}
e.stopPropagation()}> ( - - - -) - -const DiscoveryIcon = () => ( - - - -) +import { RiAppsFill, RiExpandRightLine, RiLayoutLeft2Line } from '@remixicon/react' +import { useBoolean } from 'ahooks' +import NoApps from './no-apps' export type IExploreSideBarProps = { controlUpdateInstalledApps: number @@ -44,6 +35,9 @@ const SideBar: FC = ({ const media = useBreakpoints() const isMobile = media === MediaType.mobile + const [isFold, { + toggle: toggleIsFold, + }] = useBoolean(false) const [showConfirm, setShowConfirm] = useState(false) const [currId, setCurrId] = useState('') @@ -83,22 +77,22 @@ const SideBar: FC = ({ const pinnedAppsCount = installedApps.filter(({ is_pinned }) => is_pinned).length return ( -
-
- - {isDiscoverySelected ? : } - {!isMobile &&
{t('explore.sidebar.discovery')}
} - -
+
+ +
+ +
+ {!isMobile && !isFold &&
{t('explore.sidebar.title')}
} + {installedApps.length > 0 && ( -
-

{t('explore.sidebar.workspace')}

-
+ {!isMobile && !isFold &&

{t('explore.sidebar.webApps')}

} + {installedApps.length === 0 && !isMobile && !isFold && } +
= ({ {installedApps.map(({ id, is_pinned, uninstallable, app: { name, icon_type, icon, icon_url, icon_background } }, index) => ( = ({
)} + + {!isMobile && ( +
+ {isFold ? : ( + + )} +
+ )} + {showConfirm && ( { + const { t } = useTranslation() + const { theme } = useTheme() + return ( +
+
+
{t(`${i18nPrefix}.title`)}
+
{t(`${i18nPrefix}.description`)}
+ {t(`${i18nPrefix}.learnMore`)} +
+ ) +} +export default React.memo(NoApps) diff --git a/web/app/components/explore/sidebar/no-apps/no-web-apps-dark.png b/web/app/components/explore/sidebar/no-apps/no-web-apps-dark.png new file mode 100644 index 0000000000..e153686fcd Binary files /dev/null and b/web/app/components/explore/sidebar/no-apps/no-web-apps-dark.png differ diff --git a/web/app/components/explore/sidebar/no-apps/no-web-apps-light.png b/web/app/components/explore/sidebar/no-apps/no-web-apps-light.png new file mode 100644 index 0000000000..2416b957d2 Binary files /dev/null and b/web/app/components/explore/sidebar/no-apps/no-web-apps-light.png differ diff --git a/web/app/components/explore/sidebar/no-apps/style.module.css b/web/app/components/explore/sidebar/no-apps/style.module.css new file mode 100644 index 0000000000..ad3787ce2b --- /dev/null +++ b/web/app/components/explore/sidebar/no-apps/style.module.css @@ -0,0 +1,7 @@ +.light { + background-image: url('./no-web-apps-light.png'); +} + +.dark { + background-image: url('./no-web-apps-dark.png'); +} diff --git a/web/app/components/explore/try-app/app-info/index.tsx b/web/app/components/explore/try-app/app-info/index.tsx new file mode 100644 index 0000000000..fa662b1920 --- /dev/null +++ b/web/app/components/explore/try-app/app-info/index.tsx @@ -0,0 +1,92 @@ +'use client' +import type { FC } from 'react' +import React from 'react' +import AppIcon from '@/app/components/base/app-icon' +import { AppTypeIcon } from '@/app/components/app/type-selector' +import { useTranslation } from 'react-i18next' +import type { TryAppInfo } from '@/service/try-app' +import cn from '@/utils/classnames' +import Button from '@/app/components/base/button' +import { RiAddLine } from '@remixicon/react' +import useGetRequirements from './use-get-requirements' + +type Props = { + appId: string + appDetail: TryAppInfo + category?: string + className?: string + onCreate: () => void +} + +const headerClassName = 'system-sm-semibold-uppercase text-text-secondary mb-3' + +const AppInfo: FC = ({ + appId, + className, + category, + appDetail, + onCreate, +}) => { + const { t } = useTranslation() + const mode = appDetail?.mode + const { requirements } = useGetRequirements({ appDetail, appId }) + return ( +
+ {/* name and icon */} +
+
+ + +
+
+
+
{appDetail.name}
+
+
+ {mode === 'advanced-chat' &&
{t('app.types.advanced').toUpperCase()}
} + {mode === 'chat' &&
{t('app.types.chatbot').toUpperCase()}
} + {mode === 'agent-chat' &&
{t('app.types.agent').toUpperCase()}
} + {mode === 'workflow' &&
{t('app.types.workflow').toUpperCase()}
} + {mode === 'completion' &&
{t('app.types.completion').toUpperCase()}
} +
+
+
+ {appDetail.description && ( +
{appDetail.description}
+ )} + + + {category && ( +
+
{t('explore.tryApp.category')}
+
{category}
+
+ )} + {requirements.length > 0 && ( +
+
{t('explore.tryApp.requirements')}
+
+ {requirements.map(item => ( +
+
+
{item.name}
+
+ ))} +
+
+ )} + +
+ ) +} +export default React.memo(AppInfo) diff --git a/web/app/components/explore/try-app/app-info/use-get-requirements.ts b/web/app/components/explore/try-app/app-info/use-get-requirements.ts new file mode 100644 index 0000000000..e9ef0c22f5 --- /dev/null +++ b/web/app/components/explore/try-app/app-info/use-get-requirements.ts @@ -0,0 +1,78 @@ +import type { LLMNodeType } from '@/app/components/workflow/nodes/llm/types' +import type { ToolNodeType } from '@/app/components/workflow/nodes/tool/types' +import { BlockEnum } from '@/app/components/workflow/types' +import { MARKETPLACE_API_PREFIX } from '@/config' +import type { TryAppInfo } from '@/service/try-app' +import { useGetTryAppFlowPreview } from '@/service/use-try-app' +import type { AgentTool } from '@/types/app' +import { uniqBy } from 'lodash-es' + +type Params = { + appDetail: TryAppInfo + appId: string +} + +type RequirementItem = { + name: string + iconUrl: string +} +const getIconUrl = (provider: string, tool: string) => { + return `${MARKETPLACE_API_PREFIX}/plugins/${provider}/${tool}/icon` +} + +const useGetRequirements = ({ appDetail, appId }: Params) => { + const isBasic = ['chat', 'completion', 'agent-chat'].includes(appDetail.mode) + const isAgent = appDetail.mode === 'agent-chat' + const isAdvanced = !isBasic + const { data: flowData } = useGetTryAppFlowPreview(appId, isBasic) + + const requirements: RequirementItem[] = [] + if(isBasic) { + const modelProviderAndName = appDetail.model_config.model.provider.split('/') + const name = appDetail.model_config.model.provider.split('/').pop() || '' + requirements.push({ + name, + iconUrl: getIconUrl(modelProviderAndName[0], modelProviderAndName[1]), + }) + } + if(isAgent) { + requirements.push(...appDetail.model_config.agent_mode.tools.filter(data => (data as AgentTool).enabled).map((data) => { + const tool = data as AgentTool + const modelProviderAndName = tool.provider_id.split('/') + return { + name: tool.tool_label, + iconUrl: getIconUrl(modelProviderAndName[0], modelProviderAndName[1]), + } + })) + } + if(isAdvanced && flowData && flowData?.graph?.nodes?.length > 0) { + const nodes = flowData.graph.nodes + const llmNodes = nodes.filter(node => node.data.type === BlockEnum.LLM) + requirements.push(...llmNodes.map((node) => { + const data = node.data as LLMNodeType + const modelProviderAndName = data.model.provider.split('/') + return { + name: data.model.name, + iconUrl: getIconUrl(modelProviderAndName[0], modelProviderAndName[1]), + } + })) + + const toolNodes = nodes.filter(node => node.data.type === BlockEnum.Tool) + requirements.push(...toolNodes.map((node) => { + const data = node.data as ToolNodeType + const toolProviderAndName = data.provider_id.split('/') + return { + name: data.tool_label, + iconUrl: getIconUrl(toolProviderAndName[0], toolProviderAndName[1]), + } + })) + } + + const uniqueRequirements = uniqBy(requirements, 'name') + + return { + requirements: uniqueRequirements, + } +} + +export default useGetRequirements diff --git a/web/app/components/explore/try-app/app/chat.tsx b/web/app/components/explore/try-app/app/chat.tsx new file mode 100644 index 0000000000..54f167391d --- /dev/null +++ b/web/app/components/explore/try-app/app/chat.tsx @@ -0,0 +1,70 @@ +'use client' +import type { FC } from 'react' +import React 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' +import { + EmbeddedChatbotContext, +} from '@/app/components/base/chat/embedded-chatbot/context' +import { + useEmbeddedChatbot, +} from '@/app/components/base/chat/embedded-chatbot/hooks' +import cn from '@/utils/classnames' +import { AppSourceType } from '@/service/share' +import Alert from '@/app/components/base/alert' +import { useTranslation } from 'react-i18next' +import { useBoolean } from 'ahooks' +import type { TryAppInfo } from '@/service/try-app' +import AppIcon from '@/app/components/base/app-icon' + +type Props = { + appId: string + appDetail: TryAppInfo + className: string +} + +const TryApp: FC = ({ + appId, + appDetail, + className, +}) => { + const { t } = useTranslation() + const media = useBreakpoints() + const isMobile = media === MediaType.mobile + const themeBuilder = useThemeContext() + const chatData = useEmbeddedChatbot(AppSourceType.tryApp, appId) + const [isHideTryNotice, { + setTrue: hideTryNotice, + }] = useBoolean(false) + return ( + +
+
+
+ +
{appDetail.name}
+
+
+
+ {!isHideTryNotice && ( + + )} + +
+
+
+ ) +} +export default React.memo(TryApp) diff --git a/web/app/components/explore/try-app/app/index.tsx b/web/app/components/explore/try-app/app/index.tsx new file mode 100644 index 0000000000..db8c5cd764 --- /dev/null +++ b/web/app/components/explore/try-app/app/index.tsx @@ -0,0 +1,44 @@ +'use client' +import type { FC } from 'react' +import React from 'react' +import Chat from './chat' +import TextGeneration from './text-generation' +import type { AppData } from '@/models/share' +import useDocumentTitle from '@/hooks/use-document-title' +import type { TryAppInfo } from '@/service/try-app' + +type Props = { + appId: string + appDetail: TryAppInfo +} + +const TryApp: FC = ({ + appId, + appDetail, +}) => { + const mode = appDetail?.mode + const isChat = ['chat', 'advanced-chat', 'agent-chat'].includes(mode!) + const isCompletion = !isChat + + useDocumentTitle(appDetail?.site?.title || '') + return ( +
+ {isChat && ( + + )} + {isCompletion && ( + + )} +
+ ) +} +export default React.memo(TryApp) diff --git a/web/app/components/explore/try-app/app/text-generation.tsx b/web/app/components/explore/try-app/app/text-generation.tsx new file mode 100644 index 0000000000..1cb778ba52 --- /dev/null +++ b/web/app/components/explore/try-app/app/text-generation.tsx @@ -0,0 +1,252 @@ +'use client' +import type { FC } from 'react' +import React, { useCallback, useEffect, useRef, useState } from 'react' +import cn from '@/utils/classnames' +import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' +import AppIcon from '@/app/components/base/app-icon' +import Loading from '@/app/components/base/loading' +import { appDefaultIconBackground } from '@/config' +import RunOnce from '../../../share/text-generation/run-once' +import { useWebAppStore } from '@/context/web-app-context' +import type { AppData, SiteInfo } from '@/models/share' +import { useGetTryAppParams } from '@/service/use-try-app' +import type { MoreLikeThisConfig, PromptConfig, TextToSpeechConfig } from '@/models/debug' +import { userInputsFormToPromptVariables } from '@/utils/model-config' +import type { VisionFile, VisionSettings } from '@/types/app' +import { Resolution, TransferMethod } from '@/types/app' +import { useBoolean } from 'ahooks' +import { noop } from 'lodash-es' +import type { Task } from '../../../share/text-generation/types' +import Res from '@/app/components/share/text-generation/result' +import { AppSourceType } from '@/service/share' +import { TaskStatus } from '@/app/components/share/text-generation/types' +import Alert from '@/app/components/base/alert' +import { useTranslation } from 'react-i18next' + +type Props = { + appId: string + className?: string + isWorkflow?: boolean + appData: AppData | null +} + +const TextGeneration: FC = ({ + appId, + className, + isWorkflow, + appData, +}) => { + const { t } = useTranslation() + const media = useBreakpoints() + const isPC = media === MediaType.pc + + const [inputs, doSetInputs] = useState>({}) + const inputsRef = useRef>(inputs) + const setInputs = useCallback((newInputs: Record) => { + doSetInputs(newInputs) + inputsRef.current = newInputs + }, []) + + const updateAppInfo = useWebAppStore(s => s.updateAppInfo) + const { data: tryAppParams } = useGetTryAppParams(appId) + + const updateAppParams = useWebAppStore(s => s.updateAppParams) + const appParams = useWebAppStore(s => s.appParams) + const [siteInfo, setSiteInfo] = useState(null) + const [promptConfig, setPromptConfig] = useState(null) + const [customConfig, setCustomConfig] = useState | null>(null) + const [moreLikeThisConfig, setMoreLikeThisConfig] = useState(null) + const [textToSpeechConfig, setTextToSpeechConfig] = useState(null) + const [controlSend, setControlSend] = useState(0) + const [visionConfig, setVisionConfig] = useState({ + enabled: false, + number_limits: 2, + detail: Resolution.low, + transfer_methods: [TransferMethod.local_file], + }) + const [completionFiles, setCompletionFiles] = useState([]) + const [isShowResultPanel, { setTrue: doShowResultPanel, setFalse: hideResultPanel }] = useBoolean(false) + const showResultPanel = () => { + // fix: useClickAway hideResSidebar will close sidebar + setTimeout(() => { + doShowResultPanel() + }, 0) + } + + const handleSend = () => { + setControlSend(Date.now()) + showResultPanel() + } + + const [resultExisted, setResultExisted] = useState(false) + + useEffect(() => { + if (!appData) return + updateAppInfo(appData) + }, [appData, updateAppInfo]) + + useEffect(() => { + if (!tryAppParams) return + updateAppParams(tryAppParams) + }, [tryAppParams, updateAppParams]) + + useEffect(() => { + (async () => { + if (!appData || !appParams) + return + const { site: siteInfo, custom_config } = appData + setSiteInfo(siteInfo as SiteInfo) + setCustomConfig(custom_config) + + const { user_input_form, more_like_this, file_upload, text_to_speech }: any = appParams + setVisionConfig({ + // legacy of image upload compatible + ...file_upload, + transfer_methods: file_upload?.allowed_file_upload_methods || file_upload?.allowed_upload_methods, + // legacy of image upload compatible + image_file_size_limit: appParams?.system_parameters.image_file_size_limit, + fileUploadConfig: appParams?.system_parameters, + } as any) + const prompt_variables = userInputsFormToPromptVariables(user_input_form) + setPromptConfig({ + prompt_template: '', // placeholder for future + prompt_variables, + } as PromptConfig) + setMoreLikeThisConfig(more_like_this) + setTextToSpeechConfig(text_to_speech) + })() + }, [appData, appParams]) + + const [isCompleted, setIsCompleted] = useState(false) + const handleCompleted = useCallback(() => { + setIsCompleted(true) + }, []) + const [isHideTryNotice, { + setTrue: hideTryNotice, + }] = useBoolean(false) + + const renderRes = (task?: Task) => ( setResultExisted(true)} + />) + + const renderResWrap = ( +
+
+ {isCompleted && !isHideTryNotice && ( + + )} + {renderRes()} +
+
+ ) + + if (!siteInfo || !promptConfig) { + return ( +
+ +
+ ) + } + + return ( +
+ {/* Left */} +
+ {/* Header */} +
+
+ +
{siteInfo.title}
+
+ {siteInfo.description && ( +
{siteInfo.description}
+ )} +
+ {/* form */} +
+ +
+
+ + {/* Result */} +
+ {!isPC && ( +
{ + if (isShowResultPanel) + hideResultPanel() + else + showResultPanel() + }} + > +
+
+ )} + {renderResWrap} +
+
+ ) +} + +export default React.memo(TextGeneration) diff --git a/web/app/components/explore/try-app/index.tsx b/web/app/components/explore/try-app/index.tsx new file mode 100644 index 0000000000..47dd77932b --- /dev/null +++ b/web/app/components/explore/try-app/index.tsx @@ -0,0 +1,70 @@ +'use client' +import type { FC } from 'react' +import React, { useState } from 'react' +import Modal from '@/app/components/base/modal/index' +import Tab, { TypeEnum } from './tab' +import Button from '../../base/button' +import { RiCloseLine } from '@remixicon/react' +import AppInfo from './app-info' +import App from './app' +import Preview from './preview' +import { useGetTryAppInfo } from '@/service/use-try-app' +import Loading from '@/app/components/base/loading' + +type Props = { + appId: string + category?: string + onClose: () => void + onCreate: () => void +} + +const TryApp: FC = ({ + appId, + category, + onClose, + onCreate, +}) => { + const [type, setType] = useState(TypeEnum.TRY) + const { data: appDetail, isLoading } = useGetTryAppInfo(appId) + + return ( + + {isLoading ? (
+ +
) : ( +
+
+ + +
+ {/* Main content */} +
+ {type === TypeEnum.TRY ? : } + +
+
+ )} +
+ ) +} +export default React.memo(TryApp) diff --git a/web/app/components/explore/try-app/preview/basic-app-preview.tsx b/web/app/components/explore/try-app/preview/basic-app-preview.tsx new file mode 100644 index 0000000000..16e9f4abd5 --- /dev/null +++ b/web/app/components/explore/try-app/preview/basic-app-preview.tsx @@ -0,0 +1,361 @@ +'use client' +import type { FC } from 'react' +import React, { useMemo, useState } from 'react' +import { clone } from 'lodash-es' + +import Loading from '@/app/components/base/loading' + +import type { ModelConfig as BackendModelConfig, PromptVariable } from '@/types/app' +import ConfigContext from '@/context/debug-configuration' +import Config from '@/app/components/app/configuration/config' +import Debug from '@/app/components/app/configuration/debug' +import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' +import { ModelModeType, Resolution, TransferMethod } from '@/types/app' +import type { ModelConfig } from '@/models/debug' +import { PromptMode } from '@/models/debug' +import { ANNOTATION_DEFAULT, DEFAULT_AGENT_SETTING, DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG } from '@/config' +import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' +import type { FormValue } from '@/app/components/header/account-setting/model-provider-page/declarations' + +import { FeaturesProvider } from '@/app/components/base/features' +import type { Features as FeaturesData, FileUpload } from '@/app/components/base/features/types' +import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants' +import { SupportUploadFileTypes } from '@/app/components/workflow/types' + +import { useGetTryAppDataSets, useGetTryAppInfo } from '@/service/use-try-app' +import { noop } from 'lodash-es' +import { correctModelProvider, correctToolProvider } from '@/utils' +import { userInputsFormToPromptVariables } from '@/utils/model-config' +import { useTextGenerationCurrentProviderAndModelAndModelList } from '../../../header/account-setting/model-provider-page/hooks' +import { useAllToolProviders } from '@/service/use-tools' +import { basePath } from '@/utils/var' + +type Props = { + appId: string +} + +const defaultModelConfig = { + provider: 'langgenius/openai/openai', + model_id: 'gpt-3.5-turbo', + mode: ModelModeType.unset, + configs: { + prompt_template: '', + prompt_variables: [] as PromptVariable[], + }, + more_like_this: null, + opening_statement: '', + suggested_questions: [], + sensitive_word_avoidance: null, + speech_to_text: null, + text_to_speech: null, + file_upload: null, + suggested_questions_after_answer: null, + retriever_resource: null, + annotation_reply: null, + dataSets: [], + agentConfig: DEFAULT_AGENT_SETTING, +} +const BasicAppPreview: FC = ({ + appId, +}) => { + const media = useBreakpoints() + const isMobile = media === MediaType.mobile + + const { data: appDetail, isLoading: isLoadingAppDetail } = useGetTryAppInfo(appId) + const { data: collectionListFromServer, isLoading: isLoadingToolProviders } = useAllToolProviders() + const collectionList = collectionListFromServer?.map((item) => { + return { + ...item, + icon: basePath && typeof item.icon == 'string' && !item.icon.includes(basePath) ? `${basePath}${item.icon}` : item.icon, + } + }) + const datasetIds = (() => { + if (isLoadingAppDetail) + return [] + const modelConfig = appDetail?.model_config + if (!modelConfig) + return [] + let datasets: any = null + + if (modelConfig.agent_mode?.tools?.find(({ dataset }: any) => dataset?.enabled)) + datasets = modelConfig.agent_mode?.tools.filter(({ dataset }: any) => dataset?.enabled) + // new dataset struct + else if (modelConfig.dataset_configs.datasets?.datasets?.length > 0) + datasets = modelConfig.dataset_configs?.datasets?.datasets + + if (datasets?.length && datasets?.length > 0) + return datasets.map(({ dataset }: any) => dataset.id) + + return [] + })() + const { data: dataSetData, isLoading: isLoadingDatasets } = useGetTryAppDataSets(appId, datasetIds) + const dataSets = dataSetData?.data || [] + const isLoading = isLoadingAppDetail || isLoadingDatasets || isLoadingToolProviders + + const modelConfig: ModelConfig = ((modelConfig?: BackendModelConfig) => { + if (isLoading || !modelConfig) + return defaultModelConfig + + const model = modelConfig.model + + const newModelConfig = { + provider: correctModelProvider(model.provider), + model_id: model.name, + mode: model.mode, + configs: { + prompt_template: modelConfig.pre_prompt || '', + prompt_variables: userInputsFormToPromptVariables( + [ + ...(modelConfig.user_input_form as any), + ...( + modelConfig.external_data_tools?.length + ? modelConfig.external_data_tools.map((item: any) => { + return { + external_data_tool: { + variable: item.variable as string, + label: item.label as string, + enabled: item.enabled, + type: item.type as string, + config: item.config, + required: true, + icon: item.icon, + icon_background: item.icon_background, + }, + } + }) + : [] + ), + ], + modelConfig.dataset_query_variable, + ), + }, + more_like_this: modelConfig.more_like_this, + opening_statement: modelConfig.opening_statement, + suggested_questions: modelConfig.suggested_questions, + sensitive_word_avoidance: modelConfig.sensitive_word_avoidance, + speech_to_text: modelConfig.speech_to_text, + text_to_speech: modelConfig.text_to_speech, + file_upload: modelConfig.file_upload, + suggested_questions_after_answer: modelConfig.suggested_questions_after_answer, + retriever_resource: modelConfig.retriever_resource, + annotation_reply: modelConfig.annotation_reply, + external_data_tools: modelConfig.external_data_tools, + dataSets, + agentConfig: appDetail?.mode === 'agent-chat' ? { + max_iteration: DEFAULT_AGENT_SETTING.max_iteration, + ...modelConfig.agent_mode, + // remove dataset + enabled: true, // modelConfig.agent_mode?.enabled is not correct. old app: the value of app with dataset's is always true + tools: modelConfig.agent_mode?.tools.filter((tool: any) => { + return !tool.dataset + }).map((tool: any) => { + const toolInCollectionList = collectionList?.find(c => tool.provider_id === c.id) + return { + ...tool, + isDeleted: appDetail?.deleted_tools?.some((deletedTool: any) => deletedTool.id === tool.id && deletedTool.tool_name === tool.tool_name), + notAuthor: toolInCollectionList?.is_team_authorization === false, + ...(tool.provider_type === 'builtin' ? { + provider_id: correctToolProvider(tool.provider_name, !!toolInCollectionList), + provider_name: correctToolProvider(tool.provider_name, !!toolInCollectionList), + } : {}), + } + }), + } : DEFAULT_AGENT_SETTING, + } + return (newModelConfig as any) + })(appDetail?.model_config) + const mode = appDetail?.mode + // const isChatApp = ['chat', 'advanced-chat', 'agent-chat'].includes(mode!) + + // chat configuration + const promptMode = modelConfig?.prompt_type === PromptMode.advanced ? PromptMode.advanced : PromptMode.simple + const isAdvancedMode = promptMode === PromptMode.advanced + const isAgent = mode === 'agent-chat' + const chatPromptConfig = isAdvancedMode ? (modelConfig?.chat_prompt_config || clone(DEFAULT_CHAT_PROMPT_CONFIG)) : undefined + const suggestedQuestions = modelConfig?.suggested_questions || [] + const moreLikeThisConfig = modelConfig?.more_like_this || { enabled: false } + const suggestedQuestionsAfterAnswerConfig = modelConfig?.suggested_questions_after_answer || { enabled: false } + const speechToTextConfig = modelConfig?.speech_to_text || { enabled: false } + const textToSpeechConfig = modelConfig?.text_to_speech || { enabled: false, voice: '', language: '' } + const citationConfig = modelConfig?.retriever_resource || { enabled: false } + const annotationConfig = modelConfig?.annotation_reply || { + id: '', + enabled: false, + score_threshold: ANNOTATION_DEFAULT.score_threshold, + embedding_model: { + embedding_provider_name: '', + embedding_model_name: '', + }, + } + const moderationConfig = modelConfig?.sensitive_word_avoidance || { enabled: false } + // completion configuration + const completionPromptConfig = modelConfig?.completion_prompt_config || clone(DEFAULT_COMPLETION_PROMPT_CONFIG) as any + + // prompt & model config + const inputs = {} + const query = '' + const completionParams = useState({}) + + const { + currentModel: currModel, + } = useTextGenerationCurrentProviderAndModelAndModelList( + { + provider: modelConfig.provider, + model: modelConfig.model_id, + }, + ) + + const isShowVisionConfig = !!currModel?.features?.includes(ModelFeatureEnum.vision) + const isShowDocumentConfig = !!currModel?.features?.includes(ModelFeatureEnum.document) + const isShowAudioConfig = !!currModel?.features?.includes(ModelFeatureEnum.audio) + const isAllowVideoUpload = !!currModel?.features?.includes(ModelFeatureEnum.video) + const visionConfig = { + enabled: false, + number_limits: 2, + detail: Resolution.low, + transfer_methods: [TransferMethod.local_file], + } + + const featuresData: FeaturesData = useMemo(() => { + return { + moreLikeThis: modelConfig.more_like_this || { enabled: false }, + opening: { + enabled: !!modelConfig.opening_statement, + opening_statement: modelConfig.opening_statement || '', + suggested_questions: modelConfig.suggested_questions || [], + }, + moderation: modelConfig.sensitive_word_avoidance || { enabled: false }, + speech2text: modelConfig.speech_to_text || { enabled: false }, + text2speech: modelConfig.text_to_speech || { enabled: false }, + file: { + image: { + detail: modelConfig.file_upload?.image?.detail || Resolution.high, + enabled: !!modelConfig.file_upload?.image?.enabled, + number_limits: modelConfig.file_upload?.image?.number_limits || 3, + transfer_methods: modelConfig.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'], + }, + enabled: !!(modelConfig.file_upload?.enabled || modelConfig.file_upload?.image?.enabled), + allowed_file_types: modelConfig.file_upload?.allowed_file_types || [], + allowed_file_extensions: modelConfig.file_upload?.allowed_file_extensions || [...FILE_EXTS[SupportUploadFileTypes.image], ...FILE_EXTS[SupportUploadFileTypes.video]].map(ext => `.${ext}`), + allowed_file_upload_methods: modelConfig.file_upload?.allowed_file_upload_methods || modelConfig.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'], + number_limits: modelConfig.file_upload?.number_limits || modelConfig.file_upload?.image?.number_limits || 3, + fileUploadConfig: {}, + } as FileUpload, + suggested: modelConfig.suggested_questions_after_answer || { enabled: false }, + citation: modelConfig.retriever_resource || { enabled: false }, + annotationReply: modelConfig.annotation_reply || { enabled: false }, + } + }, [modelConfig]) + + if (isLoading) { + return
+ +
+ } + const value = { + readonly: true, + appId, + isAPIKeySet: true, + isTrailFinished: false, + mode, + modelModeType: '', + promptMode, + isAdvancedMode, + isAgent, + isOpenAI: false, + isFunctionCall: false, + collectionList: [], + setPromptMode: noop, + canReturnToSimpleMode: false, + setCanReturnToSimpleMode: noop, + chatPromptConfig, + completionPromptConfig, + currentAdvancedPrompt: '', + setCurrentAdvancedPrompt: noop, + conversationHistoriesRole: completionPromptConfig.conversation_histories_role, + showHistoryModal: false, + setConversationHistoriesRole: noop, + hasSetBlockStatus: true, + conversationId: '', + introduction: '', + setIntroduction: noop, + suggestedQuestions, + setSuggestedQuestions: noop, + setConversationId: noop, + controlClearChatMessage: false, + setControlClearChatMessage: noop, + prevPromptConfig: {}, + setPrevPromptConfig: noop, + moreLikeThisConfig, + setMoreLikeThisConfig: noop, + suggestedQuestionsAfterAnswerConfig, + setSuggestedQuestionsAfterAnswerConfig: noop, + speechToTextConfig, + setSpeechToTextConfig: noop, + textToSpeechConfig, + setTextToSpeechConfig: noop, + citationConfig, + setCitationConfig: noop, + annotationConfig, + setAnnotationConfig: noop, + moderationConfig, + setModerationConfig: noop, + externalDataToolsConfig: {}, + setExternalDataToolsConfig: noop, + formattingChanged: false, + setFormattingChanged: noop, + inputs, + setInputs: noop, + query, + setQuery: noop, + completionParams, + setCompletionParams: noop, + modelConfig, + setModelConfig: noop, + showSelectDataSet: noop, + dataSets, + setDataSets: noop, + datasetConfigs: [], + datasetConfigsRef: {}, + setDatasetConfigs: noop, + hasSetContextVar: true, + isShowVisionConfig, + visionConfig, + setVisionConfig: noop, + isAllowVideoUpload, + isShowDocumentConfig, + isShowAudioConfig, + rerankSettingModalOpen: false, + setRerankSettingModalOpen: noop, + } + return ( + + +
+
+
+ +
+ {!isMobile &&
+
+ +
+
} +
+
+
+
+ ) +} +export default React.memo(BasicAppPreview) diff --git a/web/app/components/explore/try-app/preview/flow-app-preview.tsx b/web/app/components/explore/try-app/preview/flow-app-preview.tsx new file mode 100644 index 0000000000..a1f682124f --- /dev/null +++ b/web/app/components/explore/try-app/preview/flow-app-preview.tsx @@ -0,0 +1,37 @@ +'use client' +import Loading from '@/app/components/base/loading' +import { useGetTryAppFlowPreview } from '@/service/use-try-app' +import type { FC } from 'react' +import React from 'react' +import WorkflowPreview from '@/app/components/workflow/workflow-preview' +import cn from '@/utils/classnames' + +type Props = { + appId: string + className?: string +} + +const FlowAppPreview: FC = ({ + appId, + className, +}) => { + const { data, isLoading } = useGetTryAppFlowPreview(appId) + + if (isLoading) { + return
+ +
+ } + if (!data) + return null + return ( +
+ +
+ ) +} +export default React.memo(FlowAppPreview) diff --git a/web/app/components/explore/try-app/preview/index.tsx b/web/app/components/explore/try-app/preview/index.tsx new file mode 100644 index 0000000000..5463aa7b7b --- /dev/null +++ b/web/app/components/explore/try-app/preview/index.tsx @@ -0,0 +1,23 @@ +'use client' +import type { FC } from 'react' +import React from 'react' +import BasicAppPreview from './basic-app-preview' +import FlowAppPreview from './flow-app-preview' +import type { TryAppInfo } from '@/service/try-app' + +type Props = { + appId: string + appDetail: TryAppInfo +} + +const Preview: FC = ({ + appId, + appDetail, +}) => { + const isBasicApp = ['agent-chat', 'chat', 'completion'].includes(appDetail.mode) + + return
+ {isBasicApp ? : } +
+} +export default React.memo(Preview) diff --git a/web/app/components/explore/try-app/tab.tsx b/web/app/components/explore/try-app/tab.tsx new file mode 100644 index 0000000000..030edec7bd --- /dev/null +++ b/web/app/components/explore/try-app/tab.tsx @@ -0,0 +1,37 @@ +'use client' +import type { FC } from 'react' +import React from 'react' +import TabHeader from '../../base/tab-header' +import { useTranslation } from 'react-i18next' + +export enum TypeEnum { + TRY = 'try', + DETAIL = 'detail', +} + +type Props = { + value: TypeEnum + onChange: (value: TypeEnum) => void +} + +const Tab: FC = ({ + value, + onChange, +}) => { + const { t } = useTranslation() + const tabs = [ + { id: TypeEnum.TRY, name: t('explore.tryApp.tabHeader.try') }, + { id: TypeEnum.DETAIL, name: t('explore.tryApp.tabHeader.detail') }, + ] + return ( + void} + itemClassName='ml-0 system-md-semibold-uppercase' + itemWrapClassName='pt-2' + activeItemClassName='border-util-colors-blue-brand-blue-brand-500' + /> + ) +} +export default React.memo(Tab) diff --git a/web/app/components/share/text-generation/index.tsx b/web/app/components/share/text-generation/index.tsx index 98804c7311..f542089c5f 100644 --- a/web/app/components/share/text-generation/index.tsx +++ b/web/app/components/share/text-generation/index.tsx @@ -14,7 +14,7 @@ import RunBatch from './run-batch' import ResDownload from './run-batch/res-download' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import RunOnce from '@/app/components/share/text-generation/run-once' -import { fetchSavedMessage as doFetchSavedMessage, removeMessage, saveMessage } from '@/service/share' +import { AppSourceType, fetchSavedMessage as doFetchSavedMessage, removeMessage, saveMessage } from '@/service/share' import type { SiteInfo } from '@/models/share' import type { MoreLikeThisConfig, @@ -41,24 +41,9 @@ import { AccessMode } from '@/models/access-control' import { useGlobalPublicStore } from '@/context/global-public-context' import useDocumentTitle from '@/hooks/use-document-title' import { useWebAppStore } from '@/context/web-app-context' - +import type { Task } from './types' +import { TaskStatus } from './types' const GROUP_SIZE = 5 // to avoid RPM(Request per minute) limit. The group task finished then the next group. -enum TaskStatus { - pending = 'pending', - running = 'running', - completed = 'completed', - failed = 'failed', -} - -type TaskParam = { - inputs: Record -} - -type Task = { - id: number - status: TaskStatus - params: TaskParam -} export type IMainProps = { isInstalledApp?: boolean @@ -72,6 +57,7 @@ const TextGeneration: FC = ({ isWorkflow = false, }) => { const { notify } = Toast + const appSourceType = isInstalledApp ? AppSourceType.installedApp : AppSourceType.webApp const { t } = useTranslation() const media = useBreakpoints() @@ -101,16 +87,16 @@ const TextGeneration: FC = ({ // save message const [savedMessages, setSavedMessages] = useState([]) const fetchSavedMessage = useCallback(async () => { - const res: any = await doFetchSavedMessage(isInstalledApp, appId) + const res: any = await doFetchSavedMessage(appSourceType, appId) setSavedMessages(res.data) - }, [isInstalledApp, appId]) + }, [appSourceType, appId]) const handleSaveMessage = async (messageId: string) => { - await saveMessage(messageId, isInstalledApp, appId) + await saveMessage(messageId, appSourceType, appId) notify({ type: 'success', message: t('common.api.saved') }) fetchSavedMessage() } const handleRemoveSavedMessage = async (messageId: string) => { - await removeMessage(messageId, isInstalledApp, appId) + await removeMessage(messageId, appSourceType, appId) notify({ type: 'success', message: t('common.api.remove') }) fetchSavedMessage() } @@ -416,8 +402,8 @@ const TextGeneration: FC = ({ isCallBatchAPI={isCallBatchAPI} isPC={isPC} isMobile={!isPC} - isInstalledApp={isInstalledApp} - installedAppInfo={installedAppInfo} + appSourceType={isInstalledApp ? AppSourceType.installedApp : AppSourceType.webApp} + appId={installedAppInfo?.id} isError={task?.status === TaskStatus.failed} promptConfig={promptConfig} moreLikeThisEnabled={!!moreLikeThisConfig?.enabled} diff --git a/web/app/components/share/text-generation/result/index.tsx b/web/app/components/share/text-generation/result/index.tsx index 7d21df448d..19af68aa90 100644 --- a/web/app/components/share/text-generation/result/index.tsx +++ b/web/app/components/share/text-generation/result/index.tsx @@ -7,11 +7,11 @@ import { produce } from 'immer' import TextGenerationRes from '@/app/components/app/text-generate/item' import NoData from '@/app/components/share/text-generation/no-data' import Toast from '@/app/components/base/toast' +import type { AppSourceType } from '@/service/share' import { sendCompletionMessage, sendWorkflowMessage, updateFeedback } from '@/service/share' import type { FeedbackType } from '@/app/components/base/chat/chat/type' import Loading from '@/app/components/base/loading' import type { PromptConfig } from '@/models/debug' -import type { InstalledApp } from '@/models/explore' import { TransferMethod, type VisionFile, type VisionSettings } from '@/types/app' import { NodeRunningStatus, WorkflowRunningStatus } from '@/app/components/workflow/types' import type { WorkflowProcess } from '@/app/components/base/chat/types' @@ -30,8 +30,8 @@ export type IResultProps = { isCallBatchAPI: boolean isPC: boolean isMobile: boolean - isInstalledApp: boolean - installedAppInfo?: InstalledApp + appSourceType: AppSourceType + appId?: string isError: boolean isShowTextToSpeech: boolean promptConfig: PromptConfig | null @@ -55,8 +55,8 @@ const Result: FC = ({ isCallBatchAPI, isPC, isMobile, - isInstalledApp, - installedAppInfo, + appSourceType, + appId, isError, isShowTextToSpeech, promptConfig, @@ -104,7 +104,7 @@ const Result: FC = ({ }) const handleFeedback = async (feedback: FeedbackType) => { - await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating, content: feedback.content } }, isInstalledApp, installedAppInfo?.id) + await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating, content: feedback.content } }, appSourceType, appId) setFeedback(feedback) } @@ -374,8 +374,8 @@ const Result: FC = ({ })) }, }, - isInstalledApp, - installedAppInfo?.id, + appSourceType, + appId, ) } else { @@ -408,7 +408,7 @@ const Result: FC = ({ onCompleted(getCompletionRes(), taskId, false) isEnd = true }, - }, isInstalledApp, installedAppInfo?.id) + }, appSourceType, appId) } } @@ -439,8 +439,8 @@ const Result: FC = ({ feedback={feedback} onSave={handleSaveMessage} isMobile={isMobile} - isInstalledApp={isInstalledApp} - installedAppId={installedAppInfo?.id} + appSourceType={appSourceType} + installedAppId={appId} isLoading={isCallBatchAPI ? (!completionRes && isResponding) : false} taskId={isCallBatchAPI ? ((taskId as number) < 10 ? `0${taskId}` : `${taskId}`) : undefined} controlClearMoreLikeThis={controlClearMoreLikeThis} diff --git a/web/app/components/share/text-generation/types.ts b/web/app/components/share/text-generation/types.ts new file mode 100644 index 0000000000..dba8eb2ca9 --- /dev/null +++ b/web/app/components/share/text-generation/types.ts @@ -0,0 +1,16 @@ +type TaskParam = { + inputs: Record +} + +export type Task = { + id: number + status: TaskStatus + params: TaskParam +} + +export enum TaskStatus { + pending = 'pending', + running = 'running', + completed = 'completed', + failed = 'failed', +} diff --git a/web/app/components/workflow/nodes/_base/components/before-run-form/bool-input.tsx b/web/app/components/workflow/nodes/_base/components/before-run-form/bool-input.tsx index 73219a551b..29f6e13025 100644 --- a/web/app/components/workflow/nodes/_base/components/before-run-form/bool-input.tsx +++ b/web/app/components/workflow/nodes/_base/components/before-run-form/bool-input.tsx @@ -9,6 +9,7 @@ type Props = { value: boolean required?: boolean onChange: (value: boolean) => void + readonly?: boolean } const BoolInput: FC = ({ @@ -16,6 +17,7 @@ const BoolInput: FC = ({ onChange, name, required, + readonly, }) => { const { t } = useTranslation() const handleChange = useCallback(() => { @@ -27,6 +29,7 @@ const BoolInput: FC = ({ className='!h-4 !w-4' checked={!!value} onCheck={handleChange} + disabled={readonly} />
{name} diff --git a/web/app/components/workflow/workflow-preview/index.tsx b/web/app/components/workflow/workflow-preview/index.tsx index 5fd4b9097c..0a10a9d416 100644 --- a/web/app/components/workflow/workflow-preview/index.tsx +++ b/web/app/components/workflow/workflow-preview/index.tsx @@ -61,12 +61,14 @@ type WorkflowPreviewProps = { edges: Edge[] viewport: Viewport className?: string + miniMapToRight?: boolean } const WorkflowPreview = ({ nodes, edges, viewport, className, + miniMapToRight, }: WorkflowPreviewProps) => { const [nodesData, setNodesData] = useState(() => initialNodes(nodes, edges)) const [edgesData, setEdgesData] = useState(() => initialEdges(edges, nodes)) @@ -97,8 +99,9 @@ const WorkflowPreview = ({ height: 72, }} maskColor='var(--color-workflow-minimap-bg)' - className='!absolute !bottom-14 !left-4 z-[9] !m-0 !h-[72px] !w-[102px] !rounded-lg !border-[0.5px] - !border-divider-subtle !bg-background-default-subtle !shadow-md !shadow-shadow-shadow-5' + className={cn('!absolute !bottom-14 z-[9] !m-0 !h-[72px] !w-[102px] !rounded-lg !border-[0.5px] !border-divider-subtle !bg-background-default-subtle !shadow-md !shadow-shadow-shadow-5', + miniMapToRight ? '!right-4' : '!left-4', + )} />
diff --git a/web/context/debug-configuration.ts b/web/context/debug-configuration.ts index dba2e7a231..3ec82ee8ef 100644 --- a/web/context/debug-configuration.ts +++ b/web/context/debug-configuration.ts @@ -29,6 +29,7 @@ import type { Collection } from '@/app/components/tools/types' import { noop } from 'lodash-es' type IDebugConfiguration = { + readonly?: boolean appId: string isAPIKeySet: boolean isTrailFinished: boolean @@ -108,6 +109,7 @@ type IDebugConfiguration = { } const DebugConfigurationContext = createContext({ + readonly: false, appId: '', isAPIKeySet: false, isTrailFinished: false, diff --git a/web/context/explore-context.ts b/web/context/explore-context.ts index d8d64fb34c..6d11a919b1 100644 --- a/web/context/explore-context.ts +++ b/web/context/explore-context.ts @@ -1,7 +1,12 @@ import { createContext } from 'use-context-selector' -import type { InstalledApp } from '@/models/explore' +import type { App, InstalledApp } from '@/models/explore' import { noop } from 'lodash-es' +export type CurrentTryAppParams = { + appId: string + app: App +} + type IExplore = { controlUpdateInstalledApps: number setControlUpdateInstalledApps: (controlUpdateInstalledApps: number) => void @@ -10,6 +15,9 @@ type IExplore = { setInstalledApps: (installedApps: InstalledApp[]) => void isFetchingInstalledApps: boolean setIsFetchingInstalledApps: (isFetchingInstalledApps: boolean) => void + currentApp?: CurrentTryAppParams + isShowTryAppPanel: boolean + setShowTryAppPanel: (showTryAppPanel: boolean, params?: CurrentTryAppParams) => void } const ExploreContext = createContext({ @@ -20,6 +28,9 @@ const ExploreContext = createContext({ setInstalledApps: noop, isFetchingInstalledApps: false, setIsFetchingInstalledApps: noop, + isShowTryAppPanel: false, + setShowTryAppPanel: noop, + currentApp: undefined, }) export default ExploreContext diff --git a/web/i18n-config/check-i18n-sync.js b/web/i18n-config/check-i18n-sync.js index e67c567f49..8935e1d58b 100644 --- a/web/i18n-config/check-i18n-sync.js +++ b/web/i18n-config/check-i18n-sync.js @@ -2,19 +2,19 @@ const fs = require('fs') const path = require('path') -const { camelCase } = require('lodash') +const { camelCase } = require('lodash-es') // Import the NAMESPACES array from i18next-config.ts function getNamespacesFromConfig() { const configPath = path.join(__dirname, 'i18next-config.ts') const configContent = fs.readFileSync(configPath, 'utf8') - + // Extract NAMESPACES array using regex const namespacesMatch = configContent.match(/const NAMESPACES = \[([\s\S]*?)\]/) if (!namespacesMatch) { throw new Error('Could not find NAMESPACES array in i18next-config.ts') } - + // Parse the namespaces const namespacesStr = namespacesMatch[1] const namespaces = namespacesStr @@ -22,25 +22,25 @@ function getNamespacesFromConfig() { .map(line => line.trim()) .filter(line => line.startsWith("'") || line.startsWith('"')) .map(line => line.slice(1, -1)) // Remove quotes - + return namespaces } function getNamespacesFromTypes() { const typesPath = path.join(__dirname, '../types/i18n.d.ts') - + if (!fs.existsSync(typesPath)) { return null } - + const typesContent = fs.readFileSync(typesPath, 'utf8') - + // Extract namespaces from Messages type const messagesMatch = typesContent.match(/export type Messages = \{([\s\S]*?)\}/) if (!messagesMatch) { return null } - + // Parse the properties const propertiesStr = messagesMatch[1] const properties = propertiesStr @@ -49,66 +49,66 @@ function getNamespacesFromTypes() { .filter(line => line.includes(':')) .map(line => line.split(':')[0].trim()) .filter(prop => prop.length > 0) - + return properties } function main() { try { console.log('🔍 Checking i18n types synchronization...') - + // Get namespaces from config const configNamespaces = getNamespacesFromConfig() console.log(`📦 Found ${configNamespaces.length} namespaces in config`) - + // Convert to camelCase for comparison const configCamelCase = configNamespaces.map(ns => camelCase(ns)).sort() - + // Get namespaces from type definitions const typeNamespaces = getNamespacesFromTypes() - + if (!typeNamespaces) { console.error('❌ Type definitions file not found or invalid') console.error(' Run: pnpm run gen:i18n-types') process.exit(1) } - + console.log(`🔧 Found ${typeNamespaces.length} namespaces in types`) - + const typeCamelCase = typeNamespaces.sort() - + // Compare arrays const configSet = new Set(configCamelCase) const typeSet = new Set(typeCamelCase) - + // Find missing in types const missingInTypes = configCamelCase.filter(ns => !typeSet.has(ns)) - + // Find extra in types const extraInTypes = typeCamelCase.filter(ns => !configSet.has(ns)) - + let hasErrors = false - + if (missingInTypes.length > 0) { hasErrors = true console.error('❌ Missing in type definitions:') missingInTypes.forEach(ns => console.error(` - ${ns}`)) } - + if (extraInTypes.length > 0) { hasErrors = true console.error('❌ Extra in type definitions:') extraInTypes.forEach(ns => console.error(` - ${ns}`)) } - + if (hasErrors) { console.error('\n💡 To fix synchronization issues:') console.error(' Run: pnpm run gen:i18n-types') process.exit(1) } - + console.log('✅ i18n types are synchronized') - + } catch (error) { console.error('❌ Error:', error.message) process.exit(1) @@ -117,4 +117,4 @@ function main() { if (require.main === module) { main() -} \ No newline at end of file +} diff --git a/web/i18n-config/generate-i18n-types.js b/web/i18n-config/generate-i18n-types.js index ba34446962..c1ca0b59be 100644 --- a/web/i18n-config/generate-i18n-types.js +++ b/web/i18n-config/generate-i18n-types.js @@ -2,19 +2,19 @@ const fs = require('fs') const path = require('path') -const { camelCase } = require('lodash') +const { camelCase } = require('lodash-es') // Import the NAMESPACES array from i18next-config.ts function getNamespacesFromConfig() { const configPath = path.join(__dirname, 'i18next-config.ts') const configContent = fs.readFileSync(configPath, 'utf8') - + // Extract NAMESPACES array using regex const namespacesMatch = configContent.match(/const NAMESPACES = \[([\s\S]*?)\]/) if (!namespacesMatch) { throw new Error('Could not find NAMESPACES array in i18next-config.ts') } - + // Parse the namespaces const namespacesStr = namespacesMatch[1] const namespaces = namespacesStr @@ -22,7 +22,7 @@ function getNamespacesFromConfig() { .map(line => line.trim()) .filter(line => line.startsWith("'") || line.startsWith('"')) .map(line => line.slice(1, -1)) // Remove quotes - + return namespaces } @@ -90,40 +90,40 @@ declare module 'i18next' { function main() { const args = process.argv.slice(2) const checkMode = args.includes('--check') - + try { console.log('📦 Generating i18n type definitions...') - + // Get namespaces from config const namespaces = getNamespacesFromConfig() console.log(`✅ Found ${namespaces.length} namespaces`) - + // Generate type definitions const typeDefinitions = generateTypeDefinitions(namespaces) - + const outputPath = path.join(__dirname, '../types/i18n.d.ts') - + if (checkMode) { // Check mode: compare with existing file if (!fs.existsSync(outputPath)) { console.error('❌ Type definitions file does not exist') process.exit(1) } - + const existingContent = fs.readFileSync(outputPath, 'utf8') if (existingContent.trim() !== typeDefinitions.trim()) { console.error('❌ Type definitions are out of sync') console.error(' Run: pnpm run gen:i18n-types') process.exit(1) } - + console.log('✅ Type definitions are in sync') } else { // Generate mode: write file fs.writeFileSync(outputPath, typeDefinitions) console.log(`✅ Generated type definitions: ${outputPath}`) } - + } catch (error) { console.error('❌ Error:', error.message) process.exit(1) @@ -132,4 +132,4 @@ function main() { if (require.main === module) { main() -} \ No newline at end of file +} diff --git a/web/i18n/de-DE/explore.ts b/web/i18n/de-DE/explore.ts index 7a8e8e04bb..abada1081f 100644 --- a/web/i18n/de-DE/explore.ts +++ b/web/i18n/de-DE/explore.ts @@ -1,9 +1,7 @@ const translation = { title: 'Entdecken', sidebar: { - discovery: 'Entdeckung', chat: 'Chat', - workspace: 'Arbeitsbereich', action: { pin: 'Anheften', unpin: 'Lösen', @@ -16,12 +14,8 @@ const translation = { }, }, apps: { - title: 'Apps von Dify erkunden', - description: 'Nutzen Sie diese Vorlagen-Apps sofort oder passen Sie Ihre eigenen Apps basierend auf den Vorlagen an.', - allCategories: 'Alle Kategorien', }, appCard: { - addToWorkspace: 'Zum Arbeitsbereich hinzufügen', customize: 'Anpassen', }, appCustomize: { diff --git a/web/i18n/en-US/common.ts b/web/i18n/en-US/common.ts index a90fb1580f..2e2b9f97e7 100644 --- a/web/i18n/en-US/common.ts +++ b/web/i18n/en-US/common.ts @@ -666,6 +666,7 @@ const translation = { hitScore: 'Retrieval Score:', }, inputPlaceholder: 'Talk to {{botName}}', + inputDisabledPlaceholder: 'Preview Only', thinking: 'Thinking...', thought: 'Thought', resend: 'Resend', diff --git a/web/i18n/en-US/explore.ts b/web/i18n/en-US/explore.ts index 7ae457ce9d..b1891f6d3e 100644 --- a/web/i18n/en-US/explore.ts +++ b/web/i18n/en-US/explore.ts @@ -1,9 +1,9 @@ const translation = { title: 'Explore', sidebar: { - discovery: 'Discovery', + title: 'App gallery', chat: 'Chat', - workspace: 'Workspace', + webApps: 'Web apps', action: { pin: 'Pin', unpin: 'Unpin', @@ -14,15 +14,31 @@ const translation = { title: 'Delete app', content: 'Are you sure you want to delete this app?', }, + noApps: { + title: 'No web apps', + description: 'Published web apps will appear here', + learnMore: 'Learn more', + }, }, apps: { - title: 'Explore Apps', - description: 'Use these template apps instantly or customize your own apps based on the templates.', - allCategories: 'Recommended', + title: 'Try Dify\'s curated apps to find AI solutions for your business', + allCategories: 'All', + resultNum: '{{num}} results', + resetFilter: 'Clear filter', }, appCard: { - addToWorkspace: 'Add to Workspace', - customize: 'Customize', + addToWorkspace: 'Use template', + try: 'Details', + }, + tryApp: { + tabHeader: { + try: 'Try it', + detail: 'Orchestration Details', + }, + createFromSampleApp: 'Create from this sample app', + category: 'Category', + requirements: 'Requirements', + tryInfo: 'This is a sample app. You can try up to 5 messages. To keep using it, click "Create form this sample app" and set it up!', }, appCustomize: { title: 'Create app from {{name}}', @@ -39,6 +55,9 @@ const translation = { Workflow: 'Workflow', Entertainment: 'Entertainment', }, + banner: { + viewMore: 'VIEW MORE', + }, } export default translation diff --git a/web/i18n/es-ES/explore.ts b/web/i18n/es-ES/explore.ts index 204f8da6c3..dcd7e7ab91 100644 --- a/web/i18n/es-ES/explore.ts +++ b/web/i18n/es-ES/explore.ts @@ -1,9 +1,7 @@ const translation = { title: 'Explorar', sidebar: { - discovery: 'Descubrimiento', chat: 'Chat', - workspace: 'Espacio de trabajo', action: { pin: 'Anclar', unpin: 'Desanclar', @@ -16,12 +14,8 @@ const translation = { }, }, apps: { - title: 'Explorar aplicaciones de Dify', - description: 'Utiliza estas aplicaciones de plantilla al instante o personaliza tus propias aplicaciones basadas en las plantillas.', - allCategories: 'Recomendado', }, appCard: { - addToWorkspace: 'Agregar al espacio de trabajo', customize: 'Personalizar', }, appCustomize: { diff --git a/web/i18n/fa-IR/explore.ts b/web/i18n/fa-IR/explore.ts index b2c6708b54..0c28102380 100644 --- a/web/i18n/fa-IR/explore.ts +++ b/web/i18n/fa-IR/explore.ts @@ -1,9 +1,7 @@ const translation = { title: 'کاوش', sidebar: { - discovery: 'کشف', chat: 'چت', - workspace: 'فضای کاری', action: { pin: 'سنجاق کردن', unpin: 'برداشتن سنجاق', @@ -16,12 +14,8 @@ const translation = { }, }, apps: { - title: 'کاوش برنامه‌ها توسط دیفی', - description: 'از این برنامه‌های قالبی بلافاصله استفاده کنید یا برنامه‌های خود را بر اساس این قالب‌ها سفارشی کنید.', - allCategories: 'پیشنهاد شده', }, appCard: { - addToWorkspace: 'افزودن به فضای کاری', customize: 'سفارشی کردن', }, appCustomize: { diff --git a/web/i18n/fr-FR/explore.ts b/web/i18n/fr-FR/explore.ts index d868ebd2df..1ab22c160b 100644 --- a/web/i18n/fr-FR/explore.ts +++ b/web/i18n/fr-FR/explore.ts @@ -1,9 +1,7 @@ const translation = { title: 'Explorer', sidebar: { - discovery: 'Découverte', chat: 'Discussion', - workspace: 'Espace de travail', action: { pin: 'Épingle', unpin: 'Détacher', @@ -16,12 +14,8 @@ const translation = { }, }, apps: { - title: 'Explorez les applications par Dify', - description: 'Utilisez ces applications modèles instantanément ou personnalisez vos propres applications basées sur les modèles.', - allCategories: 'Recommandé', }, appCard: { - addToWorkspace: 'Ajouter à l\'espace de travail', customize: 'Personnaliser', }, appCustomize: { diff --git a/web/i18n/hi-IN/explore.ts b/web/i18n/hi-IN/explore.ts index a9e850cb3b..879b5c4a5d 100644 --- a/web/i18n/hi-IN/explore.ts +++ b/web/i18n/hi-IN/explore.ts @@ -1,9 +1,7 @@ const translation = { title: 'अन्वेषण करें', sidebar: { - discovery: 'खोज', chat: 'चैट', - workspace: 'कार्यक्षेत्र', action: { pin: 'पिन करें', unpin: 'पिन हटाएँ', @@ -16,13 +14,8 @@ const translation = { }, }, apps: { - title: 'डिफ़ी द्वारा ऐप्स का अन्वेषण करें', - description: - 'इन टेम्प्लेट ऐप्स का तुरंत उपयोग करें या टेम्प्लेट्स के आधार पर अपने स्वयं के ऐप्स को कस्टमाइज़ करें।', - allCategories: 'अनुशंसित', }, appCard: { - addToWorkspace: 'कार्यक्षेत्र में जोड़ें', customize: 'अनुकूलित करें', }, appCustomize: { diff --git a/web/i18n/id-ID/explore.ts b/web/i18n/id-ID/explore.ts index a482d8f755..aa6fa46d16 100644 --- a/web/i18n/id-ID/explore.ts +++ b/web/i18n/id-ID/explore.ts @@ -10,18 +10,12 @@ const translation = { content: 'Apakah Anda yakin ingin menghapus aplikasi ini?', title: 'Hapus aplikasi', }, - workspace: 'Workspace', - discovery: 'Penemuan', chat: 'Mengobrol', }, apps: { - allCategories: 'Direkomendasikan', - description: 'Gunakan aplikasi templat ini secara instan atau sesuaikan aplikasi Anda sendiri berdasarkan templat.', - title: 'Jelajahi Aplikasi', }, appCard: { customize: 'Menyesuaikan', - addToWorkspace: 'Tambahkan ke Ruang Kerja', }, appCustomize: { subTitle: 'Ikon & nama aplikasi', diff --git a/web/i18n/it-IT/explore.ts b/web/i18n/it-IT/explore.ts index d94df45d54..1150f609af 100644 --- a/web/i18n/it-IT/explore.ts +++ b/web/i18n/it-IT/explore.ts @@ -1,9 +1,7 @@ const translation = { title: 'Esplora', sidebar: { - discovery: 'Scoperta', chat: 'Chat', - workspace: 'Workspace', action: { pin: 'Fissa', unpin: 'Sblocca', @@ -16,13 +14,8 @@ const translation = { }, }, apps: { - title: 'Esplora App di Dify', - description: - 'Usa queste app modello istantaneamente o personalizza le tue app basate sui modelli.', - allCategories: 'Consigliato', }, appCard: { - addToWorkspace: 'Aggiungi a Workspace', customize: 'Personalizza', }, appCustomize: { diff --git a/web/i18n/ja-JP/common.ts b/web/i18n/ja-JP/common.ts index 64c0ac91ec..32c7d6b20d 100644 --- a/web/i18n/ja-JP/common.ts +++ b/web/i18n/ja-JP/common.ts @@ -657,6 +657,7 @@ const translation = { hitScore: '検索スコア:', }, inputPlaceholder: '{{botName}} と話す', + inputDisabledPlaceholder: 'プレビューのみ', thought: '思考', thinking: '考え中...', resend: '再送信してください', diff --git a/web/i18n/ja-JP/explore.ts b/web/i18n/ja-JP/explore.ts index 09a0748f08..2639bfc1dd 100644 --- a/web/i18n/ja-JP/explore.ts +++ b/web/i18n/ja-JP/explore.ts @@ -1,9 +1,9 @@ const translation = { title: '探索', sidebar: { - discovery: '探索', + title: 'アプリギャラリー', chat: 'チャット', - workspace: 'ワークスペース', + webApps: 'Webアプリ', action: { pin: 'ピン留め', unpin: 'ピン留め解除', @@ -14,16 +14,33 @@ const translation = { title: 'アプリを削除', content: 'このアプリを削除してもよろしいですか?', }, + noApps: { + title: 'Webアプリなし', + description: '公開されたWebアプリがここに表示されます', + learnMore: '詳細', + }, }, apps: { - title: 'アプリを探索', - description: 'これらのテンプレートアプリを即座に使用するか、テンプレートに基づいて独自のアプリをカスタマイズしてください。', - allCategories: '推奨', + title: 'Difyの厳選アプリを試して、ビジネス向けのAIソリューションを見つけましょう', + allCategories: '全て', + resultNum: '{{num}}件の結果', + resetFilter: 'クリア', }, appCard: { - addToWorkspace: 'ワークスペースに追加', + addToWorkspace: 'テンプレートを使用', + try: '詳細', customize: 'カスタマイズ', }, + tryApp: { + tabHeader: { + try: 'お試し', + detail: 'オーケストレーション詳細', + }, + createFromSampleApp: 'テンプレートから作成', + category: 'カテゴリー', + requirements: '必要項目', + tryInfo: 'これはサンプルアプリです。最大5件のメッセージまでお試しいただけます。引き続き利用するには、「テンプレートから作成」 をクリックして設定を行ってください。', + }, appCustomize: { title: '{{name}}からアプリを作成', subTitle: 'アプリアイコンと名前', @@ -39,6 +56,9 @@ const translation = { Agent: 'エージェント', Entertainment: 'エンターテイメント', }, + banner: { + viewMore: 'もっと見る', + }, } export default translation diff --git a/web/i18n/ko-KR/explore.ts b/web/i18n/ko-KR/explore.ts index bc6438af2b..756849b374 100644 --- a/web/i18n/ko-KR/explore.ts +++ b/web/i18n/ko-KR/explore.ts @@ -1,9 +1,7 @@ const translation = { title: '탐색', sidebar: { - discovery: '탐색', chat: '채팅', - workspace: '작업 공간', action: { pin: '고정', unpin: '고정 해제', @@ -16,12 +14,8 @@ const translation = { }, }, apps: { - title: 'Dify 로 앱 탐색', - description: '이 템플릿 앱을 즉시 사용하거나 템플릿을 기반으로 고유한 앱을 사용자 정의하세요.', - allCategories: '모든 카테고리', }, appCard: { - addToWorkspace: '작업 공간에 추가', customize: '사용자 정의', }, appCustomize: { diff --git a/web/i18n/pl-PL/explore.ts b/web/i18n/pl-PL/explore.ts index f9e8b30f8b..864dee6f49 100644 --- a/web/i18n/pl-PL/explore.ts +++ b/web/i18n/pl-PL/explore.ts @@ -1,9 +1,7 @@ const translation = { title: 'Odkryj', sidebar: { - discovery: 'Odkrywanie', chat: 'Czat', - workspace: 'Przestrzeń robocza', action: { pin: 'Przypnij', unpin: 'Odepnij', @@ -16,13 +14,8 @@ const translation = { }, }, apps: { - title: 'Odkrywaj aplikacje stworzone przez Dify', - description: - 'Wykorzystaj te aplikacje szablonowe natychmiast lub dostosuj własne aplikacje na podstawie szablonów.', - allCategories: 'Polecane', }, appCard: { - addToWorkspace: 'Dodaj do przestrzeni roboczej', customize: 'Dostosuj', }, appCustomize: { diff --git a/web/i18n/pt-BR/explore.ts b/web/i18n/pt-BR/explore.ts index 2a15d07f95..5bd24bb581 100644 --- a/web/i18n/pt-BR/explore.ts +++ b/web/i18n/pt-BR/explore.ts @@ -1,9 +1,7 @@ const translation = { title: 'Badać', sidebar: { - discovery: 'Descoberta', chat: 'Chat', - workspace: 'Espaço de Trabalho', action: { pin: 'Fixar', unpin: 'Desafixar', @@ -16,12 +14,8 @@ const translation = { }, }, apps: { - title: 'Explorar Aplicações por Dify', - description: 'Use esses aplicativos modelo instantaneamente ou personalize seus próprios aplicativos com base nos modelos.', - allCategories: 'Recomendado', }, appCard: { - addToWorkspace: 'Adicionar ao Espaço de Trabalho', customize: 'Personalizar', }, appCustomize: { diff --git a/web/i18n/ro-RO/explore.ts b/web/i18n/ro-RO/explore.ts index 153b236200..918713bc90 100644 --- a/web/i18n/ro-RO/explore.ts +++ b/web/i18n/ro-RO/explore.ts @@ -1,9 +1,7 @@ const translation = { title: 'Explorați', sidebar: { - discovery: 'Descoperire', chat: 'Chat', - workspace: 'Spațiu de lucru', action: { pin: 'Fixați', unpin: 'Deblocați', @@ -16,12 +14,8 @@ const translation = { }, }, apps: { - title: 'Explorați aplicațiile Dify', - description: 'Utilizați aceste aplicații model imediat sau personalizați-vă propria aplicație pe baza modelelor.', - allCategories: 'Recomandate', }, appCard: { - addToWorkspace: 'Adăugați la spațiul de lucru', customize: 'Personalizați', }, appCustomize: { diff --git a/web/i18n/ru-RU/explore.ts b/web/i18n/ru-RU/explore.ts index 919d1e49d8..fd23926d7b 100644 --- a/web/i18n/ru-RU/explore.ts +++ b/web/i18n/ru-RU/explore.ts @@ -1,9 +1,7 @@ const translation = { title: 'Обзор', sidebar: { - discovery: 'Открытия', chat: 'Чат', - workspace: 'Рабочее пространство', action: { pin: 'Закрепить', unpin: 'Открепить', @@ -16,12 +14,8 @@ const translation = { }, }, apps: { - title: 'Обзор приложений от Dify', - description: 'Используйте эти шаблонные приложения мгновенно или настройте свои собственные приложения на основе шаблонов.', - allCategories: 'Рекомендуемые', }, appCard: { - addToWorkspace: 'Добавить в рабочее пространство', customize: 'Настроить', }, appCustomize: { diff --git a/web/i18n/sl-SI/explore.ts b/web/i18n/sl-SI/explore.ts index add905631d..ae25382b46 100644 --- a/web/i18n/sl-SI/explore.ts +++ b/web/i18n/sl-SI/explore.ts @@ -1,9 +1,7 @@ const translation = { title: 'Razišči', sidebar: { - discovery: 'Odkritja', chat: 'Klepet', - workspace: 'Delovni prostor', action: { pin: 'Pripni', unpin: 'Odpni', @@ -16,12 +14,8 @@ const translation = { }, }, apps: { - title: 'Razišči aplikacije Dify', - description: 'Uporabite te predloge aplikacij takoj ali prilagodite svoje aplikacije na podlagi predlog.', - allCategories: 'Priporočeno', }, appCard: { - addToWorkspace: 'Dodaj v delovni prostor', customize: 'Prilagodi', }, appCustomize: { diff --git a/web/i18n/th-TH/explore.ts b/web/i18n/th-TH/explore.ts index d8eb53d194..239d1e7182 100644 --- a/web/i18n/th-TH/explore.ts +++ b/web/i18n/th-TH/explore.ts @@ -1,9 +1,7 @@ const translation = { title: 'สํารวจ', sidebar: { - discovery: 'การค้นพบ', chat: 'สนทนา', - workspace: 'พื้นที่', action: { pin: 'เข็มกลัด', unpin: 'ปลดหมุด', @@ -16,12 +14,8 @@ const translation = { }, }, apps: { - title: 'สํารวจแอพโดย Dify', - description: 'ใช้แอปเทมเพลตเหล่านี้ทันทีหรือปรับแต่งแอปของคุณเองตามเทมเพลต', - allCategories: 'แนะ นำ', }, appCard: { - addToWorkspace: 'เพิ่มไปยังพื้นที่ทํางาน', customize: 'ปรับแต่ง', }, appCustomize: { diff --git a/web/i18n/tr-TR/explore.ts b/web/i18n/tr-TR/explore.ts index 78b305ee47..b2e3a48e7b 100644 --- a/web/i18n/tr-TR/explore.ts +++ b/web/i18n/tr-TR/explore.ts @@ -1,9 +1,7 @@ const translation = { title: 'Keşfet', sidebar: { - discovery: 'Keşif', chat: 'Sohbet', - workspace: 'Çalışma Alanı', action: { pin: 'Sabitle', unpin: 'Sabitlemeyi Kaldır', @@ -16,12 +14,8 @@ const translation = { }, }, apps: { - title: 'Dify Tarafından Keşfet Uygulamaları', - description: 'Bu şablon uygulamalarını anında kullanın veya şablonlara dayalı kendi uygulamalarınızı özelleştirin.', - allCategories: 'Önerilen', }, appCard: { - addToWorkspace: 'Çalışma Alanına Ekle', customize: 'Özelleştir', }, appCustomize: { diff --git a/web/i18n/uk-UA/explore.ts b/web/i18n/uk-UA/explore.ts index 0bb03af719..eb6adae23a 100644 --- a/web/i18n/uk-UA/explore.ts +++ b/web/i18n/uk-UA/explore.ts @@ -1,9 +1,7 @@ const translation = { title: 'Досліджувати', sidebar: { - discovery: 'Відкриття', chat: 'Чат', - workspace: 'Робочий простір', action: { pin: 'Закріпити', unpin: 'Відкріпити', @@ -16,12 +14,8 @@ const translation = { }, }, apps: { - title: 'Вивчайте програми від Dify', - description: 'Використовуйте ці шаблони миттєво або налаштуйте власні програми на основі шаблонів.', - allCategories: 'Рекомендовані', }, appCard: { - addToWorkspace: 'Додати до робочого простору', customize: 'Налаштувати', }, appCustomize: { diff --git a/web/i18n/vi-VN/explore.ts b/web/i18n/vi-VN/explore.ts index 860bfd76f0..ed27f42b32 100644 --- a/web/i18n/vi-VN/explore.ts +++ b/web/i18n/vi-VN/explore.ts @@ -1,9 +1,7 @@ const translation = { title: 'Khám phá', sidebar: { - discovery: 'Khám phá', chat: 'Trò chuyện', - workspace: 'Không gian làm việc', action: { pin: 'Ghim', unpin: 'Bỏ ghim', @@ -16,12 +14,8 @@ const translation = { }, }, apps: { - title: 'Khám phá ứng dụng bởi Dify', - description: 'Sử dụng ngay các ứng dụng mẫu này hoặc tùy chỉnh ứng dụng của bạn dựa trên các mẫu có sẵn.', - allCategories: 'Tất cả danh mục', }, appCard: { - addToWorkspace: 'Thêm vào không gian làm việc', customize: 'Tùy chỉnh', }, appCustomize: { diff --git a/web/i18n/zh-Hans/common.ts b/web/i18n/zh-Hans/common.ts index 015835462c..c934e1203b 100644 --- a/web/i18n/zh-Hans/common.ts +++ b/web/i18n/zh-Hans/common.ts @@ -660,6 +660,7 @@ const translation = { hitScore: '召回得分:', }, inputPlaceholder: '和 {{botName}} 聊天', + inputDisabledPlaceholder: '仅供试用', thinking: '深度思考中...', thought: '已深度思考', resend: '重新发送', diff --git a/web/i18n/zh-Hans/explore.ts b/web/i18n/zh-Hans/explore.ts index 7f16cd32f2..2080033904 100644 --- a/web/i18n/zh-Hans/explore.ts +++ b/web/i18n/zh-Hans/explore.ts @@ -1,9 +1,9 @@ const translation = { title: '探索', sidebar: { - discovery: '发现', + title: '应用库', chat: '智聊', - workspace: '工作区', + webApps: 'WEB APPS', action: { pin: '置顶', unpin: '取消置顶', @@ -14,16 +14,33 @@ const translation = { title: '删除程序', content: '您确定要删除此程序吗?', }, + noApps: { + title: '没有 web apps', + description: '已发布的 web apps 将出现在此处', + learnMore: '了解更多', + }, }, apps: { - title: '探索应用', - description: '使用这些模板应用程序,或根据模板自定义您自己的应用程序。', - allCategories: '推荐', + title: '试用 Dify 精选示例应用,为您的业务寻找 AI 解决方案', + allCategories: '所有', + resultNum: '{{num}} 个结果', + resetFilter: '清除筛选', }, appCard: { - addToWorkspace: '添加到工作区', + addToWorkspace: '使用模板', + try: '详情', customize: '自定义', }, + tryApp: { + tabHeader: { + try: '试用', + detail: '编排详情', + }, + createFromSampleApp: '从此模板创建应用', + category: '分类', + requirements: '必须配置项', + tryInfo: '这是一个示例应用,您可以试用最多 5 条消息。如需继续使用,请点击 “从此模板创建应用” 并完成配置!', + }, appCustomize: { title: '从 {{name}} 创建应用程序', subTitle: '应用程序图标和名称', @@ -39,6 +56,9 @@ const translation = { Workflow: '工作流', Entertainment: '娱乐', }, + banner: { + viewMore: '查看更多', + }, } export default translation diff --git a/web/i18n/zh-Hant/explore.ts b/web/i18n/zh-Hant/explore.ts index 7ff61a39bc..cbb20f0e77 100644 --- a/web/i18n/zh-Hant/explore.ts +++ b/web/i18n/zh-Hant/explore.ts @@ -1,9 +1,7 @@ const translation = { title: '探索', sidebar: { - discovery: '發現', chat: '智聊', - workspace: '工作區', action: { pin: '置頂', unpin: '取消置頂', @@ -16,12 +14,8 @@ const translation = { }, }, apps: { - title: '探索應用', - description: '使用這些模板應用程式,或根據模板自定義您自己的應用程式。', - allCategories: '推薦', }, appCard: { - addToWorkspace: '新增到工作區', customize: '自定義', }, appCustomize: { diff --git a/web/models/debug.ts b/web/models/debug.ts index 630c48a970..90995e72dc 100644 --- a/web/models/debug.ts +++ b/web/models/debug.ts @@ -132,6 +132,9 @@ export type ModelConfig = { provider: string // LLM Provider: for example "OPENAI" model_id: string mode: ModelModeType + prompt_type?: PromptMode + chat_prompt_config?: ChatPromptConfig | null + completion_prompt_config?: CompletionPromptConfig | null configs: PromptConfig opening_statement: string | null more_like_this: MoreLikeThisConfig | null diff --git a/web/models/explore.ts b/web/models/explore.ts index ad243e931e..735aa54eab 100644 --- a/web/models/explore.ts +++ b/web/models/explore.ts @@ -27,6 +27,7 @@ export type App = { installed: boolean editable: boolean is_agent: boolean + can_trial: boolean } export type InstalledApp = { diff --git a/web/package.json b/web/package.json index 8f4f28772f..f646da4fed 100644 --- a/web/package.json +++ b/web/package.json @@ -79,6 +79,8 @@ "echarts": "^5.6.0", "echarts-for-react": "^3.0.2", "elkjs": "^0.9.3", + "embla-carousel-autoplay": "^8.6.0", + "embla-carousel-react": "^8.6.0", "emoji-mart": "^5.6.0", "fast-deep-equal": "^3.1.3", "html-to-image": "1.11.13", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 083d518a5c..28c5a1ad85 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -165,6 +165,12 @@ importers: elkjs: specifier: ^0.9.3 version: 0.9.3 + embla-carousel-autoplay: + specifier: ^8.6.0 + version: 8.6.0(embla-carousel@8.6.0) + embla-carousel-react: + specifier: ^8.6.0 + version: 8.6.0(react@19.1.1) emoji-mart: specifier: ^5.6.0 version: 5.6.0 @@ -4770,6 +4776,24 @@ packages: elliptic@6.6.1: resolution: {integrity: sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==} + embla-carousel-autoplay@8.6.0: + resolution: {integrity: sha512-OBu5G3nwaSXkZCo1A6LTaFMZ8EpkYbwIaH+bPqdBnDGQ2fh4+NbzjXjs2SktoPNKCtflfVMc75njaDHOYXcrsA==} + peerDependencies: + embla-carousel: 8.6.0 + + embla-carousel-react@8.6.0: + resolution: {integrity: sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==} + peerDependencies: + react: ^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + embla-carousel-reactive-utils@8.6.0: + resolution: {integrity: sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A==} + peerDependencies: + embla-carousel: 8.6.0 + + embla-carousel@8.6.0: + resolution: {integrity: sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==} + emittery@0.13.1: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} engines: {node: '>=12'} @@ -13496,6 +13520,22 @@ snapshots: minimalistic-assert: 1.0.1 minimalistic-crypto-utils: 1.0.1 + embla-carousel-autoplay@8.6.0(embla-carousel@8.6.0): + dependencies: + embla-carousel: 8.6.0 + + embla-carousel-react@8.6.0(react@19.1.1): + dependencies: + embla-carousel: 8.6.0 + embla-carousel-reactive-utils: 8.6.0(embla-carousel@8.6.0) + react: 19.1.1 + + embla-carousel-reactive-utils@8.6.0(embla-carousel@8.6.0): + dependencies: + embla-carousel: 8.6.0 + + embla-carousel@8.6.0: {} + emittery@0.13.1: {} emoji-mart@5.6.0: {} diff --git a/web/service/debug.ts b/web/service/debug.ts index fab2910c5e..61057c591f 100644 --- a/web/service/debug.ts +++ b/web/service/debug.ts @@ -1,5 +1,5 @@ import { get, post, ssePost } from './base' -import type { IOnCompleted, IOnData, IOnError, IOnFile, IOnMessageEnd, IOnMessageReplace, IOnThought } from './base' +import type { IOnCompleted, IOnData, IOnError, IOnMessageReplace } from './base' import type { ChatPromptConfig, CompletionPromptConfig } from '@/models/debug' import type { ModelModeType } from '@/types/app' import type { ModelParameterRule } from '@/app/components/header/account-setting/model-provider-page/declarations' @@ -24,24 +24,6 @@ export type CodeGenRes = { error?: string } -export const sendChatMessage = async (appId: string, body: Record, { onData, onCompleted, onThought, onFile, onError, getAbortController, onMessageEnd, onMessageReplace }: { - onData: IOnData - onCompleted: IOnCompleted - onFile: IOnFile - onThought: IOnThought - onMessageEnd: IOnMessageEnd - onMessageReplace: IOnMessageReplace - onError: IOnError - getAbortController?: (abortController: AbortController) => void -}) => { - return ssePost(`apps/${appId}/chat-messages`, { - body: { - ...body, - response_mode: 'streaming', - }, - }, { onData, onCompleted, onThought, onFile, onError, getAbortController, onMessageEnd, onMessageReplace }) -} - export const stopChatMessageResponding = async (appId: string, taskId: string) => { return post(`apps/${appId}/chat-messages/${taskId}/stop`) } diff --git a/web/service/explore.ts b/web/service/explore.ts index 6a440d7f5d..98acda346c 100644 --- a/web/service/explore.ts +++ b/web/service/explore.ts @@ -44,3 +44,8 @@ export const getToolProviders = () => { export const getAppAccessModeByAppId = (appId: string) => { return get<{ accessMode: AccessMode }>(`/enterprise/webapp/app/access-mode?appId=${appId}`) } + +export const fetchBanners = (language?: string): Promise => { + const url = language ? `/explore/banners?language=${language}` : '/explore/banners' + return get(url) +} diff --git a/web/service/share.ts b/web/service/share.ts index b19dbc896d..fe2b9c2290 100644 --- a/web/service/share.ts +++ b/web/service/share.ts @@ -2,22 +2,17 @@ import type { IOnCompleted, IOnData, IOnError, - IOnFile, IOnIterationFinished, IOnIterationNext, IOnIterationStarted, IOnLoopFinished, IOnLoopNext, IOnLoopStarted, - IOnMessageEnd, IOnMessageReplace, IOnNodeFinished, IOnNodeStarted, - IOnTTSChunk, - IOnTTSEnd, IOnTextChunk, IOnTextReplace, - IOnThought, IOnWorkflowFinished, IOnWorkflowStarted, } from './base' @@ -37,45 +32,43 @@ import type { AccessMode } from '@/models/access-control' import { WEB_APP_SHARE_CODE_HEADER_NAME } from '@/config' import { getWebAppAccessToken } from './webapp-auth' -function getAction(action: 'get' | 'post' | 'del' | 'patch', isInstalledApp: boolean) { +export enum AppSourceType { + webApp = 'webApp', + installedApp = 'installedApp', + tryApp = 'tryApp', +} + +const apiPrefix = { + [AppSourceType.webApp]: '', + [AppSourceType.installedApp]: 'installed-apps', + [AppSourceType.tryApp]: 'trial-apps', +} + +function getIsPublicAPI(appSourceType: AppSourceType) { + return appSourceType === AppSourceType.webApp +} + +function getAction(action: 'get' | 'post' | 'del' | 'patch', appSourceType: AppSourceType) { + const isNeedLogin = !getIsPublicAPI(appSourceType) switch (action) { case 'get': - return isInstalledApp ? consoleGet : get + return isNeedLogin ? consoleGet : get case 'post': - return isInstalledApp ? consolePost : post + return isNeedLogin ? consolePost : post case 'patch': - return isInstalledApp ? consolePatch : patch + return isNeedLogin ? consolePatch : patch case 'del': - return isInstalledApp ? consoleDel : del + return isNeedLogin ? consoleDel : del } } -export function getUrl(url: string, isInstalledApp: boolean, installedAppId: string) { - return isInstalledApp ? `installed-apps/${installedAppId}/${url.startsWith('/') ? url.slice(1) : url}` : url +export function getUrl(url: string, appSourceType: AppSourceType, appId: string) { + const hasPrefix = appSourceType !== AppSourceType.webApp + return hasPrefix ? `${apiPrefix[appSourceType]}/${appId}/${url.startsWith('/') ? url.slice(1) : url}` : url } -export const sendChatMessage = async (body: Record, { onData, onCompleted, onThought, onFile, onError, getAbortController, onMessageEnd, onMessageReplace, onTTSChunk, onTTSEnd }: { - onData: IOnData - onCompleted: IOnCompleted - onFile: IOnFile - onThought: IOnThought - onError: IOnError - onMessageEnd?: IOnMessageEnd - onMessageReplace?: IOnMessageReplace - getAbortController?: (abortController: AbortController) => void - onTTSChunk?: IOnTTSChunk - onTTSEnd?: IOnTTSEnd -}, isInstalledApp: boolean, installedAppId = '') => { - return ssePost(getUrl('chat-messages', isInstalledApp, installedAppId), { - body: { - ...body, - response_mode: 'streaming', - }, - }, { onData, onCompleted, onThought, onFile, isPublicAPI: !isInstalledApp, onError, getAbortController, onMessageEnd, onMessageReplace, onTTSChunk, onTTSEnd }) -} - -export const stopChatMessageResponding = async (appId: string, taskId: string, isInstalledApp: boolean, installedAppId = '') => { - return getAction('post', isInstalledApp)(getUrl(`chat-messages/${taskId}/stop`, isInstalledApp, installedAppId)) +export const stopChatMessageResponding = async (appId: string, taskId: string, appSourceType: AppSourceType, installedAppId = '') => { + return getAction('post', appSourceType)(getUrl(`chat-messages/${taskId}/stop`, appSourceType, installedAppId)) } export const sendCompletionMessage = async (body: Record, { onData, onCompleted, onError, onMessageReplace }: { @@ -83,13 +76,13 @@ export const sendCompletionMessage = async (body: Record, { onData, onCompleted: IOnCompleted onError: IOnError onMessageReplace: IOnMessageReplace -}, isInstalledApp: boolean, installedAppId = '') => { - return ssePost(getUrl('completion-messages', isInstalledApp, installedAppId), { +}, appSourceType: AppSourceType, installedAppId = '') => { + return ssePost(getUrl('completion-messages', appSourceType, installedAppId), { body: { ...body, response_mode: 'streaming', }, - }, { onData, onCompleted, isPublicAPI: !isInstalledApp, onError, onMessageReplace }) + }, { onData, onCompleted, isPublicAPI: !appSourceType, onError, onMessageReplace }) } export const sendWorkflowMessage = async ( @@ -121,10 +114,10 @@ export const sendWorkflowMessage = async ( onTextChunk: IOnTextChunk onTextReplace: IOnTextReplace }, - isInstalledApp: boolean, - installedAppId = '', + appSourceType: AppSourceType, + appId = '', ) => { - return ssePost(getUrl('workflows/run', isInstalledApp, installedAppId), { + return ssePost(getUrl('workflows/run', appSourceType, appId), { body: { ...body, response_mode: 'streaming', @@ -133,7 +126,7 @@ export const sendWorkflowMessage = async ( onNodeStarted, onWorkflowStarted, onWorkflowFinished, - isPublicAPI: !isInstalledApp, + isPublicAPI: getIsPublicAPI(appSourceType), onNodeFinished, onIterationStart, onIterationNext, @@ -150,32 +143,32 @@ export const fetchAppInfo = async () => { return get('/site') as Promise } -export const fetchConversations = async (isInstalledApp: boolean, installedAppId = '', last_id?: string, pinned?: boolean, limit?: number) => { - return getAction('get', isInstalledApp)(getUrl('conversations', isInstalledApp, installedAppId), { params: { limit: limit || 20, ...(last_id ? { last_id } : {}), ...(pinned !== undefined ? { pinned } : {}) } }) as Promise +export const fetchConversations = async (appSourceType: AppSourceType, installedAppId = '', last_id?: string, pinned?: boolean, limit?: number) => { + return getAction('get', appSourceType)(getUrl('conversations', appSourceType, installedAppId), { params: { limit: limit || 20, ...(last_id ? { last_id } : {}), ...(pinned !== undefined ? { pinned } : {}) } }) as Promise } -export const pinConversation = async (isInstalledApp: boolean, installedAppId = '', id: string) => { - return getAction('patch', isInstalledApp)(getUrl(`conversations/${id}/pin`, isInstalledApp, installedAppId)) +export const pinConversation = async (appSourceType: AppSourceType, installedAppId = '', id: string) => { + return getAction('patch', appSourceType)(getUrl(`conversations/${id}/pin`, appSourceType, installedAppId)) } -export const unpinConversation = async (isInstalledApp: boolean, installedAppId = '', id: string) => { - return getAction('patch', isInstalledApp)(getUrl(`conversations/${id}/unpin`, isInstalledApp, installedAppId)) +export const unpinConversation = async (appSourceType: AppSourceType, installedAppId = '', id: string) => { + return getAction('patch', appSourceType)(getUrl(`conversations/${id}/unpin`, appSourceType, installedAppId)) } -export const delConversation = async (isInstalledApp: boolean, installedAppId = '', id: string) => { - return getAction('del', isInstalledApp)(getUrl(`conversations/${id}`, isInstalledApp, installedAppId)) +export const delConversation = async (appSourceType: AppSourceType, installedAppId = '', id: string) => { + return getAction('del', appSourceType)(getUrl(`conversations/${id}`, appSourceType, installedAppId)) } -export const renameConversation = async (isInstalledApp: boolean, installedAppId = '', id: string, name: string) => { - return getAction('post', isInstalledApp)(getUrl(`conversations/${id}/name`, isInstalledApp, installedAppId), { body: { name } }) +export const renameConversation = async (appSourceType: AppSourceType, installedAppId = '', id: string, name: string) => { + return getAction('post', appSourceType)(getUrl(`conversations/${id}/name`, appSourceType, installedAppId), { body: { name } }) } -export const generationConversationName = async (isInstalledApp: boolean, installedAppId = '', id: string) => { - return getAction('post', isInstalledApp)(getUrl(`conversations/${id}/name`, isInstalledApp, installedAppId), { body: { auto_generate: true } }) as Promise +export const generationConversationName = async (appSourceType: AppSourceType, installedAppId = '', id: string) => { + return getAction('post', appSourceType)(getUrl(`conversations/${id}/name`, appSourceType, installedAppId), { body: { auto_generate: true } }) as Promise } -export const fetchChatList = async (conversationId: string, isInstalledApp: boolean, installedAppId = '') => { - return getAction('get', isInstalledApp)(getUrl('messages', isInstalledApp, installedAppId), { params: { conversation_id: conversationId, limit: 20, last_id: '' } }) as any +export const fetchChatList = async (conversationId: string, appSourceType: AppSourceType, installedAppId = '') => { + return getAction('get', appSourceType)(getUrl('messages', appSourceType, installedAppId), { params: { conversation_id: conversationId, limit: 20, last_id: '' } }) as any } // Abandoned API interface @@ -184,12 +177,12 @@ export const fetchChatList = async (conversationId: string, isInstalledApp: bool // } // init value. wait for server update -export const fetchAppParams = async (isInstalledApp: boolean, installedAppId = '') => { - return (getAction('get', isInstalledApp))(getUrl('parameters', isInstalledApp, installedAppId)) as Promise +export const fetchAppParams = async (appSourceType: AppSourceType, appId = '') => { + return (getAction('get', appSourceType))(getUrl('parameters', appSourceType, appId)) as Promise } export const fetchWebSAMLSSOUrl = async (appCode: string, redirectUrl: string) => { - return (getAction('get', false))(getUrl('/enterprise/sso/saml/login', false, ''), { + return (getAction('get', AppSourceType.webApp))(getUrl('/enterprise/sso/saml/login', AppSourceType.webApp, ''), { params: { app_code: appCode, redirect_url: redirectUrl, @@ -198,7 +191,7 @@ export const fetchWebSAMLSSOUrl = async (appCode: string, redirectUrl: string) = } export const fetchWebOIDCSSOUrl = async (appCode: string, redirectUrl: string) => { - return (getAction('get', false))(getUrl('/enterprise/sso/oidc/login', false, ''), { + return (getAction('get', AppSourceType.webApp))(getUrl('/enterprise/sso/oidc/login', AppSourceType.webApp, ''), { params: { app_code: appCode, redirect_url: redirectUrl, @@ -208,7 +201,7 @@ export const fetchWebOIDCSSOUrl = async (appCode: string, redirectUrl: string) = } export const fetchWebOAuth2SSOUrl = async (appCode: string, redirectUrl: string) => { - return (getAction('get', false))(getUrl('/enterprise/sso/oauth2/login', false, ''), { + return (getAction('get', AppSourceType.webApp))(getUrl('/enterprise/sso/oauth2/login', AppSourceType.webApp, ''), { params: { app_code: appCode, redirect_url: redirectUrl, @@ -217,7 +210,7 @@ export const fetchWebOAuth2SSOUrl = async (appCode: string, redirectUrl: string) } export const fetchMembersSAMLSSOUrl = async (appCode: string, redirectUrl: string) => { - return (getAction('get', false))(getUrl('/enterprise/sso/members/saml/login', false, ''), { + return (getAction('get', AppSourceType.webApp))(getUrl('/enterprise/sso/members/saml/login', AppSourceType.webApp, ''), { params: { app_code: appCode, redirect_url: redirectUrl, @@ -226,7 +219,7 @@ export const fetchMembersSAMLSSOUrl = async (appCode: string, redirectUrl: strin } export const fetchMembersOIDCSSOUrl = async (appCode: string, redirectUrl: string) => { - return (getAction('get', false))(getUrl('/enterprise/sso/members/oidc/login', false, ''), { + return (getAction('get', AppSourceType.webApp))(getUrl('/enterprise/sso/members/oidc/login', AppSourceType.webApp, ''), { params: { app_code: appCode, redirect_url: redirectUrl, @@ -236,7 +229,7 @@ export const fetchMembersOIDCSSOUrl = async (appCode: string, redirectUrl: strin } export const fetchMembersOAuth2SSOUrl = async (appCode: string, redirectUrl: string) => { - return (getAction('get', false))(getUrl('/enterprise/sso/members/oauth2/login', false, ''), { + return (getAction('get', AppSourceType.webApp))(getUrl('/enterprise/sso/members/oauth2/login', AppSourceType.webApp, ''), { params: { app_code: appCode, redirect_url: redirectUrl, @@ -244,48 +237,44 @@ export const fetchMembersOAuth2SSOUrl = async (appCode: string, redirectUrl: str }) as Promise<{ url: string }> } -export const fetchAppMeta = async (isInstalledApp: boolean, installedAppId = '') => { - return (getAction('get', isInstalledApp))(getUrl('meta', isInstalledApp, installedAppId)) as Promise +export const fetchAppMeta = async (appSourceType: AppSourceType, installedAppId = '') => { + return (getAction('get', appSourceType))(getUrl('meta', appSourceType, installedAppId)) as Promise } -export const updateFeedback = async ({ url, body }: { url: string; body: FeedbackType }, isInstalledApp: boolean, installedAppId = '') => { - return (getAction('post', isInstalledApp))(getUrl(url, isInstalledApp, installedAppId), { body }) +export const updateFeedback = async ({ url, body }: { url: string; body: FeedbackType }, appSourceType: AppSourceType, installedAppId = '') => { + return (getAction('post', appSourceType))(getUrl(url, appSourceType, installedAppId), { body }) } -export const fetchMoreLikeThis = async (messageId: string, isInstalledApp: boolean, installedAppId = '') => { - return (getAction('get', isInstalledApp))(getUrl(`/messages/${messageId}/more-like-this`, isInstalledApp, installedAppId), { +export const fetchMoreLikeThis = async (messageId: string, appSourceType: AppSourceType, installedAppId = '') => { + return (getAction('get', appSourceType))(getUrl(`/messages/${messageId}/more-like-this`, appSourceType, installedAppId), { params: { response_mode: 'blocking', }, }) } -export const saveMessage = (messageId: string, isInstalledApp: boolean, installedAppId = '') => { - return (getAction('post', isInstalledApp))(getUrl('/saved-messages', isInstalledApp, installedAppId), { body: { message_id: messageId } }) +export const saveMessage = (messageId: string, appSourceType: AppSourceType, installedAppId = '') => { + return (getAction('post', appSourceType))(getUrl('/saved-messages', appSourceType, installedAppId), { body: { message_id: messageId } }) } -export const fetchSavedMessage = async (isInstalledApp: boolean, installedAppId = '') => { - return (getAction('get', isInstalledApp))(getUrl('/saved-messages', isInstalledApp, installedAppId)) +export const fetchSavedMessage = async (appSourceType: AppSourceType, installedAppId = '') => { + return (getAction('get', appSourceType))(getUrl('/saved-messages', appSourceType, installedAppId)) } -export const removeMessage = (messageId: string, isInstalledApp: boolean, installedAppId = '') => { - return (getAction('del', isInstalledApp))(getUrl(`/saved-messages/${messageId}`, isInstalledApp, installedAppId)) +export const removeMessage = (messageId: string, appSourceType: AppSourceType, installedAppId = '') => { + return (getAction('del', appSourceType))(getUrl(`/saved-messages/${messageId}`, appSourceType, installedAppId)) } -export const fetchSuggestedQuestions = (messageId: string, isInstalledApp: boolean, installedAppId = '') => { - return (getAction('get', isInstalledApp))(getUrl(`/messages/${messageId}/suggested-questions`, isInstalledApp, installedAppId)) +export const fetchSuggestedQuestions = (messageId: string, appSourceType: AppSourceType, installedAppId = '') => { + return (getAction('get', appSourceType))(getUrl(`/messages/${messageId}/suggested-questions`, appSourceType, installedAppId)) } -export const audioToText = (url: string, isPublicAPI: boolean, body: FormData) => { - return (getAction('post', !isPublicAPI))(url, { body }, { bodyStringify: false, deleteContentType: true }) as Promise<{ text: string }> +export const audioToText = (url: string, appSourceType: AppSourceType, body: FormData) => { + return (getAction('post', appSourceType))(url, { body }, { bodyStringify: false, deleteContentType: true }) as Promise<{ text: string }> } -export const textToAudio = (url: string, isPublicAPI: boolean, body: FormData) => { - return (getAction('post', !isPublicAPI))(url, { body }, { bodyStringify: false, deleteContentType: true }) as Promise<{ data: string }> -} - -export const textToAudioStream = (url: string, isPublicAPI: boolean, header: { content_type: string }, body: { streaming: boolean; voice?: string; message_id?: string; text?: string | null | undefined }) => { - return (getAction('post', !isPublicAPI))(url, { body, header }, { needAllResponseContent: true }) +export const textToAudioStream = (url: string, appSourceType: AppSourceType, header: { content_type: string }, body: { streaming: boolean; voice?: string; message_id?: string; text?: string | null | undefined }) => { + return (getAction('post', appSourceType))(url, { body, header }, { needAllResponseContent: true }) } export const fetchAccessToken = async ({ userId, appCode }: { userId?: string, appCode: string }) => { diff --git a/web/service/try-app.ts b/web/service/try-app.ts new file mode 100644 index 0000000000..17624c7a1c --- /dev/null +++ b/web/service/try-app.ts @@ -0,0 +1,42 @@ +import type { AppMode } from '@/types/app' +import { + get, +} from './base' +import type { + SiteInfo, +} from '@/models/share' +import type { ModelConfig } from '@/types/app' +import qs from 'qs' +import type { DataSetListResponse } from '@/models/datasets' +import type { Edge, Node } from '@/app/components/workflow/types' +import type { Viewport } from 'reactflow' + +export type TryAppInfo = { + name: string + description: string + mode: AppMode + site: SiteInfo + model_config: ModelConfig + deleted_tools: any[] +} + +export const fetchTryAppInfo = async (appId: string) => { + return get(`/trial-apps/${appId}`) as Promise +} + +export const fetchTryAppDatasets = (appId: string, ids: string[]) => { + const urlParams = qs.stringify({ ids }, { indices: false }) + return get(`/trial-apps/${appId}/datasets?${urlParams}`) +} + +type TryAppFlowPreview = { + graph: { + nodes: Node[] + edges: Edge[] + viewport: Viewport + } +} + +export const fetchTryAppFlowPreview = (appId: string) => { + return get(`/trial-apps/${appId}/workflows`) +} diff --git a/web/service/use-explore.ts b/web/service/use-explore.ts index b7d078edbc..f1a441f577 100644 --- a/web/service/use-explore.ts +++ b/web/service/use-explore.ts @@ -1,8 +1,8 @@ import { useGlobalPublicStore } from '@/context/global-public-context' import { AccessMode } from '@/models/access-control' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import { fetchInstalledAppList, getAppAccessModeByAppId, uninstallApp, updatePinStatus } from './explore' -import { fetchAppMeta, fetchAppParams } from './share' +import { fetchBanners, fetchInstalledAppList, getAppAccessModeByAppId, uninstallApp, updatePinStatus } from './explore' +import { AppSourceType, fetchAppMeta, fetchAppParams } from './share' const NAME_SPACE = 'explore' @@ -62,7 +62,7 @@ export const useGetInstalledAppParams = (appId: string | null) => { queryFn: () => { if (!appId || appId.length === 0) return Promise.reject(new Error('App ID is required to get app params')) - return fetchAppParams(true, appId) + return fetchAppParams(AppSourceType.installedApp, appId) }, enabled: !!appId, }) @@ -74,8 +74,17 @@ export const useGetInstalledAppMeta = (appId: string | null) => { queryFn: () => { if (!appId || appId.length === 0) return Promise.reject(new Error('App ID is required to get app meta')) - return fetchAppMeta(true, appId) + return fetchAppMeta(AppSourceType.installedApp, appId) }, enabled: !!appId, }) } + +export const useGetBanners = (locale?: string) => { + return useQuery({ + queryKey: [NAME_SPACE, 'banners', locale], + queryFn: () => { + return fetchBanners(locale) + }, + }) +} diff --git a/web/service/use-share.ts b/web/service/use-share.ts index a5e0a11100..dd0eff431b 100644 --- a/web/service/use-share.ts +++ b/web/service/use-share.ts @@ -1,5 +1,5 @@ import { useQuery } from '@tanstack/react-query' -import { fetchAppInfo, fetchAppMeta, fetchAppParams, getAppAccessModeByAppCode } from './share' +import { AppSourceType, fetchAppInfo, fetchAppMeta, fetchAppParams, getAppAccessModeByAppCode } from './share' const NAME_SPACE = 'webapp' @@ -26,7 +26,7 @@ export const useGetWebAppParams = () => { return useQuery({ queryKey: [NAME_SPACE, 'appParams'], queryFn: () => { - return fetchAppParams(false) + return fetchAppParams(AppSourceType.webApp) }, }) } @@ -35,7 +35,7 @@ export const useGetWebAppMeta = () => { return useQuery({ queryKey: [NAME_SPACE, 'appMeta'], queryFn: () => { - return fetchAppMeta(false) + return fetchAppMeta(AppSourceType.webApp) }, }) } diff --git a/web/service/use-try-app.ts b/web/service/use-try-app.ts new file mode 100644 index 0000000000..35fdad6526 --- /dev/null +++ b/web/service/use-try-app.ts @@ -0,0 +1,44 @@ +import { useQuery } from '@tanstack/react-query' +import { fetchTryAppDatasets, fetchTryAppFlowPreview, fetchTryAppInfo } from './try-app' +import { AppSourceType, fetchAppParams } from './share' +import type { DataSetListResponse } from '@/models/datasets' + +const NAME_SPACE = 'try-app' + +export const useGetTryAppInfo = (appId: string) => { + return useQuery({ + queryKey: [NAME_SPACE, 'appInfo', appId], + queryFn: () => { + return fetchTryAppInfo(appId) + }, + }) +} + +export const useGetTryAppParams = (appId: string) => { + return useQuery({ + queryKey: [NAME_SPACE, 'appParams', appId], + queryFn: () => { + return fetchAppParams(AppSourceType.tryApp, appId) + }, + }) +} + +export const useGetTryAppDataSets = (appId: string, ids: string[]) => { + return useQuery({ + queryKey: [NAME_SPACE, 'dataSets', appId, ids], + queryFn: () => { + return fetchTryAppDatasets(appId, ids) + }, + enabled: ids.length > 0, + }) +} + +export const useGetTryAppFlowPreview = (appId: string, disabled?: boolean) => { + return useQuery({ + queryKey: [NAME_SPACE, 'preview', appId], + enabled: !disabled, + queryFn: () => { + return fetchTryAppFlowPreview(appId) + }, + }) +} diff --git a/web/types/app.ts b/web/types/app.ts index abc5b34ca5..0a12e28729 100644 --- a/web/types/app.ts +++ b/web/types/app.ts @@ -243,6 +243,7 @@ export type ModelConfig = { image: VisionSettings } & UploadFileSetting files?: VisionFile[] + external_data_tools: any[] created_at?: number updated_at?: number } diff --git a/web/types/feature.ts b/web/types/feature.ts index 611f3a173f..ce5cda6a61 100644 --- a/web/types/feature.ts +++ b/web/types/feature.ts @@ -60,6 +60,8 @@ export type SystemFeatures = { allow_email_code_login: boolean allow_email_password_login: boolean } + enable_trial_app: boolean + enable_explore_banner: boolean } export const defaultSystemFeatures: SystemFeatures = { @@ -100,6 +102,8 @@ export const defaultSystemFeatures: SystemFeatures = { allow_email_code_login: false, allow_email_password_login: false, }, + enable_trial_app: false, + enable_explore_banner: false, } export enum DatasetAttr {