Feat: web app dark mode (#14732)

This commit is contained in:
KVOJJJin
2025-03-03 14:44:51 +08:00
committed by GitHub
parent e53052ab7a
commit d0d0bf570e
98 changed files with 3006 additions and 2496 deletions

View File

@@ -3,18 +3,16 @@ import type { FC } from 'react'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiBookmark3Line,
RiErrorWarningFill,
} from '@remixicon/react'
import { useBoolean, useClickAway } from 'ahooks'
import { XMarkIcon } from '@heroicons/react/24/outline'
import { useBoolean } from 'ahooks'
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 MenuDropdown from './menu-dropdown'
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'
@@ -26,6 +24,7 @@ import type {
TextToSpeechConfig,
} from '@/models/debug'
import AppIcon from '@/app/components/base/app-icon'
import Badge from '@/app/components/base/badge'
import { changeLanguage } from '@/i18n/i18next-config'
import Loading from '@/app/components/base/loading'
import { userInputsFormToPromptVariables } from '@/utils/model-config'
@@ -37,6 +36,8 @@ 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'
import LogoSite from '@/app/components/base/logo/logo-site'
import cn from '@/utils/classnames'
const GROUP_SIZE = 5 // to avoid RPM(Request per minute) limit. The group task finished then the next group.
enum TaskStatus {
@@ -72,8 +73,6 @@ const TextGeneration: FC<IMainProps> = ({
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'
@@ -102,6 +101,7 @@ const TextGeneration: FC<IMainProps> = ({
const [appId, setAppId] = useState<string>('')
const [siteInfo, setSiteInfo] = useState<SiteInfo | null>(null)
const [canReplaceLogo, setCanReplaceLogo] = useState<boolean>(false)
const [customConfig, setCustomConfig] = useState<Record<string, any> | null>(null)
const [promptConfig, setPromptConfig] = useState<PromptConfig | null>(null)
const [moreLikeThisConfig, setMoreLikeThisConfig] = useState<MoreLikeThisConfig | null>(null)
const [textToSpeechConfig, setTextToSpeechConfig] = useState<TextToSpeechConfig | null>(null)
@@ -142,7 +142,7 @@ const TextGeneration: FC<IMainProps> = ({
setAllTaskList([]) // clear batch task running status
// eslint-disable-next-line ts/no-use-before-define
showResSidebar()
showResultPanel()
}
const [controlRetry, setControlRetry] = useState(0)
@@ -323,7 +323,7 @@ const TextGeneration: FC<IMainProps> = ({
setControlStopResponding(Date.now())
// eslint-disable-next-line ts/no-use-before-define
showResSidebar()
showResultPanel()
}
const handleCompleted = (completionRes: string, taskId?: number, isSuccess?: boolean) => {
const allTaskListLatest = getLatestTaskList()
@@ -388,10 +388,11 @@ const TextGeneration: FC<IMainProps> = ({
useEffect(() => {
(async () => {
const [appData, appParams]: any = await fetchInitData()
const { app_id: appId, site: siteInfo, can_replace_logo } = appData
const { app_id: appId, site: siteInfo, can_replace_logo, custom_config } = appData
setAppId(appId)
setSiteInfo(siteInfo as SiteInfo)
setCanReplaceLogo(can_replace_logo)
setCustomConfig(custom_config)
changeLanguage(siteInfo.default_language)
const { user_input_form, more_like_this, file_upload, text_to_speech }: any = appParams
@@ -431,24 +432,21 @@ const TextGeneration: FC<IMainProps> = ({
icon_url: siteInfo?.icon_url,
})
const [isShowResSidebar, { setTrue: doShowResSidebar, setFalse: hideResSidebar }] = useBoolean(false)
const showResSidebar = () => {
const [isShowResultPanel, { setTrue: doShowResultPanel, setFalse: hideResultPanel }] = useBoolean(false)
const showResultPanel = () => {
// fix: useClickAway hideResSidebar will close sidebar
setTimeout(() => {
doShowResSidebar()
doShowResultPanel()
}, 0)
}
const resRef = useRef<HTMLDivElement>(null)
useClickAway(() => {
hideResSidebar()
}, resRef)
const [resultExisted, setResultExisted] = useState(false)
const renderRes = (task?: Task) => (<Res
key={task?.id}
isWorkflow={isWorkflow}
isCallBatchAPI={isCallBatchAPI}
isPC={isPC}
isMobile={isMobile}
isMobile={!isPC}
isInstalledApp={isInstalledApp}
installedAppInfo={installedAppInfo}
isError={task?.status === TaskStatus.failed}
@@ -458,7 +456,7 @@ const TextGeneration: FC<IMainProps> = ({
controlSend={controlSend}
controlRetry={task?.status === TaskStatus.failed ? controlRetry : 0}
controlStopResponding={controlStopResponding}
onShowRes={showResSidebar}
onShowRes={showResultPanel}
handleSaveMessage={handleSaveMessage}
taskId={task?.id}
onCompleted={handleCompleted}
@@ -466,77 +464,60 @@ const TextGeneration: FC<IMainProps> = ({
completionFiles={completionFiles}
isShowTextToSpeech={!!textToSpeechConfig?.enabled}
siteInfo={siteInfo}
onRunStart={() => setResultExisted(true)}
/>)
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 = (
<div
ref={resRef}
className={
cn(
'flex flex-col h-full shrink-0',
isPC ? 'px-10 py-8' : 'bg-gray-50',
isTablet && 'p-6', isMobile && 'p-4')
}
className={cn(
'relative flex flex-col h-full',
!isPC && 'h-[calc(100vh_-_36px)] rounded-t-2xl shadow-lg backdrop-blur-sm',
!isPC
? isShowResultPanel
? 'bg-background-default-burn'
: 'bg-components-panel-bg border-t-[0.5px] border-divider-regular'
: 'bg-chatbot-bg',
)}
>
<>
<div className='flex items-center justify-between shrink-0'>
<div className='flex items-center space-x-3'>
<div className={s.starIcon}></div>
<div className='text-lg font-semibold text-gray-800'>{t('share.generation.title')}</div>
</div>
<div className='flex items-center space-x-2'>
{allFailedTaskList.length > 0 && (
<div className='flex items-center'>
<RiErrorWarningFill className='w-4 h-4 text-[#D92D20]' />
<div className='ml-1 text-[#D92D20]'>{t('share.generation.batchFailed.info', { num: allFailedTaskList.length })}</div>
<Button
variant='primary'
className='ml-2'
onClick={handleRetryAllFailedTask}
>{t('share.generation.batchFailed.retry')}</Button>
<div className='mx-3 w-[1px] h-3.5 bg-gray-200'></div>
</div>
)}
{allSuccessTaskList.length > 0 && (
<ResDownload
isMobile={isMobile}
values={exportRes}
/>
)}
{!isPC && (
<div
className='flex items-center justify-center cursor-pointer'
onClick={hideResSidebar}
>
<XMarkIcon className='w-4 h-4 text-gray-800' />
</div>
)}
</div>
</div>
<div className='overflow-y-auto grow'>
{!isCallBatchAPI ? renderRes() : renderBatchRes()}
{!noPendingTask && (
<div className='mt-4'>
<Loading type='area' />
</div>
{isCallBatchAPI && (
<div className={cn(
'shrink-0 px-14 pt-9 pb-2 flex items-center justify-between',
!isPC && 'px-4 pt-3 pb-1',
)}>
<div className='text-text-primary system-md-semibold-uppercase'>{t('share.generation.executions', { num: allTaskList.length })}</div>
{allSuccessTaskList.length > 0 && (
<ResDownload
isMobile={!isPC}
values={exportRes}
/>
)}
</div>
</>
)}
<div className={cn(
'grow flex flex-col h-0 overflow-y-auto',
isPC && 'px-14 py-8',
isPC && isCallBatchAPI && 'pt-0',
!isPC && 'p-0 pb-2',
)}>
{!isCallBatchAPI ? renderRes() : renderBatchRes()}
{!noPendingTask && (
<div className='mt-4'>
<Loading type='area' />
</div>
)}
</div>
{isCallBatchAPI && allFailedTaskList.length > 0 && (
<div className='z-10 absolute bottom-6 left-1/2 -translate-x-1/2 flex items-center gap-2 p-3 rounded-xl bg-components-panel-bg-blur backdrop-blur-sm border border-components-panel-border shadow-lg'>
<RiErrorWarningFill className='w-4 h-4 text-text-destructive' />
<div className='text-text-secondary system-sm-medium'>{t('share.generation.batchFailed.info', { num: allFailedTaskList.length })}</div>
<div className='w-px h-3.5 bg-divider-regular'></div>
<div onClick={handleRetryAllFailedTask} className='text-text-accent system-sm-semibold-uppercase cursor-pointer'>{t('share.generation.batchFailed.retry')}</div>
</div>
)}
</div>
)
@@ -548,46 +529,34 @@ const TextGeneration: FC<IMainProps> = ({
}
return (
<>
<div className={cn(
'bg-background-default-burn',
isPC && 'flex',
!isPC && 'flex-col',
isInstalledApp ? 'h-full rounded-2xl shadow-md' : 'h-screen',
)}>
{/* Left */}
<div className={cn(
isPC && 'flex',
isInstalledApp ? s.installedApp : 'h-screen',
'bg-gray-50',
'shrink-0 relative flex flex-col h-full',
isPC ? 'w-[600px] max-w-[50%]' : resultExisted ? 'h-[calc(100%_-_64px)]' : '',
isInstalledApp && 'rounded-l-2xl',
)}>
{/* Left */}
<div className={cn(
isPC ? 'w-[600px] max-w-[50%] p-8' : 'p-4',
isInstalledApp && 'rounded-l-2xl',
'shrink-0 relative flex flex-col pb-10 h-full border-r border-gray-100 bg-white',
)}>
<div className='mb-6'>
<div className='flex items-center justify-between'>
<div className='flex items-center space-x-3'>
<AppIcon
size="small"
iconType={siteInfo.icon_type}
icon={siteInfo.icon}
background={siteInfo.icon_background || appDefaultIconBackground}
imageUrl={siteInfo.icon_url}
/>
<div className='text-lg font-semibold text-gray-800'>{siteInfo.title}</div>
</div>
{!isPC && (
<Button
className='shrink-0 ml-2'
onClick={showResSidebar}
>
<div className='flex items-center space-x-2 text-primary-600 text-[13px] font-medium'>
<div className={s.starIcon}></div>
<span>{t('share.generation.title')}</span>
</div>
</Button>
)}
</div>
{siteInfo.description && (
<div className='mt-2 text-xs text-gray-500'>{siteInfo.description}</div>
)}
{/* header */}
<div className={cn('shrink-0 space-y-4 border-b border-divider-subtle', isPC ? 'p-8 pb-0 bg-components-panel-bg' : 'p-4 pb-0')}>
<div className='flex items-center gap-3'>
<AppIcon
size={isPC ? 'large' : 'small'}
iconType={siteInfo.icon_type}
icon={siteInfo.icon}
background={siteInfo.icon_background || appDefaultIconBackground}
imageUrl={siteInfo.icon_url}
/>
<div className='grow text-text-secondary system-md-semibold truncate'>{siteInfo.title}</div>
<MenuDropdown data={siteInfo} />
</div>
{siteInfo.description && (
<div className='system-xs-regular text-text-tertiary'>{siteInfo.description}</div>
)}
<TabHeader
items={[
{ id: 'create', name: t('share.generation.tabs.create') },
@@ -597,11 +566,12 @@ const TextGeneration: FC<IMainProps> = ({
id: 'saved',
name: t('share.generation.tabs.saved'),
isRight: true,
icon: <RiBookmark3Line className='w-4 h-4' />,
extra: savedMessages.length > 0
? (
<div className='ml-1 flex items-center h-5 px-1.5 rounded-md border border-gray-200 text-gray-500 text-xs font-medium'>
<Badge className='ml-1'>
{savedMessages.length}
</div>
</Badge>
)
: null,
}]
@@ -610,72 +580,89 @@ const TextGeneration: FC<IMainProps> = ({
value={currentTab}
onChange={setCurrentTab}
/>
<div className='h-20 overflow-y-auto grow'>
<div className={cn(currentTab === 'create' ? 'block' : 'hidden')}>
<RunOnce
siteInfo={siteInfo}
inputs={inputs}
inputsRef={inputsRef}
onInputsChange={setInputs}
promptConfig={promptConfig}
onSend={handleSend}
visionConfig={visionConfig}
onVisionFilesChange={setCompletionFiles}
/>
</div>
<div className={cn(isInBatchTab ? 'block' : 'hidden')}>
<RunBatch
vars={promptConfig.prompt_variables}
onSend={handleRunBatch}
isAllFinished={allTasksRun}
/>
</div>
{currentTab === 'saved' && (
<SavedItems
className='mt-4'
isShowTextToSpeech={textToSpeechConfig?.enabled}
list={savedMessages}
onRemove={handleRemoveSavedMessage}
onStartCreateContent={() => setCurrentTab('create')}
/>
)}
</div>
{/* form */}
<div className={cn(
'grow h-0 bg-components-panel-bg overflow-y-auto',
isPC ? 'px-8' : 'px-4',
!isPC && resultExisted && customConfig?.remove_webapp_brand && 'rounded-b-2xl border-b-[0.5px] border-divider-regular',
)}>
<div className={cn(currentTab === 'create' ? 'block' : 'hidden')}>
<RunOnce
siteInfo={siteInfo}
inputs={inputs}
inputsRef={inputsRef}
onInputsChange={setInputs}
promptConfig={promptConfig}
onSend={handleSend}
visionConfig={visionConfig}
onVisionFilesChange={setCompletionFiles}
/>
</div>
{/* copyright */}
<div className={cn(isInBatchTab ? 'block' : 'hidden')}>
<RunBatch
vars={promptConfig.prompt_variables}
onSend={handleRunBatch}
isAllFinished={allTasksRun}
/>
</div>
{currentTab === 'saved' && (
<SavedItems
className={cn(isPC ? 'mt-6' : 'mt-4')}
isShowTextToSpeech={textToSpeechConfig?.enabled}
list={savedMessages}
onRemove={handleRemoveSavedMessage}
onStartCreateContent={() => setCurrentTab('create')}
/>
)}
</div>
{/* powered by */}
{!customConfig?.remove_webapp_brand && (
<div className={cn(
isInstalledApp ? 'left-[248px]' : 'left-8',
'fixed bottom-4 flex space-x-2 text-gray-400 font-normal text-xs',
'shrink-0 py-3 flex items-center gap-1.5 bg-components-panel-bg',
isPC ? 'px-8' : 'px-4',
!isPC && resultExisted && 'rounded-b-2xl border-b-[0.5px] border-divider-regular',
)}>
{siteInfo.copyright && (
<div className="">© {(new Date()).getFullYear()} {siteInfo.copyright}</div>
<div className='text-text-tertiary system-2xs-medium-uppercase'>{t('share.chat.poweredBy')}</div>
{customConfig?.replace_webapp_logo && (
<img src={customConfig?.replace_webapp_logo} alt='logo' className='block w-auto h-5' />
)}
{siteInfo.privacy_policy && (
<>
{siteInfo.copyright && <div>·</div>}
<div>{t('share.chat.privacyPolicyLeft')}
<a
className='text-gray-500 px-1'
href={siteInfo.privacy_policy}
target='_blank' rel='noopener noreferrer'>{t('share.chat.privacyPolicyMiddle')}</a>
{t('share.chat.privacyPolicyRight')}
</div>
</>
{!customConfig?.replace_webapp_logo && (
<LogoSite className='!h-5' />
)}
</div>
</div>
{/* Result */}
<div
className={resWrapClassNames}
style={{
background: (!isPC && isShowResSidebar) ? 'rgba(35, 56, 118, 0.2)' : 'none',
}}
>
{renderResWrap}
</div>
)}
</div>
</>
{/* Result */}
<div className={cn(
isPC
? 'grow h-full'
: isShowResultPanel
? 'fixed z-50 inset-0 bg-background-overlay backdrop-blur-sm'
: resultExisted
? 'relative shrink-0 h-16 pt-2.5 bg-background-default-burn overflow-hidden'
: '',
)}>
{!isPC && (
<div
className={cn(
isShowResultPanel
? 'p-2 pt-6 flex items-center justify-center'
: 'z-10 absolute top-0 left-0 w-full px-2 pt-[3px] pb-[57px] flex items-center justify-center',
)}
onClick={() => {
if (isShowResultPanel)
hideResultPanel()
else
showResultPanel()
}}
>
<div className='w-8 h-1 rounded bg-divider-solid cursor-grab'/>
</div>
)}
{renderResWrap}
</div>
</div>
)
}

View File

@@ -0,0 +1,49 @@
import React from 'react'
import Modal from '@/app/components/base/modal'
import AppIcon from '@/app/components/base/app-icon'
import type { SiteInfo } from '@/models/share'
import { appDefaultIconBackground } from '@/config'
import cn from 'classnames'
type Props = {
data?: SiteInfo
isShow: boolean
onClose: () => void
}
const InfoModal = ({
isShow,
onClose,
data,
}: Props) => {
return (
<Modal
isShow={isShow}
onClose={onClose}
className='!p-0 min-w-[400px] max-w-[400px]'
closable
>
<div className={cn('pt-10 px-4 pb-8 flex flex-col items-center gap-4')}>
<AppIcon
size='xxl'
iconType={data?.icon_type}
icon={data?.icon}
background={data?.icon_background || appDefaultIconBackground}
imageUrl={data?.icon_url}
/>
<div className='text-text-secondary system-xl-semibold'>{data?.title}</div>
<div className='text-text-tertiary system-xs-regular'>
{/* copyright */}
{data?.copyright && (
<div>© {(new Date()).getFullYear()} {data?.copyright}</div>
)}
{data?.custom_disclaimer && (
<div className='mt-2'>{data.custom_disclaimer}</div>
)}
</div>
</div>
</Modal>
)
}
export default InfoModal

View File

@@ -0,0 +1,91 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import type { Placement } from '@floating-ui/react'
import {
RiEqualizer2Line,
} from '@remixicon/react'
import ActionButton from '@/app/components/base/action-button'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import InfoModal from './info-modal'
import type { SiteInfo } from '@/models/share'
import cn from '@/utils/classnames'
type Props = {
data?: SiteInfo
placement?: Placement
}
const MenuDropdown: FC<Props> = ({
data,
placement,
}) => {
const { t } = useTranslation()
const [open, doSetOpen] = useState(false)
const openRef = useRef(open)
const setOpen = useCallback((v: boolean) => {
doSetOpen(v)
openRef.current = v
}, [doSetOpen])
const handleTrigger = useCallback(() => {
setOpen(!openRef.current)
}, [setOpen])
const [show, setShow] = useState(false)
return (
<>
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement={placement || 'bottom-end'}
offset={{
mainAxis: 4,
crossAxis: -4,
}}
>
<PortalToFollowElemTrigger onClick={handleTrigger}>
<div>
<ActionButton size='l' className={cn(open && 'bg-state-base-hover')}>
<RiEqualizer2Line className='w-[18px] h-[18px]' />
</ActionButton>
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-50'>
<div className='w-[224px] bg-components-panel-bg-blur backdrop-blur-sm rounded-xl border-[0.5px] border-components-panel-border shadow-lg'>
<div className='p-1'>
{data?.privacy_policy && (
<a href={data.privacy_policy} target='_blank' className='flex items-center px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover'>
<span className='grow'>{t('share.chat.privacyPolicyMiddle')}</span>
</a>
)}
<div
onClick={() => {
handleTrigger()
setShow(true)
}}
className='px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover'
>{t('common.userProfile.about')}</div>
</div>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
{show && (
<InfoModal
isShow={show}
onClose={() => {
setShow(false)
}}
data={data}
/>
)}
</>
)
}
export default React.memo(MenuDropdown)

View File

@@ -1,22 +1,18 @@
import type { FC } from 'react'
import React from 'react'
import {
RiSparklingFill,
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
const StarIcon = (
<svg width="50" height="50" viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.50033 48.3337V36.667M7.50033 13.3337V1.66699M1.66699 7.50033H13.3337M1.66699 42.5003H13.3337M27.3337 4.00032L23.2872 14.521C22.6292 16.2319 22.3002 17.0873 21.7886 17.8069C21.3351 18.4446 20.7779 19.0018 20.1402 19.4552C19.4206 19.9669 18.5652 20.2959 16.8543 20.9539L6.33366 25.0003L16.8543 29.0467C18.5652 29.7048 19.4206 30.0338 20.1402 30.5454C20.7779 30.9989 21.3351 31.5561 21.7886 32.1938C22.3002 32.9133 22.6292 33.7688 23.2872 35.4796L27.3337 46.0003L31.3801 35.4796C32.0381 33.7688 32.3671 32.9133 32.8788 32.1938C33.3322 31.5561 33.8894 30.9989 34.5271 30.5454C35.2467 30.0338 36.1021 29.7048 37.813 29.0467L48.3337 25.0003L37.813 20.9539C36.1021 20.2959 35.2467 19.9669 34.5271 19.4552C33.8894 19.0018 33.3322 18.4446 32.8788 17.8069C32.3671 17.0873 32.0381 16.2319 31.3801 14.521L27.3337 4.00032Z" stroke="#EAECF0" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" />
</svg>
)
export type INoDataProps = {}
const NoData: FC<INoDataProps> = () => {
const { t } = useTranslation()
return (
<div className='flex flex-col h-full w-full justify-center items-center'>
{StarIcon}
<RiSparklingFill className='w-12 h-12 text-text-empty-state-icon' />
<div
className='mt-3 text-gray-300 text-xs leading-3'
className='mt-2 text-text-quaternary system-sm-regular'
>
{t('share.generation.noData')}
</div>

View File

@@ -4,7 +4,6 @@ import React, { useEffect, useRef, useState } from 'react'
import { useBoolean } from 'ahooks'
import { t } from 'i18next'
import produce from 'immer'
import cn from '@/utils/classnames'
import TextGenerationRes from '@/app/components/app/text-generate/item'
import NoData from '@/app/components/share/text-generation/no-data'
import Toast from '@/app/components/base/toast'
@@ -13,7 +12,6 @@ import type { FeedbackType } from '@/app/components/base/chat/chat/type'
import Loading from '@/app/components/base/loading'
import type { PromptConfig } from '@/models/debug'
import type { InstalledApp } from '@/models/explore'
import type { ModerationService } from '@/models/common'
import { TransferMethod, type VisionFile, type VisionSettings } from '@/types/app'
import { NodeRunningStatus, WorkflowRunningStatus } from '@/app/components/workflow/types'
import type { WorkflowProcess } from '@/app/components/base/chat/types'
@@ -24,7 +22,7 @@ import {
getFilesInLogs,
} from '@/app/components/base/file-uploader/utils'
export interface IResultProps {
export type IResultProps = {
isWorkflow: boolean
isCallBatchAPI: boolean
isPC: boolean
@@ -43,11 +41,10 @@ export interface IResultProps {
handleSaveMessage: (messageId: string) => void
taskId?: number
onCompleted: (completionRes: string, taskId?: number, success?: boolean) => void
enableModeration?: boolean
moderationService?: (text: string) => ReturnType<ModerationService>
visionConfig: VisionSettings
completionFiles: VisionFile[]
siteInfo: SiteInfo | null
onRunStart: () => void
}
const Result: FC<IResultProps> = ({
@@ -72,6 +69,7 @@ const Result: FC<IResultProps> = ({
visionConfig,
completionFiles,
siteInfo,
onRunStart,
}) => {
const [isResponding, { setTrue: setRespondingTrue, setFalse: setRespondingFalse }] = useBoolean(false)
useEffect(() => {
@@ -183,8 +181,10 @@ const Result: FC<IResultProps> = ({
let res: string[] = []
let tempMessageId = ''
if (!isPC)
if (!isPC) {
onShowRes()
onRunStart()
}
setRespondingTrue()
let isEnd = false
@@ -375,7 +375,6 @@ const Result: FC<IResultProps> = ({
<TextGenerationRes
isWorkflow={isWorkflow}
workflowProcessData={workflowProcessData}
className='mt-3'
isError={isError}
onRetry={handleSend}
content={completionRes}
@@ -398,7 +397,7 @@ const Result: FC<IResultProps> = ({
)
return (
<div className={cn(isNoData && !isCallBatchAPI && 'h-full')}>
<>
{!isCallBatchAPI && !isWorkflow && (
(isResponding && !completionRes)
? (
@@ -414,25 +413,19 @@ const Result: FC<IResultProps> = ({
</>
)
)}
{
!isCallBatchAPI && isWorkflow && (
(isResponding && !workflowProcessData)
? (
<div className='flex h-full w-full justify-center items-center'>
<Loading type='area' />
</div>
)
: !workflowProcessData
? <NoData />
: renderTextGenerationRes()
)
}
{isCallBatchAPI && (
<div className='mt-2'>
{renderTextGenerationRes()}
</div>
{!isCallBatchAPI && isWorkflow && (
(isResponding && !workflowProcessData)
? (
<div className='flex h-full w-full justify-center items-center'>
<Loading type='area' />
</div>
)
: !workflowProcessData
? <NoData />
: renderTextGenerationRes()
)}
</div>
{isCallBatchAPI && renderTextGenerationRes()}
</>
)
}
export default React.memo(Result)

View File

@@ -27,17 +27,17 @@ const CSVDownload: FC<ICSVDownloadProps> = ({
return (
<div className='mt-6'>
<div className='text-sm text-gray-900 font-medium'>{t('share.generation.csvStructureTitle')}</div>
<div className='system-sm-medium text-text-primary'>{t('share.generation.csvStructureTitle')}</div>
<div className='mt-2 max-h-[500px] overflow-auto'>
<table className='w-full border-separate border-spacing-0 border border-gray-200 rounded-lg text-xs'>
<thead className='text-gray-500'>
<table className='table-fixed w-full border-separate border-spacing-0 border border-divider-regular rounded-lg text-xs'>
<thead className='text-text-tertiary'>
<tr>
{addQueryContentVars.map((item, i) => (
<td key={i} className='h-9 pl-4 border-b border-gray-200'>{item.name}</td>
<td key={i} className='h-9 pl-3 pr-2 border-b border-divider-regular'>{item.name}</td>
))}
</tr>
</thead>
<tbody className='text-gray-300'>
<tbody className='text-text-secondary'>
<tr>
{addQueryContentVars.map((item, i) => (
<td key={i} className='h-9 pl-4'>{item.name} {t('share.generation.field')}</td>
@@ -58,7 +58,7 @@ const CSVDownload: FC<ICSVDownloadProps> = ({
template,
]}
>
<div className='flex items-center h-[18px] space-x-1 text-[#155EEF] text-xs font-medium'>
<div className='flex items-center h-[18px] space-x-1 text-text-accent system-xs-medium'>
<DownloadIcon className='w-3 h-3' />
<span>{t('share.generation.downloadTemplate')}</span>
</div>

View File

@@ -5,7 +5,6 @@ import {
useCSVReader,
} from 'react-papaparse'
import { useTranslation } from 'react-i18next'
import s from './style.module.css'
import cn from '@/utils/classnames'
import { Csv as CSVIcon } from '@/app/components/base/icons/src/public/files'
@@ -41,7 +40,11 @@ const CSVReader: FC<Props> = ({
<>
<div
{...getRootProps()}
className={cn(s.zone, zoneHover && s.zoneHover, acceptedFile ? 'px-6' : 'justify-center border-dashed text-gray-500')}
className={cn(
'flex items-center h-20 rounded-xl bg-components-dropzone-bg border border-dashed border-components-dropzone-border system-sm-regular',
acceptedFile && 'px-6 bg-components-panel-on-panel-item-bg border-solid border-components-panel-border hover:bg-components-panel-on-panel-item-bg-hover hover:border-components-panel-bg-blur',
zoneHover && 'bg-components-dropzone-bg-accent border border-components-dropzone-border-accent',
)}
>
{
acceptedFile
@@ -49,15 +52,15 @@ const CSVReader: FC<Props> = ({
<div className='w-full flex items-center space-x-2'>
<CSVIcon className="shrink-0" />
<div className='flex w-0 grow'>
<span className='max-w-[calc(100%_-_30px)] text-ellipsis whitespace-nowrap overflow-hidden text-gray-800'>{acceptedFile.name.replace(/.csv$/, '')}</span>
<span className='shrink-0 text-gray-500'>.csv</span>
<span className='max-w-[calc(100%_-_30px)] truncate text-text-secondary'>{acceptedFile.name.replace(/.csv$/, '')}</span>
<span className='shrink-0 text-text-tertiary'>.csv</span>
</div>
</div>
)
: (
<div className='flex items-center justify-center space-x-2'>
<div className='w-full flex items-center justify-center space-x-2'>
<CSVIcon className="shrink-0" />
<div className='text-gray-500'>{t('share.generation.csvUploadTitle')}<span className='text-primary-400'>{t('share.generation.browse')}</span></div>
<div className='text-text-tertiary'>{t('share.generation.csvUploadTitle')}<span className='text-text-accent cursor-pointer'>{t('share.generation.browse')}</span></div>
</div>
)}
</div>

View File

@@ -1,11 +0,0 @@
.zone {
@apply flex items-center h-20 rounded-xl bg-gray-50 border border-gray-200 cursor-pointer text-sm font-normal;
}
.zoneHover {
@apply border-solid bg-gray-100;
}
.info {
@apply text-gray-800 text-sm;
}

View File

@@ -1,17 +1,16 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import {
PlayIcon,
} from '@heroicons/react/24/solid'
import { useTranslation } from 'react-i18next'
import {
RiLoader2Line,
RiPlayLargeLine,
} from '@remixicon/react'
import CSVReader from './csv-reader'
import CSVDownload from './csv-download'
import cn from '@/utils/classnames'
import Button from '@/app/components/base/button'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import cn from '@/utils/classnames'
export type IRunBatchProps = {
vars: { name: string }[]
onSend: (data: string[][]) => void
@@ -24,6 +23,8 @@ const RunBatch: FC<IRunBatchProps> = ({
isAllFinished,
}) => {
const { t } = useTranslation()
const media = useBreakpoints()
const isPC = media === MediaType.pc
const [csvData, setCsvData] = React.useState<string[][]>([])
const [isParsed, setIsParsed] = React.useState(false)
@@ -36,16 +37,15 @@ const RunBatch: FC<IRunBatchProps> = ({
const handleSend = () => {
onSend(csvData)
}
const Icon = isAllFinished ? PlayIcon : RiLoader2Line
const Icon = isAllFinished ? RiPlayLargeLine : RiLoader2Line
return (
<div className='pt-4'>
<CSVReader onParsed={handleParsed} />
<CSVDownload vars={vars} />
<div className='mt-4 h-[1px] bg-gray-100'></div>
<div className='flex justify-end'>
<Button
variant="primary"
className='mt-4 pl-3 pr-4'
className={cn('mt-4 pl-3 pr-4', !isPC && 'grow')}
onClick={handleSend}
disabled={!isParsed || !isAllFinished}
>

View File

@@ -1,13 +1,15 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { RiDownloadLine } from '@remixicon/react'
import {
useCSVDownloader,
} from 'react-papaparse'
import { useTranslation } from 'react-i18next'
import cn from '@/utils/classnames'
import { Download02 as DownloadIcon } from '@/app/components/base/icons/src/vender/solid/general'
import ActionButton from '@/app/components/base/action-button'
import Button from '@/app/components/base/button'
import cn from '@/utils/classnames'
export type IResDownloadProps = {
isMobile: boolean
values: Record<string, string>[]
@@ -31,10 +33,17 @@ const ResDownload: FC<IResDownloadProps> = ({
}}
data={values}
>
<Button className={cn('space-x-2 bg-white', isMobile ? '!p-0 !w-8 justify-center' : '')}>
<DownloadIcon className='w-4 h-4 text-[#155EEF]' />
{!isMobile && <span className='text-[#155EEF]'>{t('common.operation.download')}</span>}
</Button>
{isMobile && (
<ActionButton>
<RiDownloadLine className='w-4 h-4' />
</ActionButton>
)}
{!isMobile && (
<Button className={cn('space-x-1')}>
<RiDownloadLine className='w-4 h-4' />
<span>{t('common.operation.download')}</span>
</Button>
)}
</CSVDownloader>
)
}

View File

@@ -2,18 +2,21 @@ import type { FC, FormEvent } from 'react'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import {
PlayIcon,
} from '@heroicons/react/24/solid'
RiPlayLargeLine,
} from '@remixicon/react'
import Select from '@/app/components/base/select'
import type { SiteInfo } from '@/models/share'
import type { PromptConfig } from '@/models/debug'
import Button from '@/app/components/base/button'
import Textarea from '@/app/components/base/textarea'
import Input from '@/app/components/base/input'
import { DEFAULT_VALUE_MAX_LEN } from '@/config'
import TextGenerationImageUploader from '@/app/components/base/image-uploader/text-generation-image-uploader'
import type { VisionFile, VisionSettings } from '@/types/app'
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
import { getProcessedFiles } from '@/app/components/base/file-uploader/utils'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import cn from '@/utils/classnames'
export type IRunOnceProps = {
siteInfo: SiteInfo
@@ -35,6 +38,8 @@ const RunOnce: FC<IRunOnceProps> = ({
onVisionFilesChange,
}) => {
const { t } = useTranslation()
const media = useBreakpoints()
const isPC = media === MediaType.pc
const onClear = () => {
const newInputs: Record<string, any> = {}
@@ -61,8 +66,8 @@ const RunOnce: FC<IRunOnceProps> = ({
<form onSubmit={onSubmit}>
{promptConfig.prompt_variables.map(item => (
<div className='w-full mt-4' key={item.key}>
<label className='text-gray-900 text-sm font-medium'>{item.name}</label>
<div className='mt-2'>
<label className='h-6 flex items-center text-text-secondary system-md-semibold'>{item.name}</label>
<div className='mt-1'>
{item.type === 'select' && (
<Select
className='w-full'
@@ -70,13 +75,11 @@ const RunOnce: FC<IRunOnceProps> = ({
onSelect={(i) => { handleInputsChange({ ...inputsRef.current, [item.key]: i.value }) }}
items={(item.options || []).map(i => ({ name: i, value: i }))}
allowSearch={false}
bgClassName='bg-gray-50'
/>
)}
{item.type === 'string' && (
<input
<Input
type="text"
className="block w-full p-2 text-gray-900 border border-gray-300 rounded-lg bg-gray-50 sm:text-xs focus:ring-blue-500 focus:border-blue-500 "
placeholder={`${item.name}${!item.required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
value={inputs[item.key]}
onChange={(e) => { handleInputsChange({ ...inputsRef.current, [item.key]: e.target.value }) }}
@@ -92,9 +95,8 @@ const RunOnce: FC<IRunOnceProps> = ({
/>
)}
{item.type === 'number' && (
<input
<Input
type="number"
className="block w-full p-2 text-gray-900 border border-gray-300 rounded-lg bg-gray-50 sm:text-xs focus:ring-blue-500 focus:border-blue-500 "
placeholder={`${item.name}${!item.required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
value={inputs[item.key]}
onChange={(e) => { handleInputsChange({ ...inputsRef.current, [item.key]: e.target.value }) }}
@@ -124,8 +126,8 @@ const RunOnce: FC<IRunOnceProps> = ({
{
visionConfig?.enabled && (
<div className="w-full mt-4">
<div className="text-gray-900 text-sm font-medium">{t('common.imageUploader.imageUpload')}</div>
<div className='mt-2'>
<div className="h-6 flex items-center text-text-secondary system-md-semibold">{t('common.imageUploader.imageUpload')}</div>
<div className='mt-1'>
<TextGenerationImageUploader
settings={visionConfig}
onFilesChange={files => onVisionFilesChange(files.filter(file => file.progress !== -1).map(fileItem => ({
@@ -139,11 +141,8 @@ const RunOnce: FC<IRunOnceProps> = ({
</div>
)
}
{promptConfig.prompt_variables.length > 0 && (
<div className='mt-4 h-[1px] bg-gray-100'></div>
)}
<div className='w-full mt-4'>
<div className="flex items-center justify-between">
<div className='w-full mt-6 mb-3'>
<div className="flex items-center justify-between gap-2">
<Button
onClick={onClear}
disabled={false}
@@ -151,11 +150,12 @@ const RunOnce: FC<IRunOnceProps> = ({
<span className='text-[13px]'>{t('common.operation.clear')}</span>
</Button>
<Button
className={cn(!isPC && 'grow')}
type='submit'
variant="primary"
disabled={false}
>
<PlayIcon className="shrink-0 w-4 h-4 mr-1" aria-hidden="true" />
<RiPlayLargeLine className="shrink-0 w-4 h-4 mr-1" aria-hidden="true" />
<span className='text-[13px]'>{t('share.generation.run')}</span>
</Button>
</div>

View File

@@ -1,12 +0,0 @@
.installedApp {
height: 100%;
border-radius: 16px;
box-shadow: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03);
}
.starIcon {
width: 16px;
height: 16px;
background: url(./icons/star.svg) center center no-repeat;
background-size: contain;
}