Feat: dark mode for logs and annotations (#11575)
This commit is contained in:
@@ -5,9 +5,8 @@ import useSWR from 'swr'
|
||||
import {
|
||||
HandThumbDownIcon,
|
||||
HandThumbUpIcon,
|
||||
XMarkIcon,
|
||||
} from '@heroicons/react/24/outline'
|
||||
import { RiEditFill, RiQuestionLine } from '@remixicon/react'
|
||||
import { RiCloseLine, RiEditFill } from '@remixicon/react'
|
||||
import { get } from 'lodash-es'
|
||||
import InfiniteScroll from 'react-infinite-scroll-component'
|
||||
import dayjs from 'dayjs'
|
||||
@@ -18,20 +17,16 @@ import { useShallow } from 'zustand/react/shallow'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { ChatItemInTree } from '../../base/chat/types'
|
||||
import VarPanel from './var-panel'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { FeedbackFunc, FeedbackType, IChatItem, SubmitAnnotationFunc } from '@/app/components/base/chat/chat/type'
|
||||
import type { Annotation, ChatConversationGeneralDetail, ChatConversationsResponse, ChatMessage, ChatMessagesRequest, CompletionConversationGeneralDetail, CompletionConversationsResponse, LogAnnotation } from '@/models/log'
|
||||
import type { App } from '@/types/app'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Drawer from '@/app/components/base/drawer'
|
||||
import Popover from '@/app/components/base/popover'
|
||||
import Chat from '@/app/components/base/chat/chat'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { fetchChatConversationDetail, fetchChatMessages, fetchCompletionConversationDetail, updateLogMessageAnnotations, updateLogMessageFeedbacks } from '@/service/log'
|
||||
import { TONE_LIST } from '@/config'
|
||||
import ModelIcon from '@/app/components/header/account-setting/model-provider-page/model-icon'
|
||||
import { useTextGenerationCurrentProviderAndModelAndModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import ModelName from '@/app/components/header/account-setting/model-provider-page/model-name'
|
||||
import ModelInfo from '@/app/components/app/log/model-info'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import TextGeneration from '@/app/components/app/text-generate/item'
|
||||
import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils'
|
||||
@@ -44,6 +39,7 @@ import Tooltip from '@/app/components/base/tooltip'
|
||||
import { CopyIcon } from '@/app/components/base/copy-icon'
|
||||
import { buildChatItemTree, getThreadMessages } from '@/app/components/base/chat/utils'
|
||||
import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(timezone)
|
||||
@@ -75,15 +71,6 @@ const HandThumbIconWithCount: FC<{ count: number; iconType: 'up' | 'down' }> = (
|
||||
</div>
|
||||
}
|
||||
|
||||
const PARAM_MAP = {
|
||||
temperature: 'Temperature',
|
||||
top_p: 'Top P',
|
||||
presence_penalty: 'Presence Penalty',
|
||||
max_tokens: 'Max Token',
|
||||
stop: 'Stop',
|
||||
frequency_penalty: 'Frequency Penalty',
|
||||
}
|
||||
|
||||
const getFormattedChatList = (messages: ChatMessage[], conversationId: string, timezone: string, format: string) => {
|
||||
const newChatList: IChatItem[] = []
|
||||
messages.forEach((item: ChatMessage) => {
|
||||
@@ -156,9 +143,6 @@ const getFormattedChatList = (messages: ChatMessage[], conversationId: string, t
|
||||
return newChatList
|
||||
}
|
||||
|
||||
// const displayedParams = CompletionParams.slice(0, -2)
|
||||
const validatedParams = ['temperature', 'top_p', 'presence_penalty', 'frequency_penalty']
|
||||
|
||||
type IDetailPanel = {
|
||||
detail: any
|
||||
onFeedback: FeedbackFunc
|
||||
@@ -315,22 +299,6 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||
const isChatMode = appDetail?.mode !== 'completion'
|
||||
const isAdvanced = appDetail?.mode === 'advanced-chat'
|
||||
|
||||
const targetTone = TONE_LIST.find((item: any) => {
|
||||
let res = true
|
||||
validatedParams.forEach((param) => {
|
||||
res = item.config?.[param] === detail?.model_config.model?.completion_params?.[param]
|
||||
})
|
||||
return res
|
||||
})?.name ?? 'custom'
|
||||
|
||||
const modelName = (detail.model_config as any).model?.name
|
||||
const provideName = (detail.model_config as any).model?.provider as any
|
||||
const {
|
||||
currentModel,
|
||||
currentProvider,
|
||||
} = useTextGenerationCurrentProviderAndModelAndModelList(
|
||||
{ provider: provideName, model: modelName },
|
||||
)
|
||||
const varList = (detail.model_config as any).user_input_form?.map((item: any) => {
|
||||
const itemContent = item[Object.keys(item)[0]]
|
||||
return {
|
||||
@@ -342,18 +310,6 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||
? detail.message.message_files.map((item: any) => item.url)
|
||||
: []
|
||||
|
||||
const getParamValue = (param: string) => {
|
||||
const value = detail?.model_config.model?.completion_params?.[param] || '-'
|
||||
if (param === 'stop') {
|
||||
if (Array.isArray(value))
|
||||
return value.join(',')
|
||||
else
|
||||
return '-'
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
const [width, setWidth] = useState(0)
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
|
||||
@@ -367,162 +323,71 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div ref={ref} className='rounded-xl border-[0.5px] border-gray-200 h-full flex flex-col overflow-auto'>
|
||||
<div ref={ref} className='rounded-xl border-[0.5px] border-components-panel-border h-full flex flex-col'>
|
||||
{/* Panel Header */}
|
||||
<div className='border-b border-gray-100 py-4 px-6 flex items-center justify-between bg-components-panel-bg'>
|
||||
<div>
|
||||
<div className='text-gray-500 text-[10px] leading-[14px]'>{isChatMode ? t('appLog.detail.conversationId') : t('appLog.detail.time')}</div>
|
||||
<div className='shrink-0 pl-4 pt-3 pr-3 pb-2 flex items-center gap-2 bg-components-panel-bg rounded-t-xl'>
|
||||
<div className='shrink-0'>
|
||||
<div className='mb-0.5 text-text-primary system-xs-semibold-uppercase'>{isChatMode ? t('appLog.detail.conversationId') : t('appLog.detail.time')}</div>
|
||||
{isChatMode && (
|
||||
<div className='flex items-center text-gray-700 text-[13px] leading-[18px]'>
|
||||
<div className='flex items-center text-text-secondary system-2xs-regular-uppercase'>
|
||||
<Tooltip
|
||||
popupContent={detail.id}
|
||||
>
|
||||
<div className='max-w-[105px] truncate'>{detail.id}</div>
|
||||
<div className='truncate'>{detail.id}</div>
|
||||
</Tooltip>
|
||||
<CopyIcon content={detail.id} />
|
||||
</div>
|
||||
)}
|
||||
{!isChatMode && (
|
||||
<div className='text-gray-700 text-[13px] leading-[18px]'>{formatTime(detail.created_at, t('appLog.dateTimeFormat') as string)}</div>
|
||||
<div className='text-text-secondary system-2xs-regular-uppercase'>{formatTime(detail.created_at, t('appLog.dateTimeFormat') as string)}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='flex items-center flex-wrap gap-y-1 justify-end'>
|
||||
{!isAdvanced && (
|
||||
<>
|
||||
<div
|
||||
className={cn('mr-2 flex items-center border h-8 px-2 space-x-2 rounded-lg bg-indigo-25 border-[#2A87F5]')}
|
||||
>
|
||||
<ModelIcon
|
||||
className='!w-5 !h-5'
|
||||
provider={currentProvider}
|
||||
modelName={currentModel?.model}
|
||||
/>
|
||||
<ModelName
|
||||
modelItem={currentModel!}
|
||||
showMode
|
||||
/>
|
||||
</div>
|
||||
<Popover
|
||||
position='br'
|
||||
className='!w-[280px]'
|
||||
btnClassName='mr-4 !bg-gray-50 !py-1.5 !px-2.5 border-none font-normal'
|
||||
btnElement={<>
|
||||
<span className='text-[13px]'>{targetTone}</span>
|
||||
<RiQuestionLine className='h-4 w-4 text-gray-800 ml-1.5' />
|
||||
</>}
|
||||
htmlContent={<div className='w-[280px]'>
|
||||
<div className='flex justify-between py-2 px-4 font-medium text-sm text-gray-700'>
|
||||
<span>Tone of responses</span>
|
||||
<div>{targetTone}</div>
|
||||
</div>
|
||||
{['temperature', 'top_p', 'presence_penalty', 'max_tokens', 'stop'].map((param: string, index: number) => {
|
||||
return <div className='flex justify-between py-2 px-4 bg-gray-50' key={index}>
|
||||
<span className='text-xs text-gray-700'>{PARAM_MAP[param as keyof typeof PARAM_MAP]}</span>
|
||||
<span className='text-gray-800 font-medium text-xs'>{getParamValue(param)}</span>
|
||||
</div>
|
||||
})}
|
||||
</div>}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div className='w-6 h-6 rounded-lg flex items-center justify-center hover:cursor-pointer hover:bg-gray-100'>
|
||||
<XMarkIcon className='w-4 h-4 text-gray-500' onClick={onClose} />
|
||||
</div>
|
||||
<div className='grow flex items-center flex-wrap gap-y-1 justify-end'>
|
||||
{!isAdvanced && <ModelInfo model={detail.model_config.model} />}
|
||||
</div>
|
||||
|
||||
<ActionButton size='l' onClick={onClose}>
|
||||
<RiCloseLine className='w-4 h-4 text-text-tertiary' />
|
||||
</ActionButton>
|
||||
</div>
|
||||
{/* Panel Body */}
|
||||
{(varList.length > 0 || (!isChatMode && message_files.length > 0)) && (
|
||||
<div className='px-6 pt-4 pb-2'>
|
||||
<VarPanel
|
||||
varList={varList}
|
||||
message_files={message_files}
|
||||
/>
|
||||
<div className='shrink-0 pt-1 px-1'>
|
||||
<div className='p-3 pb-2 rounded-t-xl bg-background-section-burn'>
|
||||
{(varList.length > 0 || (!isChatMode && message_files.length > 0)) && (
|
||||
<VarPanel
|
||||
varList={varList}
|
||||
message_files={message_files}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isChatMode
|
||||
? <div className="px-6 py-4">
|
||||
<div className='flex h-[18px] items-center space-x-3'>
|
||||
<div className='leading-[18px] text-xs font-semibold text-gray-500 uppercase'>{t('appLog.table.header.output')}</div>
|
||||
<div className='grow h-[1px]' style={{
|
||||
background: 'linear-gradient(270deg, rgba(243, 244, 246, 0) 0%, rgb(243, 244, 246) 100%)',
|
||||
}}></div>
|
||||
</div>
|
||||
<TextGeneration
|
||||
className='mt-2'
|
||||
content={detail.message.answer}
|
||||
messageId={detail.message.id}
|
||||
isError={false}
|
||||
onRetry={() => { }}
|
||||
isInstalledApp={false}
|
||||
supportFeedback
|
||||
feedback={detail.message.feedbacks.find((item: any) => item.from_source === 'admin')}
|
||||
onFeedback={feedback => onFeedback(detail.message.id, feedback)}
|
||||
supportAnnotation
|
||||
isShowTextToSpeech
|
||||
appId={appDetail?.id}
|
||||
varList={varList}
|
||||
siteInfo={null}
|
||||
/>
|
||||
</div>
|
||||
: threadChatItems.length < 8
|
||||
? <div className="pt-4 mb-4">
|
||||
<Chat
|
||||
config={{
|
||||
appId: appDetail?.id,
|
||||
text_to_speech: {
|
||||
enabled: true,
|
||||
},
|
||||
supportAnnotation: true,
|
||||
annotation_reply: {
|
||||
enabled: true,
|
||||
},
|
||||
supportFeedback: true,
|
||||
} as any}
|
||||
chatList={threadChatItems}
|
||||
onAnnotationAdded={handleAnnotationAdded}
|
||||
onAnnotationEdited={handleAnnotationEdited}
|
||||
onAnnotationRemoved={handleAnnotationRemoved}
|
||||
onFeedback={onFeedback}
|
||||
noChatInput
|
||||
showPromptLog
|
||||
hideProcessDetail
|
||||
chatContainerInnerClassName='px-6'
|
||||
switchSibling={switchSibling}
|
||||
</div>
|
||||
<div className='grow mx-1 mb-1 bg-background-section-burn rounded-b-xl overflow-auto'>
|
||||
{!isChatMode
|
||||
? <div className="px-6 py-4">
|
||||
<div className='flex h-[18px] items-center space-x-3'>
|
||||
<div className='text-text-tertiary system-xs-semibold-uppercase'>{t('appLog.table.header.output')}</div>
|
||||
<div className='grow h-[1px]' style={{
|
||||
background: 'linear-gradient(270deg, rgba(243, 244, 246, 0) 0%, rgb(243, 244, 246) 100%)',
|
||||
}}></div>
|
||||
</div>
|
||||
<TextGeneration
|
||||
className='mt-2'
|
||||
content={detail.message.answer}
|
||||
messageId={detail.message.id}
|
||||
isError={false}
|
||||
onRetry={() => { }}
|
||||
isInstalledApp={false}
|
||||
supportFeedback
|
||||
feedback={detail.message.feedbacks.find((item: any) => item.from_source === 'admin')}
|
||||
onFeedback={feedback => onFeedback(detail.message.id, feedback)}
|
||||
supportAnnotation
|
||||
isShowTextToSpeech
|
||||
appId={appDetail?.id}
|
||||
varList={varList}
|
||||
siteInfo={null}
|
||||
/>
|
||||
</div>
|
||||
: <div
|
||||
className="py-4"
|
||||
id="scrollableDiv"
|
||||
style={{
|
||||
height: 1000, // Specify a value
|
||||
overflow: 'auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'column-reverse',
|
||||
}}>
|
||||
{/* Put the scroll bar always on the bottom */}
|
||||
<InfiniteScroll
|
||||
scrollableTarget="scrollableDiv"
|
||||
dataLength={threadChatItems.length}
|
||||
next={fetchData}
|
||||
hasMore={hasMore}
|
||||
loader={<div className='text-center text-gray-400 text-xs'>{t('appLog.detail.loading')}...</div>}
|
||||
// endMessage={<div className='text-center'>Nothing more to show</div>}
|
||||
// below props only if you need pull down functionality
|
||||
refreshFunction={fetchData}
|
||||
pullDownToRefresh
|
||||
pullDownToRefreshThreshold={50}
|
||||
// pullDownToRefreshContent={
|
||||
// <div className='text-center'>Pull down to refresh</div>
|
||||
// }
|
||||
// releaseToRefreshContent={
|
||||
// <div className='text-center'>Release to refresh</div>
|
||||
// }
|
||||
// To put endMessage and loader to the top.
|
||||
style={{ display: 'flex', flexDirection: 'column-reverse' }}
|
||||
inverse={true}
|
||||
>
|
||||
: threadChatItems.length < 8
|
||||
? <div className="pt-4 mb-4">
|
||||
<Chat
|
||||
config={{
|
||||
appId: appDetail?.id,
|
||||
@@ -543,12 +408,68 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||
noChatInput
|
||||
showPromptLog
|
||||
hideProcessDetail
|
||||
chatContainerInnerClassName='px-6'
|
||||
chatContainerInnerClassName='px-3'
|
||||
switchSibling={switchSibling}
|
||||
/>
|
||||
</InfiniteScroll>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
: <div
|
||||
className="py-4"
|
||||
id="scrollableDiv"
|
||||
style={{
|
||||
height: 1000, // Specify a value
|
||||
overflow: 'auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'column-reverse',
|
||||
}}>
|
||||
{/* Put the scroll bar always on the bottom */}
|
||||
<InfiniteScroll
|
||||
scrollableTarget="scrollableDiv"
|
||||
dataLength={threadChatItems.length}
|
||||
next={fetchData}
|
||||
hasMore={hasMore}
|
||||
loader={<div className='text-center text-text-tertiary system-xs-regular'>{t('appLog.detail.loading')}...</div>}
|
||||
// endMessage={<div className='text-center'>Nothing more to show</div>}
|
||||
// below props only if you need pull down functionality
|
||||
refreshFunction={fetchData}
|
||||
pullDownToRefresh
|
||||
pullDownToRefreshThreshold={50}
|
||||
// pullDownToRefreshContent={
|
||||
// <div className='text-center'>Pull down to refresh</div>
|
||||
// }
|
||||
// releaseToRefreshContent={
|
||||
// <div className='text-center'>Release to refresh</div>
|
||||
// }
|
||||
// To put endMessage and loader to the top.
|
||||
style={{ display: 'flex', flexDirection: 'column-reverse' }}
|
||||
inverse={true}
|
||||
>
|
||||
<Chat
|
||||
config={{
|
||||
appId: appDetail?.id,
|
||||
text_to_speech: {
|
||||
enabled: true,
|
||||
},
|
||||
supportAnnotation: true,
|
||||
annotation_reply: {
|
||||
enabled: true,
|
||||
},
|
||||
supportFeedback: true,
|
||||
} as any}
|
||||
chatList={threadChatItems}
|
||||
onAnnotationAdded={handleAnnotationAdded}
|
||||
onAnnotationEdited={handleAnnotationEdited}
|
||||
onAnnotationRemoved={handleAnnotationRemoved}
|
||||
onFeedback={onFeedback}
|
||||
noChatInput
|
||||
showPromptLog
|
||||
hideProcessDetail
|
||||
chatContainerInnerClassName='px-3'
|
||||
switchSibling={switchSibling}
|
||||
/>
|
||||
</InfiniteScroll>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
{showMessageLogModal && (
|
||||
<MessageLogModal
|
||||
width={width}
|
||||
@@ -780,7 +701,7 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh })
|
||||
onClose={onCloseDrawer}
|
||||
mask={isMobile}
|
||||
footer={null}
|
||||
panelClassname='mt-16 mx-2 sm:mr-2 mb-4 !p-0 !max-w-[640px] rounded-xl bg-background-gradient-bg-fill-chat-bg-1'
|
||||
panelClassname='mt-16 mx-2 sm:mr-2 mb-4 !p-0 !max-w-[640px] rounded-xl bg-components-panel-bg'
|
||||
>
|
||||
<DrawerContext.Provider value={{
|
||||
onClose: onCloseDrawer,
|
||||
|
||||
107
web/app/components/app/log/model-info.tsx
Normal file
107
web/app/components/app/log/model-info.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiInformation2Line,
|
||||
} from '@remixicon/react'
|
||||
import ModelIcon from '@/app/components/header/account-setting/model-provider-page/model-icon'
|
||||
import ModelName from '@/app/components/header/account-setting/model-provider-page/model-name'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import { useTextGenerationCurrentProviderAndModelAndModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
const PARAM_MAP = {
|
||||
temperature: 'Temperature',
|
||||
top_p: 'Top P',
|
||||
presence_penalty: 'Presence Penalty',
|
||||
max_tokens: 'Max Token',
|
||||
stop: 'Stop',
|
||||
frequency_penalty: 'Frequency Penalty',
|
||||
}
|
||||
|
||||
type Props = {
|
||||
model: any
|
||||
}
|
||||
|
||||
const ModelInfo: FC<Props> = ({
|
||||
model,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const modelName = model.name
|
||||
const provideName = model.provider as any
|
||||
const {
|
||||
currentModel,
|
||||
currentProvider,
|
||||
} = useTextGenerationCurrentProviderAndModelAndModelList(
|
||||
{ provider: provideName, model: modelName },
|
||||
)
|
||||
|
||||
const [open, setOpen] = React.useState(false)
|
||||
|
||||
const getParamValue = (param: string) => {
|
||||
const value = model.completion_params?.[param] || '-'
|
||||
if (param === 'stop') {
|
||||
if (Array.isArray(value))
|
||||
return value.join(',')
|
||||
else
|
||||
return '-'
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('flex items-center rounded-lg')}>
|
||||
<div className='shrink-0 flex items-center gap-1 mr-px h-8 pl-1.5 pr-2 rounded-l-lg bg-components-input-bg-normal'>
|
||||
<ModelIcon
|
||||
className='!w-5 !h-5'
|
||||
provider={currentProvider}
|
||||
modelName={currentModel?.model}
|
||||
/>
|
||||
<ModelName
|
||||
modelItem={currentModel!}
|
||||
showMode
|
||||
/>
|
||||
</div>
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-end'
|
||||
offset={4}
|
||||
>
|
||||
<div className='relative'>
|
||||
<PortalToFollowElemTrigger
|
||||
onClick={() => setOpen(v => !v)}
|
||||
className='block'
|
||||
>
|
||||
<div className={cn(
|
||||
'p-2 rounded-r-lg bg-components-button-tertiary-bg hover:bg-components-button-tertiary-bg-hover cursor-pointer',
|
||||
open && 'bg-components-button-tertiary-bg-hover',
|
||||
)}>
|
||||
<RiInformation2Line className='h-4 w-4 text-text-tertiary' />
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[1002]'>
|
||||
<div className='relative w-[280px] pt-3 px-4 pb-2 bg-components-panel-bg rounded-2xl border-[0.5px] border-components-panel-border shadow-xl overflow-hidden'>
|
||||
<div className='mb-1 h-6 text-text-secondary system-sm-semibold-uppercase'>{t('appLog.detail.modelParams')}</div>
|
||||
<div className='py-1'>
|
||||
{['temperature', 'top_p', 'presence_penalty', 'max_tokens', 'stop'].map((param: string, index: number) => {
|
||||
return <div className='flex justify-between py-1.5' key={index}>
|
||||
<span className='text-text-tertiary system-xs-medium-uppercase'>{PARAM_MAP[param as keyof typeof PARAM_MAP]}</span>
|
||||
<span className='text-text-secondary system-xs-medium-uppercase'>{getParamValue(param)}</span>
|
||||
</div>
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</div>
|
||||
</PortalToFollowElem>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(ModelInfo)
|
||||
@@ -7,7 +7,9 @@ import {
|
||||
RiArrowDownSLine,
|
||||
RiArrowRightSLine,
|
||||
} from '@remixicon/react'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import ImagePreview from '@/app/components/base/image-uploader/image-preview'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
varList: { label: string; value: string }[]
|
||||
@@ -23,34 +25,35 @@ const VarPanel: FC<Props> = ({
|
||||
const [imagePreviewUrl, setImagePreviewUrl] = useState('')
|
||||
|
||||
return (
|
||||
<div className='rounded-xl border border-color-indigo-100 bg-indigo-25'>
|
||||
<div className='rounded-[10px] border border-divider-subtle bg-chat-bubble-bg'>
|
||||
<div
|
||||
className='flex items-center h-6 pl-2 py-6 space-x-1 cursor-pointer'
|
||||
className={cn('flex items-center gap-1 px-3 pt-2.5 pb-2 border-b border-divider-subtle text-text-secondary cursor-pointer', isCollapse && 'pb-2.5 border-0')}
|
||||
onClick={toggleCollapse}
|
||||
>
|
||||
<Variable02 className='w-4 h-4' />
|
||||
<div className='grow system-md-medium'>{t('appLog.detail.variables')}</div>
|
||||
{
|
||||
isCollapse
|
||||
? <RiArrowRightSLine className='w-3 h-3 text-gray-300' />
|
||||
: <RiArrowDownSLine className='w-3 h-3 text-gray-300' />
|
||||
? <RiArrowRightSLine className='w-4 h-4' />
|
||||
: <RiArrowDownSLine className='w-4 h-4' />
|
||||
}
|
||||
<div className='text-sm font-semibold text-indigo-800 uppercase'>{t('appLog.detail.variables')}</div>
|
||||
</div>
|
||||
{!isCollapse && (
|
||||
<div className='px-6 pb-3'>
|
||||
<div className='p-3 flex flex-col gap-2'>
|
||||
{varList.map(({ label, value }, index) => (
|
||||
<div key={index} className='flex py-2 leading-[18px] text-[13px]'>
|
||||
<div className='shrink-0 w-[128px] flex text-primary-600'>
|
||||
<div key={index} className='flex py-2 system-xs-medium'>
|
||||
<div className='shrink-0 w-[128px] flex text-text-accent'>
|
||||
<span className='shrink-0 opacity-60'>{'{{'}</span>
|
||||
<span className='truncate'>{label}</span>
|
||||
<span className='shrink-0 opacity-60'>{'}}'}</span>
|
||||
</div>
|
||||
<div className='pl-2.5 whitespace-pre-wrap'>{value}</div>
|
||||
<div className='pl-2.5 whitespace-pre-wrap text-text-secondary'>{value}</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{message_files.length > 0 && (
|
||||
<div className='mt-1 flex py-2'>
|
||||
<div className='shrink-0 w-[128px] leading-[18px] text-[13px] font-medium text-gray-700'>{t('appLog.detail.uploadImages')}</div>
|
||||
<div className='shrink-0 w-[128px] system-xs-medium text-text-tertiary'>{t('appLog.detail.uploadImages')}</div>
|
||||
<div className="flex space-x-2">
|
||||
{message_files.map((url, index) => (
|
||||
<div
|
||||
@@ -69,6 +72,7 @@ const VarPanel: FC<Props> = ({
|
||||
imagePreviewUrl && (
|
||||
<ImagePreview
|
||||
url={imagePreviewUrl}
|
||||
title={imagePreviewUrl}
|
||||
onCancel={() => setImagePreviewUrl('')}
|
||||
/>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user