| 'use client' | 'use client' | ||||
| import React, { FC, useEffect, useState, useRef } from 'react' | |||||
| import type { FC } from 'react' | |||||
| import React, { useEffect, useRef, useState } from 'react' | |||||
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||
| import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' | |||||
| import cn from 'classnames' | import cn from 'classnames' | ||||
| import { useBoolean, useClickAway } from 'ahooks' | import { useBoolean, useClickAway } from 'ahooks' | ||||
| import { XMarkIcon } from '@heroicons/react/24/outline' | |||||
| import TabHeader from '../../base/tab-header' | |||||
| import Button from '../../base/button' | |||||
| import s from './style.module.css' | |||||
| import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' | |||||
| import ConfigScence from '@/app/components/share/text-generation/config-scence' | import ConfigScence from '@/app/components/share/text-generation/config-scence' | ||||
| import NoData from '@/app/components/share/text-generation/no-data' | import NoData from '@/app/components/share/text-generation/no-data' | ||||
| // import History from '@/app/components/share/text-generation/history' | // import History from '@/app/components/share/text-generation/history' | ||||
| import { fetchAppInfo, fetchAppParams, sendCompletionMessage, updateFeedback, saveMessage, fetchSavedMessage as doFetchSavedMessage, removeMessage } from '@/service/share' | |||||
| import { fetchSavedMessage as doFetchSavedMessage, fetchAppInfo, fetchAppParams, removeMessage, saveMessage, sendCompletionMessage, updateFeedback } from '@/service/share' | |||||
| import type { SiteInfo } from '@/models/share' | import type { SiteInfo } from '@/models/share' | ||||
| import type { PromptConfig, MoreLikeThisConfig, SavedMessage } from '@/models/debug' | |||||
| import type { MoreLikeThisConfig, PromptConfig, SavedMessage } from '@/models/debug' | |||||
| import Toast from '@/app/components/base/toast' | import Toast from '@/app/components/base/toast' | ||||
| import AppIcon from '@/app/components/base/app-icon' | import AppIcon from '@/app/components/base/app-icon' | ||||
| import { Feedbacktype } from '@/app/components/app/chat' | |||||
| import type { Feedbacktype } from '@/app/components/app/chat' | |||||
| import { changeLanguage } from '@/i18n/i18next-config' | import { changeLanguage } from '@/i18n/i18next-config' | ||||
| import Loading from '@/app/components/base/loading' | import Loading from '@/app/components/base/loading' | ||||
| import { userInputsFormToPromptVariables } from '@/utils/model-config' | import { userInputsFormToPromptVariables } from '@/utils/model-config' | ||||
| import TextGenerationRes from '@/app/components/app/text-generate/item' | import TextGenerationRes from '@/app/components/app/text-generate/item' | ||||
| import SavedItems from '@/app/components/app/text-generate/saved-items' | import SavedItems from '@/app/components/app/text-generate/saved-items' | ||||
| import TabHeader from '../../base/tab-header' | |||||
| import { XMarkIcon } from '@heroicons/react/24/outline' | |||||
| import s from './style.module.css' | |||||
| import Button from '../../base/button' | |||||
| import { App } from '@/types/app' | |||||
| import { InstalledApp } from '@/models/explore' | |||||
| import type { InstalledApp } from '@/models/explore' | |||||
| import { appDefaultIconBackground } from '@/config' | import { appDefaultIconBackground } from '@/config' | ||||
| export type IMainProps = { | export type IMainProps = { | ||||
| isInstalledApp?: boolean, | |||||
| installedAppInfo? : InstalledApp | |||||
| isInstalledApp?: boolean | |||||
| installedAppInfo?: InstalledApp | |||||
| } | } | ||||
| const TextGeneration: FC<IMainProps> = ({ | const TextGeneration: FC<IMainProps> = ({ | ||||
| isInstalledApp = false, | isInstalledApp = false, | ||||
| installedAppInfo | |||||
| installedAppInfo, | |||||
| }) => { | }) => { | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| const media = useBreakpoints() | const media = useBreakpoints() | ||||
| const [messageId, setMessageId] = useState<string | null>(null) | const [messageId, setMessageId] = useState<string | null>(null) | ||||
| const [feedback, setFeedback] = useState<Feedbacktype>({ | const [feedback, setFeedback] = useState<Feedbacktype>({ | ||||
| rating: null | |||||
| rating: null, | |||||
| }) | }) | ||||
| const handleFeedback = async (feedback: Feedbacktype) => { | const handleFeedback = async (feedback: Feedbacktype) => { | ||||
| const checkCanSend = () => { | const checkCanSend = () => { | ||||
| const prompt_variables = promptConfig?.prompt_variables | const prompt_variables = promptConfig?.prompt_variables | ||||
| if (!prompt_variables || prompt_variables?.length === 0) { | |||||
| if (!prompt_variables || prompt_variables?.length === 0) | |||||
| return true | return true | ||||
| } | |||||
| let hasEmptyInput = false | let hasEmptyInput = false | ||||
| const requiredVars = prompt_variables?.filter(({ key, name, required }) => { | const requiredVars = prompt_variables?.filter(({ key, name, required }) => { | ||||
| const res = (!key || !key.trim()) || (!name || !name.trim()) || (required || required === undefined || required === null) | const res = (!key || !key.trim()) || (!name || !name.trim()) || (required || required === undefined || required === null) | ||||
| return res | return res | ||||
| }) || [] // compatible with old version | }) || [] // compatible with old version | ||||
| requiredVars.forEach(({ key }) => { | requiredVars.forEach(({ key }) => { | ||||
| if (hasEmptyInput) { | |||||
| if (hasEmptyInput) | |||||
| return | return | ||||
| } | |||||
| if (!inputs[key]) { | |||||
| if (!inputs[key]) | |||||
| hasEmptyInput = true | hasEmptyInput = true | ||||
| } | |||||
| }) | }) | ||||
| if (hasEmptyInput) { | if (hasEmptyInput) { | ||||
| setMessageId(null) | setMessageId(null) | ||||
| setFeedback({ | setFeedback({ | ||||
| rating: null | |||||
| rating: null, | |||||
| }) | }) | ||||
| setCompletionRes('') | setCompletionRes('') | ||||
| const res: string[] = [] | const res: string[] = [] | ||||
| let tempMessageId = '' | let tempMessageId = '' | ||||
| if (!isPC) { | |||||
| if (!isPC) | |||||
| // eslint-disable-next-line @typescript-eslint/no-use-before-define | |||||
| showResSidebar() | showResSidebar() | ||||
| } | |||||
| setResponsingTrue() | setResponsingTrue() | ||||
| sendCompletionMessage(data, { | sendCompletionMessage(data, { | ||||
| onData: (data: string, _isFirstMessage: boolean, { messageId }: any) => { | onData: (data: string, _isFirstMessage: boolean, { messageId }: any) => { | ||||
| }, | }, | ||||
| onError() { | onError() { | ||||
| setResponsingFalse() | setResponsingFalse() | ||||
| } | |||||
| }, | |||||
| }, isInstalledApp, installedAppInfo?.id) | }, isInstalledApp, installedAppInfo?.id) | ||||
| } | } | ||||
| const fetchInitData = () => { | const fetchInitData = () => { | ||||
| return Promise.all([isInstalledApp ? { | |||||
| app_id: installedAppInfo?.id, | |||||
| site: { | |||||
| title: installedAppInfo?.app.name, | |||||
| prompt_public: false, | |||||
| copyright: '' | |||||
| }, | |||||
| plan: 'basic', | |||||
| }: fetchAppInfo(), fetchAppParams(isInstalledApp, installedAppInfo?.id)]) | |||||
| return Promise.all([isInstalledApp | |||||
| ? { | |||||
| app_id: installedAppInfo?.id, | |||||
| site: { | |||||
| title: installedAppInfo?.app.name, | |||||
| prompt_public: false, | |||||
| copyright: '', | |||||
| }, | |||||
| plan: 'basic', | |||||
| } | |||||
| : fetchAppInfo(), fetchAppParams(isInstalledApp, installedAppInfo?.id)]) | |||||
| } | } | ||||
| useEffect(() => { | useEffect(() => { | ||||
| })() | })() | ||||
| }, []) | }, []) | ||||
| // Can Use metadata(https://beta.nextjs.org/docs/api-reference/metadata) to set title. But it only works in server side client. | |||||
| // Can Use metadata(https://beta.nextjs.org/docs/api-reference/metadata) to set title. But it only works in server side client. | |||||
| useEffect(() => { | useEffect(() => { | ||||
| if (siteInfo?.title) | if (siteInfo?.title) | ||||
| document.title = `${siteInfo.title} - Powered by Dify` | document.title = `${siteInfo.title} - Powered by Dify` | ||||
| const [isShowResSidebar, { setTrue: showResSidebar, setFalse: hideResSidebar }] = useBoolean(false) | const [isShowResSidebar, { setTrue: showResSidebar, setFalse: hideResSidebar }] = useBoolean(false) | ||||
| const resRef = useRef<HTMLDivElement>(null) | const resRef = useRef<HTMLDivElement>(null) | ||||
| useClickAway(() => { | useClickAway(() => { | ||||
| hideResSidebar(); | |||||
| hideResSidebar() | |||||
| }, resRef) | }, resRef) | ||||
| const renderRes = ( | const renderRes = ( | ||||
| ref={resRef} | ref={resRef} | ||||
| className={ | className={ | ||||
| cn( | cn( | ||||
| "flex flex-col h-full shrink-0", | |||||
| 'flex flex-col h-full shrink-0', | |||||
| isPC ? 'px-10 py-8' : 'bg-gray-50', | isPC ? 'px-10 py-8' : 'bg-gray-50', | ||||
| isTablet && 'p-6', isMoble && 'p-4') | isTablet && 'p-6', isMoble && 'p-4') | ||||
| } | |||||
| } | |||||
| > | > | ||||
| <> | <> | ||||
| <div className='shrink-0 flex items-center justify-between'> | <div className='shrink-0 flex items-center justify-between'> | ||||
| )} | )} | ||||
| </div> | </div> | ||||
| <div className='grow'> | |||||
| {(isResponsing && !completionRes) ? ( | |||||
| <div className='flex h-full w-full justify-center items-center'> | |||||
| <Loading type='area' /> | |||||
| </div>) : ( | |||||
| <> | |||||
| {isNoData | |||||
| ? <NoData /> | |||||
| : ( | |||||
| <TextGenerationRes | |||||
| className='mt-3' | |||||
| content={completionRes} | |||||
| messageId={messageId} | |||||
| isInWebApp | |||||
| moreLikeThis={moreLikeThisConifg?.enabled} | |||||
| onFeedback={handleFeedback} | |||||
| feedback={feedback} | |||||
| onSave={handleSaveMessage} | |||||
| isMobile={isMoble} | |||||
| isInstalledApp={isInstalledApp} | |||||
| installedAppId={installedAppInfo?.id} | |||||
| /> | |||||
| ) | |||||
| } | |||||
| </> | |||||
| )} | |||||
| <div className='grow overflow-y-auto'> | |||||
| {(isResponsing && !completionRes) | |||||
| ? ( | |||||
| <div className='flex h-full w-full justify-center items-center'> | |||||
| <Loading type='area' /> | |||||
| </div>) | |||||
| : ( | |||||
| <> | |||||
| {isNoData | |||||
| ? <NoData /> | |||||
| : ( | |||||
| <TextGenerationRes | |||||
| className='mt-3' | |||||
| content={completionRes} | |||||
| messageId={messageId} | |||||
| isInWebApp | |||||
| moreLikeThis={moreLikeThisConifg?.enabled} | |||||
| onFeedback={handleFeedback} | |||||
| feedback={feedback} | |||||
| onSave={handleSaveMessage} | |||||
| isMobile={isMoble} | |||||
| isInstalledApp={isInstalledApp} | |||||
| installedAppId={installedAppInfo?.id} | |||||
| /> | |||||
| ) | |||||
| } | |||||
| </> | |||||
| )} | |||||
| </div> | </div> | ||||
| </> | </> | ||||
| </div> | </div> | ||||
| if (!appId || !siteInfo || !promptConfig) | if (!appId || !siteInfo || !promptConfig) | ||||
| return <Loading type='app' /> | return <Loading type='app' /> | ||||
| return ( | return ( | ||||
| <> | <> | ||||
| <div className={cn( | <div className={cn( | ||||
| isPC && 'flex', | isPC && 'flex', | ||||
| isInstalledApp ? s.installedApp : 'h-screen', | isInstalledApp ? s.installedApp : 'h-screen', | ||||
| 'bg-gray-50' | |||||
| 'bg-gray-50', | |||||
| )}> | )}> | ||||
| {/* Left */} | {/* Left */} | ||||
| <div className={cn( | <div className={cn( | ||||
| isPC ? 'w-[600px] max-w-[50%] p-8' : 'p-4', | isPC ? 'w-[600px] max-w-[50%] p-8' : 'p-4', | ||||
| isInstalledApp && 'rounded-l-2xl', | isInstalledApp && 'rounded-l-2xl', | ||||
| "shrink-0 relative flex flex-col pb-10 h-full border-r border-gray-100 bg-white" | |||||
| 'shrink-0 relative flex flex-col pb-10 h-full border-r border-gray-100 bg-white', | |||||
| )}> | )}> | ||||
| <div className='mb-6'> | <div className='mb-6'> | ||||
| <div className='flex justify-between items-center'> | <div className='flex justify-between items-center'> | ||||
| items={[ | items={[ | ||||
| { id: 'create', name: t('share.generation.tabs.create') }, | { id: 'create', name: t('share.generation.tabs.create') }, | ||||
| { | { | ||||
| id: 'saved', name: t('share.generation.tabs.saved'), extra: savedMessages.length > 0 ? ( | |||||
| <div className='ml-1 flext items-center h-5 px-1.5 rounded-md border border-gray-200 text-gray-500 text-xs font-medium'> | |||||
| {savedMessages.length} | |||||
| </div> | |||||
| ) : null | |||||
| } | |||||
| id: 'saved', | |||||
| name: t('share.generation.tabs.saved'), | |||||
| extra: savedMessages.length > 0 | |||||
| ? ( | |||||
| <div className='ml-1 flext items-center h-5 px-1.5 rounded-md border border-gray-200 text-gray-500 text-xs font-medium'> | |||||
| {savedMessages.length} | |||||
| </div> | |||||
| ) | |||||
| : null, | |||||
| }, | |||||
| ]} | ]} | ||||
| value={currTab} | value={currTab} | ||||
| onChange={setCurrTab} | onChange={setCurrTab} | ||||
| )} | )} | ||||
| </div> | </div> | ||||
| {/* copyright */} | {/* copyright */} | ||||
| <div className={cn( | <div className={cn( | ||||
| isInstalledApp ? 'left-[248px]' : 'left-8', | isInstalledApp ? 'left-[248px]' : 'left-8', | ||||
| 'fixed bottom-4 flex space-x-2 text-gray-400 font-normal text-xs' | |||||
| )}> | |||||
| 'fixed bottom-4 flex space-x-2 text-gray-400 font-normal text-xs', | |||||
| )}> | |||||
| <div className="">© {siteInfo.copyright || siteInfo.title} {(new Date()).getFullYear()}</div> | <div className="">© {siteInfo.copyright || siteInfo.title} {(new Date()).getFullYear()}</div> | ||||
| {siteInfo.privacy_policy && ( | {siteInfo.privacy_policy && ( | ||||
| <> | <> | ||||
| <div | <div | ||||
| className={cn('fixed z-50 inset-0', isTablet ? 'pl-[128px]' : 'pl-6')} | className={cn('fixed z-50 inset-0', isTablet ? 'pl-[128px]' : 'pl-6')} | ||||
| style={{ | style={{ | ||||
| background: 'rgba(35, 56, 118, 0.2)' | |||||
| background: 'rgba(35, 56, 118, 0.2)', | |||||
| }} | }} | ||||
| > | > | ||||
| {renderRes} | {renderRes} |