feat: frontend part of support try apps (#31287)

Co-authored-by: CodingOnStar <hanxujiang@dify.ai>
Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com>
This commit is contained in:
Joel
2026-01-22 18:16:37 +08:00
committed by GitHub
parent c575c34ca6
commit b9f718005c
130 changed files with 3233 additions and 685 deletions

View File

@ -34,7 +34,7 @@ import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import useDocumentTitle from '@/hooks/use-document-title'
import { changeLanguage } from '@/i18n-config/client'
import { AccessMode } from '@/models/access-control'
import { fetchSavedMessage as doFetchSavedMessage, removeMessage, saveMessage } from '@/service/share'
import { AppSourceType, fetchSavedMessage as doFetchSavedMessage, removeMessage, saveMessage } from '@/service/share'
import { Resolution, TransferMethod } from '@/types/app'
import { cn } from '@/utils/classnames'
import { userInputsFormToPromptVariables } from '@/utils/model-config'
@ -69,10 +69,10 @@ export type IMainProps = {
const TextGeneration: FC<IMainProps> = ({
isInstalledApp = false,
installedAppInfo,
isWorkflow = false,
}) => {
const { notify } = Toast
const appSourceType = isInstalledApp ? AppSourceType.installedApp : AppSourceType.webApp
const { t } = useTranslation()
const media = useBreakpoints()
@ -102,16 +102,18 @@ const TextGeneration: FC<IMainProps> = ({
// save message
const [savedMessages, setSavedMessages] = useState<SavedMessage[]>([])
const fetchSavedMessage = useCallback(async () => {
const res: any = await doFetchSavedMessage(isInstalledApp, appId)
if (!appId)
return
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('api.saved', { ns: 'common' }) })
fetchSavedMessage()
}
const handleRemoveSavedMessage = async (messageId: string) => {
await removeMessage(messageId, isInstalledApp, appId)
await removeMessage(messageId, appSourceType, appId)
notify({ type: 'success', message: t('api.remove', { ns: 'common' }) })
fetchSavedMessage()
}
@ -423,9 +425,8 @@ const TextGeneration: FC<IMainProps> = ({
isCallBatchAPI={isCallBatchAPI}
isPC={isPC}
isMobile={!isPC}
isInstalledApp={isInstalledApp}
appSourceType={isInstalledApp ? AppSourceType.installedApp : AppSourceType.webApp}
appId={appId}
installedAppInfo={installedAppInfo}
isError={task?.status === TaskStatus.failed}
promptConfig={promptConfig}
moreLikeThisEnabled={!!moreLikeThisConfig?.enabled}

View File

@ -4,8 +4,8 @@ import type { FeedbackType } from '@/app/components/base/chat/chat/type'
import type { WorkflowProcess } from '@/app/components/base/chat/types'
import type { FileEntity } from '@/app/components/base/file-uploader/types'
import type { PromptConfig } from '@/models/debug'
import type { InstalledApp } from '@/models/explore'
import type { SiteInfo } from '@/models/share'
import type { AppSourceType } from '@/service/share'
import type { VisionFile, VisionSettings } from '@/types/app'
import { RiLoader2Line } from '@remixicon/react'
import { useBoolean } from 'ahooks'
@ -35,9 +35,8 @@ export type IResultProps = {
isCallBatchAPI: boolean
isPC: boolean
isMobile: boolean
isInstalledApp: boolean
appId: string
installedAppInfo?: InstalledApp
appSourceType: AppSourceType
appId?: string
isError: boolean
isShowTextToSpeech: boolean
promptConfig: PromptConfig | null
@ -63,9 +62,8 @@ const Result: FC<IResultProps> = ({
isCallBatchAPI,
isPC,
isMobile,
isInstalledApp,
appSourceType,
appId,
installedAppInfo,
isError,
isShowTextToSpeech,
promptConfig,
@ -133,7 +131,7 @@ const Result: FC<IResultProps> = ({
})
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)
}
@ -147,9 +145,9 @@ const Result: FC<IResultProps> = ({
setIsStopping(true)
try {
if (isWorkflow)
await stopWorkflowMessage(appId, currentTaskId, isInstalledApp, installedAppInfo?.id || '')
await stopWorkflowMessage(appId!, currentTaskId, appSourceType, appId || '')
else
await stopChatMessageResponding(appId, currentTaskId, isInstalledApp, installedAppInfo?.id || '')
await stopChatMessageResponding(appId!, currentTaskId, appSourceType, appId || '')
abortControllerRef.current?.abort()
}
catch (error) {
@ -159,7 +157,7 @@ const Result: FC<IResultProps> = ({
finally {
setIsStopping(false)
}
}, [appId, currentTaskId, installedAppInfo?.id, isInstalledApp, isStopping, isWorkflow, notify])
}, [appId, currentTaskId, appSourceType, appId, isStopping, isWorkflow, notify])
useEffect(() => {
if (!onRunControlChange)
@ -468,8 +466,8 @@ const Result: FC<IResultProps> = ({
}))
},
},
isInstalledApp,
installedAppInfo?.id,
appSourceType,
appId,
).catch((error) => {
setRespondingFalse()
resetRunState()
@ -514,7 +512,7 @@ const Result: FC<IResultProps> = ({
getAbortController: (abortController) => {
abortControllerRef.current = abortController
},
}, isInstalledApp, installedAppInfo?.id)
}, appSourceType, appId)
}
}
@ -562,8 +560,8 @@ const Result: FC<IResultProps> = ({
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}

View File

@ -1,4 +1,5 @@
import type { ChangeEvent, FC, FormEvent } from 'react'
import type { InputValueTypes } from '../types'
import type { PromptConfig } from '@/models/debug'
import type { SiteInfo } from '@/models/share'
import type { VisionFile, VisionSettings } from '@/types/app'
@ -25,9 +26,9 @@ import { cn } from '@/utils/classnames'
export type IRunOnceProps = {
siteInfo: SiteInfo
promptConfig: PromptConfig
inputs: Record<string, any>
inputsRef: React.RefObject<Record<string, any>>
onInputsChange: (inputs: Record<string, any>) => void
inputs: Record<string, InputValueTypes>
inputsRef: React.RefObject<Record<string, InputValueTypes>>
onInputsChange: (inputs: Record<string, InputValueTypes>) => void
onSend: () => void
visionConfig: VisionSettings
onVisionFilesChange: (files: VisionFile[]) => void
@ -52,7 +53,7 @@ const RunOnce: FC<IRunOnceProps> = ({
const [isInitialized, setIsInitialized] = useState(false)
const onClear = () => {
const newInputs: Record<string, any> = {}
const newInputs: Record<string, InputValueTypes> = {}
promptConfig.prompt_variables.forEach((item) => {
if (item.type === 'string' || item.type === 'paragraph')
newInputs[item.key] = ''
@ -127,7 +128,7 @@ const RunOnce: FC<IRunOnceProps> = ({
{item.type === 'select' && (
<Select
className="w-full"
defaultValue={inputs[item.key]}
defaultValue={inputs[item.key] as (string | number | undefined)}
onSelect={(i) => { handleInputsChange({ ...inputsRef.current, [item.key]: i.value }) }}
items={(item.options || []).map(i => ({ name: i, value: i }))}
allowSearch={false}
@ -137,7 +138,7 @@ const RunOnce: FC<IRunOnceProps> = ({
<Input
type="text"
placeholder={item.name}
value={inputs[item.key]}
value={inputs[item.key] as string}
onChange={(e: ChangeEvent<HTMLInputElement>) => { handleInputsChange({ ...inputsRef.current, [item.key]: e.target.value }) }}
maxLength={item.max_length}
/>
@ -146,7 +147,7 @@ const RunOnce: FC<IRunOnceProps> = ({
<Textarea
className="h-[104px] sm:text-xs"
placeholder={item.name}
value={inputs[item.key]}
value={inputs[item.key] as string}
onChange={(e: ChangeEvent<HTMLTextAreaElement>) => { handleInputsChange({ ...inputsRef.current, [item.key]: e.target.value }) }}
/>
)}
@ -154,14 +155,14 @@ const RunOnce: FC<IRunOnceProps> = ({
<Input
type="number"
placeholder={item.name}
value={inputs[item.key]}
value={inputs[item.key] as number}
onChange={(e: ChangeEvent<HTMLInputElement>) => { handleInputsChange({ ...inputsRef.current, [item.key]: e.target.value }) }}
/>
)}
{item.type === 'checkbox' && (
<BoolInput
name={item.name || item.key}
value={!!inputs[item.key]}
value={!!inputs[item.key] as boolean}
required={item.required}
onChange={(value) => { handleInputsChange({ ...inputsRef.current, [item.key]: value }) }}
/>
@ -182,6 +183,7 @@ const RunOnce: FC<IRunOnceProps> = ({
onChange={(files) => { handleInputsChange({ ...inputsRef.current, [item.key]: files }) }}
fileConfig={{
...item.config,
// eslint-disable-next-line ts/no-explicit-any
fileUploadConfig: (visionConfig as any).fileUploadConfig,
}}
/>
@ -189,7 +191,7 @@ const RunOnce: FC<IRunOnceProps> = ({
{item.type === 'json_object' && (
<CodeEditor
language={CodeLanguage.json}
value={inputs[item.key]}
value={inputs[item.key] as string}
onChange={(value) => { handleInputsChange({ ...inputsRef.current, [item.key]: value }) }}
noWrapper
className="bg h-[80px] overflow-y-auto rounded-[10px] bg-components-input-bg-normal p-1"

View File

@ -0,0 +1,19 @@
type TaskParam = {
inputs: Record<string, string | boolean | undefined>
}
export type Task = {
id: number
status: TaskStatus
params: TaskParam
}
export enum TaskStatus {
pending = 'pending',
running = 'running',
completed = 'completed',
failed = 'failed',
}
// eslint-disable-next-line ts/no-explicit-any
export type InputValueTypes = string | boolean | number | string[] | object | undefined | any