'use client' import type { FC } from 'react' import React, { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import cn from 'classnames' import { useBoolean, useClickAway, useGetState } from 'ahooks' import { XMarkIcon } from '@heroicons/react/24/outline' 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 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 } 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 { appDefaultIconBackground } from '@/config' import Toast from '@/app/components/base/toast' const PARALLEL_LIMIT = 5 enum TaskStatus { pending = 'pending', running = 'running', completed = 'completed', } type TaskParam = { inputs: Record query: string } type Task = { id: number status: TaskStatus params: TaskParam } export type IMainProps = { isInstalledApp?: boolean installedAppInfo?: InstalledApp } const TextGeneration: FC = ({ isInstalledApp = false, installedAppInfo, }) => { 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 [currTab, setCurrTab] = useState('create') // Notice this situation isCallBatchAPI but not in batch tab const [isCallBatchAPI, setIsCallBatchAPI] = useState(false) const isInBatchTab = currTab === 'batch' const [inputs, setInputs] = useState>({}) const [query, setQuery] = useState('') // run once query content const [appId, setAppId] = useState('') const [siteInfo, setSiteInfo] = useState(null) const [promptConfig, setPromptConfig] = useState(null) const [moreLikeThisConfig, setMoreLikeThisConfig] = 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 handleSend = () => { setIsCallBatchAPI(false) setControlSend(Date.now()) // eslint-disable-next-line @typescript-eslint/no-use-before-define setAllTaskList([]) // clear batch task running status } const [allTaskList, setAllTaskList, getLatestTaskList] = useGetState([]) const pendingTaskList = allTaskList.filter(task => task.status === TaskStatus.pending) const noPendingTask = pendingTaskList.length === 0 const showTaskList = allTaskList.filter(task => task.status !== TaskStatus.pending) const allTaskFinished = allTaskList.every(task => task.status === TaskStatus.completed) const checkBatchInputs = (data: string[][]) => { if (!data || data.length === 0) { notify({ type: 'error', message: t('share.generation.errorMsg.empty') }) return false } const headerData = data[0] const varLen = promptConfig?.prompt_variables.length || 0 let isMapVarName = true promptConfig?.prompt_variables.forEach((item, index) => { if (!isMapVarName) return if (item.name !== headerData[index]) isMapVarName = false }) if (headerData[varLen] !== t('share.generation.queryTitle')) 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 = '' payloadData.forEach((item, index) => { if (errorRowIndex !== 0) return promptConfig?.prompt_variables.forEach((varItem, varIndex) => { if (errorRowIndex !== 0) return if (varItem.required === false) return if (item[varIndex].trim() === '') { requiredVarName = varItem.name errorRowIndex = index + 1 } }) if (errorRowIndex !== 0) return if (item[varLen] === '') { requiredVarName = t('share.generation.queryTitle') errorRowIndex = index + 1 } }) if (errorRowIndex !== 0) { notify({ type: 'error', message: t('share.generation.errorMsg.invalidLine', { rowIndex: errorRowIndex + 1, varName: requiredVarName }) }) return false } return true } const handleRunBatch = (data: string[][]) => { if (!checkBatchInputs(data)) return if (!allTaskFinished) { 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 < PARALLEL_LIMIT ? TaskStatus.running : TaskStatus.pending, params: { inputs, query: item[varLen], }, } }) setAllTaskList(allTaskList) setControlSend(Date.now()) // clear run once task status setControlStopResponding(Date.now()) } const handleCompleted = (taskId?: number, isSuccess?: boolean) => { // console.log(taskId, isSuccess) const allTasklistLatest = getLatestTaskList() const pendingTaskList = allTasklistLatest.filter(task => task.status === TaskStatus.pending) const nextPendingTaskId = pendingTaskList[0]?.id // console.log(`start: ${allTasklistLatest.map(item => item.status).join(',')}`) const newAllTaskList = allTasklistLatest.map((item) => { if (item.id === taskId) { return { ...item, status: TaskStatus.completed, } } if (item.id === nextPendingTaskId) { return { ...item, status: TaskStatus.running, } } return item }) // console.log(`end: ${newAllTaskList.map(item => item.status).join(',')}`) setAllTaskList(newAllTaskList) } const fetchInitData = async () => { await checkOrSetAccessToken() return Promise.all([isInstalledApp ? { app_id: installedAppInfo?.id, site: { title: installedAppInfo?.app.name, prompt_public: false, copyright: '', }, plan: 'basic', } : fetchAppInfo(), fetchAppParams(isInstalledApp, installedAppInfo?.id), fetchSavedMessage()]) } useEffect(() => { (async () => { const [appData, appParams]: any = await fetchInitData() const { app_id: appId, site: siteInfo } = appData setAppId(appId) setSiteInfo(siteInfo as SiteInfo) changeLanguage(siteInfo.default_language) const { user_input_form, more_like_this }: any = appParams const prompt_variables = userInputsFormToPromptVariables(user_input_form) setPromptConfig({ prompt_template: '', // placeholder for feture prompt_variables, } as PromptConfig) setMoreLikeThisConfig(more_like_this) })() }, []) // 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) document.title = `${siteInfo.title} - Powered by Dify` }, [siteInfo?.title]) const [isShowResSidebar, { setTrue: showResSidebar, setFalse: hideResSidebar }] = useBoolean(false) const resRef = useRef(null) useClickAway(() => { hideResSidebar() }, resRef) const renderRes = (task?: Task) => () const renderBatchRes = () => { return (showTaskList.map(task => renderRes(task))) } const renderResWrap = (
<>
{t('share.generation.title')}
{!isPC && (
)}
{!isCallBatchAPI ? renderRes() : renderBatchRes()} {!noPendingTask && (
)}
) if (!appId || !siteInfo || !promptConfig) return return ( <>
{/* Left */}
{siteInfo.title}
{!isPC && ( )}
{siteInfo.description && (
{siteInfo.description}
)}
0 ? (
{savedMessages.length}
) : null, }, ]} value={currTab} onChange={setCurrTab} />
{currTab === 'saved' && ( setCurrTab('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 */} {isPC && (
{renderResWrap}
)} {(!isPC && isShowResSidebar) && (
{renderResWrap}
)}
) } export default TextGeneration