Feat: web app dark mode (#14732)
parent
e53052ab7a
commit
d0d0bf570e
@ -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)
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
@ -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
|
|
||||||
@ -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
|
|
||||||
@ -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)
|
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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)
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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)
|
||||||
@ -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)
|
|
||||||
@ -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
|
|
||||||
@ -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
|
|
||||||
@ -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)
|
|
||||||
@ -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)
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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)
|
|
||||||
@ -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 |
@ -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"
|
||||||
|
}
|
||||||
@ -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
|
||||||
@ -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'
|
||||||
|
|||||||
@ -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
|
||||||
@ -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
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
.itemActive::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
bottom: -1px;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 2px;
|
|
||||||
background-color: #155EEF;
|
|
||||||
}
|
|
||||||
@ -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
|
||||||
@ -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)
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -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
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue