'use client' import type { FC } from 'react' import React, { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { RiErrorWarningFill, } from '@remixicon/react' import { useBoolean, useClickAway } from 'ahooks' import { XMarkIcon } from '@heroicons/react/24/outline' import { usePathname, useRouter, useSearchParams } from 'next/navigation' import TabHeader from '../../base/tab-header' import Button from '../../base/button' import { checkOrSetAccessToken } from '../utils' import s from './style.module.css' import RunBatch from './run-batch' import ResDownload from './run-batch/res-download' import cn from '@/utils/classnames' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import RunOnce from '@/app/components/share/text-generation/run-once' import { fetchSavedMessage as doFetchSavedMessage, fetchAppInfo, fetchAppParams, removeMessage, saveMessage } from '@/service/share' import type { SiteInfo } from '@/models/share' import type { MoreLikeThisConfig, PromptConfig, SavedMessage, TextToSpeechConfig, } from '@/models/debug' import AppIcon from '@/app/components/base/app-icon' import { changeLanguage } from '@/i18n/i18next-config' import Loading from '@/app/components/base/loading' import { userInputsFormToPromptVariables } from '@/utils/model-config' import Res from '@/app/components/share/text-generation/result' import SavedItems from '@/app/components/app/text-generate/saved-items' import type { InstalledApp } from '@/models/explore' import { DEFAULT_VALUE_MAX_LEN, appDefaultIconBackground } from '@/config' import Toast from '@/app/components/base/toast' import type { VisionFile, VisionSettings } from '@/types/app' import { Resolution, TransferMethod } from '@/types/app' import { useAppFavicon } from '@/hooks/use-app-favicon' 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 installedAppInfo?: InstalledApp isWorkflow?: boolean } const TextGeneration: FC = ({ isInstalledApp = false, installedAppInfo, isWorkflow = false, }) => { const { notify } = Toast const { t } = useTranslation() const media = useBreakpoints() const isPC = media === MediaType.pc const isTablet = media === MediaType.tablet const isMobile = media === MediaType.mobile const searchParams = useSearchParams() const mode = searchParams.get('mode') || 'create' const [currentTab, setCurrentTab] = useState(['create', 'batch'].includes(mode) ? mode : 'create') const router = useRouter() const pathname = usePathname() useEffect(() => { const params = new URLSearchParams(searchParams) if (params.has('mode')) { params.delete('mode') router.replace(`${pathname}?${params.toString()}`) } // eslint-disable-next-line react-hooks/exhaustive-deps }, []) // Notice this situation isCallBatchAPI but not in batch tab const [isCallBatchAPI, setIsCallBatchAPI] = useState(false) const isInBatchTab = currentTab === 'batch' const [inputs, setInputs] = useState>({}) const [appId, setAppId] = useState('') const [siteInfo, setSiteInfo] = useState(null) const [canReplaceLogo, setCanReplaceLogo] = useState(false) const [promptConfig, setPromptConfig] = useState(null) const [moreLikeThisConfig, setMoreLikeThisConfig] = useState(null) const [textToSpeechConfig, setTextToSpeechConfig] = useState(null) // save message const [savedMessages, setSavedMessages] = useState([]) const fetchSavedMessage = async () => { const res: any = await doFetchSavedMessage(isInstalledApp, installedAppInfo?.id) setSavedMessages(res.data) } const handleSaveMessage = async (messageId: string) => { await saveMessage(messageId, isInstalledApp, installedAppInfo?.id) notify({ type: 'success', message: t('common.api.saved') }) fetchSavedMessage() } const handleRemoveSavedMessage = async (messageId: string) => { await removeMessage(messageId, isInstalledApp, installedAppInfo?.id) notify({ type: 'success', message: t('common.api.remove') }) fetchSavedMessage() } // send message task 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 handleSend = () => { setIsCallBatchAPI(false) setControlSend(Date.now()) // eslint-disable-next-line @typescript-eslint/no-use-before-define setAllTaskList([]) // clear batch task running status // eslint-disable-next-line @typescript-eslint/no-use-before-define showResSidebar() } const [controlRetry, setControlRetry] = useState(0) const handleRetryAllFailedTask = () => { setControlRetry(Date.now()) } const [allTaskList, doSetAllTaskList] = useState([]) const allTaskListRef = useRef([]) const getLatestTaskList = () => allTaskListRef.current const setAllTaskList = (taskList: Task[]) => { doSetAllTaskList(taskList) allTaskListRef.current = taskList } const pendingTaskList = allTaskList.filter(task => task.status === TaskStatus.pending) const noPendingTask = pendingTaskList.length === 0 const showTaskList = allTaskList.filter(task => task.status !== TaskStatus.pending) const [currGroupNum, doSetCurrGroupNum] = useState(0) const currGroupNumRef = useRef(0) const setCurrGroupNum = (num: number) => { doSetCurrGroupNum(num) currGroupNumRef.current = num } const getCurrGroupNum = () => { return currGroupNumRef.current } const allSuccessTaskList = allTaskList.filter(task => task.status === TaskStatus.completed) const allFailedTaskList = allTaskList.filter(task => task.status === TaskStatus.failed) const allTasksFinished = allTaskList.every(task => task.status === TaskStatus.completed) const allTasksRun = allTaskList.every(task => [TaskStatus.completed, TaskStatus.failed].includes(task.status)) const [batchCompletionRes, doSetBatchCompletionRes] = useState>({}) const batchCompletionResRef = useRef>({}) const setBatchCompletionRes = (res: Record) => { doSetBatchCompletionRes(res) batchCompletionResRef.current = res } const getBatchCompletionRes = () => batchCompletionResRef.current const exportRes = allTaskList.map((task) => { const batchCompletionResLatest = getBatchCompletionRes() const res: Record = {} const { inputs } = task.params promptConfig?.prompt_variables.forEach((v) => { res[v.name] = inputs[v.key] }) let result = batchCompletionResLatest[task.id] // task might return multiple fields, should marshal object to string if (typeof batchCompletionResLatest[task.id] === 'object') result = JSON.stringify(result) res[t('share.generation.completionResult')] = result return res }) const checkBatchInputs = (data: string[][]) => { if (!data || data.length === 0) { notify({ type: 'error', message: t('share.generation.errorMsg.empty') }) return false } const headerData = data[0] let isMapVarName = true promptConfig?.prompt_variables.forEach((item, index) => { if (!isMapVarName) return if (item.name !== headerData[index]) isMapVarName = false }) if (!isMapVarName) { notify({ type: 'error', message: t('share.generation.errorMsg.fileStructNotMatch') }) return false } let payloadData = data.slice(1) if (payloadData.length === 0) { notify({ type: 'error', message: t('share.generation.errorMsg.atLeastOne') }) return false } // check middle empty line const allEmptyLineIndexes = payloadData.filter(item => item.every(i => i === '')).map(item => payloadData.indexOf(item)) if (allEmptyLineIndexes.length > 0) { let hasMiddleEmptyLine = false let startIndex = allEmptyLineIndexes[0] - 1 allEmptyLineIndexes.forEach((index) => { if (hasMiddleEmptyLine) return if (startIndex + 1 !== index) { hasMiddleEmptyLine = true return } startIndex++ }) if (hasMiddleEmptyLine) { notify({ type: 'error', message: t('share.generation.errorMsg.emptyLine', { rowIndex: startIndex + 2 }) }) return false } } // check row format payloadData = payloadData.filter(item => !item.every(i => i === '')) // after remove empty rows in the end, checked again if (payloadData.length === 0) { notify({ type: 'error', message: t('share.generation.errorMsg.atLeastOne') }) return false } let errorRowIndex = 0 let requiredVarName = '' let moreThanMaxLengthVarName = '' let maxLength = 0 payloadData.forEach((item, index) => { if (errorRowIndex !== 0) return promptConfig?.prompt_variables.forEach((varItem, varIndex) => { if (errorRowIndex !== 0) return if (varItem.type === 'string') { const maxLen = varItem.max_length || DEFAULT_VALUE_MAX_LEN if (item[varIndex].length > maxLen) { moreThanMaxLengthVarName = varItem.name maxLength = maxLen errorRowIndex = index + 1 return } } if (!varItem.required) return if (item[varIndex].trim() === '') { requiredVarName = varItem.name errorRowIndex = index + 1 } }) }) if (errorRowIndex !== 0) { if (requiredVarName) notify({ type: 'error', message: t('share.generation.errorMsg.invalidLine', { rowIndex: errorRowIndex + 1, varName: requiredVarName }) }) if (moreThanMaxLengthVarName) notify({ type: 'error', message: t('share.generation.errorMsg.moreThanMaxLengthLine', { rowIndex: errorRowIndex + 1, varName: moreThanMaxLengthVarName, maxLength }) }) return false } return true } const handleRunBatch = (data: string[][]) => { if (!checkBatchInputs(data)) return if (!allTasksFinished) { notify({ type: 'info', message: t('appDebug.errorMessage.waitForBatchResponse') }) return } const payloadData = data.filter(item => !item.every(i => i === '')).slice(1) const varLen = promptConfig?.prompt_variables.length || 0 setIsCallBatchAPI(true) const allTaskList: Task[] = payloadData.map((item, i) => { const inputs: Record = {} if (varLen > 0) { item.slice(0, varLen).forEach((input, index) => { inputs[promptConfig?.prompt_variables[index].key as string] = input }) } return { id: i + 1, status: i < GROUP_SIZE ? TaskStatus.running : TaskStatus.pending, params: { inputs, }, } }) setAllTaskList(allTaskList) setCurrGroupNum(0) setControlSend(Date.now()) // clear run once task status setControlStopResponding(Date.now()) // eslint-disable-next-line @typescript-eslint/no-use-before-define showResSidebar() } const handleCompleted = (completionRes: string, taskId?: number, isSuccess?: boolean) => { const allTaskListLatest = getLatestTaskList() const batchCompletionResLatest = getBatchCompletionRes() const pendingTaskList = allTaskListLatest.filter(task => task.status === TaskStatus.pending) const runTasksCount = 1 + allTaskListLatest.filter(task => [TaskStatus.completed, TaskStatus.failed].includes(task.status)).length const needToAddNextGroupTask = (getCurrGroupNum() !== runTasksCount) && pendingTaskList.length > 0 && (runTasksCount % GROUP_SIZE === 0 || (allTaskListLatest.length - runTasksCount < GROUP_SIZE)) // avoid add many task at the same time if (needToAddNextGroupTask) setCurrGroupNum(runTasksCount) const nextPendingTaskIds = needToAddNextGroupTask ? pendingTaskList.slice(0, GROUP_SIZE).map(item => item.id) : [] const newAllTaskList = allTaskListLatest.map((item) => { if (item.id === taskId) { return { ...item, status: isSuccess ? TaskStatus.completed : TaskStatus.failed, } } if (needToAddNextGroupTask && nextPendingTaskIds.includes(item.id)) { return { ...item, status: TaskStatus.running, } } return item }) setAllTaskList(newAllTaskList) if (taskId) { setBatchCompletionRes({ ...batchCompletionResLatest, [`${taskId}`]: completionRes, }) } } const fetchInitData = async () => { if (!isInstalledApp) await checkOrSetAccessToken() return Promise.all([ isInstalledApp ? { app_id: installedAppInfo?.id, site: { title: installedAppInfo?.app.name, prompt_public: false, copyright: '', icon: installedAppInfo?.app.icon, icon_background: installedAppInfo?.app.icon_background, }, plan: 'basic', } : fetchAppInfo(), fetchAppParams(isInstalledApp, installedAppInfo?.id), !isWorkflow ? fetchSavedMessage() : {}, ]) } useEffect(() => { (async () => { const [appData, appParams]: any = await fetchInitData() const { app_id: appId, site: siteInfo, can_replace_logo } = appData setAppId(appId) setSiteInfo(siteInfo as SiteInfo) setCanReplaceLogo(can_replace_logo) changeLanguage(siteInfo.default_language) const { user_input_form, more_like_this, file_upload, text_to_speech }: any = appParams setVisionConfig({ ...file_upload.image, image_file_size_limit: appParams?.system_parameters?.image_file_size_limit, fileUploadConfig: appParams?.system_parameters, }) 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) })() }, []) // Can Use metadata(https://beta.nextjs.org/docs/api-reference/metadata) to set title. But it only works in server side client. useEffect(() => { if (siteInfo?.title) { if (canReplaceLogo) document.title = `${siteInfo.title}` else document.title = `${siteInfo.title} - Powered by Dify` } }, [siteInfo?.title, canReplaceLogo]) useAppFavicon({ enable: !isInstalledApp, icon_type: siteInfo?.icon_type, icon: siteInfo?.icon, icon_background: siteInfo?.icon_background, icon_url: siteInfo?.icon_url, }) const [isShowResSidebar, { setTrue: doShowResSidebar, setFalse: hideResSidebar }] = useBoolean(false) const showResSidebar = () => { // fix: useClickAway hideResSidebar will close sidebar setTimeout(() => { doShowResSidebar() }, 0) } const resRef = useRef(null) useClickAway(() => { hideResSidebar() }, resRef) const renderRes = (task?: Task) => () const renderBatchRes = () => { return (showTaskList.map(task => renderRes(task))) } const resWrapClassNames = (() => { if (isPC) return 'grow h-full' if (!isShowResSidebar) return 'none' return cn('fixed z-50 inset-0', isTablet ? 'pl-[128px]' : 'pl-6') })() const renderResWrap = (
<>
{t('share.generation.title')}
{allFailedTaskList.length > 0 && (
{t('share.generation.batchFailed.info', { num: allFailedTaskList.length })}
)} {allSuccessTaskList.length > 0 && ( )} {!isPC && (
)}
{!isCallBatchAPI ? renderRes() : renderBatchRes()} {!noPendingTask && (
)}
) if (!appId || !siteInfo || !promptConfig) { return (
) } return ( <>
{/* Left */}
{siteInfo.title}
{!isPC && ( )}
{siteInfo.description && (
{siteInfo.description}
)}
0 ? (
{savedMessages.length}
) : null, }] : []), ]} value={currentTab} onChange={setCurrentTab} />
{currentTab === 'saved' && ( setCurrentTab('create')} /> )}
{/* copyright */}
© {siteInfo.copyright || siteInfo.title} {(new Date()).getFullYear()}
{siteInfo.privacy_policy && ( <>
·
{t('share.chat.privacyPolicyLeft')} {t('share.chat.privacyPolicyMiddle')} {t('share.chat.privacyPolicyRight')}
)}
{/* Result */}
{renderResWrap}
) } export default TextGeneration