Initial commit

This commit is contained in:
饺子大魔王
2025-04-11 08:14:27 +08:00
commit 626af60456
1540 changed files with 110511 additions and 0 deletions
@@ -0,0 +1,41 @@
@tailwind components;
@layer components {
.action-btn {
@apply inline-flex justify-center items-center cursor-pointer text-text-tertiary hover:text-text-secondary hover:bg-state-base-hover
}
.action-btn-disabled {
@apply cursor-not-allowed
}
.action-btn-xl {
@apply p-2 w-9 h-9 rounded-lg
}
.action-btn-l {
@apply p-1.5 w-8 h-8 rounded-lg
}
/* m is for the regular button */
.action-btn-m {
@apply p-0.5 w-6 h-6 rounded-lg
}
.action-btn-xs {
@apply p-0 w-4 h-4 rounded
}
.action-btn.action-btn-active {
@apply text-text-accent bg-state-accent-active hover:bg-state-accent-active-alt
}
.action-btn.action-btn-disabled {
@apply text-text-disabled
}
.action-btn.action-btn-destructive {
@apply text-text-destructive bg-state-destructive-hover
}
}
@@ -0,0 +1,70 @@
import type { CSSProperties } from 'react'
import React from 'react'
import { type VariantProps, cva } from 'class-variance-authority'
import classNames from '@/utils/classnames'
enum ActionButtonState {
Destructive = 'destructive',
Active = 'active',
Disabled = 'disabled',
Default = '',
}
const actionButtonVariants = cva(
'action-btn',
{
variants: {
size: {
xs: 'action-btn-xs',
m: 'action-btn-m',
l: 'action-btn-l',
xl: 'action-btn-xl',
},
},
defaultVariants: {
size: 'm',
},
},
)
export type ActionButtonProps = {
size?: 'xs' | 's' | 'm' | 'l' | 'xl'
state?: ActionButtonState
styleCss?: CSSProperties
} & React.ButtonHTMLAttributes<HTMLButtonElement> & VariantProps<typeof actionButtonVariants>
function getActionButtonState(state: ActionButtonState) {
switch (state) {
case ActionButtonState.Destructive:
return 'action-btn-destructive'
case ActionButtonState.Active:
return 'action-btn-active'
case ActionButtonState.Disabled:
return 'action-btn-disabled'
default:
return ''
}
}
const ActionButton = React.forwardRef<HTMLButtonElement, ActionButtonProps>(
({ className, size, state = ActionButtonState.Default, styleCss, children, ...props }, ref) => {
return (
<button
type='button'
className={classNames(
actionButtonVariants({ className, size }),
getActionButtonState(state),
)}
ref={ref}
style={styleCss}
{...props}
>
{children}
</button>
)
},
)
ActionButton.displayName = 'ActionButton'
export default ActionButton
export { ActionButton, ActionButtonState, actionButtonVariants }
@@ -0,0 +1,137 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { useContext } from 'use-context-selector'
import { useTranslation } from 'react-i18next'
import { flatten, uniq } from 'lodash-es'
import ResultPanel from './result'
import TracingPanel from './tracing'
import cn from '@/utils/classnames'
import { ToastContext } from '@/app/components/base/toast'
import Loading from '@/app/components/base/loading'
// import { fetchAgentLogDetail } from '@/service/log'
// TODO
const fetchAgentLogDetail = () => {
console.log('TODO MARS')
}
import type { AgentIteration, AgentLogDetailResponse } from '@/models/log'
import { useStore as useAppStore } from '@/app/components/app/store'
import type { IChatItem } from '@/app/components/chat/type'
export type AgentLogDetailProps = {
activeTab?: 'DETAIL' | 'TRACING'
conversationID: string
log: IChatItem
messageID: string
}
const AgentLogDetail: FC<AgentLogDetailProps> = ({
activeTab = 'DETAIL',
conversationID,
messageID,
log,
}) => {
const { t } = useTranslation()
const { notify } = useContext(ToastContext)
const [currentTab, setCurrentTab] = useState<string>(activeTab)
const appDetail = useAppStore(s => s.appDetail)
const [loading, setLoading] = useState<boolean>(true)
const [runDetail, setRunDetail] = useState<AgentLogDetailResponse>()
const [list, setList] = useState<AgentIteration[]>([])
const tools = useMemo(() => {
const res = uniq(flatten(runDetail?.iterations.map((iteration: any) => {
return iteration.tool_calls.map((tool: any) => tool.tool_name).filter(Boolean)
})).filter(Boolean))
return res
}, [runDetail])
const getLogDetail = useCallback(async (appID: string, conversationID: string, messageID: string) => {
try {
const res = await fetchAgentLogDetail({
appID,
params: {
conversation_id: conversationID,
message_id: messageID,
},
})
setRunDetail(res)
setList(res.iterations)
}
catch (err) {
notify({
type: 'error',
message: `${err}`,
})
}
}, [notify])
const getData = async (appID: string, conversationID: string, messageID: string) => {
setLoading(true)
await getLogDetail(appID, conversationID, messageID)
setLoading(false)
}
const switchTab = async (tab: string) => {
setCurrentTab(tab)
}
useEffect(() => {
// fetch data
if (appDetail)
getData(appDetail.id, conversationID, messageID)
}, [appDetail, conversationID, messageID])
return (
<div className='grow relative flex flex-col'>
{/* tab */}
<div className='shrink-0 flex items-center px-4 border-b-[0.5px] border-divider-regular'>
<div
className={cn(
'mr-6 py-3 border-b-2 border-transparent text-[13px] font-semibold leading-[18px] text-text-tertiary cursor-pointer',
currentTab === 'DETAIL' && '!border-[rgb(21,94,239)] text-text-secondary',
)}
onClick={() => switchTab('DETAIL')}
>{t('runLog.detail')}</div>
<div
className={cn(
'mr-6 py-3 border-b-2 border-transparent text-[13px] font-semibold leading-[18px] text-text-tertiary cursor-pointer',
currentTab === 'TRACING' && '!border-[rgb(21,94,239)] text-text-secondary',
)}
onClick={() => switchTab('TRACING')}
>{t('runLog.tracing')}</div>
</div>
{/* panel detail */}
<div className={cn('grow bg-components-panel-bg h-0 overflow-y-auto rounded-b-2xl', currentTab !== 'DETAIL' && '!bg-background-section')}>
{loading && (
<div className='flex h-full items-center justify-center bg-components-panel-bg'>
<Loading />
</div>
)}
{!loading && currentTab === 'DETAIL' && runDetail && (
<ResultPanel
inputs={log.input}
outputs={log.content}
status={runDetail.meta.status}
error={runDetail.meta.error}
elapsed_time={runDetail.meta.elapsed_time}
total_tokens={runDetail.meta.total_tokens}
created_at={runDetail.meta.start_time}
created_by={runDetail.meta.executor}
agentMode={runDetail.meta.agent_mode}
tools={tools}
iterations={runDetail.iterations.length}
/>
)}
{!loading && currentTab === 'TRACING' && (
<TracingPanel
list={list}
/>
)}
</div>
</div>
)
}
export default AgentLogDetail
@@ -0,0 +1,61 @@
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { RiCloseLine } from '@remixicon/react'
import { useEffect, useRef, useState } from 'react'
import { useClickAway } from 'ahooks'
import AgentLogDetail from './detail'
import cn from '@/utils/classnames'
import type { IChatItem } from '@/app/components/chat/type'
type AgentLogModalProps = {
currentLogItem?: IChatItem
width: number
onCancel: () => void
}
const AgentLogModal: FC<AgentLogModalProps> = ({
currentLogItem,
width,
onCancel,
}) => {
const { t } = useTranslation()
const ref = useRef(null)
const [mounted, setMounted] = useState(false)
useClickAway(() => {
if (mounted)
onCancel()
}, ref)
useEffect(() => {
setMounted(true)
}, [])
if (!currentLogItem || !currentLogItem.conversationId)
return null
return (
<div
className={cn('relative flex flex-col py-3 bg-components-panel-bg border-[0.5px] border-components-panel-border rounded-xl shadow-xl z-10')}
style={{
width: 480,
position: 'fixed',
top: 56 + 8,
left: 8 + (width - 480),
bottom: 16,
}}
ref={ref}
>
<h1 className='shrink-0 px-4 py-1 text-md font-semibold text-text-primary'>{t('appLog.runDetail.workflowTitle')}</h1>
<span className='absolute right-3 top-4 p-1 cursor-pointer z-20' onClick={onCancel}>
<RiCloseLine className='w-4 h-4 text-text-tertiary' />
</span>
<AgentLogDetail
conversationID={currentLogItem.conversationId}
messageID={currentLogItem.id}
log={currentLogItem}
/>
</div>
)
}
export default AgentLogModal
@@ -0,0 +1,51 @@
'use client'
import { useTranslation } from 'react-i18next'
import type { FC } from 'react'
import ToolCall from './tool-call'
import Divider from '@/app/components/base/divider'
import type { AgentIteration } from '@/models/log'
import cn from '@/utils/classnames'
type Props = {
isFinal: boolean
index: number
iterationInfo: AgentIteration
}
const Iteration: FC<Props> = ({ iterationInfo, isFinal, index }) => {
const { t } = useTranslation()
return (
<div className={cn('px-4 py-2')}>
<div className='flex items-center'>
{isFinal && (
<div className='shrink-0 mr-3 text-text-tertiary text-xs leading-[18px] font-semibold'>{t('appLog.agentLogDetail.finalProcessing')}</div>
)}
{!isFinal && (
<div className='shrink-0 mr-3 text-text-tertiary text-xs leading-[18px] font-semibold'>{`${t('appLog.agentLogDetail.iteration').toUpperCase()} ${index}`}</div>
)}
<Divider bgStyle='gradient' className='grow h-[1px] mx-0'/>
</div>
<ToolCall
isLLM
isFinal={isFinal}
tokens={iterationInfo.tokens}
observation={iterationInfo.tool_raw.outputs}
finalAnswer={iterationInfo.thought}
toolCall={{
status: 'success',
tool_icon: null,
}}
/>
{iterationInfo.tool_calls.map((toolCall, index) => (
<ToolCall
isLLM={false}
key={index}
toolCall={toolCall}
/>
))}
</div>
)
}
export default Iteration
@@ -0,0 +1,127 @@
'use client'
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import StatusPanel from '@/app/components/workflow/run/status'
// TODO mars
// import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
// import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
import useTimestamp from '@/hooks/use-timestamp'
// TODO MARS
type ResultPanelProps = {
status: string
elapsed_time?: number
total_tokens?: number
error?: string
inputs?: any
outputs?: any
created_by?: string
created_at: string
agentMode?: string
tools?: string[]
iterations?: number
}
const ResultPanel: FC<ResultPanelProps> = ({
elapsed_time,
total_tokens,
error,
inputs,
outputs,
created_by,
created_at,
agentMode,
tools,
iterations,
}) => {
const { t } = useTranslation()
const { formatTime } = useTimestamp()
return (
<div className='bg-components-panel-bg py-2'>
<div className='px-4 py-2'>
<StatusPanel
status='succeeded'
time={elapsed_time}
tokens={total_tokens}
error={error}
/>
</div>
<div className='px-4 py-2 flex flex-col gap-2'>
{/* <CodeEditor
readOnly
title={<div>INPUT</div>}
language={CodeLanguage.json}
value={inputs}
isJSONStringifyBeauty
/>
<CodeEditor
readOnly
title={<div>OUTPUT</div>}
language={CodeLanguage.json}
value={outputs}
isJSONStringifyBeauty
/> */}
</div>
<div className='px-4 py-2'>
<div className='h-[0.5px] bg-divider-regular opacity-5' />
</div>
<div className='px-4 py-2'>
<div className='relative'>
<div className='h-6 leading-6 text-text-tertiary text-xs font-medium'>{t('runLog.meta.title')}</div>
<div className='py-1'>
<div className='flex'>
<div className='shrink-0 w-[104px] px-2 py-[5px] text-text-tertiary text-xs leading-[18px] truncate'>{t('runLog.meta.status')}</div>
<div className='grow px-2 py-[5px] text-text-primary text-xs leading-[18px]'>
<span>SUCCESS</span>
</div>
</div>
<div className='flex'>
<div className='shrink-0 w-[104px] px-2 py-[5px] text-text-tertiary text-xs leading-[18px] truncate'>{t('runLog.meta.executor')}</div>
<div className='grow px-2 py-[5px] text-text-primary text-xs leading-[18px]'>
<span>{created_by || 'N/A'}</span>
</div>
</div>
<div className='flex'>
<div className='shrink-0 w-[104px] px-2 py-[5px] text-text-tertiary text-xs leading-[18px] truncate'>{t('runLog.meta.startTime')}</div>
<div className='grow px-2 py-[5px] text-text-primary text-xs leading-[18px]'>
<span>{formatTime(Date.parse(created_at) / 1000, t('appLog.dateTimeFormat') as string)}</span>
</div>
</div>
<div className='flex'>
<div className='shrink-0 w-[104px] px-2 py-[5px] text-text-tertiary text-xs leading-[18px] truncate'>{t('runLog.meta.time')}</div>
<div className='grow px-2 py-[5px] text-text-primary text-xs leading-[18px]'>
<span>{`${elapsed_time?.toFixed(3)}s`}</span>
</div>
</div>
<div className='flex'>
<div className='shrink-0 w-[104px] px-2 py-[5px] text-text-tertiary text-xs leading-[18px] truncate'>{t('runLog.meta.tokens')}</div>
<div className='grow px-2 py-[5px] text-text-primary text-xs leading-[18px]'>
<span>{`${total_tokens || 0} Tokens`}</span>
</div>
</div>
<div className='flex'>
<div className='shrink-0 w-[104px] px-2 py-[5px] text-text-tertiary text-xs leading-[18px] truncate'>{t('appLog.agentLogDetail.agentMode')}</div>
<div className='grow px-2 py-[5px] text-text-primary text-xs leading-[18px]'>
<span>{agentMode === 'function_call' ? t('appDebug.agent.agentModeType.functionCall') : t('appDebug.agent.agentModeType.ReACT')}</span>
</div>
</div>
<div className='flex'>
<div className='shrink-0 w-[104px] px-2 py-[5px] text-text-tertiary text-xs leading-[18px] truncate'>{t('appLog.agentLogDetail.toolUsed')}</div>
<div className='grow px-2 py-[5px] text-text-primary text-xs leading-[18px]'>
<span>{tools?.length ? tools?.join(', ') : 'Null'}</span>
</div>
</div>
<div className='flex'>
<div className='shrink-0 w-[104px] px-2 py-[5px] text-text-tertiary text-xs leading-[18px] truncate'>{t('appLog.agentLogDetail.iterations')}</div>
<div className='grow px-2 py-[5px] text-text-primary text-xs leading-[18px]'>
<span>{iterations}</span>
</div>
</div>
</div>
</div>
</div>
</div>
)
}
export default ResultPanel
@@ -0,0 +1,143 @@
'use client'
import type { FC } from 'react'
import { useState } from 'react'
import {
RiCheckboxCircleLine,
RiErrorWarningLine,
} from '@remixicon/react'
import { useContext } from 'use-context-selector'
import cn from '@/utils/classnames'
import BlockIcon from '@/app/components/workflow/block-icon'
// TODO MARS CodeEditor
// import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
// import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
import { ChevronRight } from '@/app/components/base/icons/src/vender/line/arrows'
import type { ToolCall } from '@/models/log'
import { BlockEnum } from '@/app/components/workflow/types'
import I18n from '@/context/i18n'
// TODO MARS
type Props = {
toolCall: ToolCall
isLLM: boolean
isFinal?: boolean
tokens?: number
observation?: any
finalAnswer?: any
}
const ToolCallItem: FC<Props> = ({ toolCall, isLLM = false, isFinal, tokens, observation, finalAnswer }) => {
const [collapseState, setCollapseState] = useState<boolean>(true)
const { locale } = useContext(I18n)
const toolName = isLLM ? 'LLM' : (toolCall.tool_label[locale] || toolCall.tool_label[locale.replaceAll('-', '_')])
const getTime = (time: number) => {
if (time < 1)
return `${(time * 1000).toFixed(3)} ms`
if (time > 60)
return `${Number.parseInt(Math.round(time / 60).toString())} m ${(time % 60).toFixed(3)} s`
return `${time.toFixed(3)} s`
}
const getTokenCount = (tokens: number) => {
if (tokens < 1000)
return tokens
if (tokens >= 1000 && tokens < 1000000)
return `${Number.parseFloat((tokens / 1000).toFixed(3))}K`
if (tokens >= 1000000)
return `${Number.parseFloat((tokens / 1000000).toFixed(3))}M`
}
return (
<div className={cn('py-1')}>
<div className={cn('group transition-all bg-background-default border border-components-panel-border rounded-2xl shadow-xs hover:shadow-md')}>
<div
className={cn(
'flex items-center py-3 pl-[6px] pr-3 cursor-pointer',
!collapseState && '!pb-2',
)}
onClick={() => setCollapseState(!collapseState)}
>
<ChevronRight
className={cn(
'shrink-0 w-3 h-3 mr-1 text-text-quaternary transition-all group-hover:text-text-tertiary',
!collapseState && 'rotate-90',
)}
/>
<BlockIcon className={cn('shrink-0 mr-2')} type={isLLM ? BlockEnum.LLM : BlockEnum.Tool} toolIcon={toolCall.tool_icon} />
<div className={cn(
'grow text-text-secondary text-[13px] leading-[16px] font-semibold truncate',
)} title={toolName}>{toolName}</div>
<div className='shrink-0 text-text-tertiary text-xs leading-[18px]'>
{toolCall.time_cost && (
<span>{getTime(toolCall.time_cost || 0)}</span>
)}
{isLLM && (
<span>{`${getTokenCount(tokens || 0)} tokens`}</span>
)}
</div>
{toolCall.status === 'success' && (
<RiCheckboxCircleLine className='shrink-0 ml-2 w-3.5 h-3.5 text-[#12B76A]' />
)}
{toolCall.status === 'error' && (
<RiErrorWarningLine className='shrink-0 ml-2 w-3.5 h-3.5 text-[#F04438]' />
)}
</div>
{!collapseState && (
<div className='pb-2'>
<div className={cn('px-[10px] py-1')}>
{toolCall.status === 'error' && (
<div className='px-3 py-[10px] bg-[#fef3f2] rounded-lg border-[0.5px] border-[rbga(0,0,0,0.05)] text-xs leading-[18px] text-[#d92d20] shadow-xs'>{toolCall.error}</div>
)}
</div>
{/* {toolCall.tool_input && (
<div className={cn('px-[10px] py-1')}>
<CodeEditor
readOnly
title={<div>INPUT</div>}
language={CodeLanguage.json}
value={toolCall.tool_input}
isJSONStringifyBeauty
/>
</div>
)}
{toolCall.tool_output && (
<div className={cn('px-[10px] py-1')}>
<CodeEditor
readOnly
title={<div>OUTPUT</div>}
language={CodeLanguage.json}
value={toolCall.tool_output}
isJSONStringifyBeauty
/>
</div>
)}
{isLLM && (
<div className={cn('px-[10px] py-1')}>
<CodeEditor
readOnly
title={<div>OBSERVATION</div>}
language={CodeLanguage.json}
value={observation}
isJSONStringifyBeauty
/>
</div>
)}
{isLLM && (
<div className={cn('px-[10px] py-1')}>
<CodeEditor
readOnly
title={<div>{isFinal ? 'FINAL ANSWER' : 'THOUGHT'}</div>}
language={CodeLanguage.json}
value={finalAnswer}
isJSONStringifyBeauty
/>
</div>
)} */}
</div>
)}
</div>
</div>
)
}
export default ToolCallItem
@@ -0,0 +1,25 @@
'use client'
import type { FC } from 'react'
import Iteration from './iteration'
import type { AgentIteration } from '@/models/log'
type TracingPanelProps = {
list: AgentIteration[]
}
const TracingPanel: FC<TracingPanelProps> = ({ list }) => {
return (
<div className='bg-background-section'>
{list.map((iteration, index) => (
<Iteration
key={index}
index={index + 1}
isFinal={index + 1 === list.length}
iterationInfo={iteration}
/>
))}
</div>
)
}
export default TracingPanel
+47
View File
@@ -0,0 +1,47 @@
'use client'
import type { FC } from 'react'
import { init } from 'emoji-mart'
import data from '@emoji-mart/data'
import classNames from '@/utils/classnames'
import type { AppIconType } from '@/types/app'
init({ data })
export type AnswerIconProps = {
iconType?: AppIconType | null
icon?: string | null
background?: string | null
imageUrl?: string | null
}
const AnswerIcon: FC<AnswerIconProps> = ({
iconType,
icon,
background,
imageUrl,
}) => {
const wrapperClassName = classNames(
'flex',
'items-center',
'justify-center',
'w-full',
'h-full',
'rounded-full',
'border-[0.5px]',
'border-black/5',
'text-xl',
)
const isValidImageIcon = iconType === 'image' && imageUrl
return <div
className={wrapperClassName}
style={{ background: background || '#D5F5F6' }}
>
{isValidImageIcon
? <img src={imageUrl} className="w-full h-full rounded-full" alt="answer icon" />
: (icon && icon !== '') ? <em-emoji id={icon} /> : <em-emoji id='🤖' />
}
</div>
}
export default AnswerIcon
+71
View File
@@ -0,0 +1,71 @@
'use client'
import type { FC } from 'react'
import { init } from 'emoji-mart'
import data from '@emoji-mart/data'
import { cva } from 'class-variance-authority'
import type { AppIconType } from '@/types/app'
import classNames from '@/utils/classnames'
init({ data })
export type AppIconProps = {
size?: 'xs' | 'tiny' | 'small' | 'medium' | 'large' | 'xl' | 'xxl'
rounded?: boolean
iconType?: AppIconType | null
icon?: string
background?: string | null
imageUrl?: string | null
className?: string
innerIcon?: React.ReactNode
onClick?: () => void
}
const appIconVariants = cva(
'flex items-center justify-center relative text-lg rounded-lg grow-0 shrink-0 overflow-hidden leading-none',
{
variants: {
size: {
xs: 'w-4 h-4 text-xs',
tiny: 'w-6 h-6 text-base',
small: 'w-8 h-8 text-xl',
medium: 'w-9 h-9 text-[22px]',
large: 'w-10 h-10 text-[24px]',
xl: 'w-12 h-12 text-[28px]',
xxl: 'w-14 h-14 text-[32px]',
},
rounded: {
true: 'rounded-full',
},
},
defaultVariants: {
size: 'medium',
rounded: false,
},
})
const AppIcon: FC<AppIconProps> = ({
size = 'medium',
rounded = false,
iconType,
icon,
background,
imageUrl,
className,
innerIcon,
onClick,
}) => {
const isValidImageIcon = iconType === 'image' && imageUrl
return <span
className={classNames(appIconVariants({ size, rounded }), className)}
style={{ background: isValidImageIcon ? undefined : (background || '#FFEAD5') }}
onClick={onClick}
>
{isValidImageIcon
// eslint-disable-next-line @next/next/no-img-element
? <img src={imageUrl} className="w-full h-full" alt="app icon" />
: (innerIcon || ((icon && icon !== '') ? <em-emoji id={icon} /> : <em-emoji id='🤖' />))
}
</span>
}
export default AppIcon
@@ -0,0 +1,23 @@
.appIcon {
@apply flex items-center justify-center relative w-9 h-9 text-lg rounded-lg grow-0 shrink-0;
}
.appIcon.large {
@apply w-10 h-10;
}
.appIcon.small {
@apply w-8 h-8;
}
.appIcon.tiny {
@apply w-6 h-6 text-base;
}
.appIcon.xs {
@apply w-5 h-5 text-base;
}
.appIcon.rounded {
@apply rounded-full;
}
+29
View File
@@ -0,0 +1,29 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
type IAppUnavailableProps = {
code?: number
isUnknownReason?: boolean
unknownReason?: string
}
const AppUnavailable: FC<IAppUnavailableProps> = ({
code = 404,
isUnknownReason,
unknownReason,
}) => {
const { t } = useTranslation()
return (
<div className='flex items-center justify-center w-screen h-screen'>
<h1 className='mr-5 h-[50px] leading-[50px] pr-5 text-[24px] font-medium'
style={{
borderRight: '1px solid rgba(0,0,0,.3)',
}}>{code}</h1>
<div className='text-sm'>{unknownReason || (isUnknownReason ? t('share.common.appUnknownError') : t('share.common.appUnavailable'))}</div>
</div>
)
}
export default React.memo(AppUnavailable)
@@ -0,0 +1,54 @@
import AudioPlayer from '@/app/components/base/audio-btn/audio'
declare global {
// eslint-disable-next-line ts/consistent-type-definitions
interface AudioPlayerManager {
instance: AudioPlayerManager
}
}
export class AudioPlayerManager {
private static instance: AudioPlayerManager
private audioPlayers: AudioPlayer | null = null
private msgId: string | undefined
// eslint-disable-next-line
private constructor() {
}
public static getInstance(): AudioPlayerManager {
if (!AudioPlayerManager.instance) {
AudioPlayerManager.instance = new AudioPlayerManager()
this.instance = AudioPlayerManager.instance
}
return AudioPlayerManager.instance
}
public getAudioPlayer(url: string, isPublic: boolean, id: string | undefined, msgContent: string | null | undefined, voice: string | undefined, callback: ((event: string) => {}) | null): AudioPlayer {
if (this.msgId && this.msgId === id && this.audioPlayers) {
this.audioPlayers.setCallback(callback)
return this.audioPlayers
}
else {
if (this.audioPlayers) {
try {
this.audioPlayers.pauseAudio()
this.audioPlayers.cacheBuffers = []
this.audioPlayers.sourceBuffer?.abort()
}
catch (e) {
}
}
this.msgId = id
this.audioPlayers = new AudioPlayer(url, isPublic, id, msgContent, voice, callback)
return this.audioPlayers
}
}
public resetMsgId(msgId: string) {
this.msgId = msgId
this.audioPlayers?.resetMsgId(msgId)
}
}
+252
View File
@@ -0,0 +1,252 @@
import Toast from '@/app/components/base/toast'
// TODO Mars
// import { textToAudioStream } from '@/service/share'
declare global {
// eslint-disable-next-line ts/consistent-type-definitions
interface Window {
ManagedMediaSource: any
}
}
export default class AudioPlayer {
mediaSource: MediaSource | null
audio: HTMLAudioElement
audioContext: AudioContext
sourceBuffer?: any
cacheBuffers: ArrayBuffer[] = []
pauseTimer: number | null = null
msgId: string | undefined
msgContent: string | null | undefined = null
voice: string | undefined = undefined
isLoadData = false
url: string
isPublic: boolean
callback: ((event: string) => {}) | null
constructor(streamUrl: string, isPublic: boolean, msgId: string | undefined, msgContent: string | null | undefined, voice: string | undefined, callback: ((event: string) => {}) | null) {
this.audioContext = new AudioContext()
this.msgId = msgId
this.msgContent = msgContent
this.url = streamUrl
this.isPublic = isPublic
this.voice = voice
this.callback = callback
// Compatible with iphone ios17 ManagedMediaSource
const MediaSource = window.ManagedMediaSource || window.MediaSource
if (!MediaSource) {
Toast.notify({
message: 'Your browser does not support audio streaming, if you are using an iPhone, please update to iOS 17.1 or later.',
type: 'error',
})
}
this.mediaSource = MediaSource ? new MediaSource() : null
this.audio = new Audio()
this.setCallback(callback)
if (!window.MediaSource) { // if use ManagedMediaSource
this.audio.disableRemotePlayback = true
this.audio.controls = true
}
this.audio.src = this.mediaSource ? URL.createObjectURL(this.mediaSource) : ''
this.audio.autoplay = true
const source = this.audioContext.createMediaElementSource(this.audio)
source.connect(this.audioContext.destination)
this.listenMediaSource('audio/mpeg')
}
public resetMsgId(msgId: string) {
this.msgId = msgId
}
private listenMediaSource(contentType: string) {
this.mediaSource?.addEventListener('sourceopen', () => {
if (this.sourceBuffer)
return
this.sourceBuffer = this.mediaSource?.addSourceBuffer(contentType)
})
}
public setCallback(callback: ((event: string) => {}) | null) {
this.callback = callback
if (callback) {
this.audio.addEventListener('ended', () => {
callback('ended')
}, false)
this.audio.addEventListener('paused', () => {
callback('paused')
}, true)
this.audio.addEventListener('loaded', () => {
callback('loaded')
}, true)
this.audio.addEventListener('play', () => {
callback('play')
}, true)
this.audio.addEventListener('timeupdate', () => {
callback('timeupdate')
}, true)
this.audio.addEventListener('loadeddate', () => {
callback('loadeddate')
}, true)
this.audio.addEventListener('canplay', () => {
callback('canplay')
}, true)
this.audio.addEventListener('error', () => {
callback('error')
}, true)
}
}
private async loadAudio() {
try {
const audioResponse: any = await textToAudioStream(this.url, this.isPublic, { content_type: 'audio/mpeg' }, {
message_id: this.msgId,
streaming: true,
voice: this.voice,
text: this.msgContent,
})
if (audioResponse.status !== 200) {
this.isLoadData = false
if (this.callback)
this.callback('error')
}
const reader = audioResponse.body.getReader()
while (true) {
const { value, done } = await reader.read()
if (done) {
this.receiveAudioData(value)
break
}
this.receiveAudioData(value)
}
}
catch (error) {
this.isLoadData = false
this.callback && this.callback('error')
}
}
// play audio
public playAudio() {
if (this.isLoadData) {
if (this.audioContext.state === 'suspended') {
this.audioContext.resume().then((_) => {
this.audio.play()
this.callback && this.callback('play')
})
}
else if (this.audio.ended) {
this.audio.play()
this.callback && this.callback('play')
}
if (this.callback)
this.callback('play')
}
else {
this.isLoadData = true
this.loadAudio()
}
}
private theEndOfStream() {
const endTimer = setInterval(() => {
if (!this.sourceBuffer?.updating) {
this.mediaSource?.endOfStream()
clearInterval(endTimer)
}
}, 10)
}
private finishStream() {
const timer = setInterval(() => {
if (!this.cacheBuffers.length) {
this.theEndOfStream()
clearInterval(timer)
}
if (this.cacheBuffers.length && !this.sourceBuffer?.updating) {
const arrayBuffer = this.cacheBuffers.shift()!
this.sourceBuffer?.appendBuffer(arrayBuffer)
}
}, 10)
}
public async playAudioWithAudio(audio: string, play = true) {
if (!audio || !audio.length) {
this.finishStream()
return
}
const audioContent = Buffer.from(audio, 'base64')
this.receiveAudioData(new Uint8Array(audioContent))
if (play) {
this.isLoadData = true
if (this.audio.paused) {
this.audioContext.resume().then((_) => {
this.audio.play()
this.callback && this.callback('play')
})
}
else if (this.audio.ended) {
this.audio.play()
this.callback && this.callback('play')
}
else if (this.audio.played) { /* empty */ }
else {
this.audio.play()
this.callback && this.callback('play')
}
}
}
public pauseAudio() {
this.callback && this.callback('paused')
this.audio.pause()
this.audioContext.suspend()
}
private cancer() {
}
private receiveAudioData(unit8Array: Uint8Array) {
if (!unit8Array) {
this.finishStream()
return
}
const audioData = this.byteArrayToArrayBuffer(unit8Array)
if (!audioData.byteLength) {
if (this.mediaSource?.readyState === 'open')
this.finishStream()
return
}
if (this.sourceBuffer?.updating) {
this.cacheBuffers.push(audioData)
}
else {
if (this.cacheBuffers.length && !this.sourceBuffer?.updating) {
this.cacheBuffers.push(audioData)
const cacheBuffer = this.cacheBuffers.shift()!
this.sourceBuffer?.appendBuffer(cacheBuffer)
}
else {
this.sourceBuffer?.appendBuffer(audioData)
}
}
}
private byteArrayToArrayBuffer(byteArray: Uint8Array): ArrayBuffer {
const arrayBuffer = new ArrayBuffer(byteArray.length)
const uint8Array = new Uint8Array(arrayBuffer)
uint8Array.set(byteArray)
return arrayBuffer
}
}
+110
View File
@@ -0,0 +1,110 @@
'use client'
import { useState } from 'react'
import { t } from 'i18next'
import { useParams, usePathname } from 'next/navigation'
import s from './style.module.css'
import Tooltip from '@/app/components/base/tooltip'
import Loading from '@/app/components/base/loading'
import { AudioPlayerManager } from '@/app/components/base/audio-btn/audio.player.manager'
interface AudioBtnProps {
id?: string
voice?: string
value?: string
className?: string
isAudition?: boolean
noCache?: boolean
}
type AudioState = 'initial' | 'loading' | 'playing' | 'paused' | 'ended'
const AudioBtn = ({
id,
voice,
value,
className,
isAudition,
}: 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 (
<div className={`inline-flex items-center justify-center ${(audioState === 'loading' || audioState === 'playing') ? 'mr-1' : className}`}>
<Tooltip
popupContent={tooltipContent}
>
<button
disabled={audioState === 'loading'}
className={`box-border w-6 h-6 flex items-center justify-center cursor-pointer ${isAudition ? 'p-0.5' : 'p-0 rounded-md bg-white'}`}
onClick={handleToggle}
>
{audioState === 'loading'
? (
<div className='w-full h-full rounded-md flex items-center justify-center'>
<Loading />
</div>
)
: (
<div className={`w-full h-full rounded-md flex items-center justify-center ${!isAudition ? 'hover:bg-gray-50' : 'hover:bg-gray-50'}`}>
<div className={`w-4 h-4 ${(audioState === 'playing') ? s.pauseIcon : s.playIcon}`}></div>
</div>
)}
</button>
</Tooltip>
</div>
)
}
export default AudioBtn
@@ -0,0 +1,10 @@
.playIcon {
background-image: url(~@/app/components/develop/secret-key/assets/play.svg);
background-position: center;
background-repeat: no-repeat;
}
.pauseIcon {
background-image: url(~@/app/components/develop/secret-key/assets/pause.svg);
background-position: center;
background-repeat: no-repeat;
}
@@ -0,0 +1,117 @@
.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;
}
@@ -0,0 +1,320 @@
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { t } from 'i18next'
import styles from './AudioPlayer.module.css'
import Toast from '@/app/components/base/toast'
type AudioPlayerProps = {
src: string
}
const AudioPlayer: React.FC<AudioPlayerProps> = ({ src }) => {
const [isPlaying, setIsPlaying] = useState(false)
const [currentTime, setCurrentTime] = useState(0)
const [duration, setDuration] = useState(0)
const [waveformData, setWaveformData] = useState<number[]>([])
const [bufferedTime, setBufferedTime] = useState(0)
const audioRef = useRef<HTMLAudioElement>(null)
const canvasRef = useRef<HTMLCanvasElement>(null)
const [hasStartedPlaying, setHasStartedPlaying] = useState(false)
const [hoverTime, setHoverTime] = useState(0)
const [isAudioAvailable, setIsAudioAvailable] = useState(true)
useEffect(() => {
const audio = audioRef.current
if (!audio)
return
const handleError = () => {
setIsAudioAvailable(false)
}
const setAudioData = () => {
setDuration(audio.duration)
}
const setAudioTime = () => {
setCurrentTime(audio.currentTime)
}
const handleProgress = () => {
if (audio.buffered.length > 0)
setBufferedTime(audio.buffered.end(audio.buffered.length - 1))
}
const handleEnded = () => {
setIsPlaying(false)
}
audio.addEventListener('loadedmetadata', setAudioData)
audio.addEventListener('timeupdate', setAudioTime)
audio.addEventListener('progress', handleProgress)
audio.addEventListener('ended', handleEnded)
audio.addEventListener('error', handleError)
// Preload audio metadata
audio.load()
// Delayed generation of waveform data
// eslint-disable-next-line ts/no-use-before-define
const timer = setTimeout(() => generateWaveformData(src), 1000)
return () => {
audio.removeEventListener('loadedmetadata', setAudioData)
audio.removeEventListener('timeupdate', setAudioTime)
audio.removeEventListener('progress', handleProgress)
audio.removeEventListener('ended', handleEnded)
audio.removeEventListener('error', handleError)
clearTimeout(timer)
}
}, [src])
const generateWaveformData = async (audioSrc: string) => {
if (!window.AudioContext && !(window as any).webkitAudioContext) {
setIsAudioAvailable(false)
Toast.notify({
type: 'error',
message: 'Web Audio API is not supported in this browser',
})
return null
}
const url = new URL(src)
const isHttp = url.protocol === 'http:' || url.protocol === 'https:'
if (!isHttp) {
setIsAudioAvailable(false)
return null
}
const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)()
const samples = 70
try {
const response = await fetch(audioSrc, { mode: 'cors' })
if (!response || !response.ok) {
setIsAudioAvailable(false)
return null
}
const arrayBuffer = await response.arrayBuffer()
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer)
const channelData = audioBuffer.getChannelData(0)
const blockSize = Math.floor(channelData.length / samples)
const waveformData: number[] = []
for (let i = 0; i < samples; i++) {
let sum = 0
for (let j = 0; j < blockSize; j++)
sum += Math.abs(channelData[i * blockSize + j])
// Apply nonlinear scaling to enhance small amplitudes
waveformData.push((sum / blockSize) * 5)
}
// Normalized waveform data
const maxAmplitude = Math.max(...waveformData)
const normalizedWaveform = waveformData.map(amp => amp / maxAmplitude)
setWaveformData(normalizedWaveform)
setIsAudioAvailable(true)
}
catch (error) {
const waveform: number[] = []
let prevValue = Math.random()
for (let i = 0; i < samples; i++) {
const targetValue = Math.random()
const interpolatedValue = prevValue + (targetValue - prevValue) * 0.3
waveform.push(interpolatedValue)
prevValue = interpolatedValue
}
const maxAmplitude = Math.max(...waveform)
const randomWaveform = waveform.map(amp => amp / maxAmplitude)
setWaveformData(randomWaveform)
setIsAudioAvailable(true)
}
finally {
await audioContext.close()
}
}
const togglePlay = useCallback(() => {
const audio = audioRef.current
if (audio && isAudioAvailable) {
if (isPlaying) {
setHasStartedPlaying(false)
audio.pause()
}
else {
setHasStartedPlaying(true)
audio.play().catch(error => console.error('Error playing audio:', error))
}
setIsPlaying(!isPlaying)
}
else {
Toast.notify({
type: 'error',
message: 'Audio element not found',
})
setIsAudioAvailable(false)
}
}, [isAudioAvailable, isPlaying])
const handleCanvasInteraction = useCallback((e: React.MouseEvent | React.TouchEvent) => {
e.preventDefault()
const getClientX = (event: React.MouseEvent | React.TouchEvent): number => {
if ('touches' in event)
return event.touches[0].clientX
return event.clientX
}
const updateProgress = (clientX: number) => {
const canvas = canvasRef.current
const audio = audioRef.current
if (!canvas || !audio)
return
const rect = canvas.getBoundingClientRect()
const percent = Math.min(Math.max(0, clientX - rect.left), rect.width) / rect.width
const newTime = percent * duration
// Removes the buffer check, allowing drag to any location
audio.currentTime = newTime
setCurrentTime(newTime)
if (!isPlaying) {
setIsPlaying(true)
audio.play().catch((error) => {
Toast.notify({
type: 'error',
message: `Error playing audio: ${error}`,
})
setIsPlaying(false)
})
}
}
updateProgress(getClientX(e))
}, [duration, isPlaying])
const formatTime = (time: number) => {
const minutes = Math.floor(time / 60)
const seconds = Math.floor(time % 60)
return `${minutes}:${seconds.toString().padStart(2, '0')}`
}
const drawWaveform = useCallback(() => {
const canvas = canvasRef.current
if (!canvas)
return
const ctx = canvas.getContext('2d')
if (!ctx)
return
const width = canvas.width
const height = canvas.height
const data = waveformData
ctx.clearRect(0, 0, width, height)
const barWidth = width / data.length
const playedWidth = (currentTime / duration) * width
const cornerRadius = 2
// Draw waveform bars
data.forEach((value, index) => {
let color
if (index * barWidth <= playedWidth)
color = '#296DFF'
else if ((index * barWidth / width) * duration <= hoverTime)
color = 'rgba(21,90,239,.40)'
else
color = 'rgba(21,90,239,.20)'
const barHeight = value * height
const rectX = index * barWidth
const rectY = (height - barHeight) / 2
const rectWidth = barWidth * 0.5
const rectHeight = barHeight
ctx.lineWidth = 1
ctx.fillStyle = color
if (ctx.roundRect) {
ctx.beginPath()
ctx.roundRect(rectX, rectY, rectWidth, rectHeight, cornerRadius)
ctx.fill()
}
else {
ctx.fillRect(rectX, rectY, rectWidth, rectHeight)
}
})
}, [currentTime, duration, hoverTime, waveformData])
useEffect(() => {
drawWaveform()
}, [drawWaveform, bufferedTime, hasStartedPlaying])
const handleMouseMove = useCallback((e: React.MouseEvent) => {
const canvas = canvasRef.current
const audio = audioRef.current
if (!canvas || !audio)
return
const rect = canvas.getBoundingClientRect()
const percent = Math.min(Math.max(0, e.clientX - rect.left), rect.width) / rect.width
const time = percent * duration
// Check if the hovered position is within a buffered range before updating hoverTime
for (let i = 0; i < audio.buffered.length; i++) {
if (time >= audio.buffered.start(i) && time <= audio.buffered.end(i)) {
setHoverTime(time)
break
}
}
}, [duration])
return (
<div className={styles.audioPlayer}>
<audio ref={audioRef} src={src} preload="auto"/>
<button className={styles.playButton} onClick={togglePlay} disabled={!isAudioAvailable}>
{isPlaying
? (
<svg viewBox="0 0 24 24" width="16" height="16">
<rect x="7" y="6" width="3" height="12" rx="1.5" ry="1.5"/>
<rect x="15" y="6" width="3" height="12" rx="1.5" ry="1.5"/>
</svg>
)
: (
<svg viewBox="0 0 24 24" width="16" height="16">
<path d="M8 5v14l11-7z" fill="currentColor"/>
</svg>
)}
</button>
<div className={isAudioAvailable ? styles.audioControls : styles.audioControls_disabled} hidden={!isAudioAvailable}>
<div className={styles.progressBarContainer}>
<canvas
ref={canvasRef}
className={styles.waveform}
onClick={handleCanvasInteraction}
onMouseMove={handleMouseMove}
onMouseDown={handleCanvasInteraction}
/>
{/* <div className={styles.currentTime} style={{ left: `${(currentTime / duration) * 81}%`, bottom: '29px' }}>
{formatTime(currentTime)}
</div> */}
<div className={styles.timeDisplay}>
<span className={styles.duration}>{formatTime(duration)}</span>
</div>
</div>
</div>
<div className={styles.source_unavailable} hidden={isAudioAvailable}>{t('common.operation.audioSourceUnavailable')}</div>
</div>
)
}
export default AudioPlayer
@@ -0,0 +1,12 @@
import React from 'react'
import AudioPlayer from './AudioPlayer'
type Props = {
srcs: string[]
}
const AudioGallery: React.FC<Props> = ({ srcs }) => {
return (<><br/>{srcs.map((src, index) => (<AudioPlayer key={`audio_${index}`} src={src}/>))}</>)
}
export default React.memo(AudioGallery)
@@ -0,0 +1,74 @@
import { forwardRef, useEffect, useRef } from 'react'
import cn from 'classnames'
type IProps = {
placeholder?: string
value: string
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void
className?: string
minHeight?: number
maxHeight?: number
autoFocus?: boolean
controlFocus?: number
onKeyDown?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void
onKeyUp?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void
}
const AutoHeightTextarea = forwardRef(
(
{ value, onChange, placeholder, className, minHeight = 36, maxHeight = 96, autoFocus, controlFocus, onKeyDown, onKeyUp }: IProps,
outerRef: any,
) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const ref = outerRef || useRef<HTMLTextAreaElement>(null)
const doFocus = () => {
if (ref.current) {
ref.current.setSelectionRange(value.length, value.length)
ref.current.focus()
return true
}
return false
}
const focus = () => {
if (!doFocus()) {
let hasFocus = false
const runId = setInterval(() => {
hasFocus = doFocus()
if (hasFocus)
clearInterval(runId)
}, 100)
}
}
useEffect(() => {
if (autoFocus)
focus()
}, [])
useEffect(() => {
if (controlFocus)
focus()
}, [controlFocus])
return (
<div className='relative'>
<div className={cn(className, 'invisible whitespace-pre-wrap break-all overflow-y-auto')} style={{ minHeight, maxHeight }}>
{!value ? placeholder : value.replace(/\n$/, '\n ')}
</div>
<textarea
ref={ref}
autoFocus={autoFocus}
className={cn(className, 'absolute inset-0 resize-none overflow-hidden')}
placeholder={placeholder}
onChange={onChange}
onKeyDown={onKeyDown}
onKeyUp={onKeyUp}
value={value}
/>
</div>
)
},
)
export default AutoHeightTextarea
+28
View File
@@ -0,0 +1,28 @@
@tailwind components;
@layer components {
.badge {
@apply inline-flex justify-center items-center text-text-tertiary border border-divider-deep
}
.badge-l {
@apply rounded-md gap-1 min-w-6
}
/* m is for the regular button */
.badge-m {
@apply rounded-md gap-[3px] min-w-5
}
.badge-s {
@apply rounded-[5px] gap-0.5 min-w-[18px]
}
.badge.badge-warning {
@apply text-text-warning border border-text-warning
}
.badge.badge-accent {
@apply text-text-accent-secondary border border-text-accent-secondary
}
}
+81
View File
@@ -0,0 +1,81 @@
import type { CSSProperties, ReactNode } from 'react'
import React from 'react'
import { type VariantProps, cva } from 'class-variance-authority'
import classNames from '@/utils/classnames'
import './index.css'
enum BadgeState {
Warning = 'warning',
Accent = 'accent',
Default = '',
}
const BadgeVariants = cva(
'badge',
{
variants: {
size: {
s: 'badge-s',
m: 'badge-m',
l: 'badge-l',
},
},
defaultVariants: {
size: 'm',
},
},
)
type BadgeProps = {
size?: 's' | 'm' | 'l'
iconOnly?: boolean
uppercase?: boolean
state?: BadgeState
styleCss?: CSSProperties
children?: ReactNode
} & React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof BadgeVariants>
function getBadgeState(state: BadgeState) {
switch (state) {
case BadgeState.Warning:
return 'badge-warning'
case BadgeState.Accent:
return 'badge-accent'
default:
return ''
}
}
const Badge: React.FC<BadgeProps> = ({
className,
size,
state = BadgeState.Default,
iconOnly = false,
uppercase = false,
styleCss,
children,
...props
}) => {
return (
<div
className={classNames(
BadgeVariants({ size, className }),
getBadgeState(state),
size === 's'
? (iconOnly ? 'p-[3px]' : 'px-[5px] py-[3px]')
: size === 'l'
? (iconOnly ? 'p-1.5' : 'px-2 py-1')
: (iconOnly ? 'p-1' : 'px-[5px] py-[2px]'),
uppercase ? 'system-2xs-medium-uppercase' : 'system-2xs-medium',
)}
style={styleCss}
{...props}
>
{children}
</div>
)
}
Badge.displayName = 'Badge'
export default Badge
export { Badge, BadgeState, BadgeVariants }
+22
View File
@@ -0,0 +1,22 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { RiAddLine } from '@remixicon/react'
import cn from '@/utils/classnames'
interface Props {
className?: string
onClick: () => void
}
const AddButton: FC<Props> = ({
className,
onClick,
}) => {
return (
<div className={cn(className, 'p-1 rounded-md cursor-pointer hover:bg-state-base-hover select-none')} onClick={onClick}>
<RiAddLine className='w-4 h-4 text-text-tertiary' />
</div>
)
}
export default React.memo(AddButton)
+112
View File
@@ -0,0 +1,112 @@
@tailwind components;
@layer components {
.btn {
@apply inline-flex justify-center items-center cursor-pointer whitespace-nowrap;
}
.btn-disabled {
@apply cursor-not-allowed;
}
.btn-small {
@apply px-2 h-6 rounded-md text-xs font-medium;
}
.btn-medium {
@apply px-3.5 h-8 rounded-lg text-[13px] leading-4 font-medium;
}
.btn-large {
@apply px-4 h-9 rounded-[10px] text-sm font-semibold;
}
.btn-primary {
@apply shadow bg-components-button-primary-bg border-components-button-primary-border hover:bg-components-button-primary-bg-hover hover:border-components-button-primary-border-hover text-components-button-primary-text;
}
.btn-primary.btn-destructive {
@apply bg-components-button-destructive-primary-bg border-components-button-destructive-primary-border hover:bg-components-button-destructive-primary-bg-hover hover:border-components-button-destructive-primary-border-hover text-components-button-destructive-primary-text;
}
.btn-primary.btn-disabled {
@apply shadow-none bg-components-button-primary-bg-disabled border-components-button-primary-border-disabled text-components-button-primary-text-disabled;
}
.btn-primary.btn-destructive.btn-disabled {
@apply shadow-none bg-components-button-destructive-primary-bg-disabled border-components-button-destructive-primary-border-disabled text-components-button-destructive-primary-text-disabled;
}
.btn-secondary {
@apply border-[0.5px] shadow-xs bg-components-button-secondary-bg border-components-button-secondary-border hover:bg-components-button-secondary-bg-hover hover:border-components-button-secondary-border-hover text-components-button-secondary-text;
}
.btn-secondary.btn-disabled {
@apply bg-components-button-secondary-bg-disabled border-components-button-secondary-border-disabled text-components-button-secondary-text-disabled;
}
.btn-secondary.btn-destructive {
@apply bg-components-button-destructive-secondary-bg border-components-button-destructive-secondary-border hover:bg-components-button-destructive-secondary-bg-hover hover:border-components-button-destructive-secondary-border-hover text-components-button-destructive-secondary-text;
}
.btn-secondary.btn-destructive.btn-disabled {
@apply bg-components-button-destructive-secondary-bg-disabled border-components-button-destructive-secondary-border-disabled text-components-button-destructive-secondary-text-disabled;
}
.btn-secondary-accent {
@apply border-[0.5px] shadow-xs bg-components-button-secondary-bg border-components-button-secondary-border hover:bg-components-button-secondary-bg-hover hover:border-components-button-secondary-border-hover text-components-button-secondary-accent-text;
}
.btn-secondary-accent.btn-disabled {
@apply bg-components-button-secondary-bg-disabled border-components-button-secondary-border-disabled text-components-button-secondary-accent-text-disabled;
}
.btn-warning {
@apply bg-components-button-destructive-primary-bg border-components-button-destructive-primary-border hover:bg-components-button-destructive-primary-bg-hover hover:border-components-button-destructive-primary-border-hover text-components-button-destructive-primary-text;
}
.btn-warning.btn-disabled {
@apply bg-components-button-destructive-primary-bg-disabled border-components-button-destructive-primary-border-disabled text-components-button-destructive-primary-text-disabled;
}
.btn-tertiary {
@apply bg-components-button-tertiary-bg hover:bg-components-button-tertiary-bg-hover text-components-button-tertiary-text;
}
.btn-tertiary.btn-disabled {
@apply bg-components-button-tertiary-bg-disabled text-components-button-tertiary-text-disabled;
}
.btn-tertiary.btn-destructive {
@apply bg-components-button-destructive-tertiary-bg hover:bg-components-button-destructive-tertiary-bg-hover text-components-button-destructive-tertiary-text;
}
.btn-tertiary.btn-destructive.btn-disabled {
@apply bg-components-button-destructive-tertiary-bg-disabled text-components-button-destructive-tertiary-text-disabled;
}
.btn-ghost {
@apply hover:bg-components-button-ghost-bg-hover text-components-button-ghost-text;
}
.btn-ghost.btn-disabled {
@apply text-components-button-ghost-text-disabled;
}
.btn-ghost.btn-destructive {
@apply hover:bg-components-button-destructive-ghost-bg-hover text-components-button-destructive-ghost-text;
}
.btn-ghost.btn-destructive.btn-disabled {
@apply text-components-button-destructive-ghost-text-disabled;
}
.btn-ghost-accent {
@apply hover:bg-state-accent-hover text-components-button-secondary-accent-text;
}
.btn-ghost-accent.btn-disabled {
@apply text-components-button-secondary-accent-text-disabled;
}
}
+60
View File
@@ -0,0 +1,60 @@
import type { CSSProperties } from 'react'
import React from 'react'
import { type VariantProps, cva } from 'class-variance-authority'
import Spinner from '../spinner'
import classNames from '@/utils/classnames'
const buttonVariants = cva(
'btn disabled:btn-disabled',
{
variants: {
variant: {
'primary': 'btn-primary',
'warning': 'btn-warning',
'secondary': 'btn-secondary',
'secondary-accent': 'btn-secondary-accent',
'ghost': 'btn-ghost',
'ghost-accent': 'btn-ghost-accent',
'tertiary': 'btn-tertiary',
},
size: {
small: 'btn-small',
medium: 'btn-medium',
large: 'btn-large',
},
},
defaultVariants: {
variant: 'secondary',
size: 'medium',
},
},
)
export type ButtonProps = {
destructive?: boolean
loading?: boolean
styleCss?: CSSProperties
} & React.ButtonHTMLAttributes<HTMLButtonElement> & VariantProps<typeof buttonVariants>
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, destructive, loading, styleCss, children, ...props }, ref) => {
return (
<button
type='button'
className={classNames(
buttonVariants({ variant, size, className }),
destructive && 'btn-destructive',
)}
ref={ref}
style={styleCss}
{...props}
>
{children}
{loading && <Spinner loading={loading} className='!text-white !h-3 !w-3 !border-2 !ml-1' />}
</button>
)
},
)
Button.displayName = 'Button'
export default Button
export { Button, buttonVariants }
+104
View File
@@ -0,0 +1,104 @@
import React, { useEffect, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { useTranslation } from 'react-i18next'
import Button from '../button'
export type IConfirm = {
className?: string
isShow: boolean
type?: 'info' | 'warning'
title: string
content?: React.ReactNode
confirmText?: string | null
onConfirm: () => void
cancelText?: string
onCancel: () => void
isLoading?: boolean
isDisabled?: boolean
showConfirm?: boolean
showCancel?: boolean
maskClosable?: boolean
}
function Confirm({
isShow,
type = 'warning',
title,
content,
confirmText,
cancelText,
onConfirm,
onCancel,
showConfirm = true,
showCancel = true,
isLoading = false,
isDisabled = false,
maskClosable = true,
}: IConfirm) {
const { t } = useTranslation()
const dialogRef = useRef<HTMLDivElement>(null)
const [isVisible, setIsVisible] = useState(isShow)
const confirmTxt = confirmText || `${t('common.operation.confirm')}`
const cancelTxt = cancelText || `${t('common.operation.cancel')}`
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape')
onCancel()
}
document.addEventListener('keydown', handleKeyDown)
return () => {
document.removeEventListener('keydown', handleKeyDown)
}
}, [onCancel])
const handleClickOutside = (event: MouseEvent) => {
if (maskClosable && dialogRef.current && !dialogRef.current.contains(event.target as Node))
onCancel()
}
useEffect(() => {
document.addEventListener('mousedown', handleClickOutside)
return () => {
document.removeEventListener('mousedown', handleClickOutside)
}
}, [maskClosable])
useEffect(() => {
if (isShow) {
setIsVisible(true)
}
else {
const timer = setTimeout(() => setIsVisible(false), 200)
return () => clearTimeout(timer)
}
}, [isShow])
if (!isVisible)
return null
return createPortal(
<div className={'fixed inset-0 flex items-center justify-center z-[10000000] bg-background-overlay'}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
}}>
<div ref={dialogRef} className={'relative w-full max-w-[480px] overflow-hidden'}>
<div className='flex flex-col items-start max-w-full rounded-2xl border-[0.5px] border-solid border-components-panel-border shadows-shadow-lg bg-components-panel-bg'>
<div className='flex pt-6 pl-6 pr-6 pb-4 flex-col items-start gap-2 self-stretch'>
<div className='title-2xl-semi-bold text-text-primary'>{title}</div>
<div className='system-md-regular text-text-tertiary w-full'>{content}</div>
</div>
<div className='flex p-6 gap-2 justify-end items-start self-stretch'>
{showCancel && <Button onClick={onCancel}>{cancelTxt}</Button>}
{showConfirm && <Button variant={'primary'} destructive={type !== 'info'} loading={isLoading} disabled={isDisabled} onClick={onConfirm}>{confirmTxt}</Button>}
</div>
</div>
</div>
</div>, document.body,
)
}
export default React.memo(Confirm)
+54
View File
@@ -0,0 +1,54 @@
'use client'
import { useState } from 'react'
import { t } from 'i18next'
import { debounce } from 'lodash-es'
import copy from 'copy-to-clipboard'
import s from './style.module.css'
import Tooltip from '@/app/components/base/tooltip'
type ICopyBtnProps = {
value: string
className?: string
isPlain?: boolean
}
const CopyBtn = ({
value,
className,
isPlain,
}: ICopyBtnProps) => {
const [isCopied, setIsCopied] = useState(false)
const onClickCopy = debounce(() => {
copy(value)
setIsCopied(true)
}, 100)
const onMouseLeave = debounce(() => {
setIsCopied(false)
}, 100)
return (
<div className={`${className}`}>
<Tooltip
popupContent={(isCopied ? t('appApi.copied') : t('appApi.copy'))}
asChild={false}
>
<div
onMouseLeave={onMouseLeave}
className={'box-border p-0.5 flex items-center justify-center rounded-md bg-components-button-secondary-bg cursor-pointer'}
style={!isPlain
? {
boxShadow: '0px 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06)',
}
: {}}
onClick={onClickCopy}
>
<div className={`w-6 h-6 rounded-md hover:bg-components-button-secondary-bg-hover ${s.copyIcon} ${isCopied ? s.copied : ''}`}></div>
</div>
</Tooltip>
</div>
)
}
export default CopyBtn
@@ -0,0 +1,15 @@
.copyIcon {
background-image: url(~@/app/components/develop/secret-key/assets/copy.svg);
background-position: center;
background-repeat: no-repeat;
}
.copyIcon:hover {
background-image: url(~@/app/components/develop/secret-key/assets/copy-hover.svg);
background-position: center;
background-repeat: no-repeat;
}
.copyIcon.copied {
background-image: url(~@/app/components/develop/secret-key/assets/copied.svg);
}
@@ -0,0 +1,91 @@
'use client'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiClipboardFill,
RiClipboardLine,
} from '@remixicon/react'
import { debounce } from 'lodash-es'
import copy from 'copy-to-clipboard'
import copyStyle from './style.module.css'
import Tooltip from '@/app/components/base/tooltip'
import ActionButton from '@/app/components/base/action-button'
type Props = {
content: string
className?: string
}
const prefixEmbedded = 'appOverview.overview.appInfo.embedded'
const CopyFeedback = ({ content }: Props) => {
const { t } = useTranslation()
const [isCopied, setIsCopied] = useState<boolean>(false)
const onClickCopy = debounce(() => {
copy(content)
setIsCopied(true)
}, 100)
const onMouseLeave = debounce(() => {
setIsCopied(false)
}, 100)
return (
<Tooltip
popupContent={
(isCopied
? t(`${prefixEmbedded}.copied`)
: t(`${prefixEmbedded}.copy`)) || ''
}
>
<ActionButton>
<div
onClick={onClickCopy}
onMouseLeave={onMouseLeave}
>
{isCopied && <RiClipboardFill className='w-4 h-4' />}
{!isCopied && <RiClipboardLine className='w-4 h-4' />}
</div>
</ActionButton>
</Tooltip>
)
}
export default CopyFeedback
export const CopyFeedbackNew = ({ content, className }: Pick<Props, 'className' | 'content'>) => {
const { t } = useTranslation()
const [isCopied, setIsCopied] = useState<boolean>(false)
const onClickCopy = debounce(() => {
copy(content)
setIsCopied(true)
}, 100)
const onMouseLeave = debounce(() => {
setIsCopied(false)
}, 100)
return (
<Tooltip
popupContent={
(isCopied
? t(`${prefixEmbedded}.copied`)
: t(`${prefixEmbedded}.copy`)) || ''
}
>
<div
className={`w-8 h-8 cursor-pointer hover:bg-components-button-ghost-bg-hover rounded-lg ${className ?? ''
}`}
>
<div
onClick={onClickCopy}
onMouseLeave={onMouseLeave}
className={`w-full h-full ${copyStyle.copyIcon} ${isCopied ? copyStyle.copied : ''
}`}
></div>
</div>
</Tooltip>
)
}
@@ -0,0 +1,15 @@
.copyIcon {
background-image: url(~@/app/components/develop/secret-key/assets/copy.svg);
background-position: center;
background-repeat: no-repeat;
}
.copyIcon:hover {
background-image: url(~@/app/components/develop/secret-key/assets/copy-hover.svg);
background-position: center;
background-repeat: no-repeat;
}
.copyIcon.copied {
background-image: url(~@/app/components/develop/secret-key/assets/copied.svg);
}
+53
View File
@@ -0,0 +1,53 @@
'use client'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { debounce } from 'lodash-es'
import copy from 'copy-to-clipboard'
import Tooltip from '../tooltip'
import {
Clipboard,
ClipboardCheck,
} from '@/app/components/base/icons/src/vender/line/files'
type Props = {
content: string
}
const prefixEmbedded = 'appOverview.overview.appInfo.embedded'
export const CopyIcon = ({ content }: Props) => {
const { t } = useTranslation()
const [isCopied, setIsCopied] = useState<boolean>(false)
const onClickCopy = debounce(() => {
copy(content)
setIsCopied(true)
}, 100)
const onMouseLeave = debounce(() => {
setIsCopied(false)
}, 100)
return (
<Tooltip
popupContent={
(isCopied
? t(`${prefixEmbedded}.copied`)
: t(`${prefixEmbedded}.copy`)) || ''
}
>
<div onMouseLeave={onMouseLeave}>
{!isCopied
? (
<Clipboard className='mx-1 w-3.5 h-3.5 text-text-tertiary cursor-pointer' onClick={onClickCopy} />
)
: (
<ClipboardCheck className='mx-1 w-3.5 h-3.5 text-text-tertiary' />
)
}
</div>
</Tooltip>
)
}
export default CopyIcon
+36
View File
@@ -0,0 +1,36 @@
import type { CSSProperties, FC } from 'react'
import React from 'react'
import { type VariantProps, cva } from 'class-variance-authority'
import classNames from '@/utils/classnames'
const dividerVariants = cva('',
{
variants: {
type: {
horizontal: 'w-full h-[0.5px] my-2 ',
vertical: 'w-[1px] h-full mx-2',
},
bgStyle: {
gradient: 'bg-gradient-to-r from-divider-regular to-background-gradient-mask-transparent',
solid: 'bg-divider-regular',
},
},
defaultVariants: {
type: 'horizontal',
bgStyle: 'solid',
},
},
)
export type DividerProps = {
className?: string
style?: CSSProperties
} & VariantProps<typeof dividerVariants>
const Divider: FC<DividerProps> = ({ type, bgStyle, className = '', style }) => {
return (
<div className={classNames(dividerVariants({ type, bgStyle }), className)} style={style}></div>
)
}
export default Divider
@@ -0,0 +1,23 @@
import type { FC } from 'react'
import type { DividerProps } from '.'
import Divider from '.'
import classNames from '@/utils/classnames'
export type DividerWithLabelProps = DividerProps & {
label: string
}
export const DividerWithLabel: FC<DividerWithLabelProps> = (props) => {
const { label, className, ...rest } = props
return <div
className="flex items-center gap-2 my-2"
>
<Divider {...rest} className={classNames('flex-1', className)} />
<span className="text-text-tertiary text-xs">
{label}
</span>
<Divider {...rest} className={classNames('flex-1', className)} />
</div>
}
export default DividerWithLabel
@@ -0,0 +1,136 @@
// '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)
// TODO MARS
+80
View File
@@ -0,0 +1,80 @@
import type { Resolution, TransferMethod, TtsAutoPlay } from '@/types/app'
import type { FileUploadConfigResponse } from '@/models/common'
export interface EnabledOrDisabled {
enabled?: boolean
}
export type MoreLikeThis = EnabledOrDisabled
export type OpeningStatement = EnabledOrDisabled & {
opening_statement?: string
suggested_questions?: string[]
}
export type SuggestedQuestionsAfterAnswer = EnabledOrDisabled
export type TextToSpeech = EnabledOrDisabled & {
language?: string
voice?: string
autoPlay?: TtsAutoPlay
}
export type SpeechToText = EnabledOrDisabled
export type RetrieverResource = EnabledOrDisabled
export type SensitiveWordAvoidance = EnabledOrDisabled & {
type?: string
config?: any
}
export type FileUpload = {
image?: EnabledOrDisabled & {
detail?: Resolution
number_limits?: number
transfer_methods?: TransferMethod[]
}
allowed_file_types?: string[]
allowed_file_extensions?: string[]
allowed_file_upload_methods?: TransferMethod[]
number_limits?: number
fileUploadConfig?: FileUploadConfigResponse
} & EnabledOrDisabled
export interface AnnotationReplyConfig {
enabled: boolean
id?: string
score_threshold?: number
embedding_model?: {
embedding_provider_name: string
embedding_model_name: string
}
}
export enum FeatureEnum {
moreLikeThis = 'moreLikeThis',
opening = 'opening',
suggested = 'suggested',
text2speech = 'text2speech',
speech2text = 'speech2text',
citation = 'citation',
moderation = 'moderation',
file = 'file',
annotationReply = 'annotationReply',
}
export interface Features {
[FeatureEnum.moreLikeThis]?: MoreLikeThis
[FeatureEnum.opening]?: OpeningStatement
[FeatureEnum.suggested]?: SuggestedQuestionsAfterAnswer
[FeatureEnum.text2speech]?: TextToSpeech
[FeatureEnum.speech2text]?: SpeechToText
[FeatureEnum.citation]?: RetrieverResource
[FeatureEnum.moderation]?: SensitiveWordAvoidance
[FeatureEnum.file]?: FileUpload
[FeatureEnum.annotationReply]?: AnnotationReplyConfig
}
export type OnFeaturesChange = (features?: Features) => void
+55
View File
@@ -0,0 +1,55 @@
import type { FC } from 'react'
import {
Csv,
Doc,
Docx,
Html,
Json,
Md,
Pdf,
Txt,
Unknown,
Xlsx,
} from '@/app/components/base/icons/src/public/files'
import { Notion } from '@/app/components/base/icons/src/public/common'
type FileIconProps = {
type: string
className?: string
}
const FileIcon: FC<FileIconProps> = ({
type,
className,
}) => {
switch (type) {
case 'csv':
return <Csv className={className} />
case 'doc':
return <Doc className={className} />
case 'docx':
return <Docx className={className} />
case 'htm':
case 'html':
return <Html className={className} />
case 'json':
return <Json className={className} />
case 'md':
case 'markdown':
case 'mdx':
return <Md className={className} />
case 'pdf':
return <Pdf className={className} />
case 'txt':
return <Txt className={className} />
case 'xls':
case 'xlsx':
return <Xlsx className={className} />
case 'notion':
return <Notion className={className} />
default:
return <Unknown className={className} />
}
}
export default FileIcon
@@ -0,0 +1,47 @@
import type { FC } from 'react'
import { createPortal } from 'react-dom'
import { RiCloseLine } from '@remixicon/react'
import React from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
type AudioPreviewProps = {
url: string
title: string
onCancel: () => void
}
const AudioPreview: FC<AudioPreviewProps> = ({
url,
title,
onCancel,
}) => {
useHotkeys('esc', onCancel)
return createPortal(
<div
className='fixed inset-0 p-8 flex items-center justify-center bg-black/80 z-[1000]'
onClick={e => e.stopPropagation()}
tabIndex={-1}
>
<div>
<audio controls title={title} autoPlay={false} preload="metadata">
<source
type="audio/mpeg"
src={url}
className='max-w-full max-h-full'
/>
</audio>
</div>
<div
className='absolute top-6 right-6 flex items-center justify-center w-8 h-8 bg-white/[0.08] rounded-lg backdrop-blur-[2px] cursor-pointer'
onClick={onCancel}
>
<RiCloseLine className='w-4 h-4 text-gray-500'/>
</div>
</div>
,
document.body,
)
}
export default AudioPreview
@@ -0,0 +1,8 @@
// fallback for file size limit of dify_config
export const IMG_SIZE_LIMIT = 10 * 1024 * 1024
export const FILE_SIZE_LIMIT = 15 * 1024 * 1024
export const AUDIO_SIZE_LIMIT = 50 * 1024 * 1024
export const VIDEO_SIZE_LIMIT = 100 * 1024 * 1024
export const MAX_FILE_UPLOAD_LIMIT = 10
export const FILE_URL_REGEX = /^(https?|ftp):\/\//
@@ -0,0 +1,17 @@
'use client'
import dynamic from 'next/dynamic'
type DynamicPdfPreviewProps = {
url: string
onCancel: () => void
}
const DynamicPdfPreview = dynamic<DynamicPdfPreviewProps>(
(() => {
if (typeof window !== 'undefined')
return import('./pdf-preview')
}) as any,
{ ssr: false }, // This will prevent the module from being loaded on the server-side
)
export default DynamicPdfPreview
@@ -0,0 +1,129 @@
import {
memo,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import { RiUploadCloud2Line } from '@remixicon/react'
import FileInput from '../file-input'
import { useFile } from '../hooks'
import { useStore } from '../store'
import { FILE_URL_REGEX } from '../constants'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import Button from '@/app/components/base/button'
import type { FileUpload } from '@/app/components/base/features/types'
import cn from '@/utils/classnames'
type FileFromLinkOrLocalProps = {
showFromLink?: boolean
showFromLocal?: boolean
trigger: (open: boolean) => React.ReactNode
fileConfig: FileUpload
}
const FileFromLinkOrLocal = ({
showFromLink = true,
showFromLocal = true,
trigger,
fileConfig,
}: FileFromLinkOrLocalProps) => {
const { t } = useTranslation()
const files = useStore(s => s.files)
const [open, setOpen] = useState(false)
const [url, setUrl] = useState('')
const [showError, setShowError] = useState(false)
const { handleLoadFileFromLink } = useFile(fileConfig)
const disabled = !!fileConfig.number_limits && files.length >= fileConfig.number_limits
const handleSaveUrl = () => {
if (!url)
return
if (!FILE_URL_REGEX.test(url)) {
setShowError(true)
return
}
handleLoadFileFromLink(url)
setUrl('')
}
return (
<PortalToFollowElem
placement='top'
offset={4}
open={open}
onOpenChange={setOpen}
>
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)} asChild>
{trigger(open)}
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1001]'>
<div className='p-3 w-[280px] bg-components-panel-bg-blur border-[0.5px] border-components-panel-border rounded-xl shadow-lg'>
{
showFromLink && (
<>
<div className={cn(
'flex items-center p-1 h-8 bg-components-input-bg-active border border-components-input-border-active rounded-lg shadow-xs',
showError && 'border-components-input-border-destructive',
)}>
<input
className='grow block mr-0.5 px-1 bg-transparent system-sm-regular outline-none appearance-none'
placeholder={t('common.fileUploader.pasteFileLinkInputPlaceholder') || ''}
value={url}
onChange={(e) => {
setShowError(false)
setUrl(e.target.value)
}}
disabled={disabled}
/>
<Button
className='shrink-0'
size='small'
variant='primary'
disabled={!url || disabled}
onClick={handleSaveUrl}
>
{t('common.operation.ok')}
</Button>
</div>
{
showError && (
<div className='mt-0.5 body-xs-regular text-text-destructive'>
{t('common.fileUploader.pasteFileLinkInvalid')}
</div>
)
}
</>
)
}
{
showFromLink && showFromLocal && (
<div className='flex items-center p-2 h-7 system-2xs-medium-uppercase text-text-quaternary'>
<div className='mr-2 w-[93px] h-[1px] bg-gradient-to-l from-[rgba(16,24,40,0.08)]' />
OR
<div className='ml-2 w-[93px] h-[1px] bg-gradient-to-r from-[rgba(16,24,40,0.08)]' />
</div>
)
}
{
showFromLocal && (
<Button
className='relative w-full'
variant='secondary-accent'
disabled={disabled}
>
<RiUploadCloud2Line className='mr-1 w-4 h-4' />
{t('common.fileUploader.uploadFromComputer')}
<FileInput fileConfig={fileConfig} />
</Button>
)
}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default memo(FileFromLinkOrLocal)
@@ -0,0 +1,32 @@
import cn from '@/utils/classnames'
type FileImageRenderProps = {
imageUrl: string
className?: string
alt?: string
onLoad?: () => void
onError?: () => void
showDownloadAction?: boolean
}
const FileImageRender = ({
imageUrl,
className,
alt,
onLoad,
onError,
showDownloadAction,
}: FileImageRenderProps) => {
return (
<div className={cn('border-[2px] border-effects-image-frame shadow-xs', className)}>
<img
className={cn('w-full h-full object-cover', showDownloadAction && 'cursor-pointer')}
alt={alt || 'Preview'}
onLoad={onLoad}
onError={onError}
src={imageUrl}
/>
</div>
)
}
export default FileImageRender
@@ -0,0 +1,49 @@
import { useFile } from './hooks'
import { useStore } from './store'
import type { FileUpload } from '@/app/components/base/features/types'
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
type FileInputProps = {
fileConfig: FileUpload
}
const FileInput = ({
fileConfig,
}: FileInputProps) => {
const files = useStore(s => s.files)
const { handleLocalFileUpload } = useFile(fileConfig)
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const targetFiles = e.target.files
if (targetFiles) {
if (fileConfig.number_limits) {
for (let i = 0; i < targetFiles.length; i++) {
if (i + 1 + files.length <= fileConfig.number_limits)
handleLocalFileUpload(targetFiles[i])
}
}
else {
handleLocalFileUpload(targetFiles[0])
}
}
}
const allowedFileTypes = fileConfig.allowed_file_types
const isCustom = allowedFileTypes?.includes(SupportUploadFileTypes.custom)
const exts = isCustom ? (fileConfig.allowed_file_extensions || []) : (allowedFileTypes?.map(type => FILE_EXTS[type]) || []).flat().map(item => `.${item}`)
const accept = exts.join(',')
return (
<input
className='absolute block inset-0 opacity-0 text-[0] w-full disabled:cursor-not-allowed cursor-pointer'
onClick={e => ((e.target as HTMLInputElement).value = '')}
type='file'
onChange={handleChange}
accept={accept}
disabled={!!(fileConfig.number_limits && files.length >= fileConfig?.number_limits)}
multiple={!!fileConfig.number_limits && fileConfig.number_limits > 1}
/>
)
}
export default FileInput
@@ -0,0 +1,106 @@
import React, { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { RiArrowRightSLine } from '@remixicon/react'
import FileImageRender from './file-image-render'
import FileTypeIcon from './file-type-icon'
import FileItem from './file-uploader-in-attachment/file-item'
import type { FileEntity } from './types'
import {
getFileAppearanceType,
} from './utils'
import Tooltip from '@/app/components/base/tooltip'
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
import cn from '@/utils/classnames'
type Props = {
fileList: {
varName: string
list: FileEntity[]
}[]
isExpanded?: boolean
noBorder?: boolean
noPadding?: boolean
}
const FileListInLog = ({ fileList, isExpanded = false, noBorder = false, noPadding = false }: Props) => {
const { t } = useTranslation()
const [expanded, setExpanded] = useState(isExpanded)
const fullList = useMemo(() => {
return fileList.reduce((acc: FileEntity[], { list }) => {
return [...acc, ...list]
}, [])
}, [fileList])
if (!fileList.length)
return null
return (
<div className={cn('px-3 py-2', expanded && 'py-3', !noBorder && 'border-t border-divider-subtle', noPadding && '!p-0')}>
<div className='flex justify-between gap-1'>
{expanded && (
<div className='grow py-1 text-text-secondary system-xs-semibold-uppercase cursor-pointer' onClick={() => setExpanded(!expanded)}>{t('appLog.runDetail.fileListLabel')}</div>
)}
{!expanded && (
<div className='flex gap-1'>
{fullList.map((file) => {
const { id, name, type, supportFileType, base64Url, url } = file
const isImageFile = supportFileType === SupportUploadFileTypes.image
return (
<>
{isImageFile && (
<Tooltip
popupContent={name}
>
<div key={id}>
<FileImageRender
className='w-8 h-8'
imageUrl={base64Url || url || ''}
/>
</div>
</Tooltip>
)}
{!isImageFile && (
<Tooltip
popupContent={name}
>
<div key={id} className='p-1.5 rounded-md bg-components-panel-on-panel-item-bg border-[0.5px] border-components-panel-border shadow-xs'>
<FileTypeIcon
type={getFileAppearanceType(name, type)}
size='md'
/>
</div>
</Tooltip>
)}
</>
)
})}
</div>
)}
<div className='flex items-center gap-1 cursor-pointer' onClick={() => setExpanded(!expanded)}>
{!expanded && <div className='text-text-tertiary system-xs-medium-uppercase'>{t('appLog.runDetail.fileListDetail')}</div>}
<RiArrowRightSLine className={cn('w-4 h-4 text-text-tertiary', expanded && 'rotate-90')} />
</div>
</div>
{expanded && (
<div className='flex flex-col gap-3'>
{fileList.map(item => (
<div key={item.varName} className='flex flex-col gap-1 system-xs-regular'>
<div className='py-1 text-text-tertiary '>{item.varName}</div>
{item.list.map(file => (
<FileItem
key={file.id}
file={file}
showDeleteAction={false}
showDownloadAction
canPreview
/>
))}
</div>
))}
</div>
)}
</div>
)
}
export default FileListInLog
@@ -0,0 +1,91 @@
import { memo } from 'react'
import {
RiFile3Fill,
RiFileCodeFill,
RiFileExcelFill,
RiFileGifFill,
RiFileImageFill,
RiFileMusicFill,
RiFilePdf2Fill,
RiFilePpt2Fill,
RiFileTextFill,
RiFileVideoFill,
RiFileWordFill,
RiMarkdownFill,
} from '@remixicon/react'
import { FileAppearanceTypeEnum } from './types'
import type { FileAppearanceType } from './types'
import cn from '@/utils/classnames'
const FILE_TYPE_ICON_MAP = {
[FileAppearanceTypeEnum.pdf]: {
component: RiFilePdf2Fill,
color: 'text-[#EA3434]',
},
[FileAppearanceTypeEnum.image]: {
component: RiFileImageFill,
color: 'text-[#00B2EA]',
},
[FileAppearanceTypeEnum.video]: {
component: RiFileVideoFill,
color: 'text-[#844FDA]',
},
[FileAppearanceTypeEnum.audio]: {
component: RiFileMusicFill,
color: 'text-[#FF3093]',
},
[FileAppearanceTypeEnum.document]: {
component: RiFileTextFill,
color: 'text-[#6F8BB5]',
},
[FileAppearanceTypeEnum.code]: {
component: RiFileCodeFill,
color: 'text-[#BCC0D1]',
},
[FileAppearanceTypeEnum.markdown]: {
component: RiMarkdownFill,
color: 'text-[#309BEC]',
},
[FileAppearanceTypeEnum.custom]: {
component: RiFile3Fill,
color: 'text-[#BCC0D1]',
},
[FileAppearanceTypeEnum.excel]: {
component: RiFileExcelFill,
color: 'text-[#01AC49]',
},
[FileAppearanceTypeEnum.word]: {
component: RiFileWordFill,
color: 'text-[#2684FF]',
},
[FileAppearanceTypeEnum.ppt]: {
component: RiFilePpt2Fill,
color: 'text-[#FF650F]',
},
[FileAppearanceTypeEnum.gif]: {
component: RiFileGifFill,
color: 'text-[#00B2EA]',
},
}
type FileTypeIconProps = {
type: FileAppearanceType
size?: 'sm' | 'lg' | 'md'
className?: string
}
const SizeMap = {
sm: 'w-4 h-4',
md: 'w-5 h-5',
lg: 'w-6 h-6',
}
const FileTypeIcon = ({
type,
size = 'sm',
className,
}: FileTypeIconProps) => {
const Icon = FILE_TYPE_ICON_MAP[type]?.component || FILE_TYPE_ICON_MAP[FileAppearanceTypeEnum.document].component
const color = FILE_TYPE_ICON_MAP[type]?.color || FILE_TYPE_ICON_MAP[FileAppearanceTypeEnum.document].color
return <Icon className={cn('shrink-0', SizeMap[size], color, className)} />
}
export default memo(FileTypeIcon)
@@ -0,0 +1,154 @@
import {
memo,
useState,
} from 'react'
import {
RiDeleteBinLine,
RiDownloadLine,
RiEyeLine,
} from '@remixicon/react'
import FileTypeIcon from '../file-type-icon'
import {
downloadFile,
fileIsUploaded,
getFileAppearanceType,
getFileExtension,
} from '../utils'
import FileImageRender from '../file-image-render'
import type { FileEntity } from '../types'
import ActionButton from '@/app/components/base/action-button'
import ProgressCircle from '@/app/components/base/progress-bar/progress-circle'
import { formatFileSize } from '@/utils/format'
import cn from '@/utils/classnames'
import { ReplayLine } from '@/app/components/base/icons/src/vender/other'
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
import ImagePreview from '@/app/components/base/image-uploader/image-preview'
type FileInAttachmentItemProps = {
file: FileEntity
showDeleteAction?: boolean
showDownloadAction?: boolean
onRemove?: (fileId: string) => void
onReUpload?: (fileId: string) => void
canPreview?: boolean
}
const FileInAttachmentItem = ({
file,
showDeleteAction,
showDownloadAction = true,
onRemove,
onReUpload,
canPreview,
}: FileInAttachmentItemProps) => {
const { id, name, type, progress, supportFileType, base64Url, url, isRemote } = file
const ext = getFileExtension(name, type, isRemote)
const isImageFile = supportFileType === SupportUploadFileTypes.image
const [imagePreviewUrl, setImagePreviewUrl] = useState('')
return (
<>
<div className={cn(
'flex items-center pr-3 h-12 rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg shadow-xs',
progress === -1 && 'bg-state-destructive-hover border-state-destructive-border',
)}>
<div className='flex items-center justify-center w-12 h-12'>
{
isImageFile && (
<FileImageRender
className='w-8 h-8'
imageUrl={base64Url || url || ''}
/>
)
}
{
!isImageFile && (
<FileTypeIcon
type={getFileAppearanceType(name, type)}
size='lg'
/>
)
}
</div>
<div className='grow w-0 mr-1'>
<div
className='flex items-center mb-0.5 system-xs-medium text-text-secondary truncate'
title={file.name}
>
<div className='truncate'>{name}</div>
</div>
<div className='flex items-center system-2xs-medium-uppercase text-text-tertiary'>
{
ext && (
<span>{ext.toLowerCase()}</span>
)
}
{
ext && (
<span className='mx-1 system-2xs-medium'>•</span>
)
}
{
!!file.size && (
<span>{formatFileSize(file.size)}</span>
)
}
</div>
</div>
<div className='shrink-0 flex items-center'>
{
progress >= 0 && !fileIsUploaded(file) && (
<ProgressCircle
className='mr-2.5'
percentage={progress}
/>
)
}
{
progress === -1 && (
<ActionButton
className='mr-1'
onClick={() => onReUpload?.(id)}
>
<ReplayLine className='w-4 h-4 text-text-tertiary' />
</ActionButton>
)
}
{
showDeleteAction && (
<ActionButton onClick={() => onRemove?.(id)}>
<RiDeleteBinLine className='w-4 h-4' />
</ActionButton>
)
}
{
canPreview && isImageFile && (
<ActionButton className='mr-1' onClick={() => setImagePreviewUrl(url || '')}>
<RiEyeLine className='w-4 h-4' />
</ActionButton>
)
}
{
showDownloadAction && (
<ActionButton onClick={(e) => {
e.stopPropagation()
downloadFile(url || base64Url || '', name)
}}>
<RiDownloadLine className='w-4 h-4' />
</ActionButton>
)
}
</div>
</div>
{
imagePreviewUrl && canPreview && (
<ImagePreview
title={name}
url={imagePreviewUrl}
onCancel={() => setImagePreviewUrl('')}
/>
)
}
</>
)
}
export default memo(FileInAttachmentItem)
@@ -0,0 +1,134 @@
import {
JSX,
useCallback,
} from 'react'
import {
RiLink,
RiUploadCloud2Line,
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import FileFromLinkOrLocal from '../file-from-link-or-local'
import {
FileContextProvider,
useStore,
} from '../store'
import type { FileEntity } from '../types'
import FileInput from '../file-input'
import { useFile } from '../hooks'
import FileItem from './file-item'
import Button from '@/app/components/base/button'
import cn from '@/utils/classnames'
import type { FileUpload } from '@/app/components/base/features/types'
import { TransferMethod } from '@/types/app'
type Option = {
value: string
label: string
icon: JSX.Element
}
type FileUploaderInAttachmentProps = {
fileConfig: FileUpload
}
const FileUploaderInAttachment = ({
fileConfig,
}: FileUploaderInAttachmentProps) => {
const { t } = useTranslation()
const files = useStore(s => s.files)
const {
handleRemoveFile,
handleReUploadFile,
} = useFile(fileConfig)
const options = [
{
value: TransferMethod.local_file,
label: t('common.fileUploader.uploadFromComputer'),
icon: <RiUploadCloud2Line className='w-4 h-4' />,
},
{
value: TransferMethod.remote_url,
label: t('common.fileUploader.pasteFileLink'),
icon: <RiLink className='w-4 h-4' />,
},
]
const renderButton = useCallback((option: Option, open?: boolean) => {
return (
<Button
key={option.value}
variant='tertiary'
className={cn('grow relative', open && 'bg-components-button-tertiary-bg-hover')}
disabled={!!(fileConfig.number_limits && files.length >= fileConfig.number_limits)}
>
{option.icon}
<span className='ml-1'>{option.label}</span>
{
option.value === TransferMethod.local_file && (
<FileInput fileConfig={fileConfig} />
)
}
</Button>
)
}, [fileConfig, files.length])
const renderTrigger = useCallback((option: Option) => {
return (open: boolean) => renderButton(option, open)
}, [renderButton])
const renderOption = useCallback((option: Option) => {
if (option.value === TransferMethod.local_file && fileConfig?.allowed_file_upload_methods?.includes(TransferMethod.local_file))
return renderButton(option)
if (option.value === TransferMethod.remote_url && fileConfig?.allowed_file_upload_methods?.includes(TransferMethod.remote_url)) {
return (
<FileFromLinkOrLocal
key={option.value}
showFromLocal={false}
trigger={renderTrigger(option)}
fileConfig={fileConfig}
/>
)
}
}, [renderButton, renderTrigger, fileConfig])
return (
<div>
<div className='flex items-center space-x-1'>
{options.map(renderOption)}
</div>
<div className='mt-1 space-y-1'>
{
files.map(file => (
<FileItem
key={file.id}
file={file}
showDeleteAction
showDownloadAction={false}
onRemove={() => handleRemoveFile(file.id)}
onReUpload={() => handleReUploadFile(file.id)}
/>
))
}
</div>
</div>
)
}
type FileUploaderInAttachmentWrapperProps = {
value?: FileEntity[]
onChange: (files: FileEntity[]) => void
fileConfig: FileUpload
}
const FileUploaderInAttachmentWrapper = ({
value,
onChange,
fileConfig,
}: FileUploaderInAttachmentWrapperProps) => {
return (
<FileContextProvider
value={value}
onChange={onChange}
>
<FileUploaderInAttachment fileConfig={fileConfig} />
</FileContextProvider>
)
}
export default FileUploaderInAttachmentWrapper
@@ -0,0 +1,109 @@
import { useState } from 'react'
import {
RiCloseLine,
RiDownloadLine,
} from '@remixicon/react'
import FileImageRender from '../file-image-render'
import type { FileEntity } from '../types'
import {
downloadFile,
fileIsUploaded,
} from '../utils'
import Button from '@/app/components/base/button'
import ProgressCircle from '@/app/components/base/progress-bar/progress-circle'
import { ReplayLine } from '@/app/components/base/icons/src/vender/other'
import ImagePreview from '@/app/components/base/image-uploader/image-preview'
type FileImageItemProps = {
file: FileEntity
showDeleteAction?: boolean
showDownloadAction?: boolean
canPreview?: boolean
onRemove?: (fileId: string) => void
onReUpload?: (fileId: string) => void
}
const FileImageItem = ({
file,
showDeleteAction,
showDownloadAction,
canPreview,
onRemove,
onReUpload,
}: FileImageItemProps) => {
const { id, progress, base64Url, url, name } = file
const [imagePreviewUrl, setImagePreviewUrl] = useState('')
return (
<>
<div
className='group/file-image relative cursor-pointer'
onClick={() => canPreview && setImagePreviewUrl(base64Url || url || '')}
>
{
showDeleteAction && (
<Button
className='hidden group-hover/file-image:flex absolute -right-1.5 -top-1.5 p-0 w-5 h-5 rounded-full z-[11]'
onClick={() => onRemove?.(id)}
>
<RiCloseLine className='w-4 h-4 text-components-button-secondary-text' />
</Button>
)
}
<FileImageRender
className='w-[68px] h-[68px] shadow-md'
imageUrl={base64Url || url || ''}
showDownloadAction={showDownloadAction}
/>
{
progress >= 0 && !fileIsUploaded(file) && (
<div className='absolute inset-0 flex items-center justify-center border-[2px] border-effects-image-frame bg-background-overlay-alt z-10'>
<ProgressCircle
percentage={progress}
size={12}
circleStrokeColor='stroke-components-progress-white-border'
circleFillColor='fill-transparent'
sectorFillColor='fill-components-progress-white-progress'
/>
</div>
)
}
{
progress === -1 && (
<div className='absolute inset-0 flex items-center justify-center border-[2px] border-state-destructive-border bg-background-overlay-destructive z-10'>
<ReplayLine
className='w-5 h-5'
onClick={() => onReUpload?.(id)}
/>
</div>
)
}
{
showDownloadAction && (
<div className='hidden group-hover/file-image:block absolute inset-0.5 bg-background-overlay-alt bg-opacity-[0.3] z-10'>
<div
className='absolute bottom-0.5 right-0.5 flex items-center justify-center w-6 h-6 rounded-lg bg-components-actionbar-bg shadow-md'
onClick={(e) => {
e.stopPropagation()
downloadFile(url || base64Url || '', name)
}}
>
<RiDownloadLine className='w-4 h-4 text-text-tertiary' />
</div>
</div>
)
}
</div>
{
imagePreviewUrl && canPreview && (
<ImagePreview
title={name}
url={imagePreviewUrl}
onCancel={() => setImagePreviewUrl('')}
/>
)
}
</>
)
}
export default FileImageItem
@@ -0,0 +1,160 @@
import {
RiCloseLine,
RiDownloadLine,
} from '@remixicon/react'
import { useState } from 'react'
import {
downloadFile,
fileIsUploaded,
getFileAppearanceType,
getFileExtension,
} from '../utils'
import FileTypeIcon from '../file-type-icon'
import type { FileEntity } from '../types'
import cn from '@/utils/classnames'
import { formatFileSize } from '@/utils/format'
import ProgressCircle from '@/app/components/base/progress-bar/progress-circle'
import { ReplayLine } from '@/app/components/base/icons/src/vender/other'
import ActionButton from '@/app/components/base/action-button'
import Button from '@/app/components/base/button'
import PdfPreview from '@/app/components/base/file-uploader/dynamic-pdf-preview'
import AudioPreview from '@/app/components/base/file-uploader/audio-preview'
import VideoPreview from '@/app/components/base/file-uploader/video-preview'
type FileItemProps = {
file: FileEntity
showDeleteAction?: boolean
showDownloadAction?: boolean
canPreview?: boolean
onRemove?: (fileId: string) => void
onReUpload?: (fileId: string) => void
}
const FileItem = ({
file,
showDeleteAction,
showDownloadAction = true,
onRemove,
onReUpload,
canPreview,
}: FileItemProps) => {
const { id, name, type, progress, url, base64Url, isRemote } = file
const [previewUrl, setPreviewUrl] = useState('')
const ext = getFileExtension(name, type, isRemote)
const uploadError = progress === -1
let tmp_preview_url = url || base64Url
if (!tmp_preview_url && file?.originalFile)
tmp_preview_url = URL.createObjectURL(file.originalFile.slice()).toString()
return (
<>
<div
className={cn(
'group/file-item relative p-2 w-[144px] h-[68px] rounded-lg border-[0.5px] border-components-panel-border bg-components-card-bg shadow-xs',
!uploadError && 'hover:bg-components-card-bg-alt',
uploadError && 'border border-state-destructive-border bg-state-destructive-hover',
uploadError && 'hover:border-[0.5px] hover:border-state-destructive-border bg-state-destructive-hover-alt',
)}
>
{
showDeleteAction && (
<Button
className='hidden group-hover/file-item:flex absolute -right-1.5 -top-1.5 p-0 w-5 h-5 rounded-full z-[11]'
onClick={() => onRemove?.(id)}
>
<RiCloseLine className='w-4 h-4 text-components-button-secondary-text' />
</Button>
)
}
<div
className='mb-1 h-8 line-clamp-2 system-xs-medium text-text-tertiary break-all cursor-pointer'
title={name}
onClick={() => canPreview && setPreviewUrl(tmp_preview_url || '')}
>
{name}
</div>
<div className='relative flex items-center justify-between'>
<div className='flex items-center system-2xs-medium-uppercase text-text-tertiary'>
<FileTypeIcon
size='sm'
type={getFileAppearanceType(name, type)}
className='mr-1'
/>
{
ext && (
<>
{ext}
<div className='mx-1'>·</div>
</>
)
}
{
!!file.size && formatFileSize(file.size)
}
</div>
{
showDownloadAction && tmp_preview_url && (
<ActionButton
size='m'
className='hidden group-hover/file-item:flex absolute -right-1 -top-1'
onClick={(e) => {
//手动修改下载文件名
let filename = name
if (tmp_preview_url.indexOf('.bin')) {
filename = `${name.split('.')[0]}.${ext}`
}
e.stopPropagation()
downloadFile(tmp_preview_url || '', filename)
}}
>
<RiDownloadLine className='w-3.5 h-3.5 text-text-tertiary' />
</ActionButton>
)
}
{
progress >= 0 && !fileIsUploaded(file) && (
<ProgressCircle
percentage={progress}
size={12}
className='shrink-0'
/>
)
}
{
uploadError && (
<ReplayLine
className='w-4 h-4 text-text-tertiary'
onClick={() => onReUpload?.(id)}
/>
)
}
</div>
</div>
{
type.split('/')[0] === 'audio' && canPreview && previewUrl && (
<AudioPreview
title={name}
url={previewUrl}
onCancel={() => setPreviewUrl('')}
/>
)
}
{
type.split('/')[0] === 'video' && canPreview && previewUrl && (
<VideoPreview
title={name}
url={previewUrl}
onCancel={() => setPreviewUrl('')}
/>
)
}
{
type.split('/')[1] === 'pdf' && canPreview && previewUrl && (
<PdfPreview url={previewUrl} onCancel={() => { setPreviewUrl('') }} />
)
}
</>
)
}
export default FileItem
@@ -0,0 +1,82 @@
import { useFile } from '../hooks'
import { useStore } from '../store'
import type { FileEntity } from '../types'
import FileImageItem from './file-image-item'
import FileItem from './file-item'
import type { FileUpload } from '@/app/components/base/features/types'
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
import cn from '@/utils/classnames'
type FileListProps = {
className?: string
files: FileEntity[]
onRemove?: (fileId: string) => void
onReUpload?: (fileId: string) => void
showDeleteAction?: boolean
showDownloadAction?: boolean
canPreview?: boolean
}
export const FileList = ({
className,
files,
onReUpload,
onRemove,
showDeleteAction = true,
showDownloadAction = false,
canPreview = true,
}: FileListProps) => {
return (
<div className={cn('flex flex-wrap gap-2', className)}>
{
files.map((file) => {
if (file.supportFileType === SupportUploadFileTypes.image) {
return (
<FileImageItem
key={file.id}
file={file}
showDeleteAction={showDeleteAction}
showDownloadAction={showDownloadAction}
onRemove={onRemove}
onReUpload={onReUpload}
canPreview={canPreview}
/>
)
}
return (
<FileItem
key={file.id}
file={file}
showDeleteAction={showDeleteAction}
showDownloadAction={showDownloadAction}
onRemove={onRemove}
onReUpload={onReUpload}
canPreview={canPreview}
/>
)
})
}
</div>
)
}
type FileListInChatInputProps = {
fileConfig: FileUpload
}
export const FileListInChatInput = ({
fileConfig,
}: FileListInChatInputProps) => {
const files = useStore(s => s.files)
const {
handleRemoveFile,
handleReUploadFile,
} = useFile(fileConfig)
return (
<FileList
files={files}
onReUpload={handleReUploadFile}
onRemove={handleRemoveFile}
/>
)
}
@@ -0,0 +1,41 @@
import {
memo,
useCallback,
} from 'react'
import {
RiAttachmentLine,
} from '@remixicon/react'
import FileFromLinkOrLocal from '../file-from-link-or-local'
import ActionButton from '@/app/components/base/action-button'
import cn from '@/utils/classnames'
import type { FileUpload } from '@/app/components/base/features/types'
import { TransferMethod } from '@/types/app'
type FileUploaderInChatInputProps = {
fileConfig: FileUpload
}
const FileUploaderInChatInput = ({
fileConfig,
}: FileUploaderInChatInputProps) => {
const renderTrigger = useCallback((open: boolean) => {
return (
<ActionButton
size='l'
className={cn(open && 'bg-state-base-hover')}
>
<RiAttachmentLine className='w-5 h-5' />
</ActionButton>
)
}, [])
return (
<FileFromLinkOrLocal
trigger={renderTrigger}
fileConfig={fileConfig}
showFromLocal={fileConfig?.allowed_file_upload_methods?.includes(TransferMethod.local_file)}
showFromLink={fileConfig?.allowed_file_upload_methods?.includes(TransferMethod.remote_url)}
/>
)
}
export default memo(FileUploaderInChatInput)
+365
View File
@@ -0,0 +1,365 @@
import type { ClipboardEvent } from 'react'
import {
useCallback,
useState,
} from 'react'
import { useParams } from 'next/navigation'
import { produce } from 'immer'
import { v4 as uuid4 } from 'uuid'
import { useTranslation } from 'react-i18next'
import type { FileEntity } from './types'
import { useFileStore } from './store'
import {
fileUpload,
getSupportFileType,
isAllowedFileExtension,
} from './utils'
import {
AUDIO_SIZE_LIMIT,
FILE_SIZE_LIMIT,
IMG_SIZE_LIMIT,
MAX_FILE_UPLOAD_LIMIT,
VIDEO_SIZE_LIMIT,
} from '@/app/components/base/file-uploader/constants'
import { useToastContext } from '@/app/components/base/toast'
import { TransferMethod } from '@/types/app'
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
import type { FileUpload } from '@/app/components/base/features/types'
import { formatFileSize } from '@/utils/format'
import { uploadRemoteFileInfo } from '@/service/index'
import type { FileUploadConfigResponse } from '@/models/common'
export const useFileSizeLimit = (fileUploadConfig?: FileUploadConfigResponse) => {
const imgSizeLimit = Number(fileUploadConfig?.image_file_size_limit) * 1024 * 1024 || IMG_SIZE_LIMIT
const docSizeLimit = Number(fileUploadConfig?.file_size_limit) * 1024 * 1024 || FILE_SIZE_LIMIT
const audioSizeLimit = Number(fileUploadConfig?.audio_file_size_limit) * 1024 * 1024 || AUDIO_SIZE_LIMIT
const videoSizeLimit = Number(fileUploadConfig?.video_file_size_limit) * 1024 * 1024 || VIDEO_SIZE_LIMIT
const maxFileUploadLimit = Number(fileUploadConfig?.workflow_file_upload_limit) || MAX_FILE_UPLOAD_LIMIT
return {
imgSizeLimit,
docSizeLimit,
audioSizeLimit,
videoSizeLimit,
maxFileUploadLimit,
}
}
export const useFile = (fileConfig: FileUpload) => {
const { t } = useTranslation()
const { notify } = useToastContext()
const fileStore = useFileStore()
const params = useParams()
const { imgSizeLimit, docSizeLimit, audioSizeLimit, videoSizeLimit } = useFileSizeLimit(fileConfig.fileUploadConfig)
const checkSizeLimit = useCallback((fileType: string, fileSize: number) => {
switch (fileType) {
case SupportUploadFileTypes.image: {
if (fileSize > imgSizeLimit) {
notify({
type: 'error',
message: t('common.fileUploader.uploadFromComputerLimit', {
type: SupportUploadFileTypes.image,
size: formatFileSize(imgSizeLimit),
}),
})
return false
}
return true
}
case SupportUploadFileTypes.document: {
if (fileSize > docSizeLimit) {
notify({
type: 'error',
message: t('common.fileUploader.uploadFromComputerLimit', {
type: SupportUploadFileTypes.document,
size: formatFileSize(docSizeLimit),
}),
})
return false
}
return true
}
case SupportUploadFileTypes.audio: {
if (fileSize > audioSizeLimit) {
notify({
type: 'error',
message: t('common.fileUploader.uploadFromComputerLimit', {
type: SupportUploadFileTypes.audio,
size: formatFileSize(audioSizeLimit),
}),
})
return false
}
return true
}
case SupportUploadFileTypes.video: {
if (fileSize > videoSizeLimit) {
notify({
type: 'error',
message: t('common.fileUploader.uploadFromComputerLimit', {
type: SupportUploadFileTypes.video,
size: formatFileSize(videoSizeLimit),
}),
})
return false
}
return true
}
case SupportUploadFileTypes.custom: {
if (fileSize > docSizeLimit) {
notify({
type: 'error',
message: t('common.fileUploader.uploadFromComputerLimit', {
type: SupportUploadFileTypes.document,
size: formatFileSize(docSizeLimit),
}),
})
return false
}
return true
}
default: {
return true
}
}
}, [audioSizeLimit, docSizeLimit, imgSizeLimit, notify, t, videoSizeLimit])
const handleAddFile = useCallback((newFile: FileEntity) => {
const {
files,
setFiles,
} = fileStore.getState()
const newFiles = produce(files, (draft) => {
draft.push(newFile)
})
setFiles(newFiles)
}, [fileStore])
const handleUpdateFile = useCallback((newFile: FileEntity) => {
const {
files,
setFiles,
} = fileStore.getState()
const newFiles = produce(files, (draft) => {
const index = draft.findIndex(file => file.id === newFile.id)
if (index > -1)
draft[index] = newFile
})
setFiles(newFiles)
}, [fileStore])
const handleRemoveFile = useCallback((fileId: string) => {
const {
files,
setFiles,
} = fileStore.getState()
const newFiles = files.filter(file => file.id !== fileId)
setFiles(newFiles)
}, [fileStore])
const handleReUploadFile = useCallback((fileId: string) => {
const {
files,
setFiles,
} = fileStore.getState()
const index = files.findIndex(file => file.id === fileId)
if (index > -1) {
const uploadingFile = files[index]
const newFiles = produce(files, (draft) => {
draft[index].progress = 0
})
setFiles(newFiles)
fileUpload({
file: uploadingFile.originalFile!,
onProgressCallback: (progress) => {
handleUpdateFile({ ...uploadingFile, progress })
},
onSuccessCallback: (res) => {
handleUpdateFile({ ...uploadingFile, uploadedId: res.id, progress: 100 })
},
onErrorCallback: () => {
notify({ type: 'error', message: t('common.fileUploader.uploadFromComputerUploadError') })
handleUpdateFile({ ...uploadingFile, progress: -1 })
},
}, !!params.token)
}
}, [fileStore, notify, t, handleUpdateFile, params])
const startProgressTimer = useCallback((fileId: string) => {
const timer = setInterval(() => {
const files = fileStore.getState().files
const file = files.find(file => file.id === fileId)
if (file && file.progress < 80 && file.progress >= 0)
handleUpdateFile({ ...file, progress: file.progress + 20 })
else
clearTimeout(timer)
}, 200)
}, [fileStore, handleUpdateFile])
const handleLoadFileFromLink = useCallback((url: string) => {
const allowedFileTypes = fileConfig.allowed_file_types
const uploadingFile = {
id: uuid4(),
name: url,
type: '',
size: 0,
progress: 0,
transferMethod: TransferMethod.remote_url,
supportFileType: '',
url,
isRemote: true,
}
handleAddFile(uploadingFile)
startProgressTimer(uploadingFile.id)
uploadRemoteFileInfo(url, !!params.token).then((res) => {
const newFile = {
...uploadingFile,
type: res.mime_type,
size: res.size,
progress: 100,
supportFileType: getSupportFileType(res.name, res.mime_type, allowedFileTypes?.includes(SupportUploadFileTypes.custom)),
uploadedId: res.id,
url: res.url,
}
if (!isAllowedFileExtension(res.name, res.mime_type, fileConfig.allowed_file_types || [], fileConfig.allowed_file_extensions || [])) {
notify({ type: 'error', message: t('common.fileUploader.fileExtensionNotSupport') })
handleRemoveFile(uploadingFile.id)
}
if (!checkSizeLimit(newFile.supportFileType, newFile.size))
handleRemoveFile(uploadingFile.id)
else
handleUpdateFile(newFile)
}).catch(() => {
notify({ type: 'error', message: t('common.fileUploader.pasteFileLinkInvalid') })
handleRemoveFile(uploadingFile.id)
})
}, [checkSizeLimit, handleAddFile, handleUpdateFile, notify, t, handleRemoveFile, fileConfig?.allowed_file_types, fileConfig.allowed_file_extensions, startProgressTimer, params.token])
const handleLoadFileFromLinkSuccess = useCallback(() => { }, [])
const handleLoadFileFromLinkError = useCallback(() => { }, [])
const handleClearFiles = useCallback(() => {
const {
setFiles,
} = fileStore.getState()
setFiles([])
}, [fileStore])
const handleLocalFileUpload = useCallback((file: File) => {
if (!isAllowedFileExtension(file.name, file.type, fileConfig.allowed_file_types || [], fileConfig.allowed_file_extensions || [])) {
notify({ type: 'error', message: t('common.fileUploader.fileExtensionNotSupport') })
return
}
const allowedFileTypes = fileConfig.allowed_file_types
const fileType = getSupportFileType(file.name, file.type, allowedFileTypes?.includes(SupportUploadFileTypes.custom))
if (!checkSizeLimit(fileType, file.size))
return
const reader = new FileReader()
const isImage = file.type.startsWith('image')
reader.addEventListener(
'load',
() => {
const uploadingFile = {
id: uuid4(),
name: file.name,
type: file.type,
size: file.size,
progress: 0,
transferMethod: TransferMethod.local_file,
supportFileType: getSupportFileType(file.name, file.type, allowedFileTypes?.includes(SupportUploadFileTypes.custom)),
originalFile: file,
base64Url: isImage ? reader.result as string : '',
}
handleAddFile(uploadingFile)
fileUpload({
file: uploadingFile.originalFile,
onProgressCallback: (progress) => {
handleUpdateFile({ ...uploadingFile, progress })
},
onSuccessCallback: (res) => {
handleUpdateFile({ ...uploadingFile, uploadedId: res.id, progress: 100 })
},
onErrorCallback: () => {
notify({ type: 'error', message: t('common.fileUploader.uploadFromComputerUploadError') })
handleUpdateFile({ ...uploadingFile, progress: -1 })
},
}, !!params.token)
},
false,
)
reader.addEventListener(
'error',
() => {
notify({ type: 'error', message: t('common.fileUploader.uploadFromComputerReadError') })
},
false,
)
reader.readAsDataURL(file)
}, [checkSizeLimit, notify, t, handleAddFile, handleUpdateFile, params.token, fileConfig?.allowed_file_types, fileConfig?.allowed_file_extensions])
const handleClipboardPasteFile = useCallback((e: ClipboardEvent<HTMLTextAreaElement>) => {
const file = e.clipboardData?.files[0]
if (file) {
e.preventDefault()
handleLocalFileUpload(file)
}
}, [handleLocalFileUpload])
const [isDragActive, setIsDragActive] = useState(false)
const handleDragFileEnter = useCallback((e: React.DragEvent<HTMLElement>) => {
e.preventDefault()
e.stopPropagation()
setIsDragActive(true)
}, [])
const handleDragFileOver = useCallback((e: React.DragEvent<HTMLElement>) => {
e.preventDefault()
e.stopPropagation()
}, [])
const handleDragFileLeave = useCallback((e: React.DragEvent<HTMLElement>) => {
e.preventDefault()
e.stopPropagation()
setIsDragActive(false)
}, [])
const handleDropFile = useCallback((e: React.DragEvent<HTMLElement>) => {
e.preventDefault()
e.stopPropagation()
setIsDragActive(false)
const file = e.dataTransfer.files[0]
if (file)
handleLocalFileUpload(file)
}, [handleLocalFileUpload])
return {
handleAddFile,
handleUpdateFile,
handleRemoveFile,
handleReUploadFile,
handleLoadFileFromLink,
handleLoadFileFromLinkSuccess,
handleLoadFileFromLinkError,
handleClearFiles,
handleLocalFileUpload,
handleClipboardPasteFile,
isDragActive,
handleDragFileEnter,
handleDragFileOver,
handleDragFileLeave,
handleDropFile,
}
}
@@ -0,0 +1,7 @@
export { default as FileUploaderInAttachmentWrapper } from './file-uploader-in-attachment'
export { default as FileItemInAttachment } from './file-uploader-in-attachment/file-item'
export { default as FileUploaderInChatInput } from './file-uploader-in-chat-input'
export { default as FileTypeIcon } from './file-type-icon'
export { FileListInChatInput } from './file-uploader-in-chat-input/file-list'
export { FileList } from './file-uploader-in-chat-input/file-list'
export { default as FileItem } from './file-uploader-in-chat-input/file-item'
@@ -0,0 +1,102 @@
import type { FC } from 'react'
import { createPortal } from 'react-dom'
import 'react-pdf-highlighter/dist/style.css'
import { PdfHighlighter, PdfLoader } from 'react-pdf-highlighter'
import { t } from 'i18next'
import { RiCloseLine, RiZoomInLine, RiZoomOutLine } from '@remixicon/react'
import React, { useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import Loading from '@/app/components/base/loading'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import Tooltip from '@/app/components/base/tooltip'
type PdfPreviewProps = {
url: string
onCancel: () => void
}
const PdfPreview: FC<PdfPreviewProps> = ({
url,
onCancel,
}) => {
const media = useBreakpoints()
const [scale, setScale] = useState(1)
const [position, setPosition] = useState({ x: 0, y: 0 })
const isMobile = media === MediaType.mobile
const zoomIn = () => {
setScale(prevScale => Math.min(prevScale * 1.2, 15))
setPosition({ x: position.x - 50, y: position.y - 50 })
}
const zoomOut = () => {
setScale((prevScale) => {
const newScale = Math.max(prevScale / 1.2, 0.5)
if (newScale === 1)
setPosition({ x: 0, y: 0 })
else
setPosition({ x: position.x + 50, y: position.y + 50 })
return newScale
})
}
useHotkeys('esc', onCancel)
useHotkeys('up', zoomIn)
useHotkeys('down', zoomOut)
return createPortal(
<div
className={`fixed inset-0 flex items-center justify-center bg-black/80 z-[1000] ${!isMobile && 'p-8'}`}
onClick={e => e.stopPropagation()}
tabIndex={-1}
>
<div
className='h-[95vh] w-[100vw] max-w-full max-h-full overflow-hidden'
style={{ transform: `scale(${scale})`, transformOrigin: 'center', scrollbarWidth: 'none', msOverflowStyle: 'none' }}
>
<PdfLoader
workerSrc='/pdf.worker.min.mjs'
url={url}
beforeLoad={<div className='flex justify-center items-center h-64'><Loading type='app' /></div>}
>
{(pdfDocument) => {
return (
<PdfHighlighter
pdfDocument={pdfDocument}
enableAreaSelection={event => event.altKey}
scrollRef={() => { }}
onScrollChange={() => { }}
onSelectionFinished={() => null}
highlightTransform={() => { return <div/> }}
highlights={[]}
/>
)
}}
</PdfLoader>
</div>
<Tooltip popupContent={t('common.operation.zoomOut')}>
<div className='absolute top-6 right-24 flex items-center justify-center w-8 h-8 rounded-lg cursor-pointer'
onClick={zoomOut}>
<RiZoomOutLine className='w-4 h-4 text-gray-500'/>
</div>
</Tooltip>
<Tooltip popupContent={t('common.operation.zoomIn')}>
<div className='absolute top-6 right-16 flex items-center justify-center w-8 h-8 rounded-lg cursor-pointer'
onClick={zoomIn}>
<RiZoomInLine className='w-4 h-4 text-gray-500'/>
</div>
</Tooltip>
<Tooltip popupContent={t('common.operation.cancel')}>
<div
className='absolute top-6 right-6 flex items-center justify-center w-8 h-8 bg-white/8 rounded-lg backdrop-blur-[2px] cursor-pointer'
onClick={onCancel}>
<RiCloseLine className='w-4 h-4 text-gray-500'/>
</div>
</Tooltip>
</div>,
document.body,
)
}
export default PdfPreview
@@ -0,0 +1,67 @@
import {
createContext,
useContext,
useRef,
} from 'react'
import {
create,
useStore as useZustandStore,
} from 'zustand'
import type {
FileEntity,
} from './types'
type Shape = {
files: FileEntity[]
setFiles: (files: FileEntity[]) => void
}
export const createFileStore = (
value: FileEntity[] = [],
onChange?: (files: FileEntity[]) => void,
) => {
return create<Shape>(set => ({
files: [...value],
setFiles: (files) => {
set({ files })
onChange?.(files)
},
}))
}
type FileStore = ReturnType<typeof createFileStore>
export const FileContext = createContext<FileStore | null>(null)
export function useStore<T>(selector: (state: Shape) => T): T {
const store = useContext(FileContext)
if (!store)
throw new Error('Missing FileContext.Provider in the tree')
return useZustandStore(store, selector)
}
export const useFileStore = () => {
return useContext(FileContext)!
}
type FileProviderProps = {
children: React.ReactNode
value?: FileEntity[]
onChange?: (files: FileEntity[]) => void
}
export const FileContextProvider = ({
children,
value,
onChange,
}: FileProviderProps) => {
const storeRef = useRef<FileStore>()
if (!storeRef.current)
storeRef.current = createFileStore(value, onChange)
return (
<FileContext.Provider value={storeRef.current}>
{children}
</FileContext.Provider>
)
}
@@ -0,0 +1,33 @@
import type { TransferMethod } from '@/types/app'
export enum FileAppearanceTypeEnum {
image = 'image',
video = 'video',
audio = 'audio',
document = 'document',
code = 'code',
pdf = 'pdf',
markdown = 'markdown',
excel = 'excel',
word = 'word',
ppt = 'ppt',
gif = 'gif',
custom = 'custom',
}
export type FileAppearanceType = keyof typeof FileAppearanceTypeEnum
export type FileEntity = {
id: string
name: string
size: number
type: string
progress: number
transferMethod: TransferMethod
supportFileType: string
originalFile?: File
uploadedId?: string
base64Url?: string
url?: string
isRemote?: boolean
}
+197
View File
@@ -0,0 +1,197 @@
import mime from 'mime'
import { FileAppearanceTypeEnum } from './types'
import type { FileEntity } from './types'
import { upload } from '@/service/base'
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
import type { FileResponse } from '@/types/workflow'
import { TransferMethod } from '@/types/app'
type FileUploadParams = {
file: File
onProgressCallback: (progress: number) => void
onSuccessCallback: (res: { id: string }) => void
onErrorCallback: () => void
}
type FileUpload = (v: FileUploadParams, isPublic?: boolean, url?: string) => void
export const fileUpload: FileUpload = ({
file,
onProgressCallback,
onSuccessCallback,
onErrorCallback,
}, isPublic, url) => {
const formData = new FormData()
formData.append('file', file)
const onProgress = (e: ProgressEvent) => {
if (e.lengthComputable) {
const percent = Math.floor(e.loaded / e.total * 100)
onProgressCallback(percent)
}
}
// TODO
upload({
xhr: new XMLHttpRequest(),
data: formData,
onprogress: onProgress,
}, isPublic, url)
.then((res) => {
// debugger
onSuccessCallback(res)
})
.catch(() => {
onErrorCallback()
})
}
export const getFileExtension = (fileName: string, fileMimetype: string, isRemote?: boolean) => {
let extension = ''
if (fileMimetype)
extension = mime.getExtension(fileMimetype) || ''
if (fileName && !extension) {
const fileNamePair = fileName.split('.')
const fileNamePairLength = fileNamePair.length
if (fileNamePairLength > 1)
extension = fileNamePair[fileNamePairLength - 1]
else
extension = ''
}
if (isRemote)
extension = ''
return extension
}
export const getFileAppearanceType = (fileName: string, fileMimetype: string) => {
const extension = getFileExtension(fileName, fileMimetype)
if (extension === 'gif')
return FileAppearanceTypeEnum.gif
if (FILE_EXTS.image.includes(extension.toUpperCase()))
return FileAppearanceTypeEnum.image
if (FILE_EXTS.video.includes(extension.toUpperCase()))
return FileAppearanceTypeEnum.video
if (FILE_EXTS.audio.includes(extension.toUpperCase()))
return FileAppearanceTypeEnum.audio
if (extension === 'html')
return FileAppearanceTypeEnum.code
if (extension === 'pdf')
return FileAppearanceTypeEnum.pdf
if (extension === 'md' || extension === 'markdown' || extension === 'mdx')
return FileAppearanceTypeEnum.markdown
if (extension === 'xlsx' || extension === 'xls')
return FileAppearanceTypeEnum.excel
if (extension === 'docx' || extension === 'doc')
return FileAppearanceTypeEnum.word
if (extension === 'pptx' || extension === 'ppt')
return FileAppearanceTypeEnum.ppt
if (FILE_EXTS.document.includes(extension.toUpperCase()))
return FileAppearanceTypeEnum.document
return FileAppearanceTypeEnum.custom
}
export const getSupportFileType = (fileName: string, fileMimetype: string, isCustom?: boolean) => {
if (isCustom)
return SupportUploadFileTypes.custom
const extension = getFileExtension(fileName, fileMimetype)
for (const key in FILE_EXTS) {
if ((FILE_EXTS[key]).includes(extension.toUpperCase()))
return key
}
return ''
}
export const getProcessedFiles = (files: FileEntity[]) => {
return files.filter(file => file.progress !== -1).map(fileItem => ({
type: fileItem.supportFileType,
transfer_method: fileItem.transferMethod,
url: fileItem.url || '',
upload_file_id: fileItem.uploadedId || '',
}))
}
export const getProcessedFilesFromResponse = (files: FileResponse[]) => {
return files.map((fileItem) => {
return {
id: fileItem.related_id,
name: fileItem.filename,
size: fileItem.size || 0,
type: fileItem.mime_type,
progress: 100,
transferMethod: fileItem.transfer_method,
supportFileType: fileItem.type,
uploadedId: fileItem.related_id,
url: fileItem.url,
}
})
}
export const getFileNameFromUrl = (url: string) => {
const urlParts = url.split('/')
return urlParts[urlParts.length - 1] || ''
}
export const getSupportFileExtensionList = (allowFileTypes: string[], allowFileExtensions: string[]) => {
if (allowFileTypes.includes(SupportUploadFileTypes.custom))
return allowFileExtensions.map(item => item.slice(1).toUpperCase())
return allowFileTypes.map(type => FILE_EXTS[type]).flat()
}
export const isAllowedFileExtension = (fileName: string, fileMimetype: string, allowFileTypes: string[], allowFileExtensions: string[]) => {
return getSupportFileExtensionList(allowFileTypes, allowFileExtensions).includes(getFileExtension(fileName, fileMimetype).toUpperCase())
}
export const getFilesInLogs = (rawData: any) => {
const result = Object.keys(rawData || {}).map((key) => {
if (typeof rawData[key] === 'object' && rawData[key]?.dify_model_identity === '__dify__file__') {
return {
varName: key,
list: getProcessedFilesFromResponse([rawData[key]]),
}
}
if (Array.isArray(rawData[key]) && rawData[key].some(item => item?.dify_model_identity === '__dify__file__')) {
return {
varName: key,
list: getProcessedFilesFromResponse(rawData[key]),
}
}
return undefined
}).filter(Boolean)
return result
}
export const fileIsUploaded = (file: FileEntity) => {
if (file.uploadedId)
return true
if (file.transferMethod === TransferMethod.remote_url && file.progress === 100)
return true
}
export const downloadFile = (url: string, filename: string) => {
const anchor = document.createElement('a')
anchor.href = url
anchor.download = filename
anchor.style.display = 'none'
anchor.target = '_blank'
anchor.title = filename
document.body.appendChild(anchor)
anchor.click()
document.body.removeChild(anchor)
}
@@ -0,0 +1,45 @@
import type { FC } from 'react'
import { createPortal } from 'react-dom'
import { RiCloseLine } from '@remixicon/react'
import React from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
type VideoPreviewProps = {
url: string
title: string
onCancel: () => void
}
const VideoPreview: FC<VideoPreviewProps> = ({
url,
title,
onCancel,
}) => {
useHotkeys('esc', onCancel)
return createPortal(
<div
className='fixed inset-0 p-8 flex items-center justify-center bg-black/80 z-[1000]'
onClick={e => e.stopPropagation()}
tabIndex={-1}
>
<div>
<video controls title={title} autoPlay={false} preload="metadata">
<source
type="video/mp4"
src={url}
className='max-w-full max-h-full'
/>
</video>
</div>
<div
className='absolute top-6 right-6 flex items-center justify-center w-8 h-8 bg-white/[0.08] rounded-lg backdrop-blur-[2px] cursor-pointer'
onClick={onCancel}
>
<RiCloseLine className='w-4 h-4 text-gray-500'/>
</div>
</div>
, document.body,
)
}
export default VideoPreview
+33
View File
@@ -0,0 +1,33 @@
import { forwardRef } from 'react'
import { generate } from './utils'
import type { AbstractNode } from './utils'
export type IconData = {
name: string
icon: AbstractNode
}
export type IconBaseProps = {
data: IconData
className?: string
onClick?: React.MouseEventHandler<SVGElement>
style?: React.CSSProperties
}
const IconBase = forwardRef<React.MutableRefObject<HTMLOrSVGElement>, IconBaseProps>((props, ref) => {
const { data, className, onClick, style, ...restProps } = props
return generate(data.icon, `svg-${data.name}`, {
className,
onClick,
style,
'data-icon': data.name,
'aria-hidden': 'true',
...restProps,
'ref': ref,
})
})
IconBase.displayName = 'IconBase'
export default IconBase
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 36 KiB

@@ -0,0 +1,12 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_5968_39205)">
<rect width="512" height="512" rx="256" fill="#B2DDFF"/>
<circle opacity="0.68" cx="256" cy="196" r="84" fill="white"/>
<ellipse opacity="0.68" cx="256" cy="583.5" rx="266" ry="274.5" fill="white"/>
</g>
<defs>
<clipPath id="clip0_5968_39205">
<rect width="512" height="512" rx="256" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 465 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 364 KiB

@@ -0,0 +1,16 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 1H7.94339C11.8094 1 14.9434 4.13401 14.9434 8C14.9434 11.866 11.8094 15 7.9434 15H2V1Z" fill="white"/>
<path d="M2 1H7.94339C11.8094 1 14.9434 4.13401 14.9434 8C14.9434 11.866 11.8094 15 7.9434 15H2V1Z" fill="url(#paint0_angular_19344_240446)"/>
<path d="M7.94336 8H8.20751V15H7.94336V8Z" fill="url(#paint1_linear_19344_240446)"/>
<defs>
<radialGradient id="paint0_angular_19344_240446" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(7.9434 8) rotate(90) scale(8.75 8.75)">
<stop stop-color="#001FC2"/>
<stop offset="0.711334" stop-color="#0667F8" stop-opacity="0.2"/>
<stop offset="1" stop-color="#155EEF" stop-opacity="0"/>
</radialGradient>
<linearGradient id="paint1_linear_19344_240446" x1="8.06244" y1="8.43754" x2="7.93744" y2="9.20317" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0"/>
<stop offset="1" stop-color="white"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1,3 @@
<svg width="7" height="20" viewBox="0 0 7 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="Line 3" d="M1 19.3544L5.94174 0.645657" stroke="#EAECF0" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 193 B

@@ -0,0 +1,8 @@
<svg width="50" height="26" viewBox="0 0 50 26" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Dify">
<path d="M6.61784 2.064C8.37784 2.064 9.92184 2.408 11.2498 3.096C12.5938 3.784 13.6258 4.768 14.3458 6.048C15.0818 7.312 15.4498 8.784 15.4498 10.464C15.4498 12.144 15.0818 13.616 14.3458 14.88C13.6258 16.128 12.5938 17.096 11.2498 17.784C9.92184 18.472 8.37784 18.816 6.61784 18.816H0.761841V2.064H6.61784ZM6.49784 15.96C8.25784 15.96 9.61784 15.48 10.5778 14.52C11.5378 13.56 12.0178 12.208 12.0178 10.464C12.0178 8.72 11.5378 7.36 10.5778 6.384C9.61784 5.392 8.25784 4.896 6.49784 4.896H4.12184V15.96H6.49784Z" fill="#1D2939"/>
<path d="M20.869 3.936C20.277 3.936 19.781 3.752 19.381 3.384C18.997 3 18.805 2.528 18.805 1.968C18.805 1.408 18.997 0.944 19.381 0.576C19.781 0.192 20.277 0 20.869 0C21.461 0 21.949 0.192 22.333 0.576C22.733 0.944 22.933 1.408 22.933 1.968C22.933 2.528 22.733 3 22.333 3.384C21.949 3.752 21.461 3.936 20.869 3.936ZM22.525 5.52V18.816H19.165V5.52H22.525Z" fill="#1D2939"/>
<path d="M33.1407 8.28H30.8127V18.816H27.4047V8.28H25.8927V5.52H27.4047V4.848C27.4047 3.216 27.8687 2.016 28.7967 1.248C29.7247 0.48 31.1247 0.12 32.9967 0.168001V3C32.1807 2.984 31.6127 3.12 31.2927 3.408C30.9727 3.696 30.8127 4.216 30.8127 4.968V5.52H33.1407V8.28Z" fill="#1D2939"/>
<path d="M49.2381 5.52L41.0061 25.104H37.4301L40.3101 18.48L34.9821 5.52H38.7501L42.1821 14.808L45.6621 5.52H49.2381Z" fill="#1D2939"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,5 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="github">
<path id="Vector" d="M9 1.125C4.64906 1.125 1.125 4.64906 1.125 9C1.125 12.4847 3.37922 15.428 6.50953 16.4714C6.90328 16.5403 7.05094 16.3041 7.05094 16.0973C7.05094 15.9103 7.04109 15.2902 7.04109 14.6306C5.0625 14.9948 4.55062 14.1483 4.39312 13.7053C4.30453 13.4789 3.92063 12.78 3.58594 12.593C3.31031 12.4453 2.91656 12.0811 3.57609 12.0712C4.19625 12.0614 4.63922 12.6422 4.78688 12.8784C5.49563 14.0695 6.62766 13.7348 7.08047 13.5281C7.14938 13.0163 7.35609 12.6717 7.5825 12.4748C5.83031 12.278 3.99938 11.5987 3.99938 8.58656C3.99938 7.73016 4.30453 7.02141 4.80656 6.47016C4.72781 6.27328 4.45219 5.46609 4.88531 4.38328C4.88531 4.38328 5.54484 4.17656 7.05094 5.19047C7.68094 5.01328 8.35031 4.92469 9.01969 4.92469C9.68906 4.92469 10.3584 5.01328 10.9884 5.19047C12.4945 4.16672 13.1541 4.38328 13.1541 4.38328C13.5872 5.46609 13.3116 6.27328 13.2328 6.47016C13.7348 7.02141 14.04 7.72031 14.04 8.58656C14.04 11.6086 12.1992 12.278 10.447 12.4748C10.7325 12.7209 10.9786 13.1934 10.9786 13.9317C10.9786 14.985 10.9688 15.8316 10.9688 16.0973C10.9688 16.3041 11.1164 16.5502 11.5102 16.4714C13.0735 15.9436 14.432 14.9389 15.3943 13.5986C16.3567 12.2583 16.8746 10.65 16.875 9C16.875 4.64906 13.3509 1.125 9 1.125Z" fill="#24292F"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="46" height="24" viewBox="0 0 46 24" fill="none">
<path opacity="0.5" d="M-6.5 8C-6.5 3.58172 -2.91828 0 1.5 0H45.5L33.0248 24H1.49999C-2.91829 24 -6.5 20.4183 -6.5 16V8Z" fill="url(#paint0_linear_6333_42118)"/>
<defs>
<linearGradient id="paint0_linear_6333_42118" x1="1.81679" y1="5.47784e-07" x2="101.257" y2="30.3866" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.12"/>
<stop offset="1" stop-color="white" stop-opacity="0.3"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 561 B

@@ -0,0 +1,3 @@
<svg width="5" height="12" viewBox="0 0 5 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="Line 3" d="M1 11.3545L3.94174 0.645781" stroke="#D0D5DD" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 193 B

@@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="lock">
<path id="Vector" fill-rule="evenodd" clip-rule="evenodd" d="M8 1.75C6.27411 1.75 4.875 3.14911 4.875 4.875V6.125C3.83947 6.125 3 6.96444 3 8V12.375C3 13.4106 3.83947 14.25 4.875 14.25H11.125C12.1606 14.25 13 13.4106 13 12.375V8C13 6.96444 12.1606 6.125 11.125 6.125V4.875C11.125 3.14911 9.72587 1.75 8 1.75ZM9.875 6.125V4.875C9.875 3.83947 9.03556 3 8 3C6.96444 3 6.125 3.83947 6.125 4.875V6.125H9.875ZM8 8.625C8.34519 8.625 8.625 8.90481 8.625 9.25V11.125C8.625 11.4702 8.34519 11.75 8 11.75C7.65481 11.75 7.375 11.4702 7.375 11.125V9.25C7.375 8.90481 7.65481 8.625 8 8.625Z" fill="#155AEF"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 717 B

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.77438 6.6665H12.5591C12.9105 6.66649 13.2137 6.66648 13.4634 6.68688C13.727 6.70842 13.9891 6.75596 14.2414 6.88449C14.6177 7.07624 14.9237 7.3822 15.1154 7.75852C15.244 8.01078 15.2915 8.27292 15.313 8.53649C15.3334 8.7862 15.3334 9.08938 15.3334 9.44082V11.2974C15.3334 11.5898 15.3334 11.8421 15.3192 12.0509C15.3042 12.2708 15.2712 12.4908 15.1812 12.7081C14.9782 13.1981 14.5888 13.5875 14.0988 13.7905C13.8815 13.8805 13.6616 13.9135 13.4417 13.9285C13.4068 13.9308 13.3707 13.9328 13.3334 13.9345V14.6665C13.3334 14.9147 13.1955 15.1424 12.9756 15.2573C12.7556 15.3723 12.49 15.3556 12.2862 15.2139L10.8353 14.2051C10.6118 14.0498 10.5666 14.0214 10.5238 14.0021C10.4746 13.9798 10.4228 13.9635 10.3696 13.9537C10.3235 13.9452 10.2702 13.9427 9.99803 13.9427H8.7744C8.42296 13.9427 8.11978 13.9427 7.87006 13.9223C7.6065 13.9008 7.34435 13.8532 7.0921 13.7247C6.71578 13.533 6.40981 13.227 6.21807 12.8507C6.08954 12.5984 6.04199 12.3363 6.02046 12.0727C6.00006 11.823 6.00007 11.5198 6.00008 11.1684V9.44081C6.00007 9.08938 6.00006 8.7862 6.02046 8.53649C6.04199 8.27292 6.08954 8.01078 6.21807 7.75852C6.40981 7.3822 6.71578 7.07624 7.0921 6.88449C7.34435 6.75596 7.6065 6.70842 7.87006 6.68688C8.11978 6.66648 8.42295 6.66649 8.77438 6.6665Z" fill="#444CE7"/>
<path d="M9.4943 0.666504H4.5059C3.96926 0.666496 3.52635 0.666489 3.16555 0.695967C2.79082 0.726584 2.44635 0.792293 2.12279 0.957154C1.62103 1.21282 1.21308 1.62076 0.957417 2.12253C0.792557 2.44609 0.726847 2.79056 0.69623 3.16529C0.666752 3.52608 0.666759 3.96899 0.666768 4.50564L0.666758 7.6804C0.666669 7.97482 0.666603 8.19298 0.694924 8.38632C0.86568 9.55207 1.78121 10.4676 2.94695 10.6383C2.99461 10.6453 3.02432 10.6632 3.03714 10.6739L3.03714 11.7257C3.03711 11.9075 3.03708 12.0858 3.04976 12.2291C3.06103 12.3565 3.09053 12.6202 3.27795 12.8388C3.48686 13.0825 3.80005 13.2111 4.11993 13.1845C4.40689 13.1607 4.61323 12.9938 4.71072 12.9111C4.73849 12.8875 4.76726 12.8618 4.7968 12.8344C4.73509 12.594 4.70707 12.3709 4.69157 12.1813C4.66659 11.8756 4.66668 11.5224 4.66676 11.1966V9.41261C4.66668 9.08685 4.66659 8.73364 4.69157 8.42793C4.71984 8.08191 4.78981 7.62476 5.03008 7.15322C5.34965 6.52601 5.85959 6.01608 6.4868 5.6965C6.95834 5.45624 7.41549 5.38627 7.7615 5.358C8.06722 5.33302 8.42041 5.3331 8.74617 5.33318H12.5873C12.8311 5.33312 13.0903 5.33306 13.3334 5.3435V4.50562C13.3334 3.96898 13.3334 3.52608 13.304 3.16529C13.2734 2.79056 13.2076 2.44609 13.0428 2.12253C12.7871 1.62076 12.3792 1.21282 11.8774 0.957154C11.5539 0.792293 11.2094 0.726584 10.8347 0.695967C10.4739 0.666489 10.0309 0.666496 9.4943 0.666504Z" fill="#444CE7"/>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

@@ -0,0 +1,19 @@
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_13429_43710)">
<rect width="36" height="36" rx="8" fill="#FFF6ED"/>
<path opacity="0.7" d="M22.25 28C22.25 29.7949 20.7949 31.25 19 31.25C17.2051 31.25 15.75 29.7949 15.75 28C15.75 26.2051 17.2051 24.75 19 24.75C20.7949 24.75 22.25 26.2051 22.25 28Z" stroke="#FB6514" stroke-width="1.5"/>
<path d="M19 12C21.2091 12 23 10.2091 23 8C23 5.79086 21.2091 4 19 4C16.7909 4 15 5.79086 15 8C15 10.2091 16.7909 12 19 12Z" fill="#FB6514"/>
<path d="M15 22C17.2091 22 19 20.2091 19 18C19 15.7909 17.2091 14 15 14C12.7909 14 11 15.7909 11 18C11 20.2091 12.7909 22 15 22Z" fill="#FB6514"/>
<path d="M36 23C38.7614 23 41 20.7614 41 18C41 15.2386 38.7614 13 36 13C33.2386 13 31 15.2386 31 18C31 20.7614 33.2386 23 36 23Z" fill="#FB6514"/>
<path d="M0 18H10" stroke="#FB6514" stroke-width="1.5"/>
<path d="M20 18L30 18" stroke="#FB6514" stroke-width="1.5"/>
<path d="M0.00112438 15C0.00112438 15 -5.64364 15 0.851673 15C7.34699 15 7.84654 8 14 8" stroke="#FB6514" stroke-width="1.5"/>
<path d="M23.75 9.28125C26.5688 10.1847 27.699 13.2045 30.625 15.0312" stroke="#FB6514" stroke-width="1.5"/>
<path opacity="0.7" d="M-0.000543833 21C-0.000543833 21 -5.57819 21 0.893635 21C7.36546 21 7.8688 28 14 28" stroke="#FB6514" stroke-width="1.5"/>
</g>
<defs>
<clipPath id="clip0_13429_43710">
<rect width="36" height="36" rx="8" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,18 @@
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_13429_43700)">
<rect width="36" height="36" rx="8" fill="#EEF4FF"/>
<path opacity="0.7" d="M23.25 28C23.25 29.7949 21.7949 31.25 20 31.25C18.2051 31.25 16.75 29.7949 16.75 28C16.75 26.2051 18.2051 24.75 20 24.75C21.7949 24.75 23.25 26.2051 23.25 28Z" stroke="#444CE7" stroke-width="1.5"/>
<path opacity="0.7" d="M23.25 8C23.25 9.79493 21.7949 11.25 20 11.25C18.2051 11.25 16.75 9.79493 16.75 8C16.75 6.20507 18.2051 4.75 20 4.75C21.7949 4.75 23.25 6.20507 23.25 8Z" stroke="#444CE7" stroke-width="1.5"/>
<path d="M16 22C18.2091 22 20 20.2091 20 18C20 15.7909 18.2091 14 16 14C13.7909 14 12 15.7909 12 18C12 20.2091 13.7909 22 16 22Z" fill="#444CE7"/>
<path d="M36 23C38.7614 23 41 20.7614 41 18C41 15.2386 38.7614 13 36 13C33.2386 13 31 15.2386 31 18C31 20.7614 33.2386 23 36 23Z" fill="#444CE7"/>
<path d="M0 18L11 18" stroke="#444CE7" stroke-width="1.5"/>
<path d="M21 18L30 18" stroke="#444CE7" stroke-width="1.5"/>
<path opacity="0.7" d="M-0.00160408 15C-0.00160408 15 -6.00089 15 1.12411 15C8.24911 15 8.24908 8.25 14.9991 8.25" stroke="#444CE7" stroke-width="1.5"/>
<path opacity="0.7" d="M0.000488281 21C0.000488281 21 -5.92692 21 1.17228 21C8.27148 21 8.27423 27.75 14.9998 27.75" stroke="#444CE7" stroke-width="1.5"/>
</g>
<defs>
<clipPath id="clip0_13429_43700">
<rect width="36" height="36" rx="8" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,12 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_5364_42310)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.5725 18.2611L1.4229 15.5832C0.905706 14.9389 0.625 14.1466 0.625 13.3312V3.63437C0.625 2.4129 1.60224 1.39936 2.86295 1.31328L12.8326 0.632614C13.5569 0.583164 14.2768 0.775682 14.8717 1.17794L18.3745 3.5462C19.0015 3.97012 19.375 4.66312 19.375 5.40266V16.427C19.375 17.6223 18.4141 18.6121 17.1798 18.688L6.11458 19.3692C5.12958 19.4298 4.17749 19.0148 3.5725 18.2611Z" fill="white"/>
<path d="M7.03006 8.48663V8.35968C7.03006 8.03787 7.28779 7.77098 7.61997 7.7488L10.0396 7.58726L13.3857 12.5146V8.19003L12.5244 8.07522V8.01492C12.5244 7.68933 12.788 7.42068 13.1244 7.40344L15.326 7.29066V7.60749C15.326 7.75622 15.2154 7.88343 15.0638 7.90907L14.534 7.99868V15.0022L13.8691 15.2309C13.3136 15.4219 12.6952 15.2174 12.3772 14.7376L9.12879 9.83568V14.5143L10.1287 14.7056L10.1147 14.7984C10.0711 15.0889 9.82028 15.3086 9.51687 15.3221L7.03006 15.4328C6.99718 15.1204 7.23132 14.8409 7.55431 14.807L7.88143 14.7726V8.53447L7.03006 8.48663Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.9218 1.85418L2.95217 2.53485C2.35499 2.57562 1.89209 3.05572 1.89209 3.63431V13.3311C1.89209 13.8748 2.07923 14.4029 2.42402 14.8325L4.57362 17.5104C4.92117 17.9433 5.46812 18.1817 6.03397 18.1469L17.0991 17.4658C17.6663 17.4309 18.1078 16.9761 18.1078 16.4269V5.4026C18.1078 5.06281 17.9362 4.74441 17.6481 4.54963L14.1453 2.18137C13.7883 1.94002 13.3564 1.82451 12.9218 1.85418ZM3.44654 3.78556C3.30788 3.6829 3.37387 3.46903 3.54806 3.45654L12.9889 2.77938C13.2897 2.75781 13.5886 2.84064 13.8318 3.01299L15.7261 4.35502C15.798 4.40597 15.7642 4.51596 15.6752 4.5208L5.67742 5.06454C5.37485 5.081 5.0762 4.99211 4.83563 4.814L3.44654 3.78556ZM5.20848 6.76913C5.20848 6.44433 5.47088 6.17604 5.80642 6.15777L16.3769 5.5821C16.7039 5.56429 16.9792 5.81577 16.9792 6.13232V15.6782C16.9792 16.0024 16.7177 16.2705 16.3829 16.2895L5.8793 16.8871C5.51537 16.9079 5.20848 16.6282 5.20848 16.2759V6.76913Z" fill="black"/>
</g>
<defs>
<clipPath id="clip0_5364_42310">
<rect width="20" height="20" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

@@ -0,0 +1,6 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="sparkles-soft">
<path id="Vector" opacity="0.5" d="M10.9963 1.36798C10.9839 1.25339 10.8909 1.16677 10.7802 1.16666C10.6695 1.16654 10.5763 1.25295 10.5636 1.36752C10.5045 1.90085 10.3525 2.26673 10.1143 2.5149C9.87599 2.76307 9.52476 2.92145 9.01275 2.98296C8.90277 2.99618 8.81983 3.09324 8.81995 3.20856C8.82006 3.32388 8.90322 3.42076 9.0132 3.43373C9.51653 3.49312 9.87583 3.65148 10.1201 3.90135C10.3631 4.14986 10.518 4.51523 10.563 5.04321C10.573 5.16035 10.6673 5.25012 10.7802 5.24999C10.8931 5.24986 10.9872 5.15987 10.9969 5.0427C11.0401 4.52364 11.1949 4.15004 11.4394 3.89528C11.684 3.64052 12.0426 3.47926 12.5409 3.43433C12.6534 3.42419 12.7398 3.32619 12.7399 3.20858C12.7401 3.09097 12.6539 2.99277 12.5414 2.98236C12.0346 2.93546 11.6838 2.77407 11.4452 2.52098C11.2054 2.2665 11.0533 1.89229 10.9963 1.36798Z" fill="#F5F8FF"/>
<path id="Vector_2" d="M7.13646 2.85102C7.10442 2.55638 6.8653 2.33365 6.5806 2.33334C6.29595 2.33304 6.05633 2.55526 6.02374 2.84984C5.87186 4.22127 5.48089 5.1621 4.86827 5.80025C4.25565 6.43838 3.35245 6.84566 2.03587 7.00386C1.75307 7.03781 1.53975 7.28742 1.54004 7.58393C1.54033 7.88049 1.75415 8.12958 2.03701 8.16294C3.33132 8.31566 4.25509 8.72289 4.88328 9.36543C5.50807 10.0045 5.90647 10.9439 6.02222 12.3016C6.04793 12.6029 6.29035 12.8337 6.58066 12.8333C6.87102 12.833 7.11294 12.6016 7.13797 12.3003C7.24885 10.9656 7.64695 10.0049 8.27583 9.34979C8.90477 8.69471 9.82698 8.28002 11.1083 8.16452C11.3976 8.13844 11.6197 7.88644 11.62 7.58399C11.6204 7.28159 11.3988 7.02906 11.1096 7.00229C9.8062 6.88171 8.90432 6.46673 8.29084 5.81589C7.674 5.16152 7.28306 4.19926 7.13646 2.85102Z" fill="#F5F8FF"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -0,0 +1,24 @@
<svg width="32" height="34" viewBox="0 0 32 34" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="File Icons/csv">
<g id="sharp" filter="url(#filter0_d_6816_769)">
<path d="M4 7.73398C4 5.49377 4 4.37367 4.43597 3.51802C4.81947 2.76537 5.43139 2.15345 6.18404 1.76996C7.03969 1.33398 8.15979 1.33398 10.4 1.33398H18.6667L28 10.6673V24.2673C28 26.5075 28 27.6276 27.564 28.4833C27.1805 29.2359 26.5686 29.8478 25.816 30.2313C24.9603 30.6673 23.8402 30.6673 21.6 30.6673H10.4C8.15979 30.6673 7.03969 30.6673 6.18404 30.2313C5.43139 29.8478 4.81947 29.2359 4.43597 28.4833C4 27.6276 4 26.5075 4 24.2673V7.73398Z" fill="#169951"/>
</g>
<g id="CSV" opacity="0.96">
<path d="M13.0846 21.8908C12.8419 23.3562 11.8246 24.0562 10.5646 24.0562C9.78992 24.0562 9.20192 23.7948 8.71659 23.3095C8.01659 22.6095 8.04459 21.6762 8.04459 20.6775C8.04459 19.6788 8.01659 18.7455 8.71659 18.0455C9.20192 17.5602 9.78992 17.2988 10.5646 17.2988C11.8246 17.2988 12.8419 17.9988 13.0846 19.4642H11.4233C11.3206 19.0908 11.1153 18.7548 10.5739 18.7548C10.2753 18.7548 10.0513 18.8762 9.92992 19.0348C9.78059 19.2308 9.67792 19.4642 9.67792 20.6775C9.67792 21.8908 9.78059 22.1242 9.92992 22.3202C10.0513 22.4788 10.2753 22.6002 10.5739 22.6002C11.1153 22.6002 11.3206 22.2642 11.4233 21.8908H13.0846Z" fill="white"/>
<path d="M18.4081 21.9655C18.4081 23.3188 17.2414 24.0562 15.8414 24.0562C14.8241 24.0562 13.9934 23.8695 13.3214 23.1788L14.3668 22.1335C14.7121 22.4788 15.3188 22.6002 15.8508 22.6002C16.4948 22.6002 16.8028 22.3855 16.8028 22.0028C16.8028 21.8442 16.7654 21.7135 16.6721 21.6108C16.5881 21.5268 16.4481 21.4615 16.2334 21.4335L15.4308 21.3215C14.8428 21.2375 14.3948 21.0415 14.0961 20.7335C13.7881 20.4162 13.6388 19.9682 13.6388 19.3988C13.6388 18.1855 14.5534 17.2988 16.0654 17.2988C17.0174 17.2988 17.7361 17.5228 18.3054 18.0922L17.2788 19.1188C16.8588 18.6988 16.3081 18.7268 16.0188 18.7268C15.4494 18.7268 15.2161 19.0535 15.2161 19.3428C15.2161 19.4268 15.2441 19.5482 15.3468 19.6508C15.4308 19.7348 15.5708 19.8188 15.8041 19.8468L16.6068 19.9588C17.2041 20.0428 17.6334 20.2295 17.9134 20.5095C18.2681 20.8548 18.4081 21.3495 18.4081 21.9655Z" fill="white"/>
<path d="M24.4166 17.3548L22.214 24.0002H21.0006L18.8073 17.3548H20.4966L21.6166 21.0695L22.718 17.3548H24.4166Z" fill="white"/>
</g>
<path id="bevel" opacity="0.5" d="M18.6667 1.33398L28.0001 10.6673H21.3334C19.8607 10.6673 18.6667 9.47341 18.6667 8.00065V1.33398Z" fill="white"/>
</g>
<defs>
<filter id="filter0_d_6816_769" x="2" y="0.333984" width="28" height="33.334" 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="1"/>
<feGaussianBlur stdDeviation="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.0627451 0 0 0 0 0.0941176 0 0 0 0 0.156863 0 0 0 0.05 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_6816_769"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_6816_769" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

@@ -0,0 +1,22 @@
<svg width="32" height="34" viewBox="0 0 32 34" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_17194_49206)">
<path d="M4 7.73301C4 5.4928 4 4.37269 4.43597 3.51705C4.81947 2.7644 5.43139 2.15248 6.18404 1.76898C7.03969 1.33301 8.15979 1.33301 10.4 1.33301H18.6667L28 10.6663V24.2663C28 26.5066 28 27.6267 27.564 28.4823C27.1805 29.2349 26.5686 29.8469 25.816 30.2304C24.9603 30.6663 23.8402 30.6663 21.6 30.6663H10.4C8.15979 30.6663 7.03969 30.6663 6.18404 30.2304C5.43139 29.8469 4.81947 29.2349 4.43597 28.4823C4 27.6267 4 26.5066 4 24.2663V7.73301Z" fill="#2349A9"/>
</g>
<path opacity="0.5" d="M18.6665 1.33301L27.9998 10.6663H21.3332C19.8604 10.6663 18.6665 9.47243 18.6665 7.99967V1.33301Z" fill="white"/>
<g opacity="0.96">
<path d="M13.6329 21.4112C13.6329 22.2603 13.7059 22.9501 13.0326 23.5793C12.6351 23.9508 12.0754 24.11 11.4751 24.11H9.3335V18.7125H11.4751C12.0754 18.7125 12.6351 18.8717 13.0326 19.2431C13.7059 19.8723 13.6329 20.5622 13.6329 21.4112ZM12.2133 21.4112C12.2133 20.5015 12.1727 20.3499 12.0591 20.1983C11.9293 20.0164 11.7347 19.8951 11.3777 19.8951H10.7531V22.9274H11.3777C11.7347 22.9274 11.9293 22.8061 12.0591 22.6242C12.1727 22.4725 12.2133 22.3285 12.2133 21.4112Z" fill="white"/>
<path d="M18.8275 21.4112C18.8275 22.2224 18.8519 22.9805 18.2435 23.549C17.8217 23.9432 17.3349 24.1555 16.6292 24.1555C15.9234 24.1555 15.4367 23.9432 15.0149 23.549C14.4065 22.9805 14.4308 22.2224 14.4308 21.4112C14.4308 20.6001 14.4065 19.842 15.0149 19.2735C15.4367 18.8793 15.9234 18.667 16.6292 18.667C17.3349 18.667 17.8217 18.8793 18.2435 19.2735C18.8519 19.842 18.8275 20.6001 18.8275 21.4112ZM17.4079 21.4112C17.4079 20.4257 17.3268 20.2438 17.197 20.0846C17.0916 19.9557 16.8888 19.8496 16.6292 19.8496C16.3696 19.8496 16.1668 19.9557 16.0613 20.0846C15.9316 20.2438 15.8504 20.4257 15.8504 21.4112C15.8504 22.3967 15.9316 22.5711 16.0613 22.7303C16.1668 22.8592 16.3696 22.9729 16.6292 22.9729C16.8888 22.9729 17.0916 22.8592 17.197 22.7303C17.3268 22.5711 17.4079 22.3967 17.4079 21.4112Z" fill="white"/>
<path d="M24.0002 22.3967C23.7893 23.5869 22.905 24.1555 21.8099 24.1555C21.1366 24.1555 20.6256 23.9432 20.2037 23.549C19.5953 22.9805 19.6197 22.2224 19.6197 21.4112C19.6197 20.6001 19.5953 19.842 20.2037 19.2735C20.6256 18.8793 21.1366 18.667 21.8099 18.667C22.905 18.667 23.7893 19.2356 24.0002 20.4257H22.5562C22.467 20.1225 22.2885 19.8496 21.818 19.8496C21.5584 19.8496 21.3638 19.9481 21.2583 20.077C21.1285 20.2362 21.0393 20.4257 21.0393 21.4112C21.0393 22.3967 21.1285 22.5863 21.2583 22.7455C21.3638 22.8743 21.5584 22.9729 21.818 22.9729C22.2885 22.9729 22.467 22.7 22.5562 22.3967H24.0002Z" fill="white"/>
</g>
<defs>
<filter id="filter0_d_17194_49206" x="2" y="0.333008" width="28" height="33.333" 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="1"/>
<feGaussianBlur stdDeviation="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.0627451 0 0 0 0 0.0941176 0 0 0 0 0.156863 0 0 0 0.05 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_17194_49206"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_17194_49206" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

@@ -0,0 +1,23 @@
<svg width="32" height="34" viewBox="0 0 32 34" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_10291_62253)">
<path d="M4 7.73301C4 5.4928 4 4.37269 4.43597 3.51705C4.81947 2.7644 5.43139 2.15248 6.18404 1.76898C7.03969 1.33301 8.15979 1.33301 10.4 1.33301H18.6667L28 10.6663V24.2663C28 26.5065 28 27.6267 27.564 28.4823C27.1805 29.2349 26.5686 29.8469 25.816 30.2304C24.9603 30.6663 23.8402 30.6663 21.6 30.6663H10.4C8.15979 30.6663 7.03969 30.6663 6.18404 30.2304C5.43139 29.8469 4.81947 29.2349 4.43597 28.4823C4 27.6267 4 26.5065 4 24.2663V7.73301Z" fill="#2349A9"/>
</g>
<path opacity="0.5" d="M18.6665 1.33301L27.9998 10.6663H21.3332C19.8604 10.6663 18.6665 9.47243 18.6665 7.99967V1.33301Z" fill="white"/>
<g opacity="0.96">
<path d="M10.8443 21.3337C10.8443 22.1587 10.9153 22.8291 10.261 23.4405C9.87477 23.8014 9.33086 23.9561 8.74754 23.9561H6.6665V18.7112H8.74754C9.33086 18.7112 9.87477 18.8659 10.261 19.2268C10.9153 19.8383 10.8443 20.5086 10.8443 21.3337ZM9.46487 21.3337C9.46487 20.4497 9.42545 20.3024 9.31509 20.155C9.18897 19.9782 8.99979 19.8604 8.65295 19.8604H8.04598V22.807H8.65295C8.99979 22.807 9.18897 22.6891 9.31509 22.5123C9.42545 22.365 9.46487 22.225 9.46487 21.3337Z" fill="white"/>
<path d="M15.8922 21.3337C15.8922 22.1219 15.9158 22.8585 15.3246 23.411C14.9147 23.7941 14.4418 24.0003 13.756 24.0003C13.0702 24.0003 12.5972 23.7941 12.1873 23.411C11.5961 22.8585 11.6197 22.1219 11.6197 21.3337C11.6197 20.5454 11.5961 19.8088 12.1873 19.2563C12.5972 18.8733 13.0702 18.667 13.756 18.667C14.4418 18.667 14.9147 18.8733 15.3246 19.2563C15.9158 19.8088 15.8922 20.5454 15.8922 21.3337ZM14.5127 21.3337C14.5127 20.376 14.4339 20.1992 14.3077 20.0445C14.2053 19.9193 14.0082 19.8162 13.756 19.8162C13.5037 19.8162 13.3066 19.9193 13.2042 20.0445C13.078 20.1992 12.9992 20.376 12.9992 21.3337C12.9992 22.2913 13.078 22.4607 13.2042 22.6154C13.3066 22.7407 13.5037 22.8512 13.756 22.8512C14.0082 22.8512 14.2053 22.7407 14.3077 22.6154C14.4339 22.4607 14.5127 22.2913 14.5127 21.3337Z" fill="white"/>
<path d="M20.9186 22.2913C20.7136 23.4478 19.8544 24.0003 18.7902 24.0003C18.136 24.0003 17.6394 23.7941 17.2295 23.411C16.6383 22.8585 16.6619 22.1219 16.6619 21.3337C16.6619 20.5454 16.6383 19.8088 17.2295 19.2563C17.6394 18.8733 18.136 18.667 18.7902 18.667C19.8544 18.667 20.7136 19.2195 20.9186 20.376H19.5154C19.4287 20.0814 19.2553 19.8162 18.7981 19.8162C18.5459 19.8162 18.3567 19.9119 18.2542 20.0372C18.1281 20.1919 18.0414 20.376 18.0414 21.3337C18.0414 22.2913 18.1281 22.4755 18.2542 22.6302C18.3567 22.7554 18.5459 22.8512 18.7981 22.8512C19.2553 22.8512 19.4287 22.586 19.5154 22.2913H20.9186Z" fill="white"/>
<path d="M25.9998 23.9561H24.4233L23.501 22.3429L22.5787 23.9561H21.0022L22.7522 21.2674L21.1126 18.7112H22.6812L23.501 20.1919L24.3208 18.7112H25.8895L24.2499 21.2674L25.9998 23.9561Z" fill="white"/>
</g>
<defs>
<filter id="filter0_d_10291_62253" x="2" y="0.333008" width="28" height="33.333" 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="1"/>
<feGaussianBlur stdDeviation="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.0627451 0 0 0 0 0.0941176 0 0 0 0 0.156863 0 0 0 0.05 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_10291_62253"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_10291_62253" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

@@ -0,0 +1,23 @@
<svg width="32" height="34" viewBox="0 0 32 34" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_3055_14424)">
<path d="M4 7.73349C4 5.49329 4 4.37318 4.43597 3.51753C4.81947 2.76489 5.43139 2.15296 6.18404 1.76947C7.03969 1.3335 8.15979 1.3335 10.4 1.3335H18.6667L28 10.6668V24.2668C28 26.507 28 27.6271 27.564 28.4828C27.1805 29.2354 26.5686 29.8474 25.816 30.2309C24.9603 30.6668 23.8402 30.6668 21.6 30.6668H10.4C8.15979 30.6668 7.03969 30.6668 6.18404 30.2309C5.43139 29.8474 4.81947 29.2354 4.43597 28.4828C4 27.6271 4 26.507 4 24.2668V7.73349Z" fill="#EC5B27"/>
</g>
<g opacity="0.96">
<path d="M10.2704 24.0002V18.3042H8.87042V20.4962H7.38242V18.3042H5.98242V24.0002H7.38242V21.7442H8.87042V24.0002H10.2704Z" fill="white"/>
<path d="M15.2839 19.5522V18.3042H11.0839V19.5522H12.4839V24.0002H13.8839V19.5522H15.2839Z" fill="white"/>
<path d="M21.4116 24.0002V18.3042H20.0356L18.7556 20.8162L17.4756 18.3042H16.0996V24.0002H17.4996V21.2722L18.3076 22.6802H19.2036L20.0116 21.2722V24.0002H21.4116Z" fill="white"/>
<path d="M26.3525 24.0002V22.7522H23.9605V18.3042H22.5605V24.0002H26.3525Z" fill="white"/>
</g>
<path opacity="0.5" d="M18.6665 1.3335L27.9998 10.6668H21.3332C19.8604 10.6668 18.6665 9.47292 18.6665 8.00016V1.3335Z" fill="white"/>
<defs>
<filter id="filter0_d_3055_14424" x="2" y="0.333496" width="28" height="33.3335" 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="1"/>
<feGaussianBlur stdDeviation="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.0627451 0 0 0 0 0.0941176 0 0 0 0 0.156863 0 0 0 0.05 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_3055_14424"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_3055_14424" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -0,0 +1,23 @@
<svg width="32" height="34" viewBox="0 0 32 34" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_3055_14428)">
<path d="M4 7.73349C4 5.49329 4 4.37318 4.43597 3.51753C4.81947 2.76489 5.43139 2.15296 6.18404 1.76947C7.03969 1.3335 8.15979 1.3335 10.4 1.3335H18.6667L28 10.6668V24.2668C28 26.507 28 27.6271 27.564 28.4828C27.1805 29.2354 26.5686 29.8474 25.816 30.2309C24.9603 30.6668 23.8402 30.6668 21.6 30.6668H10.4C8.15979 30.6668 7.03969 30.6668 6.18404 30.2309C5.43139 29.8474 4.81947 29.2354 4.43597 28.4828C4 27.6271 4 26.507 4 24.2668V7.73349Z" fill="#2D2D2E"/>
</g>
<g opacity="0.96">
<path d="M9.83907 22.0479V18.3039H8.43907V22.0159C8.43907 22.5599 8.12707 22.7999 7.69507 22.7999C7.38307 22.7999 7.23907 22.6879 7.06307 22.5119L6.14307 23.4239C6.60707 23.8879 7.03107 24.0479 7.69507 24.0479C8.76707 24.0479 9.83907 23.3999 9.83907 22.0479Z" fill="white"/>
<path d="M14.7321 22.2559C14.7321 21.7279 14.6121 21.3039 14.3081 21.0079C14.0681 20.7679 13.7001 20.6079 13.1881 20.5359L12.5001 20.4399C12.3001 20.4159 12.1801 20.3439 12.1081 20.2719C12.0201 20.1839 11.9961 20.0799 11.9961 20.0079C11.9961 19.7599 12.1961 19.4799 12.6841 19.4799C12.9321 19.4799 13.4041 19.4559 13.7641 19.8159L14.6441 18.9359C14.1561 18.4479 13.5401 18.2559 12.7241 18.2559C11.4281 18.2559 10.6441 19.0159 10.6441 20.0559C10.6441 20.5439 10.7721 20.9279 11.0361 21.1999C11.2921 21.4639 11.6761 21.6319 12.1801 21.7039L12.8681 21.7999C13.0521 21.8239 13.1721 21.8799 13.2441 21.9519C13.3241 22.0399 13.3561 22.1519 13.3561 22.2879C13.3561 22.6159 13.0921 22.7999 12.5401 22.7999C12.0841 22.7999 11.5641 22.6959 11.2681 22.3999L10.3721 23.2959C10.9481 23.8879 11.6601 24.0479 12.5321 24.0479C13.7321 24.0479 14.7321 23.4159 14.7321 22.2559Z" fill="white"/>
<path d="M19.8023 21.1519C19.8023 20.2959 19.8263 19.4959 19.2263 18.8959C18.8103 18.4799 18.3303 18.2559 17.6343 18.2559C16.9383 18.2559 16.4583 18.4799 16.0423 18.8959C15.4423 19.4959 15.4663 20.2959 15.4663 21.1519C15.4663 22.0079 15.4423 22.8079 16.0423 23.4079C16.4583 23.8239 16.9383 24.0479 17.6343 24.0479C18.3303 24.0479 18.8103 23.8239 19.2263 23.4079C19.8263 22.8079 19.8023 22.0079 19.8023 21.1519ZM18.4023 21.1519C18.4023 22.1919 18.3223 22.3759 18.1943 22.5439C18.0903 22.6799 17.8903 22.7999 17.6343 22.7999C17.3783 22.7999 17.1783 22.6799 17.0743 22.5439C16.9463 22.3759 16.8663 22.1919 16.8663 21.1519C16.8663 20.1119 16.9463 19.9199 17.0743 19.7519C17.1783 19.6159 17.3783 19.5039 17.6343 19.5039C17.8903 19.5039 18.0903 19.6159 18.1943 19.7519C18.3223 19.9199 18.4023 20.1119 18.4023 21.1519Z" fill="white"/>
<path d="M25.2154 23.9999V18.3039H23.8154V21.1679L21.9914 18.3039H20.7674V23.9999H22.1674V21.1359L23.9914 23.9999H25.2154Z" fill="white"/>
</g>
<path opacity="0.5" d="M18.6665 1.3335L27.9998 10.6668H21.3332C19.8604 10.6668 18.6665 9.47292 18.6665 8.00016V1.3335Z" fill="white"/>
<defs>
<filter id="filter0_d_3055_14428" x="2" y="0.333496" width="28" height="33.3335" 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="1"/>
<feGaussianBlur stdDeviation="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.0627451 0 0 0 0 0.0941176 0 0 0 0 0.156863 0 0 0 0.05 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_3055_14428"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_3055_14428" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

@@ -0,0 +1,18 @@
<svg width="32" height="34" viewBox="0 0 32 34" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_3777_37339)">
<path d="M4 7.73349C4 5.49329 4 4.37318 4.43597 3.51753C4.81947 2.76489 5.43139 2.15296 6.18404 1.76947C7.03969 1.3335 8.15979 1.3335 10.4 1.3335H18.6667L28 10.6668V24.2668C28 26.507 28 27.6271 27.564 28.4828C27.1805 29.2354 26.5686 29.8474 25.816 30.2309C24.9603 30.6668 23.8402 30.6668 21.6 30.6668H10.4C8.15979 30.6668 7.03969 30.6668 6.18404 30.2309C5.43139 29.8474 4.81947 29.2354 4.43597 28.4828C4 27.6271 4 26.507 4 24.2668V7.73349Z" fill="#309BEC"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M21.9904 25.3335H10.0096C9.45202 25.3335 9 24.9138 9 24.396V18.271C9 17.7532 9.45202 17.3335 10.0096 17.3335H21.9904C22.548 17.3335 23 17.7532 23 18.271V24.396C23 24.9138 22.548 25.3335 21.9904 25.3335ZM12.3654 23.4585V21.021L13.7115 22.5835L15.0577 21.021V23.4585H16.4038V19.2085H15.0577L13.7115 20.771L12.3654 19.2085H11.0192V23.4585H12.3654ZM20.0385 21.3335H21.3846L19.3654 23.521L17.3462 21.3335H18.6923V19.2085H20.0385V21.3335Z" fill="white"/>
<path opacity="0.5" d="M18.6665 1.3335L27.9998 10.6668H21.3332C19.8604 10.6668 18.6665 9.47292 18.6665 8.00016V1.3335Z" fill="white"/>
<defs>
<filter id="filter0_d_3777_37339" x="2" y="0.333496" width="28" height="33.3335" 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="1"/>
<feGaussianBlur stdDeviation="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.0627451 0 0 0 0 0.0941176 0 0 0 0 0.156863 0 0 0 0.05 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_3777_37339"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_3777_37339" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -0,0 +1,22 @@
<svg width="32" height="34" viewBox="0 0 32 34" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_3055_14420)">
<path d="M4 7.73349C4 5.49329 4 4.37318 4.43597 3.51753C4.81947 2.76489 5.43139 2.15296 6.18404 1.76947C7.03969 1.3335 8.15979 1.3335 10.4 1.3335H18.6667L28 10.6668V24.2668C28 26.507 28 27.6271 27.564 28.4828C27.1805 29.2354 26.5686 29.8474 25.816 30.2309C24.9603 30.6668 23.8402 30.6668 21.6 30.6668H10.4C8.15979 30.6668 7.03969 30.6668 6.18404 30.2309C5.43139 29.8474 4.81947 29.2354 4.43597 28.4828C4 27.6271 4 26.507 4 24.2668V7.73349Z" fill="#DD3633"/>
</g>
<g opacity="0.96">
<path d="M13.2801 20.1362C13.2801 19.2002 12.6001 18.3042 11.3361 18.3042H9.08008V24.0002H10.4801V21.9682H11.3361C12.6001 21.9682 13.2801 21.0722 13.2801 20.1362ZM11.8801 20.1362C11.8801 20.4322 11.6561 20.7122 11.2721 20.7122H10.4801V19.5602H11.2721C11.6561 19.5602 11.8801 19.8402 11.8801 20.1362Z" fill="white"/>
<path d="M18.3357 21.1522C18.3357 20.2562 18.4077 19.5282 17.7437 18.8642C17.3517 18.4722 16.7997 18.3042 16.2077 18.3042H14.0957V24.0002H16.2077C16.7997 24.0002 17.3517 23.8322 17.7437 23.4402C18.4077 22.7762 18.3357 22.0482 18.3357 21.1522ZM16.9357 21.1522C16.9357 22.1202 16.8957 22.2722 16.7837 22.4322C16.6557 22.6242 16.4637 22.7522 16.1117 22.7522H15.4957V19.5522H16.1117C16.4637 19.5522 16.6557 19.6802 16.7837 19.8722C16.8957 20.0322 16.9357 20.1922 16.9357 21.1522Z" fill="white"/>
<path d="M23.1786 19.5522V18.3042H19.3066V24.0002H20.7066V21.8002H22.8186V20.5522H20.7066V19.5522H23.1786Z" fill="white"/>
</g>
<path opacity="0.5" d="M18.6665 1.3335L27.9998 10.6668H21.3332C19.8604 10.6668 18.6665 9.47292 18.6665 8.00016V1.3335Z" fill="white"/>
<defs>
<filter id="filter0_d_3055_14420" x="2" y="0.333496" width="28" height="33.3335" 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="1"/>
<feGaussianBlur stdDeviation="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.0627451 0 0 0 0 0.0941176 0 0 0 0 0.156863 0 0 0 0.05 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_3055_14420"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_3055_14420" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

@@ -0,0 +1,23 @@
<svg width="32" height="34" viewBox="0 0 32 34" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_3055_14432)">
<path d="M4 7.73349C4 5.49329 4 4.37318 4.43597 3.51753C4.81947 2.76489 5.43139 2.15296 6.18404 1.76947C7.03969 1.3335 8.15979 1.3335 10.4 1.3335H18.6667L28 10.6668V24.2668C28 26.507 28 27.6271 27.564 28.4828C27.1805 29.2354 26.5686 29.8474 25.816 30.2309C24.9603 30.6668 23.8402 30.6668 21.6 30.6668H10.4C8.15979 30.6668 7.03969 30.6668 6.18404 30.2309C5.43139 29.8474 4.81947 29.2354 4.43597 28.4828C4 27.6271 4 26.507 4 24.2668V7.73349Z" fill="#E3E5E8"/>
<path d="M4.25 7.73349C4.25 6.60926 4.25019 5.78113 4.30367 5.12666C4.3569 4.47511 4.46169 4.01774 4.65873 3.63103C5.01825 2.92542 5.59193 2.35175 6.29754 1.99222C6.68424 1.79518 7.14162 1.6904 7.79317 1.63716C8.44763 1.58369 9.27577 1.5835 10.4 1.5835H18.5631L27.75 10.7704V24.2668C27.75 25.3911 27.7498 26.2192 27.6963 26.8737C27.6431 27.5252 27.5383 27.9826 27.3413 28.3693C26.9817 29.0749 26.4081 29.6486 25.7025 30.0081C25.3158 30.2051 24.8584 30.3099 24.2068 30.3632C23.5524 30.4166 22.7242 30.4168 21.6 30.4168H10.4C9.27577 30.4168 8.44763 30.4166 7.79317 30.3632C7.14162 30.3099 6.68424 30.2051 6.29754 30.0081C5.59193 29.6486 5.01825 29.0749 4.65873 28.3693C4.46169 27.9826 4.3569 27.5252 4.30367 26.8737C4.25019 26.2192 4.25 25.3911 4.25 24.2668V7.73349Z" stroke="black" stroke-opacity="0.03" stroke-width="0.5"/>
</g>
<g opacity="0.96">
<path d="M13.2254 19.5522V18.3042H9.02539V19.5522H10.4254V24.0002H11.8254V19.5522H13.2254Z" fill="#667085"/>
<path d="M18.5371 24.0002L16.7611 21.0802L18.4251 18.3042H16.8331L16.0011 19.9122L15.1691 18.3042H13.5771L15.2411 21.0802L13.4651 24.0002H15.0651L16.0011 22.2482L16.9371 24.0002H18.5371Z" fill="#667085"/>
<path d="M22.9754 19.5522V18.3042H18.7754V19.5522H20.1754V24.0002H21.5754V19.5522H22.9754Z" fill="#667085"/>
</g>
<path opacity="0.5" d="M18.6665 1.3335L27.9998 10.6668H21.3332C19.8604 10.6668 18.6665 9.47292 18.6665 8.00016V1.3335Z" fill="white"/>
<defs>
<filter id="filter0_d_3055_14432" x="2" y="0.333496" width="28" height="33.3335" 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="1"/>
<feGaussianBlur stdDeviation="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.0627451 0 0 0 0 0.0941176 0 0 0 0 0.156863 0 0 0 0.05 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_3055_14432"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_3055_14432" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

@@ -0,0 +1,23 @@
<svg width="32" height="34" viewBox="0 0 32 34" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_3055_14436)">
<path d="M4 7.73349C4 5.49329 4 4.37318 4.43597 3.51753C4.81947 2.76489 5.43139 2.15296 6.18404 1.76947C7.03969 1.3335 8.15979 1.3335 10.4 1.3335H18.6667L28 10.6668V24.2668C28 26.507 28 27.6271 27.564 28.4828C27.1805 29.2354 26.5686 29.8474 25.816 30.2309C24.9603 30.6668 23.8402 30.6668 21.6 30.6668H10.4C8.15979 30.6668 7.03969 30.6668 6.18404 30.2309C5.43139 29.8474 4.81947 29.2354 4.43597 28.4828C4 27.6271 4 26.507 4 24.2668V7.73349Z" fill="#E3E5E8"/>
<path d="M4.25 7.73349C4.25 6.60926 4.25019 5.78113 4.30367 5.12666C4.3569 4.47511 4.46169 4.01774 4.65873 3.63103C5.01825 2.92542 5.59193 2.35175 6.29754 1.99222C6.68424 1.79518 7.14162 1.6904 7.79317 1.63716C8.44763 1.58369 9.27577 1.5835 10.4 1.5835H18.5631L27.75 10.7704V24.2668C27.75 25.3911 27.7498 26.2192 27.6963 26.8737C27.6431 27.5252 27.5383 27.9826 27.3413 28.3693C26.9817 29.0749 26.4081 29.6486 25.7025 30.0081C25.3158 30.2051 24.8584 30.3099 24.2068 30.3632C23.5524 30.4166 22.7242 30.4168 21.6 30.4168H10.4C9.27577 30.4168 8.44763 30.4166 7.79317 30.3632C7.14162 30.3099 6.68424 30.2051 6.29754 30.0081C5.59193 29.6486 5.01825 29.0749 4.65873 28.3693C4.46169 27.9826 4.3569 27.5252 4.30367 26.8737C4.25019 26.2192 4.25 25.3911 4.25 24.2668V7.73349Z" stroke="black" stroke-opacity="0.03" stroke-width="0.5"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.9998 23.1992C15.8014 23.1992 15.6039 23.1968 15.4077 23.1924V24.0549C15.4077 24.3819 15.6728 24.647 15.9998 24.647C16.3268 24.647 16.592 24.3819 16.592 24.0549V23.1924C16.3957 23.1968 16.1983 23.1992 15.9998 23.1992Z" fill="#98A2B3"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.0984 22.8838L11.757 23.8593C11.649 24.168 11.8117 24.5058 12.1203 24.6138C12.185 24.6364 12.251 24.6472 12.3159 24.6472C12.5605 24.6472 12.7894 24.4944 12.8747 24.2505L13.2936 23.0534C12.8807 23.0073 12.481 22.9506 12.0984 22.8838Z" fill="#98A2B3"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.2431 23.8593L19.9018 22.8838C19.5192 22.9506 19.1195 23.0073 18.7065 23.0534L19.1254 24.2505C19.2108 24.4944 19.4396 24.6472 19.6843 24.6472C19.7491 24.6472 19.8151 24.6364 19.8798 24.6138C20.1885 24.5058 20.3511 24.168 20.2431 23.8593Z" fill="#98A2B3"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.1624 17.2634C20.2697 17.6416 20.3254 18.0369 20.3254 18.4409C20.3254 18.9087 20.05 19.3327 19.6226 19.5228C19.5564 19.5522 17.9801 20.2436 16.0359 20.2436C14.0917 20.2436 12.5153 19.5522 12.4492 19.5228C12.0218 19.3327 11.7464 18.9086 11.7464 18.4409C11.7464 18.0312 11.8037 17.6305 11.914 17.2476C10.3343 17.5645 8.5 18.2009 8.5 19.4464C8.5 20.2859 9.32512 20.9477 10.9525 21.4134C11.4194 21.547 11.9381 21.66 12.4949 21.7506C12.8783 21.813 13.28 21.8648 13.6953 21.9056C14.2455 21.9597 14.8197 21.9942 15.4079 22.0082C15.6039 22.0128 15.8013 22.0153 16 22.0153C16.1987 22.0153 16.3962 22.0128 16.5921 22.0082C17.1803 21.9943 17.7545 21.9596 18.3047 21.9056C18.72 21.8648 19.1217 21.8131 19.5051 21.7506C20.062 21.66 20.5807 21.547 21.0476 21.4134C22.6749 20.9477 23.5 20.2859 23.5 19.4464C23.5 18.2187 21.7108 17.5833 20.1624 17.2634Z" fill="#98A2B3"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.8441 17.1144C18.7585 16.9335 18.6559 16.7622 18.5384 16.6025C18.4174 16.4382 18.2809 16.286 18.1307 16.1486C17.5784 15.6437 16.8433 15.3354 16.036 15.3354C15.2318 15.3354 14.499 15.6411 13.9476 16.1426C13.7974 16.2791 13.6609 16.4303 13.5399 16.5937C13.4217 16.753 13.3185 16.924 13.2322 17.1048C13.039 17.5095 12.9307 17.9624 12.9307 18.4407C12.9307 18.4407 14.321 19.0592 16.036 19.0592C17.751 19.0592 19.1412 18.4407 19.1412 18.4407C19.1412 17.9662 19.0344 17.5167 18.8441 17.1144Z" fill="#98A2B3"/>
<path opacity="0.5" d="M18.6665 1.3335L27.9998 10.6668H21.3332C19.8604 10.6668 18.6665 9.47292 18.6665 8.00016V1.3335Z" fill="white"/>
<defs>
<filter id="filter0_d_3055_14436" x="2" y="0.333496" width="28" height="33.3335" 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="1"/>
<feGaussianBlur stdDeviation="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.0627451 0 0 0 0 0.0941176 0 0 0 0 0.156863 0 0 0 0.05 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_3055_14436"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_3055_14436" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

@@ -0,0 +1,18 @@
<svg width="24" height="26" viewBox="0 0 24 26" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_5938_927)">
<path d="M3 5.8C3 4.11984 3 3.27976 3.32698 2.63803C3.6146 2.07354 4.07354 1.6146 4.63803 1.32698C5.27976 1 6.11984 1 7.8 1H14L21 8V18.2C21 19.8802 21 20.7202 20.673 21.362C20.3854 21.9265 19.9265 22.3854 19.362 22.673C18.7202 23 17.8802 23 16.2 23H7.8C6.11984 23 5.27976 23 4.63803 22.673C4.07354 22.3854 3.6146 21.9265 3.32698 21.362C3 20.7202 3 19.8802 3 18.2V5.8Z" fill="#169951"/>
</g>
<path opacity="0.5" d="M14 1L21 8H16C14.8954 8 14 7.10457 14 6V1Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M17 12C17.5523 12 18 12.4477 18 13V18C18 18.5523 17.5523 19 17 19H7C6.44772 19 6 18.5523 6 18V13C6 12.4477 6.44772 12 7 12H17ZM11.5 13H7L7 15H11.5V13ZM12.5 18H17V16H12.5V18ZM11.5 16V18H7L7 16H11.5ZM12.5 15H17V13H12.5V15Z" fill="white" fill-opacity="0.96"/>
<defs>
<filter id="filter0_d_5938_927" x="1" y="0" width="22" height="26" 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="1"/>
<feGaussianBlur stdDeviation="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.0627451 0 0 0 0 0.0941176 0 0 0 0 0.156863 0 0 0 0.05 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_5938_927"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_5938_927" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -0,0 +1 @@
<svg fill="none" height="26" viewBox="0 0 24 26" width="24" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><filter id="a" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse" height="26" width="22" x="1" y="0"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" result="hardAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dy="1"/><feGaussianBlur stdDeviation="1"/><feColorMatrix type="matrix" values="0 0 0 0 0.0627451 0 0 0 0 0.0941176 0 0 0 0 0.156863 0 0 0 0.05 0"/><feBlend in2="BackgroundImageFix" mode="normal" result="effect1_dropShadow_7605_8828"/><feBlend in="SourceGraphic" in2="effect1_dropShadow_7605_8828" mode="normal" result="shape"/></filter><g filter="url(#a)"><path d="m3 5.8c0-1.68016 0-2.52024.32698-3.16197.28762-.56449.74656-1.02343 1.31105-1.31105.64173-.32698 1.48181-.32698 3.16197-.32698h6.2l7 7v10.2c0 1.6802 0 2.5202-.327 3.162-.2876.5645-.7465 1.0234-1.311 1.311-.6418.327-1.4818.327-3.162.327h-8.4c-1.68016 0-2.52024 0-3.16197-.327-.56449-.2876-1.02343-.7465-1.31105-1.311-.32698-.6418-.32698-1.4818-.32698-3.162z" fill="#e8eaed"/><path d="m16.2 22.75h-8.4c-.8442 0-1.46232-.0002-1.95004-.04-.48479-.0397-.81868-.1172-1.09843-.2597-.51745-.2637-.93815-.6844-1.2018-1.2018-.14254-.2798-.22008-.6137-.25969-1.0985-.03985-.4877-.04004-1.1058-.04004-1.95v-12.4c0-.8442.00019-1.46232.04004-1.95004.03961-.48479.11715-.81868.25969-1.09843.26365-.51745.68435-.93815 1.2018-1.2018.27975-.14254.61364-.22008 1.09843-.25969.48772-.03985 1.10584-.04004 1.95004-.04004h6.0964l6.8536 6.85355v10.09645c0 .8442-.0002 1.4623-.04 1.95-.0397.4848-.1172.8187-.2597 1.0985-.2637.5174-.6844.9381-1.2018 1.2018-.2798.1425-.6137.22-1.0985.2597-.4877.0398-1.1058.04-1.95.04z" stroke="#000" stroke-opacity=".03" stroke-width=".5"/></g><path d="m14 1 7 7h-5c-1.1046 0-2-.89543-2-2z" fill="#fff" opacity=".5"/><path d="m11.5264 9-2.15191 3.2267v2.0455h-1.31897v-2.0455l-2.05552-3.2267h1.48242l1.30707 2.0776 1.31781-2.0776z" fill="#000"/><path d="m13.7426 13.1121h-2.3874l-.4855 1.1724h-1.0572l2.2355-5.27223h1.0813l2.1448 5.27223h-1.1297zm-.3966-1.0526-.7318-1.9348-.8165 1.9348z" fill="#cb171e"/><g fill="#000"><path d="m8.05469 14.8635v5.1673h1.10866v-3.5643l1.16025 2.3957h.8727l1.1999-2.4799v3.6474h1.0636v-5.1662h-1.4522l-1.2885 2.3369-1.22722-2.3369z"/><path d="m17.9994 18.9079h-2.7272v-4.0456h-1.1296v5.1451h3.8568z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

@@ -0,0 +1,13 @@
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Group">
<path id="Vector" d="M2.5 10H0V7.5H2.5V10Z" fill="#676F83"/>
<path id="Vector_2" d="M6.25 6.25H3.75V3.75H6.25V6.25Z" fill="#676F83"/>
<path id="Vector_3" d="M2.5 6.25H0V3.75H2.5V6.25Z" fill="#676F83"/>
<path id="Vector_4" d="M6.25 2.5H3.75V0H6.25V2.5Z" fill="#676F83"/>
<path id="Vector_5" d="M2.5 2.5H0V0H2.5V2.5Z" fill="#676F83"/>
<path id="Vector_6" d="M10 2.5H7.5V0H10V2.5Z" fill="#676F83"/>
<path id="Vector_7" d="M9.58342 7.91663H7.91675V9.58329H9.58342V7.91663Z" fill="#676F83"/>
<path id="Vector_8" d="M9.58342 4.16663H7.91675V5.83329H9.58342V4.16663Z" fill="#676F83"/>
<path id="Vector_9" d="M5.83341 7.91663H4.16675V9.58329H5.83341V7.91663Z" fill="#676F83"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 792 B

@@ -0,0 +1,9 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Icon L">
<g id="Vector">
<path d="M2.66602 11.3333H0.666016L3.33268 8.66667L5.99935 11.3333H3.99935L3.99935 14H2.66602L2.66602 11.3333Z" fill="#354052"/>
<path d="M2.66602 4.66667L2.66602 2L3.99935 2L3.99935 4.66667L5.99935 4.66667L3.33268 7.33333L0.666016 4.66667L2.66602 4.66667Z" fill="#354052"/>
<path d="M7.33268 2.66667H13.9993V4H7.33268V2.66667ZM7.33268 12H13.9993V13.3333H7.33268V12ZM5.99935 7.33333H13.9993V8.66667H5.99935V7.33333Z" fill="#354052"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 579 B

@@ -0,0 +1,5 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Icon L">
<path id="Vector" fill-rule="evenodd" clip-rule="evenodd" d="M6 0.5C6.27615 0.5 6.5 0.72386 6.5 1V1.52755C6.95855 1.57831 7.3967 1.69804 7.80355 1.87619L8.067 1.41997C8.20505 1.18083 8.51085 1.09889 8.75 1.23696C8.98915 1.37503 9.07105 1.68082 8.933 1.91998L8.6692 2.37685C9.033 2.64523 9.3548 2.96707 9.6232 3.33084L10.0801 3.06703C10.3193 2.92896 10.6251 3.0109 10.7632 3.25005C10.9012 3.4892 10.8193 3.79499 10.5801 3.93306L10.1238 4.19649C10.302 4.60333 10.4218 5.0415 10.4725 5.50005H11C11.2761 5.50005 11.5 5.7239 11.5 6.00005C11.5 6.2762 11.2761 6.50005 11 6.50005H10.4725C10.4218 6.9586 10.302 7.3968 10.1238 7.80365L10.5801 8.0671C10.8193 8.20515 10.9012 8.51095 10.7632 8.7501C10.6251 8.98925 10.3193 9.0712 10.0801 8.9331L9.6232 8.6693C9.3548 9.03305 9.03295 9.3549 8.6692 9.62325L8.933 10.0802C9.07105 10.3193 8.98915 10.6251 8.75 10.7632C8.51085 10.9012 8.20505 10.8193 8.067 10.5802L7.80355 10.1239C7.3967 10.3021 6.95855 10.4218 6.5 10.4726V11C6.5 11.2761 6.27615 11.5 6 11.5C5.72385 11.5 5.5 11.2761 5.5 11V10.4726C5.04145 10.4218 4.60328 10.3021 4.19644 10.1239L3.933 10.5802C3.79493 10.8194 3.48914 10.9013 3.24999 10.7633C3.01084 10.6252 2.92891 10.3194 3.06698 10.0802L3.3308 9.62325C2.96702 9.3549 2.64517 9.03305 2.37678 8.66925L1.91986 8.93305C1.68071 9.07115 1.37492 8.9892 1.23685 8.75005C1.09878 8.5109 1.18072 8.2051 1.41986 8.06705L1.87612 7.8036C1.69797 7.39675 1.57824 6.9586 1.52749 6.50005L0.999975 6.5C0.723835 6.5 0.499987 6.2761 0.5 6C0.500015 5.72385 0.72388 5.5 1.00003 5.5L1.5275 5.50005C1.57825 5.0415 1.69796 4.60335 1.87611 4.19652L1.41987 3.93312C1.18072 3.79504 1.09878 3.48925 1.23685 3.2501C1.37492 3.01095 1.68071 2.92901 1.91985 3.06709L2.37675 3.33086C2.64514 2.96708 2.967 2.64524 3.33078 2.37684L3.06698 1.91992C2.92891 1.68077 3.01084 1.37498 3.24999 1.23691C3.48914 1.09884 3.79493 1.18077 3.933 1.41992L4.19642 1.87619C4.60327 1.69803 5.04145 1.57831 5.5 1.52755V1C5.5 0.72386 5.72385 0.5 6 0.5ZM3.83484 3.24991C3.48643 3.52463 3.19141 3.86415 2.96808 4.25014C2.67048 4.7645 2.49999 5.3616 2.49999 6.00005C2.49999 6.6385 2.67048 7.2356 2.96809 7.75C3.19142 8.13595 3.48645 8.4755 3.83486 8.7502L4.8599 6.97475C4.63581 6.71285 4.49999 6.37245 4.49999 6.00005C4.49999 5.62765 4.63581 5.28725 4.8599 5.02535L3.83484 3.24991ZM5.7258 4.52514L4.70041 2.74911C5.10185 2.58847 5.5402 2.50005 6 2.50005C6.63845 2.50005 7.23555 2.67054 7.74995 2.96816C8.28125 3.27557 8.7245 3.71882 9.0319 4.25012C9.2503 4.62764 9.4003 5.04975 9.4646 5.50005H7.41465C7.2087 4.91745 6.6531 4.50005 6 4.50005C5.9065 4.50005 5.8148 4.50865 5.7258 4.52514ZM7.41465 6.50005C7.2087 7.08265 6.6531 7.50005 6 7.50005C5.9065 7.50005 5.8148 7.49145 5.7258 7.47495L4.70043 9.251C5.10185 9.41165 5.5402 9.50005 6 9.50005C6.63845 9.50005 7.23555 9.32955 7.7499 9.03195C8.2812 8.72455 8.72445 8.2813 9.03185 7.75C9.2503 7.3725 9.4003 6.95035 9.4646 6.50005H7.41465Z" fill="#676F83"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

@@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Icon L">
<path id="Vector" d="M14.0002 2C14.3684 2 14.6668 2.29848 14.6668 2.66667V13.3333C14.6668 13.7015 14.3684 14 14.0002 14H2.00016C1.63198 14 1.3335 13.7015 1.3335 13.3333V2.66667C1.3335 2.29848 1.63198 2 2.00016 2H14.0002ZM13.3335 3.33333H2.66683V12.6667H13.3335V3.33333ZM14.0002 2.66667V13.3333H10.0002V2.66667H14.0002Z" fill="#354052"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 461 B

@@ -0,0 +1,7 @@
<svg width="10" height="11" viewBox="0 0 10 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Group">
<path id="Vector" d="M2.70833 3.87501C3.51375 3.87501 4.16666 3.22209 4.16666 2.41668C4.16666 1.61126 3.51375 0.958344 2.70833 0.958344C1.90292 0.958344 1.25 1.61126 1.25 2.41668C1.25 3.22209 1.90292 3.87501 2.70833 3.87501Z" fill="#676F83"/>
<path id="Vector_2" d="M7.29158 3.87501C8.097 3.87501 8.74992 3.22209 8.74992 2.41668C8.74992 1.61126 8.097 0.958344 7.29158 0.958344C6.48617 0.958344 5.83325 1.61126 5.83325 2.41668C5.83325 3.22209 6.48617 3.87501 7.29158 3.87501Z" fill="#676F83"/>
<path id="Vector_3" d="M7.29167 4.70835C6.83771 4.70886 6.39118 4.82363 5.99324 5.04208C5.59529 5.26053 5.25874 5.57563 5.01459 5.95835C5.34482 5.9622 5.66011 6.09658 5.89159 6.33215C6.12306 6.56771 6.25191 6.8853 6.24998 7.21555C6.24805 7.5458 6.11551 7.86187 5.8813 8.09472C5.6471 8.32756 5.33026 8.45826 5 8.45826C4.66975 8.45826 4.35291 8.32756 4.1187 8.09472C3.8845 7.86187 3.75195 7.5458 3.75003 7.21555C3.7481 6.8853 3.87695 6.56771 4.10842 6.33215C4.3399 6.09658 4.65519 5.9622 4.98542 5.95835C4.67086 5.46415 4.20432 5.08546 3.656 4.87926C3.10767 4.67306 2.50721 4.6505 1.94496 4.81497C1.3827 4.97944 0.889064 5.32205 0.538306 5.79125C0.187547 6.26045 -0.00135882 6.83086 7.35834e-06 7.41668V10.125C7.35834e-06 10.2355 0.043906 10.3415 0.122046 10.4196C0.200186 10.4978 0.306167 10.5417 0.416674 10.5417H3.33334V9.50001L1.83334 8.37501C1.78957 8.34218 1.75269 8.30105 1.72481 8.25397C1.69693 8.20688 1.6786 8.15477 1.67086 8.1006C1.65523 7.99121 1.6837 7.88008 1.75001 7.79168C1.81631 7.70327 1.91502 7.64483 2.02441 7.6292C2.13381 7.61357 2.24493 7.64204 2.33334 7.70835L3.88875 8.87501H6.11125L7.66667 7.70835C7.75507 7.64204 7.8662 7.61357 7.97559 7.6292C8.08499 7.64483 8.1837 7.70327 8.25 7.79168C8.31631 7.88008 8.34478 7.99121 8.32915 8.1006C8.31352 8.21 8.25507 8.30871 8.16667 8.37501L6.66667 9.50001V10.5417H9.58333C9.69384 10.5417 9.79982 10.4978 9.87796 10.4196C9.9561 10.3415 10 10.2355 10 10.125V7.41668C9.99912 6.69866 9.71349 6.01029 9.20577 5.50257C8.69805 4.99485 8.00969 4.70923 7.29167 4.70835Z" fill="#676F83"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

@@ -0,0 +1,13 @@
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Group">
<path id="Vector" d="M2.5 10H0V7.5H2.5V10Z" fill="#676F83"/>
<path id="Vector_2" d="M6.25 6.25H3.75V3.75H6.25V6.25Z" fill="#676F83"/>
<path id="Vector_3" d="M2.5 6.25H0V3.75H2.5V6.25Z" fill="#676F83"/>
<path id="Vector_4" d="M6.25 2.5H3.75V0H6.25V2.5Z" fill="#676F83"/>
<path id="Vector_5" d="M2.5 2.5H0V0H2.5V2.5Z" fill="#676F83"/>
<path id="Vector_6" d="M10 2.5H7.5V0H10V2.5Z" fill="#676F83"/>
<path id="Vector_7" d="M9.58332 7.91663H7.91666V9.58329H9.58332V7.91663Z" fill="#676F83"/>
<path id="Vector_8" d="M9.58332 4.16663H7.91666V5.83329H9.58332V4.16663Z" fill="#676F83"/>
<path id="Vector_9" d="M5.83332 7.91663H4.16666V9.58329H5.83332V7.91663Z" fill="#676F83"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 792 B

@@ -0,0 +1,186 @@
<svg width="90" height="10" viewBox="0 0 90 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Anthropic" clip-path="url(#clip0_5981_49007)">
<g id="Clip path group">
<mask id="mask0_5981_49007" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="-1" width="90" height="11">
<g id="__lottie_element_2">
<path id="Vector" d="M89.375 -0.00195312H0V9.99805H89.375V-0.00195312Z" fill="white"/>
</g>
</mask>
<g mask="url(#mask0_5981_49007)">
<g id="Group">
<g id="Clip path group_2">
<mask id="mask1_5981_49007" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="-1" width="90" height="11">
<g id="__lottie_element_4">
<path id="Vector_2" d="M0 -0.00390625H89.375V9.99609H0V-0.00390625Z" fill="white"/>
</g>
</mask>
<g mask="url(#mask1_5981_49007)">
<g id="Group_2">
<g id="Clip path group_3">
<mask id="mask2_5981_49007" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="-1" width="90" height="11">
<g id="__lottie_element_12">
<path id="Vector_3" d="M0 -0.00585938H89.375V9.99414H0V-0.00585938Z" fill="white"/>
</g>
</mask>
<g mask="url(#mask2_5981_49007)">
<g id="Group_3">
<g id="Clip path group_4">
<mask id="mask3_5981_49007" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="-1" width="90" height="11">
<g id="__lottie_element_89">
<path id="Vector_4" d="M0 -0.0078125H89.375V9.99219H0V-0.0078125Z" fill="white"/>
</g>
</mask>
<g mask="url(#mask3_5981_49007)">
<g id="Group_4">
<g id="Group_5">
<g id="Group_6">
<path id="Vector_5" d="M18.1273 6.92438L13.7773 0.15625H11.4297V9.82501H13.4321V3.05688L17.7821 9.82501H20.1297V0.15625H18.1273V6.92438Z" fill="black" fill-opacity="0.95"/>
</g>
</g>
</g>
</g>
</g>
<g id="Clip path group_5">
<mask id="mask4_5981_49007" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="-1" width="90" height="11">
<g id="__lottie_element_80">
<path id="Vector_6" d="M0 -0.0078125H89.375V9.99219H0V-0.0078125Z" fill="white"/>
</g>
</mask>
<g mask="url(#mask4_5981_49007)">
<g id="Group_7">
<g id="Group_8">
<g id="Group_9">
<path id="Vector_7" d="M21.7969 2.02094H25.0423V9.82501H27.1139V2.02094H30.3594V0.15625H21.7969V2.02094Z" fill="black" fill-opacity="0.95"/>
</g>
</g>
</g>
</g>
</g>
<g id="Clip path group_6">
<mask id="mask5_5981_49007" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="-1" width="90" height="11">
<g id="__lottie_element_71">
<path id="Vector_8" d="M0 -0.0078125H89.375V9.99219H0V-0.0078125Z" fill="white"/>
</g>
</mask>
<g mask="url(#mask5_5981_49007)">
<g id="Group_10">
<g id="Group_11">
<g id="Group_12">
<path id="Vector_9" d="M38.6442 4.00994H34.0871V0.15625H32.0156V9.82501H34.0871V5.87463H38.6442V9.82501H40.7156V0.15625H38.6442V4.00994Z" fill="black" fill-opacity="0.95"/>
</g>
</g>
</g>
</g>
</g>
<g id="Clip path group_7">
<mask id="mask6_5981_49007" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="-1" width="90" height="11">
<g id="__lottie_element_62">
<path id="Vector_10" d="M0 -0.0078125H89.375V9.99219H0V-0.0078125Z" fill="white"/>
</g>
</mask>
<g mask="url(#mask6_5981_49007)">
<g id="Group_13">
<g id="Group_14">
<g id="Group_15">
<path id="Vector_11" d="M45.3376 2.02094H47.893C48.9152 2.02094 49.4539 2.39387 49.4539 3.09831C49.4539 3.80275 48.9152 4.17569 47.893 4.17569H45.3376V2.02094ZM51.5259 3.09831C51.5259 1.27506 50.186 0.15625 47.9897 0.15625H43.2656V9.82501H45.3376V6.04037H47.6443L49.7164 9.82501H52.0094L49.715 5.75211C50.8666 5.30941 51.5259 4.37721 51.5259 3.09831Z" fill="black" fill-opacity="0.95"/>
</g>
</g>
</g>
</g>
</g>
<g id="Clip path group_8">
<mask id="mask7_5981_49007" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="-1" width="90" height="11">
<g id="__lottie_element_53">
<path id="Vector_12" d="M0 -0.0078125H89.375V9.99219H0V-0.0078125Z" fill="white"/>
</g>
</mask>
<g mask="url(#mask7_5981_49007)">
<g id="Group_16">
<g id="Group_17">
<g id="Group_18">
<path id="Vector_13" d="M57.8732 8.05653C56.2438 8.05653 55.2496 6.89631 55.2496 5.00404C55.2496 3.08416 56.2438 1.92394 57.8732 1.92394C59.4887 1.92394 60.4691 3.08416 60.4691 5.00404C60.4691 6.89631 59.4887 8.05653 57.8732 8.05653ZM57.8732 -0.00976562C55.0839 -0.00976562 53.1094 2.06206 53.1094 5.00404C53.1094 7.91841 55.0839 9.99023 57.8732 9.99023C60.6486 9.99023 62.6094 7.91841 62.6094 5.00404C62.6094 2.06206 60.6486 -0.00976562 57.8732 -0.00976562Z" fill="black" fill-opacity="0.95"/>
</g>
</g>
</g>
</g>
</g>
<g id="Clip path group_9">
<mask id="mask8_5981_49007" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="-1" width="90" height="11">
<g id="__lottie_element_44">
<path id="Vector_14" d="M0 -0.0078125H89.375V9.99219H0V-0.0078125Z" fill="white"/>
</g>
</mask>
<g mask="url(#mask8_5981_49007)">
<g id="Group_19">
<g id="Group_20">
<g id="Group_21">
<path id="Vector_15" d="M69.1794 4.45194H66.6233V2.02094H69.1794C70.2019 2.02094 70.7407 2.43532 70.7407 3.23644C70.7407 4.03756 70.2019 4.45194 69.1794 4.45194ZM69.2762 0.15625H64.5508V9.82501H66.6233V6.31662H69.2762C71.473 6.31662 72.8133 5.15637 72.8133 3.23644C72.8133 1.3165 71.473 0.15625 69.2762 0.15625Z" fill="black" fill-opacity="0.95"/>
</g>
</g>
</g>
</g>
</g>
<g id="Clip path group_10">
<mask id="mask9_5981_49007" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="-1" width="90" height="11">
<g id="__lottie_element_35">
<path id="Vector_16" d="M0 -0.0078125H89.375V9.99219H0V-0.0078125Z" fill="white"/>
</g>
</mask>
<g mask="url(#mask9_5981_49007)">
<g id="Group_22">
<g id="Group_23">
<g id="Group_24">
<path id="Vector_17" d="M86.8413 6.57863C86.4823 7.51786 85.7642 8.05653 84.7837 8.05653C83.1542 8.05653 82.16 6.89631 82.16 5.00404C82.16 3.08416 83.1542 1.92394 84.7837 1.92394C85.7642 1.92394 86.4823 2.46261 86.8413 3.40183H89.0369C88.4984 1.33002 86.8827 -0.00976562 84.7837 -0.00976562C81.9942 -0.00976562 80.0195 2.06206 80.0195 5.00404C80.0195 7.91841 81.9942 9.99023 84.7837 9.99023C86.8965 9.99023 88.5122 8.63664 89.0508 6.57863H86.8413Z" fill="black" fill-opacity="0.95"/>
</g>
</g>
</g>
</g>
</g>
<g id="Clip path group_11">
<mask id="mask10_5981_49007" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="-1" width="90" height="11">
<g id="__lottie_element_26">
<path id="Vector_18" d="M0 -0.0078125H89.375V9.99219H0V-0.0078125Z" fill="white"/>
</g>
</mask>
<g mask="url(#mask10_5981_49007)">
<g id="Group_25">
<g id="Group_26">
<g id="Group_27">
<path id="Vector_19" d="M73.6484 0.15625L77.5033 9.82501H79.6172L75.7624 0.15625H73.6484Z" fill="black" fill-opacity="0.95"/>
</g>
</g>
</g>
</g>
</g>
<g id="Clip path group_12">
<mask id="mask11_5981_49007" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="-1" width="90" height="11">
<g id="__lottie_element_17">
<path id="Vector_20" d="M0 -0.0078125H89.375V9.99219H0V-0.0078125Z" fill="white"/>
</g>
</mask>
<g mask="url(#mask11_5981_49007)">
<g id="Group_28">
<g id="Group_29">
<g id="Group_30">
<path id="Vector_21" d="M3.64038 5.99893L4.95938 2.60106L6.27838 5.99893H3.64038ZM3.85422 0.15625L0 9.82501H2.15505L2.9433 7.79456H6.97558L7.76371 9.82501H9.91875L6.06453 0.15625H3.85422Z" fill="black" fill-opacity="0.95"/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<defs>
<clipPath id="clip0_5981_49007">
<rect width="89.375" height="10" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 7.1 KiB

Some files were not shown because too many files have changed in this diff Show More