From 73845cbec5d4e16a09049dfcf400ca56cd4570fa Mon Sep 17 00:00:00 2001 From: Joel Date: Fri, 19 Sep 2025 16:32:11 +0800 Subject: [PATCH] feat: text generation --- .../base/chat/embedded-chatbot/hooks.tsx | 6 +- .../explore/installed-app/index.tsx | 1 - .../share/text-generation/index.tsx | 19 +- .../components/share/text-generation/types.ts | 16 ++ web/app/components/try/app/index.tsx | 6 +- .../components/try/app/text-generation.tsx | 263 ++++++++++++++++++ web/service/share.ts | 5 - web/service/try-app.ts | 11 + web/service/use-try-app.ts | 23 ++ 9 files changed, 324 insertions(+), 26 deletions(-) create mode 100644 web/app/components/share/text-generation/types.ts create mode 100644 web/app/components/try/app/text-generation.tsx create mode 100644 web/service/try-app.ts create mode 100644 web/service/use-try-app.ts diff --git a/web/app/components/base/chat/embedded-chatbot/hooks.tsx b/web/app/components/base/chat/embedded-chatbot/hooks.tsx index 773d3d3a19..ba7b902d5f 100644 --- a/web/app/components/base/chat/embedded-chatbot/hooks.tsx +++ b/web/app/components/base/chat/embedded-chatbot/hooks.tsx @@ -24,10 +24,12 @@ import { fetchAppParams, fetchChatList, fetchConversations, - fetchTryAppInfo, generationConversationName, updateFeedback, } from '@/service/share' +import { + fetchTryAppInfo, +} from '@/service/try-app' import type { // AppData, ConversationItem, @@ -257,6 +259,8 @@ export const useEmbeddedChatbot = (appSourceType = AppSourceType.webApp, tryAppI useEffect(() => { // init inputs from url params (async () => { + if(isTryApp) + return const inputs = await getProcessedInputsFromUrlParams() const userVariables = await getProcessedUserVariablesFromUrlParams() setInitInputs(inputs) diff --git a/web/app/components/explore/installed-app/index.tsx b/web/app/components/explore/installed-app/index.tsx index 05729dfb08..b4321d6336 100644 --- a/web/app/components/explore/installed-app/index.tsx +++ b/web/app/components/explore/installed-app/index.tsx @@ -33,7 +33,6 @@ const InstalledApp: FC = ({ const { isFetching: isFetchingAppMeta, data: appMeta, error: appMetaError } = useGetInstalledAppMeta(installedApp?.id ?? null) const { data: userCanAccessApp, error: useCanAccessAppError } = useGetUserCanAccessApp({ appId: installedApp?.app.id, isInstalledApp: true }) - console.log(appParams, appMeta) useEffect(() => { if (!installedApp) { updateAppInfo(null) diff --git a/web/app/components/share/text-generation/index.tsx b/web/app/components/share/text-generation/index.tsx index 2384e1fc3a..4a8f37a61e 100644 --- a/web/app/components/share/text-generation/index.tsx +++ b/web/app/components/share/text-generation/index.tsx @@ -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 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/try/app/index.tsx b/web/app/components/try/app/index.tsx index 4005cbe675..90c9f57677 100644 --- a/web/app/components/try/app/index.tsx +++ b/web/app/components/try/app/index.tsx @@ -2,6 +2,7 @@ import type { FC } from 'react' import React from 'react' import Chat from './chat' +import TextGeneration from './text-generation' type Props = { appId: string @@ -10,7 +11,8 @@ type Props = { const TryApp: FC = ({ appId, }) => { - const isChat = true + // get app type by /trial-apps/ + const isChat = appId === 'fsVnyqGJbriqnPxK' const isCompletion = !isChat return (
@@ -18,7 +20,7 @@ const TryApp: FC = ({ )} {isCompletion && ( -
Completion
+ )}
Right panel diff --git a/web/app/components/try/app/text-generation.tsx b/web/app/components/try/app/text-generation.tsx new file mode 100644 index 0000000000..5b967d083a --- /dev/null +++ b/web/app/components/try/app/text-generation.tsx @@ -0,0 +1,263 @@ +'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 MenuDropdown from '@/app/components/share/text-generation/menu-dropdown' +import type { SiteInfo } from '@/models/share' +import type { VisionFile, VisionSettings } from '@/types/app' +import { Resolution, TransferMethod } from '@/types/app' +import type { MoreLikeThisConfig, PromptConfig, TextToSpeechConfig } from '@/models/debug' +import { userInputsFormToPromptVariables } from '@/utils/model-config' +import { useWebAppStore } from '@/context/web-app-context' +import { useGetTryAppInfo, useGetTryAppParams } from '@/service/use-try-app' +import Loading from '@/app/components/base/loading' +import { appDefaultIconBackground } from '@/config' +import RunOnce from '../../share/text-generation/run-once' +import { useBoolean } from 'ahooks' +import Res from '@/app/components/share/text-generation/result' +import type { Task } from '../../share/text-generation/types' +import { TaskStatus } from '../../share/text-generation/types' +import { noop } from 'lodash' + +type Props = { + appId: string + isWorkflow?: boolean + className?: string +} + +const TextGeneration: FC = ({ + isWorkflow = false, + className, +}) => { + // 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 { isFetching: isFetchingAppInfo, data: appInfo } = useGetTryAppInfo() + const updateAppInfo = useWebAppStore(s => s.updateAppInfo) + const appData = useWebAppStore(s => s.appInfo) + const { data: tryAppParams } = useGetTryAppParams() + 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 [controlStopResponding, setControlStopResponding] = useState(0) + const [visionConfig, setVisionConfig] = useState({ + enabled: false, + number_limits: 2, + detail: Resolution.low, + transfer_methods: [TransferMethod.local_file], + }) + const [completionFiles, setCompletionFiles] = useState([]) + const [controlRetry, setControlRetry] = useState(0) + const [isShowResultPanel, { setTrue: doShowResultPanel, setFalse: hideResultPanel }] = useBoolean(false) +// to temp fix lint + console.log(setControlStopResponding, setControlRetry) + const showResultPanel = () => { + // fix: useClickAway hideResSidebar will close sidebar + setTimeout(() => { + doShowResultPanel() + }, 0) + } + + const handleSend = () => { + setControlSend(Date.now()) + showResultPanel() + } + + const [resultExisted, setResultExisted] = useState(false) + + useEffect(() => { + if (!appInfo) return + updateAppInfo(appInfo) + }, [appInfo, 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 handleCompleted = noop + + const renderRes = (task?: Task) => ( setResultExisted(true)} + />) + + const renderResWrap = ( +
+
+ {renderRes()} +
+
+ ) + + // console.log(siteInfo, promptConfig) + + if(isFetchingAppInfo || !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/service/share.ts b/web/service/share.ts index 9b6eef8b97..dabddfdd4c 100644 --- a/web/service/share.ts +++ b/web/service/share.ts @@ -141,11 +141,6 @@ export const fetchAppInfo = async () => { return get('/site') as Promise } -// would use trial-apps after api is ok -export const fetchTryAppInfo = async () => { - return get('/site') 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 } diff --git a/web/service/try-app.ts b/web/service/try-app.ts new file mode 100644 index 0000000000..05a55e0c51 --- /dev/null +++ b/web/service/try-app.ts @@ -0,0 +1,11 @@ +import { + getPublic as get, +} from './base' +import type { + AppData, +} from '@/models/share' + +// would use trial-apps after api is ok +export const fetchTryAppInfo = async () => { + return get('/site') as Promise +} diff --git a/web/service/use-try-app.ts b/web/service/use-try-app.ts new file mode 100644 index 0000000000..c3a686dbcb --- /dev/null +++ b/web/service/use-try-app.ts @@ -0,0 +1,23 @@ +import { useQuery } from '@tanstack/react-query' +import { fetchTryAppInfo } from './try-app' +import { AppSourceType, fetchAppParams } from './share' + +const NAME_SPACE = 'try-app' + +export const useGetTryAppInfo = () => { + return useQuery({ + queryKey: [NAME_SPACE, 'appInfo'], + queryFn: () => { + return fetchTryAppInfo() + }, + }) +} + +export const useGetTryAppParams = () => { + return useQuery({ + queryKey: [NAME_SPACE, 'appParams'], + queryFn: () => { + return fetchAppParams(AppSourceType.webApp) // todo: wait api + }, + }) +}