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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
98 changed files with 3006 additions and 2496 deletions

View File

@ -1,6 +1,7 @@
import React from 'react' import React from 'react'
import type { FC } from 'react' import type { FC } from 'react'
import type { Metadata } from 'next' import type { Metadata } from 'next'
import { SharePageContextProvider } from '@/context/share-page-context'
export const metadata: Metadata = { export const metadata: Metadata = {
icons: 'data:,', // prevent browser from using default favicon icons: 'data:,', // prevent browser from using default favicon
@ -11,7 +12,9 @@ const Layout: FC<{
}> = ({ children }) => { }> = ({ children }) => {
return ( return (
<div className="min-w-[300px] h-full pb-[env(safe-area-inset-bottom)]"> <div className="min-w-[300px] h-full pb-[env(safe-area-inset-bottom)]">
{children} <SharePageContextProvider>
{children}
</SharePageContextProvider>
</div> </div>
) )
} }

View File

@ -42,7 +42,7 @@ const CSVDownload: FC = () => {
<td className='h-9 pl-3 pr-2 border-b border-divider-regular'>{t('appAnnotation.batchModal.answer')}</td> <td className='h-9 pl-3 pr-2 border-b border-divider-regular'>{t('appAnnotation.batchModal.answer')}</td>
</tr> </tr>
</thead> </thead>
<tbody className='text-gray-700'> <tbody className='text-text-secondary'>
<tr> <tr>
<td className='h-9 pl-3 pr-2 border-b border-divider-subtle text-[13px]'>{t('appAnnotation.batchModal.question')} 1</td> <td className='h-9 pl-3 pr-2 border-b border-divider-subtle text-[13px]'>{t('appAnnotation.batchModal.question')} 1</td>
<td className='h-9 pl-3 pr-2 border-b border-divider-subtle text-[13px]'>{t('appAnnotation.batchModal.answer')} 1</td> <td className='h-9 pl-3 pr-2 border-b border-divider-subtle text-[13px]'>{t('appAnnotation.batchModal.answer')} 1</td>

View File

@ -124,18 +124,9 @@ const TextGenerationItem: FC<TextGenerationItemProps> = ({
doSend(v.payload.message, v.payload.files) doSend(v.payload.message, v.payload.files)
}) })
const varList = modelConfig.configs.prompt_variables.map((item: any) => {
return {
label: item.key,
value: inputs[item.key],
}
})
return ( return (
<TextGeneration <TextGeneration
className='flex flex-col h-full overflow-y-auto border-none' className='flex flex-col h-full overflow-y-auto border-none'
innerClassName='grow flex flex-col'
contentClassName='grow'
content={completion} content={completion}
isLoading={!completion && isResponding} isLoading={!completion && isResponding}
isResponding={isResponding} isResponding={isResponding}
@ -144,8 +135,7 @@ const TextGenerationItem: FC<TextGenerationItemProps> = ({
messageId={messageId} messageId={messageId}
isError={false} isError={false}
onRetry={() => { }} onRetry={() => { }}
appId={appId} inSidePanel
varList={varList}
/> />
) )
} }

View File

@ -516,9 +516,6 @@ const Debug: FC<IDebug> = ({
messageId={messageId} messageId={messageId}
isError={false} isError={false}
onRetry={() => { }} onRetry={() => { }}
supportAnnotation
appId={appId}
varList={varList}
siteInfo={null} siteInfo={null}
/> />
</div> </div>

View File

@ -416,10 +416,7 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
supportFeedback supportFeedback
feedback={detail.message.feedbacks.find((item: any) => item.from_source === 'admin')} feedback={detail.message.feedbacks.find((item: any) => item.from_source === 'admin')}
onFeedback={feedback => onFeedback(detail.message.id, feedback)} onFeedback={feedback => onFeedback(detail.message.id, feedback)}
supportAnnotation
isShowTextToSpeech isShowTextToSpeech
appId={appDetail?.id}
varList={varList}
siteInfo={null} siteInfo={null}
/> />
</div> </div>

View File

@ -1,26 +0,0 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { format } from '@/service/base'
export type ITextGenerationProps = {
value: string
className?: string
}
const TextGeneration: FC<ITextGenerationProps> = ({
value,
className,
}) => {
return (
<div
className={className}
dangerouslySetInnerHTML={{
__html: format(value),
}}
>
</div>
)
}
export default React.memo(TextGeneration)

View File

@ -1,39 +1,40 @@
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import React, { useEffect, useRef, useState } from 'react' import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { import {
RiBookmark3Line,
RiClipboardLine, RiClipboardLine,
RiFileList3Line,
RiPlayList2Line,
RiReplay15Line,
RiSparklingFill,
RiSparklingLine,
RiThumbDownLine,
RiThumbUpLine,
} from '@remixicon/react' } from '@remixicon/react'
import copy from 'copy-to-clipboard' import copy from 'copy-to-clipboard'
import { useParams } from 'next/navigation' import { useParams } from 'next/navigation'
import { HandThumbDownIcon, HandThumbUpIcon } from '@heroicons/react/24/outline'
import { useBoolean } from 'ahooks' import { useBoolean } from 'ahooks'
import { HashtagIcon } from '@heroicons/react/24/solid'
import ResultTab from './result-tab' import ResultTab from './result-tab'
import cn from '@/utils/classnames'
import { Markdown } from '@/app/components/base/markdown' import { Markdown } from '@/app/components/base/markdown'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import Toast from '@/app/components/base/toast' import Toast from '@/app/components/base/toast'
import AudioBtn from '@/app/components/base/audio-btn'
import type { FeedbackType } from '@/app/components/base/chat/chat/type' import type { FeedbackType } from '@/app/components/base/chat/chat/type'
import { fetchMoreLikeThis, updateFeedback } from '@/service/share' import { fetchMoreLikeThis, updateFeedback } from '@/service/share'
import { File02 } from '@/app/components/base/icons/src/vender/line/files'
import { Bookmark } from '@/app/components/base/icons/src/vender/line/general'
import { Stars02 } from '@/app/components/base/icons/src/vender/line/weather'
import { RefreshCcw01 } from '@/app/components/base/icons/src/vender/line/arrows'
import AnnotationCtrlBtn from '@/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-btn'
import { fetchTextGenerationMessage } from '@/service/debug' import { fetchTextGenerationMessage } from '@/service/debug'
import EditReplyModal from '@/app/components/app/annotation/edit-annotation-modal'
import { useStore as useAppStore } from '@/app/components/app/store' import { useStore as useAppStore } from '@/app/components/app/store'
import WorkflowProcessItem from '@/app/components/base/chat/chat/answer/workflow-process' import WorkflowProcessItem from '@/app/components/base/chat/chat/answer/workflow-process'
import type { WorkflowProcess } from '@/app/components/base/chat/types' import type { WorkflowProcess } from '@/app/components/base/chat/types'
import type { SiteInfo } from '@/models/share' import type { SiteInfo } from '@/models/share'
import { useChatContext } from '@/app/components/base/chat/chat/context' import { useChatContext } from '@/app/components/base/chat/chat/context'
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
import NewAudioButton from '@/app/components/base/new-audio-button'
import cn from '@/utils/classnames'
const MAX_DEPTH = 3 const MAX_DEPTH = 3
export interface IGenerationItemProps { export type IGenerationItemProps = {
isWorkflow?: boolean isWorkflow?: boolean
workflowProcessData?: WorkflowProcess workflowProcessData?: WorkflowProcess
className?: string className?: string
@ -56,31 +57,12 @@ export interface IGenerationItemProps {
taskId?: string taskId?: string
controlClearMoreLikeThis?: number controlClearMoreLikeThis?: number
supportFeedback?: boolean supportFeedback?: boolean
supportAnnotation?: boolean
isShowTextToSpeech?: boolean isShowTextToSpeech?: boolean
appId?: string
varList?: { label: string; value: string | number | object }[]
innerClassName?: string
contentClassName?: string
footerClassName?: string
hideProcessDetail?: boolean hideProcessDetail?: boolean
siteInfo: SiteInfo | null siteInfo: SiteInfo | null
inSidePanel?: boolean
} }
export const SimpleBtn = ({ className, isDisabled, onClick, children }: {
className?: string
isDisabled?: boolean
onClick?: () => void
children: React.ReactNode
}) => (
<div
className={cn(isDisabled ? 'border-gray-100 text-gray-300' : 'border-gray-200 text-gray-700 cursor-pointer hover:border-gray-300 hover:shadow-sm', 'flex items-center h-7 px-3 rounded-md border text-xs font-medium', className)}
onClick={() => !isDisabled && onClick?.()}
>
{children}
</div>
)
export const copyIcon = ( export const copyIcon = (
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.3335 2.33341C9.87598 2.33341 10.1472 2.33341 10.3698 2.39304C10.9737 2.55486 11.4454 3.02657 11.6072 3.63048C11.6668 3.85302 11.6668 4.12426 11.6668 4.66675V10.0334C11.6668 11.0135 11.6668 11.5036 11.4761 11.8779C11.3083 12.2072 11.0406 12.4749 10.7113 12.6427C10.337 12.8334 9.84692 12.8334 8.86683 12.8334H5.1335C4.1534 12.8334 3.66336 12.8334 3.28901 12.6427C2.95973 12.4749 2.69201 12.2072 2.52423 11.8779C2.3335 11.5036 2.3335 11.0135 2.3335 10.0334V4.66675C2.3335 4.12426 2.3335 3.85302 2.39313 3.63048C2.55494 3.02657 3.02665 2.55486 3.63056 2.39304C3.8531 2.33341 4.12435 2.33341 4.66683 2.33341M5.60016 3.50008H8.40016C8.72686 3.50008 8.89021 3.50008 9.01499 3.4365C9.12475 3.38058 9.21399 3.29134 9.26992 3.18158C9.3335 3.05679 9.3335 2.89345 9.3335 2.56675V2.10008C9.3335 1.77338 9.3335 1.61004 9.26992 1.48525C9.21399 1.37549 9.12475 1.28625 9.01499 1.23033C8.89021 1.16675 8.72686 1.16675 8.40016 1.16675H5.60016C5.27347 1.16675 5.11012 1.16675 4.98534 1.23033C4.87557 1.28625 4.78634 1.37549 4.73041 1.48525C4.66683 1.61004 4.66683 1.77338 4.66683 2.10008V2.56675C4.66683 2.89345 4.66683 3.05679 4.73041 3.18158C4.78634 3.29134 4.87557 3.38058 4.98534 3.4365C5.11012 3.50008 5.27347 3.50008 5.60016 3.50008Z" stroke="#344054" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" /> <path d="M9.3335 2.33341C9.87598 2.33341 10.1472 2.33341 10.3698 2.39304C10.9737 2.55486 11.4454 3.02657 11.6072 3.63048C11.6668 3.85302 11.6668 4.12426 11.6668 4.66675V10.0334C11.6668 11.0135 11.6668 11.5036 11.4761 11.8779C11.3083 12.2072 11.0406 12.4749 10.7113 12.6427C10.337 12.8334 9.84692 12.8334 8.86683 12.8334H5.1335C4.1534 12.8334 3.66336 12.8334 3.28901 12.6427C2.95973 12.4749 2.69201 12.2072 2.52423 11.8779C2.3335 11.5036 2.3335 11.0135 2.3335 10.0334V4.66675C2.3335 4.12426 2.3335 3.85302 2.39313 3.63048C2.55494 3.02657 3.02665 2.55486 3.63056 2.39304C3.8531 2.33341 4.12435 2.33341 4.66683 2.33341M5.60016 3.50008H8.40016C8.72686 3.50008 8.89021 3.50008 9.01499 3.4365C9.12475 3.38058 9.21399 3.29134 9.26992 3.18158C9.3335 3.05679 9.3335 2.89345 9.3335 2.56675V2.10008C9.3335 1.77338 9.3335 1.61004 9.26992 1.48525C9.21399 1.37549 9.12475 1.28625 9.01499 1.23033C8.89021 1.16675 8.72686 1.16675 8.40016 1.16675H5.60016C5.27347 1.16675 5.11012 1.16675 4.98534 1.23033C4.87557 1.28625 4.78634 1.37549 4.73041 1.48525C4.66683 1.61004 4.66683 1.77338 4.66683 2.10008V2.56675C4.66683 2.89345 4.66683 3.05679 4.73041 3.18158C4.78634 3.29134 4.87557 3.38058 4.98534 3.4365C5.11012 3.50008 5.27347 3.50008 5.60016 3.50008Z" stroke="#344054" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
@ -109,22 +91,16 @@ const GenerationItem: FC<IGenerationItemProps> = ({
taskId, taskId,
controlClearMoreLikeThis, controlClearMoreLikeThis,
supportFeedback, supportFeedback,
supportAnnotation,
isShowTextToSpeech, isShowTextToSpeech,
appId,
varList,
innerClassName,
contentClassName,
hideProcessDetail, hideProcessDetail,
siteInfo, siteInfo,
inSidePanel,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const params = useParams() const params = useParams()
const isTop = depth === 1 const isTop = depth === 1
const ref = useRef(null)
const [completionRes, setCompletionRes] = useState('') const [completionRes, setCompletionRes] = useState('')
const [childMessageId, setChildMessageId] = useState<string | null>(null) const [childMessageId, setChildMessageId] = useState<string | null>(null)
const hasChild = !!childMessageId
const [childFeedback, setChildFeedback] = useState<FeedbackType>({ const [childFeedback, setChildFeedback] = useState<FeedbackType>({
rating: null, rating: null,
}) })
@ -140,8 +116,6 @@ const GenerationItem: FC<IGenerationItemProps> = ({
setChildFeedback(childFeedback) setChildFeedback(childFeedback)
} }
const [isShowReplyModal, setIsShowReplyModal] = useState(false)
const question = (varList && varList?.length > 0) ? varList?.map(({ label, value }) => `${label}:${value}`).join('&') : ''
const [isQuerying, { setTrue: startQuerying, setFalse: stopQuerying }] = useBoolean(false) const [isQuerying, { setTrue: startQuerying, setFalse: stopQuerying }] = useBoolean(false)
const childProps = { const childProps = {
@ -161,6 +135,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
controlClearMoreLikeThis, controlClearMoreLikeThis,
isWorkflow, isWorkflow,
siteInfo, siteInfo,
taskId,
} }
const handleMoreLikeThis = async () => { const handleMoreLikeThis = async () => {
@ -178,19 +153,6 @@ const GenerationItem: FC<IGenerationItemProps> = ({
stopQuerying() stopQuerying()
} }
const mainStyle = (() => {
const res: React.CSSProperties = !isTop
? {
background: depth % 2 === 0 ? 'linear-gradient(90.07deg, #F9FAFB 0.05%, rgba(249, 250, 251, 0) 99.93%)' : '#fff',
}
: {}
if (hasChild)
res.boxShadow = '0px 1px 2px rgba(16, 24, 40, 0.05)'
return res
})()
useEffect(() => { useEffect(() => {
if (controlClearMoreLikeThis) { if (controlClearMoreLikeThis) {
setChildMessageId(null) setChildMessageId(null)
@ -228,123 +190,125 @@ const GenerationItem: FC<IGenerationItemProps> = ({
setShowPromptLogModal(true) setShowPromptLogModal(true)
} }
const ratingContent = (
<>
{!isWorkflow && !isError && messageId && !feedback?.rating && (
<SimpleBtn className="!px-0">
<>
<div
onClick={() => {
onFeedback?.({
rating: 'like',
})
}}
className='flex w-6 h-6 items-center justify-center rounded-md cursor-pointer hover:bg-gray-100'>
<HandThumbUpIcon width={16} height={16} />
</div>
<div
onClick={() => {
onFeedback?.({
rating: 'dislike',
})
}}
className='flex w-6 h-6 items-center justify-center rounded-md cursor-pointer hover:bg-gray-100'>
<HandThumbDownIcon width={16} height={16} />
</div>
</>
</SimpleBtn>
)}
{!isWorkflow && !isError && messageId && feedback?.rating === 'like' && (
<div
onClick={() => {
onFeedback?.({
rating: null,
})
}}
className='flex w-7 h-7 items-center justify-center rounded-md cursor-pointer !text-primary-600 border border-primary-200 bg-primary-100 hover:border-primary-300 hover:bg-primary-200'>
<HandThumbUpIcon width={16} height={16} />
</div>
)}
{!isWorkflow && !isError && messageId && feedback?.rating === 'dislike' && (
<div
onClick={() => {
onFeedback?.({
rating: null,
})
}}
className='flex w-7 h-7 items-center justify-center rounded-md cursor-pointer !text-red-600 border border-red-200 bg-red-100 hover:border-red-300 hover:bg-red-200'>
<HandThumbDownIcon width={16} height={16} />
</div>
)}
</>
)
const [currentTab, setCurrentTab] = useState<string>('DETAIL') const [currentTab, setCurrentTab] = useState<string>('DETAIL')
const showResultTabs = !!workflowProcessData?.resultText || !!workflowProcessData?.files?.length
const switchTab = async (tab: string) => {
setCurrentTab(tab)
}
useEffect(() => {
if (workflowProcessData?.resultText || !!workflowProcessData?.files?.length)
switchTab('RESULT')
else
switchTab('DETAIL')
}, [workflowProcessData?.files?.length, workflowProcessData?.resultText])
return ( return (
<div ref={ref} className={cn(isTop ? `rounded-xl border ${!isError ? 'border-gray-200 bg-chat-bubble-bg' : 'border-[#FECDCA] bg-[#FEF3F2]'} ` : 'rounded-br-xl !mt-0', className)} <>
style={isTop <div className={cn('relative', !isTop && 'mt-3', className)}>
? { {isLoading && (
boxShadow: '0px 1px 2px rgba(16, 24, 40, 0.05)', <div className={cn('flex items-center h-10', !inSidePanel && 'bg-chat-bubble-bg rounded-2xl border-t border-divider-subtle')}><Loading type='area' /></div>
} )}
: {}} {!isLoading && (
> <>
{isLoading {/* result content */}
? ( <div className={cn(
<div className='flex items-center h-10'><Loading type='area' /></div> 'relative',
) !inSidePanel && 'bg-chat-bubble-bg rounded-2xl border-t border-divider-subtle',
: ( )}>
<div {workflowProcessData && (
className={cn(!isTop && 'rounded-br-xl border-l-2 border-primary-400', 'p-4', innerClassName)} <>
style={mainStyle} <div className={cn(
> 'p-3 pb-0',
{(isTop && taskId) && ( showResultTabs && 'border-b border-divider-subtle',
<div className='mb-2 text-gray-500 border border-gray-200 box-border flex items-center rounded-md italic text-[11px] pl-1 pr-1.5 font-medium w-fit group-hover:opacity-100'> )}>
<HashtagIcon className='w-3 h-3 text-gray-400 fill-current mr-1 stroke-current stroke-1' /> {taskId && (
{taskId} <div className={cn('mb-2 flex items-center system-2xs-medium-uppercase text-text-accent-secondary', isError && 'text-text-destructive')}>
</div>) <RiPlayList2Line className='w-3 h-3 mr-1' />
} <span>{t('share.generation.execution')}</span>
<div className={`flex ${contentClassName}`}> <span className='px-1'>·</span>
<div className='grow w-0'> <span>{taskId}</span>
{siteInfo && workflowProcessData && ( </div>
<WorkflowProcessItem )}
data={workflowProcessData} {siteInfo && workflowProcessData && (
expand={workflowProcessData.expand} <WorkflowProcessItem
hideProcessDetail={hideProcessDetail} data={workflowProcessData}
hideInfo={hideProcessDetail} expand={workflowProcessData.expand}
readonly={!siteInfo.show_workflow_steps} hideProcessDetail={hideProcessDetail}
/> hideInfo={hideProcessDetail}
)} readonly={!siteInfo.show_workflow_steps}
{workflowProcessData && !isError && ( />
<ResultTab data={workflowProcessData} content={content} currentTab={currentTab} onCurrentTabChange={setCurrentTab} /> )}
)} {showResultTabs && (
{isError && ( <div className='flex items-center px-1 space-x-6'>
<div className='text-gray-400 text-sm'>{t('share.generation.batchFailed.outputPlaceholder')}</div> <div
)} className={cn(
{!workflowProcessData && !isError && (typeof content === 'string') && ( 'py-3 border-b-2 border-transparent system-sm-semibold-uppercase text-text-tertiary cursor-pointer',
currentTab === 'RESULT' && 'text-text-primary border-util-colors-blue-brand-blue-brand-600',
)}
onClick={() => switchTab('RESULT')}
>{t('runLog.result')}</div>
<div
className={cn(
'py-3 border-b-2 border-transparent system-sm-semibold-uppercase text-text-tertiary cursor-pointer',
currentTab === 'DETAIL' && 'text-text-primary border-util-colors-blue-brand-blue-brand-600',
)}
onClick={() => switchTab('DETAIL')}
>{t('runLog.detail')}</div>
</div>
)}
</div>
{!isError && (
<ResultTab data={workflowProcessData} content={content} currentTab={currentTab} />
)}
</>
)}
{!workflowProcessData && taskId && (
<div className={cn('sticky left-0 top-0 flex items-center w-full p-4 pb-3 bg-components-actionbar-bg rounded-t-2xl system-2xs-medium-uppercase text-text-accent-secondary', isError && 'text-text-destructive')}>
<RiPlayList2Line className='w-3 h-3 mr-1' />
<span>{t('share.generation.execution')}</span>
<span className='px-1'>·</span>
<span>{`${taskId}${depth > 1 ? `-${depth - 1}` : ''}`}</span>
</div>
)}
{isError && (
<div className='p-4 pt-0 text-text-quaternary body-lg-regular'>{t('share.generation.batchFailed.outputPlaceholder')}</div>
)}
{!workflowProcessData && !isError && (typeof content === 'string') && (
<div className={cn('p-4', taskId && 'pt-0')}>
<Markdown content={content} /> <Markdown content={content} />
)} </div>
</div> )}
</div> </div>
{/* meta data */}
<div className='flex items-center justify-between mt-3'> <div className={cn(
<div className='flex items-center'> 'relative mt-1 h-4 px-4 text-text-quaternary system-xs-regular',
{ isMobile && ((childMessageId || isQuerying) && depth < 3) && 'pl-10',
!isInWebApp && !isInstalledApp && !isResponding && ( )}>
<SimpleBtn {!isWorkflow && <span>{content?.length} {t('common.unit.char')}</span>}
isDisabled={isError || !messageId} {/* action buttons */}
className={cn(isMobile && '!px-1.5', 'space-x-1 mr-1')} <div className='absolute right-2 bottom-1 flex items-center'>
onClick={handleOpenLogModal}> {!isInWebApp && !isInstalledApp && !isResponding && (
<File02 className='w-3.5 h-3.5' /> <div className='ml-1 flex items-center gap-0.5 p-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg shadow-md backdrop-blur-sm'>
{!isMobile && <div>{t('common.operation.log')}</div>} <ActionButton disabled={isError || !messageId} onClick={handleOpenLogModal}>
</SimpleBtn> <RiFileList3Line className='w-4 h-4' />
) {/* <div>{t('common.operation.log')}</div> */}
} </ActionButton>
{((currentTab === 'RESULT' && workflowProcessData?.resultText) || !isWorkflow) && ( </div>
<SimpleBtn )}
isDisabled={isError || !messageId} <div className='ml-1 flex items-center gap-0.5 p-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg shadow-md backdrop-blur-sm'>
className={cn(isMobile && '!px-1.5', 'space-x-1')} {moreLikeThis && (
onClick={() => { <ActionButton state={depth === MAX_DEPTH ? ActionButtonState.Disabled : ActionButtonState.Default} disabled={depth === MAX_DEPTH} onClick={handleMoreLikeThis}>
<RiSparklingLine className='w-4 h-4' />
</ActionButton>
)}
{isShowTextToSpeech && (
<NewAudioButton
id={messageId!}
voice={config?.text_to_speech?.voice}
/>
)}
{((currentTab === 'RESULT' && workflowProcessData?.resultText) || !isWorkflow) && (
<ActionButton disabled={isError || !messageId} onClick={() => {
const copyContent = isWorkflow ? workflowProcessData?.resultText : content const copyContent = isWorkflow ? workflowProcessData?.resultText : content
if (typeof copyContent === 'string') if (typeof copyContent === 'string')
copy(copyContent) copy(copyContent)
@ -352,117 +316,68 @@ const GenerationItem: FC<IGenerationItemProps> = ({
copy(JSON.stringify(copyContent)) copy(JSON.stringify(copyContent))
Toast.notify({ type: 'success', message: t('common.actionMsg.copySuccessfully') }) Toast.notify({ type: 'success', message: t('common.actionMsg.copySuccessfully') })
}}> }}>
<RiClipboardLine className='w-3.5 h-3.5' /> <RiClipboardLine className='w-4 h-4' />
{!isMobile && <div>{t('common.operation.copy')}</div>} </ActionButton>
</SimpleBtn> )}
)} {isInWebApp && isError && (
<ActionButton onClick={onRetry}>
{isInWebApp && ( <RiReplay15Line className='w-4 h-4' />
<> </ActionButton>
{!isWorkflow && ( )}
<SimpleBtn {isInWebApp && !isWorkflow && (
isDisabled={isError || !messageId} <ActionButton disabled={isError || !messageId} onClick={() => { onSave?.(messageId as string) }}>
className={cn(isMobile && '!px-1.5', 'ml-2 space-x-1')} <RiBookmark3Line className='w-4 h-4' />
onClick={() => { onSave?.(messageId as string) }} </ActionButton>
> )}
<Bookmark className='w-3.5 h-3.5' /> </div>
{!isMobile && <div>{t('common.operation.save')}</div>} {(supportFeedback || isInWebApp) && !isWorkflow && !isError && messageId && (
</SimpleBtn> <div className='ml-1 flex items-center gap-0.5 p-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg shadow-md backdrop-blur-sm'>
{!feedback?.rating && (
<>
<ActionButton onClick={() => onFeedback?.({ rating: 'like' })}>
<RiThumbUpLine className='w-4 h-4' />
</ActionButton>
<ActionButton onClick={() => onFeedback?.({ rating: 'dislike' })}>
<RiThumbDownLine className='w-4 h-4' />
</ActionButton>
</>
)} )}
{(moreLikeThis && depth < MAX_DEPTH) && ( {feedback?.rating === 'like' && (
<SimpleBtn <ActionButton state={ActionButtonState.Active} onClick={() => onFeedback?.({ rating: null })}>
isDisabled={isError || !messageId} <RiThumbUpLine className='w-4 h-4' />
className={cn(isMobile && '!px-1.5', 'ml-2 space-x-1')} </ActionButton>
onClick={handleMoreLikeThis}
>
<Stars02 className='w-3.5 h-3.5' />
{!isMobile && <div>{t('appDebug.feature.moreLikeThis.title')}</div>}
</SimpleBtn>
)} )}
{isError && ( {feedback?.rating === 'dislike' && (
<SimpleBtn <ActionButton state={ActionButtonState.Destructive} onClick={() => onFeedback?.({ rating: null })}>
onClick={onRetry} <RiThumbDownLine className='w-4 h-4' />
className={cn(isMobile && '!px-1.5', 'ml-2 space-x-1')} </ActionButton>
>
<RefreshCcw01 className='w-3.5 h-3.5' />
{!isMobile && <div>{t('share.generation.batchFailed.retry')}</div>}
</SimpleBtn>
)} )}
{!isError && messageId && !isWorkflow && (
<div className="mx-3 w-[1px] h-[14px] bg-gray-200"></div>
)}
{ratingContent}
</>
)}
{supportAnnotation && (
<>
<div className='ml-2 mr-1 h-[14px] w-[1px] bg-gray-200'></div>
<AnnotationCtrlBtn
appId={appId!}
messageId={messageId!}
className='ml-1'
query={question}
answer={content}
// not support cache. So can not be cached
cached={false}
onAdded={() => {
}}
onEdit={() => setIsShowReplyModal(true)}
onRemoved={() => { }}
/>
</>
)}
<EditReplyModal
appId={appId!}
messageId={messageId!}
isShow={isShowReplyModal}
onHide={() => setIsShowReplyModal(false)}
query={question}
answer={content}
onAdded={() => { }}
onEdited={() => { }}
createdAt={0}
onRemove={() => { }}
onlyEditResponse
/>
{supportFeedback && (
<div className='ml-1'>
{ratingContent}
</div> </div>
)} )}
{isShowTextToSpeech && (
<>
<div className='ml-2 mr-2 h-[14px] w-[1px] bg-gray-200'></div>
<AudioBtn
id={messageId!}
className={'mr-1'}
voice={config?.text_to_speech?.voice}
/>
</>
)}
</div>
<div>
{!workflowProcessData && (
<div className='text-xs text-gray-500'>{content?.length} {t('common.unit.char')}</div>
)}
</div> </div>
</div> </div>
{/* more like this elements */}
</div> {!isTop && (
<div className={cn(
'absolute top-[-32px] w-4 h-[33px] flex justify-center',
isMobile ? 'left-[17px]' : 'left-[50%] translate-x-[-50%]',
)}>
<div className='h-full w-0.5 bg-divider-regular'></div>
<div className={cn(
'absolute left-0 w-4 h-4 flex items-center justify-center bg-util-colors-blue-blue-500 rounded-2xl border-[0.5px] border-divider-subtle shadow-xs',
isMobile ? 'top-[3.5px]' : 'top-2',
)}>
<RiSparklingFill className='w-3 h-3 text-text-primary-on-surface' />
</div>
</div>
)}
</>
)} )}
</div>
{((childMessageId || isQuerying) && depth < 3) && ( {((childMessageId || isQuerying) && depth < 3) && (
<div className='pl-4'> <GenerationItem {...childProps as any} />
<GenerationItem {...childProps as any} />
</div>
)} )}
</>
</div>
) )
} }
export default React.memo(GenerationItem) export default React.memo(GenerationItem)

View File

@ -1,9 +1,6 @@
import { import {
memo, memo,
useEffect,
} from 'react' } from 'react'
import { useTranslation } from 'react-i18next'
import cn from '@/utils/classnames'
import { Markdown } from '@/app/components/base/markdown' import { Markdown } from '@/app/components/base/markdown'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor' import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
@ -14,79 +11,45 @@ const ResultTab = ({
data, data,
content, content,
currentTab, currentTab,
onCurrentTabChange,
}: { }: {
data?: WorkflowProcess data?: WorkflowProcess
content: any content: any
currentTab: string currentTab: string
onCurrentTabChange: (tab: string) => void
}) => { }) => {
const { t } = useTranslation()
const switchTab = async (tab: string) => {
onCurrentTabChange(tab)
}
useEffect(() => {
if (data?.resultText || !!data?.files?.length)
switchTab('RESULT')
else
switchTab('DETAIL')
}, [data?.files?.length, data?.resultText])
return ( return (
<div className='grow relative flex flex-col'> <>
{(data?.resultText || !!data?.files?.length) && ( {currentTab === 'RESULT' && (
<div className='shrink-0 flex items-center mb-2 border-b-[0.5px] border-[rgba(0,0,0,0.05)]'> <div className='p-4 space-y-3'>
<div {data?.resultText && <Markdown content={data?.resultText || ''} />}
className={cn( {!!data?.files?.length && (
'mr-6 py-3 border-b-2 border-transparent text-[13px] font-semibold leading-[18px] text-gray-400 cursor-pointer', <div className='flex flex-col gap-2'>
currentTab === 'RESULT' && '!border-[rgb(21,94,239)] text-gray-700', {data?.files.map((item: any) => (
)} <div key={item.varName} className='flex flex-col gap-1 system-xs-regular'>
onClick={() => switchTab('RESULT')} <div className='py-1 text-text-tertiary '>{item.varName}</div>
>{t('runLog.result')}</div> <FileList
<div files={item.list}
className={cn( showDeleteAction={false}
'mr-6 py-3 border-b-2 border-transparent text-[13px] font-semibold leading-[18px] text-gray-400 cursor-pointer', showDownloadAction
currentTab === 'DETAIL' && '!border-[rgb(21,94,239)] text-gray-700', canPreview
)} />
onClick={() => switchTab('DETAIL')} </div>
>{t('runLog.detail')}</div> ))}
</div>
)}
</div> </div>
)} )}
<div className={cn('grow bg-white')}> {currentTab === 'DETAIL' && content && (
{currentTab === 'RESULT' && ( <div className='p-4'>
<> <CodeEditor
{data?.resultText && <Markdown content={data?.resultText || ''} />} readOnly
{!!data?.files?.length && ( title={<div>JSON OUTPUT</div>}
<div className='flex flex-col gap-2'> language={CodeLanguage.json}
{data?.files.map((item: any) => ( value={content}
<div key={item.varName} className='flex flex-col gap-1 system-xs-regular'> isJSONStringifyBeauty
<div className='py-1 text-text-tertiary '>{item.varName}</div> />
<FileList </div>
files={item.list} )}
showDeleteAction={false} </>
showDownloadAction
canPreview
/>
</div>
))}
</div>
)}
</>
)}
{currentTab === 'DETAIL' && content && (
<div className='mt-1'>
<CodeEditor
readOnly
title={<div>JSON OUTPUT</div>}
language={CodeLanguage.json}
value={content}
isJSONStringifyBeauty
/>
</div>
)}
</div>
</div>
) )
} }

View File

@ -1,15 +1,19 @@
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import React from 'react' import React from 'react'
import {
RiClipboardLine,
RiDeleteBinLine,
} from '@remixicon/react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import copy from 'copy-to-clipboard' import copy from 'copy-to-clipboard'
import NoData from './no-data' import NoData from './no-data'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import type { SavedMessage } from '@/models/debug' import type { SavedMessage } from '@/models/debug'
import { Markdown } from '@/app/components/base/markdown' import { Markdown } from '@/app/components/base/markdown'
import { SimpleBtn, copyIcon } from '@/app/components/app/text-generate/item'
import Toast from '@/app/components/base/toast' import Toast from '@/app/components/base/toast'
import AudioBtn from '@/app/components/base/audio-btn' import ActionButton from '@/app/components/base/action-button'
import NewAudioButton from '@/app/components/base/new-audio-button'
export type ISavedItemsProps = { export type ISavedItemsProps = {
className?: string className?: string
@ -19,12 +23,6 @@ export type ISavedItemsProps = {
onStartCreateContent: () => void onStartCreateContent: () => void
} }
const removeIcon = (
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.25 1.75H8.75M1.75 3.5H12.25M11.0833 3.5L10.6742 9.63625C10.6129 10.5569 10.5822 11.0172 10.3833 11.3663C10.2083 11.6735 9.94422 11.9206 9.62597 12.0748C9.26448 12.25 8.80314 12.25 7.88045 12.25H6.11955C5.19686 12.25 4.73552 12.25 4.37403 12.0748C4.05577 11.9206 3.79172 11.6735 3.61666 11.3663C3.41781 11.0172 3.38713 10.5569 3.32575 9.63625L2.91667 3.5M5.83333 6.125V9.04167M8.16667 6.125V9.04167" stroke="#344054" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
</svg>
)
const SavedItems: FC<ISavedItemsProps> = ({ const SavedItems: FC<ISavedItemsProps> = ({
className, className,
isShowTextToSpeech, isShowTextToSpeech,
@ -35,56 +33,37 @@ const SavedItems: FC<ISavedItemsProps> = ({
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<div className={cn(className, 'space-y-3')}> <div className={cn('space-y-4', className)}>
{list.length === 0 {list.length === 0
? ( ? (
<div className='px-6'> <NoData onStartCreateContent={onStartCreateContent} />
<NoData onStartCreateContent={onStartCreateContent} />
</div>
) )
: (<> : (<>
{list.map(({ id, answer }) => ( {list.map(({ id, answer }) => (
<div <div key={id} className='relative'>
key={id} <div className={cn(
className='p-4 rounded-xl bg-gray-50' 'p-4 bg-background-section-burn rounded-2xl',
style={{ )}>
boxShadow: '0px 1px 2px rgba(16, 24, 40, 0.05)', <Markdown content={answer} />
}} </div>
> <div className='mt-1 h-4 px-4 text-text-quaternary system-xs-regular'>
<Markdown content={answer} /> <span>{answer.length} {t('common.unit.char')}</span>
<div className='flex items-center justify-between mt-3'> </div>
<div className='flex items-center space-x-2'> <div className='absolute right-2 bottom-1'>
<SimpleBtn <div className='ml-1 flex items-center gap-0.5 p-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg shadow-md backdrop-blur-sm'>
className='space-x-1' {isShowTextToSpeech && <NewAudioButton value={answer}/>}
onClick={() => { <ActionButton onClick={() => {
copy(answer) copy(answer)
Toast.notify({ type: 'success', message: t('common.actionMsg.copySuccessfully') }) Toast.notify({ type: 'success', message: t('common.actionMsg.copySuccessfully') })
}}> }}>
{copyIcon} <RiClipboardLine className='w-4 h-4' />
<div>{t('common.operation.copy')}</div> </ActionButton>
</SimpleBtn> <ActionButton onClick={() => {
onRemove(id)
<SimpleBtn }}>
className='space-x-1' <RiDeleteBinLine className='w-4 h-4' />
onClick={() => { </ActionButton>
onRemove(id)
}}>
{removeIcon}
<div>{t('common.operation.remove')}</div>
</SimpleBtn>
{isShowTextToSpeech && (
<>
<div className='ml-2 mr-2 h-[14px] w-[1px] bg-gray-200'></div>
<AudioBtn
value={answer}
noCache={false}
className={'mr-1'}
/>
</>
)}
</div> </div>
<div className='text-xs text-gray-500'>{answer?.length} {t('common.unit.char')}</div>
</div> </div>
</div> </div>
))} ))}

View File

@ -2,47 +2,38 @@
import type { FC } from 'react' import type { FC } from 'react'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { PlusIcon } from '@heroicons/react/24/outline' import {
RiAddLine,
RiBookmark3Line,
} from '@remixicon/react'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
export type INoDataProps = { export type INoDataProps = {
onStartCreateContent: () => void onStartCreateContent: () => void
} }
const markIcon = (
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.16699 6.5C4.16699 5.09987 4.16699 4.3998 4.43948 3.86502C4.67916 3.39462 5.06161 3.01217 5.53202 2.77248C6.0668 2.5 6.76686 2.5 8.16699 2.5H11.8337C13.2338 2.5 13.9339 2.5 14.4686 2.77248C14.939 3.01217 15.3215 3.39462 15.5612 3.86502C15.8337 4.3998 15.8337 5.09987 15.8337 6.5V17.5L10.0003 14.1667L4.16699 17.5V6.5Z" stroke="#667085" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
)
const lightIcon = (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className="inline relative -top-3 -left-1.5"><path d="M5 6.5V5M8.93934 7.56066L10 6.5M10.0103 11.5H11.5103" stroke="#374151" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"></path></svg>
)
const NoData: FC<INoDataProps> = ({ const NoData: FC<INoDataProps> = ({
onStartCreateContent, onStartCreateContent,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<div className='mt-[60px] px-5 py-4 rounded-2xl bg-gray-50 '> <div className='p-6 rounded-xl bg-background-section-burn '>
<div className='flex items-center justify-center w-11 h-11 border border-gray-100 rounded-lg'> <div className='flex items-center justify-center w-10 h-10 border-[0.5px] border-components-card-border bg-components-card-bg-alt rounded-[10px] shadow-lg backdrop-blur-sm'>
{markIcon} <RiBookmark3Line className='w-4 h-4 text-text-accent'/>
</div> </div>
<div className='mt-2'> <div className='mt-3'>
<span className='text-gray-700 font-semibold'>{t('share.generation.savedNoData.title')}</span> <span className='text-text-secondary system-xl-semibold'>{t('share.generation.savedNoData.title')}</span>
{lightIcon}
</div> </div>
<div className='mt-2 text-gray-500 text-[13px] font-normal'> <div className='mt-1 text-text-tertiary system-sm-regular'>
{t('share.generation.savedNoData.description')} {t('share.generation.savedNoData.description')}
</div> </div>
<Button <Button
className='mt-4' variant='primary'
className='mt-3'
onClick={onStartCreateContent} onClick={onStartCreateContent}
> >
<div className='flex items-center space-x-2 text-primary-600 text-[13px] font-medium'> <RiAddLine className='mr-1 w-4 h-4' />
<PlusIcon className='w-4 h-4' /> <span>{t('share.generation.savedNoData.startCreateContent')}</span>
<span>{t('share.generation.savedNoData.startCreateContent')}</span>
</div>
</Button> </Button>
</div> </div>
) )

View File

@ -5,6 +5,10 @@
@apply inline-flex justify-center items-center cursor-pointer text-text-tertiary hover:text-text-secondary hover:bg-state-base-hover @apply inline-flex justify-center items-center cursor-pointer text-text-tertiary hover:text-text-secondary hover:bg-state-base-hover
} }
.action-btn-hover {
@apply bg-state-base-hover
}
.action-btn-disabled { .action-btn-disabled {
@apply cursor-not-allowed @apply cursor-not-allowed
} }

View File

@ -8,6 +8,7 @@ enum ActionButtonState {
Active = 'active', Active = 'active',
Disabled = 'disabled', Disabled = 'disabled',
Default = '', Default = '',
Hover = 'hover',
} }
const actionButtonVariants = cva( const actionButtonVariants = cva(
@ -41,6 +42,8 @@ function getActionButtonState(state: ActionButtonState) {
return 'action-btn-active' return 'action-btn-active'
case ActionButtonState.Disabled: case ActionButtonState.Disabled:
return 'action-btn-disabled' return 'action-btn-disabled'
case ActionButtonState.Hover:
return 'action-btn-hover'
default: default:
return '' return ''
} }

View File

@ -1,117 +0,0 @@
.audioPlayer {
display: flex;
flex-direction: row;
align-items: center;
background-color: var(--color-components-chat-input-audio-bg-alt);
border-radius: 10px;
padding: 8px;
min-width: 240px;
max-width: 420px;
max-height: 40px;
backdrop-filter: blur(5px);
border: 1px solid var(--color-components-panel-border-subtle);
box-shadow: 0 1px 2px var(--color-shadow-shadow-3);
gap: 8px;
}
.playButton {
display: inline-flex;
width: 16px;
height: 16px;
border-radius: 50%;
background-color: var(--color-components-button-primary-bg);
color: var(--color-components-chat-input-audio-bg-alt);
border: none;
cursor: pointer;
align-items: center;
justify-content: center;
transition: background-color 0.1s;
flex-shrink: 0;
}
.playButton:hover {
background-color: var(--color-components-button-primary-bg-hover);
}
.playButton:disabled {
background-color: var(--color-components-button-primary-bg-disabled);
}
.audioControls {
flex-grow: 1;
}
.progressBarContainer {
height: 32px;
display: flex;
align-items: center;
justify-content: center;
}
.waveform {
position: relative;
display: flex;
cursor: pointer;
height: 24px;
width: 100%;
flex-grow: 1;
align-items: center;
justify-content: center;
}
.progressBar {
position: absolute;
top: 0;
left: 0;
opacity: 0.5;
border-radius: 2px;
flex: none;
order: 55;
flex-grow: 0;
height: 100%;
background-color: rgba(66, 133, 244, 0.3);
pointer-events: none;
}
.timeDisplay {
/* position: absolute; */
color: var(--color-text-accent-secondary);
font-size: 12px;
order: 0;
height: 100%;
width: 50px;
display: inline-flex;
align-items: center;
justify-content: center;
}
/* .currentTime {
position: absolute;
bottom: calc(100% + 5px);
transform: translateX(-50%);
background-color: rgba(255,255,255,.8);
padding: 2px 4px;
border-radius:10px;
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.08);
} */
.duration {
padding: 2px 4px;
border-radius: 10px;
}
.source_unavailable {
border: none;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
position: absolute;
color: #bdbdbf;
}
.playButton svg path,
.playButton svg rect {
fill: currentColor;
}

View File

@ -1,7 +1,13 @@
import React, { useCallback, useEffect, useRef, useState } from 'react' import React, { useCallback, useEffect, useRef, useState } from 'react'
import { t } from 'i18next' import { t } from 'i18next'
import styles from './AudioPlayer.module.css' import {
RiPauseCircleFill,
RiPlayLargeFill,
} from '@remixicon/react'
import Toast from '@/app/components/base/toast' import Toast from '@/app/components/base/toast'
import { useAppContext } from '@/context/app-context'
import { Theme } from '@/types/app'
import cn from '@/utils/classnames'
type AudioPlayerProps = { type AudioPlayerProps = {
src: string src: string
@ -18,6 +24,7 @@ const AudioPlayer: React.FC<AudioPlayerProps> = ({ src }) => {
const [hasStartedPlaying, setHasStartedPlaying] = useState(false) const [hasStartedPlaying, setHasStartedPlaying] = useState(false)
const [hoverTime, setHoverTime] = useState(0) const [hoverTime, setHoverTime] = useState(0)
const [isAudioAvailable, setIsAudioAvailable] = useState(true) const [isAudioAvailable, setIsAudioAvailable] = useState(true)
const { theme } = useAppContext()
useEffect(() => { useEffect(() => {
const audio = audioRef.current const audio = audioRef.current
@ -230,11 +237,11 @@ const AudioPlayer: React.FC<AudioPlayerProps> = ({ src }) => {
let color let color
if (index * barWidth <= playedWidth) if (index * barWidth <= playedWidth)
color = '#296DFF' color = theme === Theme.light ? '#296DFF' : '#84ABFF'
else if ((index * barWidth / width) * duration <= hoverTime) else if ((index * barWidth / width) * duration <= hoverTime)
color = 'rgba(21,90,239,.40)' color = theme === Theme.light ? 'rgba(21,90,239,.40)' : 'rgba(200, 206, 218, 0.28)'
else else
color = 'rgba(21,90,239,.20)' color = theme === Theme.light ? 'rgba(21,90,239,.20)' : 'rgba(200, 206, 218, 0.14)'
const barHeight = value * height const barHeight = value * height
const rectX = index * barWidth const rectX = index * barWidth
@ -253,7 +260,7 @@ const AudioPlayer: React.FC<AudioPlayerProps> = ({ src }) => {
ctx.fillRect(rectX, rectY, rectWidth, rectHeight) ctx.fillRect(rectX, rectY, rectWidth, rectHeight)
} }
}) })
}, [currentTime, duration, hoverTime, waveformData]) }, [currentTime, duration, hoverTime, theme, waveformData])
useEffect(() => { useEffect(() => {
drawWaveform() drawWaveform()
@ -279,40 +286,32 @@ const AudioPlayer: React.FC<AudioPlayerProps> = ({ src }) => {
}, [duration]) }, [duration])
return ( return (
<div className={styles.audioPlayer}> <div className='flex items-end gap-2 h-9 min-w-[240px] max-w-[420px] p-2 bg-components-chat-input-audio-bg-alt backdrop-blur-sm rounded-[10px] border border-components-panel-border-subtle shadow-xs'>
<audio ref={audioRef} src={src} preload="auto"/> <audio ref={audioRef} src={src} preload="auto"/>
<button className={styles.playButton} onClick={togglePlay} disabled={!isAudioAvailable}> <button className='shrink-0 inline-flex items-center justify-center border-none text-text-accent hover:text-text-accent-secondary transition-all cursor-pointer disabled:text-components-button-primary-bg-disabled' onClick={togglePlay} disabled={!isAudioAvailable}>
{isPlaying {isPlaying
? ( ? (
<svg viewBox="0 0 24 24" width="16" height="16"> <RiPauseCircleFill className='w-5 h-5' />
<rect x="7" y="6" width="3" height="12" rx="1.5" ry="1.5"/>
<rect x="15" y="6" width="3" height="12" rx="1.5" ry="1.5"/>
</svg>
) )
: ( : (
<svg viewBox="0 0 24 24" width="16" height="16"> <RiPlayLargeFill className='w-5 h-5' />
<path d="M8 5v14l11-7z" fill="currentColor"/>
</svg>
)} )}
</button> </button>
<div className={isAudioAvailable ? styles.audioControls : styles.audioControls_disabled} hidden={!isAudioAvailable}> <div className={cn(isAudioAvailable && 'grow')} hidden={!isAudioAvailable}>
<div className={styles.progressBarContainer}> <div className='h-8 flex items-center justify-center'>
<canvas <canvas
ref={canvasRef} ref={canvasRef}
className={styles.waveform} className='relative grow h-6 w-full flex items-center justify-center cursor-pointer'
onClick={handleCanvasInteraction} onClick={handleCanvasInteraction}
onMouseMove={handleMouseMove} onMouseMove={handleMouseMove}
onMouseDown={handleCanvasInteraction} onMouseDown={handleCanvasInteraction}
/> />
{/* <div className={styles.currentTime} style={{ left: `${(currentTime / duration) * 81}%`, bottom: '29px' }}> <div className='inline-flex items-center justify-center min-w-[50px] text-text-accent-secondary system-xs-medium'>
{formatTime(currentTime)} <span className='px-0.5 py-1 rounded-[10px]'>{formatTime(duration)}</span>
</div> */}
<div className={styles.timeDisplay}>
<span className={styles.duration}>{formatTime(duration)}</span>
</div> </div>
</div> </div>
</div> </div>
<div className={styles.source_unavailable} hidden={isAudioAvailable}>{t('common.operation.audioSourceUnavailable')}</div> <div className='absolute top-0 left-0 w-full h-full flex items-center justify-center text-text-quaternary' hidden={isAudioAvailable}>{t('common.operation.audioSourceUnavailable')}</div>
</div> </div>
) )
} }

View File

@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react'
import Chat from '../chat' import Chat from '../chat'
import type { import type {
ChatConfig, ChatConfig,
@ -9,14 +9,17 @@ import type {
import { useChat } from '../chat/hooks' import { useChat } from '../chat/hooks'
import { getLastAnswer, isValidGeneratedAnswer } from '../utils' import { getLastAnswer, isValidGeneratedAnswer } from '../utils'
import { useChatWithHistoryContext } from './context' import { useChatWithHistoryContext } from './context'
import Header from './header' import { InputVarType } from '@/app/components/workflow/types'
import ConfigPanel from './config-panel' import { TransferMethod } from '@/types/app'
import InputsForm from '@/app/components/base/chat/chat-with-history/inputs-form'
import { import {
fetchSuggestedQuestions, fetchSuggestedQuestions,
getUrl, getUrl,
stopChatMessageResponding, stopChatMessageResponding,
} from '@/service/share' } from '@/service/share'
import AppIcon from '@/app/components/base/app-icon'
import AnswerIcon from '@/app/components/base/answer-icon' import AnswerIcon from '@/app/components/base/answer-icon'
import cn from '@/utils/classnames'
const ChatWrapper = () => { const ChatWrapper = () => {
const { const {
@ -26,6 +29,7 @@ const ChatWrapper = () => {
currentConversationItem, currentConversationItem,
inputsForms, inputsForms,
newConversationInputs, newConversationInputs,
newConversationInputsRef,
handleNewConversationCompleted, handleNewConversationCompleted,
isMobile, isMobile,
isInstalledApp, isInstalledApp,
@ -65,6 +69,38 @@ const ChatWrapper = () => {
appPrevChatTree, appPrevChatTree,
taskId => stopChatMessageResponding('', taskId, isInstalledApp, appId), taskId => stopChatMessageResponding('', taskId, isInstalledApp, appId),
) )
const inputsFormValue = currentConversationId ? currentConversationItem?.inputs : newConversationInputsRef?.current
const inputDisabled = useMemo(() => {
let hasEmptyInput = ''
let fileIsUploading = false
const requiredVars = inputsForms.filter(({ required }) => required)
if (requiredVars.length) {
requiredVars.forEach(({ variable, label, type }) => {
if (hasEmptyInput)
return
if (fileIsUploading)
return
if (!inputsFormValue?.[variable])
hasEmptyInput = label as string
if ((type === InputVarType.singleFile || type === InputVarType.multiFiles) && inputsFormValue?.[variable]) {
const files = inputsFormValue[variable]
if (Array.isArray(files))
fileIsUploading = files.find(item => item.transferMethod === TransferMethod.local_file && !item.uploadedId)
else
fileIsUploading = files.transferMethod === TransferMethod.local_file && !files.uploadedId
}
})
}
if (hasEmptyInput)
return true
if (fileIsUploading)
return true
return false
}, [inputsFormValue, inputsForms])
useEffect(() => { useEffect(() => {
if (currentChatInstanceRef.current) if (currentChatInstanceRef.current)
@ -107,42 +143,48 @@ const ChatWrapper = () => {
doSend(question.content, question.message_files, true, isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null) doSend(question.content, question.message_files, true, isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null)
}, [chatList, doSend]) }, [chatList, doSend])
const chatNode = useMemo(() => { const messageList = useMemo(() => {
if (inputsForms.length) { if (currentConversationId)
return ( return chatList
<> return chatList.filter(item => !item.isOpeningStatement)
<Header }, [chatList, currentConversationId])
isMobile={isMobile}
title={currentConversationItem?.name || ''}
/>
{
!currentConversationId && (
<div className={`mx-auto w-full max-w-[720px] ${isMobile && 'px-4'}`}>
<div className='mb-6' />
<ConfigPanel />
<div
className='my-6 h-[1px]'
style={{ background: 'linear-gradient(90deg, rgba(242, 244, 247, 0.00) 0%, #F2F4F7 49.17%, rgba(242, 244, 247, 0.00) 100%)' }}
/>
</div>
)
}
</>
)
}
const [collapsed, setCollapsed] = useState(!!currentConversationId)
const chatNode = useMemo(() => {
if (!inputsForms.length)
return null
if (isMobile) {
if (!currentConversationId)
return <InputsForm collapsed={collapsed} setCollapsed={setCollapsed} />
return null
}
else {
return <InputsForm collapsed={collapsed} setCollapsed={setCollapsed} />
}
}, [inputsForms.length, isMobile, currentConversationId, collapsed])
const welcome = useMemo(() => {
const welcomeMessage = chatList.find(item => item.isOpeningStatement)
if (currentConversationId)
return null
if (!welcomeMessage)
return null
if (!collapsed && inputsForms.length > 0)
return null
return ( return (
<Header <div className={cn('h-[50vh] py-12 flex flex-col items-center justify-center gap-3')}>
isMobile={isMobile} <AppIcon
title={currentConversationItem?.name || ''} size='xl'
/> iconType={appData?.site.icon_type}
icon={appData?.site.icon}
background={appData?.site.icon_background}
imageUrl={appData?.site.icon_url}
/>
<div className='text-text-tertiary body-2xl-regular'>{welcomeMessage.content}</div>
</div>
) )
}, [ }, [appData?.site.icon, appData?.site.icon_background, appData?.site.icon_type, appData?.site.icon_url, chatList, collapsed, currentConversationId, inputsForms.length])
currentConversationId,
inputsForms,
currentConversationItem,
isMobile,
])
const answerIcon = (appData?.site && appData.site.use_icon_as_answer_icon) const answerIcon = (appData?.site && appData.site.use_icon_as_answer_icon)
? <AnswerIcon ? <AnswerIcon
@ -160,7 +202,7 @@ const ChatWrapper = () => {
<Chat <Chat
appData={appData} appData={appData}
config={appConfig} config={appConfig}
chatList={chatList} chatList={messageList}
isResponding={isResponding} isResponding={isResponding}
chatContainerInnerClassName={`mx-auto pt-6 w-full max-w-[720px] ${isMobile && 'px-4'}`} chatContainerInnerClassName={`mx-auto pt-6 w-full max-w-[720px] ${isMobile && 'px-4'}`}
chatFooterClassName='pb-4' chatFooterClassName='pb-4'
@ -170,7 +212,12 @@ const ChatWrapper = () => {
inputsForm={inputsForms} inputsForm={inputsForms}
onRegenerate={doRegenerate} onRegenerate={doRegenerate}
onStopResponding={handleStop} onStopResponding={handleStop}
chatNode={chatNode} chatNode={
<>
{chatNode}
{welcome}
</>
}
allToolIcons={appMeta?.tool_icons || {}} allToolIcons={appMeta?.tool_icons || {}}
onFeedback={handleFeedback} onFeedback={handleFeedback}
suggestedQuestions={suggestedQuestions} suggestedQuestions={suggestedQuestions}
@ -178,6 +225,8 @@ const ChatWrapper = () => {
hideProcessDetail hideProcessDetail
themeBuilder={themeBuilder} themeBuilder={themeBuilder}
switchSibling={siblingMessageId => setTargetMessageId(siblingMessageId)} switchSibling={siblingMessageId => setTargetMessageId(siblingMessageId)}
inputDisabled={inputDisabled}
isMobile={isMobile}
/> />
</div> </div>
) )

View File

@ -1,47 +0,0 @@
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { memo } from 'react'
import Textarea from '@/app/components/base/textarea'
interface InputProps {
form: any
value: string
onChange: (variable: string, value: string) => void
}
const FormInput: FC<InputProps> = ({
form,
value,
onChange,
}) => {
const { t } = useTranslation()
const {
type,
label,
required,
max_length,
variable,
} = form
if (type === 'paragraph') {
return (
<Textarea
value={value}
className='resize-none'
onChange={e => onChange(variable, e.target.value)}
placeholder={`${label}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
/>
)
}
return (
<input
className='grow h-9 rounded-lg bg-gray-100 px-2.5 outline-none appearance-none'
value={value || ''}
maxLength={max_length}
onChange={e => onChange(variable, e.target.value)}
placeholder={`${label}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
/>
)
}
export default memo(FormInput)

View File

@ -1,117 +0,0 @@
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useChatWithHistoryContext } from '../context'
import Input from './form-input'
import { PortalSelect } from '@/app/components/base/select'
import { InputVarType } from '@/app/components/workflow/types'
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
const Form = () => {
const { t } = useTranslation()
const {
appParams,
inputsForms,
newConversationInputs,
newConversationInputsRef,
handleNewConversationInputsChange,
isMobile,
} = useChatWithHistoryContext()
const handleFormChange = useCallback((variable: string, value: any) => {
handleNewConversationInputsChange({
...newConversationInputsRef.current,
[variable]: value,
})
}, [newConversationInputsRef, handleNewConversationInputsChange])
const renderField = (form: any) => {
const {
label,
required,
variable,
options,
} = form
if (form.type === 'text-input' || form.type === 'paragraph') {
return (
<Input
form={form}
value={newConversationInputs[variable]}
onChange={handleFormChange}
/>
)
}
if (form.type === 'number') {
return (
<input
className="grow h-9 rounded-lg bg-gray-100 px-2.5 outline-none appearance-none"
type="number"
value={newConversationInputs[variable] || ''}
onChange={e => handleFormChange(variable, e.target.value)}
placeholder={`${label}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
/>
)
}
if (form.type === InputVarType.singleFile) {
return (
<FileUploaderInAttachmentWrapper
value={newConversationInputs[variable] ? [newConversationInputs[variable]] : []}
onChange={files => handleFormChange(variable, files[0])}
fileConfig={{
allowed_file_types: form.allowed_file_types,
allowed_file_extensions: form.allowed_file_extensions,
allowed_file_upload_methods: form.allowed_file_upload_methods,
number_limits: 1,
fileUploadConfig: (appParams as any).system_parameters,
}}
/>
)
}
if (form.type === InputVarType.multiFiles) {
return (
<FileUploaderInAttachmentWrapper
value={newConversationInputs[variable]}
onChange={files => handleFormChange(variable, files)}
fileConfig={{
allowed_file_types: form.allowed_file_types,
allowed_file_extensions: form.allowed_file_extensions,
allowed_file_upload_methods: form.allowed_file_upload_methods,
number_limits: form.max_length,
fileUploadConfig: (appParams as any).system_parameters,
}}
/>
)
}
return (
<PortalSelect
popupClassName='w-[200px]'
value={newConversationInputs[variable]}
items={options.map((option: string) => ({ value: option, name: option }))}
onSelect={item => handleFormChange(variable, item.value as string)}
placeholder={`${label}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
/>
)
}
if (!inputsForms.length)
return null
return (
<div className='mb-4 py-2'>
{
inputsForms.map(form => (
<div
key={form.variable}
className={`flex mb-3 last-of-type:mb-0 text-sm text-gray-900 ${isMobile && '!flex-wrap'}`}
>
<div className={`shrink-0 mr-2 py-2 w-[128px] ${isMobile && '!w-full'}`}>{form.label}</div>
{renderField(form)}
</div>
))
}
</div>
)
}
export default Form

View File

@ -1,172 +0,0 @@
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useChatWithHistoryContext } from '../context'
import Form from './form'
import Button from '@/app/components/base/button'
import AppIcon from '@/app/components/base/app-icon'
import { MessageDotsCircle } from '@/app/components/base/icons/src/vender/solid/communication'
import { Edit02 } from '@/app/components/base/icons/src/vender/line/general'
import { Star06 } from '@/app/components/base/icons/src/vender/solid/shapes'
import LogoSite from '@/app/components/base/logo/logo-site'
const ConfigPanel = () => {
const { t } = useTranslation()
const {
appData,
inputsForms,
handleStartChat,
showConfigPanelBeforeChat,
isMobile,
} = useChatWithHistoryContext()
const [collapsed, setCollapsed] = useState(true)
const customConfig = appData?.custom_config
const site = appData?.site
return (
<div className='flex flex-col max-h-[80%] w-full max-w-[720px]'>
<div
className={`
grow rounded-xl overflow-y-auto
${showConfigPanelBeforeChat && 'border-[0.5px] border-gray-100 shadow-lg'}
${!showConfigPanelBeforeChat && collapsed && 'border border-indigo-100'}
${!showConfigPanelBeforeChat && !collapsed && 'border-[0.5px] border-gray-100 shadow-lg'}
`}
>
<div
className={`
flex flex-wrap px-6 py-4 rounded-t-xl bg-indigo-25
${isMobile && '!px-4 !py-3'}
`}
>
{
showConfigPanelBeforeChat && (
<>
<div className='flex items-center h-8 text-2xl font-semibold text-gray-800'>
<AppIcon
iconType={appData?.site.icon_type}
icon={appData?.site.icon}
background='transparent'
imageUrl={appData?.site.icon_url}
size='small'
className="mr-2"
/>
{appData?.site.title}
</div>
{
appData?.site.description && (
<div className='mt-2 w-full text-sm text-gray-500'>
{appData?.site.description}
</div>
)
}
</>
)
}
{
!showConfigPanelBeforeChat && collapsed && (
<>
<Star06 className='mr-1 mt-1 w-4 h-4 text-indigo-600' />
<div className='grow py-[3px] text-[13px] text-indigo-600 leading-[18px] font-medium'>
{t('share.chat.configStatusDes')}
</div>
<Button
variant='secondary-accent'
size='small'
className='shrink-0'
onClick={() => setCollapsed(false)}
>
<Edit02 className='mr-1 w-3 h-3' />
{t('common.operation.edit')}
</Button>
</>
)
}
{
!showConfigPanelBeforeChat && !collapsed && (
<>
<Star06 className='mr-1 mt-1 w-4 h-4 text-indigo-600' />
<div className='grow py-[3px] text-[13px] text-indigo-600 leading-[18px] font-medium'>
{t('share.chat.privatePromptConfigTitle')}
</div>
</>
)
}
</div>
{
!collapsed && !showConfigPanelBeforeChat && (
<div className='p-6 rounded-b-xl'>
<Form />
<div className={`pl-[136px] flex items-center ${isMobile && '!pl-0'}`}>
<Button
variant='primary'
className='mr-2'
onClick={() => {
setCollapsed(true)
handleStartChat()
}}
>
{t('common.operation.save')}
</Button>
<Button
onClick={() => setCollapsed(true)}
>
{t('common.operation.cancel')}
</Button>
</div>
</div>
)
}
{
showConfigPanelBeforeChat && (
<div className='p-6 rounded-b-xl'>
<Form />
<Button
className={`${inputsForms.length && !isMobile && 'ml-[136px]'}`}
variant='primary'
size='large'
onClick={handleStartChat}
>
<MessageDotsCircle className='mr-2 w-4 h-4 text-white' />
{t('share.chat.startChat')}
</Button>
</div>
)
}
</div>
{
showConfigPanelBeforeChat && (site || customConfig) && (
<div className='mt-4 flex flex-wrap justify-between items-center py-2 text-xs text-gray-400'>
{site?.privacy_policy
? <div className={`flex items-center ${isMobile && 'w-full justify-end'}`}>{t('share.chat.privacyPolicyLeft')}
<a
className='text-gray-500 px-1'
href={site?.privacy_policy}
target='_blank' rel='noopener noreferrer'>{t('share.chat.privacyPolicyMiddle')}</a>
{t('share.chat.privacyPolicyRight')}
</div>
: <div>
</div>}
{
customConfig?.remove_webapp_brand
? null
: (
<div className={`flex items-center justify-end ${isMobile && 'w-full'}`}>
<div className='flex items-center pr-3 space-x-3'>
<span className='uppercase'>{t('share.chat.poweredBy')}</span>
{
customConfig?.replace_webapp_logo
? <img src={customConfig?.replace_webapp_logo} alt='logo' className='block w-auto h-5' />
: <LogoSite className='!h-5' />
}
</div>
</div>
)
}
</div>
)
}
</div>
)
}
export default ConfigPanel

View File

@ -28,13 +28,12 @@ export type ChatWithHistoryContextValue = {
appPrevChatTree: ChatItemInTree[] appPrevChatTree: ChatItemInTree[]
pinnedConversationList: AppConversationData['data'] pinnedConversationList: AppConversationData['data']
conversationList: AppConversationData['data'] conversationList: AppConversationData['data']
showConfigPanelBeforeChat: boolean
newConversationInputs: Record<string, any> newConversationInputs: Record<string, any>
newConversationInputsRef: RefObject<Record<string, any>> newConversationInputsRef: RefObject<Record<string, any>>
handleNewConversationInputsChange: (v: Record<string, any>) => void handleNewConversationInputsChange: (v: Record<string, any>) => void
inputsForms: any[] inputsForms: any[]
handleNewConversation: () => void handleNewConversation: () => void
handleStartChat: () => void handleStartChat: (callback?: any) => void
handleChangeConversation: (conversationId: string) => void handleChangeConversation: (conversationId: string) => void
handlePinConversation: (conversationId: string) => void handlePinConversation: (conversationId: string) => void
handleUnpinConversation: (conversationId: string) => void handleUnpinConversation: (conversationId: string) => void
@ -49,6 +48,8 @@ export type ChatWithHistoryContextValue = {
handleFeedback: (messageId: string, feedback: Feedback) => void handleFeedback: (messageId: string, feedback: Feedback) => void
currentChatInstanceRef: RefObject<{ handleStop: () => void }> currentChatInstanceRef: RefObject<{ handleStop: () => void }>
themeBuilder?: ThemeBuilder themeBuilder?: ThemeBuilder
sidebarCollapseState?: boolean
handleSidebarCollapse: (state: boolean) => void
} }
export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>({ export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>({
@ -56,7 +57,6 @@ export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>
appPrevChatTree: [], appPrevChatTree: [],
pinnedConversationList: [], pinnedConversationList: [],
conversationList: [], conversationList: [],
showConfigPanelBeforeChat: false,
newConversationInputs: {}, newConversationInputs: {},
newConversationInputsRef: { current: {} }, newConversationInputsRef: { current: {} },
handleNewConversationInputsChange: () => {}, handleNewConversationInputsChange: () => {},
@ -75,5 +75,7 @@ export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>
isInstalledApp: false, isInstalledApp: false,
handleFeedback: () => {}, handleFeedback: () => {},
currentChatInstanceRef: { current: { handleStop: () => {} } }, currentChatInstanceRef: { current: { handleStop: () => {} } },
sidebarCollapseState: false,
handleSidebarCollapse: () => {},
}) })
export const useChatWithHistoryContext = () => useContext(ChatWithHistoryContext) export const useChatWithHistoryContext = () => useContext(ChatWithHistoryContext)

View File

@ -1,60 +1,148 @@
import { useState } from 'react' import { useCallback, useState } from 'react'
import { useChatWithHistoryContext } from './context' import { useTranslation } from 'react-i18next'
import Sidebar from './sidebar'
import AppIcon from '@/app/components/base/app-icon'
import { import {
Edit05, RiMenuLine,
Menu01, } from '@remixicon/react'
} from '@/app/components/base/icons/src/vender/line/general' import { useChatWithHistoryContext } from './context'
import Operation from './header/operation'
import Sidebar from './sidebar'
import MobileOperationDropdown from './header/mobile-operation-dropdown'
import AppIcon from '@/app/components/base/app-icon'
import ActionButton from '@/app/components/base/action-button'
import { Message3Fill } from '@/app/components/base/icons/src/public/other'
import InputsFormContent from '@/app/components/base/chat/chat-with-history/inputs-form/content'
import Confirm from '@/app/components/base/confirm'
import RenameModal from '@/app/components/base/chat/chat-with-history/sidebar/rename-modal'
import type { ConversationItem } from '@/models/share'
const HeaderInMobile = () => { const HeaderInMobile = () => {
const { const {
appData, appData,
currentConversationId,
currentConversationItem,
pinnedConversationList,
handleNewConversation, handleNewConversation,
handlePinConversation,
handleUnpinConversation,
handleDeleteConversation,
handleRenameConversation,
conversationRenaming,
} = useChatWithHistoryContext() } = useChatWithHistoryContext()
const { t } = useTranslation()
const isPin = pinnedConversationList.some(item => item.id === currentConversationId)
const [showConfirm, setShowConfirm] = useState<ConversationItem | null>(null)
const [showRename, setShowRename] = useState<ConversationItem | null>(null)
const handleOperate = useCallback((type: string) => {
if (type === 'pin')
handlePinConversation(currentConversationId)
if (type === 'unpin')
handleUnpinConversation(currentConversationId)
if (type === 'delete')
setShowConfirm(currentConversationItem as any)
if (type === 'rename')
setShowRename(currentConversationItem as any)
}, [currentConversationId, currentConversationItem, handlePinConversation, handleUnpinConversation])
const handleCancelConfirm = useCallback(() => {
setShowConfirm(null)
}, [])
const handleDelete = useCallback(() => {
if (showConfirm)
handleDeleteConversation(showConfirm.id, { onSuccess: handleCancelConfirm })
}, [showConfirm, handleDeleteConversation, handleCancelConfirm])
const handleCancelRename = useCallback(() => {
setShowRename(null)
}, [])
const handleRename = useCallback((newName: string) => {
if (showRename)
handleRenameConversation(showRename.id, newName, { onSuccess: handleCancelRename })
}, [showRename, handleRenameConversation, handleCancelRename])
const [showSidebar, setShowSidebar] = useState(false) const [showSidebar, setShowSidebar] = useState(false)
const [showChatSettings, setShowChatSettings] = useState(false)
return ( return (
<> <>
<div className='shrink-0 flex items-center px-3 h-[44px] border-b-[0.5px] border-b-gray-200'> <div className='shrink-0 flex items-center px-2 py-3 gap-1 bg-mask-top2bottom-gray-50-to-transparent'>
<div <ActionButton size='l' className='shrink-0' onClick={() => setShowSidebar(true)}>
className='shrink-0 flex items-center justify-center w-8 h-8 rounded-lg' <RiMenuLine className='w-[18px] h-[18px]' />
onClick={() => setShowSidebar(true)} </ActionButton>
> <div className='grow flex justify-center items-center'>
<Menu01 className='w-4 h-4 text-gray-700' /> {!currentConversationId && (
<>
<AppIcon
className='mr-2'
size='tiny'
icon={appData?.site.icon}
iconType={appData?.site.icon_type}
imageUrl={appData?.site.icon_url}
background={appData?.site.icon_background}
/>
<div className='text-text-secondary system-md-semibold truncate'>
{appData?.site.title}
</div>
</>
)}
{currentConversationId && (
<Operation
title={currentConversationItem?.name || ''}
isPinned={!!isPin}
togglePin={() => handleOperate(isPin ? 'unpin' : 'pin')}
isShowDelete
isShowRenameConversation
onRenameConversation={() => handleOperate('rename')}
onDelete={() => handleOperate('delete')}
/>
)}
</div> </div>
<div className='grow flex justify-center items-center px-3'> <MobileOperationDropdown
<AppIcon handleResetChat={handleNewConversation}
className='mr-2' handleViewChatSettings={() => setShowChatSettings(true)}
size='tiny' />
icon={appData?.site.icon} </div>
iconType={appData?.site.icon_type} {showSidebar && (
imageUrl={appData?.site.icon_url} <div className='fixed inset-0 z-50 flex p-1 bg-background-overlay'
background={appData?.site.icon_background} onClick={() => setShowSidebar(false)}
/> >
<div className='py-1 text-base font-semibold text-gray-800 truncate'> <div className='flex h-full w-[calc(100vw_-_40px)] bg-components-panel-bg backdrop-blur-sm rounded-xl shadow-lg' onClick={e => e.stopPropagation()}>
{appData?.site.title} <Sidebar />
</div> </div>
</div> </div>
<div )}
className='shrink-0 flex items-center justify-center w-8 h-8 rounded-lg' {showChatSettings && (
onClick={handleNewConversation} <div className='fixed inset-0 z-50 flex justify-end p-1 bg-background-overlay'
onClick={() => setShowChatSettings(false)}
> >
<Edit05 className='w-4 h-4 text-gray-700' /> <div className='flex flex-col h-full w-[calc(100vw_-_40px)] bg-components-panel-bg backdrop-blur-sm rounded-xl shadow-lg' onClick={e => e.stopPropagation()}>
</div> <div className='flex items-center gap-3 px-4 py-3 rounded-t-2xl border-b border-divider-subtle'>
</div> <Message3Fill className='shrink-0 w-6 h-6' />
{ <div className='grow text-text-secondary system-xl-semibold'>{t('share.chat.chatSettingsTitle')}</div>
showSidebar && ( </div>
<div className='fixed inset-0 z-50' <div className='p-4'>
style={{ backgroundColor: 'rgba(35, 56, 118, 0.2)' }} <InputsFormContent showTip />
onClick={() => setShowSidebar(false)}
>
<div className='inline-block h-full bg-white' onClick={e => e.stopPropagation()}>
<Sidebar />
</div> </div>
</div> </div>
) </div>
} )}
{!!showConfirm && (
<Confirm
title={t('share.chat.deleteConversation.title')}
content={t('share.chat.deleteConversation.content') || ''}
isShow
onCancel={handleCancelConfirm}
onConfirm={handleDelete}
/>
)}
{showRename && (
<RenameModal
isShow
onClose={handleCancelRename}
saveLoading={conversationRenaming}
name={showRename?.name || ''}
onSave={handleRename}
/>
)}
</> </>
) )
} }

View File

@ -1,25 +0,0 @@
import type { FC } from 'react'
import { memo } from 'react'
type HeaderProps = {
title: string
isMobile: boolean
}
const Header: FC<HeaderProps> = ({
title,
isMobile,
}) => {
return (
<div
className={`
sticky top-0 flex items-center px-8 h-16 bg-white/80 text-base font-medium
text-gray-900 border-b-[0.5px] border-b-gray-100 backdrop-blur-md z-10
${isMobile && '!h-12'}
`}
>
{title}
</div>
)
}
export default memo(Header)

View File

@ -0,0 +1,151 @@
import { useCallback, useState } from 'react'
import {
RiEditBoxLine,
RiLayoutRight2Line,
RiResetLeftLine,
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import {
useChatWithHistoryContext,
} from '../context'
import Operation from './operation'
import ActionButton from '@/app/components/base/action-button'
import AppIcon from '@/app/components/base/app-icon'
import Tooltip from '@/app/components/base/tooltip'
import ViewFormDropdown from '@/app/components/base/chat/chat-with-history/inputs-form/view-form-dropdown'
import Confirm from '@/app/components/base/confirm'
import RenameModal from '@/app/components/base/chat/chat-with-history/sidebar/rename-modal'
import type { ConversationItem } from '@/models/share'
import cn from '@/utils/classnames'
const Header = () => {
const {
appData,
currentConversationId,
currentConversationItem,
inputsForms,
pinnedConversationList,
handlePinConversation,
handleUnpinConversation,
conversationRenaming,
handleRenameConversation,
handleDeleteConversation,
handleNewConversation,
sidebarCollapseState,
handleSidebarCollapse,
} = useChatWithHistoryContext()
const { t } = useTranslation()
const isSidebarCollapsed = sidebarCollapseState
const isPin = pinnedConversationList.some(item => item.id === currentConversationId)
const [showConfirm, setShowConfirm] = useState<ConversationItem | null>(null)
const [showRename, setShowRename] = useState<ConversationItem | null>(null)
const handleOperate = useCallback((type: string) => {
if (type === 'pin')
handlePinConversation(currentConversationId)
if (type === 'unpin')
handleUnpinConversation(currentConversationId)
if (type === 'delete')
setShowConfirm(currentConversationItem as any)
if (type === 'rename')
setShowRename(currentConversationItem as any)
}, [currentConversationId, currentConversationItem, handlePinConversation, handleUnpinConversation])
const handleCancelConfirm = useCallback(() => {
setShowConfirm(null)
}, [])
const handleDelete = useCallback(() => {
if (showConfirm)
handleDeleteConversation(showConfirm.id, { onSuccess: handleCancelConfirm })
}, [showConfirm, handleDeleteConversation, handleCancelConfirm])
const handleCancelRename = useCallback(() => {
setShowRename(null)
}, [])
const handleRename = useCallback((newName: string) => {
if (showRename)
handleRenameConversation(showRename.id, newName, { onSuccess: handleCancelRename })
}, [showRename, handleRenameConversation, handleCancelRename])
return (
<>
<div className='shrink-0 h-14 p-3 flex items-center justify-between'>
<div className={cn('flex items-center gap-1 transition-all duration-200 ease-in-out', !isSidebarCollapsed && 'opacity-0 user-select-none')}>
<ActionButton className={cn(!isSidebarCollapsed && 'cursor-default')} size='l' onClick={() => handleSidebarCollapse(false)}>
<RiLayoutRight2Line className='w-[18px] h-[18px]' />
</ActionButton>
<div className='shrink-0 mr-1'>
<AppIcon
size='large'
iconType={appData?.site.icon_type}
icon={appData?.site.icon}
background={appData?.site.icon_background}
imageUrl={appData?.site.icon_url}
/>
</div>
{!currentConversationId && (
<div className={cn('grow text-text-secondary system-md-semibold truncate')}>{appData?.site.title}</div>
)}
{currentConversationId && currentConversationItem && isSidebarCollapsed && (
<>
<div className='p-1 text-divider-deep'>/</div>
<Operation
title={currentConversationItem?.name || ''}
isPinned={!!isPin}
togglePin={() => handleOperate(isPin ? 'unpin' : 'pin')}
isShowDelete
isShowRenameConversation
onRenameConversation={() => handleOperate('rename')}
onDelete={() => handleOperate('delete')}
/>
</>
)}
<div className='px-1 flex items-center'>
<div className='h-[14px] w-px bg-divider-regular'></div>
</div>
{isSidebarCollapsed && (
<ActionButton size='l' onClick={handleNewConversation}>
<RiEditBoxLine className='w-[18px] h-[18px]' />
</ActionButton>
)}
</div>
<div className='flex items-center gap-1'>
{currentConversationId && (
<Tooltip
popupContent={t('share.chat.resetChat')}
>
<ActionButton size='l' onClick={handleNewConversation}>
<RiResetLeftLine className='w-[18px] h-[18px]' />
</ActionButton>
</Tooltip>
)}
{currentConversationId && inputsForms.length > 0 && (
<ViewFormDropdown />
)}
</div>
</div>
{!!showConfirm && (
<Confirm
title={t('share.chat.deleteConversation.title')}
content={t('share.chat.deleteConversation.content') || ''}
isShow
onCancel={handleCancelConfirm}
onConfirm={handleDelete}
/>
)}
{showRename && (
<RenameModal
isShow
onClose={handleCancelRename}
saveLoading={conversationRenaming}
name={showRename?.name || ''}
onSave={handleRename}
/>
)}
</>
)
}
export default Header

View File

@ -0,0 +1,55 @@
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiMoreFill,
} from '@remixicon/react'
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
type Props = {
handleResetChat: () => void
handleViewChatSettings: () => void
}
const MobileOperationDropdown = ({
handleResetChat,
handleViewChatSettings,
}: Props) => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='bottom-end'
offset={{
mainAxis: 4,
crossAxis: -4,
}}
>
<PortalToFollowElemTrigger
onClick={() => setOpen(v => !v)}
>
<ActionButton size='l' state={open ? ActionButtonState.Hover : ActionButtonState.Default}>
<RiMoreFill className='w-[18px] h-[18px]' />
</ActionButton>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className="z-40">
<div
className={'min-w-[160px] p-1 bg-components-panel-bg-blur backdrop-blur-sm rounded-xl border-[0.5px] border-components-panel-border shadow-lg'}
>
<div className='flex items-center space-x-1 px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover' onClick={handleResetChat}>
<span className='grow'>{t('share.chat.resetChat')}</span>
</div>
<div className='flex items-center space-x-1 px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover' onClick={handleViewChatSettings}>
<span className='grow'>{t('share.chat.viewChatSettings')}</span>
</div>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default MobileOperationDropdown

View File

@ -0,0 +1,73 @@
'use client'
import type { FC } from 'react'
import React, { useState } from 'react'
import type { Placement } from '@floating-ui/react'
import {
RiArrowDownSLine,
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
import cn from '@/utils/classnames'
type Props = {
title: string
isPinned: boolean
isShowRenameConversation?: boolean
onRenameConversation?: () => void
isShowDelete: boolean
togglePin: () => void
onDelete: () => void
placement?: Placement
}
const Operation: FC<Props> = ({
title,
isPinned,
togglePin,
isShowRenameConversation,
onRenameConversation,
isShowDelete,
onDelete,
placement = 'bottom-start',
}) => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement={placement}
offset={4}
>
<PortalToFollowElemTrigger
onClick={() => setOpen(v => !v)}
>
<div className={cn('flex items-center p-1.5 pl-2 rounded-lg text-text-secondary cursor-pointer hover:bg-state-base-hover', open && 'bg-state-base-hover')}>
<div className='system-md-semibold'>{title}</div>
<RiArrowDownSLine className='w-4 h-4 ' />
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className="z-50">
<div
className={'min-w-[120px] p-1 bg-components-panel-bg-blur backdrop-blur-sm rounded-xl border-[0.5px] border-components-panel-border shadow-lg'}
>
<div className={cn('flex items-center space-x-1 px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover')} onClick={togglePin}>
<span className='grow'>{isPinned ? t('explore.sidebar.action.unpin') : t('explore.sidebar.action.pin')}</span>
</div>
{isShowRenameConversation && (
<div className={cn('flex items-center space-x-1 px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover')} onClick={onRenameConversation}>
<span className='grow'>{t('explore.sidebar.action.rename')}</span>
</div>
)}
{isShowDelete && (
<div className={cn('group flex items-center space-x-1 px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-destructive-hover hover:text-text-destructive')} onClick={onDelete} >
<span className='grow'>{t('explore.sidebar.action.delete')}</span>
</div>
)}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default React.memo(Operation)

View File

@ -110,6 +110,19 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
changeLanguage(appData.site.default_language) changeLanguage(appData.site.default_language)
}, [appData]) }, [appData])
const [sidebarCollapseState, setSidebarCollapseState] = useState<boolean>(false)
const handleSidebarCollapse = useCallback((state: boolean) => {
if (appId) {
setSidebarCollapseState(state)
localStorage.setItem('webappSidebarCollapse', state ? 'collapsed' : 'expanded')
}
}, [appId, setSidebarCollapseState])
useEffect(() => {
if (appId) {
const localState = localStorage.getItem('webappSidebarCollapse')
setSidebarCollapseState(localState === 'collapsed')
}
}, [appId])
const [conversationIdInfo, setConversationIdInfo] = useLocalStorageState<Record<string, string>>(CONVERSATION_ID_INFO, { const [conversationIdInfo, setConversationIdInfo] = useLocalStorageState<Record<string, string>>(CONVERSATION_ID_INFO, {
defaultValue: {}, defaultValue: {},
}) })
@ -122,7 +135,6 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
}) })
} }
}, [appId, conversationIdInfo, setConversationIdInfo]) }, [appId, conversationIdInfo, setConversationIdInfo])
const [showConfigPanelBeforeChat, setShowConfigPanelBeforeChat] = useState(true)
const [newConversationId, setNewConversationId] = useState('') const [newConversationId, setNewConversationId] = useState('')
const chatShouldReloadKey = useMemo(() => { const chatShouldReloadKey = useMemo(() => {
@ -287,23 +299,18 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
return true return true
}, [inputsForms, notify, t]) }, [inputsForms, notify, t])
const handleStartChat = useCallback(() => { const handleStartChat = useCallback((callback: any) => {
if (checkInputsRequired()) { if (checkInputsRequired()) {
setShowConfigPanelBeforeChat(false)
setShowNewConversationItemInList(true) setShowNewConversationItemInList(true)
callback?.()
} }
}, [setShowConfigPanelBeforeChat, setShowNewConversationItemInList, checkInputsRequired]) }, [setShowNewConversationItemInList, checkInputsRequired])
const currentChatInstanceRef = useRef<{ handleStop: () => void }>({ handleStop: () => { } }) const currentChatInstanceRef = useRef<{ handleStop: () => void }>({ handleStop: () => { } })
const handleChangeConversation = useCallback((conversationId: string) => { const handleChangeConversation = useCallback((conversationId: string) => {
currentChatInstanceRef.current.handleStop() currentChatInstanceRef.current.handleStop()
setNewConversationId('') setNewConversationId('')
handleConversationIdInfoChange(conversationId) handleConversationIdInfoChange(conversationId)
}, [handleConversationIdInfoChange])
if (conversationId === '' && !checkInputsRequired(true))
setShowConfigPanelBeforeChat(true)
else
setShowConfigPanelBeforeChat(false)
}, [handleConversationIdInfoChange, setShowConfigPanelBeforeChat, checkInputsRequired])
const handleNewConversation = useCallback(() => { const handleNewConversation = useCallback(() => {
currentChatInstanceRef.current.handleStop() currentChatInstanceRef.current.handleStop()
setNewConversationId('') setNewConversationId('')
@ -313,11 +320,10 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
} }
else if (currentConversationId) { else if (currentConversationId) {
handleConversationIdInfoChange('') handleConversationIdInfoChange('')
setShowConfigPanelBeforeChat(true)
setShowNewConversationItemInList(true) setShowNewConversationItemInList(true)
handleNewConversationInputsChange({}) handleNewConversationInputsChange({})
} }
}, [handleChangeConversation, currentConversationId, handleConversationIdInfoChange, setShowConfigPanelBeforeChat, setShowNewConversationItemInList, showNewConversationItemInList, handleNewConversationInputsChange]) }, [handleChangeConversation, currentConversationId, handleConversationIdInfoChange, setShowNewConversationItemInList, showNewConversationItemInList, handleNewConversationInputsChange])
const handleUpdateConversationList = useCallback(() => { const handleUpdateConversationList = useCallback(() => {
mutateAppConversationData() mutateAppConversationData()
mutateAppPinnedConversationData() mutateAppPinnedConversationData()
@ -435,8 +441,6 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
appPrevChatTree, appPrevChatTree,
pinnedConversationList, pinnedConversationList,
conversationList, conversationList,
showConfigPanelBeforeChat,
setShowConfigPanelBeforeChat,
setShowNewConversationItemInList, setShowNewConversationItemInList,
newConversationInputs, newConversationInputs,
newConversationInputsRef, newConversationInputsRef,
@ -456,5 +460,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
chatShouldReloadKey, chatShouldReloadKey,
handleFeedback, handleFeedback,
currentChatInstanceRef, currentChatInstanceRef,
sidebarCollapseState,
handleSidebarCollapse,
} }
} }

View File

@ -11,14 +11,15 @@ import {
} from './context' } from './context'
import { useChatWithHistory } from './hooks' import { useChatWithHistory } from './hooks'
import Sidebar from './sidebar' import Sidebar from './sidebar'
import Header from './header'
import HeaderInMobile from './header-in-mobile' import HeaderInMobile from './header-in-mobile'
import ConfigPanel from './config-panel'
import ChatWrapper from './chat-wrapper' import ChatWrapper from './chat-wrapper'
import type { InstalledApp } from '@/models/explore' import type { InstalledApp } from '@/models/explore'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import { checkOrSetAccessToken } from '@/app/components/share/utils' import { checkOrSetAccessToken } from '@/app/components/share/utils'
import AppUnavailable from '@/app/components/base/app-unavailable' import AppUnavailable from '@/app/components/base/app-unavailable'
import cn from '@/utils/classnames'
type ChatWithHistoryProps = { type ChatWithHistoryProps = {
className?: string className?: string
@ -30,18 +31,18 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({
appInfoError, appInfoError,
appData, appData,
appInfoLoading, appInfoLoading,
appPrevChatTree,
showConfigPanelBeforeChat,
appChatListDataLoading, appChatListDataLoading,
chatShouldReloadKey, chatShouldReloadKey,
isMobile, isMobile,
themeBuilder, themeBuilder,
sidebarCollapseState,
} = useChatWithHistoryContext() } = useChatWithHistoryContext()
const isSidebarCollapsed = sidebarCollapseState
const chatReady = (!showConfigPanelBeforeChat || !!appPrevChatTree.length)
const customConfig = appData?.custom_config const customConfig = appData?.custom_config
const site = appData?.site const site = appData?.site
const [showSidePanel, setShowSidePanel] = useState(false)
useEffect(() => { useEffect(() => {
themeBuilder?.buildTheme(site?.chat_color_theme, site?.chat_color_theme_inverted) themeBuilder?.buildTheme(site?.chat_color_theme, site?.chat_color_theme_inverted)
if (site) { if (site) {
@ -65,35 +66,44 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({
} }
return ( return (
<div className={`h-full flex bg-white ${className} ${isMobile && 'flex-col'}`}> <div className={cn(
{ 'h-full flex bg-background-default-burn',
!isMobile && ( isMobile && 'flex-col',
className,
)}>
{!isMobile && (
<div className={cn(
'flex flex-col w-[236px] p-1 pr-0 transition-all duration-200 ease-in-out',
isSidebarCollapsed && 'w-0 !p-0 overflow-hidden',
)}>
<Sidebar /> <Sidebar />
) </div>
} )}
{ {isMobile && (
isMobile && ( <HeaderInMobile />
<HeaderInMobile /> )}
) <div className={cn('relative grow p-2')}>
} {isSidebarCollapsed && (
<div className={`grow overflow-hidden ${showConfigPanelBeforeChat && !appPrevChatTree.length && 'flex items-center justify-center'}`}> <div
{ className={cn(
showConfigPanelBeforeChat && !appChatListDataLoading && !appPrevChatTree.length && ( 'z-20 absolute top-0 w-[256px] h-full flex flex-col p-2 transition-all duration-500 ease-in-out',
<div className={`flex w-full items-center justify-center h-full ${isMobile && 'px-4'}`}> showSidePanel ? 'left-0' : 'left-[-248px]',
<ConfigPanel /> )}
</div> onMouseEnter={() => setShowSidePanel(true)}
) onMouseLeave={() => setShowSidePanel(false)}
} >
{ <Sidebar isPanel />
appChatListDataLoading && chatReady && ( </div>
)}
<div className='h-full flex flex-col bg-chatbot-bg rounded-2xl border-[0,5px] border-components-panel-border-subtle overflow-hidden'>
{!isMobile && <Header />}
{appChatListDataLoading && (
<Loading type='app' /> <Loading type='app' />
) )}
} {!appChatListDataLoading && (
{
chatReady && !appChatListDataLoading && (
<ChatWrapper key={chatShouldReloadKey} /> <ChatWrapper key={chatShouldReloadKey} />
) )}
} </div>
</div> </div>
</div> </div>
) )
@ -123,7 +133,6 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
appPrevChatTree, appPrevChatTree,
pinnedConversationList, pinnedConversationList,
conversationList, conversationList,
showConfigPanelBeforeChat,
newConversationInputs, newConversationInputs,
newConversationInputsRef, newConversationInputsRef,
handleNewConversationInputsChange, handleNewConversationInputsChange,
@ -142,6 +151,8 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
appId, appId,
handleFeedback, handleFeedback,
currentChatInstanceRef, currentChatInstanceRef,
sidebarCollapseState,
handleSidebarCollapse,
} = useChatWithHistory(installedAppInfo) } = useChatWithHistory(installedAppInfo)
return ( return (
@ -157,7 +168,6 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
appPrevChatTree, appPrevChatTree,
pinnedConversationList, pinnedConversationList,
conversationList, conversationList,
showConfigPanelBeforeChat,
newConversationInputs, newConversationInputs,
newConversationInputsRef, newConversationInputsRef,
handleNewConversationInputsChange, handleNewConversationInputsChange,
@ -178,6 +188,8 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
handleFeedback, handleFeedback,
currentChatInstanceRef, currentChatInstanceRef,
themeBuilder, themeBuilder,
sidebarCollapseState,
handleSidebarCollapse,
}}> }}>
<ChatWithHistory className={className} /> <ChatWithHistory className={className} />
</ChatWithHistoryContext.Provider> </ChatWithHistoryContext.Provider>

View File

@ -0,0 +1,118 @@
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useChatWithHistoryContext } from '../context'
import Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea'
import { PortalSelect } from '@/app/components/base/select'
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
import { InputVarType } from '@/app/components/workflow/types'
type Props = {
showTip?: boolean
}
const InputsFormContent = ({ showTip }: Props) => {
const { t } = useTranslation()
const {
appParams,
inputsForms,
currentConversationId,
currentConversationItem,
newConversationInputs,
newConversationInputsRef,
handleNewConversationInputsChange,
} = useChatWithHistoryContext()
const inputsFormValue = currentConversationId ? currentConversationItem?.inputs : newConversationInputs
const readonly = !!currentConversationId
const handleFormChange = useCallback((variable: string, value: any) => {
handleNewConversationInputsChange({
...newConversationInputsRef.current,
[variable]: value,
})
}, [newConversationInputsRef, handleNewConversationInputsChange])
return (
<div className='space-y-4'>
{inputsForms.map(form => (
<div key={form.variable} className='space-y-1'>
<div className='h-6 flex items-center gap-1'>
<div className='text-text-secondary system-md-semibold'>{form.label}</div>
{!form.required && (
<div className='text-text-tertiary system-xs-regular'>{t('appDebug.variableTable.optional')}</div>
)}
</div>
{form.type === InputVarType.textInput && (
<Input
value={inputsFormValue?.[form.variable] || ''}
onChange={e => handleFormChange(form.variable, e.target.value)}
placeholder={form.label}
readOnly={readonly}
disabled={readonly}
/>
)}
{form.type === InputVarType.number && (
<Input
type='number'
value={inputsFormValue?.[form.variable] || ''}
onChange={e => handleFormChange(form.variable, e.target.value)}
placeholder={form.label}
readOnly={readonly}
disabled={readonly}
/>
)}
{form.type === InputVarType.paragraph && (
<Textarea
value={inputsFormValue?.[form.variable] || ''}
onChange={e => handleFormChange(form.variable, e.target.value)}
placeholder={form.label}
readOnly={readonly}
disabled={readonly}
/>
)}
{form.type === InputVarType.select && (
<PortalSelect
popupClassName='w-[200px]'
value={inputsFormValue?.[form.variable]}
items={form.options.map((option: string) => ({ value: option, name: option }))}
onSelect={item => handleFormChange(form.variable, item.value as string)}
placeholder={form.label}
readonly={readonly}
/>
)}
{form.type === InputVarType.singleFile && (
<FileUploaderInAttachmentWrapper
value={inputsFormValue?.[form.variable] ? [inputsFormValue?.[form.variable]] : []}
onChange={files => handleFormChange(form.variable, files[0])}
fileConfig={{
allowed_file_types: form.allowed_file_types,
allowed_file_extensions: form.allowed_file_extensions,
allowed_file_upload_methods: form.allowed_file_upload_methods,
number_limits: 1,
fileUploadConfig: (appParams as any).system_parameters,
}}
/>
)}
{form.type === InputVarType.multiFiles && (
<FileUploaderInAttachmentWrapper
value={inputsFormValue?.[form.variable] || []}
onChange={files => handleFormChange(form.variable, files)}
fileConfig={{
allowed_file_types: form.allowed_file_types,
allowed_file_extensions: form.allowed_file_extensions,
allowed_file_upload_methods: form.allowed_file_upload_methods,
number_limits: form.max_length,
fileUploadConfig: (appParams as any).system_parameters,
}}
/>
)}
</div>
))}
{showTip && (
<div className='text-text-tertiary system-xs-regular'>{t('share.chat.chatFormTip')}</div>
)}
</div>
)
}
export default InputsFormContent

View File

@ -0,0 +1,79 @@
import React from 'react'
import { useTranslation } from 'react-i18next'
import { Message3Fill } from '@/app/components/base/icons/src/public/other'
import Button from '@/app/components/base/button'
import Divider from '@/app/components/base/divider'
import InputsFormContent from '@/app/components/base/chat/chat-with-history/inputs-form/content'
import { useChatWithHistoryContext } from '../context'
import cn from '@/utils/classnames'
type Props = {
collapsed: boolean
setCollapsed: (collapsed: boolean) => void
}
const InputsFormNode = ({
collapsed,
setCollapsed,
}: Props) => {
const { t } = useTranslation()
const {
isMobile,
currentConversationId,
handleStartChat,
themeBuilder,
} = useChatWithHistoryContext()
return (
<div className={cn('pt-6 px-4 flex flex-col items-center', isMobile && 'pt-4')}>
<div className={cn(
'w-full max-w-[672px] bg-components-panel-bg rounded-2xl border-[0.5px] border-components-panel-border shadow-md',
collapsed && 'bg-components-card-bg border border-components-card-border shadow-none',
)}>
<div className={cn(
'flex items-center gap-3 px-6 py-4 rounded-t-2xl',
!collapsed && 'border-b border-divider-subtle',
isMobile && 'px-4 py-3',
)}>
<Message3Fill className='shrink-0 w-6 h-6' />
<div className='grow text-text-secondary system-xl-semibold'>{t('share.chat.chatSettingsTitle')}</div>
{collapsed && (
<Button className='text-text-tertiary uppercase' size='small' variant='ghost' onClick={() => setCollapsed(false)}>{currentConversationId ? t('common.operation.view') : t('common.operation.edit')}</Button>
)}
{!collapsed && currentConversationId && (
<Button className='text-text-tertiary uppercase' size='small' variant='ghost' onClick={() => setCollapsed(true)}>{t('common.operation.close')}</Button>
)}
</div>
{!collapsed && (
<div className={cn('p-6', isMobile && 'p-4')}>
<InputsFormContent showTip={!!currentConversationId} />
</div>
)}
{!collapsed && !currentConversationId && (
<div className={cn('p-6', isMobile && 'p-4')}>
<Button
variant='primary'
className='w-full'
onClick={() => handleStartChat(() => setCollapsed(true))}
style={
themeBuilder?.theme
? {
backgroundColor: themeBuilder?.theme.primaryColor,
}
: {}
}
>{t('share.chat.startChat')}</Button>
</div>
)}
</div>
{collapsed && (
<div className='py-4 flex items-center w-full max-w-[720px]'>
<Divider bgStyle='gradient' className='basis-1/2 h-px rotate-180' />
<Divider bgStyle='gradient' className='basis-1/2 h-px' />
</div>
)}
</div>
)
}
export default InputsFormNode

View File

@ -0,0 +1,48 @@
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiChatSettingsLine,
} from '@remixicon/react'
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
import { Message3Fill } from '@/app/components/base/icons/src/public/other'
import InputsFormContent from '@/app/components/base/chat/chat-with-history/inputs-form/content'
const ViewFormDropdown = () => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='bottom-end'
offset={{
mainAxis: 4,
crossAxis: 4,
}}
>
<PortalToFollowElemTrigger
onClick={() => setOpen(v => !v)}
>
<ActionButton size='l' state={open ? ActionButtonState.Hover : ActionButtonState.Default}>
<RiChatSettingsLine className='w-[18px] h-[18px]' />
</ActionButton>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className="z-50">
<div className='w-[400px] bg-components-panel-bg backdrop-blur-sm rounded-2xl border-[0.5px] border-components-panel-border shadow-lg'>
<div className='flex items-center gap-3 px-6 py-4 rounded-t-2xl border-b border-divider-subtle'>
<Message3Fill className='shrink-0 w-6 h-6' />
<div className='grow text-text-secondary system-xl-semibold'>{t('share.chat.chatSettingsTitle')}</div>
</div>
<div className='p-6'>
<InputsFormContent showTip />
</div>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default ViewFormDropdown

View File

@ -3,22 +3,34 @@ import {
useState, useState,
} from 'react' } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import {
RiEditBoxLine,
RiExpandRightLine,
RiLayoutLeft2Line,
} from '@remixicon/react'
import { useChatWithHistoryContext } from '../context' import { useChatWithHistoryContext } from '../context'
import List from './list'
import AppIcon from '@/app/components/base/app-icon' import AppIcon from '@/app/components/base/app-icon'
import ActionButton from '@/app/components/base/action-button'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import { Edit05 } from '@/app/components/base/icons/src/vender/line/general' import List from '@/app/components/base/chat/chat-with-history/sidebar/list'
import type { ConversationItem } from '@/models/share' import MenuDropdown from '@/app/components/share/text-generation/menu-dropdown'
import Confirm from '@/app/components/base/confirm' import Confirm from '@/app/components/base/confirm'
import RenameModal from '@/app/components/base/chat/chat-with-history/sidebar/rename-modal' import RenameModal from '@/app/components/base/chat/chat-with-history/sidebar/rename-modal'
import LogoSite from '@/app/components/base/logo/logo-site'
import type { ConversationItem } from '@/models/share'
import cn from '@/utils/classnames'
const Sidebar = () => { type Props = {
isPanel?: boolean
}
const Sidebar = ({ isPanel }: Props) => {
const { t } = useTranslation() const { t } = useTranslation()
const { const {
appData, appData,
handleNewConversation,
pinnedConversationList, pinnedConversationList,
conversationList, conversationList,
handleNewConversation,
currentConversationId, currentConversationId,
handleChangeConversation, handleChangeConversation,
handlePinConversation, handlePinConversation,
@ -26,8 +38,12 @@ const Sidebar = () => {
conversationRenaming, conversationRenaming,
handleRenameConversation, handleRenameConversation,
handleDeleteConversation, handleDeleteConversation,
sidebarCollapseState,
handleSidebarCollapse,
isMobile, isMobile,
} = useChatWithHistoryContext() } = useChatWithHistoryContext()
const isSidebarCollapsed = sidebarCollapseState
const [showConfirm, setShowConfirm] = useState<ConversationItem | null>(null) const [showConfirm, setShowConfirm] = useState<ConversationItem | null>(null)
const [showRename, setShowRename] = useState<ConversationItem | null>(null) const [showRename, setShowRename] = useState<ConversationItem | null>(null)
@ -60,66 +76,83 @@ const Sidebar = () => {
}, [showRename, handleRenameConversation, handleCancelRename]) }, [showRename, handleRenameConversation, handleCancelRename])
return ( return (
<div className='shrink-0 h-full flex flex-col w-[240px] border-r border-r-gray-100'> <div className={cn(
{ 'grow flex flex-col',
!isMobile && ( isPanel && 'rounded-xl bg-components-panel-bg border-[0.5px] border-components-panel-border-subtle shadow-lg',
<div className='shrink-0 flex p-4'> )}>
<AppIcon <div className={cn(
className='mr-3' 'shrink-0 flex items-center gap-3 p-3 pr-2',
size='small' )}>
iconType={appData?.site.icon_type} <div className='shrink-0'>
icon={appData?.site.icon} <AppIcon
background={appData?.site.icon_background} size='large'
imageUrl={appData?.site.icon_url} iconType={appData?.site.icon_type}
/> icon={appData?.site.icon}
<div className='py-1 text-base font-semibold text-gray-800'> background={appData?.site.icon_background}
{appData?.site.title} imageUrl={appData?.site.icon_url}
</div> />
</div> </div>
) <div className={cn('grow text-text-secondary system-md-semibold truncate')}>{appData?.site.title}</div>
} {!isMobile && isSidebarCollapsed && (
<div className='shrink-0 p-4'> <ActionButton size='l' onClick={() => handleSidebarCollapse(false)}>
<Button <RiExpandRightLine className='w-[18px] h-[18px]' />
variant='secondary-accent' </ActionButton>
className='justify-start w-full' )}
onClick={handleNewConversation} {!isMobile && !isSidebarCollapsed && (
> <ActionButton size='l' onClick={() => handleSidebarCollapse(true)}>
<Edit05 className='mr-2 w-4 h-4' /> <RiLayoutLeft2Line className='w-[18px] h-[18px]' />
</ActionButton>
)}
</div>
<div className='shrink-0 px-3 py-4'>
<Button variant='secondary-accent' className='w-full justify-center' onClick={handleNewConversation}>
<RiEditBoxLine className='w-4 h-4 mr-1' />
{t('share.chat.newChat')} {t('share.chat.newChat')}
</Button> </Button>
</div> </div>
<div className='grow px-4 py-2 overflow-y-auto'> <div className='grow h-0 pt-4 px-3 space-y-2 overflow-y-auto'>
{ {/* pinned list */}
!!pinnedConversationList.length && ( {!!pinnedConversationList.length && (
<div className='mb-4'> <div className='mb-4'>
<List
isPin
title={t('share.chat.pinnedTitle') || ''}
list={pinnedConversationList}
onChangeConversation={handleChangeConversation}
onOperate={handleOperate}
currentConversationId={currentConversationId}
/>
</div>
)
}
{
!!conversationList.length && (
<List <List
title={(pinnedConversationList.length && t('share.chat.unpinnedTitle')) || ''} isPin
list={conversationList} title={t('share.chat.pinnedTitle') || ''}
list={pinnedConversationList}
onChangeConversation={handleChangeConversation} onChangeConversation={handleChangeConversation}
onOperate={handleOperate} onOperate={handleOperate}
currentConversationId={currentConversationId} currentConversationId={currentConversationId}
/> />
) </div>
} )}
{!!conversationList.length && (
<List
title={(pinnedConversationList.length && t('share.chat.unpinnedTitle')) || ''}
list={conversationList}
onChangeConversation={handleChangeConversation}
onOperate={handleOperate}
currentConversationId={currentConversationId}
/>
)}
</div> </div>
{appData?.site.copyright && ( <div className='shrink-0 p-3 flex items-center justify-between'>
<div className='px-4 pb-4 text-xs text-gray-400'> <MenuDropdown placement='top-start' data={appData?.site} />
© {(new Date()).getFullYear()} {appData?.site.copyright} {/* powered by */}
<div className='shrink-0'>
{!appData?.custom_config?.remove_webapp_brand && (
<div className={cn(
'shrink-0 px-2 flex items-center gap-1.5',
)}>
<div className='text-text-tertiary system-2xs-medium-uppercase'>{t('share.chat.poweredBy')}</div>
{appData?.custom_config?.replace_webapp_logo && (
<img src={appData?.custom_config?.replace_webapp_logo} alt='logo' className='block w-auto h-5' />
)}
{!appData?.custom_config?.replace_webapp_logo && (
<LogoSite className='!h-5' />
)}
</div>
)}
</div> </div>
)} </div>
{!!showConfirm && ( {!!showConfirm && (
<Confirm <Confirm
title={t('share.chat.deleteConversation.title')} title={t('share.chat.deleteConversation.title')}

View File

@ -5,8 +5,8 @@ import {
} from 'react' } from 'react'
import { useHover } from 'ahooks' import { useHover } from 'ahooks'
import type { ConversationItem } from '@/models/share' import type { ConversationItem } from '@/models/share'
import { MessageDotsCircle } from '@/app/components/base/icons/src/vender/solid/communication' import Operation from '@/app/components/base/chat/chat-with-history/sidebar/operation'
import ItemOperation from '@/app/components/explore/item-operation' import cn from '@/utils/classnames'
type ItemProps = { type ItemProps = {
isPin?: boolean isPin?: boolean
@ -24,23 +24,23 @@ const Item: FC<ItemProps> = ({
}) => { }) => {
const ref = useRef(null) const ref = useRef(null)
const isHovering = useHover(ref) const isHovering = useHover(ref)
const isSelected = currentConversationId === item.id
return ( return (
<div <div
ref={ref} ref={ref}
key={item.id} key={item.id}
className={` className={cn(
flex mb-0.5 last-of-type:mb-0 py-1.5 pl-3 pr-1.5 text-sm font-medium text-gray-700 'group flex p-1 pl-3 rounded-lg cursor-pointer text-components-menu-item-text system-sm-medium hover:bg-state-base-hover',
rounded-lg cursor-pointer hover:bg-gray-50 group isSelected && 'bg-state-accent-active hover:bg-state-accent-active text-text-accent',
${currentConversationId === item.id && 'text-primary-600 bg-primary-50'} )}
`}
onClick={() => onChangeConversation(item.id)} onClick={() => onChangeConversation(item.id)}
> >
<MessageDotsCircle className={`shrink-0 mt-1 mr-2 w-4 h-4 text-gray-400 ${currentConversationId === item.id && 'text-primary-600'}`} /> <div className='grow p-1 pl-0 truncate' title={item.name}>{item.name}</div>
<div className='grow py-0.5 break-all' title={item.name}>{item.name}</div>
{item.id !== '' && ( {item.id !== '' && (
<div className='shrink-0 h-6' onClick={e => e.stopPropagation()}> <div className='shrink-0' onClick={e => e.stopPropagation()}>
<ItemOperation <Operation
isActive={isSelected}
isPinned={!!isPin} isPinned={!!isPin}
isItemHovering={isHovering} isItemHovering={isHovering}
togglePin={() => onOperate(isPin ? 'unpin' : 'pin', item)} togglePin={() => onOperate(isPin ? 'unpin' : 'pin', item)}

View File

@ -19,26 +19,20 @@ const List: FC<ListProps> = ({
currentConversationId, currentConversationId,
}) => { }) => {
return ( return (
<div> <div className='space-y-0.5'>
{ {title && (
title && ( <div className='px-3 pt-2 pb-1 text-text-tertiary system-xs-medium-uppercase'>{title}</div>
<div className='mb-0.5 px-3 h-[26px] text-xs font-medium text-gray-500'> )}
{title} {list.map(item => (
</div> <Item
) key={item.id}
} isPin={isPin}
{ item={item}
list.map(item => ( onOperate={onOperate}
<Item onChangeConversation={onChangeConversation}
key={item.id} currentConversationId={currentConversationId}
isPin={isPin} />
item={item} ))}
onOperate={onOperate}
onChangeConversation={onChangeConversation}
currentConversationId={currentConversationId}
/>
))
}
</div> </div>
) )
} }

View File

@ -0,0 +1,101 @@
'use client'
import type { FC } from 'react'
import React, { useEffect, useRef, useState } from 'react'
import {
RiDeleteBinLine,
RiEditLine,
RiMoreFill,
RiPushpinLine,
RiUnpinLine,
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks'
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
import cn from '@/utils/classnames'
type Props = {
isActive?: boolean
isItemHovering?: boolean
isPinned: boolean
isShowRenameConversation?: boolean
onRenameConversation?: () => void
isShowDelete: boolean
togglePin: () => void
onDelete: () => void
}
const Operation: FC<Props> = ({
isActive,
isItemHovering,
isPinned,
togglePin,
isShowRenameConversation,
onRenameConversation,
isShowDelete,
onDelete,
}) => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const ref = useRef(null)
const [isHovering, { setTrue: setIsHovering, setFalse: setNotHovering }] = useBoolean(false)
useEffect(() => {
if (!isItemHovering && !isHovering)
setOpen(false)
}, [isItemHovering, isHovering])
return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='bottom-end'
offset={4}
>
<PortalToFollowElemTrigger
onClick={() => setOpen(v => !v)}
>
<ActionButton
className={cn((isItemHovering || open) ? 'opacity-100' : 'opacity-0')}
state={
isActive
? ActionButtonState.Active
: open
? ActionButtonState.Hover
: ActionButtonState.Default
}
>
<RiMoreFill className='w-4 h-4' />
</ActionButton>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className="z-50">
<div
ref={ref}
className={'min-w-[120px] p-1 bg-components-panel-bg-blur backdrop-blur-sm rounded-xl border-[0.5px] border-components-panel-border shadow-lg'}
onMouseEnter={setIsHovering}
onMouseLeave={setNotHovering}
onClick={(e) => {
e.stopPropagation()
}}
>
<div className={cn('flex items-center space-x-1 px-2 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover')} onClick={togglePin}>
{isPinned && <RiUnpinLine className='shrink-0 w-4 h-4 text-text-tertiary' />}
{!isPinned && <RiPushpinLine className='shrink-0 w-4 h-4 text-text-tertiary' />}
<span className='grow'>{isPinned ? t('explore.sidebar.action.unpin') : t('explore.sidebar.action.pin')}</span>
</div>
{isShowRenameConversation && (
<div className={cn('flex items-center space-x-1 px-2 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover')} onClick={onRenameConversation}>
<RiEditLine className='shrink-0 w-4 h-4 text-text-tertiary' />
<span className='grow'>{t('explore.sidebar.action.rename')}</span>
</div>
)}
{isShowDelete && (
<div className={cn('group flex items-center space-x-1 px-2 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-destructive-hover hover:text-text-destructive')} onClick={onDelete} >
<RiDeleteBinLine className={cn('shrink-0 w-4 h-4 text-text-tertiary group-hover:text-text-destructive')} />
<span className='grow'>{t('explore.sidebar.action.delete')}</span>
</div>
)}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default React.memo(Operation)

View File

@ -4,6 +4,7 @@ import React, { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal' import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
export type IRenameModalProps = { export type IRenameModalProps = {
isShow: boolean isShow: boolean
@ -29,16 +30,16 @@ const RenameModal: FC<IRenameModalProps> = ({
isShow={isShow} isShow={isShow}
onClose={onClose} onClose={onClose}
> >
<div className={'mt-6 font-medium text-sm leading-[21px] text-gray-900'}>{t('common.chat.conversationName')}</div> <div className={'mt-6 font-medium text-sm leading-[21px] text-text-primary'}>{t('common.chat.conversationName')}</div>
<input className={'mt-2 w-full rounded-lg h-10 box-border px-3 text-sm leading-10 bg-gray-100'} <Input className='mt-2 w-full h-10'
value={tempName} value={tempName}
onChange={e => setTempName(e.target.value)} onChange={e => setTempName(e.target.value)}
placeholder={t('common.chat.conversationNamePlaceholder') || ''} placeholder={t('common.chat.conversationNamePlaceholder') || ''}
/> />
<div className='mt-10 flex justify-end'> <div className='mt-10 flex justify-end'>
<Button className='mr-2 flex-shrink-0' onClick={onClose}>{t('common.operation.cancel')}</Button> <Button className='mr-2 shrink-0' onClick={onClose}>{t('common.operation.cancel')}</Button>
<Button variant='primary' className='flex-shrink-0' onClick={() => onSave(tempName)} loading={saveLoading}>{t('common.operation.save')}</Button> <Button variant='primary' className='shrink-0' onClick={() => onSave(tempName)} loading={saveLoading}>{t('common.operation.save')}</Button>
</div> </div>
</Modal> </Modal>
) )

View File

@ -105,7 +105,7 @@ const Answer: FC<AnswerProps> = ({
<div className='shrink-0 relative w-10 h-10'> <div className='shrink-0 relative w-10 h-10'>
{answerIcon || <AnswerIcon />} {answerIcon || <AnswerIcon />}
{responding && ( {responding && (
<div className='absolute -top-[3px] -left-[3px] pl-[6px] flex items-center w-4 h-4 bg-white rounded-full shadow-xs border-[0.5px] border-gray-50'> <div className='absolute -top-[3px] -left-[3px] pl-[6px] flex items-center w-4 h-4 bg-background-section-burn rounded-full shadow-xs border-[0.5px] border-divider-subtle'>
<LoadingAnim type='avatar' /> <LoadingAnim type='avatar' />
</div> </div>
)} )}

View File

@ -13,7 +13,7 @@ const More: FC<MoreProps> = ({
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<div className='flex items-center mt-1 h-[18px] text-xs text-gray-400 opacity-0 group-hover:opacity-100'> <div className='mt-1 flex items-center system-xs-regular text-text-quaternary opacity-0 group-hover:opacity-100'>
{ {
more && ( more && (
<> <>

View File

@ -5,23 +5,24 @@ import {
useState, useState,
} from 'react' } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import {
RiClipboardLine,
RiEditLine,
RiReplay15Line,
RiThumbDownLine,
RiThumbUpLine,
} from '@remixicon/react'
import type { ChatItem } from '../../types' import type { ChatItem } from '../../types'
import { useChatContext } from '../context' import { useChatContext } from '../context'
import RegenerateBtn from '@/app/components/base/regenerate-btn' import copy from 'copy-to-clipboard'
import cn from '@/utils/classnames' import Toast from '@/app/components/base/toast'
import CopyBtn from '@/app/components/base/copy-btn'
import { MessageFast } from '@/app/components/base/icons/src/vender/solid/communication'
import AudioBtn from '@/app/components/base/audio-btn'
import AnnotationCtrlBtn from '@/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-btn'
import EditReplyModal from '@/app/components/app/annotation/edit-annotation-modal' import EditReplyModal from '@/app/components/app/annotation/edit-annotation-modal'
import {
ThumbsDown,
ThumbsUp,
} from '@/app/components/base/icons/src/vender/line/alertsAndFeedback'
import Tooltip from '@/app/components/base/tooltip'
import Log from '@/app/components/base/chat/chat/log' import Log from '@/app/components/base/chat/chat/log'
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
import NewAudioButton from '@/app/components/base/new-audio-button'
import cn from '@/utils/classnames'
interface OperationProps { type OperationProps = {
item: ChatItem item: ChatItem
question: string question: string
index: number index: number
@ -60,7 +61,6 @@ const Operation: FC<OperationProps> = ({
adminFeedback, adminFeedback,
agent_thoughts, agent_thoughts,
} = item } = item
const hasAnnotation = !!annotation?.id
const [localFeedback, setLocalFeedback] = useState(config?.supportAnnotation ? adminFeedback : feedback) const [localFeedback, setLocalFeedback] = useState(config?.supportAnnotation ? adminFeedback : feedback)
const content = useMemo(() => { const content = useMemo(() => {
@ -102,121 +102,68 @@ const Operation: FC<OperationProps> = ({
<div <div
className={cn( className={cn(
'absolute flex justify-end gap-1', 'absolute flex justify-end gap-1',
hasWorkflowProcess && '-top-3.5 -right-3.5', hasWorkflowProcess && '-bottom-4 right-2',
!positionRight && '-top-3.5 -right-3.5', !positionRight && '-bottom-4 right-2',
!hasWorkflowProcess && positionRight && '!top-[9px]', !hasWorkflowProcess && positionRight && '!top-[9px]',
)} )}
style={(!hasWorkflowProcess && positionRight) ? { left: contentWidth + 8 } : {}} style={(!hasWorkflowProcess && positionRight) ? { left: contentWidth + 8 } : {}}
> >
{!isOpeningStatement && ( {showPromptLog && (
<CopyBtn <div className='hidden group-hover:block'>
value={content} <Log logItem={item} />
className='hidden group-hover:block' </div>
/>
)} )}
{!isOpeningStatement && (
{!isOpeningStatement && (showPromptLog || config?.text_to_speech?.enabled) && ( <div className='hidden group-hover:flex ml-1 items-center gap-0.5 p-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg shadow-md backdrop-blur-sm'>
<div className='hidden group-hover:flex items-center w-max h-[28px] p-0.5 rounded-lg bg-white border-[0.5px] border-gray-100 shadow-md shrink-0'>
{showPromptLog && (
<>
<Log logItem={item} />
<div className='mx-1 w-[1px] h-[14px] bg-gray-200' />
</>
)}
{(config?.text_to_speech?.enabled) && ( {(config?.text_to_speech?.enabled) && (
<> <NewAudioButton
<AudioBtn id={id}
id={id} value={content}
value={content} voice={config?.text_to_speech?.voice}
noCache={false} />
voice={config?.text_to_speech?.voice} )}
className='hidden group-hover:block' <ActionButton onClick={() => {
/> copy(content)
</> Toast.notify({ type: 'success', message: t('common.actionMsg.copySuccessfully') })
}}>
<RiClipboardLine className='w-4 h-4' />
</ActionButton>
{!noChatInput && (
<ActionButton onClick={() => onRegenerate?.(item)}>
<RiReplay15Line className='w-4 h-4' />
</ActionButton>
)}
{(config?.supportAnnotation && config.annotation_reply?.enabled) && (
<ActionButton onClick={() => setIsShowReplyModal(true)}>
<RiEditLine className='w-4 h-4' />
</ActionButton>
)} )}
</div> </div>
)} )}
{!isOpeningStatement && config?.supportFeedback && onFeedback && (
{(!isOpeningStatement && config?.supportAnnotation && config.annotation_reply?.enabled) && ( <div className='hidden group-hover:flex ml-1 items-center gap-0.5 p-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg shadow-md backdrop-blur-sm'>
<AnnotationCtrlBtn {!localFeedback?.rating && (
appId={config?.appId || ''} <>
messageId={id} <ActionButton onClick={() => handleFeedback('like')}>
annotationId={annotation?.id || ''} <RiThumbUpLine className='w-4 h-4' />
className='hidden group-hover:block ml-1 shrink-0' </ActionButton>
cached={hasAnnotation} <ActionButton onClick={() => handleFeedback('dislike')}>
query={question} <RiThumbDownLine className='w-4 h-4' />
answer={content} </ActionButton>
onAdded={(id, authorName) => onAnnotationAdded?.(id, authorName, question, content, index)} </>
onEdit={() => setIsShowReplyModal(true)} )}
onRemoved={() => onAnnotationRemoved?.(index)} {localFeedback?.rating === 'like' && (
/> <ActionButton state={ActionButtonState.Active} onClick={() => handleFeedback(null)}>
<RiThumbUpLine className='w-4 h-4' />
</ActionButton>
)}
{localFeedback?.rating === 'dislike' && (
<ActionButton state={ActionButtonState.Destructive} onClick={() => handleFeedback(null)}>
<RiThumbDownLine className='w-4 h-4' />
</ActionButton>
)}
</div>
)} )}
{
annotation?.id && (
<div
className='relative box-border flex items-center justify-center h-7 w-7 p-0.5 rounded-lg bg-white cursor-pointer text-[#444CE7] shadow-md group-hover:hidden'
>
<div className='p-1 rounded-lg bg-[#EEF4FF] '>
<MessageFast className='w-4 h-4' />
</div>
</div>
)
}
{
!isOpeningStatement && !noChatInput && <RegenerateBtn className='hidden group-hover:block mr-1' onClick={() => onRegenerate?.(item)} />
}
{
config?.supportFeedback && !localFeedback?.rating && onFeedback && !isOpeningStatement && (
<div className='hidden group-hover:flex shrink-0 items-center px-0.5 bg-white border-[0.5px] border-gray-100 shadow-md text-gray-500 rounded-lg'>
<Tooltip popupContent={t('appDebug.operation.agree')}>
<div
className='flex items-center justify-center mr-0.5 w-6 h-6 rounded-md hover:bg-black/5 hover:text-gray-800 cursor-pointer'
onClick={() => handleFeedback('like')}
>
<ThumbsUp className='w-4 h-4' />
</div>
</Tooltip>
<Tooltip
popupContent={t('appDebug.operation.disagree')}
>
<div
className='flex items-center justify-center w-6 h-6 rounded-md hover:bg-black/5 hover:text-gray-800 cursor-pointer'
onClick={() => handleFeedback('dislike')}
>
<ThumbsDown className='w-4 h-4' />
</div>
</Tooltip>
</div>
)
}
{
config?.supportFeedback && localFeedback?.rating && onFeedback && !isOpeningStatement && (
<Tooltip
popupContent={localFeedback.rating === 'like' ? t('appDebug.operation.cancelAgree') : t('appDebug.operation.cancelDisagree')}
>
<div
className={`
flex items-center justify-center w-7 h-7 rounded-[10px] border-[2px] border-white cursor-pointer
${localFeedback.rating === 'like' && 'bg-blue-50 text-blue-600'}
${localFeedback.rating === 'dislike' && 'bg-red-100 text-red-600'}
`}
onClick={() => handleFeedback(null)}
>
{
localFeedback.rating === 'like' && (
<ThumbsUp className='w-4 h-4' />
)
}
{
localFeedback.rating === 'dislike' && (
<ThumbsDown className='w-4 h-4' />
)
}
</div>
</Tooltip>
)
}
</div> </div>
<EditReplyModal <EditReplyModal
isShow={isShowReplyModal} isShow={isShowReplyModal}

View File

@ -2,6 +2,7 @@ import type { FC } from 'react'
import { memo } from 'react' import { memo } from 'react'
import type { ChatItem } from '../../types' import type { ChatItem } from '../../types'
import { useChatContext } from '../context' import { useChatContext } from '../context'
import Button from '@/app/components/base/button'
type SuggestedQuestionsProps = { type SuggestedQuestionsProps = {
item: ChatItem item: ChatItem
@ -21,13 +22,14 @@ const SuggestedQuestions: FC<SuggestedQuestionsProps> = ({
return ( return (
<div className='flex flex-wrap'> <div className='flex flex-wrap'>
{suggestedQuestions.filter(q => !!q && q.trim()).map((question, index) => ( {suggestedQuestions.filter(q => !!q && q.trim()).map((question, index) => (
<div <Button
key={index} key={index}
className='mt-1 mr-1 max-w-full last:mr-0 shrink-0 py-[5px] leading-[18px] items-center px-4 rounded-lg border border-gray-200 shadow-xs bg-white text-xs font-medium text-primary-600 cursor-pointer' variant='secondary-accent'
className='mt-1 mr-1 max-w-full last:mr-0 shrink-0'
onClick={() => onSend?.(question)} onClick={() => onSend?.(question)}
> >
{question} {question}
</div>), </Button>),
)} )}
</div> </div>
) )

View File

@ -1,6 +1,5 @@
import { import {
useEffect, useEffect,
useMemo,
useState, useState,
} from 'react' } from 'react'
import { import {
@ -36,19 +35,6 @@ const WorkflowProcessItem = ({
const succeeded = data.status === WorkflowRunningStatus.Succeeded const succeeded = data.status === WorkflowRunningStatus.Succeeded
const failed = data.status === WorkflowRunningStatus.Failed || data.status === WorkflowRunningStatus.Stopped const failed = data.status === WorkflowRunningStatus.Failed || data.status === WorkflowRunningStatus.Stopped
const background = useMemo(() => {
if (collapse)
return 'linear-gradient(90deg, rgba(200, 206, 218, 0.20) 0%, rgba(200, 206, 218, 0.04) 100%)'
if (running && !collapse)
return 'linear-gradient(180deg, #E1E4EA 0%, #EAECF0 100%)'
if (succeeded && !collapse)
return 'linear-gradient(180deg, #ECFDF3 0%, #F6FEF9 100%)'
if (failed && !collapse)
return 'linear-gradient(180deg, #FEE4E2 0%, #FEF3F2 100%)'
}, [running, succeeded, failed, collapse])
useEffect(() => { useEffect(() => {
setCollapse(!expand) setCollapse(!expand)
}, [expand]) }, [expand])
@ -56,12 +42,13 @@ const WorkflowProcessItem = ({
return ( return (
<div <div
className={cn( className={cn(
'-mx-1 px-2.5 rounded-xl border-[0.5px]', '-mx-1 px-2.5 rounded-xl',
collapse ? 'py-[7px] border-components-panel-border' : 'pt-[7px] px-1 pb-1 border-components-panel-border-subtle', collapse ? 'py-[7px] border-l-[0.25px] border-components-panel-border' : 'pt-[7px] px-1 pb-1 border-[0.5px] border-components-panel-border-subtle',
running && !collapse && 'bg-background-section-burn',
succeeded && !collapse && 'bg-state-success-hover',
failed && !collapse && 'bg-state-destructive-hover',
collapse && 'bg-workflow-process-bg',
)} )}
style={{
background,
}}
> >
<div <div
className={cn('flex items-center cursor-pointer', !collapse && 'px-1.5', readonly && 'cursor-default')} className={cn('flex items-center cursor-pointer', !collapse && 'px-1.5', readonly && 'cursor-default')}
@ -85,7 +72,7 @@ const WorkflowProcessItem = ({
<div className={cn('system-xs-medium text-text-secondary', !collapse && 'grow')}> <div className={cn('system-xs-medium text-text-secondary', !collapse && 'grow')}>
{t('workflow.common.workflowProcess')} {t('workflow.common.workflowProcess')}
</div> </div>
{!readonly && <RiArrowRightSLine className={`'ml-1 w-4 h-4 text-text-tertiary' ${collapse ? '' : 'rotate-90'}`} />} {!readonly && <RiArrowRightSLine className={cn('ml-1 w-4 h-4 text-text-tertiary', !collapse && 'rotate-90')} />}
</div> </div>
{ {
!collapse && !readonly && ( !collapse && !readonly && (

View File

@ -40,6 +40,7 @@ type ChatInputAreaProps = {
inputsForm?: InputForm[] inputsForm?: InputForm[]
theme?: Theme | null theme?: Theme | null
isResponding?: boolean isResponding?: boolean
disabled?: boolean
} }
const ChatInputArea = ({ const ChatInputArea = ({
showFeatureBar, showFeatureBar,
@ -53,6 +54,7 @@ const ChatInputArea = ({
inputsForm = [], inputsForm = [],
theme, theme,
isResponding, isResponding,
disabled,
}: ChatInputAreaProps) => { }: ChatInputAreaProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const { notify } = useToastContext() const { notify } = useToastContext()
@ -155,6 +157,7 @@ const ChatInputArea = ({
className={cn( className={cn(
'relative pb-[9px] bg-components-panel-bg-blur border border-components-chat-input-border rounded-xl shadow-md z-10', 'relative pb-[9px] bg-components-panel-bg-blur border border-components-chat-input-border rounded-xl shadow-md z-10',
isDragActive && 'border border-dashed border-components-option-card-option-selected-border', isDragActive && 'border border-dashed border-components-option-card-option-selected-border',
disabled && 'opacity-50 pointer-events-none border-components-panel-border shadow-none',
)} )}
> >
<div className='relative px-[9px] pt-[9px] max-h-[158px] overflow-x-hidden overflow-y-auto'> <div className='relative px-[9px] pt-[9px] max-h-[158px] overflow-x-hidden overflow-y-auto'>

View File

@ -77,9 +77,9 @@ const Citation: FC<CitationProps> = ({
return ( return (
<div className='mt-3 -mb-1'> <div className='mt-3 -mb-1'>
<div className='flex items-center mb-2 text-xs font-medium text-gray-500'> <div className='flex items-center mb-2 system-xs-medium text-text-tertiary'>
{t('common.chat.citation.title')} {t('common.chat.citation.title')}
<div className='grow ml-2 h-[1px] bg-black/5' /> <div className='grow ml-2 h-[1px] bg-divider-regular' />
</div> </div>
<div className='relative flex flex-wrap'> <div className='relative flex flex-wrap'>
{ {
@ -87,7 +87,7 @@ const Citation: FC<CitationProps> = ({
<div <div
key={index} key={index}
className='absolute top-0 left-0 w-auto mr-1 mb-1 pl-7 pr-2 max-w-[240px] h-7 text-xs whitespace-nowrap opacity-0 -z-10' className='absolute top-0 left-0 w-auto mr-1 mb-1 pl-7 pr-2 max-w-[240px] h-7 text-xs whitespace-nowrap opacity-0 -z-10'
ref={ele => (elesRef.current[index] = ele!)} ref={(ele: any) => (elesRef.current[index] = ele!)}
> >
{res.documentName} {res.documentName}
</div> </div>
@ -106,13 +106,13 @@ const Citation: FC<CitationProps> = ({
{ {
limitNumberInOneLine < resourcesLength && ( limitNumberInOneLine < resourcesLength && (
<div <div
className='flex items-center px-2 h-7 bg-white rounded-lg text-xs font-medium text-gray-500 cursor-pointer' className='flex items-center px-2 h-7 bg-components-panel-bg rounded-lg text-text-tertiary system-xs-medium cursor-pointer'
onClick={() => setShowMore(v => !v)} onClick={() => setShowMore(v => !v)}
> >
{ {
!showMore !showMore
? `+ ${resourcesLength - limitNumberInOneLine}` ? `+ ${resourcesLength - limitNumberInOneLine}`
: <RiArrowDownSLine className='w-4 h-4 text-gray-600 rotate-180' /> : <RiArrowDownSLine className='w-4 h-4 text-text-tertiary rotate-180' />
} }
</div> </div>
) )

View File

@ -47,29 +47,29 @@ const Popup: FC<PopupProps> = ({
}} }}
> >
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}> <PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}>
<div className='flex items-center px-2 max-w-[240px] h-7 bg-white rounded-lg'> <div className='flex items-center px-2 max-w-[240px] h-7 bg-components-button-secondary-bg rounded-lg'>
<FileIcon type={fileType} className='shrink-0 mr-1 w-4 h-4' /> <FileIcon type={fileType} className='shrink-0 mr-1 w-4 h-4' />
<div className='text-xs text-gray-600 truncate'>{data.documentName}</div> <div className='text-xs text-text-tertiary truncate'>{data.documentName}</div>
</div> </div>
</PortalToFollowElemTrigger> </PortalToFollowElemTrigger>
<PortalToFollowElemContent style={{ zIndex: 1000 }}> <PortalToFollowElemContent style={{ zIndex: 1000 }}>
<div className='max-w-[360px] bg-gray-50 rounded-xl shadow-lg'> <div className='max-w-[360px] bg-background-section-burn rounded-xl shadow-lg'>
<div className='px-4 pt-3 pb-2'> <div className='px-4 pt-3 pb-2'>
<div className='flex items-center h-[18px]'> <div className='flex items-center h-[18px]'>
<FileIcon type={fileType} className='shrink-0 mr-1 w-4 h-4' /> <FileIcon type={fileType} className='shrink-0 mr-1 w-4 h-4' />
<div className='text-xs font-medium text-gray-600 truncate'>{data.documentName}</div> <div className='system-xs-medium text-text-tertiary truncate'>{data.documentName}</div>
</div> </div>
</div> </div>
<div className='px-4 py-0.5 max-h-[450px] bg-white rounded-lg overflow-y-auto'> <div className='px-4 py-0.5 max-h-[450px] bg-components-panel-bg rounded-lg overflow-y-auto'>
<div className='w-full'> <div className='w-full'>
{ {
data.sources.map((source, index) => ( data.sources.map((source, index) => (
<Fragment key={index}> <Fragment key={index}>
<div className='group py-3'> <div className='group py-3'>
<div className='flex items-center justify-between mb-2'> <div className='flex items-center justify-between mb-2'>
<div className='flex items-center px-1.5 h-5 border border-gray-200 rounded-md'> <div className='flex items-center px-1.5 h-5 border border-divider-subtle rounded-md'>
<Hash02 className='mr-0.5 w-3 h-3 text-gray-400' /> <Hash02 className='mr-0.5 w-3 h-3 text-text-quaternary' />
<div className='text-[11px] font-medium text-gray-500'> <div className='text-[11px] font-medium text-text-tertiary'>
{source.segment_position || index + 1} {source.segment_position || index + 1}
</div> </div>
</div> </div>
@ -77,17 +77,17 @@ const Popup: FC<PopupProps> = ({
showHitInfo && ( showHitInfo && (
<Link <Link
href={`/datasets/${source.dataset_id}/documents/${source.document_id}`} href={`/datasets/${source.dataset_id}/documents/${source.document_id}`}
className='hidden items-center h-[18px] text-xs text-primary-600 group-hover:flex'> className='hidden items-center h-[18px] text-xs text-text-accent group-hover:flex'>
{t('common.chat.citation.linkToDataset')} {t('common.chat.citation.linkToDataset')}
<ArrowUpRight className='ml-1 w-3 h-3' /> <ArrowUpRight className='ml-1 w-3 h-3' />
</Link> </Link>
) )
} }
</div> </div>
<div className='text-[13px] text-gray-800 break-words'>{source.content}</div> <div className='text-[13px] text-text-secondary break-words'>{source.content}</div>
{ {
showHitInfo && ( showHitInfo && (
<div className='flex items-center mt-2 text-xs font-medium text-gray-500 flex-wrap'> <div className='flex items-center mt-2 system-xs-medium text-text-quaternary flex-wrap'>
<Tooltip <Tooltip
text={t('common.chat.citation.characters')} text={t('common.chat.citation.characters')}
data={source.word_count} data={source.word_count}
@ -114,7 +114,7 @@ const Popup: FC<PopupProps> = ({
</div> </div>
{ {
index !== data.sources.length - 1 && ( index !== data.sources.length - 1 && (
<div className='my-1 h-[1px] bg-black/5' /> <div className='my-1 h-[1px] bg-divider-regular' />
) )
} }
</Fragment> </Fragment>

View File

@ -28,14 +28,14 @@ const ProgressTooltip: FC<ProgressTooltipProps> = ({
onMouseLeave={() => setOpen(false)} onMouseLeave={() => setOpen(false)}
> >
<div className='grow flex items-center'> <div className='grow flex items-center'>
<div className='mr-1 w-16 h-1.5 rounded-[3px] border border-gray-400 overflow-hidden'> <div className='mr-1 w-16 h-1.5 rounded-[3px] border border-components-progress-gray-border overflow-hidden'>
<div className='bg-gray-400 h-full' style={{ width: `${data * 100}%` }}></div> <div className='bg-components-progress-gray-progress h-full' style={{ width: `${data * 100}%` }}></div>
</div> </div>
{data} {data}
</div> </div>
</PortalToFollowElemTrigger> </PortalToFollowElemTrigger>
<PortalToFollowElemContent style={{ zIndex: 1001 }}> <PortalToFollowElemContent style={{ zIndex: 1001 }}>
<div className='p-3 bg-white text-xs font-medium text-gray-500 rounded-lg shadow-lg'> <div className='p-3 bg-components-tooltip-bg system-xs-medium text-text-quaternary rounded-lg shadow-lg'>
{t('common.chat.citation.hitScore')} {data} {t('common.chat.citation.hitScore')} {data}
</div> </div>
</PortalToFollowElemContent> </PortalToFollowElemContent>

View File

@ -35,7 +35,7 @@ const Tooltip: FC<TooltipProps> = ({
</div> </div>
</PortalToFollowElemTrigger> </PortalToFollowElemTrigger>
<PortalToFollowElemContent style={{ zIndex: 1001 }}> <PortalToFollowElemContent style={{ zIndex: 1001 }}>
<div className='p-3 bg-white text-xs font-medium text-gray-500 rounded-lg shadow-lg'> <div className='p-3 bg-components-tooltip-bg system-xs-medium text-text-quaternary rounded-lg shadow-lg'>
{text} {data} {text} {data}
</div> </div>
</PortalToFollowElemContent> </PortalToFollowElemContent>

View File

@ -397,6 +397,7 @@ export const useChat = (
) )
setSuggestQuestions(data) setSuggestQuestions(data)
} }
// eslint-disable-next-line unused-imports/no-unused-vars
catch (e) { catch (e) {
setSuggestQuestions([]) setSuggestQuestions([])
} }
@ -555,7 +556,7 @@ export const useChat = (
if (!item.execution_metadata?.parallel_id) if (!item.execution_metadata?.parallel_id)
return item.node_id === nodeFinishedData.node_id return item.node_id === nodeFinishedData.node_id
return item.node_id === nodeFinishedData.node_id && (item.execution_metadata?.parallel_id === nodeFinishedData.execution_metadata.parallel_id) return item.node_id === nodeFinishedData.node_id && (item.execution_metadata?.parallel_id === nodeFinishedData.execution_metadata?.parallel_id)
}) })
responseItem.workflowProcess!.tracing[currentIndex] = nodeFinishedData as any responseItem.workflowProcess!.tracing[currentIndex] = nodeFinishedData as any

View File

@ -70,6 +70,8 @@ export type ChatProps = {
showFileUpload?: boolean showFileUpload?: boolean
onFeatureBarClick?: (state: boolean) => void onFeatureBarClick?: (state: boolean) => void
noSpacing?: boolean noSpacing?: boolean
inputDisabled?: boolean
isMobile?: boolean
} }
const Chat: FC<ChatProps> = ({ const Chat: FC<ChatProps> = ({
@ -106,6 +108,8 @@ const Chat: FC<ChatProps> = ({
showFileUpload, showFileUpload,
onFeatureBarClick, onFeatureBarClick,
noSpacing, noSpacing,
inputDisabled,
isMobile,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const { currentLogItem, setCurrentLogItem, showPromptLogModal, setShowPromptLogModal, showAgentLogModal, setShowAgentLogModal } = useAppStore(useShallow(state => ({ const { currentLogItem, setCurrentLogItem, showPromptLogModal, setShowPromptLogModal, showAgentLogModal, setShowAgentLogModal } = useAppStore(useShallow(state => ({
@ -273,12 +277,14 @@ const Chat: FC<ChatProps> = ({
<TryToAsk <TryToAsk
suggestedQuestions={suggestedQuestions} suggestedQuestions={suggestedQuestions}
onSend={onSend} onSend={onSend}
isMobile={isMobile}
/> />
) )
} }
{ {
!noChatInput && ( !noChatInput && (
<ChatInputArea <ChatInputArea
disabled={inputDisabled}
showFeatureBar={showFeatureBar} showFeatureBar={showFeatureBar}
showFileUpload={showFileUpload} showFileUpload={showFileUpload}
featureBarDisabled={isResponding} featureBarDisabled={isResponding}

View File

@ -1,8 +1,8 @@
import type { FC } from 'react' import type { FC } from 'react'
import { useTranslation } from 'react-i18next' import { RiFileList3Line } from '@remixicon/react'
import { File02 } from '@/app/components/base/icons/src/vender/line/files'
import type { IChatItem } from '@/app/components/base/chat/chat/type' import type { IChatItem } from '@/app/components/base/chat/chat/type'
import { useStore as useAppStore } from '@/app/components/app/store' import { useStore as useAppStore } from '@/app/components/app/store'
import ActionButton from '@/app/components/base/action-button'
type LogProps = { type LogProps = {
logItem: IChatItem logItem: IChatItem
@ -10,7 +10,6 @@ type LogProps = {
const Log: FC<LogProps> = ({ const Log: FC<LogProps> = ({
logItem, logItem,
}) => { }) => {
const { t } = useTranslation()
const setCurrentLogItem = useAppStore(s => s.setCurrentLogItem) const setCurrentLogItem = useAppStore(s => s.setCurrentLogItem)
const setShowPromptLogModal = useAppStore(s => s.setShowPromptLogModal) const setShowPromptLogModal = useAppStore(s => s.setShowPromptLogModal)
const setShowAgentLogModal = useAppStore(s => s.setShowAgentLogModal) const setShowAgentLogModal = useAppStore(s => s.setShowAgentLogModal)
@ -20,7 +19,7 @@ const Log: FC<LogProps> = ({
return ( return (
<div <div
className='shrink-0 p-1 flex items-center justify-center rounded-[6px] font-medium text-gray-500 hover:bg-gray-50 cursor-pointer hover:text-gray-700' className='ml-1 flex items-center gap-0.5 p-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg shadow-md backdrop-blur-sm'
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
e.nativeEvent.stopImmediatePropagation() e.nativeEvent.stopImmediatePropagation()
@ -33,8 +32,9 @@ const Log: FC<LogProps> = ({
setShowPromptLogModal(true) setShowPromptLogModal(true)
}} }}
> >
<File02 className='mr-1 w-4 h-4' /> <ActionButton>
<div className='text-xs leading-4'>{runID ? t('appLog.viewLog') : isAgent ? t('appLog.agentLog') : t('appLog.promptLog')}</div> <RiFileList3Line className='w-4 h-4' />
</ActionButton>
</div> </div>
) )
} }

View File

@ -2,46 +2,37 @@ import type { FC } from 'react'
import { memo } from 'react' import { memo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import type { OnSend } from '../types' import type { OnSend } from '../types'
import { Star04 } from '@/app/components/base/icons/src/vender/solid/shapes'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import Divider from '@/app/components/base/divider'
import cn from '@/utils/classnames'
type TryToAskProps = { type TryToAskProps = {
suggestedQuestions: string[] suggestedQuestions: string[]
onSend: OnSend onSend: OnSend
isMobile?: boolean
} }
const TryToAsk: FC<TryToAskProps> = ({ const TryToAsk: FC<TryToAskProps> = ({
suggestedQuestions, suggestedQuestions,
onSend, onSend,
isMobile,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<div> <div className='mb-2 py-2'>
<div className='flex items-center mb-2.5 py-2'> <div className={cn('flex items-center justify-between gap-2 mb-2.5', isMobile && 'justify-end')}>
<div <Divider bgStyle='gradient' className='grow h-px rotate-180' />
className='grow h-[1px]' <div className='shrink-0 text-text-tertiary system-xs-medium-uppercase'>{t('appDebug.feature.suggestedQuestionsAfterAnswer.tryToAsk')}</div>
style={{ {!isMobile && <Divider bgStyle='gradient' className='grow h-px' />}
background: 'linear-gradient(270deg, #F3F4F6 0%, rgba(243, 244, 246, 0) 100%)',
}}
/>
<div className='shrink-0 flex items-center px-3 text-gray-500'>
<Star04 className='mr-1 w-2.5 h-2.5' />
<span className='text-xs text-gray-500 font-medium'>{t('appDebug.feature.suggestedQuestionsAfterAnswer.tryToAsk')}</span>
</div>
<div
className='grow h-[1px]'
style={{
background: 'linear-gradient(270deg, rgba(243, 244, 246, 0) 0%, #F3F4F6 100%)',
}}
/>
</div> </div>
<div className='flex flex-wrap justify-center'> <div className={cn('flex flex-wrap justify-center', isMobile && 'justify-end')}>
{ {
suggestedQuestions.map((suggestQuestion, index) => ( suggestedQuestions.map((suggestQuestion, index) => (
<Button <Button
size='small'
key={index} key={index}
variant='secondary-accent' variant='secondary-accent'
className='mb-2 mr-2 last:mr-0' className='mb-1 mr-1 last:mr-0'
onClick={() => onSend(suggestQuestion)} onClick={() => onSend(suggestQuestion)}
> >
{suggestQuestion} {suggestQuestion}

View File

@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react'
import Chat from '../chat' import Chat from '../chat'
import type { import type {
ChatConfig, ChatConfig,
@ -9,16 +9,19 @@ import type {
import { useChat } from '../chat/hooks' import { useChat } from '../chat/hooks'
import { getLastAnswer, isValidGeneratedAnswer } from '../utils' import { getLastAnswer, isValidGeneratedAnswer } from '../utils'
import { useEmbeddedChatbotContext } from './context' import { useEmbeddedChatbotContext } from './context'
import ConfigPanel from './config-panel'
import { isDify } from './utils' import { isDify } from './utils'
import cn from '@/utils/classnames' import { InputVarType } from '@/app/components/workflow/types'
import { TransferMethod } from '@/types/app'
import InputsForm from '@/app/components/base/chat/embedded-chatbot/inputs-form'
import { import {
fetchSuggestedQuestions, fetchSuggestedQuestions,
getUrl, getUrl,
stopChatMessageResponding, stopChatMessageResponding,
} from '@/service/share' } from '@/service/share'
import AppIcon from '@/app/components/base/app-icon'
import LogoAvatar from '@/app/components/base/logo/logo-embedded-chat-avatar' import LogoAvatar from '@/app/components/base/logo/logo-embedded-chat-avatar'
import AnswerIcon from '@/app/components/base/answer-icon' import AnswerIcon from '@/app/components/base/answer-icon'
import cn from '@/utils/classnames'
const ChatWrapper = () => { const ChatWrapper = () => {
const { const {
@ -29,6 +32,7 @@ const ChatWrapper = () => {
currentConversationItem, currentConversationItem,
inputsForms, inputsForms,
newConversationInputs, newConversationInputs,
newConversationInputsRef,
handleNewConversationCompleted, handleNewConversationCompleted,
isMobile, isMobile,
isInstalledApp, isInstalledApp,
@ -67,6 +71,38 @@ const ChatWrapper = () => {
appPrevChatList, appPrevChatList,
taskId => stopChatMessageResponding('', taskId, isInstalledApp, appId), taskId => stopChatMessageResponding('', taskId, isInstalledApp, appId),
) )
const inputsFormValue = currentConversationId ? currentConversationItem?.inputs : newConversationInputsRef?.current
const inputDisabled = useMemo(() => {
let hasEmptyInput = ''
let fileIsUploading = false
const requiredVars = inputsForms.filter(({ required }) => required)
if (requiredVars.length) {
requiredVars.forEach(({ variable, label, type }) => {
if (hasEmptyInput)
return
if (fileIsUploading)
return
if (!inputsFormValue?.[variable])
hasEmptyInput = label as string
if ((type === InputVarType.singleFile || type === InputVarType.multiFiles) && inputsFormValue?.[variable]) {
const files = inputsFormValue[variable]
if (Array.isArray(files))
fileIsUploading = files.find(item => item.transferMethod === TransferMethod.local_file && !item.uploadedId)
else
fileIsUploading = files.transferMethod === TransferMethod.local_file && !files.uploadedId
}
})
}
if (hasEmptyInput)
return true
if (fileIsUploading)
return true
return false
}, [inputsFormValue, inputsForms])
useEffect(() => { useEffect(() => {
if (currentChatInstanceRef.current) if (currentChatInstanceRef.current)
@ -108,26 +144,48 @@ const ChatWrapper = () => {
doSend(question.content, question.message_files, true, isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null) doSend(question.content, question.message_files, true, isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null)
}, [chatList, doSend]) }, [chatList, doSend])
const chatNode = useMemo(() => { const messageList = useMemo(() => {
if (inputsForms.length) { if (currentConversationId)
return ( return chatList
<> return chatList.filter(item => !item.isOpeningStatement)
{!currentConversationId && ( }, [chatList, currentConversationId])
<div className={cn('mx-auto w-full max-w-full tablet:px-4', isMobile && 'px-4')}>
<div className='mb-6' />
<ConfigPanel />
<div
className='my-6 h-[1px]'
style={{ background: 'linear-gradient(90deg, rgba(242, 244, 247, 0.00) 0%, #F2F4F7 49.17%, rgba(242, 244, 247, 0.00) 100%)' }}
/>
</div>
)}
</>
)
}
return null const [collapsed, setCollapsed] = useState(!!currentConversationId)
}, [currentConversationId, inputsForms, isMobile])
const chatNode = useMemo(() => {
if (!inputsForms.length)
return null
if (isMobile) {
if (!currentConversationId)
return <InputsForm collapsed={collapsed} setCollapsed={setCollapsed} />
return <div className='mb-4'></div>
}
else {
return <InputsForm collapsed={collapsed} setCollapsed={setCollapsed} />
}
}, [inputsForms.length, isMobile, currentConversationId, collapsed])
const welcome = useMemo(() => {
const welcomeMessage = chatList.find(item => item.isOpeningStatement)
if (currentConversationId)
return null
if (!welcomeMessage)
return null
if (!collapsed && inputsForms.length > 0)
return null
return (
<div className={cn('h-[50vh] py-12 flex flex-col items-center justify-center gap-3')}>
<AppIcon
size='xl'
iconType={appData?.site.icon_type}
icon={appData?.site.icon}
background={appData?.site.icon_background}
imageUrl={appData?.site.icon_url}
/>
<div className='text-text-tertiary body-2xl-regular'>{welcomeMessage.content}</div>
</div>
)
}, [appData?.site.icon, appData?.site.icon_background, appData?.site.icon_type, appData?.site.icon_url, chatList, collapsed, currentConversationId, inputsForms.length])
const answerIcon = isDify() const answerIcon = isDify()
? <LogoAvatar className='relative shrink-0' /> ? <LogoAvatar className='relative shrink-0' />
@ -144,17 +202,22 @@ const ChatWrapper = () => {
<Chat <Chat
appData={appData} appData={appData}
config={appConfig} config={appConfig}
chatList={chatList} chatList={messageList}
isResponding={isResponding} isResponding={isResponding}
chatContainerInnerClassName={cn('mx-auto w-full max-w-full tablet:px-4', isMobile && 'px-4')} chatContainerInnerClassName={cn('mx-auto w-full max-w-full tablet:px-4', isMobile && 'px-4')}
chatFooterClassName='pb-4' chatFooterClassName={cn('pb-4', !isMobile && 'rounded-b-2xl')}
chatFooterInnerClassName={cn('mx-auto w-full max-w-full tablet:px-4', isMobile && 'px-4')} chatFooterInnerClassName={cn('mx-auto w-full max-w-full tablet:px-4', isMobile && 'px-2')}
onSend={doSend} onSend={doSend}
inputs={currentConversationId ? currentConversationItem?.inputs as any : newConversationInputs} inputs={currentConversationId ? currentConversationItem?.inputs as any : newConversationInputs}
inputsForm={inputsForms} inputsForm={inputsForms}
onRegenerate={doRegenerate} onRegenerate={doRegenerate}
onStopResponding={handleStop} onStopResponding={handleStop}
chatNode={chatNode} chatNode={
<>
{chatNode}
{welcome}
</>
}
allToolIcons={appMeta?.tool_icons || {}} allToolIcons={appMeta?.tool_icons || {}}
onFeedback={handleFeedback} onFeedback={handleFeedback}
suggestedQuestions={suggestedQuestions} suggestedQuestions={suggestedQuestions}
@ -162,6 +225,8 @@ const ChatWrapper = () => {
hideProcessDetail hideProcessDetail
themeBuilder={themeBuilder} themeBuilder={themeBuilder}
switchSibling={siblingMessageId => setTargetMessageId(siblingMessageId)} switchSibling={siblingMessageId => setTargetMessageId(siblingMessageId)}
inputDisabled={inputDisabled}
isMobile={isMobile}
/> />
) )
} }

View File

@ -1,47 +0,0 @@
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { memo } from 'react'
import Textarea from '@/app/components/base/textarea'
interface InputProps {
form: any
value: string
onChange: (variable: string, value: string) => void
}
const FormInput: FC<InputProps> = ({
form,
value,
onChange,
}) => {
const { t } = useTranslation()
const {
type,
label,
required,
max_length,
variable,
} = form
if (type === 'paragraph') {
return (
<Textarea
value={value}
className='resize-none'
onChange={e => onChange(variable, e.target.value)}
placeholder={`${label}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
/>
)
}
return (
<input
className='grow h-9 rounded-lg bg-gray-100 px-2.5 outline-none appearance-none'
value={value || ''}
maxLength={max_length}
onChange={e => onChange(variable, e.target.value)}
placeholder={`${label}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
/>
)
}
export default memo(FormInput)

View File

@ -1,129 +0,0 @@
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useEmbeddedChatbotContext } from '../context'
import Input from './form-input'
import { PortalSelect } from '@/app/components/base/select'
import { InputVarType } from '@/app/components/workflow/types'
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
const Form = () => {
const { t } = useTranslation()
const {
appParams,
inputsForms,
newConversationInputs,
newConversationInputsRef,
handleNewConversationInputsChange,
isMobile,
} = useEmbeddedChatbotContext()
const handleFormChange = useCallback((variable: string, value: any) => {
handleNewConversationInputsChange({
...newConversationInputsRef.current,
[variable]: value,
})
}, [newConversationInputsRef, handleNewConversationInputsChange])
const renderField = (form: any) => {
const {
label,
required,
variable,
options,
} = form
if (form.type === 'text-input' || form.type === 'paragraph') {
return (
<Input
form={form}
value={newConversationInputs[variable]}
onChange={handleFormChange}
/>
)
}
if (form.type === 'number') {
return (
<input
className="grow h-9 rounded-lg bg-gray-100 px-2.5 outline-none appearance-none"
type="number"
value={newConversationInputs[variable] || ''}
onChange={e => handleFormChange(variable, e.target.value)}
placeholder={`${label}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
/>
)
}
if (form.type === 'number') {
return (
<input
className="grow h-9 rounded-lg bg-gray-100 px-2.5 outline-none appearance-none"
type="number"
value={newConversationInputs[variable] || ''}
onChange={e => handleFormChange(variable, e.target.value)}
placeholder={`${label}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
/>
)
}
if (form.type === InputVarType.singleFile) {
return (
<FileUploaderInAttachmentWrapper
value={newConversationInputs[variable] ? [newConversationInputs[variable]] : []}
onChange={files => handleFormChange(variable, files[0])}
fileConfig={{
allowed_file_types: form.allowed_file_types,
allowed_file_extensions: form.allowed_file_extensions,
allowed_file_upload_methods: form.allowed_file_upload_methods,
number_limits: 1,
fileUploadConfig: (appParams as any).system_parameters,
}}
/>
)
}
if (form.type === InputVarType.multiFiles) {
return (
<FileUploaderInAttachmentWrapper
value={newConversationInputs[variable]}
onChange={files => handleFormChange(variable, files)}
fileConfig={{
allowed_file_types: form.allowed_file_types,
allowed_file_extensions: form.allowed_file_extensions,
allowed_file_upload_methods: form.allowed_file_upload_methods,
number_limits: form.max_length,
fileUploadConfig: (appParams as any).system_parameters,
}}
/>
)
}
return (
<PortalSelect
popupClassName='w-[200px]'
value={newConversationInputs[variable]}
items={options.map((option: string) => ({ value: option, name: option }))}
onSelect={item => handleFormChange(variable, item.value as string)}
placeholder={`${label}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
/>
)
}
if (!inputsForms.length)
return null
return (
<div className='mb-4 py-2'>
{
inputsForms.map(form => (
<div
key={form.variable}
className={`flex mb-3 last-of-type:mb-0 text-sm text-gray-900 ${isMobile && '!flex-wrap'}`}
>
<div className={`shrink-0 mr-2 py-2 w-[128px] ${isMobile && '!w-full'}`}>{form.label}</div>
{renderField(form)}
</div>
))
}
</div>
)
}
export default Form

View File

@ -1,180 +0,0 @@
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useEmbeddedChatbotContext } from '../context'
import { useThemeContext } from '../theme/theme-context'
import { CssTransform } from '../theme/utils'
import Form from './form'
import cn from '@/utils/classnames'
import Button from '@/app/components/base/button'
import AppIcon from '@/app/components/base/app-icon'
import { MessageDotsCircle } from '@/app/components/base/icons/src/vender/solid/communication'
import { Edit02 } from '@/app/components/base/icons/src/vender/line/general'
import { Star06 } from '@/app/components/base/icons/src/vender/solid/shapes'
import LogoSite from '@/app/components/base/logo/logo-site'
const ConfigPanel = () => {
const { t } = useTranslation()
const {
appData,
inputsForms,
handleStartChat,
showConfigPanelBeforeChat,
isMobile,
} = useEmbeddedChatbotContext()
const [collapsed, setCollapsed] = useState(true)
const customConfig = appData?.custom_config
const site = appData?.site
const themeBuilder = useThemeContext()
return (
<div className='flex flex-col max-h-[80%] w-full max-w-[720px]'>
<div
className={cn(
'grow rounded-xl overflow-y-auto',
showConfigPanelBeforeChat && 'border-[0.5px] border-gray-100 shadow-lg',
!showConfigPanelBeforeChat && collapsed && 'border border-indigo-100',
!showConfigPanelBeforeChat && !collapsed && 'border-[0.5px] border-gray-100 shadow-lg',
)}
>
<div
style={CssTransform(themeBuilder.theme?.roundedBackgroundColorStyle ?? '')}
className={`
flex flex-wrap px-6 py-4 rounded-t-xl bg-indigo-25
${isMobile && '!px-4 !py-3'}
`}
>
{
showConfigPanelBeforeChat && (
<>
<div className='flex items-center h-8 text-2xl font-semibold text-gray-800'>
<AppIcon
iconType={appData?.site.icon_type}
icon={appData?.site.icon}
imageUrl={appData?.site.icon_url}
background='transparent'
size='small'
className="mr-2"
/>
{appData?.site.title}
</div>
{
appData?.site.description && (
<div className='mt-2 w-full text-sm text-gray-500'>
{appData?.site.description}
</div>
)
}
</>
)
}
{
!showConfigPanelBeforeChat && collapsed && (
<>
<Star06 className='mr-1 mt-1 w-4 h-4 text-indigo-600' />
<div className='grow py-[3px] text-[13px] text-indigo-600 leading-[18px] font-medium'>
{t('share.chat.configStatusDes')}
</div>
<Button
styleCss={CssTransform(themeBuilder.theme?.backgroundButtonDefaultColorStyle ?? '')}
variant='secondary-accent'
size='small'
className='shrink-0 text-white'
onClick={() => setCollapsed(false)}
>
<Edit02 className='mr-1 w-3 h-3' />
{t('common.operation.edit')}
</Button>
</>
)
}
{
!showConfigPanelBeforeChat && !collapsed && (
<>
<Star06 className='mr-1 mt-1 w-4 h-4 text-indigo-600' />
<div className='grow py-[3px] text-[13px] text-indigo-600 leading-[18px] font-medium'>
{t('share.chat.privatePromptConfigTitle')}
</div>
</>
)
}
</div>
{
!collapsed && !showConfigPanelBeforeChat && (
<div className='p-6 rounded-b-xl'>
<Form />
<div className={cn('pl-[136px] flex items-center', isMobile && '!pl-0')}>
<Button
styleCss={CssTransform(themeBuilder.theme?.backgroundButtonDefaultColorStyle ?? '')}
variant='primary'
className='mr-2'
onClick={() => {
setCollapsed(true)
handleStartChat()
}}
>
{t('common.operation.save')}
</Button>
<Button
onClick={() => setCollapsed(true)}
>
{t('common.operation.cancel')}
</Button>
</div>
</div>
)
}
{
showConfigPanelBeforeChat && (
<div className='p-6 rounded-b-xl'>
<Form />
<Button
styleCss={CssTransform(themeBuilder.theme?.backgroundButtonDefaultColorStyle ?? '')}
className={cn(inputsForms.length && !isMobile && 'ml-[136px]')}
variant='primary'
size='large'
onClick={handleStartChat}
>
<MessageDotsCircle className='mr-2 w-4 h-4 text-white' />
{t('share.chat.startChat')}
</Button>
</div>
)
}
</div>
{
showConfigPanelBeforeChat && (site || customConfig) && (
<div className='mt-4 flex flex-wrap justify-between items-center py-2 text-xs text-gray-400'>
{site?.privacy_policy
? <div className={cn(isMobile && 'mb-2 w-full text-center')}>{t('share.chat.privacyPolicyLeft')}
<a
className='text-gray-500 px-1'
href={site?.privacy_policy}
target='_blank' rel='noopener noreferrer'>{t('share.chat.privacyPolicyMiddle')}</a>
{t('share.chat.privacyPolicyRight')}
</div>
: <div>
</div>}
{
customConfig?.remove_webapp_brand
? null
: (
<div className={cn('flex items-center justify-end', isMobile && 'w-full')}>
<div className='flex items-center pr-3 space-x-3'>
<span className='uppercase'>{t('share.chat.poweredBy')}</span>
{
customConfig?.replace_webapp_logo
? <img src={customConfig?.replace_webapp_logo} alt='logo' className='block w-auto h-5' />
: <LogoSite className='!h-5' />
}
</div>
</div>
)
}
</div>
)
}
</div>
)
}
export default ConfigPanel

View File

@ -15,7 +15,7 @@ import type {
ConversationItem, ConversationItem,
} from '@/models/share' } from '@/models/share'
export interface EmbeddedChatbotContextValue { export type EmbeddedChatbotContextValue = {
appInfoError?: any appInfoError?: any
appInfoLoading?: boolean appInfoLoading?: boolean
appMeta?: AppMeta appMeta?: AppMeta
@ -27,13 +27,12 @@ export interface EmbeddedChatbotContextValue {
appPrevChatList: ChatItem[] appPrevChatList: ChatItem[]
pinnedConversationList: AppConversationData['data'] pinnedConversationList: AppConversationData['data']
conversationList: AppConversationData['data'] conversationList: AppConversationData['data']
showConfigPanelBeforeChat: boolean
newConversationInputs: Record<string, any> newConversationInputs: Record<string, any>
newConversationInputsRef: RefObject<Record<string, any>> newConversationInputsRef: RefObject<Record<string, any>>
handleNewConversationInputsChange: (v: Record<string, any>) => void handleNewConversationInputsChange: (v: Record<string, any>) => void
inputsForms: any[] inputsForms: any[]
handleNewConversation: () => void handleNewConversation: () => void
handleStartChat: () => void handleStartChat: (callback?: any) => void
handleChangeConversation: (conversationId: string) => void handleChangeConversation: (conversationId: string) => void
handleNewConversationCompleted: (newConversationId: string) => void handleNewConversationCompleted: (newConversationId: string) => void
chatShouldReloadKey: string chatShouldReloadKey: string
@ -50,7 +49,6 @@ export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>
appPrevChatList: [], appPrevChatList: [],
pinnedConversationList: [], pinnedConversationList: [],
conversationList: [], conversationList: [],
showConfigPanelBeforeChat: false,
newConversationInputs: {}, newConversationInputs: {},
newConversationInputsRef: { current: {} }, newConversationInputsRef: { current: {} },
handleNewConversationInputsChange: () => {}, handleNewConversationInputsChange: () => {},

View File

@ -1,56 +0,0 @@
import type { FC } from 'react'
import React from 'react'
import { RiRefreshLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import type { Theme } from './theme/theme-context'
import { CssTransform } from './theme/utils'
import Tooltip from '@/app/components/base/tooltip'
export type IHeaderProps = {
isMobile?: boolean
customerIcon?: React.ReactNode
title: string
theme?: Theme
onCreateNewChat?: () => void
}
const Header: FC<IHeaderProps> = ({
isMobile,
customerIcon,
title,
theme,
onCreateNewChat,
}) => {
const { t } = useTranslation()
if (!isMobile)
return null
return (
<div
className={`
shrink-0 flex items-center justify-between h-14 px-4
`}
style={Object.assign({}, CssTransform(theme?.backgroundHeaderColorStyle ?? ''), CssTransform(theme?.headerBorderBottomStyle ?? '')) }
>
<div className="flex items-center space-x-2">
{customerIcon}
<div
className={'text-sm font-bold text-white'}
style={CssTransform(theme?.colorFontOnHeaderStyle ?? '')}
>
{title}
</div>
</div>
<Tooltip
popupContent={t('share.chat.resetChat')}
>
<div className='flex cursor-pointer hover:rounded-lg hover:bg-black/5 w-8 h-8 items-center justify-center' onClick={() => {
onCreateNewChat?.()
}}>
<RiRefreshLine className="h-4 w-4 text-sm font-bold text-white" color={theme?.colorPathOnHeader}/>
</div>
</Tooltip>
</div>
)
}
export default React.memo(Header)

View File

@ -0,0 +1,109 @@
import type { FC } from 'react'
import React from 'react'
import { RiResetLeftLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import type { Theme } from '../theme/theme-context'
import { CssTransform } from '../theme/utils'
import {
useEmbeddedChatbotContext,
} from '../context'
import Tooltip from '@/app/components/base/tooltip'
import ActionButton from '@/app/components/base/action-button'
import Divider from '@/app/components/base/divider'
import ViewFormDropdown from '@/app/components/base/chat/embedded-chatbot/inputs-form/view-form-dropdown'
import LogoSite from '@/app/components/base/logo/logo-site'
import cn from '@/utils/classnames'
export type IHeaderProps = {
isMobile?: boolean
customerIcon?: React.ReactNode
title: string
theme?: Theme
onCreateNewChat?: () => void
}
const Header: FC<IHeaderProps> = ({
isMobile,
customerIcon,
title,
theme,
onCreateNewChat,
}) => {
const { t } = useTranslation()
const {
appData,
currentConversationId,
inputsForms,
} = useEmbeddedChatbotContext()
if (!isMobile) {
return (
<div className='shrink-0 h-14 p-3 flex items-center justify-end'>
<div className='flex items-center gap-1'>
{/* powered by */}
<div className='shrink-0'>
{!appData?.custom_config?.remove_webapp_brand && (
<div className={cn(
'shrink-0 px-2 flex items-center gap-1.5',
)}>
<div className='text-text-tertiary system-2xs-medium-uppercase'>{t('share.chat.poweredBy')}</div>
{appData?.custom_config?.replace_webapp_logo && (
<img src={appData?.custom_config?.replace_webapp_logo} alt='logo' className='block w-auto h-5' />
)}
{!appData?.custom_config?.replace_webapp_logo && (
<LogoSite className='!h-5' />
)}
</div>
)}
</div>
{currentConversationId && (
<Divider type='vertical' className='h-3.5' />
)}
{currentConversationId && (
<Tooltip
popupContent={t('share.chat.resetChat')}
>
<ActionButton size='l' onClick={onCreateNewChat}>
<RiResetLeftLine className='w-[18px] h-[18px]' />
</ActionButton>
</Tooltip>
)}
{currentConversationId && inputsForms.length > 0 && (
<ViewFormDropdown />
)}
</div>
</div>
)
}
return (
<div
className={cn('shrink-0 flex items-center justify-between h-14 px-3 rounded-t-2xl')}
style={Object.assign({}, CssTransform(theme?.backgroundHeaderColorStyle ?? ''), CssTransform(theme?.headerBorderBottomStyle ?? '')) }
>
<div className="grow flex items-center space-x-3">
{customerIcon}
<div
className='system-md-semibold truncate'
style={CssTransform(theme?.colorFontOnHeaderStyle ?? '')}
>
{title}
</div>
</div>
<div className='flex items-center gap-1'>
{currentConversationId && (
<Tooltip
popupContent={t('share.chat.resetChat')}
>
<ActionButton size='l' onClick={onCreateNewChat}>
<RiResetLeftLine className={cn('w-[18px] h-[18px]', theme?.colorPathOnHeader)} />
</ActionButton>
</Tooltip>
)}
{currentConversationId && inputsForms.length > 0 && (
<ViewFormDropdown iconColor={theme?.colorPathOnHeader} />
)}
</div>
</div>
)
}
export default React.memo(Header)

View File

@ -88,7 +88,6 @@ export const useEmbeddedChatbot = () => {
}) })
} }
}, [appId, conversationIdInfo, setConversationIdInfo]) }, [appId, conversationIdInfo, setConversationIdInfo])
const [showConfigPanelBeforeChat, setShowConfigPanelBeforeChat] = useState(true)
const [newConversationId, setNewConversationId] = useState('') const [newConversationId, setNewConversationId] = useState('')
const chatShouldReloadKey = useMemo(() => { const chatShouldReloadKey = useMemo(() => {
@ -273,23 +272,18 @@ export const useEmbeddedChatbot = () => {
return true return true
}, [inputsForms, notify, t]) }, [inputsForms, notify, t])
const handleStartChat = useCallback(() => { const handleStartChat = useCallback((callback?: any) => {
if (checkInputsRequired()) { if (checkInputsRequired()) {
setShowConfigPanelBeforeChat(false)
setShowNewConversationItemInList(true) setShowNewConversationItemInList(true)
callback?.()
} }
}, [setShowConfigPanelBeforeChat, setShowNewConversationItemInList, checkInputsRequired]) }, [setShowNewConversationItemInList, checkInputsRequired])
const currentChatInstanceRef = useRef<{ handleStop: () => void }>({ handleStop: () => { } }) const currentChatInstanceRef = useRef<{ handleStop: () => void }>({ handleStop: () => { } })
const handleChangeConversation = useCallback((conversationId: string) => { const handleChangeConversation = useCallback((conversationId: string) => {
currentChatInstanceRef.current.handleStop() currentChatInstanceRef.current.handleStop()
setNewConversationId('') setNewConversationId('')
handleConversationIdInfoChange(conversationId) handleConversationIdInfoChange(conversationId)
}, [handleConversationIdInfoChange])
if (conversationId === '' && !checkInputsRequired(true))
setShowConfigPanelBeforeChat(true)
else
setShowConfigPanelBeforeChat(false)
}, [handleConversationIdInfoChange, setShowConfigPanelBeforeChat, checkInputsRequired])
const handleNewConversation = useCallback(() => { const handleNewConversation = useCallback(() => {
currentChatInstanceRef.current.handleStop() currentChatInstanceRef.current.handleStop()
setNewConversationId('') setNewConversationId('')
@ -299,11 +293,10 @@ export const useEmbeddedChatbot = () => {
} }
else if (currentConversationId) { else if (currentConversationId) {
handleConversationIdInfoChange('') handleConversationIdInfoChange('')
setShowConfigPanelBeforeChat(true)
setShowNewConversationItemInList(true) setShowNewConversationItemInList(true)
handleNewConversationInputsChange({}) handleNewConversationInputsChange({})
} }
}, [handleChangeConversation, currentConversationId, handleConversationIdInfoChange, setShowConfigPanelBeforeChat, setShowNewConversationItemInList, showNewConversationItemInList, handleNewConversationInputsChange]) }, [handleChangeConversation, currentConversationId, handleConversationIdInfoChange, setShowNewConversationItemInList, showNewConversationItemInList, handleNewConversationInputsChange])
const handleNewConversationCompleted = useCallback((newConversationId: string) => { const handleNewConversationCompleted = useCallback((newConversationId: string) => {
setNewConversationId(newConversationId) setNewConversationId(newConversationId)
@ -336,8 +329,6 @@ export const useEmbeddedChatbot = () => {
appPrevChatList, appPrevChatList,
pinnedConversationList, pinnedConversationList,
conversationList, conversationList,
showConfigPanelBeforeChat,
setShowConfigPanelBeforeChat,
setShowNewConversationItemInList, setShowNewConversationItemInList,
newConversationInputs, newConversationInputs,
newConversationInputsRef, newConversationInputsRef,

View File

@ -4,7 +4,6 @@ import {
} from 'react' } from 'react'
import { useAsyncEffect } from 'ahooks' import { useAsyncEffect } from 'ahooks'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { RiLoopLeftLine } from '@remixicon/react'
import { import {
EmbeddedChatbotContext, EmbeddedChatbotContext,
useEmbeddedChatbotContext, useEmbeddedChatbotContext,
@ -12,32 +11,30 @@ import {
import { useEmbeddedChatbot } from './hooks' import { useEmbeddedChatbot } from './hooks'
import { isDify } from './utils' import { isDify } from './utils'
import { useThemeContext } from './theme/theme-context' import { useThemeContext } from './theme/theme-context'
import cn from '@/utils/classnames' import { CssTransform } from './theme/utils'
import { checkOrSetAccessToken } from '@/app/components/share/utils' import { checkOrSetAccessToken } from '@/app/components/share/utils'
import AppUnavailable from '@/app/components/base/app-unavailable' import AppUnavailable from '@/app/components/base/app-unavailable'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import LogoHeader from '@/app/components/base/logo/logo-embedded-chat-header' import LogoHeader from '@/app/components/base/logo/logo-embedded-chat-header'
import Header from '@/app/components/base/chat/embedded-chatbot/header' import Header from '@/app/components/base/chat/embedded-chatbot/header'
import ConfigPanel from '@/app/components/base/chat/embedded-chatbot/config-panel'
import ChatWrapper from '@/app/components/base/chat/embedded-chatbot/chat-wrapper' import ChatWrapper from '@/app/components/base/chat/embedded-chatbot/chat-wrapper'
import Tooltip from '@/app/components/base/tooltip' import LogoSite from '@/app/components/base/logo/logo-site'
import cn from '@/utils/classnames'
const Chatbot = () => { const Chatbot = () => {
const { t } = useTranslation()
const { const {
isMobile, isMobile,
appInfoError, appInfoError,
appInfoLoading, appInfoLoading,
appData, appData,
appPrevChatList,
showConfigPanelBeforeChat,
appChatListDataLoading, appChatListDataLoading,
chatShouldReloadKey,
handleNewConversation, handleNewConversation,
themeBuilder, themeBuilder,
} = useEmbeddedChatbotContext() } = useEmbeddedChatbotContext()
const { t } = useTranslation()
const chatReady = (!showConfigPanelBeforeChat || !!appPrevChatList.length)
const customConfig = appData?.custom_config const customConfig = appData?.custom_config
const site = appData?.site const site = appData?.site
@ -55,52 +52,76 @@ const Chatbot = () => {
if (appInfoLoading) { if (appInfoLoading) {
return ( return (
<Loading type='app' /> <>
{!isMobile && <Loading type='app' />}
{isMobile && (
<div className={cn('relative')}>
<div className={cn('flex flex-col h-[calc(100vh_-_60px)] border-[0.5px] border-components-panel-border rounded-2xl shadow-xs')}>
<Loading type='app' />
</div>
</div>
)}
</>
) )
} }
if (appInfoError) { if (appInfoError) {
return ( return (
<AppUnavailable /> <>
{!isMobile && <AppUnavailable />}
{isMobile && (
<div className={cn('relative')}>
<div className={cn('flex flex-col h-[calc(100vh_-_60px)] border-[0.5px] border-components-panel-border rounded-2xl shadow-xs')}>
<AppUnavailable />
</div>
</div>
)}
</>
) )
} }
return ( return (
<div> <div className='relative'>
<Header <div
isMobile={isMobile} className={cn(
title={site?.title || ''} 'flex flex-col border border-components-panel-border-subtle rounded-2xl',
customerIcon={isDify() ? difyIcon : ''} isMobile ? 'h-[calc(100vh_-_60px)] border-[0.5px] border-components-panel-border shadow-xs' : 'h-[100vh] bg-chatbot-bg',
theme={themeBuilder?.theme} )}
onCreateNewChat={handleNewConversation} style={isMobile ? Object.assign({}, CssTransform(themeBuilder?.theme?.backgroundHeaderColorStyle ?? '')) : {}}
/> >
<div className='flex bg-white overflow-hidden'> <Header
<div className={cn('h-[100vh] grow flex flex-col overflow-y-auto', isMobile && '!h-[calc(100vh_-_3rem)]')}> isMobile={isMobile}
{showConfigPanelBeforeChat && !appChatListDataLoading && !appPrevChatList.length && ( title={site?.title || ''}
<div className={cn('flex w-full items-center justify-center h-full tablet:px-4', isMobile && 'px-4')}> customerIcon={isDify() ? difyIcon : ''}
<ConfigPanel /> theme={themeBuilder?.theme}
</div> onCreateNewChat={handleNewConversation}
)} />
{appChatListDataLoading && chatReady && ( <div className={cn('grow flex flex-col overflow-y-auto', isMobile && '!h-[calc(100vh_-_3rem)] bg-chatbot-bg rounded-2xl')}>
{appChatListDataLoading && (
<Loading type='app' /> <Loading type='app' />
)} )}
{chatReady && !appChatListDataLoading && ( {!appChatListDataLoading && (
<div className='relative h-full pt-8 mx-auto w-full max-w-[720px]'> <ChatWrapper key={chatShouldReloadKey} />
{!isMobile && (
<div className='absolute top-2.5 right-3 z-20'>
<Tooltip
popupContent={t('share.chat.resetChat')}
>
<div className='p-1.5 bg-white border-[0.5px] border-gray-100 rounded-lg shadow-md cursor-pointer' onClick={handleNewConversation}>
<RiLoopLeftLine className="h-4 w-4 text-gray-500"/>
</div>
</Tooltip>
</div>
)}
<ChatWrapper />
</div>
)} )}
</div> </div>
</div> </div>
{/* powered by */}
{isMobile && (
<div className='shrink-0 h-[60px] pl-2 flex items-center'>
{!appData?.custom_config?.remove_webapp_brand && (
<div className={cn(
'shrink-0 px-2 flex items-center gap-1.5',
)}>
<div className='text-text-tertiary system-2xs-medium-uppercase'>{t('share.chat.poweredBy')}</div>
{appData?.custom_config?.replace_webapp_logo && (
<img src={appData?.custom_config?.replace_webapp_logo} alt='logo' className='block w-auto h-5' />
)}
{!appData?.custom_config?.replace_webapp_logo && (
<LogoSite className='!h-5' />
)}
</div>
)}
</div>
)}
</div> </div>
) )
} }
@ -122,7 +143,6 @@ const EmbeddedChatbotWrapper = () => {
appPrevChatList, appPrevChatList,
pinnedConversationList, pinnedConversationList,
conversationList, conversationList,
showConfigPanelBeforeChat,
newConversationInputs, newConversationInputs,
newConversationInputsRef, newConversationInputsRef,
handleNewConversationInputsChange, handleNewConversationInputsChange,
@ -150,7 +170,6 @@ const EmbeddedChatbotWrapper = () => {
appPrevChatList, appPrevChatList,
pinnedConversationList, pinnedConversationList,
conversationList, conversationList,
showConfigPanelBeforeChat,
newConversationInputs, newConversationInputs,
newConversationInputsRef, newConversationInputsRef,
handleNewConversationInputsChange, handleNewConversationInputsChange,

View File

@ -0,0 +1,118 @@
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useEmbeddedChatbotContext } from '../context'
import Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea'
import { PortalSelect } from '@/app/components/base/select'
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
import { InputVarType } from '@/app/components/workflow/types'
type Props = {
showTip?: boolean
}
const InputsFormContent = ({ showTip }: Props) => {
const { t } = useTranslation()
const {
appParams,
inputsForms,
currentConversationId,
currentConversationItem,
newConversationInputs,
newConversationInputsRef,
handleNewConversationInputsChange,
} = useEmbeddedChatbotContext()
const inputsFormValue = currentConversationId ? currentConversationItem?.inputs : newConversationInputs
const readonly = !!currentConversationId
const handleFormChange = useCallback((variable: string, value: any) => {
handleNewConversationInputsChange({
...newConversationInputsRef.current,
[variable]: value,
})
}, [newConversationInputsRef, handleNewConversationInputsChange])
return (
<div className='space-y-4'>
{inputsForms.map(form => (
<div key={form.variable} className='space-y-1'>
<div className='h-6 flex items-center gap-1'>
<div className='text-text-secondary system-md-semibold'>{form.label}</div>
{!form.required && (
<div className='text-text-tertiary system-xs-regular'>{t('appDebug.variableTable.optional')}</div>
)}
</div>
{form.type === InputVarType.textInput && (
<Input
value={inputsFormValue?.[form.variable] || ''}
onChange={e => handleFormChange(form.variable, e.target.value)}
placeholder={form.label}
readOnly={readonly}
disabled={readonly}
/>
)}
{form.type === InputVarType.number && (
<Input
type='number'
value={inputsFormValue?.[form.variable] || ''}
onChange={e => handleFormChange(form.variable, e.target.value)}
placeholder={form.label}
readOnly={readonly}
disabled={readonly}
/>
)}
{form.type === InputVarType.paragraph && (
<Textarea
value={inputsFormValue?.[form.variable] || ''}
onChange={e => handleFormChange(form.variable, e.target.value)}
placeholder={form.label}
readOnly={readonly}
disabled={readonly}
/>
)}
{form.type === InputVarType.select && (
<PortalSelect
popupClassName='w-[200px]'
value={inputsFormValue?.[form.variable]}
items={form.options.map((option: string) => ({ value: option, name: option }))}
onSelect={item => handleFormChange(form.variable, item.value as string)}
placeholder={form.label}
readonly={readonly}
/>
)}
{form.type === InputVarType.singleFile && (
<FileUploaderInAttachmentWrapper
value={inputsFormValue?.[form.variable] ? [inputsFormValue?.[form.variable]] : []}
onChange={files => handleFormChange(form.variable, files[0])}
fileConfig={{
allowed_file_types: form.allowed_file_types,
allowed_file_extensions: form.allowed_file_extensions,
allowed_file_upload_methods: form.allowed_file_upload_methods,
number_limits: 1,
fileUploadConfig: (appParams as any).system_parameters,
}}
/>
)}
{form.type === InputVarType.multiFiles && (
<FileUploaderInAttachmentWrapper
value={inputsFormValue?.[form.variable] || []}
onChange={files => handleFormChange(form.variable, files)}
fileConfig={{
allowed_file_types: form.allowed_file_types,
allowed_file_extensions: form.allowed_file_extensions,
allowed_file_upload_methods: form.allowed_file_upload_methods,
number_limits: form.max_length,
fileUploadConfig: (appParams as any).system_parameters,
}}
/>
)}
</div>
))}
{showTip && (
<div className='text-text-tertiary system-xs-regular'>{t('share.chat.chatFormTip')}</div>
)}
</div>
)
}
export default InputsFormContent

View File

@ -0,0 +1,79 @@
import React from 'react'
import { useTranslation } from 'react-i18next'
import { Message3Fill } from '@/app/components/base/icons/src/public/other'
import Button from '@/app/components/base/button'
import Divider from '@/app/components/base/divider'
import InputsFormContent from '@/app/components/base/chat/embedded-chatbot/inputs-form/content'
import { useEmbeddedChatbotContext } from '../context'
import cn from '@/utils/classnames'
type Props = {
collapsed: boolean
setCollapsed: (collapsed: boolean) => void
}
const InputsFormNode = ({
collapsed,
setCollapsed,
}: Props) => {
const { t } = useTranslation()
const {
isMobile,
currentConversationId,
themeBuilder,
handleStartChat,
} = useEmbeddedChatbotContext()
return (
<div className={cn('mb-6 pt-6 px-4 flex flex-col items-center', isMobile && 'mb-4 pt-4')}>
<div className={cn(
'w-full max-w-[672px] bg-components-panel-bg rounded-2xl border-[0.5px] border-components-panel-border shadow-md',
collapsed && 'bg-components-card-bg border border-components-card-border shadow-none',
)}>
<div className={cn(
'flex items-center gap-3 px-6 py-4 rounded-t-2xl',
!collapsed && 'border-b border-divider-subtle',
isMobile && 'px-4 py-3',
)}>
<Message3Fill className='shrink-0 w-6 h-6' />
<div className='grow text-text-secondary system-xl-semibold'>{t('share.chat.chatSettingsTitle')}</div>
{collapsed && (
<Button className='text-text-tertiary uppercase' size='small' variant='ghost' onClick={() => setCollapsed(false)}>{currentConversationId ? t('common.operation.view') : t('common.operation.edit')}</Button>
)}
{!collapsed && currentConversationId && (
<Button className='text-text-tertiary uppercase' size='small' variant='ghost' onClick={() => setCollapsed(true)}>{t('common.operation.close')}</Button>
)}
</div>
{!collapsed && (
<div className={cn('p-6', isMobile && 'p-4')}>
<InputsFormContent showTip={!!currentConversationId} />
</div>
)}
{!collapsed && !currentConversationId && (
<div className={cn('p-6', isMobile && 'p-4')}>
<Button
variant='primary'
className='w-full'
onClick={() => handleStartChat(() => setCollapsed(true))}
style={
themeBuilder?.theme
? {
backgroundColor: themeBuilder?.theme.primaryColor,
}
: {}
}
>{t('share.chat.startChat')}</Button>
</div>
)}
</div>
{collapsed && (
<div className='py-4 flex items-center w-full max-w-[720px]'>
<Divider bgStyle='gradient' className='basis-1/2 h-px rotate-180' />
<Divider bgStyle='gradient' className='basis-1/2 h-px' />
</div>
)}
</div>
)
}
export default InputsFormNode

View File

@ -0,0 +1,52 @@
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiChatSettingsLine,
} from '@remixicon/react'
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
import { Message3Fill } from '@/app/components/base/icons/src/public/other'
import InputsFormContent from '@/app/components/base/chat/embedded-chatbot/inputs-form/content'
import cn from '@/utils/classnames'
type Props = {
iconColor?: string
}
const ViewFormDropdown = ({ iconColor }: Props) => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='bottom-end'
offset={{
mainAxis: 4,
crossAxis: 4,
}}
>
<PortalToFollowElemTrigger
onClick={() => setOpen(v => !v)}
>
<ActionButton size='l' state={open ? ActionButtonState.Hover : ActionButtonState.Default}>
<RiChatSettingsLine className={cn('w-[18px] h-[18px]', iconColor)} />
</ActionButton>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className="z-50">
<div className='w-[400px] bg-components-panel-bg backdrop-blur-sm rounded-2xl border-[0.5px] border-components-panel-border shadow-lg'>
<div className='flex items-center gap-3 px-6 py-4 rounded-t-2xl border-b border-divider-subtle'>
<Message3Fill className='shrink-0 w-6 h-6' />
<div className='grow text-text-secondary system-xl-semibold'>{t('share.chat.chatSettingsTitle')}</div>
</div>
<div className='p-6'>
<InputsFormContent showTip />
</div>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default ViewFormDropdown

View File

@ -9,7 +9,7 @@ export class Theme {
public backgroundHeaderColorStyle = 'backgroundImage: linear-gradient(to right, #2563eb, #0ea5e9)' public backgroundHeaderColorStyle = 'backgroundImage: linear-gradient(to right, #2563eb, #0ea5e9)'
public headerBorderBottomStyle = '' public headerBorderBottomStyle = ''
public colorFontOnHeaderStyle = 'color: white' public colorFontOnHeaderStyle = 'color: white'
public colorPathOnHeader = 'white' public colorPathOnHeader = 'text-text-primary-on-surface'
public backgroundButtonDefaultColorStyle = 'backgroundColor: #1C64F2' public backgroundButtonDefaultColorStyle = 'backgroundColor: #1C64F2'
public roundedBackgroundColorStyle = 'backgroundColor: rgb(245 248 255)' public roundedBackgroundColorStyle = 'backgroundColor: rgb(245 248 255)'
public chatBubbleColorStyle = 'backgroundColor: rgb(225 239 254)' public chatBubbleColorStyle = 'backgroundColor: rgb(225 239 254)'

View File

@ -1,135 +0,0 @@
'use client'
import type { FC } from 'react'
import React, { useRef, useState } from 'react'
import { useHover } from 'ahooks'
import { useTranslation } from 'react-i18next'
import cn from '@/utils/classnames'
import { MessageCheckRemove, MessageFastPlus } from '@/app/components/base/icons/src/vender/line/communication'
import { MessageFast } from '@/app/components/base/icons/src/vender/solid/communication'
import { Edit04 } from '@/app/components/base/icons/src/vender/line/general'
import RemoveAnnotationConfirmModal from '@/app/components/app/annotation/remove-annotation-confirm-modal'
import Tooltip from '@/app/components/base/tooltip'
import { addAnnotation, delAnnotation } from '@/service/annotation'
import Toast from '@/app/components/base/toast'
import { useProviderContext } from '@/context/provider-context'
import { useModalContext } from '@/context/modal-context'
type Props = {
appId: string
messageId?: string
annotationId?: string
className?: string
cached: boolean
query: string
answer: string
onAdded: (annotationId: string, authorName: string) => void
onEdit: () => void
onRemoved: () => void
}
const CacheCtrlBtn: FC<Props> = ({
className,
cached,
query,
answer,
appId,
messageId,
annotationId,
onAdded,
onEdit,
onRemoved,
}) => {
const { t } = useTranslation()
const { plan, enableBilling } = useProviderContext()
const isAnnotationFull = (enableBilling && plan.usage.annotatedResponse >= plan.total.annotatedResponse)
const { setShowAnnotationFullModal } = useModalContext()
const [showModal, setShowModal] = useState(false)
const cachedBtnRef = useRef<HTMLDivElement>(null)
const isCachedBtnHovering = useHover(cachedBtnRef)
const handleAdd = async () => {
if (isAnnotationFull) {
setShowAnnotationFullModal()
return
}
const res: any = await addAnnotation(appId, {
message_id: messageId,
question: query,
answer,
})
Toast.notify({
message: t('common.api.actionSuccess') as string,
type: 'success',
})
onAdded(res.id, res.account?.name)
}
const handleRemove = async () => {
await delAnnotation(appId, annotationId!)
Toast.notify({
message: t('common.api.actionSuccess') as string,
type: 'success',
})
onRemoved()
setShowModal(false)
}
return (
<div className={cn('inline-block', className)}>
<div className='inline-flex p-0.5 space-x-0.5 rounded-lg bg-white border border-gray-100 shadow-md text-gray-500 cursor-pointer'>
{cached
? (
<div>
<div
ref={cachedBtnRef}
className={cn(isCachedBtnHovering ? 'bg-[#FEF3F2] text-[#D92D20]' : 'bg-[#EEF4FF] text-[#444CE7]', 'flex p-1 space-x-1 items-center rounded-md leading-4 text-xs font-medium')}
onClick={() => setShowModal(true)}
>
{!isCachedBtnHovering
? (
<>
<MessageFast className='w-4 h-4' />
<div>{t('appDebug.feature.annotation.cached')}</div>
</>
)
: <>
<MessageCheckRemove className='w-4 h-4' />
<div>{t('appDebug.feature.annotation.remove')}</div>
</>}
</div>
</div>
)
: answer
? (
<Tooltip
popupContent={t('appDebug.feature.annotation.add')}
>
<div
className='p-1 rounded-md hover:bg-[#EEF4FF] hover:text-[#444CE7] cursor-pointer'
onClick={handleAdd}
>
<MessageFastPlus className='w-4 h-4' />
</div>
</Tooltip>
)
: null
}
<Tooltip
popupContent={t('appDebug.feature.annotation.edit')}
>
<div
className='p-1 cursor-pointer rounded-md hover:bg-black/5'
onClick={onEdit}
>
<Edit04 className='w-4 h-4' />
</div>
</Tooltip>
</div>
<RemoveAnnotationConfirmModal
isShow={showModal}
onHide={() => setShowModal(false)}
onRemove={handleRemove}
/>
</div>
)
}
export default React.memo(CacheCtrlBtn)

View File

@ -0,0 +1,23 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="message-3-fill">
<g id="Vector" filter="url(#filter0_d_1071_49501)">
<path d="M2 8.99374C2 5.68349 4.67654 3 8.00066 3H15.9993C19.3134 3 22 5.69478 22 8.99374V21H8.00066C4.68659 21 2 18.3052 2 15.0063V8.99374ZM14 11V13H16V11H14ZM8 11V13H10V11H8Z" fill="url(#paint0_linear_1071_49501)"/>
</g>
</g>
<defs>
<filter id="filter0_d_1071_49501" x="1.5" y="2.75" width="21" height="19" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="0.25"/>
<feGaussianBlur stdDeviation="0.25"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1071_49501"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1071_49501" result="shape"/>
</filter>
<linearGradient id="paint0_linear_1071_49501" x1="12" y1="3" x2="12" y2="21" gradientUnits="userSpaceOnUse">
<stop stop-color="#296DFF"/>
<stop offset="1" stop-color="#0BA5EC"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,173 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "24",
"height": "24",
"viewBox": "0 0 24 24",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "message-3-fill"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "Vector",
"filter": "url(#filter0_d_1071_49501)"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M2 8.99374C2 5.68349 4.67654 3 8.00066 3H15.9993C19.3134 3 22 5.69478 22 8.99374V21H8.00066C4.68659 21 2 18.3052 2 15.0063V8.99374ZM14 11V13H16V11H14ZM8 11V13H10V11H8Z",
"fill": "url(#paint0_linear_1071_49501)"
},
"children": []
}
]
}
]
},
{
"type": "element",
"name": "defs",
"attributes": {},
"children": [
{
"type": "element",
"name": "filter",
"attributes": {
"id": "filter0_d_1071_49501",
"x": "1.5",
"y": "2.75",
"width": "21",
"height": "19",
"filterUnits": "userSpaceOnUse",
"color-interpolation-filters": "sRGB"
},
"children": [
{
"type": "element",
"name": "feFlood",
"attributes": {
"flood-opacity": "0",
"result": "BackgroundImageFix"
},
"children": []
},
{
"type": "element",
"name": "feColorMatrix",
"attributes": {
"in": "SourceAlpha",
"type": "matrix",
"values": "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0",
"result": "hardAlpha"
},
"children": []
},
{
"type": "element",
"name": "feOffset",
"attributes": {
"dy": "0.25"
},
"children": []
},
{
"type": "element",
"name": "feGaussianBlur",
"attributes": {
"stdDeviation": "0.25"
},
"children": []
},
{
"type": "element",
"name": "feComposite",
"attributes": {
"in2": "hardAlpha",
"operator": "out"
},
"children": []
},
{
"type": "element",
"name": "feColorMatrix",
"attributes": {
"type": "matrix",
"values": "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"
},
"children": []
},
{
"type": "element",
"name": "feBlend",
"attributes": {
"mode": "normal",
"in2": "BackgroundImageFix",
"result": "effect1_dropShadow_1071_49501"
},
"children": []
},
{
"type": "element",
"name": "feBlend",
"attributes": {
"mode": "normal",
"in": "SourceGraphic",
"in2": "effect1_dropShadow_1071_49501",
"result": "shape"
},
"children": []
}
]
},
{
"type": "element",
"name": "linearGradient",
"attributes": {
"id": "paint0_linear_1071_49501",
"x1": "12",
"y1": "3",
"x2": "12",
"y2": "21",
"gradientUnits": "userSpaceOnUse"
},
"children": [
{
"type": "element",
"name": "stop",
"attributes": {
"stop-color": "#296DFF"
},
"children": []
},
{
"type": "element",
"name": "stop",
"attributes": {
"offset": "1",
"stop-color": "#0BA5EC"
},
"children": []
}
]
}
]
}
]
},
"name": "Message3Fill"
}

View File

@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './Message3Fill.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'Message3Fill'
export default Icon

View File

@ -1,3 +1,4 @@
export { default as Icon3Dots } from './Icon3Dots' export { default as Icon3Dots } from './Icon3Dots'
export { default as DefaultToolIcon } from './DefaultToolIcon' export { default as DefaultToolIcon } from './DefaultToolIcon'
export { default as Message3Fill } from './Message3Fill'
export { default as RowStruct } from './RowStruct' export { default as RowStruct } from './RowStruct'

View File

@ -50,7 +50,7 @@ const PasteImageLinkButton: FC<PasteImageLinkButtonProps> = ({
> >
<PortalToFollowElemTrigger onClick={handleToggle}> <PortalToFollowElemTrigger onClick={handleToggle}>
<div className={` <div className={`
relative flex items-center justify-center px-3 h-8 bg-components-option-card-option-bg hover:bg-components-option-card-option-bg-hover text-xs text-text-tertiary rounded-lg relative flex items-center justify-center px-3 h-8 bg-components-button-tertiary-bg hover:bg-components-button-tertiary-bg-hover text-xs text-text-tertiary rounded-lg
${disabled ? 'cursor-not-allowed' : 'cursor-pointer'} ${disabled ? 'cursor-not-allowed' : 'cursor-pointer'}
`}> `}>
<Link03 className='mr-2 w-4 h-4' /> <Link03 className='mr-2 w-4 h-4' />
@ -98,9 +98,9 @@ const TextGenerationImageUploader: FC<TextGenerationImageUploaderProps> = ({
{ {
hovering => ( hovering => (
<div className={` <div className={`
flex items-center justify-center px-3 h-8 bg-components-option-card-option-bg flex items-center justify-center px-3 h-8 bg-components-button-tertiary-bg
text-xs text-text-tertiary rounded-lg cursor-pointer text-xs text-text-tertiary rounded-lg cursor-pointer
${hovering && 'bg-components-option-card-option-bg-hover'} ${hovering && 'hover:bg-components-button-tertiary-bg-hover'}
`}> `}>
<ImagePlus className='mr-2 w-4 h-4' /> <ImagePlus className='mr-2 w-4 h-4' />
{t('common.imageUploader.uploadFromComputer')} {t('common.imageUploader.uploadFromComputer')}

View File

@ -7,11 +7,14 @@ import RehypeKatex from 'rehype-katex'
import RemarkGfm from 'remark-gfm' import RemarkGfm from 'remark-gfm'
import RehypeRaw from 'rehype-raw' import RehypeRaw from 'rehype-raw'
import SyntaxHighlighter from 'react-syntax-highlighter' import SyntaxHighlighter from 'react-syntax-highlighter'
import { atelierHeathLight } from 'react-syntax-highlighter/dist/esm/styles/hljs' import {
atelierHeathDark,
atelierHeathLight,
} from 'react-syntax-highlighter/dist/esm/styles/hljs'
import { Component, memo, useMemo, useRef, useState } from 'react' import { Component, memo, useMemo, useRef, useState } from 'react'
import { flow } from 'lodash-es' import { flow } from 'lodash-es'
import cn from '@/utils/classnames' import ActionButton from '@/app/components/base/action-button'
import CopyBtn from '@/app/components/base/copy-btn' import CopyIcon from '@/app/components/base/copy-icon'
import SVGBtn from '@/app/components/base/svg' import SVGBtn from '@/app/components/base/svg'
import Flowchart from '@/app/components/base/mermaid' import Flowchart from '@/app/components/base/mermaid'
import ImageGallery from '@/app/components/base/image-gallery' import ImageGallery from '@/app/components/base/image-gallery'
@ -22,6 +25,9 @@ import SVGRenderer from '@/app/components/base/svg-gallery'
import MarkdownButton from '@/app/components/base/markdown-blocks/button' import MarkdownButton from '@/app/components/base/markdown-blocks/button'
import MarkdownForm from '@/app/components/base/markdown-blocks/form' import MarkdownForm from '@/app/components/base/markdown-blocks/form'
import ThinkBlock from '@/app/components/base/markdown-blocks/think-block' import ThinkBlock from '@/app/components/base/markdown-blocks/think-block'
import { Theme } from '@/types/app'
import { useAppContext } from '@/context/app-context'
import cn from '@/utils/classnames'
// Available language https://github.com/react-syntax-highlighter/react-syntax-highlighter/blob/master/AVAILABLE_LANGUAGES_HLJS.MD // Available language https://github.com/react-syntax-highlighter/react-syntax-highlighter/blob/master/AVAILABLE_LANGUAGES_HLJS.MD
const capitalizationLanguageNameMap: Record<string, string> = { const capitalizationLanguageNameMap: Record<string, string> = {
@ -100,7 +106,8 @@ export function PreCode(props: { children: any }) {
// visit https://reactjs.org/docs/error-decoder.html?invariant=185 for the full message // visit https://reactjs.org/docs/error-decoder.html?invariant=185 for the full message
// or use the non-minified dev environment for full errors and additional helpful warnings. // or use the non-minified dev environment for full errors and additional helpful warnings.
const CodeBlock: any = memo(({ inline, className, children, ...props }) => { const CodeBlock: any = memo(({ inline, className, children, ...props }: any) => {
const { theme } = useAppContext()
const [isSVG, setIsSVG] = useState(true) const [isSVG, setIsSVG] = useState(true)
const match = /language-(\w+)/.exec(className || '') const match = /language-(\w+)/.exec(className || '')
const language = match?.[1] const language = match?.[1]
@ -140,10 +147,12 @@ const CodeBlock: any = memo(({ inline, className, children, ...props }) => {
return ( return (
<SyntaxHighlighter <SyntaxHighlighter
{...props} {...props}
style={atelierHeathLight} style={theme === Theme.light ? atelierHeathLight : atelierHeathDark}
customStyle={{ customStyle={{
paddingLeft: 12, paddingLeft: 12,
backgroundColor: '#fff', borderBottomLeftRadius: '10px',
borderBottomRightRadius: '10px',
backgroundColor: 'var(--color-components-input-bg-normal)',
}} }}
language={match?.[1]} language={match?.[1]}
showLineNumbers showLineNumbers
@ -159,21 +168,14 @@ const CodeBlock: any = memo(({ inline, className, children, ...props }) => {
return <code {...props} className={className}>{children}</code> return <code {...props} className={className}>{children}</code>
return ( return (
<div> <div className='relative'>
<div <div className='bg-components-input-bg-normal rounded-t-[10px] flex justify-between h-8 items-center p-1 pl-3 border-b border-divider-subtle'>
className='flex justify-between h-8 items-center p-1 pl-3 border-b' <div className='system-xs-semibold-uppercase text-text-secondary'>{languageShowName}</div>
style={{ <div className='flex items-center gap-1'>
borderColor: 'rgba(0, 0, 0, 0.05)',
}}
>
<div className='text-[13px] text-gray-500 font-normal'>{languageShowName}</div>
<div style={{ display: 'flex' }}>
{(['mermaid', 'svg']).includes(language!) && <SVGBtn isSVG={isSVG} setIsSVG={setIsSVG} />} {(['mermaid', 'svg']).includes(language!) && <SVGBtn isSVG={isSVG} setIsSVG={setIsSVG} />}
<CopyBtn <ActionButton>
className='mr-1' <CopyIcon content={String(children).replace(/\n$/, '')}/>
value={String(children).replace(/\n$/, '')} </ActionButton>
isPlain
/>
</div> </div>
</div> </div>
{renderCodeContent} {renderCodeContent}
@ -182,16 +184,16 @@ const CodeBlock: any = memo(({ inline, className, children, ...props }) => {
}) })
CodeBlock.displayName = 'CodeBlock' CodeBlock.displayName = 'CodeBlock'
const VideoBlock: any = memo(({ node }) => { const VideoBlock: any = memo(({ node }: any) => {
const srcs = node.children.filter(child => 'properties' in child).map(child => (child as any).properties.src) const srcs = node.children.filter((child: any) => 'properties' in child).map((child: any) => (child as any).properties.src)
if (srcs.length === 0) if (srcs.length === 0)
return null return null
return <VideoGallery key={srcs.join()} srcs={srcs} /> return <VideoGallery key={srcs.join()} srcs={srcs} />
}) })
VideoBlock.displayName = 'VideoBlock' VideoBlock.displayName = 'VideoBlock'
const AudioBlock: any = memo(({ node }) => { const AudioBlock: any = memo(({ node }: any) => {
const srcs = node.children.filter(child => 'properties' in child).map(child => (child as any).properties.src) const srcs = node.children.filter((child: any) => 'properties' in child).map((child: any) => (child as any).properties.src)
if (srcs.length === 0) if (srcs.length === 0)
return null return null
return <AudioGallery key={srcs.join()} srcs={srcs} /> return <AudioGallery key={srcs.join()} srcs={srcs} />
@ -243,7 +245,7 @@ export function Markdown(props: { content: string; className?: string }) {
preprocessLaTeX, preprocessLaTeX,
])(props.content) ])(props.content)
return ( return (
<div className={cn(props.className, 'markdown-body')}> <div className={cn('markdown-body', '!text-text-primary', props.className)}>
<ReactMarkdown <ReactMarkdown
remarkPlugins={[ remarkPlugins={[
RemarkGfm, RemarkGfm,
@ -282,7 +284,7 @@ export function Markdown(props: { content: string; className?: string }) {
p: Paragraph, p: Paragraph,
button: MarkdownButton, button: MarkdownButton,
form: MarkdownForm, form: MarkdownForm,
script: ScriptBlock, script: ScriptBlock as any,
details: ThinkBlock, details: ThinkBlock,
}} }}
> >

View File

@ -0,0 +1,99 @@
'use client'
import { useState } from 'react'
import { useParams, usePathname } from 'next/navigation'
import {
RiVolumeUpLine,
} from '@remixicon/react'
import { t } from 'i18next'
import Tooltip from '@/app/components/base/tooltip'
import { AudioPlayerManager } from '@/app/components/base/audio-btn/audio.player.manager'
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
type AudioBtnProps = {
id?: string
voice?: string
value?: string
}
type AudioState = 'initial' | 'loading' | 'playing' | 'paused' | 'ended'
const AudioBtn = ({
id,
voice,
value,
}: AudioBtnProps) => {
const [audioState, setAudioState] = useState<AudioState>('initial')
const params = useParams()
const pathname = usePathname()
const audio_finished_call = (event: string): any => {
switch (event) {
case 'ended':
setAudioState('ended')
break
case 'paused':
setAudioState('ended')
break
case 'loaded':
setAudioState('loading')
break
case 'play':
setAudioState('playing')
break
case 'error':
setAudioState('ended')
break
}
}
let url = ''
let isPublic = false
if (params.token) {
url = '/text-to-audio'
isPublic = true
}
else if (params.appId) {
if (pathname.search('explore/installed') > -1)
url = `/installed-apps/${params.appId}/text-to-audio`
else
url = `/apps/${params.appId}/text-to-audio`
}
const handleToggle = async () => {
if (audioState === 'playing' || audioState === 'loading') {
setTimeout(() => setAudioState('paused'), 1)
AudioPlayerManager.getInstance().getAudioPlayer(url, isPublic, id, value, voice, audio_finished_call).pauseAudio()
}
else {
setTimeout(() => setAudioState('loading'), 1)
AudioPlayerManager.getInstance().getAudioPlayer(url, isPublic, id, value, voice, audio_finished_call).playAudio()
}
}
const tooltipContent = {
initial: t('appApi.play'),
ended: t('appApi.play'),
paused: t('appApi.pause'),
playing: t('appApi.playing'),
loading: t('appApi.loading'),
}[audioState]
return (
<Tooltip
popupContent={tooltipContent}
>
<ActionButton
state={
audioState === 'loading' || audioState === 'playing'
? ActionButtonState.Active
: ActionButtonState.Default
}
onClick={handleToggle}
disabled={audioState === 'loading'}
>
<RiVolumeUpLine className='w-4 h-4' />
</ActionButton>
</Tooltip>
)
}
export default AudioBtn

View File

@ -1,31 +0,0 @@
'use client'
import { t } from 'i18next'
import { Refresh } from '../icons/src/vender/line/general'
import Tooltip from '@/app/components/base/tooltip'
type Props = {
className?: string
onClick?: () => void
}
const RegenerateBtn = ({ className, onClick }: Props) => {
return (
<div className={`${className}`}>
<Tooltip
popupContent={t('appApi.regenerate') as string}
>
<div
className={'box-border p-0.5 flex items-center justify-center rounded-md bg-white cursor-pointer'}
onClick={() => onClick?.()}
style={{
boxShadow: '0px 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06)',
}}
>
<Refresh className="p-[3.5px] w-6 h-6 text-[#667085] hover:bg-gray-50" />
</div>
</Tooltip>
</div>
)
}
export default RegenerateBtn

View File

@ -1,5 +1,7 @@
import React from 'react' import React from 'react'
import s from './style.module.css' import s from './style.module.css'
import ActionButton from '../action-button'
import cn from '@/utils/classnames'
type ISVGBtnProps = { type ISVGBtnProps = {
isSVG: boolean isSVG: boolean
@ -11,12 +13,9 @@ const SVGBtn = ({
setIsSVG, setIsSVG,
}: ISVGBtnProps) => { }: ISVGBtnProps) => {
return ( return (
<div <ActionButton onClick={() => { setIsSVG(prevIsSVG => !prevIsSVG) }}>
className={'box-border p-0.5 flex items-center justify-center rounded-md bg-white cursor-pointer'} <div className={cn('w-4 h-4', isSVG ? s.svgIconed : s.svgIcon)}></div>
onClick={() => { setIsSVG(prevIsSVG => !prevIsSVG) }} </ActionButton>
>
<div className={`w-6 h-6 rounded-md hover:bg-gray-50 ${s.svgIcon} ${isSVG ? s.svgIconed : ''}`}></div>
</div>
) )
} }

View File

@ -1,13 +1,13 @@
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import React from 'react' import React from 'react'
import s from './style.module.css'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
type Item = { type Item = {
id: string id: string
name: string name: string
isRight?: boolean isRight?: boolean
icon?: React.ReactNode
extra?: React.ReactNode extra?: React.ReactNode
} }
@ -22,18 +22,22 @@ const TabHeader: FC<ITabHeaderProps> = ({
value, value,
onChange, onChange,
}) => { }) => {
const renderItem = ({ id, name, extra }: Item) => ( const renderItem = ({ id, name, icon, extra }: Item) => (
<div <div
key={id} key={id}
className={cn(id === value ? `${s.itemActive} text-gray-900` : 'text-gray-500', 'relative flex items-center pb-1.5 leading-6 cursor-pointer')} className={cn(
'relative flex items-center pt-2.5 pb-2 border-b-2 border-transparent system-md-semibold cursor-pointer',
id === value ? 'text-text-primary border-components-tab-active' : 'text-text-tertiary',
)}
onClick={() => onChange(id)} onClick={() => onChange(id)}
> >
<div className='text-base font-semibold'>{name}</div> {icon || ''}
<div className='ml-2'>{name}</div>
{extra || ''} {extra || ''}
</div> </div>
) )
return ( return (
<div className='flex justify-between border-b border-gray-200 '> <div className='flex justify-between'>
<div className='flex space-x-4'> <div className='flex space-x-4'>
{items.filter(item => !item.isRight).map(renderItem)} {items.filter(item => !item.isRight).map(renderItem)}
</div> </div>

View File

@ -1,9 +0,0 @@
.itemActive::after {
content: '';
position: absolute;
bottom: -1px;
left: 0;
width: 100%;
height: 2px;
background-color: #155EEF;
}

View File

@ -3,18 +3,16 @@ import type { FC } from 'react'
import React, { useCallback, useEffect, useRef, useState } from 'react' import React, { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { import {
RiBookmark3Line,
RiErrorWarningFill, RiErrorWarningFill,
} from '@remixicon/react' } from '@remixicon/react'
import { useBoolean, useClickAway } from 'ahooks' import { useBoolean } from 'ahooks'
import { XMarkIcon } from '@heroicons/react/24/outline'
import { usePathname, useRouter, useSearchParams } from 'next/navigation' import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import TabHeader from '../../base/tab-header' import TabHeader from '../../base/tab-header'
import Button from '../../base/button'
import { checkOrSetAccessToken } from '../utils' import { checkOrSetAccessToken } from '../utils'
import s from './style.module.css' import MenuDropdown from './menu-dropdown'
import RunBatch from './run-batch' import RunBatch from './run-batch'
import ResDownload from './run-batch/res-download' import ResDownload from './run-batch/res-download'
import cn from '@/utils/classnames'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import RunOnce from '@/app/components/share/text-generation/run-once' import RunOnce from '@/app/components/share/text-generation/run-once'
import { fetchSavedMessage as doFetchSavedMessage, fetchAppInfo, fetchAppParams, removeMessage, saveMessage } from '@/service/share' import { fetchSavedMessage as doFetchSavedMessage, fetchAppInfo, fetchAppParams, removeMessage, saveMessage } from '@/service/share'
@ -26,6 +24,7 @@ import type {
TextToSpeechConfig, TextToSpeechConfig,
} from '@/models/debug' } from '@/models/debug'
import AppIcon from '@/app/components/base/app-icon' import AppIcon from '@/app/components/base/app-icon'
import Badge from '@/app/components/base/badge'
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'
@ -37,6 +36,8 @@ import Toast from '@/app/components/base/toast'
import type { VisionFile, VisionSettings } from '@/types/app' import type { VisionFile, VisionSettings } from '@/types/app'
import { Resolution, TransferMethod } from '@/types/app' import { Resolution, TransferMethod } from '@/types/app'
import { useAppFavicon } from '@/hooks/use-app-favicon' 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. const GROUP_SIZE = 5 // to avoid RPM(Request per minute) limit. The group task finished then the next group.
enum TaskStatus { enum TaskStatus {
@ -72,8 +73,6 @@ const TextGeneration: FC<IMainProps> = ({
const { t } = useTranslation() const { t } = useTranslation()
const media = useBreakpoints() const media = useBreakpoints()
const isPC = media === MediaType.pc const isPC = media === MediaType.pc
const isTablet = media === MediaType.tablet
const isMobile = media === MediaType.mobile
const searchParams = useSearchParams() const searchParams = useSearchParams()
const mode = searchParams.get('mode') || 'create' const mode = searchParams.get('mode') || 'create'
@ -102,6 +101,7 @@ const TextGeneration: FC<IMainProps> = ({
const [appId, setAppId] = useState<string>('') const [appId, setAppId] = useState<string>('')
const [siteInfo, setSiteInfo] = useState<SiteInfo | null>(null) const [siteInfo, setSiteInfo] = useState<SiteInfo | null>(null)
const [canReplaceLogo, setCanReplaceLogo] = useState<boolean>(false) const [canReplaceLogo, setCanReplaceLogo] = useState<boolean>(false)
const [customConfig, setCustomConfig] = useState<Record<string, any> | null>(null)
const [promptConfig, setPromptConfig] = useState<PromptConfig | null>(null) const [promptConfig, setPromptConfig] = useState<PromptConfig | null>(null)
const [moreLikeThisConfig, setMoreLikeThisConfig] = useState<MoreLikeThisConfig | null>(null) const [moreLikeThisConfig, setMoreLikeThisConfig] = useState<MoreLikeThisConfig | null>(null)
const [textToSpeechConfig, setTextToSpeechConfig] = useState<TextToSpeechConfig | null>(null) const [textToSpeechConfig, setTextToSpeechConfig] = useState<TextToSpeechConfig | null>(null)
@ -142,7 +142,7 @@ const TextGeneration: FC<IMainProps> = ({
setAllTaskList([]) // clear batch task running status setAllTaskList([]) // clear batch task running status
// eslint-disable-next-line ts/no-use-before-define // eslint-disable-next-line ts/no-use-before-define
showResSidebar() showResultPanel()
} }
const [controlRetry, setControlRetry] = useState(0) const [controlRetry, setControlRetry] = useState(0)
@ -323,7 +323,7 @@ const TextGeneration: FC<IMainProps> = ({
setControlStopResponding(Date.now()) setControlStopResponding(Date.now())
// eslint-disable-next-line ts/no-use-before-define // eslint-disable-next-line ts/no-use-before-define
showResSidebar() showResultPanel()
} }
const handleCompleted = (completionRes: string, taskId?: number, isSuccess?: boolean) => { const handleCompleted = (completionRes: string, taskId?: number, isSuccess?: boolean) => {
const allTaskListLatest = getLatestTaskList() const allTaskListLatest = getLatestTaskList()
@ -388,10 +388,11 @@ const TextGeneration: FC<IMainProps> = ({
useEffect(() => { useEffect(() => {
(async () => { (async () => {
const [appData, appParams]: any = await fetchInitData() 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) setAppId(appId)
setSiteInfo(siteInfo as SiteInfo) setSiteInfo(siteInfo as SiteInfo)
setCanReplaceLogo(can_replace_logo) setCanReplaceLogo(can_replace_logo)
setCustomConfig(custom_config)
changeLanguage(siteInfo.default_language) changeLanguage(siteInfo.default_language)
const { user_input_form, more_like_this, file_upload, text_to_speech }: any = appParams 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, icon_url: siteInfo?.icon_url,
}) })
const [isShowResSidebar, { setTrue: doShowResSidebar, setFalse: hideResSidebar }] = useBoolean(false) const [isShowResultPanel, { setTrue: doShowResultPanel, setFalse: hideResultPanel }] = useBoolean(false)
const showResSidebar = () => { const showResultPanel = () => {
// fix: useClickAway hideResSidebar will close sidebar // fix: useClickAway hideResSidebar will close sidebar
setTimeout(() => { setTimeout(() => {
doShowResSidebar() doShowResultPanel()
}, 0) }, 0)
} }
const resRef = useRef<HTMLDivElement>(null) const [resultExisted, setResultExisted] = useState(false)
useClickAway(() => {
hideResSidebar()
}, resRef)
const renderRes = (task?: Task) => (<Res const renderRes = (task?: Task) => (<Res
key={task?.id} key={task?.id}
isWorkflow={isWorkflow} isWorkflow={isWorkflow}
isCallBatchAPI={isCallBatchAPI} isCallBatchAPI={isCallBatchAPI}
isPC={isPC} isPC={isPC}
isMobile={isMobile} isMobile={!isPC}
isInstalledApp={isInstalledApp} isInstalledApp={isInstalledApp}
installedAppInfo={installedAppInfo} installedAppInfo={installedAppInfo}
isError={task?.status === TaskStatus.failed} isError={task?.status === TaskStatus.failed}
@ -458,7 +456,7 @@ const TextGeneration: FC<IMainProps> = ({
controlSend={controlSend} controlSend={controlSend}
controlRetry={task?.status === TaskStatus.failed ? controlRetry : 0} controlRetry={task?.status === TaskStatus.failed ? controlRetry : 0}
controlStopResponding={controlStopResponding} controlStopResponding={controlStopResponding}
onShowRes={showResSidebar} onShowRes={showResultPanel}
handleSaveMessage={handleSaveMessage} handleSaveMessage={handleSaveMessage}
taskId={task?.id} taskId={task?.id}
onCompleted={handleCompleted} onCompleted={handleCompleted}
@ -466,77 +464,60 @@ const TextGeneration: FC<IMainProps> = ({
completionFiles={completionFiles} completionFiles={completionFiles}
isShowTextToSpeech={!!textToSpeechConfig?.enabled} isShowTextToSpeech={!!textToSpeechConfig?.enabled}
siteInfo={siteInfo} siteInfo={siteInfo}
onRunStart={() => setResultExisted(true)}
/>) />)
const renderBatchRes = () => { const renderBatchRes = () => {
return (showTaskList.map(task => renderRes(task))) 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 = ( const renderResWrap = (
<div <div
ref={resRef} className={cn(
className={ 'relative flex flex-col h-full',
cn( !isPC && 'h-[calc(100vh_-_36px)] rounded-t-2xl shadow-lg backdrop-blur-sm',
'flex flex-col h-full shrink-0', !isPC
isPC ? 'px-10 py-8' : 'bg-gray-50', ? isShowResultPanel
isTablet && 'p-6', isMobile && 'p-4') ? 'bg-background-default-burn'
} : 'bg-components-panel-bg border-t-[0.5px] border-divider-regular'
: 'bg-chatbot-bg',
)}
> >
<> {isCallBatchAPI && (
<div className='flex items-center justify-between shrink-0'> <div className={cn(
<div className='flex items-center space-x-3'> 'shrink-0 px-14 pt-9 pb-2 flex items-center justify-between',
<div className={s.starIcon}></div> !isPC && 'px-4 pt-3 pb-1',
<div className='text-lg font-semibold text-gray-800'>{t('share.generation.title')}</div> )}>
</div> <div className='text-text-primary system-md-semibold-uppercase'>{t('share.generation.executions', { num: allTaskList.length })}</div>
<div className='flex items-center space-x-2'> {allSuccessTaskList.length > 0 && (
{allFailedTaskList.length > 0 && ( <ResDownload
<div className='flex items-center'> isMobile={!isPC}
<RiErrorWarningFill className='w-4 h-4 text-[#D92D20]' /> values={exportRes}
<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>
)} )}
</div> </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> </div>
) )
@ -548,46 +529,34 @@ const TextGeneration: FC<IMainProps> = ({
} }
return ( 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( <div className={cn(
isPC && 'flex', 'shrink-0 relative flex flex-col h-full',
isInstalledApp ? s.installedApp : 'h-screen', isPC ? 'w-[600px] max-w-[50%]' : resultExisted ? 'h-[calc(100%_-_64px)]' : '',
'bg-gray-50', isInstalledApp && 'rounded-l-2xl',
)}> )}>
{/* Left */} {/* header */}
<div className={cn( <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')}>
isPC ? 'w-[600px] max-w-[50%] p-8' : 'p-4', <div className='flex items-center gap-3'>
isInstalledApp && 'rounded-l-2xl', <AppIcon
'shrink-0 relative flex flex-col pb-10 h-full border-r border-gray-100 bg-white', size={isPC ? 'large' : 'small'}
)}> iconType={siteInfo.icon_type}
<div className='mb-6'> icon={siteInfo.icon}
<div className='flex items-center justify-between'> background={siteInfo.icon_background || appDefaultIconBackground}
<div className='flex items-center space-x-3'> imageUrl={siteInfo.icon_url}
<AppIcon />
size="small" <div className='grow text-text-secondary system-md-semibold truncate'>{siteInfo.title}</div>
iconType={siteInfo.icon_type} <MenuDropdown data={siteInfo} />
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>
)}
</div> </div>
{siteInfo.description && (
<div className='system-xs-regular text-text-tertiary'>{siteInfo.description}</div>
)}
<TabHeader <TabHeader
items={[ items={[
{ id: 'create', name: t('share.generation.tabs.create') }, { id: 'create', name: t('share.generation.tabs.create') },
@ -597,11 +566,12 @@ const TextGeneration: FC<IMainProps> = ({
id: 'saved', id: 'saved',
name: t('share.generation.tabs.saved'), name: t('share.generation.tabs.saved'),
isRight: true, isRight: true,
icon: <RiBookmark3Line className='w-4 h-4' />,
extra: savedMessages.length > 0 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} {savedMessages.length}
</div> </Badge>
) )
: null, : null,
}] }]
@ -610,72 +580,89 @@ const TextGeneration: FC<IMainProps> = ({
value={currentTab} value={currentTab}
onChange={setCurrentTab} onChange={setCurrentTab}
/> />
<div className='h-20 overflow-y-auto grow'> </div>
<div className={cn(currentTab === 'create' ? 'block' : 'hidden')}> {/* form */}
<RunOnce <div className={cn(
siteInfo={siteInfo} 'grow h-0 bg-components-panel-bg overflow-y-auto',
inputs={inputs} isPC ? 'px-8' : 'px-4',
inputsRef={inputsRef} !isPC && resultExisted && customConfig?.remove_webapp_brand && 'rounded-b-2xl border-b-[0.5px] border-divider-regular',
onInputsChange={setInputs} )}>
promptConfig={promptConfig} <div className={cn(currentTab === 'create' ? 'block' : 'hidden')}>
onSend={handleSend} <RunOnce
visionConfig={visionConfig} siteInfo={siteInfo}
onVisionFilesChange={setCompletionFiles} inputs={inputs}
/> inputsRef={inputsRef}
</div> onInputsChange={setInputs}
<div className={cn(isInBatchTab ? 'block' : 'hidden')}> promptConfig={promptConfig}
<RunBatch onSend={handleSend}
vars={promptConfig.prompt_variables} visionConfig={visionConfig}
onSend={handleRunBatch} onVisionFilesChange={setCompletionFiles}
isAllFinished={allTasksRun} />
/>
</div>
{currentTab === 'saved' && (
<SavedItems
className='mt-4'
isShowTextToSpeech={textToSpeechConfig?.enabled}
list={savedMessages}
onRemove={handleRemoveSavedMessage}
onStartCreateContent={() => setCurrentTab('create')}
/>
)}
</div> </div>
<div className={cn(isInBatchTab ? 'block' : 'hidden')}>
{/* copyright */} <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( <div className={cn(
isInstalledApp ? 'left-[248px]' : 'left-8', 'shrink-0 py-3 flex items-center gap-1.5 bg-components-panel-bg',
'fixed bottom-4 flex space-x-2 text-gray-400 font-normal text-xs', isPC ? 'px-8' : 'px-4',
!isPC && resultExisted && 'rounded-b-2xl border-b-[0.5px] border-divider-regular',
)}> )}>
{siteInfo.copyright && ( <div className='text-text-tertiary system-2xs-medium-uppercase'>{t('share.chat.poweredBy')}</div>
<div className="">© {(new Date()).getFullYear()} {siteInfo.copyright}</div> {customConfig?.replace_webapp_logo && (
<img src={customConfig?.replace_webapp_logo} alt='logo' className='block w-auto h-5' />
)} )}
{siteInfo.privacy_policy && ( {!customConfig?.replace_webapp_logo && (
<> <LogoSite className='!h-5' />
{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>
</>
)} )}
</div> </div>
</div> )}
{/* Result */}
<div
className={resWrapClassNames}
style={{
background: (!isPC && isShowResSidebar) ? 'rgba(35, 56, 118, 0.2)' : 'none',
}}
>
{renderResWrap}
</div>
</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 type { FC } from 'react'
import React from 'react' import React from 'react'
import {
RiSparklingFill,
} from '@remixicon/react'
import { useTranslation } from 'react-i18next' 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 = {} export type INoDataProps = {}
const NoData: FC<INoDataProps> = () => { const NoData: FC<INoDataProps> = () => {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<div className='flex flex-col h-full w-full justify-center items-center'> <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 <div
className='mt-3 text-gray-300 text-xs leading-3' className='mt-2 text-text-quaternary system-sm-regular'
> >
{t('share.generation.noData')} {t('share.generation.noData')}
</div> </div>

View File

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

View File

@ -27,17 +27,17 @@ const CSVDownload: FC<ICSVDownloadProps> = ({
return ( return (
<div className='mt-6'> <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'> <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'> <table className='table-fixed w-full border-separate border-spacing-0 border border-divider-regular rounded-lg text-xs'>
<thead className='text-gray-500'> <thead className='text-text-tertiary'>
<tr> <tr>
{addQueryContentVars.map((item, i) => ( {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> </tr>
</thead> </thead>
<tbody className='text-gray-300'> <tbody className='text-text-secondary'>
<tr> <tr>
{addQueryContentVars.map((item, i) => ( {addQueryContentVars.map((item, i) => (
<td key={i} className='h-9 pl-4'>{item.name} {t('share.generation.field')}</td> <td key={i} className='h-9 pl-4'>{item.name} {t('share.generation.field')}</td>
@ -58,7 +58,7 @@ const CSVDownload: FC<ICSVDownloadProps> = ({
template, 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' /> <DownloadIcon className='w-3 h-3' />
<span>{t('share.generation.downloadTemplate')}</span> <span>{t('share.generation.downloadTemplate')}</span>
</div> </div>

View File

@ -5,7 +5,6 @@ import {
useCSVReader, useCSVReader,
} from 'react-papaparse' } from 'react-papaparse'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import s from './style.module.css'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { Csv as CSVIcon } from '@/app/components/base/icons/src/public/files' import { Csv as CSVIcon } from '@/app/components/base/icons/src/public/files'
@ -41,7 +40,11 @@ const CSVReader: FC<Props> = ({
<> <>
<div <div
{...getRootProps()} {...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 acceptedFile
@ -49,15 +52,15 @@ const CSVReader: FC<Props> = ({
<div className='w-full flex items-center space-x-2'> <div className='w-full flex items-center space-x-2'>
<CSVIcon className="shrink-0" /> <CSVIcon className="shrink-0" />
<div className='flex w-0 grow'> <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='max-w-[calc(100%_-_30px)] truncate text-text-secondary'>{acceptedFile.name.replace(/.csv$/, '')}</span>
<span className='shrink-0 text-gray-500'>.csv</span> <span className='shrink-0 text-text-tertiary'>.csv</span>
</div> </div>
</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" /> <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>
)} )}
</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' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import React from 'react' import React from 'react'
import {
PlayIcon,
} from '@heroicons/react/24/solid'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { import {
RiLoader2Line, RiLoader2Line,
RiPlayLargeLine,
} from '@remixicon/react' } from '@remixicon/react'
import CSVReader from './csv-reader' import CSVReader from './csv-reader'
import CSVDownload from './csv-download' import CSVDownload from './csv-download'
import cn from '@/utils/classnames'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import cn from '@/utils/classnames'
export type IRunBatchProps = { export type IRunBatchProps = {
vars: { name: string }[] vars: { name: string }[]
onSend: (data: string[][]) => void onSend: (data: string[][]) => void
@ -24,6 +23,8 @@ const RunBatch: FC<IRunBatchProps> = ({
isAllFinished, isAllFinished,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const media = useBreakpoints()
const isPC = media === MediaType.pc
const [csvData, setCsvData] = React.useState<string[][]>([]) const [csvData, setCsvData] = React.useState<string[][]>([])
const [isParsed, setIsParsed] = React.useState(false) const [isParsed, setIsParsed] = React.useState(false)
@ -36,16 +37,15 @@ const RunBatch: FC<IRunBatchProps> = ({
const handleSend = () => { const handleSend = () => {
onSend(csvData) onSend(csvData)
} }
const Icon = isAllFinished ? PlayIcon : RiLoader2Line const Icon = isAllFinished ? RiPlayLargeLine : RiLoader2Line
return ( return (
<div className='pt-4'> <div className='pt-4'>
<CSVReader onParsed={handleParsed} /> <CSVReader onParsed={handleParsed} />
<CSVDownload vars={vars} /> <CSVDownload vars={vars} />
<div className='mt-4 h-[1px] bg-gray-100'></div>
<div className='flex justify-end'> <div className='flex justify-end'>
<Button <Button
variant="primary" variant="primary"
className='mt-4 pl-3 pr-4' className={cn('mt-4 pl-3 pr-4', !isPC && 'grow')}
onClick={handleSend} onClick={handleSend}
disabled={!isParsed || !isAllFinished} disabled={!isParsed || !isAllFinished}
> >

View File

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

View File

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

View File

@ -19,11 +19,11 @@ import { useStore } from '../store'
import { import {
WorkflowRunningStatus, WorkflowRunningStatus,
} from '../types' } from '../types'
import { SimpleBtn } from '../../app/text-generate/item'
import Toast from '../../base/toast' import Toast from '../../base/toast'
import InputsPanel from './inputs-panel' import InputsPanel from './inputs-panel'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import Button from '@/app/components/base/button'
const WorkflowPreview = () => { const WorkflowPreview = () => {
const { t } = useTranslation() const { t } = useTranslation()
@ -122,8 +122,8 @@ const WorkflowPreview = () => {
onClick={() => switchTab('DETAIL')} onClick={() => switchTab('DETAIL')}
/> />
{(workflowRunningData?.result.status === WorkflowRunningStatus.Succeeded && workflowRunningData?.resultText && typeof workflowRunningData?.resultText === 'string') && ( {(workflowRunningData?.result.status === WorkflowRunningStatus.Succeeded && workflowRunningData?.resultText && typeof workflowRunningData?.resultText === 'string') && (
<SimpleBtn <Button
className={cn('ml-4 mb-4 inline-flex space-x-1')} className={cn('ml-4 mb-4 space-x-1')}
onClick={() => { onClick={() => {
const content = workflowRunningData?.resultText const content = workflowRunningData?.resultText
if (typeof content === 'string') if (typeof content === 'string')
@ -134,7 +134,7 @@ const WorkflowPreview = () => {
}}> }}>
<RiClipboardLine className='w-3.5 h-3.5' /> <RiClipboardLine className='w-3.5 h-3.5' />
<div>{t('common.operation.copy')}</div> <div>{t('common.operation.copy')}</div>
</SimpleBtn> </Button>
)} )}
</> </>
)} )}

View File

@ -89,7 +89,7 @@ const NodePanel: FC<Props> = ({
<div <div
className={cn( className={cn(
'flex items-center pl-1 pr-3 cursor-pointer', 'flex items-center pl-1 pr-3 cursor-pointer',
hideInfo ? 'py-2' : 'py-1.5', hideInfo ? 'py-2 pl-2' : 'py-1.5',
!collapseState && (hideInfo ? '!pb-1' : '!pb-1.5'), !collapseState && (hideInfo ? '!pb-1' : '!pb-1.5'),
)} )}
onClick={() => setCollapseState(!collapseState)} onClick={() => setCollapseState(!collapseState)}

View File

@ -170,7 +170,7 @@ const TracingPanel: FC<TracingPanelProps> = ({
return ( return (
<div <div
className={cn(className || 'bg-components-panel-bg', 'py-2')} className={cn('py-2', className)}
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
e.nativeEvent.stopImmediatePropagation() e.nativeEvent.stopImmediatePropagation()

View File

@ -1,77 +1,22 @@
@mixin light { @import '../../themes/light';
color-scheme: light; @import '../../themes/dark';
--color-prettylights-syntax-comment: #6e7781; @import '../../themes/markdown-light';
--color-prettylights-syntax-constant: #0550ae; @import '../../themes/markdown-dark';
--color-prettylights-syntax-entity: #8250df;
--color-prettylights-syntax-storage-modifier-import: #24292f;
--color-prettylights-syntax-entity-tag: #116329;
--color-prettylights-syntax-keyword: #cf222e;
--color-prettylights-syntax-string: #0a3069;
--color-prettylights-syntax-variable: #953800;
--color-prettylights-syntax-brackethighlighter-unmatched: #82071e;
--color-prettylights-syntax-invalid-illegal-text: #f6f8fa;
--color-prettylights-syntax-invalid-illegal-bg: #82071e;
--color-prettylights-syntax-carriage-return-text: #f6f8fa;
--color-prettylights-syntax-carriage-return-bg: #cf222e;
--color-prettylights-syntax-string-regexp: #116329;
--color-prettylights-syntax-markup-list: #3b2300;
--color-prettylights-syntax-markup-heading: #0550ae;
--color-prettylights-syntax-markup-italic: #24292f;
--color-prettylights-syntax-markup-bold: #24292f;
--color-prettylights-syntax-markup-deleted-text: #82071e;
--color-prettylights-syntax-markup-deleted-bg: #ffebe9;
--color-prettylights-syntax-markup-inserted-text: #116329;
--color-prettylights-syntax-markup-inserted-bg: #dafbe1;
--color-prettylights-syntax-markup-changed-text: #953800;
--color-prettylights-syntax-markup-changed-bg: #ffd8b5;
--color-prettylights-syntax-markup-ignored-text: #eaeef2;
--color-prettylights-syntax-markup-ignored-bg: #0550ae;
--color-prettylights-syntax-meta-diff-range: #8250df;
--color-prettylights-syntax-brackethighlighter-angle: #57606a;
--color-prettylights-syntax-sublimelinter-gutter-mark: #8c959f;
--color-prettylights-syntax-constant-other-reference-link: #0a3069;
--color-fg-default: #24292f;
--color-fg-muted: #57606a;
--color-fg-subtle: #6e7781;
--color-canvas-default: transparent;
--color-canvas-subtle: #f6f8fa;
--color-border-default: #d0d7de;
--color-border-muted: hsla(210, 18%, 87%, 1);
--color-neutral-muted: rgba(175, 184, 193, 0.2);
--color-accent-fg: #0969da;
--color-accent-emphasis: #0969da;
--color-attention-subtle: #fff8c5;
--color-danger-fg: #cf222e;
}
.markdown-body { .markdown-body {
-ms-text-size-adjust: 100%; -ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;
margin: 4px 0 0 0; margin: 0;
color: #101828; color: var(--color-text-primary);
background-color: var(--color-canvas-default); background-color: var(--color-canvas-default);
font-size: 14px; font-size: 15px;
font-weight: 400; font-weight: 400;
line-height: 1.5; line-height: 1.6;
word-wrap: break-word; word-wrap: break-word;
word-break: break-word; word-break: break-word;
user-select: text; user-select: text;
} }
.light {
@include light;
}
:root {
@include light;
}
@media (prefers-color-scheme: light) {
:root {
@include light;
}
}
.markdown-body .octicon { .markdown-body .octicon {
display: inline-block; display: inline-block;
fill: currentColor; fill: currentColor;
@ -109,18 +54,44 @@
.markdown-body a { .markdown-body a {
background-color: transparent; background-color: transparent;
color: #155EEF; color: var(--color-text-accent);
text-decoration: none; text-decoration: none;
text-decoration-color: var(--color-text-accent);
}
.markdown-body a:hover {
position: relative;
color: var(--color-text-accent-secondary);
text-decoration-color: var(--color-text-accent-secondary);
text-decoration: underline;
} }
.markdown-body abbr[title] { .markdown-body abbr[title] {
position: relative;
border-bottom: none; border-bottom: none;
text-decoration: underline dotted; text-decoration: underline dotted;
text-decoration-color: var(--color-text-accent);
}
.markdown-body abbr[title]:hover::after {
@apply shadow-xl shadow-shadow-shadow-5 rounded-md;
position: absolute;
bottom: 100%;
left: 0;
display: block;
width: max-content;
content: attr(title);
padding: 6px;
font-size: 12px;
line-height: 1;
color: var(--color-text-secondary);
border: 0.5px solid var(--color-components-panel-border);
background-color: var(--color-components-tooltip-bg);
} }
.markdown-body b, .markdown-body b,
.markdown-body strong { .markdown-body strong {
font-weight: var(--base-text-weight-semibold, 600); font-weight: var(--base-text-weight-bold, 700);
} }
.markdown-body dfn { .markdown-body dfn {
@ -152,10 +123,15 @@
top: -0.5em; top: -0.5em;
} }
.markdown-body figure {
margin: 1em 40px;
}
.markdown-body img { .markdown-body img {
border-style: none;
max-width: 100%; max-width: 100%;
box-sizing: content-box; box-sizing: content-box;
border: 2px solid var(--color-effects-image-frame);
border-radius: 0;
background-color: var(--color-canvas-default); background-color: var(--color-canvas-default);
} }
@ -167,20 +143,19 @@
font-size: 1em; font-size: 1em;
} }
.markdown-body figure { .markdown-body hr {
margin: 1em 40px; margin: 24px 0;
} }
.markdown-body hr { .markdown-body hr::before {
box-sizing: content-box; display: table;
overflow: hidden; content: "";
background: transparent; }
border-bottom: 1px solid var(--color-border-muted);
height: 0.25em; .markdown-body hr::after {
padding: 0; display: table;
margin: 24px 0; clear: both;
background-color: var(--color-border-default); content: "";
border: 0;
} }
.markdown-body input { .markdown-body input {
@ -197,13 +172,11 @@
.markdown-body [type="submit"] { .markdown-body [type="submit"] {
-webkit-appearance: button; -webkit-appearance: button;
} }
.markdown-body [type="checkbox"], .markdown-body [type="checkbox"],
.markdown-body [type="radio"] { .markdown-body [type="radio"] {
box-sizing: border-box; box-sizing: border-box;
padding: 0; padding: 0;
} }
.markdown-body [type="number"]::-webkit-inner-spin-button, .markdown-body [type="number"]::-webkit-inner-spin-button,
.markdown-body [type="number"]::-webkit-outer-spin-button { .markdown-body [type="number"]::-webkit-outer-spin-button {
height: auto; height: auto;
@ -233,24 +206,16 @@
opacity: 1; opacity: 1;
} }
.markdown-body hr::before {
display: table;
content: "";
}
.markdown-body hr::after {
display: table;
clear: both;
content: "";
}
.markdown-body table { .markdown-body table {
border-spacing: 0; border-spacing: 0;
border-collapse: collapse; border-collapse: separate;
display: block; display: block;
width: max-content; width: max-content;
max-width: 100%; max-width: 100%;
overflow: auto; overflow: hidden;
border: 1px solid var(--color-divider-regular);
border-radius: 8px;
} }
.markdown-body td, .markdown-body td,
@ -302,17 +267,14 @@
.markdown-body kbd { .markdown-body kbd {
display: inline-block; display: inline-block;
padding: 3px 5px; padding: 2px 6px;
font: 11px ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, font: 11px ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas,
Liberation Mono, monospace; Liberation Mono, monospace;
line-height: 10px; line-height: 1;
color: var(--color-fg-default); color: var(--color-text-primary);
vertical-align: middle; vertical-align: middle;
background-color: var(--color-canvas-subtle); background-color: var(--color-components-input-bg-normal);
border: solid 1px var(--color-neutral-muted);
border-bottom-color: var(--color-neutral-muted);
border-radius: 6px; border-radius: 6px;
box-shadow: inset 0 -1px 0 var(--color-neutral-muted);
} }
.markdown-body h1, .markdown-body h1,
@ -327,17 +289,25 @@
line-height: 1.25; line-height: 1.25;
} }
.markdown-body blockquote { .markdown-body h1 {
margin: 0; font-size: 18px;
padding: 0 8px;
border-left: 2px solid #2970FF;
} }
.markdown-body ul, .markdown-body h2 {
.markdown-body ol { font-size: 16px;
margin-top: 0; }
margin-bottom: 0;
padding-left: 2em; .markdown-body h3,
.markdown-body h4,
.markdown-body h5,
.markdown-body h6 {
font-size: 14px;
}
.markdown-body blockquote {
margin: 0;
padding: 0 12px;
border-left: 3px solid var(--color-text-accent-secondary);
} }
.markdown-body ol { .markdown-body ol {
@ -348,6 +318,11 @@
list-style: disc; list-style: disc;
} }
.markdown-body>ol,
.markdown-body>ul {
padding: 0;
}
.markdown-body ol ol, .markdown-body ol ol,
.markdown-body ul ol { .markdown-body ul ol {
list-style-type: lower-roman; list-style-type: lower-roman;
@ -446,6 +421,11 @@
margin-bottom: 12px; margin-bottom: 12px;
} }
.markdown-body ul,
.markdown-body ol {
padding-left: 2em;
}
.markdown-body blockquote> :first-child { .markdown-body blockquote> :first-child {
margin-top: 0; margin-top: 0;
} }
@ -587,23 +567,35 @@
} }
.markdown-body table th { .markdown-body table th {
font-weight: var(--base-text-weight-semibold, 600); color: var(--color-text-tertiary);
font-size: 12px;
font-weight: var(--base-text-weight-medium, 500);
white-space: nowrap;
}
.markdown-body table td {
color: var(--color-text-secondary);
font-size: 13px;
font-weight: var(--base-text-weight-normal, 400);
white-space: nowrap; white-space: nowrap;
} }
.markdown-body table th, .markdown-body table th,
.markdown-body table td { .markdown-body table td {
padding: 6px 13px; padding: 6px 13px;
border: 1px solid var(--color-border-default);
} }
.markdown-body table tr { .markdown-body table tr>th:not(:last-child),
background-color: var(--color-canvas-default); .markdown-body table tr>td:not(:last-child) {
border-top: 1px solid var(--color-border-muted); border-right: 1px solid var(--color-divider-subtle);
} }
.markdown-body table tr:nth-child(2n) { .markdown-body table tbody tr:first-child td {
background-color: var(--color-canvas-subtle); border-top: 1px solid var(--color-divider-regular);
}
.markdown-body table tbody tr:not(:last-child) td {
border-bottom: 1px solid var(--color-divider-subtle);
} }
.markdown-body table img { .markdown-body table img {
@ -761,11 +753,10 @@
.markdown-body .highlight pre, .markdown-body .highlight pre,
.markdown-body pre { .markdown-body pre {
padding: 16px; padding: 16px;
background: #fff; background-color: transparent;
overflow: auto; overflow: auto;
font-size: 85%; font-size: 85%;
line-height: 1.45; line-height: 1.45;
border-radius: 6px;
} }
.markdown-body pre { .markdown-body pre {
@ -1043,5 +1034,5 @@
} }
.markdown-body .react-syntax-highlighter-line-number { .markdown-body .react-syntax-highlighter-line-number {
color: #D0D5DD; color: var(--color-text-quaternary);
} }

View File

@ -0,0 +1,47 @@
'use client'
import { useCallback, useEffect, useState } from 'react'
import { createContext, useContextSelector } from 'use-context-selector'
import type { FC, ReactNode } from 'react'
import { Theme } from '@/types/app'
export type SharePageContextValue = {
theme: Theme
setTheme: (theme: Theme) => void
}
const SharePageContext = createContext<SharePageContextValue>({
theme: Theme.light,
setTheme: () => { },
})
export function useSelector<T>(selector: (value: SharePageContextValue) => T): T {
return useContextSelector(SharePageContext, selector)
}
export type SharePageContextProviderProps = {
children: ReactNode
}
export const SharePageContextProvider: FC<SharePageContextProviderProps> = ({ children }) => {
const [theme, setTheme] = useState<Theme>(Theme.light)
const handleSetTheme = useCallback((theme: Theme) => {
setTheme(theme)
globalThis.document.documentElement.setAttribute('data-theme', theme)
}, [])
useEffect(() => {
globalThis.document.documentElement.setAttribute('data-theme', theme)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return (
<SharePageContext.Provider value={{
theme,
setTheme: handleSetTheme,
}}>
{children}
</SharePageContext.Provider>
)
}
export default SharePageContextProvider

View File

@ -5,11 +5,14 @@ const translation = {
appUnknownError: 'App is unavailable', appUnknownError: 'App is unavailable',
}, },
chat: { chat: {
newChat: 'New chat', newChat: 'Start New chat',
chatSettingsTitle: 'New chat setup',
chatFormTip: 'Chat settings cannot be modified after the chat has started.',
pinnedTitle: 'Pinned', pinnedTitle: 'Pinned',
unpinnedTitle: 'Chats', unpinnedTitle: 'Recent',
newChatDefaultName: 'New conversation', newChatDefaultName: 'New conversation',
resetChat: 'Reset conversation', resetChat: 'Reset conversation',
viewChatSettings: 'View chat settings',
poweredBy: 'Powered by', poweredBy: 'Powered by',
prompt: 'Prompt', prompt: 'Prompt',
privatePromptConfigTitle: 'Conversation settings', privatePromptConfigTitle: 'Conversation settings',
@ -47,6 +50,8 @@ const translation = {
completionResult: 'Completion result', completionResult: 'Completion result',
queryPlaceholder: 'Write your query content...', queryPlaceholder: 'Write your query content...',
run: 'Execute', run: 'Execute',
execution: 'EXECUTION',
executions: '{{num}} EXECUTIONS',
copy: 'Copy', copy: 'Copy',
resultTitle: 'AI Completion', resultTitle: 'AI Completion',
noData: 'AI will give you what you want here.', noData: 'AI will give you what you want here.',

View File

@ -5,11 +5,14 @@ const translation = {
appUnknownError: '应用不可用', appUnknownError: '应用不可用',
}, },
chat: { chat: {
newChat: '新对话', newChat: '开启新对话',
chatSettingsTitle: '新对话设置',
chatFormTip: '对话开始后,对话设置将无法修改。',
pinnedTitle: '已置顶', pinnedTitle: '已置顶',
unpinnedTitle: '对话列表', unpinnedTitle: '对话列表',
newChatDefaultName: '新的对话', newChatDefaultName: '新的对话',
resetChat: '重置对话', resetChat: '重置对话',
viewChatSettings: '查看对话设置',
poweredBy: 'Powered by', poweredBy: 'Powered by',
prompt: '提示词', prompt: '提示词',
privatePromptConfigTitle: '对话设置', privatePromptConfigTitle: '对话设置',
@ -43,6 +46,8 @@ const translation = {
completionResult: '生成结果', completionResult: '生成结果',
queryPlaceholder: '请输入文本内容', queryPlaceholder: '请输入文本内容',
run: '运行', run: '运行',
execution: '运行',
executions: '{{num}} 次运行',
copy: '拷贝', copy: '拷贝',
resultTitle: 'AI 书写', resultTitle: 'AI 书写',
noData: 'AI 会在这里给你惊喜。', noData: 'AI 会在这里给你惊喜。',

View File

@ -69,11 +69,21 @@
iframe.id = iframeId; iframe.id = iframeId;
iframe.src = iframeUrl; iframe.src = iframeUrl;
iframe.style.cssText = ` iframe.style.cssText = `
border: none; position: absolute; flex-direction: column; justify-content: space-between; position: absolute;
box-shadow: rgba(150, 150, 150, 0.2) 0px 10px 30px 0px, rgba(150, 150, 150, 0.2) 0px 0px 0px 1px; display: flex;
bottom: 55px; right: 0; width: 24rem; max-width: calc(100vw - 2rem); height: 40rem; flex-direction: column;
max-height: calc(100vh - 6rem); border-radius: 0.75rem; display: flex; z-index: 2147483647; justify-content: space-between;
overflow: hidden; left: unset; background-color: #F3F4F6;user-select: none; left: unset;
right: 0;
bottom: 0;
width: 24rem;
max-width: calc(100vw - 2rem);
height: 43.75rem;
max-height: calc(100vh - 6rem);
border: none;
z-index: 2147483640;
overflow: hidden;
user-select: none;
`; `;
return iframe; return iframe;
@ -92,12 +102,12 @@
const buttonInBottom = buttonRect.top - 5 > targetIframe.clientHeight const buttonInBottom = buttonRect.top - 5 > targetIframe.clientHeight
if (buttonInBottom) { if (buttonInBottom) {
targetIframe.style.bottom = `${buttonRect.height + 5}px`; targetIframe.style.bottom = '0px';
targetIframe.style.top = 'unset'; targetIframe.style.top = 'unset';
} }
else { else {
targetIframe.style.bottom = 'unset'; targetIframe.style.bottom = 'unset';
targetIframe.style.top = `${buttonRect.height + 5}px`; targetIframe.style.top = '0px';
} }
const buttonInRight = buttonRect.right > targetIframe.clientWidth; const buttonInRight = buttonRect.right > targetIframe.clientWidth;
@ -148,8 +158,8 @@
right: var(--${containerDiv.id}-right, 1rem); right: var(--${containerDiv.id}-right, 1rem);
left: var(--${containerDiv.id}-left, unset); left: var(--${containerDiv.id}-left, unset);
top: var(--${containerDiv.id}-top, unset); top: var(--${containerDiv.id}-top, unset);
width: var(--${containerDiv.id}-width, 50px); width: var(--${containerDiv.id}-width, 48px);
height: var(--${containerDiv.id}-height, 50px); height: var(--${containerDiv.id}-height, 48px);
border-radius: var(--${containerDiv.id}-border-radius, 25px); border-radius: var(--${containerDiv.id}-border-radius, 25px);
background-color: var(--${containerDiv.id}-bg-color, #155EEF); background-color: var(--${containerDiv.id}-bg-color, #155EEF);
box-shadow: var(--${containerDiv.id}-box-shadow, rgba(0, 0, 0, 0.2) 0px 4px 8px 0px); box-shadow: var(--${containerDiv.id}-box-shadow, rgba(0, 0, 0, 0.2) 0px 4px 8px 0px);
@ -161,7 +171,7 @@
// Create display div for the button icon // Create display div for the button icon
const displayDiv = document.createElement("div"); const displayDiv = document.createElement("div");
displayDiv.style.cssText = displayDiv.style.cssText =
"display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; z-index: 2147483647;"; "position: relative; display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; z-index: 2147483647;";
displayDiv.innerHTML = svgIcons.open; displayDiv.innerHTML = svgIcons.open;
containerDiv.appendChild(displayDiv); containerDiv.appendChild(displayDiv);
document.body.appendChild(containerDiv); document.body.appendChild(containerDiv);
@ -170,7 +180,7 @@
containerDiv.addEventListener("click", function () { containerDiv.addEventListener("click", function () {
const targetIframe = document.getElementById(iframeId); const targetIframe = document.getElementById(iframeId);
if (!targetIframe) { if (!targetIframe) {
containerDiv.appendChild(createIframe()); containerDiv.prepend(createIframe());
resetIframePosition(); resetIframePosition();
this.title = "Exit (ESC)"; this.title = "Exit (ESC)";
displayDiv.innerHTML = svgIcons.close; displayDiv.innerHTML = svgIcons.close;

View File

@ -94,6 +94,8 @@ const config = {
'chat-bubble-bg': 'var(--color-chat-bubble-bg)', 'chat-bubble-bg': 'var(--color-chat-bubble-bg)',
'chat-input-mask': 'var(--color-chat-input-mask)', 'chat-input-mask': 'var(--color-chat-input-mask)',
'workflow-process-bg': 'var(--color-workflow-process-bg)', 'workflow-process-bg': 'var(--color-workflow-process-bg)',
'workflow-run-failed-bg': 'var(--color-workflow-run-failed-bg)',
'workflow-batch-failed-bg': 'var(--color-workflow-batch-failed-bg)',
'mask-top2bottom-gray-50-to-transparent': 'var(--mask-top2bottom-gray-50-to-transparent)', 'mask-top2bottom-gray-50-to-transparent': 'var(--mask-top2bottom-gray-50-to-transparent)',
'marketplace-divider-bg': 'var(--color-marketplace-divider-bg)', 'marketplace-divider-bg': 'var(--color-marketplace-divider-bg)',
'marketplace-plugin-empty': 'var(--color-marketplace-plugin-empty)', 'marketplace-plugin-empty': 'var(--color-marketplace-plugin-empty)',

View File

@ -11,6 +11,12 @@ html[data-theme="dark"] {
--color-workflow-process-bg: linear-gradient(90deg, --color-workflow-process-bg: linear-gradient(90deg,
rgba(24, 24, 27, 0.25) 0%, rgba(24, 24, 27, 0.25) 0%,
rgba(24, 24, 27, 0.04) 100%); rgba(24, 24, 27, 0.04) 100%);
--color-workflow-run-failed-bg: linear-gradient(98deg,
rgba(240, 68, 56, 0.12) 0%,
rgba(0, 0, 0, 0) 26.01%);
--color-workflow-batch-failed-bg: linear-gradient(92deg,
rgba(240, 68, 56, 0.3) 0%,
rgba(0, 0, 0, 0) 100%);
--color-marketplace-divider-bg: linear-gradient(90deg, --color-marketplace-divider-bg: linear-gradient(90deg,
rgba(200, 206, 218, 0.14) 0%, rgba(200, 206, 218, 0.14) 0%,
rgba(0, 0, 0, 0) 100%); rgba(0, 0, 0, 0) 100%);

View File

@ -11,6 +11,12 @@ html[data-theme="light"] {
--color-workflow-process-bg: linear-gradient(90deg, --color-workflow-process-bg: linear-gradient(90deg,
rgba(200, 206, 218, 0.2) 0%, rgba(200, 206, 218, 0.2) 0%,
rgba(200, 206, 218, 0.04) 100%); rgba(200, 206, 218, 0.04) 100%);
--color-workflow-run-failed-bg: linear-gradient(98deg,
rgba(240, 68, 56, 0.10) 0%,
rgba(255, 255, 255, 0) 26.01%);
--color-workflow-batch-failed-bg: linear-gradient(92deg,
rgba(240, 68, 56, 0.25) 0%,
rgba(255, 255, 255, 0) 100%);
--color-marketplace-divider-bg: linear-gradient(90deg, --color-marketplace-divider-bg: linear-gradient(90deg,
rgba(16, 24, 40, 0.08) 0%, rgba(16, 24, 40, 0.08) 0%,
rgba(255, 255, 255, 0) 100%); rgba(255, 255, 255, 0) 100%);

View File

@ -0,0 +1,44 @@
html[data-theme="dark"] {
--color-prettylights-syntax-comment: #6e7781;
--color-prettylights-syntax-constant: #0550ae;
--color-prettylights-syntax-entity: #8250df;
--color-prettylights-syntax-storage-modifier-import: #24292f;
--color-prettylights-syntax-entity-tag: #116329;
--color-prettylights-syntax-keyword: #cf222e;
--color-prettylights-syntax-string: #0a3069;
--color-prettylights-syntax-variable: #953800;
--color-prettylights-syntax-brackethighlighter-unmatched: #82071e;
--color-prettylights-syntax-invalid-illegal-text: #f6f8fa;
--color-prettylights-syntax-invalid-illegal-bg: #82071e;
--color-prettylights-syntax-carriage-return-text: #f6f8fa;
--color-prettylights-syntax-carriage-return-bg: #cf222e;
--color-prettylights-syntax-string-regexp: #116329;
--color-prettylights-syntax-markup-list: #3b2300;
--color-prettylights-syntax-markup-heading: #0550ae;
--color-prettylights-syntax-markup-italic: #24292f;
--color-prettylights-syntax-markup-bold: #24292f;
--color-prettylights-syntax-markup-deleted-text: #82071e;
--color-prettylights-syntax-markup-deleted-bg: #ffebe9;
--color-prettylights-syntax-markup-inserted-text: #116329;
--color-prettylights-syntax-markup-inserted-bg: #dafbe1;
--color-prettylights-syntax-markup-changed-text: #953800;
--color-prettylights-syntax-markup-changed-bg: #ffd8b5;
--color-prettylights-syntax-markup-ignored-text: #eaeef2;
--color-prettylights-syntax-markup-ignored-bg: #0550ae;
--color-prettylights-syntax-meta-diff-range: #8250df;
--color-prettylights-syntax-brackethighlighter-angle: #57606a;
--color-prettylights-syntax-sublimelinter-gutter-mark: #8c959f;
--color-prettylights-syntax-constant-other-reference-link: #0a3069;
--color-fg-default: #24292f;
--color-fg-muted: #57606a;
--color-fg-subtle: #6e7781;
--color-canvas-default: transparent;
--color-canvas-subtle: #f6f8fa;
--color-border-default: #d0d7de;
--color-border-muted: hsla(210, 18%, 87%, 1);
--color-neutral-muted: rgba(175, 184, 193, 0.2);
--color-accent-fg: #0969da;
--color-accent-emphasis: #0969da;
--color-attention-subtle: #fff8c5;
--color-danger-fg: #cf222e;
}

View File

@ -0,0 +1,44 @@
html[data-theme="light"] {
--color-prettylights-syntax-comment: #6e7781;
--color-prettylights-syntax-constant: #0550ae;
--color-prettylights-syntax-entity: #8250df;
--color-prettylights-syntax-storage-modifier-import: #24292f;
--color-prettylights-syntax-entity-tag: #116329;
--color-prettylights-syntax-keyword: #cf222e;
--color-prettylights-syntax-string: #0a3069;
--color-prettylights-syntax-variable: #953800;
--color-prettylights-syntax-brackethighlighter-unmatched: #82071e;
--color-prettylights-syntax-invalid-illegal-text: #f6f8fa;
--color-prettylights-syntax-invalid-illegal-bg: #82071e;
--color-prettylights-syntax-carriage-return-text: #f6f8fa;
--color-prettylights-syntax-carriage-return-bg: #cf222e;
--color-prettylights-syntax-string-regexp: #116329;
--color-prettylights-syntax-markup-list: #3b2300;
--color-prettylights-syntax-markup-heading: #0550ae;
--color-prettylights-syntax-markup-italic: #24292f;
--color-prettylights-syntax-markup-bold: #24292f;
--color-prettylights-syntax-markup-deleted-text: #82071e;
--color-prettylights-syntax-markup-deleted-bg: #ffebe9;
--color-prettylights-syntax-markup-inserted-text: #116329;
--color-prettylights-syntax-markup-inserted-bg: #dafbe1;
--color-prettylights-syntax-markup-changed-text: #953800;
--color-prettylights-syntax-markup-changed-bg: #ffd8b5;
--color-prettylights-syntax-markup-ignored-text: #eaeef2;
--color-prettylights-syntax-markup-ignored-bg: #0550ae;
--color-prettylights-syntax-meta-diff-range: #8250df;
--color-prettylights-syntax-brackethighlighter-angle: #57606a;
--color-prettylights-syntax-sublimelinter-gutter-mark: #8c959f;
--color-prettylights-syntax-constant-other-reference-link: #0a3069;
--color-fg-default: #24292f;
--color-fg-muted: #57606a;
--color-fg-subtle: #6e7781;
--color-canvas-default: transparent;
--color-canvas-subtle: #f6f8fa;
--color-border-default: #d0d7de;
--color-border-muted: hsla(210, 18%, 87%, 1);
--color-neutral-muted: rgba(175, 184, 193, 0.2);
--color-accent-fg: #0969da;
--color-accent-emphasis: #0969da;
--color-attention-subtle: #fff8c5;
--color-danger-fg: #cf222e;
}