Merge branch 'feat/parent-child-retrieval' of github.com:langgenius/dify into feat/parent-child-retrieval
@ -237,7 +237,7 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
|
||||
{appDetail.mode === 'advanced-chat' && (
|
||||
<>
|
||||
<div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.chatbot').toUpperCase()}</div>
|
||||
<div title={t('app.newApp.advanced') || ''} className='px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.newApp.advanced').toUpperCase()}</div>
|
||||
<div title={t('app.types.advanced') || ''} className='px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.advanced').toUpperCase()}</div>
|
||||
</>
|
||||
)}
|
||||
{appDetail.mode === 'agent-chat' && (
|
||||
@ -246,13 +246,13 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
|
||||
{appDetail.mode === 'chat' && (
|
||||
<>
|
||||
<div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.chatbot').toUpperCase()}</div>
|
||||
<div title={t('app.newApp.basic') || ''} className='px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{(t('app.newApp.basic').toUpperCase())}</div>
|
||||
<div title={t('app.types.basic') || ''} className='px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{(t('app.types.basic').toUpperCase())}</div>
|
||||
</>
|
||||
)}
|
||||
{appDetail.mode === 'completion' && (
|
||||
<>
|
||||
<div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.completion').toUpperCase()}</div>
|
||||
<div title={t('app.newApp.basic') || ''} className='px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{(t('app.newApp.basic').toUpperCase())}</div>
|
||||
<div title={t('app.types.basic') || ''} className='px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{(t('app.types.basic').toUpperCase())}</div>
|
||||
</>
|
||||
)}
|
||||
{appDetail.mode === 'workflow' && (
|
||||
@ -299,7 +299,7 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
|
||||
{appDetail.mode === 'advanced-chat' && (
|
||||
<>
|
||||
<div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.chatbot').toUpperCase()}</div>
|
||||
<div title={t('app.newApp.advanced') || ''} className='px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.newApp.advanced').toUpperCase()}</div>
|
||||
<div title={t('app.types.advanced') || ''} className='px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.advanced').toUpperCase()}</div>
|
||||
</>
|
||||
)}
|
||||
{appDetail.mode === 'agent-chat' && (
|
||||
@ -308,13 +308,13 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
|
||||
{appDetail.mode === 'chat' && (
|
||||
<>
|
||||
<div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.chatbot').toUpperCase()}</div>
|
||||
<div title={t('app.newApp.basic') || ''} className='px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{(t('app.newApp.basic').toUpperCase())}</div>
|
||||
<div title={t('app.types.basic') || ''} className='px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{(t('app.types.basic').toUpperCase())}</div>
|
||||
</>
|
||||
)}
|
||||
{appDetail.mode === 'completion' && (
|
||||
<>
|
||||
<div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.completion').toUpperCase()}</div>
|
||||
<div title={t('app.newApp.basic') || ''} className='px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{(t('app.newApp.basic').toUpperCase())}</div>
|
||||
<div title={t('app.types.basic') || ''} className='px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{(t('app.types.basic').toUpperCase())}</div>
|
||||
</>
|
||||
)}
|
||||
{appDetail.mode === 'workflow' && (
|
||||
@ -398,7 +398,7 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
|
||||
)} />
|
||||
<div className='px-4 pb-2'>
|
||||
<div className='flex items-center gap-1 text-gray-700 text-md leading-6 font-semibold'>
|
||||
{showSwitchTip === 'chat' ? t('app.newApp.advanced') : t('app.types.workflow')}
|
||||
{showSwitchTip === 'chat' ? t('app.types.advanced') : t('app.types.workflow')}
|
||||
<span className='px-1 rounded-[5px] bg-white border border-black/8 text-gray-500 text-[10px] leading-[18px] font-medium'>BETA</span>
|
||||
</div>
|
||||
<div className='text-orange-500 text-xs leading-[18px] font-medium'>{t('app.newApp.advancedFor').toLocaleUpperCase()}</div>
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Textarea from 'rc-textarea'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import { Robot, User } from '@/app/components/base/icons/src/public/avatar'
|
||||
|
||||
export enum EditItemType {
|
||||
@ -31,12 +31,10 @@ const EditItem: FC<Props> = ({
|
||||
{avatar}
|
||||
</div>
|
||||
<div className='grow'>
|
||||
<div className='mb-1 leading-[18px] text-xs font-semibold text-gray-900'>{name}</div>
|
||||
<div className='mb-1 system-xs-semibold text-text-primary'>{name}</div>
|
||||
<Textarea
|
||||
className='mt-1 block w-full leading-5 max-h-none text-sm text-gray-700 outline-none appearance-none resize-none'
|
||||
value={content}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => onChange(e.target.value)}
|
||||
autoSize={{ minRows: 3 }}
|
||||
placeholder={placeholder}
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
@ -4,6 +4,7 @@ import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { AnnotationItemBasic } from '../type'
|
||||
import EditItem, { EditItemType } from './edit-item'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
import Drawer from '@/app/components/base/drawer-plus'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
@ -96,11 +97,11 @@ const AddAnnotationModal: FC<Props> = ({
|
||||
<AnnotationFull />
|
||||
</div>
|
||||
)}
|
||||
<div className='px-6 flex h-16 items-center justify-between border-t border-black/5 bg-gray-50 rounded-bl-xl rounded-br-xl leading-[18px] text-[13px] font-medium text-gray-500'>
|
||||
<div className='px-4 flex h-16 items-center justify-between border-t border-divider-subtle bg-background-section-burn rounded-bl-xl rounded-br-xl system-sm-medium text-text-tertiary'>
|
||||
<div
|
||||
className='flex items-center space-x-2'
|
||||
>
|
||||
<input type="checkbox" checked={isCreateNext} onChange={() => setIsCreateNext(!isCreateNext)} className="w-4 h-4 rounded border-gray-300 text-blue-700 focus:ring-blue-700" />
|
||||
<Checkbox checked={isCreateNext} onCheck={() => setIsCreateNext(!isCreateNext)} />
|
||||
<div>{t('appAnnotation.addModal.createNext')}</div>
|
||||
</div>
|
||||
<div className='mt-2 flex space-x-2'>
|
||||
|
||||
@ -33,19 +33,19 @@ const CSVDownload: FC = () => {
|
||||
|
||||
return (
|
||||
<div className='mt-6'>
|
||||
<div className='text-sm text-gray-900 font-medium'>{t('share.generation.csvStructureTitle')}</div>
|
||||
<div className='system-sm-medium text-text-primary'>{t('share.generation.csvStructureTitle')}</div>
|
||||
<div className='mt-2 max-h-[500px] overflow-auto'>
|
||||
<table className='table-fixed w-full border-separate border-spacing-0 border border-gray-200 rounded-lg text-xs'>
|
||||
<thead className='text-gray-500'>
|
||||
<table className='table-fixed w-full border-separate border-spacing-0 border border-divider-regular rounded-lg text-xs'>
|
||||
<thead className='text-text-tertiary'>
|
||||
<tr>
|
||||
<td className='h-9 pl-3 pr-2 border-b border-gray-200'>{t('appAnnotation.batchModal.question')}</td>
|
||||
<td className='h-9 pl-3 pr-2 border-b border-gray-200'>{t('appAnnotation.batchModal.answer')}</td>
|
||||
<td className='h-9 pl-3 pr-2 border-b border-divider-regular'>{t('appAnnotation.batchModal.question')}</td>
|
||||
<td className='h-9 pl-3 pr-2 border-b border-divider-regular'>{t('appAnnotation.batchModal.answer')}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className='text-gray-700'>
|
||||
<tr>
|
||||
<td className='h-9 pl-3 pr-2 border-b border-gray-100 text-[13px]'>{t('appAnnotation.batchModal.question')} 1</td>
|
||||
<td className='h-9 pl-3 pr-2 border-b border-gray-100 text-[13px]'>{t('appAnnotation.batchModal.answer')} 1</td>
|
||||
<td className='h-9 pl-3 pr-2 border-b border-divider-subtle text-[13px]'>{t('appAnnotation.batchModal.question')} 1</td>
|
||||
<td className='h-9 pl-3 pr-2 border-b border-divider-subtle text-[13px]'>{t('appAnnotation.batchModal.answer')} 1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='h-9 pl-3 pr-2 text-[13px]'>{t('appAnnotation.batchModal.question')} 2</td>
|
||||
@ -61,7 +61,7 @@ const CSVDownload: FC = () => {
|
||||
bom={true}
|
||||
data={getTemplate()}
|
||||
>
|
||||
<div className='flex items-center h-[18px] space-x-1 text-[#155EEF] text-xs font-medium'>
|
||||
<div className='flex items-center h-[18px] space-x-1 text-text-accent system-xs-medium'>
|
||||
<DownloadIcon className='w-3 h-3 mr-1' />
|
||||
{t('appAnnotation.batchModal.template')}
|
||||
</div>
|
||||
|
||||
@ -91,29 +91,29 @@ const CSVUploader: FC<Props> = ({
|
||||
/>
|
||||
<div ref={dropRef}>
|
||||
{!file && (
|
||||
<div className={cn('flex items-center h-20 rounded-xl bg-gray-50 border border-dashed border-gray-200 text-sm font-normal', dragging && 'bg-[#F5F8FF] border border-[#B2CCFF]')}>
|
||||
<div className={cn('flex items-center h-20 rounded-xl bg-components-dropzone-bg border border-dashed border-components-dropzone-border system-sm-regular', dragging && 'bg-components-dropzone-bg-accent border border-components-dropzone-border-accent')}>
|
||||
<div className='w-full flex items-center justify-center space-x-2'>
|
||||
<CSVIcon className="shrink-0" />
|
||||
<div className='text-gray-500'>
|
||||
<div className='text-text-tertiary'>
|
||||
{t('appAnnotation.batchModal.csvUploadTitle')}
|
||||
<span className='text-primary-400 cursor-pointer' onClick={selectHandle}>{t('appAnnotation.batchModal.browse')}</span>
|
||||
<span className='text-text-accent cursor-pointer' onClick={selectHandle}>{t('appAnnotation.batchModal.browse')}</span>
|
||||
</div>
|
||||
</div>
|
||||
{dragging && <div ref={dragRef} className='absolute w-full h-full top-0 left-0' />}
|
||||
</div>
|
||||
)}
|
||||
{file && (
|
||||
<div className={cn('flex items-center h-20 px-6 rounded-xl bg-gray-50 border border-gray-200 text-sm font-normal group', 'hover:bg-[#F5F8FF] hover:border-[#B2CCFF]')}>
|
||||
<div className={cn('flex items-center h-20 px-6 rounded-xl bg-components-panel-bg border border-components-panel-border text-sm font-normal group', 'hover:bg-components-panel-bg-blur hover:border-components-panel-bg-blur')}>
|
||||
<CSVIcon className="shrink-0" />
|
||||
<div className='flex ml-2 w-0 grow'>
|
||||
<span className='max-w-[calc(100%_-_30px)] text-ellipsis whitespace-nowrap overflow-hidden text-gray-800'>{file.name.replace(/.csv$/, '')}</span>
|
||||
<span className='shrink-0 text-gray-500'>.csv</span>
|
||||
<span className='max-w-[calc(100%_-_30px)] text-ellipsis whitespace-nowrap overflow-hidden text-text-primary'>{file.name.replace(/.csv$/, '')}</span>
|
||||
<span className='shrink-0 text-text-tertiary'>.csv</span>
|
||||
</div>
|
||||
<div className='hidden group-hover:flex items-center'>
|
||||
<Button className='!h-8 !px-3 !py-[6px] bg-white !text-[13px] !leading-[18px] text-gray-700' onClick={selectHandle}>{t('datasetCreation.stepOne.uploader.change')}</Button>
|
||||
<div className='mx-2 w-px h-4 bg-gray-200' />
|
||||
<Button variant='secondary' onClick={selectHandle}>{t('datasetCreation.stepOne.uploader.change')}</Button>
|
||||
<div className='mx-2 w-px h-4 bg-divider-regular' />
|
||||
<div className='p-2 cursor-pointer' onClick={removeFile}>
|
||||
<RiDeleteBinLine className='w-4 h-4 text-gray-500' />
|
||||
<RiDeleteBinLine className='w-4 h-4 text-text-tertiary' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -88,9 +88,9 @@ const BatchModal: FC<IBatchModalProps> = ({
|
||||
|
||||
return (
|
||||
<Modal isShow={isShow} onClose={() => { }} className='px-8 py-6 !max-w-[520px] !rounded-xl'>
|
||||
<div className='relative pb-1 text-xl font-medium leading-[30px] text-gray-900'>{t('appAnnotation.batchModal.title')}</div>
|
||||
<div className='relative pb-1 system-xl-medium text-text-primary'>{t('appAnnotation.batchModal.title')}</div>
|
||||
<div className='absolute right-4 top-4 p-2 cursor-pointer' onClick={onCancel}>
|
||||
<RiCloseLine className='w-4 h-4 text-gray-500' />
|
||||
<RiCloseLine className='w-4 h-4 text-text-tertiary' />
|
||||
</div>
|
||||
<CSVUploader
|
||||
file={currentCSV}
|
||||
@ -105,11 +105,10 @@ const BatchModal: FC<IBatchModalProps> = ({
|
||||
)}
|
||||
|
||||
<div className='mt-[28px] pt-6 flex justify-end'>
|
||||
<Button className='mr-2 text-gray-700 text-sm font-medium' onClick={onCancel}>
|
||||
<Button className='mr-2 text-text-tertiary system-sm-medium' onClick={onCancel}>
|
||||
{t('appAnnotation.batchModal.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
className='text-sm font-medium'
|
||||
variant="primary"
|
||||
onClick={handleSend}
|
||||
disabled={isAnnotationFull || !currentCSV}
|
||||
|
||||
@ -2,13 +2,11 @@
|
||||
import type { FC } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Textarea from 'rc-textarea'
|
||||
import { RiDeleteBinLine } from '@remixicon/react'
|
||||
import cn from '@/utils/classnames'
|
||||
import { RiDeleteBinLine, RiEditFill, RiEditLine } from '@remixicon/react'
|
||||
import { Robot, User } from '@/app/components/base/icons/src/public/avatar'
|
||||
import { Edit04 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { Edit04 as EditSolid } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import Button from '@/app/components/base/button'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
export enum EditItemType {
|
||||
Query = 'query',
|
||||
@ -22,8 +20,8 @@ type Props = {
|
||||
}
|
||||
|
||||
export const EditTitle: FC<{ className?: string; title: string }> = ({ className, title }) => (
|
||||
<div className={cn(className, 'flex items-center height-[18px] text-xs font-medium text-gray-500')}>
|
||||
<EditSolid className='mr-1 w-3.5 h-3.5' />
|
||||
<div className={cn(className, 'flex items-center h-[18px] system-xs-medium text-text-tertiary')}>
|
||||
<RiEditFill className='mr-1 w-3.5 h-3.5' />
|
||||
<div>{title}</div>
|
||||
<div
|
||||
className='ml-2 grow h-[1px]'
|
||||
@ -64,32 +62,32 @@ const EditItem: FC<Props> = ({
|
||||
{avatar}
|
||||
</div>
|
||||
<div className='grow'>
|
||||
<div className='mb-1 leading-[18px] text-xs font-semibold text-gray-900'>{name}</div>
|
||||
<div className='leading-5 text-sm font-normal text-gray-900'>{content}</div>
|
||||
<div className='mb-1 system-xs-semibold text-text-primary'>{name}</div>
|
||||
<div className='system-sm-regular text-text-primary'>{content}</div>
|
||||
{!isEdit
|
||||
? (
|
||||
<div>
|
||||
{showNewContent && (
|
||||
<div className='mt-3'>
|
||||
<EditTitle title={editTitle} />
|
||||
<div className='mt-1 leading-5 text-sm font-normal text-gray-900'>{newContent}</div>
|
||||
<div className='mt-1 system-sm-regular text-text-primary'>{newContent}</div>
|
||||
</div>
|
||||
)}
|
||||
<div className='mt-2 flex items-center'>
|
||||
{!readonly && (
|
||||
<div
|
||||
className='flex items-center space-x-1 leading-[18px] text-xs font-medium text-[#155EEF] cursor-pointer'
|
||||
className='flex items-center space-x-1 system-xs-medium text-text-accent cursor-pointer'
|
||||
onClick={() => {
|
||||
setIsEdit(true)
|
||||
}}
|
||||
>
|
||||
<Edit04 className='mr-1 w-3.5 h-3.5' />
|
||||
<RiEditLine className='mr-1 w-3.5 h-3.5' />
|
||||
<div>{t('common.operation.edit')}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showNewContent && (
|
||||
<div className='ml-2 flex items-center leading-[18px] text-xs font-medium text-gray-500'>
|
||||
<div className='ml-2 flex items-center system-xs-medium text-text-tertiary'>
|
||||
<div className='mr-2'>·</div>
|
||||
<div
|
||||
className='flex items-center space-x-1 cursor-pointer'
|
||||
@ -112,10 +110,8 @@ const EditItem: FC<Props> = ({
|
||||
<div className='mt-3'>
|
||||
<EditTitle title={editTitle} />
|
||||
<Textarea
|
||||
className='mt-1 block w-full leading-5 max-h-none text-sm text-gray-700 outline-none appearance-none resize-none'
|
||||
value={newContent}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setNewContent(e.target.value)}
|
||||
autoSize={{ minRows: 3 }}
|
||||
placeholder={placeholder}
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
@ -123,7 +123,7 @@ const EditAnnotationModal: FC<Props> = ({
|
||||
{
|
||||
annotationId
|
||||
? (
|
||||
<div className='px-4 flex h-16 items-center justify-between border-t border-black/5 bg-gray-50 rounded-bl-xl rounded-br-xl leading-[18px] text-[13px] font-medium text-gray-500'>
|
||||
<div className='px-4 flex h-16 items-center justify-between border-t border-divider-subtle bg-background-section-burn rounded-bl-xl rounded-br-xl system-sm-medium text-text-tertiary'>
|
||||
<div
|
||||
className='flex items-center pl-3 space-x-2 cursor-pointer'
|
||||
onClick={() => setShowModal(true)}
|
||||
|
||||
@ -4,6 +4,7 @@ import React, { Fragment, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiAddLine,
|
||||
RiMoreFill,
|
||||
} from '@remixicon/react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import {
|
||||
@ -14,7 +15,6 @@ import Button from '../../../base/button'
|
||||
import AddAnnotationModal from '../add-annotation-modal'
|
||||
import type { AnnotationItemBasic } from '../type'
|
||||
import BatchAddModal from '../batch-add-annotation-modal'
|
||||
import s from './style.module.css'
|
||||
import cn from '@/utils/classnames'
|
||||
import CustomPopover from '@/app/components/base/popover'
|
||||
import { FileDownload02, FilePlus02 } from '@/app/components/base/icons/src/vender/line/files'
|
||||
@ -80,17 +80,17 @@ const HeaderOptions: FC<Props> = ({
|
||||
const Operations = () => {
|
||||
return (
|
||||
<div className="w-full py-1">
|
||||
<button className={s.actionItem} onClick={() => {
|
||||
<button className='h-9 py-2 px-3 mx-1 flex items-center space-x-2 hover:bg-components-panel-on-panel-item-bg-hover rounded-lg cursor-pointer disabled:opacity-50 w-[calc(100%_-_8px)]' onClick={() => {
|
||||
setShowBulkImportModal(true)
|
||||
}}>
|
||||
<FilePlus02 className={s.actionItemIcon} />
|
||||
<span className={s.actionName}>{t('appAnnotation.table.header.bulkImport')}</span>
|
||||
<FilePlus02 className='w-4 h-4 text-text-tertiary' />
|
||||
<span className='grow text-text-secondary system-sm-regular text-left'>{t('appAnnotation.table.header.bulkImport')}</span>
|
||||
</button>
|
||||
<Menu as="div" className="relative w-full h-full">
|
||||
<Menu.Button className={s.actionItem}>
|
||||
<FileDownload02 className={s.actionItemIcon} />
|
||||
<span className={s.actionName}>{t('appAnnotation.table.header.bulkExport')}</span>
|
||||
<ChevronRight className='shrink-0 w-[14px] h-[14px] text-gray-500' />
|
||||
<Menu.Button className='h-9 py-2 px-3 mx-1 flex items-center space-x-2 hover:bg-components-panel-on-panel-item-bg-hover rounded-lg cursor-pointer disabled:opacity-50 w-[calc(100%_-_8px)]'>
|
||||
<FileDownload02 className='w-4 h-4 text-text-tertiary' />
|
||||
<span className='grow text-text-secondary system-sm-regular text-left'>{t('appAnnotation.table.header.bulkExport')}</span>
|
||||
<ChevronRight className='shrink-0 w-[14px] h-[14px] text-text-tertiary' />
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
@ -103,11 +103,7 @@ const HeaderOptions: FC<Props> = ({
|
||||
>
|
||||
<Menu.Items
|
||||
className={cn(
|
||||
`
|
||||
absolute top-[1px] py-1 min-w-[100px] z-10 bg-white border-[0.5px] border-gray-200
|
||||
divide-y divide-gray-100 origin-top-right rounded-xl
|
||||
`,
|
||||
s.popup,
|
||||
'absolute top-[1px] left-1 -translate-x-full py-1 min-w-[100px] z-10 bg-components-panel-bg border-[0.5px] border-components-panel-on-panel-item-bg origin-top-right rounded-xl shadow-xs',
|
||||
)}
|
||||
>
|
||||
<CSVDownloader
|
||||
@ -119,12 +115,12 @@ const HeaderOptions: FC<Props> = ({
|
||||
...list.map(item => [item.question, item.answer]),
|
||||
]}
|
||||
>
|
||||
<button disabled={annotationUnavailable} className={s.actionItem}>
|
||||
<span className={s.actionName}>CSV</span>
|
||||
<button disabled={annotationUnavailable} className='h-9 py-2 px-3 mx-1 flex items-center space-x-2 hover:bg-components-panel-on-panel-item-bg-hover rounded-lg cursor-pointer disabled:opacity-50 w-[calc(100%_-_8px)]'>
|
||||
<span className='grow text-text-secondary system-sm-regular text-left'>CSV</span>
|
||||
</button>
|
||||
</CSVDownloader>
|
||||
<button disabled={annotationUnavailable} className={cn(s.actionItem, '!border-0')} onClick={JSONLOutput}>
|
||||
<span className={s.actionName}>JSONL</span>
|
||||
<button disabled={annotationUnavailable} className={cn('h-9 py-2 px-3 mx-1 flex items-center space-x-2 hover:bg-components-panel-on-panel-item-bg-hover rounded-lg cursor-pointer disabled:opacity-50 w-[calc(100%_-_8px)]', '!border-0')} onClick={JSONLOutput}>
|
||||
<span className='grow text-text-secondary system-sm-regular text-left'>JSONL</span>
|
||||
</button>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
@ -137,21 +133,20 @@ const HeaderOptions: FC<Props> = ({
|
||||
|
||||
return (
|
||||
<div className='flex space-x-2'>
|
||||
<Button variant='primary' onClick={() => setShowAddModal(true)} className='flex items-center !h-8 !px-3 !text-[13px] space-x-2'>
|
||||
<RiAddLine className='w-4 h-4' />
|
||||
<Button variant='primary' onClick={() => setShowAddModal(true)}>
|
||||
<RiAddLine className='w-4 h-4 mr-0.5' />
|
||||
<div>{t('appAnnotation.table.header.addAnnotation')}</div>
|
||||
</Button>
|
||||
<CustomPopover
|
||||
htmlContent={<Operations />}
|
||||
position="br"
|
||||
trigger="click"
|
||||
btnElement={<div className={cn(s.actionIcon, s.commonIcon)} />}
|
||||
btnClassName={open =>
|
||||
cn(
|
||||
open ? 'border-gray-300 !bg-gray-100 !shadow-none' : 'border-gray-200',
|
||||
s.actionIconWrapper,
|
||||
)
|
||||
btnElement={
|
||||
<Button variant='secondary' className='w-8 p-0'>
|
||||
<RiMoreFill className='w-4 h-4' />
|
||||
</Button>
|
||||
}
|
||||
btnClassName='p-0 border-0'
|
||||
className={'!w-[155px] h-fit !z-20'}
|
||||
popupClassName='!w-full !overflow-visible'
|
||||
manualClose
|
||||
|
||||
@ -1,38 +0,0 @@
|
||||
.actionIconWrapper {
|
||||
@apply h-8 w-8 p-2 rounded-md hover:bg-gray-100 !important;
|
||||
}
|
||||
|
||||
.commonIcon {
|
||||
@apply w-4 h-4 inline-block align-middle;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.actionIcon {
|
||||
@apply bg-gray-500;
|
||||
mask-image: url(~@/assets/action.svg);
|
||||
}
|
||||
|
||||
.actionItemIcon {
|
||||
@apply w-4 h-4 text-gray-500;
|
||||
}
|
||||
|
||||
.actionItem {
|
||||
@apply h-9 py-2 px-3 mx-1 flex items-center space-x-2 hover:bg-gray-100 rounded-lg cursor-pointer disabled:opacity-50;
|
||||
width: calc(100% - 0.5rem);
|
||||
}
|
||||
|
||||
.deleteActionItem {
|
||||
@apply hover:bg-red-50 !important;
|
||||
}
|
||||
|
||||
.actionName {
|
||||
@apply grow text-gray-700 text-sm text-left;
|
||||
}
|
||||
|
||||
.popup {
|
||||
left: 4px;
|
||||
transform: translateX(-100%);
|
||||
box-shadow: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03);
|
||||
}
|
||||
@ -3,6 +3,7 @@ import type { FC } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDebounce } from 'ahooks'
|
||||
import { RiEqualizer2Line } from '@remixicon/react'
|
||||
import Toast from '../../base/toast'
|
||||
import Filter from './filter'
|
||||
import type { QueryParam } from './filter'
|
||||
@ -11,7 +12,8 @@ import EmptyElement from './empty-element'
|
||||
import HeaderOpts from './header-opts'
|
||||
import { AnnotationEnableStatus, type AnnotationItem, type AnnotationItemBasic, JobStatus } from './type'
|
||||
import ViewAnnotationModal from './view-annotation-modal'
|
||||
import cn from '@/utils/classnames'
|
||||
import { MessageFast } from '@/app/components/base/icons/src/vender/solid/communication'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Pagination from '@/app/components/base/pagination'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import { addAnnotation, delAnnotation, fetchAnnotationConfig as doFetchAnnotationConfig, editAnnotation, fetchAnnotationList, queryAnnotationJobStatus, updateAnnotationScore, updateAnnotationStatus } from '@/service/annotation'
|
||||
@ -22,8 +24,8 @@ import type { AnnotationReplyConfig } from '@/models/debug'
|
||||
import { sleep } from '@/utils'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import AnnotationFullModal from '@/app/components/billing/annotation-full/modal'
|
||||
import { Settings04 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import type { App } from '@/types/app'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
appDetail: App
|
||||
@ -157,8 +159,9 @@ const Annotation: FC<Props> = ({
|
||||
<div className='flex items-center space-x-2'>
|
||||
{isChatApp && (
|
||||
<>
|
||||
<div className={cn(!annotationConfig?.enabled && 'pr-2', 'flex items-center h-7 rounded-lg border border-gray-200 pl-2 space-x-1')}>
|
||||
<div className='leading-[18px] text-[13px] font-medium text-gray-900'>{t('appAnnotation.name')}</div>
|
||||
<div className={cn(!annotationConfig?.enabled && 'pr-2', 'flex items-center h-7 rounded-lg bg-components-panel-bg-blur border border-components-panel-border pl-2 space-x-1')}>
|
||||
<MessageFast className='w-4 h-4 text-util-colors-indigo-indigo-600' />
|
||||
<div className='system-sm-medium text-text-primary'>{t('appAnnotation.name')}</div>
|
||||
<Switch
|
||||
key={controlRefreshSwitch}
|
||||
defaultValue={annotationConfig?.enabled}
|
||||
@ -185,22 +188,14 @@ const Annotation: FC<Props> = ({
|
||||
></Switch>
|
||||
{annotationConfig?.enabled && (
|
||||
<div className='flex items-center pl-1.5'>
|
||||
<div className='shrink-0 mr-1 w-[1px] h-3.5 bg-gray-200'></div>
|
||||
<div
|
||||
className={`
|
||||
shrink-0 h-7 w-7 flex items-center justify-center
|
||||
text-xs text-gray-700 font-medium
|
||||
`}
|
||||
onClick={() => { setIsShowEdit(true) }}
|
||||
>
|
||||
<div className='flex h-6 w-6 items-center justify-center rounded-md cursor-pointer hover:bg-gray-200'>
|
||||
<Settings04 className='w-4 h-4' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='shrink-0 mr-1 w-[1px] h-3.5 bg-divider-subtle'></div>
|
||||
<ActionButton onClick={() => setIsShowEdit(true)}>
|
||||
<RiEqualizer2Line className='w-4 h-4 text-text-tertiary' />
|
||||
</ActionButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='shrink-0 mx-3 w-[1px] h-3.5 bg-gray-200'></div>
|
||||
<div className='shrink-0 mx-3 w-[1px] h-3.5 bg-divider-regular'></div>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@ -2,12 +2,12 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiDeleteBinLine } from '@remixicon/react'
|
||||
import { Edit02 } from '../../base/icons/src/vender/line/general'
|
||||
import { RiDeleteBinLine, RiEditLine } from '@remixicon/react'
|
||||
import type { AnnotationItem } from './type'
|
||||
import RemoveAnnotationConfirmModal from './remove-annotation-confirm-modal'
|
||||
import cn from '@/utils/classnames'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import useTimestamp from '@/hooks/use-timestamp'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
list: AnnotationItem[]
|
||||
@ -59,26 +59,18 @@ const List: FC<Props> = ({
|
||||
<td className='p-3 pr-2'>{item.hit_count}</td>
|
||||
<td className='w-[96px] p-3 pr-2' onClick={e => e.stopPropagation()}>
|
||||
{/* Actions */}
|
||||
<div className='flex space-x-2 text-gray-500'>
|
||||
<div
|
||||
className='p-1 cursor-pointer rounded-md hover:bg-black/5'
|
||||
onClick={
|
||||
() => {
|
||||
onView(item)
|
||||
}
|
||||
}
|
||||
>
|
||||
<Edit02 className='w-4 h-4' />
|
||||
</div>
|
||||
<div
|
||||
className='p-1 cursor-pointer rounded-md hover:bg-black/5'
|
||||
<div className='flex space-x-1 text-text-tertiary'>
|
||||
<ActionButton onClick={() => onView(item)}>
|
||||
<RiEditLine className='w-4 h-4' />
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
onClick={() => {
|
||||
setCurrId(item.id)
|
||||
setShowConfirmDelete(true)
|
||||
}}
|
||||
>
|
||||
<RiDeleteBinLine className='w-4 h-4' />
|
||||
</div>
|
||||
</ActionButton>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@ -7,11 +7,11 @@ import { ClockFastForward } from '@/app/components/base/icons/src/vender/line/ti
|
||||
const HitHistoryNoData: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className='mx-auto mt-20 w-[480px] p-5 rounded-2xl bg-gray-50 space-y-2'>
|
||||
<div className='inline-block p-3 rounded-lg border border-gray-200'>
|
||||
<ClockFastForward className='w-5 h-5 text-gray-500' />
|
||||
<div className='mx-auto mt-20 w-[480px] p-5 rounded-2xl bg-background-section-burn space-y-2'>
|
||||
<div className='inline-block p-3 rounded-lg border border-divider-subtle'>
|
||||
<ClockFastForward className='w-5 h-5 text-text-tertiary' />
|
||||
</div>
|
||||
<div className='leading-5 text-sm font-normal text-gray-500'>{t('appAnnotation.viewModal.noHitHistory')}</div>
|
||||
<div className='system-sm-regular text-text-tertiary'>{t('appAnnotation.viewModal.noHitHistory')}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -4,17 +4,17 @@ import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import EditItem, { EditItemType } from '../edit-annotation-modal/edit-item'
|
||||
import type { AnnotationItem, HitHistoryItem } from '../type'
|
||||
import s from './style.module.css'
|
||||
import HitHistoryNoData from './hit-history-no-data'
|
||||
import cn from '@/utils/classnames'
|
||||
import Pagination from '@/app/components/base/pagination'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import Drawer from '@/app/components/base/drawer-plus'
|
||||
import Pagination from '@/app/components/base/pagination'
|
||||
import { MessageCheckRemove } from '@/app/components/base/icons/src/vender/line/communication'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import TabSlider from '@/app/components/base/tab-slider-plain'
|
||||
import { fetchHitHistoryList } from '@/service/annotation'
|
||||
import { APP_PAGE_LIMIT } from '@/config'
|
||||
import useTimestamp from '@/hooks/use-timestamp'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
appId: string
|
||||
@ -72,7 +72,9 @@ const ViewAnnotationModal: FC<Props> = ({
|
||||
? (
|
||||
<div className='flex items-center space-x-1'>
|
||||
<div>{t('appAnnotation.viewModal.hitHistory')}</div>
|
||||
<div className='flex px-1.5 item-center rounded-md border border-black/[8%] h-5 text-xs font-medium text-gray-500'>{total} {t(`appAnnotation.viewModal.hit${hitHistoryList.length > 1 ? 's' : ''}`)}</div>
|
||||
<Badge
|
||||
text={`${total} ${t(`appAnnotation.viewModal.hit${hitHistoryList.length > 1 ? 's' : ''}`)}`}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
: t('appAnnotation.viewModal.hitHistory')
|
||||
@ -111,44 +113,45 @@ const ViewAnnotationModal: FC<Props> = ({
|
||||
? (<HitHistoryNoData />)
|
||||
: (
|
||||
<div>
|
||||
<table className={cn(s.table, 'w-full min-w-[440px] border-collapse border-0 text-sm')} >
|
||||
<thead className="h-8 leading-8 border-b border-gray-200 text-gray-500 font-bold">
|
||||
<tr className='uppercase'>
|
||||
<td className='whitespace-nowrap'>{t('appAnnotation.hitHistoryTable.query')}</td>
|
||||
<td className='whitespace-nowrap'>{t('appAnnotation.hitHistoryTable.match')}</td>
|
||||
<td className='whitespace-nowrap'>{t('appAnnotation.hitHistoryTable.response')}</td>
|
||||
<td className='whitespace-nowrap'>{t('appAnnotation.hitHistoryTable.source')}</td>
|
||||
<td className='whitespace-nowrap'>{t('appAnnotation.hitHistoryTable.score')}</td>
|
||||
<td className='whitespace-nowrap w-[160px]'>{t('appAnnotation.hitHistoryTable.time')}</td>
|
||||
<table className={cn('w-full min-w-[440px] border-collapse border-0')} >
|
||||
<thead className="system-xs-medium-uppercase text-text-tertiary">
|
||||
<tr>
|
||||
<td className='pl-2 pr-1 w-5 rounded-l-lg bg-background-section-burn whitespace-nowrap'>{t('appAnnotation.hitHistoryTable.query')}</td>
|
||||
<td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{t('appAnnotation.hitHistoryTable.match')}</td>
|
||||
<td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{t('appAnnotation.hitHistoryTable.response')}</td>
|
||||
<td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{t('appAnnotation.hitHistoryTable.source')}</td>
|
||||
<td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{t('appAnnotation.hitHistoryTable.score')}</td>
|
||||
<td className='pl-3 py-1.5 rounded-r-lg bg-background-section-burn whitespace-nowrap w-[160px]'>{t('appAnnotation.hitHistoryTable.time')}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-gray-500">
|
||||
<tbody className="text-text-secondary system-sm-regular">
|
||||
{hitHistoryList.map(item => (
|
||||
<tr
|
||||
key={item.id}
|
||||
className={'border-b border-gray-200 h-8 hover:bg-gray-50 cursor-pointer'}
|
||||
className={'border-b border-divider-subtle hover:bg-background-default-hover cursor-pointer'}
|
||||
>
|
||||
<td
|
||||
className='whitespace-nowrap overflow-hidden text-ellipsis max-w-[250px]'
|
||||
className='p-3 pr-2 whitespace-nowrap overflow-hidden text-ellipsis max-w-[250px]'
|
||||
title={item.question}
|
||||
>{item.question}</td>
|
||||
<td
|
||||
className='whitespace-nowrap overflow-hidden text-ellipsis max-w-[250px]'
|
||||
className='p-3 pr-2 whitespace-nowrap overflow-hidden text-ellipsis max-w-[250px]'
|
||||
title={item.match}
|
||||
>{item.match}</td>
|
||||
<td
|
||||
className='whitespace-nowrap overflow-hidden text-ellipsis max-w-[250px]'
|
||||
className='p-3 pr-2 whitespace-nowrap overflow-hidden text-ellipsis max-w-[250px]'
|
||||
title={item.response}
|
||||
>{item.response}</td>
|
||||
<td>{item.source}</td>
|
||||
<td>{item.score ? item.score.toFixed(2) : '-'}</td>
|
||||
<td>{formatTime(item.created_at, t('appLog.dateTimeFormat') as string)}</td>
|
||||
<td className='p-3 pr-2'>{item.source}</td>
|
||||
<td className='p-3 pr-2'>{item.score ? item.score.toFixed(2) : '-'}</td>
|
||||
<td className='p-3 pr-2'>{formatTime(item.created_at, t('appLog.dateTimeFormat') as string)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{(total && total > APP_PAGE_LIMIT)
|
||||
? <Pagination
|
||||
className='px-0'
|
||||
current={currPage}
|
||||
onChange={setCurrPage}
|
||||
total={total}
|
||||
@ -163,7 +166,6 @@ const ViewAnnotationModal: FC<Props> = ({
|
||||
isShow={isShow}
|
||||
onHide={onHide}
|
||||
maxWidthClassName='!max-w-[800px]'
|
||||
// t('appAnnotation.editModal.title') as string
|
||||
title={
|
||||
<TabSlider
|
||||
className='shrink-0 relative top-[9px]'
|
||||
@ -193,7 +195,7 @@ const ViewAnnotationModal: FC<Props> = ({
|
||||
)}
|
||||
foot={id
|
||||
? (
|
||||
<div className='px-4 flex h-16 items-center justify-between border-t border-black/5 bg-gray-50 rounded-bl-xl rounded-br-xl leading-[18px] text-[13px] font-medium text-gray-500'>
|
||||
<div className='px-4 flex h-16 items-center justify-between border-t border-divider-subtle bg-background-section-burn rounded-bl-xl rounded-br-xl system-sm-medium text-text-tertiary'>
|
||||
<div
|
||||
className='flex items-center pl-3 space-x-2 cursor-pointer'
|
||||
onClick={() => setShowModal(true)}
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
.table td {
|
||||
padding: 7px 8px;
|
||||
box-sizing: border-box;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.pagination li {
|
||||
list-style: none;
|
||||
}
|
||||
@ -5,7 +5,8 @@ import {
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import dayjs from 'dayjs'
|
||||
import { RiArrowDownSLine } from '@remixicon/react'
|
||||
import { RiArrowDownSLine, RiPlanetLine } from '@remixicon/react'
|
||||
import Toast from '../../base/toast'
|
||||
import type { ModelAndParameter } from '../configuration/debug/types'
|
||||
import SuggestedAction from './suggested-action'
|
||||
import PublishWithMultipleModel from './publish-with-multiple-model'
|
||||
@ -15,6 +16,7 @@ import {
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import { fetchInstalledAppList } from '@/service/explore'
|
||||
import EmbeddedModal from '@/app/components/app/overview/embedded'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
@ -105,6 +107,19 @@ const AppPublisher = ({
|
||||
setPublished(false)
|
||||
}, [disabled, onToggle, open])
|
||||
|
||||
const handleOpenInExplore = useCallback(async () => {
|
||||
try {
|
||||
const { installed_apps }: any = await fetchInstalledAppList(appDetail?.id) || {}
|
||||
if (installed_apps?.length > 0)
|
||||
window.open(`/explore/installed/${installed_apps[0].id}`, '_blank')
|
||||
else
|
||||
throw new Error('No app found in Explore')
|
||||
}
|
||||
catch (e: any) {
|
||||
Toast.notify({ type: 'error', message: `${e.message || e}` })
|
||||
}
|
||||
}, [appDetail?.id])
|
||||
|
||||
const [embeddingModalOpen, setEmbeddingModalOpen] = useState(false)
|
||||
|
||||
return (
|
||||
@ -205,6 +220,15 @@ const AppPublisher = ({
|
||||
{t('workflow.common.embedIntoSite')}
|
||||
</SuggestedAction>
|
||||
)}
|
||||
<SuggestedAction
|
||||
onClick={() => {
|
||||
handleOpenInExplore()
|
||||
}}
|
||||
disabled={!publishedAt}
|
||||
icon={<RiPlanetLine className='w-4 h-4' />}
|
||||
>
|
||||
{t('workflow.common.openInExplore')}
|
||||
</SuggestedAction>
|
||||
<SuggestedAction disabled={!publishedAt} link='./develop' icon={<FileText className='w-4 h-4' />}>{t('workflow.common.accessAPIReference')}</SuggestedAction>
|
||||
{appDetail?.mode === 'workflow' && (
|
||||
<WorkflowToolConfigureButton
|
||||
|
||||
60
web/app/components/app/create-app-dialog/app-card/index.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
'use client'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { PlusIcon } from '@heroicons/react/20/solid'
|
||||
import { AppTypeIcon, AppTypeLabel } from '../../type-selector'
|
||||
import Button from '@/app/components/base/button'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { App } from '@/models/explore'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
|
||||
export type AppCardProps = {
|
||||
app: App
|
||||
canCreate: boolean
|
||||
onCreate: () => void
|
||||
}
|
||||
|
||||
const AppCard = ({
|
||||
app,
|
||||
onCreate,
|
||||
}: AppCardProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { app: appBasicInfo } = app
|
||||
return (
|
||||
<div className={cn('p-4 h-[132px] relative overflow-hidden flex flex-col group bg-components-panel-on-panel-item-bg border-[0.5px] border-components-panel-border rounded-xl shadow-xs hover:shadow-lg cursor-pointer')}>
|
||||
<div className='flex items-center gap-3 pb-2 grow-0 shrink-0'>
|
||||
<div className='relative shrink-0'>
|
||||
<AppIcon
|
||||
size='large'
|
||||
iconType={app.app.icon_type}
|
||||
icon={app.app.icon}
|
||||
background={app.app.icon_background}
|
||||
imageUrl={app.app.icon_url}
|
||||
/>
|
||||
<AppTypeIcon wrapperClassName='absolute -bottom-0.5 -right-0.5 w-4 h-4 rounded-[4px] border border-divider-regular outline outline-components-panel-on-panel-item-bg'
|
||||
className='w-3 h-3' type={appBasicInfo.mode} />
|
||||
</div>
|
||||
<div className='grow flex flex-col gap-1'>
|
||||
<div className='line-clamp-1'>
|
||||
<span className='system-md-semibold text-text-secondary' title={appBasicInfo.name}>{appBasicInfo.name}</span>
|
||||
</div>
|
||||
<AppTypeLabel className='system-2xs-medium-uppercase text-text-tertiary' type={app.app.mode} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="py-1 system-xs-regular text-text-tertiary">
|
||||
<div className='line-clamp-3'>
|
||||
{app.description}
|
||||
</div>
|
||||
</div>
|
||||
<div className={cn('hidden absolute bottom-0 left-0 right-0 p-4 pt-8 group-hover:flex bg-gradient-to-t from-[60.27%] from-components-panel-gradient-2 to-transparent')}>
|
||||
<div className={cn('flex items-center w-full space-x-2 h-8')}>
|
||||
<Button variant='primary' className='grow' onClick={() => onCreate()}>
|
||||
<PlusIcon className='w-4 h-4 mr-1' />
|
||||
<span className='text-xs'>{t('app.newApp.useTemplate')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AppCard
|
||||
247
web/app/components/app/create-app-dialog/app-list/index.tsx
Normal file
@ -0,0 +1,247 @@
|
||||
'use client'
|
||||
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import useSWR from 'swr'
|
||||
import { useDebounceFn } from 'ahooks'
|
||||
import { RiRobot2Line } from '@remixicon/react'
|
||||
import AppCard from '../app-card'
|
||||
import Sidebar, { AppCategories, AppCategoryLabel } from './sidebar'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import cn from '@/utils/classnames'
|
||||
import ExploreContext from '@/context/explore-context'
|
||||
import type { App } from '@/models/explore'
|
||||
import { fetchAppDetail, fetchAppList } from '@/service/explore'
|
||||
import { importDSL } from '@/service/apps'
|
||||
import { useTabSearchParams } from '@/hooks/use-tab-searchparams'
|
||||
import CreateAppModal from '@/app/components/explore/create-app-modal'
|
||||
import AppTypeSelector from '@/app/components/app/type-selector'
|
||||
import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { getRedirection } from '@/utils/app-redirection'
|
||||
import Input from '@/app/components/base/input'
|
||||
import type { AppMode } from '@/types/app'
|
||||
import { DSLImportMode } from '@/models/app'
|
||||
|
||||
type AppsProps = {
|
||||
onSuccess?: () => void
|
||||
onCreateFromBlank?: () => void
|
||||
}
|
||||
|
||||
// export enum PageType {
|
||||
// EXPLORE = 'explore',
|
||||
// CREATE = 'create',
|
||||
// }
|
||||
|
||||
const Apps = ({
|
||||
onSuccess,
|
||||
onCreateFromBlank,
|
||||
}: AppsProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { isCurrentWorkspaceEditor } = useAppContext()
|
||||
const { push } = useRouter()
|
||||
const { hasEditPermission } = useContext(ExploreContext)
|
||||
const allCategoriesEn = AppCategories.RECOMMENDED
|
||||
|
||||
const [keywords, setKeywords] = useState('')
|
||||
const [searchKeywords, setSearchKeywords] = useState('')
|
||||
|
||||
const { run: handleSearch } = useDebounceFn(() => {
|
||||
setSearchKeywords(keywords)
|
||||
}, { wait: 500 })
|
||||
|
||||
const handleKeywordsChange = (value: string) => {
|
||||
setKeywords(value)
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
const [currentType, setCurrentType] = useState<AppMode[]>([])
|
||||
const [currCategory, setCurrCategory] = useTabSearchParams({
|
||||
defaultTab: allCategoriesEn,
|
||||
disableSearchParams: true,
|
||||
})
|
||||
|
||||
const {
|
||||
data: { categories, allList },
|
||||
} = useSWR(
|
||||
['/explore/apps'],
|
||||
() =>
|
||||
fetchAppList().then(({ categories, recommended_apps }) => ({
|
||||
categories,
|
||||
allList: recommended_apps.sort((a, b) => a.position - b.position),
|
||||
})),
|
||||
{
|
||||
fallbackData: {
|
||||
categories: [],
|
||||
allList: [],
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
const filteredList = useMemo(() => {
|
||||
const filteredByCategory = allList.filter((item) => {
|
||||
if (currCategory === allCategoriesEn)
|
||||
return true
|
||||
return item.category === currCategory
|
||||
})
|
||||
if (currentType.length === 0)
|
||||
return filteredByCategory
|
||||
return filteredByCategory.filter((item) => {
|
||||
if (currentType.includes('chat') && item.app.mode === 'chat')
|
||||
return true
|
||||
if (currentType.includes('advanced-chat') && item.app.mode === 'advanced-chat')
|
||||
return true
|
||||
if (currentType.includes('agent-chat') && item.app.mode === 'agent-chat')
|
||||
return true
|
||||
if (currentType.includes('completion') && item.app.mode === 'completion')
|
||||
return true
|
||||
if (currentType.includes('workflow') && item.app.mode === 'workflow')
|
||||
return true
|
||||
return false
|
||||
})
|
||||
}, [currentType, currCategory, allCategoriesEn, allList])
|
||||
|
||||
const searchFilteredList = useMemo(() => {
|
||||
if (!searchKeywords || !filteredList || filteredList.length === 0)
|
||||
return filteredList
|
||||
|
||||
const lowerCaseSearchKeywords = searchKeywords.toLowerCase()
|
||||
|
||||
return filteredList.filter(item =>
|
||||
item.app && item.app.name && item.app.name.toLowerCase().includes(lowerCaseSearchKeywords),
|
||||
)
|
||||
}, [searchKeywords, filteredList])
|
||||
|
||||
const [currApp, setCurrApp] = React.useState<App | null>(null)
|
||||
const [isShowCreateModal, setIsShowCreateModal] = React.useState(false)
|
||||
const onCreate: CreateAppModalProps['onConfirm'] = async ({
|
||||
name,
|
||||
icon_type,
|
||||
icon,
|
||||
icon_background,
|
||||
description,
|
||||
}) => {
|
||||
const { export_data } = await fetchAppDetail(
|
||||
currApp?.app.id as string,
|
||||
)
|
||||
try {
|
||||
const app = await importDSL({
|
||||
mode: DSLImportMode.YAML_CONTENT,
|
||||
yaml_content: export_data,
|
||||
name,
|
||||
icon_type,
|
||||
icon,
|
||||
icon_background,
|
||||
description,
|
||||
})
|
||||
setIsShowCreateModal(false)
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('app.newApp.appCreated'),
|
||||
})
|
||||
if (onSuccess)
|
||||
onSuccess()
|
||||
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
|
||||
getRedirection(isCurrentWorkspaceEditor, app, push)
|
||||
}
|
||||
catch (e) {
|
||||
Toast.notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
|
||||
}
|
||||
}
|
||||
|
||||
if (!categories || categories.length === 0) {
|
||||
return (
|
||||
<div className="flex h-full items-center">
|
||||
<Loading type="area" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='h-full flex flex-col'>
|
||||
<div className='flex justify-between items-center py-3 border-b border-divider-burn'>
|
||||
<div className='min-w-[180px] pl-5'>
|
||||
<span className='title-xl-semi-bold text-text-primary'>{t('app.newApp.startFromTemplate')}</span>
|
||||
</div>
|
||||
<div className='flex-1 max-w-[548px] p-1.5 flex items-center rounded-xl shadow-md bg-components-panel-bg-blur border border-components-panel-border'>
|
||||
<AppTypeSelector value={currentType} onChange={setCurrentType} />
|
||||
<div className='h-[14px]'>
|
||||
<Divider type='vertical' />
|
||||
</div>
|
||||
<Input
|
||||
showClearIcon
|
||||
wrapperClassName='w-full flex-1'
|
||||
className='bg-transparent hover:bg-transparent focus:bg-transparent hover:border-transparent focus:border-transparent focus:shadow-none'
|
||||
placeholder={t('app.newAppFromTemplate.searchAllTemplate') as string}
|
||||
value={keywords}
|
||||
onChange={e => handleKeywordsChange(e.target.value)}
|
||||
onClear={() => handleKeywordsChange('')}
|
||||
/>
|
||||
</div>
|
||||
<div className='w-[180px] h-8'></div>
|
||||
</div>
|
||||
<div className='relative flex flex-1 overflow-y-auto'>
|
||||
{!searchKeywords && <div className='w-[200px] h-full p-4'>
|
||||
<Sidebar current={currCategory as AppCategories} onClick={(category) => { setCurrCategory(category) }} onCreateFromBlank={onCreateFromBlank} />
|
||||
</div>}
|
||||
<div className='flex-1 h-full overflow-auto shrink-0 grow p-6 pt-2 border-l border-divider-burn'>
|
||||
{searchFilteredList && searchFilteredList.length > 0 && <>
|
||||
<div className='pt-4 pb-1'>
|
||||
{searchKeywords
|
||||
? <p className='title-md-semi-bold text-text-tertiary'>{searchFilteredList.length > 1 ? t('app.newApp.foundResults', { count: searchFilteredList.length }) : t('app.newApp.foundResult', { count: searchFilteredList.length })}</p>
|
||||
: <AppCategoryLabel category={currCategory as AppCategories} className='title-md-semi-bold text-text-primary' />}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'grid content-start shrink-0 gap-3 grid-cols-1 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5 2k:grid-cols-6',
|
||||
)}>
|
||||
{searchFilteredList.map(app => (
|
||||
<AppCard
|
||||
key={app.app_id}
|
||||
app={app}
|
||||
canCreate={hasEditPermission}
|
||||
onCreate={() => {
|
||||
setCurrApp(app)
|
||||
setIsShowCreateModal(true)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>}
|
||||
{(!searchFilteredList || searchFilteredList.length === 0) && <NoTemplateFound />}
|
||||
</div>
|
||||
</div>
|
||||
{isShowCreateModal && (
|
||||
<CreateAppModal
|
||||
appIconType={currApp?.app.icon_type || 'emoji'}
|
||||
appIcon={currApp?.app.icon || ''}
|
||||
appIconBackground={currApp?.app.icon_background || ''}
|
||||
appIconUrl={currApp?.app.icon_url}
|
||||
appName={currApp?.app.name || ''}
|
||||
appDescription={currApp?.app.description || ''}
|
||||
show={isShowCreateModal}
|
||||
onConfirm={onCreate}
|
||||
onHide={() => setIsShowCreateModal(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Apps)
|
||||
|
||||
function NoTemplateFound() {
|
||||
const { t } = useTranslation()
|
||||
return <div className='p-4 rounded-lg w-full bg-workflow-process-bg'>
|
||||
<div className='w-8 h-8 rounded-lg inline-flex items-center justify-center mb-2 shadow-lg bg-components-card-bg'>
|
||||
<RiRobot2Line className='w-5 h-5 text-text-tertiary' />
|
||||
</div>
|
||||
<p className='title-md-semi-bold text-text-primary'>{t('app.newApp.noTemplateFound')}</p>
|
||||
<p className='system-sm-regular text-text-tertiary'>{t('app.newApp.noTemplateFoundTip')}</p>
|
||||
</div>
|
||||
}
|
||||
@ -0,0 +1,91 @@
|
||||
'use client'
|
||||
import { RiAppsFill, RiChatSmileAiFill, RiExchange2Fill, RiPassPendingFill, RiQuillPenAiFill, RiSpeakAiFill, RiStickyNoteAddLine, RiTerminalBoxFill, RiThumbUpFill } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import classNames from '@/utils/classnames'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
|
||||
export enum AppCategories {
|
||||
RECOMMENDED = 'Recommended',
|
||||
ASSISTANT = 'Assistant',
|
||||
AGENT = 'Agent',
|
||||
HR = 'HR',
|
||||
PROGRAMMING = 'Programming',
|
||||
WORKFLOW = 'Workflow',
|
||||
WRITING = 'Writing',
|
||||
}
|
||||
|
||||
type SidebarProps = {
|
||||
current: AppCategories
|
||||
onClick?: (category: AppCategories) => void
|
||||
onCreateFromBlank?: () => void
|
||||
}
|
||||
|
||||
export default function Sidebar({ current, onClick, onCreateFromBlank }: SidebarProps) {
|
||||
const { t } = useTranslation()
|
||||
return <div className="w-full h-full flex flex-col">
|
||||
<ul>
|
||||
<CategoryItem category={AppCategories.RECOMMENDED} active={current === AppCategories.RECOMMENDED} onClick={onClick} />
|
||||
</ul>
|
||||
<div className='px-3 pt-2 pb-1 system-xs-medium-uppercase text-text-tertiary'>{t('app.newAppFromTemplate.byCategories')}</div>
|
||||
<ul className='flex-grow flex flex-col gap-0.5'>
|
||||
<CategoryItem category={AppCategories.ASSISTANT} active={current === AppCategories.ASSISTANT} onClick={onClick} />
|
||||
<CategoryItem category={AppCategories.AGENT} active={current === AppCategories.AGENT} onClick={onClick} />
|
||||
<CategoryItem category={AppCategories.HR} active={current === AppCategories.HR} onClick={onClick} />
|
||||
<CategoryItem category={AppCategories.PROGRAMMING} active={current === AppCategories.PROGRAMMING} onClick={onClick} />
|
||||
<CategoryItem category={AppCategories.WORKFLOW} active={current === AppCategories.WORKFLOW} onClick={onClick} />
|
||||
<CategoryItem category={AppCategories.WRITING} active={current === AppCategories.WRITING} onClick={onClick} />
|
||||
</ul>
|
||||
<Divider bgStyle='gradient' />
|
||||
<div className='px-3 py-1 flex items-center gap-1 text-text-tertiary cursor-pointer' onClick={onCreateFromBlank}>
|
||||
<RiStickyNoteAddLine className='w-3.5 h-3.5' />
|
||||
<span className='system-xs-regular'>{t('app.newApp.startFromBlank')}</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
type CategoryItemProps = {
|
||||
active: boolean
|
||||
category: AppCategories
|
||||
onClick?: (category: AppCategories) => void
|
||||
}
|
||||
function CategoryItem({ category, active, onClick }: CategoryItemProps) {
|
||||
return <li
|
||||
className={classNames('p-1 pl-3 rounded-lg flex items-center gap-2 group cursor-pointer hover:bg-state-base-hover [&.active]:bg-state-base-active', active && 'active')}
|
||||
onClick={() => { onClick?.(category) }}>
|
||||
<div className='w-5 h-5 inline-flex items-center justify-center rounded-md border border-divider-regular bg-components-icon-bg-midnight-solid group-[.active]:bg-components-icon-bg-blue-solid'>
|
||||
<AppCategoryIcon category={category} />
|
||||
</div>
|
||||
<AppCategoryLabel category={category}
|
||||
className={classNames('system-sm-medium text-components-menu-item-text group-[.active]:text-components-menu-item-text-active group-hover:text-components-menu-item-text-hover', active && 'system-sm-semibold')} />
|
||||
</li >
|
||||
}
|
||||
|
||||
type AppCategoryLabelProps = {
|
||||
category: AppCategories
|
||||
className?: string
|
||||
}
|
||||
export function AppCategoryLabel({ category, className }: AppCategoryLabelProps) {
|
||||
const { t } = useTranslation()
|
||||
return <span className={className}>{t(`app.newAppFromTemplate.sidebar.${category}`)}</span>
|
||||
}
|
||||
|
||||
type AppCategoryIconProps = {
|
||||
category: AppCategories
|
||||
}
|
||||
function AppCategoryIcon({ category }: AppCategoryIconProps) {
|
||||
if (category === AppCategories.AGENT)
|
||||
return <RiSpeakAiFill className='w-3.5 h-3.5 text-components-avatar-shape-fill-stop-100' />
|
||||
if (category === AppCategories.ASSISTANT)
|
||||
return <RiChatSmileAiFill className='w-3.5 h-3.5 text-components-avatar-shape-fill-stop-100' />
|
||||
if (category === AppCategories.HR)
|
||||
return <RiPassPendingFill className='w-3.5 h-3.5 text-components-avatar-shape-fill-stop-100' />
|
||||
if (category === AppCategories.PROGRAMMING)
|
||||
return <RiTerminalBoxFill className='w-3.5 h-3.5 text-components-avatar-shape-fill-stop-100' />
|
||||
if (category === AppCategories.RECOMMENDED)
|
||||
return <RiThumbUpFill className='w-3.5 h-3.5 text-components-avatar-shape-fill-stop-100' />
|
||||
if (category === AppCategories.WRITING)
|
||||
return <RiQuillPenAiFill className='w-3.5 h-3.5 text-components-avatar-shape-fill-stop-100' />
|
||||
if (category === AppCategories.WORKFLOW)
|
||||
return <RiExchange2Fill className='w-3.5 h-3.5 text-components-avatar-shape-fill-stop-100' />
|
||||
return <RiAppsFill className='w-3.5 h-3.5 text-components-avatar-shape-fill-stop-100' />
|
||||
}
|
||||
@ -1,36 +1,26 @@
|
||||
'use client'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import NewAppDialog from './newAppDialog'
|
||||
import AppList, { PageType } from '@/app/components/explore/app-list'
|
||||
import AppList from './app-list'
|
||||
import FullScreenModal from '@/app/components/base/fullscreen-modal'
|
||||
|
||||
type CreateAppDialogProps = {
|
||||
show: boolean
|
||||
onSuccess: () => void
|
||||
onClose: () => void
|
||||
onCreateFromBlank?: () => void
|
||||
}
|
||||
|
||||
const CreateAppTemplateDialog = ({ show, onSuccess, onClose }: CreateAppDialogProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const CreateAppTemplateDialog = ({ show, onSuccess, onClose, onCreateFromBlank }: CreateAppDialogProps) => {
|
||||
return (
|
||||
<NewAppDialog
|
||||
className='flex'
|
||||
show={show}
|
||||
onClose={() => {}}
|
||||
<FullScreenModal
|
||||
open={show}
|
||||
closable
|
||||
onClose={onClose}
|
||||
>
|
||||
{/* template list */}
|
||||
<div className='grow flex flex-col h-full bg-gray-100'>
|
||||
<div className='shrink-0 pl-8 pr-6 pt-6 pb-3 bg-gray-100 rounded-se-xl text-xl leading-[30px] font-semibold text-gray-900 z-10'>{t('app.newApp.startFromTemplate')}</div>
|
||||
<AppList onSuccess={() => {
|
||||
onSuccess()
|
||||
onClose()
|
||||
}} pageType={PageType.CREATE} />
|
||||
</div>
|
||||
<div className='absolute right-6 top-6 p-2 cursor-pointer z-20' onClick={onClose}>
|
||||
<RiCloseLine className='w-4 h-4 text-gray-500' />
|
||||
</div>
|
||||
</NewAppDialog>
|
||||
<AppList onCreateFromBlank={onCreateFromBlank} onSuccess={() => {
|
||||
onSuccess()
|
||||
onClose()
|
||||
}} />
|
||||
</FullScreenModal>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 6.0 KiB |
@ -1,16 +0,0 @@
|
||||
<svg width="125" height="74" viewBox="0 0 125 74" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Grid BG">
|
||||
<mask id="mask0_3169_30051" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="125" height="74">
|
||||
<rect id="Mask" width="125" height="74" fill="url(#paint0_linear_3169_30051)"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_3169_30051)">
|
||||
<path id="Grid" d="M7 -3H-1V5M7 -3V5M7 -3H15M7 5H-1M7 5H15M7 5V13M-1 5V13M15 -3V5M15 -3H23M15 5H23M15 5V13M23 -3V5M23 -3H31M23 5H31M23 5V13M31 -3V5M31 -3H39M31 5H39M31 5V13M39 -3V5M39 -3H47M39 5H47M39 5V13M47 -3V5M47 -3H55M47 5H55M47 5V13M55 -3V5M55 -3H63M55 5H63M55 5V13M63 -3V5M63 -3H71M63 5H71M63 5V13M71 -3V5M71 -3H79M71 5H79M71 5V13M79 -3V5M79 -3H87M79 5H87M79 5V13M87 -3V5M87 -3H95M87 5H95M87 5V13M95 -3V5M95 -3H103M95 5H103M95 5V13M103 -3V5M103 -3H111M103 5H111M103 5V13M111 -3V5M111 -3H119M111 5H119M111 5V13M119 -3V5M119 -3H127V5M119 5H127M119 5V13M127 5V13M7 13H-1M7 13H15M7 13V21M-1 13V21M15 13H23M15 13V21M23 13H31M23 13V21M31 13H39M31 13V21M39 13H47M39 13V21M47 13H55M47 13V21M55 13H63M55 13V21M63 13H71M63 13V21M71 13H79M71 13V21M79 13H87M79 13V21M87 13H95M87 13V21M95 13H103M95 13V21M103 13H111M103 13V21M111 13H119M111 13V21M119 13H127M119 13V21M127 13V21M7 21H-1M7 21H15M7 21V29M-1 21V29M15 21H23M15 21V29M23 21H31M23 21V29M31 21H39M31 21V29M39 21H47M39 21V29M47 21H55M47 21V29M55 21H63M55 21V29M63 21H71M63 21V29M71 21H79M71 21V29M79 21H87M79 21V29M87 21H95M87 21V29M95 21H103M95 21V29M103 21H111M103 21V29M111 21H119M111 21V29M119 21H127M119 21V29M127 21V29M7 29H-1M7 29H15M7 29V37M-1 29V37M15 29H23M15 29V37M23 29H31M23 29V37M31 29H39M31 29V37M39 29H47M39 29V37M47 29H55M47 29V37M55 29H63M55 29V37M63 29H71M63 29V37M71 29H79M71 29V37M79 29H87M79 29V37M87 29H95M87 29V37M95 29H103M95 29V37M103 29H111M103 29V37M111 29H119M111 29V37M119 29H127M119 29V37M127 29V37M7 37H-1M7 37H15M7 37V45M-1 37V45M15 37H23M15 37V45M23 37H31M23 37V45M31 37H39M31 37V45M39 37H47M39 37V45M47 37H55M47 37V45M55 37H63M55 37V45M63 37H71M63 37V45M71 37H79M71 37V45M79 37H87M79 37V45M87 37H95M87 37V45M95 37H103M95 37V45M103 37H111M103 37V45M111 37H119M111 37V45M119 37H127M119 37V45M127 37V45M7 45H-1M7 45H15M7 45V53M-1 45V53M15 45H23M15 45V53M23 45H31M23 45V53M31 45H39M31 45V53M39 45H47M39 45V53M47 45H55M47 45V53M55 45H63M55 45V53M63 45H71M63 45V53M71 45H79M71 45V53M79 45H87M79 45V53M87 45H95M87 45V53M95 45H103M95 45V53M103 45H111M103 45V53M111 45H119M111 45V53M119 45H127M119 45V53M127 45V53M7 53H-1M7 53H15M7 53V61M-1 53V61M15 53H23M15 53V61M23 53H31M23 53V61M31 53H39M31 53V61M39 53H47M39 53V61M47 53H55M47 53V61M55 53H63M55 53V61M63 53H71M63 53V61M71 53H79M71 53V61M79 53H87M79 53V61M87 53H95M87 53V61M95 53H103M95 53V61M103 53H111M103 53V61M111 53H119M111 53V61M119 53H127M119 53V61M127 53V61M7 61H-1M7 61H15M7 61V69M-1 61V69M15 61H23M15 61V69M23 61H31M23 61V69M31 61H39M31 61V69M39 61H47M39 61V69M47 61H55M47 61V69M55 61H63M55 61V69M63 61H71M63 61V69M71 61H79M71 61V69M79 61H87M79 61V69M87 61H95M87 61V69M95 61H103M95 61V69M103 61H111M103 61V69M111 61H119M111 61V69M119 61H127M119 61V69M127 61V69M7 69H-1M7 69H15M7 69V77M-1 69V77H7M15 69H23M15 69V77M23 69H31M23 69V77M31 69H39M31 69V77M39 69H47M39 69V77M47 69H55M47 69V77M55 69H63M55 69V77M63 69H71M63 69V77M71 69H79M71 69V77M79 69H87M79 69V77M87 69H95M87 69V77M95 69H103M95 69V77M103 69H111M103 69V77M111 69H119M111 69V77M119 69H127M119 69V77M127 69V77H119M7 77H15M15 77H23M23 77H31M31 77H39M39 77H47M47 77H55M55 77H63M63 77H71M71 77H79M79 77H87M87 77H95M95 77H103M103 77H111M111 77H119" stroke="#1570EF" stroke-width="0.5"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_3169_30051" x1="62.5" y1="0" x2="62.5" y2="74" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#D9D9D9" stop-opacity="0.08"/>
|
||||
<stop offset="1" stop-color="#737373" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 6.0 KiB |
@ -1,48 +1,46 @@
|
||||
'use client'
|
||||
import type { MouseEventHandler } from 'react'
|
||||
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiCloseLine,
|
||||
RiQuestionLine,
|
||||
} from '@remixicon/react'
|
||||
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useContext, useContextSelector } from 'use-context-selector'
|
||||
import { RiArrowRightLine, RiCommandLine, RiCornerDownLeftLine, RiExchange2Fill } from '@remixicon/react'
|
||||
import Link from 'next/link'
|
||||
import { useDebounceFn, useKeyPress } from 'ahooks'
|
||||
import Image from 'next/image'
|
||||
import AppIconPicker from '../../base/app-icon-picker'
|
||||
import type { AppIconSelection } from '../../base/app-icon-picker'
|
||||
import s from './style.module.css'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import cn from '@/utils/classnames'
|
||||
import AppsContext, { useAppContext } from '@/context/app-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import type { AppMode } from '@/types/app'
|
||||
import { createApp } from '@/service/apps'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import AppsFull from '@/app/components/billing/apps-full-in-dialog'
|
||||
import { AiText, ChatBot, CuteRobot } from '@/app/components/base/icons/src/vender/solid/communication'
|
||||
import { Route } from '@/app/components/base/icons/src/vender/solid/mapsAndTravel'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { BubbleTextMod, ChatBot, ListSparkle, Logic } from '@/app/components/base/icons/src/vender/solid/communication'
|
||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
import { getRedirection } from '@/utils/app-redirection'
|
||||
import FullScreenModal from '@/app/components/base/fullscreen-modal'
|
||||
|
||||
type CreateAppDialogProps = {
|
||||
show: boolean
|
||||
type CreateAppProps = {
|
||||
onSuccess: () => void
|
||||
onClose: () => void
|
||||
onCreateFromTemplate?: () => void
|
||||
}
|
||||
|
||||
const CreateAppModal = ({ show, onSuccess, onClose }: CreateAppDialogProps) => {
|
||||
function CreateApp({ onClose, onSuccess, onCreateFromTemplate }: CreateAppProps) {
|
||||
const { t } = useTranslation()
|
||||
const { push } = useRouter()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const mutateApps = useContextSelector(AppsContext, state => state.mutateApps)
|
||||
|
||||
const [appMode, setAppMode] = useState<AppMode>('chat')
|
||||
const [showChatBotType, setShowChatBotType] = useState<boolean>(true)
|
||||
const [appIcon, setAppIcon] = useState<AppIconSelection>({ type: 'emoji', icon: '🤖', background: '#FFEAD5' })
|
||||
const [showAppIconPicker, setShowAppIconPicker] = useState(false)
|
||||
const [name, setName] = useState('')
|
||||
@ -53,7 +51,8 @@ const CreateAppModal = ({ show, onSuccess, onClose }: CreateAppDialogProps) => {
|
||||
const { isCurrentWorkspaceEditor } = useAppContext()
|
||||
|
||||
const isCreatingRef = useRef(false)
|
||||
const onCreate: MouseEventHandler = useCallback(async () => {
|
||||
|
||||
const onCreate = useCallback(async () => {
|
||||
if (!appMode) {
|
||||
notify({ type: 'error', message: t('app.newApp.appTypeRequired') })
|
||||
return
|
||||
@ -87,237 +86,281 @@ const CreateAppModal = ({ show, onSuccess, onClose }: CreateAppDialogProps) => {
|
||||
isCreatingRef.current = false
|
||||
}, [name, notify, t, appMode, appIcon, description, onSuccess, onClose, mutateApps, push, isCurrentWorkspaceEditor])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
overflowVisible
|
||||
className='!p-0 !max-w-[720px] !w-[720px] rounded-xl'
|
||||
isShow={show}
|
||||
onClose={() => { }}
|
||||
>
|
||||
{/* Heading */}
|
||||
<div className='shrink-0 flex flex-col h-full bg-white rounded-t-xl'>
|
||||
<div className='shrink-0 pl-8 pr-6 pt-6 pb-3 bg-white text-xl rounded-t-xl leading-[30px] font-semibold text-gray-900 z-10'>{t('app.newApp.startFromBlank')}</div>
|
||||
</div>
|
||||
{/* app type */}
|
||||
<div className='py-2 px-8'>
|
||||
<div className='py-2 text-sm leading-[20px] font-medium text-gray-900'>{t('app.newApp.captionAppType')}</div>
|
||||
<div className='flex'>
|
||||
<Tooltip
|
||||
popupContent={
|
||||
<div className='max-w-[280px] leading-[18px] text-xs text-gray-700'>{t('app.newApp.chatbotDescription')}</div>
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'relative grow box-border w-[158px] mr-2 px-0.5 pt-3 pb-2 flex flex-col items-center justify-center gap-1 rounded-lg border border-gray-100 bg-white text-gray-700 cursor-pointer shadow-xs hover:border-gray-300',
|
||||
showChatBotType && 'border-[1.5px] border-primary-400 hover:border-[1.5px] hover:border-primary-400',
|
||||
s['grid-bg-chat'],
|
||||
)}
|
||||
onClick={() => {
|
||||
setAppMode('chat')
|
||||
setShowChatBotType(true)
|
||||
}}
|
||||
>
|
||||
<ChatBot className='w-6 h-6 text-[#1570EF]' />
|
||||
<div className='h-5 text-[13px] font-medium leading-[18px]'>{t('app.types.chatbot')}</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
popupContent={
|
||||
<div className='flex flex-col max-w-[320px] leading-[18px] text-xs'>
|
||||
<div className='text-gray-700'>{t('app.newApp.completionDescription')}</div>
|
||||
const { run: handleCreateApp } = useDebounceFn(onCreate, { wait: 300 })
|
||||
useKeyPress(['meta.enter', 'ctrl.enter'], () => {
|
||||
if (isAppsFull)
|
||||
return
|
||||
handleCreateApp()
|
||||
})
|
||||
return <>
|
||||
<div className='flex justify-center h-full overflow-y-auto overflow-x-hidden'>
|
||||
<div className='flex-1 shrink-0 flex justify-end'>
|
||||
<div className='px-10'>
|
||||
<div className='w-full h-6 2xl:h-[139px]' />
|
||||
<div className='pt-1 pb-6'>
|
||||
<span className='title-2xl-semi-bold text-text-primary'>{t('app.newApp.startFromBlank')}</span>
|
||||
</div>
|
||||
<div className='leading-6 mb-2'>
|
||||
<span className='system-sm-semibold text-text-secondary'>{t('app.newApp.chooseAppType')}</span>
|
||||
</div>
|
||||
<div className='flex flex-col w-[660px] gap-4'>
|
||||
<div>
|
||||
<div className='mb-2'>
|
||||
<span className='system-2xs-medium-uppercase text-text-tertiary'>{t('app.newApp.forBeginners')}</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'relative grow box-border w-[158px] mr-2 px-0.5 pt-3 pb-2 flex flex-col items-center justify-center gap-1 rounded-lg border border-gray-100 text-gray-700 cursor-pointer bg-white shadow-xs hover:border-gray-300',
|
||||
s['grid-bg-completion'],
|
||||
appMode === 'completion' && 'border-[1.5px] border-primary-400 hover:border-[1.5px] hover:border-primary-400',
|
||||
)}
|
||||
onClick={() => {
|
||||
setAppMode('completion')
|
||||
setShowChatBotType(false)
|
||||
}}
|
||||
>
|
||||
<AiText className='w-6 h-6 text-[#0E9384]' />
|
||||
<div className='h-5 text-[13px] font-medium leading-[18px]'>{t('app.newApp.completeApp')}</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
popupContent={
|
||||
<div className='max-w-[280px] leading-[18px] text-xs text-gray-700'>{t('app.newApp.agentDescription')}</div>
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'relative grow box-border w-[158px] mr-2 px-0.5 pt-3 pb-2 flex flex-col items-center justify-center gap-1 rounded-lg border border-gray-100 text-gray-700 cursor-pointer bg-white shadow-xs hover:border-gray-300',
|
||||
s['grid-bg-agent-chat'],
|
||||
appMode === 'agent-chat' && 'border-[1.5px] border-primary-400 hover:border-[1.5px] hover:border-primary-400',
|
||||
)}
|
||||
onClick={() => {
|
||||
setAppMode('agent-chat')
|
||||
setShowChatBotType(false)
|
||||
}}
|
||||
>
|
||||
<CuteRobot className='w-6 h-6 text-indigo-600' />
|
||||
<div className='h-5 text-[13px] font-medium leading-[18px]'>{t('app.types.agent')}</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
popupContent={
|
||||
<div className='flex flex-col max-w-[320px] leading-[18px] text-xs'>
|
||||
<div className='text-gray-700'>{t('app.newApp.workflowDescription')}</div>
|
||||
<div className='flex flex-row gap-2'>
|
||||
<AppTypeCard
|
||||
active={appMode === 'chat'}
|
||||
title={t('app.types.chatbot')}
|
||||
description={t('app.newApp.chatbotShortDescription')}
|
||||
icon={<div className='w-6 h-6 bg-components-icon-bg-blue-solid rounded-md flex items-center justify-center'>
|
||||
<ChatBot className='w-4 h-4 text-components-avatar-shape-fill-stop-100' />
|
||||
</div>}
|
||||
onClick={() => {
|
||||
setAppMode('chat')
|
||||
}} />
|
||||
<AppTypeCard
|
||||
active={appMode === 'agent-chat'}
|
||||
title={t('app.types.agent')}
|
||||
description={t('app.newApp.agentShortDescription')}
|
||||
icon={<div className='w-6 h-6 bg-components-icon-bg-violet-solid rounded-md flex items-center justify-center'>
|
||||
<Logic className='w-4 h-4 text-components-avatar-shape-fill-stop-100' />
|
||||
</div>}
|
||||
onClick={() => {
|
||||
setAppMode('agent-chat')
|
||||
}} />
|
||||
<AppTypeCard
|
||||
active={appMode === 'completion'}
|
||||
title={t('app.newApp.completeApp')}
|
||||
description={t('app.newApp.completionShortDescription')}
|
||||
icon={<div className='w-6 h-6 bg-components-icon-bg-teal-solid rounded-md flex items-center justify-center'>
|
||||
<ListSparkle className='w-4 h-4 text-components-avatar-shape-fill-stop-100' />
|
||||
</div>}
|
||||
onClick={() => {
|
||||
setAppMode('completion')
|
||||
}} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'relative grow box-border w-[158px] px-0.5 pt-3 pb-2 flex flex-col items-center justify-center gap-1 rounded-lg border border-gray-100 text-gray-700 cursor-pointer bg-white shadow-xs hover:border-gray-300',
|
||||
s['grid-bg-workflow'],
|
||||
appMode === 'workflow' && 'border-[1.5px] border-primary-400 hover:border-[1.5px] hover:border-primary-400',
|
||||
)}
|
||||
onClick={() => {
|
||||
setAppMode('workflow')
|
||||
setShowChatBotType(false)
|
||||
}}
|
||||
>
|
||||
<Route className='w-6 h-6 text-[#f79009]' />
|
||||
<div className='h-5 text-[13px] font-medium leading-[18px]'>{t('app.types.workflow')}</div>
|
||||
<span className='absolute top-[-3px] right-[-3px] px-1 rounded-[5px] bg-white border border-black/8 text-gray-500 text-[10px] leading-[18px] font-medium'>BETA</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{showChatBotType && (
|
||||
<div className='py-2 px-8'>
|
||||
<div className='py-2 text-sm leading-[20px] font-medium text-gray-900'>{t('app.newApp.chatbotType')}</div>
|
||||
<div className='flex gap-2'>
|
||||
<div
|
||||
className={cn(
|
||||
'relative grow flex-[50%] pl-4 py-[10px] pr-[10px] rounded-lg border border-gray-100 bg-gray-25 text-gray-700 cursor-pointer hover:bg-white hover:shadow-xs hover:border-gray-300',
|
||||
appMode === 'chat' && 'bg-white shadow-xs border-[1.5px] border-primary-400 hover:border-[1.5px] hover:border-primary-400',
|
||||
)}
|
||||
onClick={() => {
|
||||
setAppMode('chat')
|
||||
}}
|
||||
>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='h-5 text-sm font-medium leading-5'>{t('app.newApp.basic')}</div>
|
||||
<div className='group'>
|
||||
<RiQuestionLine className='w-[14px] h-[14px] text-gray-400 hover:text-gray-500' />
|
||||
<div
|
||||
className={cn(
|
||||
'hidden z-20 absolute left-[327px] top-[-158px] w-[376px] rounded-xl bg-white border-[0.5px] border-[rgba(0,0,0,0.05)] shadow-lg group-hover:block',
|
||||
)}
|
||||
>
|
||||
<div className={cn('w-full h-[256px] bg-center bg-no-repeat bg-contain rounded-xl', s.basicPic)} />
|
||||
<div className='px-4 pb-2'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='text-gray-700 text-md leading-6 font-semibold'>{t('app.newApp.basic')}</div>
|
||||
<div className='text-orange-500 text-xs leading-[18px] font-medium'>{t('app.newApp.basicFor')}</div>
|
||||
</div>
|
||||
<div className='mt-1 text-gray-500 text-sm leading-5'>{t('app.newApp.basicDescription')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className='mb-2'>
|
||||
<span className='system-2xs-medium-uppercase text-text-tertiary'>{t('app.newApp.forAdvanced')}</span>
|
||||
</div>
|
||||
<div className='flex flex-row gap-2'>
|
||||
<AppTypeCard
|
||||
beta
|
||||
active={appMode === 'advanced-chat'}
|
||||
title={t('app.types.advanced')}
|
||||
description={t('app.newApp.advancedShortDescription')}
|
||||
icon={<div className='w-6 h-6 bg-components-icon-bg-blue-light-solid rounded-md flex items-center justify-center'>
|
||||
<BubbleTextMod className='w-4 h-4 text-components-avatar-shape-fill-stop-100' />
|
||||
</div>}
|
||||
onClick={() => {
|
||||
setAppMode('advanced-chat')
|
||||
}} />
|
||||
<AppTypeCard
|
||||
beta
|
||||
active={appMode === 'workflow'}
|
||||
title={t('app.types.workflow')}
|
||||
description={t('app.newApp.workflowShortDescription')}
|
||||
icon={<div className='w-6 h-6 bg-components-icon-bg-indigo-solid rounded-md flex items-center justify-center'>
|
||||
<RiExchange2Fill className='w-4 h-4 text-components-avatar-shape-fill-stop-100' />
|
||||
</div>}
|
||||
onClick={() => {
|
||||
setAppMode('workflow')
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
<Divider style={{ margin: 0 }} />
|
||||
<div className='flex space-x-3 items-center'>
|
||||
<div className='flex-1'>
|
||||
<div className='h-6 flex items-center mb-1'>
|
||||
<label className='system-sm-semibold text-text-secondary'>{t('app.newApp.captionName')}</label>
|
||||
</div>
|
||||
<Input
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
placeholder={t('app.newApp.appNamePlaceholder') || ''}
|
||||
/>
|
||||
</div>
|
||||
<div className='mt-[2px] text-gray-500 text-xs leading-[18px]'>{t('app.newApp.basicTip')}</div>
|
||||
<AppIcon
|
||||
iconType={appIcon.type}
|
||||
icon={appIcon.type === 'emoji' ? appIcon.icon : appIcon.fileId}
|
||||
background={appIcon.type === 'emoji' ? appIcon.background : undefined}
|
||||
imageUrl={appIcon.type === 'image' ? appIcon.url : undefined}
|
||||
size='xxl' className='cursor-pointer rounded-2xl'
|
||||
onClick={() => { setShowAppIconPicker(true) }}
|
||||
/>
|
||||
{showAppIconPicker && <AppIconPicker
|
||||
onSelect={(payload) => {
|
||||
setAppIcon(payload)
|
||||
setShowAppIconPicker(false)
|
||||
}}
|
||||
onClose={() => {
|
||||
setShowAppIconPicker(false)
|
||||
}}
|
||||
/>}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'relative grow flex-[50%] pl-3 py-2 pr-2 rounded-lg border border-gray-100 bg-gray-25 text-gray-700 cursor-pointer hover:bg-white hover:shadow-xs hover:border-gray-300',
|
||||
appMode === 'advanced-chat' && 'bg-white shadow-xs border-[1.5px] border-primary-400 hover:border-[1.5px] hover:border-primary-400',
|
||||
)}
|
||||
onClick={() => {
|
||||
setAppMode('advanced-chat')
|
||||
}}
|
||||
>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center'>
|
||||
<div className='mr-1 h-5 text-sm font-medium leading-5'>{t('app.newApp.advanced')}</div>
|
||||
<span className='px-1 rounded-[5px] bg-white border border-black/8 text-gray-500 text-[10px] leading-[18px] font-medium'>BETA</span>
|
||||
</div>
|
||||
<div className='group'>
|
||||
<RiQuestionLine className='w-[14px] h-[14px] text-gray-400 hover:text-gray-500' />
|
||||
<div
|
||||
className={cn(
|
||||
'hidden z-20 absolute right-[26px] top-[-158px] w-[376px] rounded-xl bg-white border-[0.5px] border-[rgba(0,0,0,0.05)] shadow-lg group-hover:block',
|
||||
)}
|
||||
>
|
||||
<div className={cn('w-full h-[256px] bg-center bg-no-repeat bg-contain rounded-xl', s.advancedPic)} />
|
||||
<div className='px-4 pb-2'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center'>
|
||||
<div className='mr-1 text-gray-700 text-md leading-6 font-semibold'>{t('app.newApp.advanced')}</div>
|
||||
<span className='px-1 rounded-[5px] bg-white border border-black/8 text-gray-500 text-[10px] leading-[18px] font-medium'>BETA</span>
|
||||
</div>
|
||||
<div className='text-orange-500 text-xs leading-[18px] font-medium'>{t('app.newApp.advancedFor').toLocaleUpperCase()}</div>
|
||||
</div>
|
||||
<div className='mt-1 text-gray-500 text-sm leading-5'>{t('app.newApp.advancedDescription')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className='h-6 flex items-center mb-1'>
|
||||
<label className='system-sm-semibold text-text-secondary'>{t('app.newApp.captionDescription')}</label>
|
||||
<span className='system-xs-regular text-text-tertiary ml-1'>({t('app.newApp.optional')})</span>
|
||||
</div>
|
||||
<div className='mt-[2px] text-gray-500 text-xs leading-[18px]'>{t('app.newApp.advancedFor')}</div>
|
||||
<Textarea
|
||||
className='resize-none'
|
||||
placeholder={t('app.newApp.appDescriptionPlaceholder') || ''}
|
||||
value={description}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='pt-5 pb-10 flex justify-between items-center'>
|
||||
<div className='flex gap-1 items-center system-xs-regular text-text-tertiary cursor-pointer' onClick={onCreateFromTemplate}>
|
||||
<span>{t('app.newApp.noIdeaTip')}</span>
|
||||
<div className='p-[1px]'>
|
||||
<RiArrowRightLine className='w-3.5 h-3.5' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex gap-2'>
|
||||
<Button onClick={onClose}>{t('app.newApp.Cancel')}</Button>
|
||||
<Button disabled={isAppsFull || !name} className='gap-1' variant="primary" onClick={handleCreateApp}>
|
||||
<span>{t('app.newApp.Create')}</span>
|
||||
<div className='flex gap-0.5'>
|
||||
<RiCommandLine size={14} className='p-0.5 system-kbd bg-components-kbd-bg-white rounded-sm' />
|
||||
<RiCornerDownLeftLine size={14} className='p-0.5 system-kbd bg-components-kbd-bg-white rounded-sm' />
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* icon & name */}
|
||||
<div className='pt-2 px-8'>
|
||||
<div className='py-2 text-sm font-medium leading-[20px] text-gray-900'>{t('app.newApp.captionName')}</div>
|
||||
<div className='flex items-center justify-between space-x-2'>
|
||||
<AppIcon
|
||||
iconType={appIcon.type}
|
||||
icon={appIcon.type === 'emoji' ? appIcon.icon : appIcon.fileId}
|
||||
background={appIcon.type === 'emoji' ? appIcon.background : undefined}
|
||||
imageUrl={appIcon.type === 'image' ? appIcon.url : undefined}
|
||||
size='large' className='cursor-pointer'
|
||||
onClick={() => { setShowAppIconPicker(true) }}
|
||||
/>
|
||||
<Input
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
placeholder={t('app.newApp.appNamePlaceholder') || ''}
|
||||
className='grow h-10'
|
||||
/>
|
||||
</div>
|
||||
<div className='flex-1 shrink h-full flex justify-start relative overflow-hidden'>
|
||||
<div className='h-6 2xl:h-[139px] absolute left-0 top-0 right-0 border-b border-b-divider-subtle'></div>
|
||||
<div className='max-w-[760px] border-x border-x-divider-subtle'>
|
||||
<div className='h-6 2xl:h-[139px]' />
|
||||
<AppPreview mode={appMode} />
|
||||
<div className='absolute left-0 right-0 border-b border-b-divider-subtle'></div>
|
||||
<div className='w-[664px] h-[448px] flex items-center justify-center' style={{ background: 'repeating-linear-gradient(135deg, transparent, transparent 2px, rgba(16,24,40,0.04) 4px,transparent 3px, transparent 6px)' }}>
|
||||
<AppScreenShot show={appMode === 'chat'} mode='chat' />
|
||||
<AppScreenShot show={appMode === 'advanced-chat'} mode='advanced-chat' />
|
||||
<AppScreenShot show={appMode === 'agent-chat'} mode='agent-chat' />
|
||||
<AppScreenShot show={appMode === 'completion'} mode='completion' />
|
||||
<AppScreenShot show={appMode === 'workflow'} mode='workflow' />
|
||||
</div>
|
||||
<div className='absolute left-0 right-0 border-b border-b-divider-subtle'></div>
|
||||
</div>
|
||||
{showAppIconPicker && <AppIconPicker
|
||||
onSelect={(payload) => {
|
||||
setAppIcon(payload)
|
||||
setShowAppIconPicker(false)
|
||||
}}
|
||||
onClose={() => {
|
||||
setShowAppIconPicker(false)
|
||||
}}
|
||||
/>}
|
||||
</div>
|
||||
{/* description */}
|
||||
<div className='pt-2 px-8'>
|
||||
<div className='py-2 text-sm font-medium leading-[20px] text-gray-900'>{t('app.newApp.captionDescription')}</div>
|
||||
<Textarea
|
||||
className='resize-none'
|
||||
placeholder={t('app.newApp.appDescriptionPlaceholder') || ''}
|
||||
value={description}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{isAppsFull && (
|
||||
</div>
|
||||
{
|
||||
isAppsFull && (
|
||||
<div className='px-8 py-2'>
|
||||
<AppsFull loc='app-create' />
|
||||
</div>
|
||||
)}
|
||||
<div className='px-8 py-6 flex justify-end'>
|
||||
<Button className='mr-2' onClick={onClose}>{t('app.newApp.Cancel')}</Button>
|
||||
<Button disabled={isAppsFull || !name} variant="primary" onClick={onCreate}>{t('app.newApp.Create')}</Button>
|
||||
</div>
|
||||
<div className='absolute right-6 top-6 p-2 cursor-pointer z-20' onClick={onClose}>
|
||||
<RiCloseLine className='w-4 h-4 text-gray-500' />
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
</>
|
||||
}
|
||||
type CreateAppDialogProps = CreateAppProps & {
|
||||
show: boolean
|
||||
}
|
||||
const CreateAppModal = ({ show, onClose, onSuccess, onCreateFromTemplate }: CreateAppDialogProps) => {
|
||||
return (
|
||||
<FullScreenModal
|
||||
overflowVisible
|
||||
closable
|
||||
open={show}
|
||||
onClose={onClose}
|
||||
>
|
||||
<CreateApp onClose={onClose} onSuccess={onSuccess} onCreateFromTemplate={onCreateFromTemplate} />
|
||||
</FullScreenModal>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreateAppModal
|
||||
|
||||
type AppTypeCardProps = {
|
||||
icon: JSX.Element
|
||||
beta?: boolean
|
||||
title: string
|
||||
description: string
|
||||
active: boolean
|
||||
onClick: () => void
|
||||
}
|
||||
function AppTypeCard({ icon, title, beta = false, description, active, onClick }: AppTypeCardProps) {
|
||||
const { t } = useTranslation()
|
||||
return <div
|
||||
className={
|
||||
cn(`w-[191px] h-[84px] p-3 border-[0.5px] relative box-content
|
||||
rounded-xl border-components-option-card-option-border
|
||||
bg-components-panel-on-panel-item-bg shadow-xs cursor-pointer hover:shadow-md`, active
|
||||
? 'outline outline-[1.5px] outline-components-option-card-option-selected-border shadow-md'
|
||||
: '')
|
||||
}
|
||||
onClick={onClick}
|
||||
>
|
||||
{beta && <div className='px-[5px] py-[3px]
|
||||
rounded-[5px] min-w-[18px] absolute top-3 right-3
|
||||
border border-divider-deep system-2xs-medium-uppercase text-text-tertiary'>{t('common.menus.status')}</div>}
|
||||
{icon}
|
||||
<div className='system-sm-semibold text-text-secondary mt-2 mb-0.5'>{title}</div>
|
||||
<div className='system-xs-regular text-text-tertiary'>{description}</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
function AppPreview({ mode }: { mode: AppMode }) {
|
||||
const { t } = useTranslation()
|
||||
const modeToPreviewInfoMap = {
|
||||
'chat': {
|
||||
title: t('app.types.chatbot'),
|
||||
description: t('app.newApp.chatbotUserDescription'),
|
||||
link: 'https://docs.dify.ai/guides/application-orchestrate/conversation-application?fallback=true',
|
||||
},
|
||||
'advanced-chat': {
|
||||
title: t('app.types.advanced'),
|
||||
description: t('app.newApp.advancedUserDescription'),
|
||||
link: 'https://docs.dify.ai/guides/workflow',
|
||||
},
|
||||
'agent-chat': {
|
||||
title: t('app.types.agent'),
|
||||
description: t('app.newApp.agentUserDescription'),
|
||||
link: 'https://docs.dify.ai/guides/application-orchestrate/agent',
|
||||
},
|
||||
'completion': {
|
||||
title: t('app.newApp.completeApp'),
|
||||
description: t('app.newApp.completionUserDescription'),
|
||||
link: null,
|
||||
},
|
||||
'workflow': {
|
||||
title: t('app.types.workflow'),
|
||||
description: t('app.newApp.workflowUserDescription'),
|
||||
link: 'https://docs.dify.ai/guides/workflow',
|
||||
},
|
||||
}
|
||||
const previewInfo = modeToPreviewInfoMap[mode]
|
||||
return <div className='px-8 py-4'>
|
||||
<h4 className='system-sm-semibold-uppercase text-text-secondary'>{previewInfo.title}</h4>
|
||||
<div className='mt-1 system-xs-regular text-text-tertiary max-w-96 min-h-8'>
|
||||
<span>{previewInfo.description}</span>
|
||||
{previewInfo.link && <Link target='_blank' href={previewInfo.link} className='text-text-accent ml-1'>{t('app.newApp.learnMore')}</Link>}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
function AppScreenShot({ mode, show }: { mode: AppMode; show: boolean }) {
|
||||
const theme = useContextSelector(AppsContext, state => state.theme)
|
||||
const modeToImageMap = {
|
||||
'chat': 'Chatbot',
|
||||
'advanced-chat': 'Chatflow',
|
||||
'agent-chat': 'Agent',
|
||||
'completion': 'TextGenerator',
|
||||
'workflow': 'Workflow',
|
||||
}
|
||||
return <picture>
|
||||
<source media="(resolution: 1x)" srcSet={`/screenshots/${theme}/${modeToImageMap[mode]}.png`} />
|
||||
<source media="(resolution: 2x)" srcSet={`/screenshots/${theme}/${modeToImageMap[mode]}@2x.png`} />
|
||||
<source media="(resolution: 3x)" srcSet={`/screenshots/${theme}/${modeToImageMap[mode]}@3x.png`} />
|
||||
<Image className={show ? '' : 'hidden'}
|
||||
src={`/screenshots/${theme}/${modeToImageMap[mode]}.png`}
|
||||
alt='App Screen Shot'
|
||||
width={664} height={448} />
|
||||
</picture>
|
||||
}
|
||||
|
||||
@ -1,23 +0,0 @@
|
||||
.grid-bg-chat {
|
||||
background-image: url('./grid-bg-chat.svg');
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
.grid-bg-completion {
|
||||
background-image: url('./grid-bg-completion.svg');
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
.grid-bg-agent-chat {
|
||||
background-image: url('./grid-bg-agent-chat.svg');
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
.grid-bg-workflow {
|
||||
background-image: url('./grid-bg-workflow.svg');
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
.basicPic {
|
||||
background-image: url('./basic.png')
|
||||
}
|
||||
|
||||
.advancedPic {
|
||||
background-image: url('./advanced.png')
|
||||
}
|
||||
@ -34,7 +34,7 @@ const LogAnnotation: FC<Props> = ({
|
||||
|
||||
if (!appDetail) {
|
||||
return (
|
||||
<div className='flex h-full items-center justify-center bg-white'>
|
||||
<div className='flex h-full items-center justify-center bg-background-body'>
|
||||
<Loading />
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -5,9 +5,8 @@ import useSWR from 'swr'
|
||||
import {
|
||||
HandThumbDownIcon,
|
||||
HandThumbUpIcon,
|
||||
XMarkIcon,
|
||||
} from '@heroicons/react/24/outline'
|
||||
import { RiEditFill, RiQuestionLine } from '@remixicon/react'
|
||||
import { RiCloseLine, RiEditFill } from '@remixicon/react'
|
||||
import { get } from 'lodash-es'
|
||||
import InfiniteScroll from 'react-infinite-scroll-component'
|
||||
import dayjs from 'dayjs'
|
||||
@ -18,20 +17,16 @@ import { useShallow } from 'zustand/react/shallow'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { ChatItemInTree } from '../../base/chat/types'
|
||||
import VarPanel from './var-panel'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { FeedbackFunc, FeedbackType, IChatItem, SubmitAnnotationFunc } from '@/app/components/base/chat/chat/type'
|
||||
import type { Annotation, ChatConversationGeneralDetail, ChatConversationsResponse, ChatMessage, ChatMessagesRequest, CompletionConversationGeneralDetail, CompletionConversationsResponse, LogAnnotation } from '@/models/log'
|
||||
import type { App } from '@/types/app'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Drawer from '@/app/components/base/drawer'
|
||||
import Popover from '@/app/components/base/popover'
|
||||
import Chat from '@/app/components/base/chat/chat'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { fetchChatConversationDetail, fetchChatMessages, fetchCompletionConversationDetail, updateLogMessageAnnotations, updateLogMessageFeedbacks } from '@/service/log'
|
||||
import { TONE_LIST } from '@/config'
|
||||
import ModelIcon from '@/app/components/header/account-setting/model-provider-page/model-icon'
|
||||
import { useTextGenerationCurrentProviderAndModelAndModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import ModelName from '@/app/components/header/account-setting/model-provider-page/model-name'
|
||||
import ModelInfo from '@/app/components/app/log/model-info'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import TextGeneration from '@/app/components/app/text-generate/item'
|
||||
import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils'
|
||||
@ -44,6 +39,7 @@ import Tooltip from '@/app/components/base/tooltip'
|
||||
import { CopyIcon } from '@/app/components/base/copy-icon'
|
||||
import { buildChatItemTree, getThreadMessages } from '@/app/components/base/chat/utils'
|
||||
import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(timezone)
|
||||
@ -75,15 +71,6 @@ const HandThumbIconWithCount: FC<{ count: number; iconType: 'up' | 'down' }> = (
|
||||
</div>
|
||||
}
|
||||
|
||||
const PARAM_MAP = {
|
||||
temperature: 'Temperature',
|
||||
top_p: 'Top P',
|
||||
presence_penalty: 'Presence Penalty',
|
||||
max_tokens: 'Max Token',
|
||||
stop: 'Stop',
|
||||
frequency_penalty: 'Frequency Penalty',
|
||||
}
|
||||
|
||||
const getFormattedChatList = (messages: ChatMessage[], conversationId: string, timezone: string, format: string) => {
|
||||
const newChatList: IChatItem[] = []
|
||||
messages.forEach((item: ChatMessage) => {
|
||||
@ -156,9 +143,6 @@ const getFormattedChatList = (messages: ChatMessage[], conversationId: string, t
|
||||
return newChatList
|
||||
}
|
||||
|
||||
// const displayedParams = CompletionParams.slice(0, -2)
|
||||
const validatedParams = ['temperature', 'top_p', 'presence_penalty', 'frequency_penalty']
|
||||
|
||||
type IDetailPanel = {
|
||||
detail: any
|
||||
onFeedback: FeedbackFunc
|
||||
@ -315,22 +299,6 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||
const isChatMode = appDetail?.mode !== 'completion'
|
||||
const isAdvanced = appDetail?.mode === 'advanced-chat'
|
||||
|
||||
const targetTone = TONE_LIST.find((item: any) => {
|
||||
let res = true
|
||||
validatedParams.forEach((param) => {
|
||||
res = item.config?.[param] === detail?.model_config.model?.completion_params?.[param]
|
||||
})
|
||||
return res
|
||||
})?.name ?? 'custom'
|
||||
|
||||
const modelName = (detail.model_config as any).model?.name
|
||||
const provideName = (detail.model_config as any).model?.provider as any
|
||||
const {
|
||||
currentModel,
|
||||
currentProvider,
|
||||
} = useTextGenerationCurrentProviderAndModelAndModelList(
|
||||
{ provider: provideName, model: modelName },
|
||||
)
|
||||
const varList = (detail.model_config as any).user_input_form?.map((item: any) => {
|
||||
const itemContent = item[Object.keys(item)[0]]
|
||||
return {
|
||||
@ -342,18 +310,6 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||
? detail.message.message_files.map((item: any) => item.url)
|
||||
: []
|
||||
|
||||
const getParamValue = (param: string) => {
|
||||
const value = detail?.model_config.model?.completion_params?.[param] || '-'
|
||||
if (param === 'stop') {
|
||||
if (Array.isArray(value))
|
||||
return value.join(',')
|
||||
else
|
||||
return '-'
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
const [width, setWidth] = useState(0)
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
|
||||
@ -367,162 +323,71 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div ref={ref} className='rounded-xl border-[0.5px] border-gray-200 h-full flex flex-col overflow-auto'>
|
||||
<div ref={ref} className='rounded-xl border-[0.5px] border-components-panel-border h-full flex flex-col'>
|
||||
{/* Panel Header */}
|
||||
<div className='border-b border-gray-100 py-4 px-6 flex items-center justify-between bg-components-panel-bg'>
|
||||
<div>
|
||||
<div className='text-gray-500 text-[10px] leading-[14px]'>{isChatMode ? t('appLog.detail.conversationId') : t('appLog.detail.time')}</div>
|
||||
<div className='shrink-0 pl-4 pt-3 pr-3 pb-2 flex items-center gap-2 bg-components-panel-bg rounded-t-xl'>
|
||||
<div className='shrink-0'>
|
||||
<div className='mb-0.5 text-text-primary system-xs-semibold-uppercase'>{isChatMode ? t('appLog.detail.conversationId') : t('appLog.detail.time')}</div>
|
||||
{isChatMode && (
|
||||
<div className='flex items-center text-gray-700 text-[13px] leading-[18px]'>
|
||||
<div className='flex items-center text-text-secondary system-2xs-regular-uppercase'>
|
||||
<Tooltip
|
||||
popupContent={detail.id}
|
||||
>
|
||||
<div className='max-w-[105px] truncate'>{detail.id}</div>
|
||||
<div className='truncate'>{detail.id}</div>
|
||||
</Tooltip>
|
||||
<CopyIcon content={detail.id} />
|
||||
</div>
|
||||
)}
|
||||
{!isChatMode && (
|
||||
<div className='text-gray-700 text-[13px] leading-[18px]'>{formatTime(detail.created_at, t('appLog.dateTimeFormat') as string)}</div>
|
||||
<div className='text-text-secondary system-2xs-regular-uppercase'>{formatTime(detail.created_at, t('appLog.dateTimeFormat') as string)}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='flex items-center flex-wrap gap-y-1 justify-end'>
|
||||
{!isAdvanced && (
|
||||
<>
|
||||
<div
|
||||
className={cn('mr-2 flex items-center border h-8 px-2 space-x-2 rounded-lg bg-indigo-25 border-[#2A87F5]')}
|
||||
>
|
||||
<ModelIcon
|
||||
className='!w-5 !h-5'
|
||||
provider={currentProvider}
|
||||
modelName={currentModel?.model}
|
||||
/>
|
||||
<ModelName
|
||||
modelItem={currentModel!}
|
||||
showMode
|
||||
/>
|
||||
</div>
|
||||
<Popover
|
||||
position='br'
|
||||
className='!w-[280px]'
|
||||
btnClassName='mr-4 !bg-gray-50 !py-1.5 !px-2.5 border-none font-normal'
|
||||
btnElement={<>
|
||||
<span className='text-[13px]'>{targetTone}</span>
|
||||
<RiQuestionLine className='h-4 w-4 text-gray-800 ml-1.5' />
|
||||
</>}
|
||||
htmlContent={<div className='w-[280px]'>
|
||||
<div className='flex justify-between py-2 px-4 font-medium text-sm text-gray-700'>
|
||||
<span>Tone of responses</span>
|
||||
<div>{targetTone}</div>
|
||||
</div>
|
||||
{['temperature', 'top_p', 'presence_penalty', 'max_tokens', 'stop'].map((param: string, index: number) => {
|
||||
return <div className='flex justify-between py-2 px-4 bg-gray-50' key={index}>
|
||||
<span className='text-xs text-gray-700'>{PARAM_MAP[param as keyof typeof PARAM_MAP]}</span>
|
||||
<span className='text-gray-800 font-medium text-xs'>{getParamValue(param)}</span>
|
||||
</div>
|
||||
})}
|
||||
</div>}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div className='w-6 h-6 rounded-lg flex items-center justify-center hover:cursor-pointer hover:bg-gray-100'>
|
||||
<XMarkIcon className='w-4 h-4 text-gray-500' onClick={onClose} />
|
||||
</div>
|
||||
<div className='grow flex items-center flex-wrap gap-y-1 justify-end'>
|
||||
{!isAdvanced && <ModelInfo model={detail.model_config.model} />}
|
||||
</div>
|
||||
|
||||
<ActionButton size='l' onClick={onClose}>
|
||||
<RiCloseLine className='w-4 h-4 text-text-tertiary' />
|
||||
</ActionButton>
|
||||
</div>
|
||||
{/* Panel Body */}
|
||||
{(varList.length > 0 || (!isChatMode && message_files.length > 0)) && (
|
||||
<div className='px-6 pt-4 pb-2'>
|
||||
<VarPanel
|
||||
varList={varList}
|
||||
message_files={message_files}
|
||||
/>
|
||||
<div className='shrink-0 pt-1 px-1'>
|
||||
<div className='p-3 pb-2 rounded-t-xl bg-background-section-burn'>
|
||||
{(varList.length > 0 || (!isChatMode && message_files.length > 0)) && (
|
||||
<VarPanel
|
||||
varList={varList}
|
||||
message_files={message_files}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isChatMode
|
||||
? <div className="px-6 py-4">
|
||||
<div className='flex h-[18px] items-center space-x-3'>
|
||||
<div className='leading-[18px] text-xs font-semibold text-gray-500 uppercase'>{t('appLog.table.header.output')}</div>
|
||||
<div className='grow h-[1px]' style={{
|
||||
background: 'linear-gradient(270deg, rgba(243, 244, 246, 0) 0%, rgb(243, 244, 246) 100%)',
|
||||
}}></div>
|
||||
</div>
|
||||
<TextGeneration
|
||||
className='mt-2'
|
||||
content={detail.message.answer}
|
||||
messageId={detail.message.id}
|
||||
isError={false}
|
||||
onRetry={() => { }}
|
||||
isInstalledApp={false}
|
||||
supportFeedback
|
||||
feedback={detail.message.feedbacks.find((item: any) => item.from_source === 'admin')}
|
||||
onFeedback={feedback => onFeedback(detail.message.id, feedback)}
|
||||
supportAnnotation
|
||||
isShowTextToSpeech
|
||||
appId={appDetail?.id}
|
||||
varList={varList}
|
||||
siteInfo={null}
|
||||
/>
|
||||
</div>
|
||||
: threadChatItems.length < 8
|
||||
? <div className="pt-4 mb-4">
|
||||
<Chat
|
||||
config={{
|
||||
appId: appDetail?.id,
|
||||
text_to_speech: {
|
||||
enabled: true,
|
||||
},
|
||||
supportAnnotation: true,
|
||||
annotation_reply: {
|
||||
enabled: true,
|
||||
},
|
||||
supportFeedback: true,
|
||||
} as any}
|
||||
chatList={threadChatItems}
|
||||
onAnnotationAdded={handleAnnotationAdded}
|
||||
onAnnotationEdited={handleAnnotationEdited}
|
||||
onAnnotationRemoved={handleAnnotationRemoved}
|
||||
onFeedback={onFeedback}
|
||||
noChatInput
|
||||
showPromptLog
|
||||
hideProcessDetail
|
||||
chatContainerInnerClassName='px-6'
|
||||
switchSibling={switchSibling}
|
||||
</div>
|
||||
<div className='grow mx-1 mb-1 bg-background-section-burn rounded-b-xl overflow-auto'>
|
||||
{!isChatMode
|
||||
? <div className="px-6 py-4">
|
||||
<div className='flex h-[18px] items-center space-x-3'>
|
||||
<div className='text-text-tertiary system-xs-semibold-uppercase'>{t('appLog.table.header.output')}</div>
|
||||
<div className='grow h-[1px]' style={{
|
||||
background: 'linear-gradient(270deg, rgba(243, 244, 246, 0) 0%, rgb(243, 244, 246) 100%)',
|
||||
}}></div>
|
||||
</div>
|
||||
<TextGeneration
|
||||
className='mt-2'
|
||||
content={detail.message.answer}
|
||||
messageId={detail.message.id}
|
||||
isError={false}
|
||||
onRetry={() => { }}
|
||||
isInstalledApp={false}
|
||||
supportFeedback
|
||||
feedback={detail.message.feedbacks.find((item: any) => item.from_source === 'admin')}
|
||||
onFeedback={feedback => onFeedback(detail.message.id, feedback)}
|
||||
supportAnnotation
|
||||
isShowTextToSpeech
|
||||
appId={appDetail?.id}
|
||||
varList={varList}
|
||||
siteInfo={null}
|
||||
/>
|
||||
</div>
|
||||
: <div
|
||||
className="py-4"
|
||||
id="scrollableDiv"
|
||||
style={{
|
||||
height: 1000, // Specify a value
|
||||
overflow: 'auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'column-reverse',
|
||||
}}>
|
||||
{/* Put the scroll bar always on the bottom */}
|
||||
<InfiniteScroll
|
||||
scrollableTarget="scrollableDiv"
|
||||
dataLength={threadChatItems.length}
|
||||
next={fetchData}
|
||||
hasMore={hasMore}
|
||||
loader={<div className='text-center text-gray-400 text-xs'>{t('appLog.detail.loading')}...</div>}
|
||||
// endMessage={<div className='text-center'>Nothing more to show</div>}
|
||||
// below props only if you need pull down functionality
|
||||
refreshFunction={fetchData}
|
||||
pullDownToRefresh
|
||||
pullDownToRefreshThreshold={50}
|
||||
// pullDownToRefreshContent={
|
||||
// <div className='text-center'>Pull down to refresh</div>
|
||||
// }
|
||||
// releaseToRefreshContent={
|
||||
// <div className='text-center'>Release to refresh</div>
|
||||
// }
|
||||
// To put endMessage and loader to the top.
|
||||
style={{ display: 'flex', flexDirection: 'column-reverse' }}
|
||||
inverse={true}
|
||||
>
|
||||
: threadChatItems.length < 8
|
||||
? <div className="pt-4 mb-4">
|
||||
<Chat
|
||||
config={{
|
||||
appId: appDetail?.id,
|
||||
@ -543,12 +408,68 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||
noChatInput
|
||||
showPromptLog
|
||||
hideProcessDetail
|
||||
chatContainerInnerClassName='px-6'
|
||||
chatContainerInnerClassName='px-3'
|
||||
switchSibling={switchSibling}
|
||||
/>
|
||||
</InfiniteScroll>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
: <div
|
||||
className="py-4"
|
||||
id="scrollableDiv"
|
||||
style={{
|
||||
height: 1000, // Specify a value
|
||||
overflow: 'auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'column-reverse',
|
||||
}}>
|
||||
{/* Put the scroll bar always on the bottom */}
|
||||
<InfiniteScroll
|
||||
scrollableTarget="scrollableDiv"
|
||||
dataLength={threadChatItems.length}
|
||||
next={fetchData}
|
||||
hasMore={hasMore}
|
||||
loader={<div className='text-center text-text-tertiary system-xs-regular'>{t('appLog.detail.loading')}...</div>}
|
||||
// endMessage={<div className='text-center'>Nothing more to show</div>}
|
||||
// below props only if you need pull down functionality
|
||||
refreshFunction={fetchData}
|
||||
pullDownToRefresh
|
||||
pullDownToRefreshThreshold={50}
|
||||
// pullDownToRefreshContent={
|
||||
// <div className='text-center'>Pull down to refresh</div>
|
||||
// }
|
||||
// releaseToRefreshContent={
|
||||
// <div className='text-center'>Release to refresh</div>
|
||||
// }
|
||||
// To put endMessage and loader to the top.
|
||||
style={{ display: 'flex', flexDirection: 'column-reverse' }}
|
||||
inverse={true}
|
||||
>
|
||||
<Chat
|
||||
config={{
|
||||
appId: appDetail?.id,
|
||||
text_to_speech: {
|
||||
enabled: true,
|
||||
},
|
||||
supportAnnotation: true,
|
||||
annotation_reply: {
|
||||
enabled: true,
|
||||
},
|
||||
supportFeedback: true,
|
||||
} as any}
|
||||
chatList={threadChatItems}
|
||||
onAnnotationAdded={handleAnnotationAdded}
|
||||
onAnnotationEdited={handleAnnotationEdited}
|
||||
onAnnotationRemoved={handleAnnotationRemoved}
|
||||
onFeedback={onFeedback}
|
||||
noChatInput
|
||||
showPromptLog
|
||||
hideProcessDetail
|
||||
chatContainerInnerClassName='px-3'
|
||||
switchSibling={switchSibling}
|
||||
/>
|
||||
</InfiniteScroll>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
{showMessageLogModal && (
|
||||
<MessageLogModal
|
||||
width={width}
|
||||
@ -780,7 +701,7 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh })
|
||||
onClose={onCloseDrawer}
|
||||
mask={isMobile}
|
||||
footer={null}
|
||||
panelClassname='mt-16 mx-2 sm:mr-2 mb-4 !p-0 !max-w-[640px] rounded-xl bg-background-gradient-bg-fill-chat-bg-1'
|
||||
panelClassname='mt-16 mx-2 sm:mr-2 mb-4 !p-0 !max-w-[640px] rounded-xl bg-components-panel-bg'
|
||||
>
|
||||
<DrawerContext.Provider value={{
|
||||
onClose: onCloseDrawer,
|
||||
|
||||
107
web/app/components/app/log/model-info.tsx
Normal file
@ -0,0 +1,107 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiInformation2Line,
|
||||
} from '@remixicon/react'
|
||||
import ModelIcon from '@/app/components/header/account-setting/model-provider-page/model-icon'
|
||||
import ModelName from '@/app/components/header/account-setting/model-provider-page/model-name'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import { useTextGenerationCurrentProviderAndModelAndModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
const PARAM_MAP = {
|
||||
temperature: 'Temperature',
|
||||
top_p: 'Top P',
|
||||
presence_penalty: 'Presence Penalty',
|
||||
max_tokens: 'Max Token',
|
||||
stop: 'Stop',
|
||||
frequency_penalty: 'Frequency Penalty',
|
||||
}
|
||||
|
||||
type Props = {
|
||||
model: any
|
||||
}
|
||||
|
||||
const ModelInfo: FC<Props> = ({
|
||||
model,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const modelName = model.name
|
||||
const provideName = model.provider as any
|
||||
const {
|
||||
currentModel,
|
||||
currentProvider,
|
||||
} = useTextGenerationCurrentProviderAndModelAndModelList(
|
||||
{ provider: provideName, model: modelName },
|
||||
)
|
||||
|
||||
const [open, setOpen] = React.useState(false)
|
||||
|
||||
const getParamValue = (param: string) => {
|
||||
const value = model.completion_params?.[param] || '-'
|
||||
if (param === 'stop') {
|
||||
if (Array.isArray(value))
|
||||
return value.join(',')
|
||||
else
|
||||
return '-'
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('flex items-center rounded-lg')}>
|
||||
<div className='shrink-0 flex items-center gap-1 mr-px h-8 pl-1.5 pr-2 rounded-l-lg bg-components-input-bg-normal'>
|
||||
<ModelIcon
|
||||
className='!w-5 !h-5'
|
||||
provider={currentProvider}
|
||||
modelName={currentModel?.model}
|
||||
/>
|
||||
<ModelName
|
||||
modelItem={currentModel!}
|
||||
showMode
|
||||
/>
|
||||
</div>
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-end'
|
||||
offset={4}
|
||||
>
|
||||
<div className='relative'>
|
||||
<PortalToFollowElemTrigger
|
||||
onClick={() => setOpen(v => !v)}
|
||||
className='block'
|
||||
>
|
||||
<div className={cn(
|
||||
'p-2 rounded-r-lg bg-components-button-tertiary-bg hover:bg-components-button-tertiary-bg-hover cursor-pointer',
|
||||
open && 'bg-components-button-tertiary-bg-hover',
|
||||
)}>
|
||||
<RiInformation2Line className='h-4 w-4 text-text-tertiary' />
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[1002]'>
|
||||
<div className='relative w-[280px] pt-3 px-4 pb-2 bg-components-panel-bg rounded-2xl border-[0.5px] border-components-panel-border shadow-xl overflow-hidden'>
|
||||
<div className='mb-1 h-6 text-text-secondary system-sm-semibold-uppercase'>{t('appLog.detail.modelParams')}</div>
|
||||
<div className='py-1'>
|
||||
{['temperature', 'top_p', 'presence_penalty', 'max_tokens', 'stop'].map((param: string, index: number) => {
|
||||
return <div className='flex justify-between py-1.5' key={index}>
|
||||
<span className='text-text-tertiary system-xs-medium-uppercase'>{PARAM_MAP[param as keyof typeof PARAM_MAP]}</span>
|
||||
<span className='text-text-secondary system-xs-medium-uppercase'>{getParamValue(param)}</span>
|
||||
</div>
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</div>
|
||||
</PortalToFollowElem>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(ModelInfo)
|
||||
@ -7,7 +7,9 @@ import {
|
||||
RiArrowDownSLine,
|
||||
RiArrowRightSLine,
|
||||
} from '@remixicon/react'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import ImagePreview from '@/app/components/base/image-uploader/image-preview'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
varList: { label: string; value: string }[]
|
||||
@ -23,34 +25,35 @@ const VarPanel: FC<Props> = ({
|
||||
const [imagePreviewUrl, setImagePreviewUrl] = useState('')
|
||||
|
||||
return (
|
||||
<div className='rounded-xl border border-color-indigo-100 bg-indigo-25'>
|
||||
<div className='rounded-[10px] border border-divider-subtle bg-chat-bubble-bg'>
|
||||
<div
|
||||
className='flex items-center h-6 pl-2 py-6 space-x-1 cursor-pointer'
|
||||
className={cn('flex items-center gap-1 px-3 pt-2.5 pb-2 border-b border-divider-subtle text-text-secondary cursor-pointer', isCollapse && 'pb-2.5 border-0')}
|
||||
onClick={toggleCollapse}
|
||||
>
|
||||
<Variable02 className='w-4 h-4' />
|
||||
<div className='grow system-md-medium'>{t('appLog.detail.variables')}</div>
|
||||
{
|
||||
isCollapse
|
||||
? <RiArrowRightSLine className='w-3 h-3 text-gray-300' />
|
||||
: <RiArrowDownSLine className='w-3 h-3 text-gray-300' />
|
||||
? <RiArrowRightSLine className='w-4 h-4' />
|
||||
: <RiArrowDownSLine className='w-4 h-4' />
|
||||
}
|
||||
<div className='text-sm font-semibold text-indigo-800 uppercase'>{t('appLog.detail.variables')}</div>
|
||||
</div>
|
||||
{!isCollapse && (
|
||||
<div className='px-6 pb-3'>
|
||||
<div className='p-3 flex flex-col gap-2'>
|
||||
{varList.map(({ label, value }, index) => (
|
||||
<div key={index} className='flex py-2 leading-[18px] text-[13px]'>
|
||||
<div className='shrink-0 w-[128px] flex text-primary-600'>
|
||||
<div key={index} className='flex py-2 system-xs-medium'>
|
||||
<div className='shrink-0 w-[128px] flex text-text-accent'>
|
||||
<span className='shrink-0 opacity-60'>{'{{'}</span>
|
||||
<span className='truncate'>{label}</span>
|
||||
<span className='shrink-0 opacity-60'>{'}}'}</span>
|
||||
</div>
|
||||
<div className='pl-2.5 whitespace-pre-wrap'>{value}</div>
|
||||
<div className='pl-2.5 whitespace-pre-wrap text-text-secondary'>{value}</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{message_files.length > 0 && (
|
||||
<div className='mt-1 flex py-2'>
|
||||
<div className='shrink-0 w-[128px] leading-[18px] text-[13px] font-medium text-gray-700'>{t('appLog.detail.uploadImages')}</div>
|
||||
<div className='shrink-0 w-[128px] system-xs-medium text-text-tertiary'>{t('appLog.detail.uploadImages')}</div>
|
||||
<div className="flex space-x-2">
|
||||
{message_files.map((url, index) => (
|
||||
<div
|
||||
@ -69,6 +72,7 @@ const VarPanel: FC<Props> = ({
|
||||
imagePreviewUrl && (
|
||||
<ImagePreview
|
||||
url={imagePreviewUrl}
|
||||
title={imagePreviewUrl}
|
||||
onCancel={() => setImagePreviewUrl('')}
|
||||
/>
|
||||
)
|
||||
|
||||
@ -282,7 +282,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
||||
const [currentTab, setCurrentTab] = useState<string>('DETAIL')
|
||||
|
||||
return (
|
||||
<div ref={ref} className={cn(isTop ? `rounded-xl border ${!isError ? 'border-gray-200 bg-white' : 'border-[#FECDCA] bg-[#FEF3F2]'} ` : 'rounded-br-xl !mt-0', className)}
|
||||
<div ref={ref} className={cn(isTop ? `rounded-xl border ${!isError ? 'border-gray-200 bg-chat-bubble-bg' : 'border-[#FECDCA] bg-[#FEF3F2]'} ` : 'rounded-br-xl !mt-0', className)}
|
||||
style={isTop
|
||||
? {
|
||||
boxShadow: '0px 1px 2px rgba(16, 24, 40, 0.05)',
|
||||
@ -334,7 +334,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
||||
</SimpleBtn>
|
||||
)
|
||||
}
|
||||
{(currentTab === 'RESULT' || !isWorkflow) && (
|
||||
{((currentTab === 'RESULT' && workflowProcessData?.resultText) || !isWorkflow) && (
|
||||
<SimpleBtn
|
||||
isDisabled={isError || !messageId}
|
||||
className={cn(isMobile && '!px-1.5', 'space-x-1')}
|
||||
|
||||
@ -27,15 +27,15 @@ const ResultTab = ({
|
||||
onCurrentTabChange(tab)
|
||||
}
|
||||
useEffect(() => {
|
||||
if (data?.resultText)
|
||||
if (data?.resultText || !!data?.files?.length)
|
||||
switchTab('RESULT')
|
||||
else
|
||||
switchTab('DETAIL')
|
||||
}, [data?.resultText])
|
||||
}, [data?.files?.length, data?.resultText])
|
||||
|
||||
return (
|
||||
<div className='grow relative flex flex-col'>
|
||||
{data?.resultText && (
|
||||
{(data?.resultText || !!data?.files?.length) && (
|
||||
<div className='shrink-0 flex items-center mb-2 border-b-[0.5px] border-[rgba(0,0,0,0.05)]'>
|
||||
<div
|
||||
className={cn(
|
||||
@ -56,14 +56,21 @@ const ResultTab = ({
|
||||
<div className={cn('grow bg-white')}>
|
||||
{currentTab === 'RESULT' && (
|
||||
<>
|
||||
<Markdown content={data?.resultText || ''} />
|
||||
{data?.resultText && <Markdown content={data?.resultText || ''} />}
|
||||
{!!data?.files?.length && (
|
||||
<FileList
|
||||
files={data?.files}
|
||||
showDeleteAction={false}
|
||||
showDownloadAction
|
||||
canPreview
|
||||
/>
|
||||
<div className='flex flex-col gap-2'>
|
||||
{data?.files.map((item: any) => (
|
||||
<div key={item.varName} className='flex flex-col gap-1 system-xs-regular'>
|
||||
<div className='py-1 text-text-tertiary '>{item.varName}</div>
|
||||
<FileList
|
||||
files={item.list}
|
||||
showDeleteAction={false}
|
||||
showDownloadAction
|
||||
canPreview
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -1,23 +1,23 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import React, { useState } from 'react'
|
||||
import { RiArrowDownSLine } from '@remixicon/react'
|
||||
import { RiArrowDownSLine, RiCloseCircleFill, RiExchange2Fill, RiFilter3Line } from '@remixicon/react'
|
||||
import Checkbox from '../../base/checkbox'
|
||||
import cn from '@/utils/classnames'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import { Check, DotsGrid } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { XCircle } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import { ChatBot, CuteRobot } from '@/app/components/base/icons/src/vender/solid/communication'
|
||||
import { Route } from '@/app/components/base/icons/src/vender/solid/mapsAndTravel'
|
||||
import { BubbleTextMod, ChatBot, ListSparkle, Logic } from '@/app/components/base/icons/src/vender/solid/communication'
|
||||
import { type AppMode } from '@/types/app'
|
||||
export type AppSelectorProps = {
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
value: Array<AppMode>
|
||||
onChange: (value: AppSelectorProps['value']) => void
|
||||
}
|
||||
|
||||
const allTypes: AppMode[] = ['chat', 'agent-chat', 'completion', 'advanced-chat', 'workflow']
|
||||
|
||||
const AppTypeSelector = ({ value, onChange }: AppSelectorProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return (
|
||||
@ -33,96 +33,136 @@ const AppTypeSelector = ({ value, onChange }: AppSelectorProps) => {
|
||||
className='block'
|
||||
>
|
||||
<div className={cn(
|
||||
'flex items-center gap-1 h-8 text-gray-700 text-[13px] leading-[18px] cursor-pointer px-2 rounded-lg bg-white shadow-xs hover:bg-gray-200',
|
||||
open && !value && '!bg-gray-200 hover:!bg-gray-200',
|
||||
!!value && '!bg-white hover:!bg-white',
|
||||
'flex items-center justify-between rounded-md cursor-pointer px-2 space-x-1 hover:bg-state-base-hover',
|
||||
)}>
|
||||
{!value && (
|
||||
<>
|
||||
<div className='w-4 h-4 p-[1px]'>
|
||||
<DotsGrid className='w-3.5 h-3.5' />
|
||||
</div>
|
||||
<div className=''>{t('app.typeSelector.all')}</div>
|
||||
<div className='w-4 h-4 p-[1px]'>
|
||||
<RiArrowDownSLine className='w-3.5 h-3.5' />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{value === 'chatbot' && (
|
||||
<>
|
||||
<div className='w-4 h-4 p-[1px]'>
|
||||
<ChatBot className='w-3.5 h-3.5 text-[#1570EF]' />
|
||||
</div>
|
||||
<div className=''>{t('app.typeSelector.chatbot')}</div>
|
||||
<div className='w-4 h-4 p-[1px]' onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onChange('')
|
||||
}}>
|
||||
<XCircle className='w-3.5 h-3.5 text-gray-400 cursor-pointer hover:text-gray-600' />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{value === 'agent' && (
|
||||
<>
|
||||
<div className='w-4 h-4 p-[1px]'>
|
||||
<CuteRobot className='w-3.5 h-3.5 text-indigo-600' />
|
||||
</div>
|
||||
<div className=''>{t('app.typeSelector.agent')}</div>
|
||||
<div className='w-4 h-4 p-[1px]' onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onChange('')
|
||||
}}>
|
||||
<XCircle className='w-3.5 h-3.5 text-gray-400 cursor-pointer hover:text-gray-600' />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{value === 'workflow' && (
|
||||
<>
|
||||
<div className='w-4 h-4 p-[1px]'>
|
||||
<Route className='w-3.5 h-3.5 text-[#F79009]' />
|
||||
</div>
|
||||
<div className=''>{t('app.typeSelector.workflow')}</div>
|
||||
<div className='w-4 h-4 p-[1px]' onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onChange('')
|
||||
}}>
|
||||
<XCircle className='w-3.5 h-3.5 text-gray-400 cursor-pointer hover:text-gray-600' />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<AppTypeSelectTrigger values={value} />
|
||||
{value && value.length > 0 && <div className='w-4 h-4' onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onChange([])
|
||||
}}>
|
||||
<RiCloseCircleFill className='w-3.5 h-3.5 text-text-quaternary hover:text-text-tertiary cursor-pointer' />
|
||||
</div>}
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[1002]'>
|
||||
<div className='relative p-1 w-[180px] bg-white rounded-lg shadow-xl'>
|
||||
<div className='flex items-center pl-3 py-[6px] pr-2 rounded-lg cursor-pointer hover:bg-gray-50' onClick={() => {
|
||||
onChange('chatbot')
|
||||
setOpen(false)
|
||||
}}>
|
||||
<ChatBot className='mr-2 w-4 h-4 text-[#1570EF]' />
|
||||
<div className='grow text-gray-700 text-[13px] font-medium leading-[18px]'>{t('app.typeSelector.chatbot')}</div>
|
||||
{value === 'chatbot' && <Check className='w-4 h-4 text-primary-600' />}
|
||||
</div>
|
||||
<div className='flex items-center pl-3 py-[6px] pr-2 rounded-lg cursor-pointer hover:bg-gray-50' onClick={() => {
|
||||
onChange('agent')
|
||||
setOpen(false)
|
||||
}}>
|
||||
<CuteRobot className='mr-2 w-4 h-4 text-indigo-600' />
|
||||
<div className='grow text-gray-700 text-[13px] font-medium leading-[18px]'>{t('app.typeSelector.agent')}</div>
|
||||
{value === 'agent' && <Check className='w-4 h-4 text-primary-600' />}
|
||||
</div>
|
||||
<div className='flex items-center pl-3 py-[6px] pr-2 rounded-lg cursor-pointer hover:bg-gray-50' onClick={() => {
|
||||
onChange('workflow')
|
||||
setOpen(false)
|
||||
}}>
|
||||
<Route className='mr-2 w-4 h-4 text-[#F79009]' />
|
||||
<div className='grow text-gray-700 text-[13px] font-medium leading-[18px]'>{t('app.typeSelector.workflow')}</div>
|
||||
{value === 'workflow' && <Check className='w-4 h-4 text-primary-600' />}
|
||||
</div>
|
||||
</div>
|
||||
<ul className='relative p-1 w-[240px] bg-components-panel-bg-blur backdrop-blur-[5px] rounded-xl shadow-lg border border-components-panel-border'>
|
||||
{allTypes.map(mode => (
|
||||
<AppTypeSelectorItem key={mode} type={mode}
|
||||
checked={Boolean(value.length > 0 && value?.indexOf(mode) !== -1)}
|
||||
onClick={() => {
|
||||
if (value?.indexOf(mode) !== -1)
|
||||
onChange(value?.filter(v => v !== mode) ?? [])
|
||||
else
|
||||
onChange([...(value || []), mode])
|
||||
}} />
|
||||
))}
|
||||
</ul>
|
||||
</PortalToFollowElemContent>
|
||||
</div>
|
||||
</PortalToFollowElem>
|
||||
</div >
|
||||
</PortalToFollowElem >
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(AppTypeSelector)
|
||||
export default AppTypeSelector
|
||||
|
||||
function AppTypeSelectTrigger({ values }: { values: AppSelectorProps['value'] }) {
|
||||
const { t } = useTranslation()
|
||||
if (!values || values.length === 0) {
|
||||
return <div className={cn(
|
||||
'flex items-center justify-between gap-1 h-8',
|
||||
)}>
|
||||
<RiFilter3Line className='w-4 h-4 text-text-tertiary' />
|
||||
<div className='grow min-w-[65px] text-center system-sm-medium text-text-tertiary'>{t('app.typeSelector.all')}</div>
|
||||
<RiArrowDownSLine className='w-4 h-4 text-text-tertiary' />
|
||||
</div>
|
||||
}
|
||||
if (values.length === 1) {
|
||||
return <div className={cn(
|
||||
'flex items-center justify-between gap-1 h-8 flex-nowrap',
|
||||
)}>
|
||||
<AppTypeIcon type={values[0]} />
|
||||
<div className='flex flex-1 items-center text-center line-clamp-1'>
|
||||
<AppTypeLabel type={values[0]} className="system-sm-medium text-components-menu-item-text" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
return <div className={cn(
|
||||
'flex items-center justify-between h-8 -space-x-2 relative',
|
||||
)}>
|
||||
{values.map((mode, index) => (<AppTypeIcon key={mode} type={mode} wrapperClassName='border border-components-panel-on-panel-item-bg' style={{ zIndex: 5 - index }} />))}
|
||||
</div>
|
||||
}
|
||||
|
||||
type AppTypeSelectorItemProps = {
|
||||
checked: boolean
|
||||
type: AppMode
|
||||
onClick: () => void
|
||||
}
|
||||
function AppTypeSelectorItem({ checked, type, onClick }: AppTypeSelectorItemProps) {
|
||||
return <li className='flex items-center space-x-2 pl-2 py-1 pr-1 rounded-lg cursor-pointer hover:bg-state-base-hover' onClick={onClick}>
|
||||
<Checkbox checked={checked} />
|
||||
<AppTypeIcon type={type} />
|
||||
<div className='grow p-1 pl-0'>
|
||||
<AppTypeLabel type={type} className="system-sm-medium text-components-menu-item-text" />
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
|
||||
type AppTypeIconProps = {
|
||||
type: AppMode
|
||||
style?: React.CSSProperties
|
||||
className?: string
|
||||
wrapperClassName?: string
|
||||
}
|
||||
|
||||
export function AppTypeIcon({ type, className, wrapperClassName, style }: AppTypeIconProps) {
|
||||
const wrapperClassNames = cn('w-5 h-5 inline-flex items-center justify-center rounded-md border border-divider-regular', wrapperClassName)
|
||||
const iconClassNames = cn('w-3.5 h-3.5 text-components-avatar-shape-fill-stop-100', className)
|
||||
if (type === 'chat') {
|
||||
return <div style={style} className={cn(wrapperClassNames, 'bg-components-icon-bg-blue-solid')}>
|
||||
<ChatBot className={iconClassNames} />
|
||||
</div>
|
||||
}
|
||||
if (type === 'agent-chat') {
|
||||
return <div style={style} className={cn(wrapperClassNames, 'bg-components-icon-bg-violet-solid')}>
|
||||
<Logic className={iconClassNames} />
|
||||
</div>
|
||||
}
|
||||
if (type === 'advanced-chat') {
|
||||
return <div style={style} className={cn(wrapperClassNames, 'bg-components-icon-bg-blue-light-solid')}>
|
||||
<BubbleTextMod className={iconClassNames} />
|
||||
</div>
|
||||
}
|
||||
if (type === 'workflow') {
|
||||
return <div style={style} className={cn(wrapperClassNames, 'bg-components-icon-bg-indigo-solid')}>
|
||||
<RiExchange2Fill className={iconClassNames} />
|
||||
</div>
|
||||
}
|
||||
if (type === 'completion') {
|
||||
return <div style={style} className={cn(wrapperClassNames, 'bg-components-icon-bg-teal-solid')}>
|
||||
<ListSparkle className={iconClassNames} />
|
||||
</div>
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
type AppTypeLabelProps = {
|
||||
type: AppMode
|
||||
className?: string
|
||||
}
|
||||
export function AppTypeLabel({ type, className }: AppTypeLabelProps) {
|
||||
const { t } = useTranslation()
|
||||
let label = ''
|
||||
if (type === 'chat')
|
||||
label = t('app.typeSelector.chatbot')
|
||||
if (type === 'agent-chat')
|
||||
label = t('app.typeSelector.agent')
|
||||
if (type === 'completion')
|
||||
label = t('app.typeSelector.completion')
|
||||
if (type === 'advanced-chat')
|
||||
label = t('app.typeSelector.advanced')
|
||||
if (type === 'workflow')
|
||||
label = t('app.typeSelector.workflow')
|
||||
|
||||
return <span className={className}>{label}</span>
|
||||
}
|
||||
|
||||
@ -11,16 +11,19 @@ import { useDraggableUploader } from './hooks'
|
||||
import { checkIsAnimatedImage } from './utils'
|
||||
import { ALLOW_FILE_EXTENSIONS } from '@/types/app'
|
||||
|
||||
type UploaderProps = {
|
||||
className?: string
|
||||
onImageCropped?: (tempUrl: string, croppedAreaPixels: Area, fileName: string) => void
|
||||
onUpload?: (file?: File) => void
|
||||
export type OnImageInput = {
|
||||
(isCropped: true, tempUrl: string, croppedAreaPixels: Area, fileName: string): void
|
||||
(isCropped: false, file: File): void
|
||||
}
|
||||
|
||||
const Uploader: FC<UploaderProps> = ({
|
||||
type UploaderProps = {
|
||||
className?: string
|
||||
onImageInput?: OnImageInput
|
||||
}
|
||||
|
||||
const ImageInput: FC<UploaderProps> = ({
|
||||
className,
|
||||
onImageCropped,
|
||||
onUpload,
|
||||
onImageInput,
|
||||
}) => {
|
||||
const [inputImage, setInputImage] = useState<{ file: File; url: string }>()
|
||||
const [isAnimatedImage, setIsAnimatedImage] = useState<boolean>(false)
|
||||
@ -37,8 +40,7 @@ const Uploader: FC<UploaderProps> = ({
|
||||
const onCropComplete = async (_: Area, croppedAreaPixels: Area) => {
|
||||
if (!inputImage)
|
||||
return
|
||||
onImageCropped?.(inputImage.url, croppedAreaPixels, inputImage.file.name)
|
||||
onUpload?.(undefined)
|
||||
onImageInput?.(true, inputImage.url, croppedAreaPixels, inputImage.file.name)
|
||||
}
|
||||
|
||||
const handleLocalFileInput = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
@ -48,7 +50,7 @@ const Uploader: FC<UploaderProps> = ({
|
||||
checkIsAnimatedImage(file).then((isAnimatedImage) => {
|
||||
setIsAnimatedImage(!!isAnimatedImage)
|
||||
if (isAnimatedImage)
|
||||
onUpload?.(file)
|
||||
onImageInput?.(false, file)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -117,4 +119,4 @@ const Uploader: FC<UploaderProps> = ({
|
||||
)
|
||||
}
|
||||
|
||||
export default Uploader
|
||||
export default ImageInput
|
||||
@ -8,12 +8,14 @@ import Button from '../button'
|
||||
import { ImagePlus } from '../icons/src/vender/line/images'
|
||||
import { useLocalFileUploader } from '../image-uploader/hooks'
|
||||
import EmojiPickerInner from '../emoji-picker/Inner'
|
||||
import Uploader from './Uploader'
|
||||
import type { OnImageInput } from './ImageInput'
|
||||
import ImageInput from './ImageInput'
|
||||
import s from './style.module.css'
|
||||
import getCroppedImg from './utils'
|
||||
import type { AppIconType, ImageFile } from '@/types/app'
|
||||
import cn from '@/utils/classnames'
|
||||
import { DISABLE_UPLOAD_IMAGE_AS_ICON } from '@/config'
|
||||
|
||||
export type AppIconEmojiSelection = {
|
||||
type: 'emoji'
|
||||
icon: string
|
||||
@ -69,14 +71,15 @@ const AppIconPicker: FC<AppIconPickerProps> = ({
|
||||
},
|
||||
})
|
||||
|
||||
const [imageCropInfo, setImageCropInfo] = useState<{ tempUrl: string; croppedAreaPixels: Area; fileName: string }>()
|
||||
const handleImageCropped = async (tempUrl: string, croppedAreaPixels: Area, fileName: string) => {
|
||||
setImageCropInfo({ tempUrl, croppedAreaPixels, fileName })
|
||||
}
|
||||
type InputImageInfo = { file: File } | { tempUrl: string; croppedAreaPixels: Area; fileName: string }
|
||||
const [inputImageInfo, setInputImageInfo] = useState<InputImageInfo>()
|
||||
|
||||
const [uploadImageInfo, setUploadImageInfo] = useState<{ file?: File }>()
|
||||
const handleUpload = async (file?: File) => {
|
||||
setUploadImageInfo({ file })
|
||||
const handleImageInput: OnImageInput = async (isCropped: boolean, fileOrTempUrl: string | File, croppedAreaPixels?: Area, fileName?: string) => {
|
||||
setInputImageInfo(
|
||||
isCropped
|
||||
? { tempUrl: fileOrTempUrl as string, croppedAreaPixels: croppedAreaPixels!, fileName: fileName! }
|
||||
: { file: fileOrTempUrl as File },
|
||||
)
|
||||
}
|
||||
|
||||
const handleSelect = async () => {
|
||||
@ -90,15 +93,15 @@ const AppIconPicker: FC<AppIconPickerProps> = ({
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!imageCropInfo && !uploadImageInfo)
|
||||
if (!inputImageInfo)
|
||||
return
|
||||
setUploading(true)
|
||||
if (imageCropInfo.file) {
|
||||
handleLocalFileUpload(imageCropInfo.file)
|
||||
if ('file' in inputImageInfo) {
|
||||
handleLocalFileUpload(inputImageInfo.file)
|
||||
return
|
||||
}
|
||||
const blob = await getCroppedImg(imageCropInfo.tempUrl, imageCropInfo.croppedAreaPixels, imageCropInfo.fileName)
|
||||
const file = new File([blob], imageCropInfo.fileName, { type: blob.type })
|
||||
const blob = await getCroppedImg(inputImageInfo.tempUrl, inputImageInfo.croppedAreaPixels, inputImageInfo.fileName)
|
||||
const file = new File([blob], inputImageInfo.fileName, { type: blob.type })
|
||||
handleLocalFileUpload(file)
|
||||
}
|
||||
}
|
||||
@ -127,10 +130,8 @@ const AppIconPicker: FC<AppIconPickerProps> = ({
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
<Divider className='m-0' />
|
||||
|
||||
<EmojiPickerInner className={activeTab === 'emoji' ? 'block' : 'hidden'} onSelect={handleSelectEmoji} />
|
||||
<Uploader className={activeTab === 'image' ? 'block' : 'hidden'} onImageCropped={handleImageCropped} onUpload={handleUpload}/>
|
||||
<EmojiPickerInner className={cn(activeTab === 'emoji' ? 'block' : 'hidden', 'pt-2')} onSelect={handleSelectEmoji} />
|
||||
<ImageInput className={activeTab === 'image' ? 'block' : 'hidden'} onImageInput={handleImageInput} />
|
||||
|
||||
<Divider className='m-0' />
|
||||
<div className='w-full flex items-center justify-center p-3 gap-2'>
|
||||
|
||||
@ -116,12 +116,12 @@ export default async function getCroppedImg(
|
||||
})
|
||||
}
|
||||
|
||||
export function checkIsAnimatedImage(file) {
|
||||
export function checkIsAnimatedImage(file: File): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fileReader = new FileReader()
|
||||
|
||||
fileReader.onload = function (e) {
|
||||
const arr = new Uint8Array(e.target.result)
|
||||
const arr = new Uint8Array(e.target?.result as ArrayBuffer)
|
||||
|
||||
// Check file extension
|
||||
const fileName = file.name.toLowerCase()
|
||||
@ -148,7 +148,7 @@ export function checkIsAnimatedImage(file) {
|
||||
}
|
||||
|
||||
// Function to check for WebP signature
|
||||
function isWebP(arr) {
|
||||
function isWebP(arr: Uint8Array) {
|
||||
return (
|
||||
arr[0] === 0x52 && arr[1] === 0x49 && arr[2] === 0x46 && arr[3] === 0x46
|
||||
&& arr[8] === 0x57 && arr[9] === 0x45 && arr[10] === 0x42 && arr[11] === 0x50
|
||||
@ -156,7 +156,7 @@ function isWebP(arr) {
|
||||
}
|
||||
|
||||
// Function to check if the WebP is animated (contains ANIM chunk)
|
||||
function checkWebPAnimation(arr) {
|
||||
function checkWebPAnimation(arr: Uint8Array) {
|
||||
// Search for the ANIM chunk in WebP to determine if it's animated
|
||||
for (let i = 12; i < arr.length - 4; i++) {
|
||||
if (arr[i] === 0x41 && arr[i + 1] === 0x4E && arr[i + 2] === 0x49 && arr[i + 3] === 0x4D)
|
||||
|
||||
@ -3,14 +3,15 @@
|
||||
import type { FC } from 'react'
|
||||
import { init } from 'emoji-mart'
|
||||
import data from '@emoji-mart/data'
|
||||
import style from './style.module.css'
|
||||
import classNames from '@/utils/classnames'
|
||||
import Image from 'next/image'
|
||||
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'
|
||||
size?: 'xs' | 'tiny' | 'small' | 'medium' | 'large' | 'xl' | 'xxl'
|
||||
rounded?: boolean
|
||||
iconType?: AppIconType | null
|
||||
icon?: string
|
||||
@ -20,7 +21,28 @@ export type AppIconProps = {
|
||||
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,
|
||||
@ -32,23 +54,15 @@ const AppIcon: FC<AppIconProps> = ({
|
||||
innerIcon,
|
||||
onClick,
|
||||
}) => {
|
||||
const wrapperClassName = classNames(
|
||||
'flex items-center justify-center relative w-9 h-9 text-lg rounded-lg grow-0 shrink-0',
|
||||
size !== 'medium' && style[size],
|
||||
rounded && style.rounded,
|
||||
className ?? '',
|
||||
'overflow-hidden',
|
||||
)
|
||||
|
||||
const isValidImageIcon = iconType === 'image' && imageUrl
|
||||
|
||||
return <span
|
||||
className={wrapperClassName}
|
||||
className={classNames(appIconVariants({ size, rounded }), className)}
|
||||
style={{ background: isValidImageIcon ? undefined : (background || '#FFEAD5') }}
|
||||
onClick={onClick}
|
||||
>
|
||||
{isValidImageIcon
|
||||
? <img src={imageUrl} className="w-full h-full" alt="app icon" />
|
||||
? <Image src={imageUrl} className="w-full h-full" alt="app icon" />
|
||||
: (innerIcon || ((icon && icon !== '') ? <em-emoji id={icon} /> : <em-emoji id='🤖' />))
|
||||
}
|
||||
</span>
|
||||
|
||||
@ -114,7 +114,7 @@ const Answer: FC<AnswerProps> = ({
|
||||
<div className={cn('group relative pr-10', chatAnswerContainerInner)}>
|
||||
<div
|
||||
ref={contentRef}
|
||||
className={cn('relative inline-block px-4 py-3 max-w-full bg-gray-100 rounded-2xl text-sm text-gray-900', workflowProcess && 'w-full')}
|
||||
className={cn('relative inline-block px-4 py-3 max-w-full bg-chat-bubble-bg rounded-2xl body-lg-regular text-text-primary', workflowProcess && 'w-full')}
|
||||
>
|
||||
{
|
||||
!responding && (
|
||||
@ -212,15 +212,15 @@ const Answer: FC<AnswerProps> = ({
|
||||
disabled={!item.prevSibling}
|
||||
onClick={() => item.prevSibling && switchSibling?.(item.prevSibling)}
|
||||
>
|
||||
<ChevronRight className="w-[14px] h-[14px] rotate-180 text-gray-500" />
|
||||
<ChevronRight className="w-[14px] h-[14px] rotate-180 text-text-tertiary" />
|
||||
</button>
|
||||
<span className="px-2 text-xs text-gray-700">{item.siblingIndex + 1} / {item.siblingCount}</span>
|
||||
<span className="px-2 text-xs text-text-quaternary">{item.siblingIndex + 1} / {item.siblingCount}</span>
|
||||
<button
|
||||
className={`${item.nextSibling ? 'opacity-100' : 'opacity-65'}`}
|
||||
disabled={!item.nextSibling}
|
||||
onClick={() => item.nextSibling && switchSibling?.(item.nextSibling)}
|
||||
>
|
||||
<ChevronRight className="w-[14px] h-[14px] text-gray-500" />
|
||||
<ChevronRight className="w-[14px] h-[14px] text-text-tertiary" />
|
||||
</button>
|
||||
</div>}
|
||||
</div>
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 3L4.5 8.5L2 6" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 212 B |
@ -1,9 +1,3 @@
|
||||
.checked {
|
||||
background: var(--color-components-checkbox-bg) url(./assets/check.svg) center center no-repeat;
|
||||
background-size: 12px 12px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.mixed {
|
||||
background: var(--color-components-checkbox-bg) url(./assets/mixed.svg) center center no-repeat;
|
||||
background-size: 12px 12px;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import s from './index.module.css'
|
||||
import { RiCheckLine } from '@remixicon/react'
|
||||
import cn from '@/utils/classnames'
|
||||
import s from './index.module.css'
|
||||
|
||||
type CheckboxProps = {
|
||||
checked?: boolean
|
||||
@ -10,13 +11,28 @@ type CheckboxProps = {
|
||||
}
|
||||
|
||||
const Checkbox = ({ checked, onCheck, className, disabled, mixed }: CheckboxProps) => {
|
||||
if (!checked) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'w-4 h-4 rounded-[4px] bg-components-checkbox-bg-unchecked border border-components-checkbox-border hover:bg-components-checkbox-bg-unchecked-hover hover:border-components-checkbox-border-hover shadow-xs cursor-pointer',
|
||||
disabled && 'border-components-checkbox-border-disabled bg-components-checkbox-bg-disabled hover:border-components-checkbox-border-disabled hover:bg-components-checkbox-bg-disabled cursor-not-allowed',
|
||||
mixed && s.mixed,
|
||||
className,
|
||||
)}
|
||||
onClick={() => {
|
||||
if (disabled)
|
||||
return
|
||||
onCheck?.()
|
||||
}}
|
||||
></div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'w-4 h-4 border rounded border-components-checkbox-border bg-components-checkbox-bg-unchecked shadow-xs shadow-shadow-shadow-3',
|
||||
checked && s.checked,
|
||||
disabled && s.disabled,
|
||||
mixed && s.mixed,
|
||||
'w-4 h-4 flex items-center justify-center rounded-[4px] bg-components-checkbox-bg hover:bg-components-checkbox-bg-hover text-components-checkbox-icon shadow-xs cursor-pointer',
|
||||
disabled && 'bg-components-checkbox-bg-disabled-checked hover:bg-components-checkbox-bg-disabled-checked text-components-checkbox-icon-disabled cursor-not-allowed',
|
||||
className,
|
||||
)}
|
||||
onClick={() => {
|
||||
@ -25,7 +41,9 @@ const Checkbox = ({ checked, onCheck, className, disabled, mixed }: CheckboxProp
|
||||
|
||||
onCheck?.()
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<RiCheckLine className={cn('w-3 h-3')} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -39,10 +39,10 @@ export const CopyIcon = ({ content }: Props) => {
|
||||
<div onMouseLeave={onMouseLeave}>
|
||||
{!isCopied
|
||||
? (
|
||||
<Clipboard className='mx-1 w-3 h-3 text-gray-500 cursor-pointer' onClick={onClickCopy} />
|
||||
<Clipboard className='mx-1 w-3.5 h-3.5 text-text-tertiary cursor-pointer' onClick={onClickCopy} />
|
||||
)
|
||||
: (
|
||||
<ClipboardCheck className='mx-1 w-3 h-3 text-gray-500' />
|
||||
<ClipboardCheck className='mx-1 w-3.5 h-3.5 text-text-tertiary' />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
@ -58,15 +58,15 @@ const DrawerPlus: FC<Props> = ({
|
||||
panelClassname={cn('mt-16 mx-2 sm:mr-2 mb-3 !p-0 rounded-xl', panelClassName, maxWidthClassName)}
|
||||
>
|
||||
<div
|
||||
className={cn(contentClassName, 'w-full flex flex-col bg-white border-[0.5px] border-gray-200 rounded-xl shadow-xl')}
|
||||
className={cn(contentClassName, 'w-full flex flex-col bg-components-panel-bg border-[0.5px] border-divider-subtle rounded-xl shadow-xl')}
|
||||
style={{
|
||||
height,
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
<div className={cn(headerClassName, 'shrink-0 border-b border-b-gray-100 py-4')}>
|
||||
<div className={cn(headerClassName, 'shrink-0 border-b border-divider-subtle py-4')}>
|
||||
<div className='flex justify-between items-center pl-6 pr-5 h-6'>
|
||||
<div className='text-base font-semibold text-gray-900'>
|
||||
<div className='system-xl-semibold text-text-primary'>
|
||||
{title}
|
||||
</div>
|
||||
<div className='flex items-center'>
|
||||
@ -74,12 +74,12 @@ const DrawerPlus: FC<Props> = ({
|
||||
onClick={onHide}
|
||||
className='flex justify-center items-center w-6 h-6 cursor-pointer'
|
||||
>
|
||||
<RiCloseLine className='w-4 h-4 text-gray-500' />
|
||||
<RiCloseLine className='w-4 h-4 text-text-tertiary' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{titleDescription && (
|
||||
<div className='pl-6 pr-10 leading-[18px] text-xs font-normal text-gray-500'>
|
||||
<div className='pl-6 pr-10 system-xs-regular text-text-tertiary'>
|
||||
{titleDescription}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -68,7 +68,7 @@ const EmojiPickerInner: FC<IEmojiPickerInnerProps> = ({
|
||||
}, [onSelect, selectedEmoji, selectedBackground])
|
||||
|
||||
return <div className={cn(className)}>
|
||||
<div className='flex flex-col items-center w-full px-3'>
|
||||
<div className='flex flex-col items-center w-full px-3 pb-2'>
|
||||
<div className="relative w-full">
|
||||
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||
<MagnifyingGlassIcon className="w-5 h-5 text-gray-400" aria-hidden="true" />
|
||||
|
||||
@ -77,9 +77,9 @@ const ConfigParamModal: FC<Props> = ({
|
||||
<Modal
|
||||
isShow={isShow}
|
||||
onClose={onHide}
|
||||
className='!p-8 !pb-6 !mt-14 !max-w-none !w-[640px]'
|
||||
className='!p-6 !mt-14 !max-w-none !w-[640px]'
|
||||
>
|
||||
<div className='mb-2 text-xl font-semibold text-[#1D2939]'>
|
||||
<div className='mb-2 title-2xl-semibold text-text-primary'>
|
||||
{t(`appAnnotation.initSetup.${isInit ? 'title' : 'configTitle'}`)}
|
||||
</div>
|
||||
|
||||
|
||||
@ -10,11 +10,11 @@ export const Item: FC<{ title: string; tooltip: string; children: JSX.Element }>
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
<div className='flex items-center space-x-1'>
|
||||
<div>{title}</div>
|
||||
<div className='flex items-center space-x-1 mb-1'>
|
||||
<div className='py-1 system-sm-semibold text-text-secondary'>{title}</div>
|
||||
<Tooltip
|
||||
popupContent={
|
||||
<div className='max-w-[200px] leading-[18px] text-[13px] font-medium text-gray-800'>{tooltip}</div>
|
||||
<div className='max-w-[200px] system-sm-regular text-text-secondary'>{tooltip}</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -26,7 +26,7 @@ const Slider: React.FC<ISliderProps> = ({ className, max, min, step, value, disa
|
||||
renderThumb={(props, state) => (
|
||||
<div {...props}>
|
||||
<div className='relative w-full h-full'>
|
||||
<div className='absolute top-[-16px] left-[50%] translate-x-[-50%] leading-[18px] text-xs font-medium text-gray-900'>
|
||||
<div className='absolute top-[-16px] left-[50%] translate-x-[-50%] system-sm-semibold text-text-primary'>
|
||||
{(state.valueNow / 100).toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -28,13 +28,13 @@ const ScoreSlider: FC<Props> = ({
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
<div className='mt-[10px] flex justify-between items-center leading-4 text-xs font-normal '>
|
||||
<div className='flex space-x-1 text-[#00A286]'>
|
||||
<div className='mt-[10px] flex justify-between items-center system-xs-semibold-uppercase'>
|
||||
<div className='flex space-x-1 text-util-colors-cyan-cyan-500'>
|
||||
<div>0.8</div>
|
||||
<div>·</div>
|
||||
<div>{t('appDebug.feature.annotation.scoreThreshold.easyMatch')}</div>
|
||||
</div>
|
||||
<div className='flex space-x-1 text-[#0057D8]'>
|
||||
<div className='flex space-x-1 text-util-colors-blue-blue-500'>
|
||||
<div>1.0</div>
|
||||
<div>·</div>
|
||||
<div>{t('appDebug.feature.annotation.scoreThreshold.accurateMatch')}</div>
|
||||
|
||||
@ -36,6 +36,7 @@ const FileIcon: FC<FileIconProps> = ({
|
||||
return <Json className={className} />
|
||||
case 'md':
|
||||
case 'markdown':
|
||||
case 'mdx':
|
||||
return <Md className={className} />
|
||||
case 'pdf':
|
||||
return <Pdf className={className} />
|
||||
|
||||
47
web/app/components/base/file-uploader/audio-preview.tsx
Normal file
@ -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,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
|
||||
@ -20,7 +20,7 @@ const FileImageRender = ({
|
||||
<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}
|
||||
alt={alt || 'Preview'}
|
||||
onLoad={onLoad}
|
||||
onError={onError}
|
||||
src={imageUrl}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import React, { useState } from 'react'
|
||||
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'
|
||||
@ -12,23 +13,36 @@ import { SupportUploadFileTypes } from '@/app/components/workflow/types'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
fileList: FileEntity[]
|
||||
fileList: {
|
||||
varName: string
|
||||
list: FileEntity[]
|
||||
}[]
|
||||
isExpanded?: boolean
|
||||
noBorder?: boolean
|
||||
noPadding?: boolean
|
||||
}
|
||||
|
||||
const FileListInLog = ({ fileList }: Props) => {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
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('border-t border-divider-subtle px-3 py-2', expanded && 'py-3')}>
|
||||
<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></div>
|
||||
<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'>
|
||||
{fileList.map((file) => {
|
||||
<div className='flex gap-1'>
|
||||
{fullList.map((file) => {
|
||||
const { id, name, type, supportFileType, base64Url, url } = file
|
||||
const isImageFile = supportFileType === SupportUploadFileTypes.image
|
||||
return (
|
||||
@ -63,19 +77,25 @@ const FileListInLog = ({ fileList }: Props) => {
|
||||
</div>
|
||||
)}
|
||||
<div className='flex items-center gap-1 cursor-pointer' onClick={() => setExpanded(!expanded)}>
|
||||
{!expanded && <div className='text-text-tertiary system-xs-medium-uppercase'>DETAIL</div>}
|
||||
{!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-1'>
|
||||
{fileList.map(file => (
|
||||
<FileItem
|
||||
key={file.id}
|
||||
file={file}
|
||||
showDeleteAction={false}
|
||||
showDownloadAction
|
||||
/>
|
||||
<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>
|
||||
)}
|
||||
|
||||
@ -1,12 +1,15 @@
|
||||
import {
|
||||
memo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {
|
||||
RiDeleteBinLine,
|
||||
RiDownloadLine,
|
||||
RiEyeLine,
|
||||
} from '@remixicon/react'
|
||||
import FileTypeIcon from '../file-type-icon'
|
||||
import {
|
||||
downloadFile,
|
||||
fileIsUploaded,
|
||||
getFileAppearanceType,
|
||||
getFileExtension,
|
||||
@ -19,6 +22,7 @@ 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
|
||||
@ -26,6 +30,7 @@ type FileInAttachmentItemProps = {
|
||||
showDownloadAction?: boolean
|
||||
onRemove?: (fileId: string) => void
|
||||
onReUpload?: (fileId: string) => void
|
||||
canPreview?: boolean
|
||||
}
|
||||
const FileInAttachmentItem = ({
|
||||
file,
|
||||
@ -33,96 +38,116 @@ const FileInAttachmentItem = ({
|
||||
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 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='flex items-center system-2xs-medium-uppercase text-text-tertiary'>
|
||||
<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'>
|
||||
{
|
||||
ext && (
|
||||
<span>{ext.toLowerCase()}</span>
|
||||
progress >= 0 && !fileIsUploaded(file) && (
|
||||
<ProgressCircle
|
||||
className='mr-2.5'
|
||||
percentage={progress}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
ext && (
|
||||
<span className='mx-1 system-2xs-medium'>•</span>
|
||||
progress === -1 && (
|
||||
<ActionButton
|
||||
className='mr-1'
|
||||
onClick={() => onReUpload?.(id)}
|
||||
>
|
||||
<ReplayLine className='w-4 h-4 text-text-tertiary' />
|
||||
</ActionButton>
|
||||
)
|
||||
}
|
||||
{
|
||||
!!file.size && (
|
||||
<span>{formatFileSize(file.size)}</span>
|
||||
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>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
{
|
||||
showDownloadAction && (
|
||||
<ActionButton
|
||||
size='xs'
|
||||
>
|
||||
<RiDownloadLine className='w-3.5 h-3.5 text-text-tertiary' />
|
||||
</ActionButton>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
imagePreviewUrl && canPreview && (
|
||||
<ImagePreview
|
||||
title={name}
|
||||
url={imagePreviewUrl}
|
||||
onCancel={() => setImagePreviewUrl('')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -37,7 +37,7 @@ const FileImageItem = ({
|
||||
<>
|
||||
<div
|
||||
className='group/file-image relative cursor-pointer'
|
||||
onClick={() => canPreview && setImagePreviewUrl(url || '')}
|
||||
onClick={() => canPreview && setImagePreviewUrl(base64Url || url || '')}
|
||||
>
|
||||
{
|
||||
showDeleteAction && (
|
||||
|
||||
@ -2,6 +2,7 @@ import {
|
||||
RiCloseLine,
|
||||
RiDownloadLine,
|
||||
} from '@remixicon/react'
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
downloadFile,
|
||||
fileIsUploaded,
|
||||
@ -16,11 +17,15 @@ 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
|
||||
}
|
||||
@ -30,88 +35,120 @@ const FileItem = ({
|
||||
showDownloadAction = true,
|
||||
onRemove,
|
||||
onReUpload,
|
||||
canPreview,
|
||||
}: FileItemProps) => {
|
||||
const { id, name, type, progress, url, isRemote } = file
|
||||
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'
|
||||
title={name}
|
||||
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',
|
||||
)}
|
||||
>
|
||||
{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'
|
||||
/>
|
||||
{
|
||||
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>
|
||||
{
|
||||
ext && (
|
||||
<>
|
||||
{ext}
|
||||
<div className='mx-1'>·</div>
|
||||
</>
|
||||
showDownloadAction && tmp_preview_url && (
|
||||
<ActionButton
|
||||
size='m'
|
||||
className='hidden group-hover/file-item:flex absolute -right-1 -top-1'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
downloadFile(tmp_preview_url || '', name)
|
||||
}}
|
||||
>
|
||||
<RiDownloadLine className='w-3.5 h-3.5 text-text-tertiary' />
|
||||
</ActionButton>
|
||||
)
|
||||
}
|
||||
{
|
||||
!!file.size && formatFileSize(file.size)
|
||||
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>
|
||||
{
|
||||
showDownloadAction && url && (
|
||||
<ActionButton
|
||||
size='m'
|
||||
className='hidden group-hover/file-item:flex absolute -right-1 -top-1'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
downloadFile(url || '', name)
|
||||
}}
|
||||
>
|
||||
<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('') }} />
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -23,7 +23,7 @@ export const FileList = ({
|
||||
onRemove,
|
||||
showDeleteAction = true,
|
||||
showDownloadAction = false,
|
||||
canPreview,
|
||||
canPreview = true,
|
||||
}: FileListProps) => {
|
||||
return (
|
||||
<div className={cn('flex flex-wrap gap-2', className)}>
|
||||
@ -51,6 +51,7 @@ export const FileList = ({
|
||||
showDownloadAction={showDownloadAction}
|
||||
onRemove={onRemove}
|
||||
onReUpload={onReUpload}
|
||||
canPreview={canPreview}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
102
web/app/components/base/file-uploader/pdf-preview.tsx
Normal file
@ -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
|
||||
@ -1,5 +1,4 @@
|
||||
import mime from 'mime'
|
||||
import { flatten } from 'lodash-es'
|
||||
import { FileAppearanceTypeEnum } from './types'
|
||||
import type { FileEntity } from './types'
|
||||
import { upload } from '@/service/base'
|
||||
@ -85,7 +84,7 @@ export const getFileAppearanceType = (fileName: string, fileMimetype: string) =>
|
||||
if (extension === 'pdf')
|
||||
return FileAppearanceTypeEnum.pdf
|
||||
|
||||
if (extension === 'md' || extension === 'markdown')
|
||||
if (extension === 'md' || extension === 'markdown' || extension === 'mdx')
|
||||
return FileAppearanceTypeEnum.markdown
|
||||
|
||||
if (extension === 'xlsx' || extension === 'xls')
|
||||
@ -158,12 +157,22 @@ export const isAllowedFileExtension = (fileName: string, fileMimetype: string, a
|
||||
}
|
||||
|
||||
export const getFilesInLogs = (rawData: any) => {
|
||||
const originalFiles = flatten(Object.keys(rawData || {}).map((key) => {
|
||||
if (typeof rawData[key] === 'object' || Array.isArray(rawData[key]))
|
||||
return rawData[key]
|
||||
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)).filter(item => item?.model_identity === '__dify__file__')
|
||||
return getProcessedFilesFromResponse(originalFiles)
|
||||
}).filter(Boolean)
|
||||
return result
|
||||
}
|
||||
|
||||
export const fileIsUploaded = (file: FileEntity) => {
|
||||
|
||||
45
web/app/components/base/file-uploader/video-preview.tsx
Normal file
@ -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
|
||||
82
web/app/components/base/fullscreen-modal/index.tsx
Normal file
@ -0,0 +1,82 @@
|
||||
import { Dialog, Transition } from '@headlessui/react'
|
||||
import { Fragment } from 'react'
|
||||
import { RiCloseLargeLine } from '@remixicon/react'
|
||||
import classNames from '@/utils/classnames'
|
||||
|
||||
type IModal = {
|
||||
className?: string
|
||||
wrapperClassName?: string
|
||||
open: boolean
|
||||
onClose?: () => void
|
||||
title?: React.ReactNode
|
||||
description?: React.ReactNode
|
||||
children?: React.ReactNode
|
||||
closable?: boolean
|
||||
overflowVisible?: boolean
|
||||
}
|
||||
|
||||
export default function FullScreenModal({
|
||||
className,
|
||||
wrapperClassName,
|
||||
open,
|
||||
onClose = () => { },
|
||||
children,
|
||||
closable = false,
|
||||
overflowVisible = false,
|
||||
}: IModal) {
|
||||
return (
|
||||
<Transition show={open} as={Fragment}>
|
||||
<Dialog as="div" className={classNames('modal-dialog', wrapperClassName)} onClose={onClose}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-background-overlay-backdrop backdrop-blur-[6px]" />
|
||||
</Transition.Child>
|
||||
|
||||
<div
|
||||
className="fixed inset-0 h-screen w-screen p-4"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}}
|
||||
>
|
||||
<div className="w-full h-full bg-background-default-subtle rounded-2xl border border-effects-highlight relative">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className={classNames(
|
||||
'h-full',
|
||||
overflowVisible ? 'overflow-visible' : 'overflow-hidden',
|
||||
className,
|
||||
)}>
|
||||
{closable
|
||||
&& <div
|
||||
className='absolute z-50 top-3 right-3 w-9 h-9 flex items-center justify-center rounded-[10px]
|
||||
bg-components-button-tertiary-bg hover:bg-components-button-tertiary-bg-hover cursor-pointer'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onClose()
|
||||
}}>
|
||||
<RiCloseLargeLine className='w-3.5 h-3.5 text-components-button-tertiary-text' />
|
||||
</div>}
|
||||
{children}
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 9C2 5.68629 4.68629 3 8 3H16C19.3137 3 22 5.68629 22 9V15C22 18.3137 19.3137 21 16 21H3C2.44772 21 2 20.5523 2 20V9ZM9 9C8.44772 9 8 9.44772 8 10C8 10.5523 8.44772 11 9 11H15C15.5523 11 16 10.5523 16 10C16 9.44772 15.5523 9 15 9H9ZM9 13C8.44772 13 8 13.4477 8 14C8 14.5523 8.44772 15 9 15H12C12.5523 15 13 14.5523 13 14C13 13.4477 12.5523 13 12 13H9Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 522 B |
@ -0,0 +1,6 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4 5C3.44772 5 3 5.44772 3 6C3 6.55228 3.44772 7 4 7H20C20.5523 7 21 6.55228 21 6C21 5.44772 20.5523 5 20 5H4Z" fill="black"/>
|
||||
<path d="M17.9191 9.60608C17.7616 9.2384 17.4 9 17 9C16.6 9 16.2384 9.2384 16.0809 9.60608L14.7384 12.7384L11.6061 14.0809C11.2384 14.2384 11 14.6 11 15C11 15.4 11.2384 15.7616 11.6061 15.9191L14.7384 17.2616L16.0809 20.3939C16.2384 20.7616 16.6 21 17 21C17.4 21 17.7616 20.7616 17.9191 20.3939L19.2616 17.2616L22.3939 15.9191C22.7616 15.7616 23 15.4 23 15C23 14.6 22.7616 14.2384 22.3939 14.0809L19.2616 12.7384L17.9191 9.60608Z" fill="black"/>
|
||||
<path d="M4 11C3.44772 11 3 11.4477 3 12C3 12.5523 3.44772 13 4 13H9C9.55228 13 10 12.5523 10 12C10 11.4477 9.55228 11 9 11H4Z" fill="black"/>
|
||||
<path d="M4 17C3.44772 17 3 17.4477 3 18C3 18.5523 3.44772 19 4 19H7C7.55228 19 8 18.5523 8 18C8 17.4477 7.55228 17 7 17H4Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 968 B |
@ -0,0 +1,8 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="logic">
|
||||
<g id="Vector">
|
||||
<path d="M12.9089 11.9999C13.913 11.9999 14.727 11.186 14.727 10.1819C14.727 9.17775 13.913 8.36376 12.9089 8.36376C11.9048 8.36376 11.0908 9.17775 11.0908 10.1819C11.0908 11.186 11.9048 11.9999 12.9089 11.9999Z" fill="black"/>
|
||||
<path d="M12.2871 1.11229C9.95219 1.3228 7.78275 2.40696 6.21264 4.14796C4.64254 5.88897 3.78749 8.15849 3.81849 10.5027V10.8763L2.09676 14.3207C2.04261 14.4277 2.01016 14.5444 2.00129 14.6639C1.99241 14.7835 2.00729 14.9037 2.04506 15.0175C2.08283 15.1313 2.14275 15.2366 2.22136 15.3271C2.29997 15.4177 2.39573 15.4918 2.50311 15.5452L3.81849 16.1979V18.3632C3.81849 19.0865 4.10581 19.7802 4.61725 20.2916C5.12869 20.803 5.82234 21.0904 6.54562 21.0904H9.27276V22.9084H19.2722V16.6606C20.5995 15.3604 21.496 13.6844 21.8409 11.8588C22.1858 10.0331 21.9625 8.14562 21.2012 6.45084C20.4398 4.75606 19.1769 3.33556 17.583 2.38094C15.989 1.42633 14.1406 0.983541 12.2871 1.11229ZM17.4542 11.0909H16.416C16.3316 11.4163 16.2016 11.7282 16.0297 12.0172L16.7651 12.7526C16.8519 12.8365 16.9212 12.9368 16.9688 13.0477C17.0165 13.1586 17.0415 13.2779 17.0426 13.3986C17.0436 13.5193 17.0206 13.639 16.9749 13.7507C16.9292 13.8624 16.8617 13.9639 16.7764 14.0493C16.691 14.1346 16.5895 14.2021 16.4778 14.2478C16.3661 14.2935 16.2464 14.3165 16.1257 14.3155C16.005 14.3144 15.8857 14.2893 15.7748 14.2417C15.6639 14.1941 15.5636 14.1248 15.4797 14.038L14.7443 13.3026C14.4553 13.4745 14.1434 13.6045 13.818 13.6889V14.727C13.818 14.9681 13.7222 15.1994 13.5517 15.3698C13.3812 15.5403 13.15 15.6361 12.9089 15.6361C12.6678 15.6361 12.4366 15.5403 12.2661 15.3698C12.0957 15.1994 11.9999 14.9681 11.9999 14.727V13.6889C11.6744 13.6045 11.3625 13.4745 11.0736 13.3026L10.3382 14.038C10.1667 14.2036 9.93708 14.2952 9.69873 14.2931C9.46038 14.2911 9.23239 14.1955 9.06384 14.0269C8.8953 13.8584 8.79969 13.6304 8.79762 13.392C8.79555 13.1537 8.88718 12.924 9.05277 12.7526L9.78818 12.0172C9.61629 11.7282 9.48622 11.4163 9.40184 11.0909H8.36371C8.12262 11.0909 7.8914 10.9951 7.72092 10.8246C7.55044 10.6541 7.45467 10.4229 7.45467 10.1818C7.45467 9.94073 7.55044 9.70951 7.72092 9.53903C7.8914 9.36855 8.12262 9.27278 8.36371 9.27278H9.40184C9.48622 8.94731 9.61629 8.63544 9.78818 8.34647L9.05277 7.61105C8.88718 7.4396 8.79555 7.20997 8.79762 6.97163C8.79969 6.73328 8.8953 6.50528 9.06384 6.33673C9.23239 6.16819 9.46038 6.07259 9.69873 6.07052C9.93708 6.06844 10.1667 6.16007 10.3382 6.32566L11.0736 7.06108C11.3625 6.88918 11.6744 6.75911 11.9999 6.67473V5.63661C11.9999 5.39551 12.0957 5.16429 12.2661 4.99381C12.4366 4.82334 12.6678 4.72756 12.9089 4.72756C13.15 4.72756 13.3812 4.82334 13.5517 4.99381C13.7222 5.16429 13.818 5.39551 13.818 5.63661V6.67473C14.1434 6.75911 14.4553 6.88918 14.7443 7.06108L15.4797 6.32566C15.5636 6.23884 15.6639 6.16958 15.7748 6.12194C15.8857 6.0743 16.005 6.04922 16.1257 6.04817C16.2464 6.04713 16.3661 6.07013 16.4778 6.11583C16.5895 6.16154 16.691 6.22904 16.7764 6.31439C16.8617 6.39975 16.9292 6.50124 16.9749 6.61296C17.0206 6.72468 17.0436 6.84438 17.0426 6.96508C17.0415 7.08578 17.0165 7.20507 16.9688 7.31598C16.9212 7.42688 16.8519 7.52719 16.7651 7.61105L16.0297 8.34647C16.2016 8.63544 16.3316 8.94731 16.416 9.27278H17.4542C17.6952 9.27278 17.9265 9.36855 18.0969 9.53903C18.2674 9.70951 18.3632 9.94073 18.3632 10.1818C18.3632 10.4229 18.2674 10.6541 18.0969 10.8246C17.9265 10.9951 17.6952 11.0909 17.4542 11.0909Z" fill="black"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
@ -0,0 +1,28 @@
|
||||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "24",
|
||||
"height": "24",
|
||||
"viewBox": "0 0 24 24",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"fill-rule": "evenodd",
|
||||
"clip-rule": "evenodd",
|
||||
"d": "M2 9C2 5.68629 4.68629 3 8 3H16C19.3137 3 22 5.68629 22 9V15C22 18.3137 19.3137 21 16 21H3C2.44772 21 2 20.5523 2 20V9ZM9 9C8.44772 9 8 9.44772 8 10C8 10.5523 8.44772 11 9 11H15C15.5523 11 16 10.5523 16 10C16 9.44772 15.5523 9 15 9H9ZM9 13C8.44772 13 8 13.4477 8 14C8 14.5523 8.44772 15 9 15H12C12.5523 15 13 14.5523 13 14C13 13.4477 12.5523 13 12 13H9Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "BubbleTextMod"
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './BubbleTextMod.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
Icon.displayName = 'BubbleTextMod'
|
||||
|
||||
export default Icon
|
||||
@ -0,0 +1,53 @@
|
||||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "24",
|
||||
"height": "24",
|
||||
"viewBox": "0 0 24 24",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M4 5C3.44772 5 3 5.44772 3 6C3 6.55228 3.44772 7 4 7H20C20.5523 7 21 6.55228 21 6C21 5.44772 20.5523 5 20 5H4Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M17.9191 9.60608C17.7616 9.2384 17.4 9 17 9C16.6 9 16.2384 9.2384 16.0809 9.60608L14.7384 12.7384L11.6061 14.0809C11.2384 14.2384 11 14.6 11 15C11 15.4 11.2384 15.7616 11.6061 15.9191L14.7384 17.2616L16.0809 20.3939C16.2384 20.7616 16.6 21 17 21C17.4 21 17.7616 20.7616 17.9191 20.3939L19.2616 17.2616L22.3939 15.9191C22.7616 15.7616 23 15.4 23 15C23 14.6 22.7616 14.2384 22.3939 14.0809L19.2616 12.7384L17.9191 9.60608Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M4 11C3.44772 11 3 11.4477 3 12C3 12.5523 3.44772 13 4 13H9C9.55228 13 10 12.5523 10 12C10 11.4477 9.55228 11 9 11H4Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M4 17C3.44772 17 3 17.4477 3 18C3 18.5523 3.44772 19 4 19H7C7.55228 19 8 18.5523 8 18C8 17.4477 7.55228 17 7 17H4Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "ListSparkle"
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './ListSparkle.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
Icon.displayName = 'ListSparkle'
|
||||
|
||||
export default Icon
|
||||
@ -0,0 +1,53 @@
|
||||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "24",
|
||||
"height": "24",
|
||||
"viewBox": "0 0 24 24",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "g",
|
||||
"attributes": {
|
||||
"id": "logic"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "g",
|
||||
"attributes": {
|
||||
"id": "Vector"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M12.9089 11.9999C13.913 11.9999 14.727 11.186 14.727 10.1819C14.727 9.17775 13.913 8.36376 12.9089 8.36376C11.9048 8.36376 11.0908 9.17775 11.0908 10.1819C11.0908 11.186 11.9048 11.9999 12.9089 11.9999Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M12.2871 1.11229C9.95219 1.3228 7.78275 2.40696 6.21264 4.14796C4.64254 5.88897 3.78749 8.15849 3.81849 10.5027V10.8763L2.09676 14.3207C2.04261 14.4277 2.01016 14.5444 2.00129 14.6639C1.99241 14.7835 2.00729 14.9037 2.04506 15.0175C2.08283 15.1313 2.14275 15.2366 2.22136 15.3271C2.29997 15.4177 2.39573 15.4918 2.50311 15.5452L3.81849 16.1979V18.3632C3.81849 19.0865 4.10581 19.7802 4.61725 20.2916C5.12869 20.803 5.82234 21.0904 6.54562 21.0904H9.27276V22.9084H19.2722V16.6606C20.5995 15.3604 21.496 13.6844 21.8409 11.8588C22.1858 10.0331 21.9625 8.14562 21.2012 6.45084C20.4398 4.75606 19.1769 3.33556 17.583 2.38094C15.989 1.42633 14.1406 0.983541 12.2871 1.11229ZM17.4542 11.0909H16.416C16.3316 11.4163 16.2016 11.7282 16.0297 12.0172L16.7651 12.7526C16.8519 12.8365 16.9212 12.9368 16.9688 13.0477C17.0165 13.1586 17.0415 13.2779 17.0426 13.3986C17.0436 13.5193 17.0206 13.639 16.9749 13.7507C16.9292 13.8624 16.8617 13.9639 16.7764 14.0493C16.691 14.1346 16.5895 14.2021 16.4778 14.2478C16.3661 14.2935 16.2464 14.3165 16.1257 14.3155C16.005 14.3144 15.8857 14.2893 15.7748 14.2417C15.6639 14.1941 15.5636 14.1248 15.4797 14.038L14.7443 13.3026C14.4553 13.4745 14.1434 13.6045 13.818 13.6889V14.727C13.818 14.9681 13.7222 15.1994 13.5517 15.3698C13.3812 15.5403 13.15 15.6361 12.9089 15.6361C12.6678 15.6361 12.4366 15.5403 12.2661 15.3698C12.0957 15.1994 11.9999 14.9681 11.9999 14.727V13.6889C11.6744 13.6045 11.3625 13.4745 11.0736 13.3026L10.3382 14.038C10.1667 14.2036 9.93708 14.2952 9.69873 14.2931C9.46038 14.2911 9.23239 14.1955 9.06384 14.0269C8.8953 13.8584 8.79969 13.6304 8.79762 13.392C8.79555 13.1537 8.88718 12.924 9.05277 12.7526L9.78818 12.0172C9.61629 11.7282 9.48622 11.4163 9.40184 11.0909H8.36371C8.12262 11.0909 7.8914 10.9951 7.72092 10.8246C7.55044 10.6541 7.45467 10.4229 7.45467 10.1818C7.45467 9.94073 7.55044 9.70951 7.72092 9.53903C7.8914 9.36855 8.12262 9.27278 8.36371 9.27278H9.40184C9.48622 8.94731 9.61629 8.63544 9.78818 8.34647L9.05277 7.61105C8.88718 7.4396 8.79555 7.20997 8.79762 6.97163C8.79969 6.73328 8.8953 6.50528 9.06384 6.33673C9.23239 6.16819 9.46038 6.07259 9.69873 6.07052C9.93708 6.06844 10.1667 6.16007 10.3382 6.32566L11.0736 7.06108C11.3625 6.88918 11.6744 6.75911 11.9999 6.67473V5.63661C11.9999 5.39551 12.0957 5.16429 12.2661 4.99381C12.4366 4.82334 12.6678 4.72756 12.9089 4.72756C13.15 4.72756 13.3812 4.82334 13.5517 4.99381C13.7222 5.16429 13.818 5.39551 13.818 5.63661V6.67473C14.1434 6.75911 14.4553 6.88918 14.7443 7.06108L15.4797 6.32566C15.5636 6.23884 15.6639 6.16958 15.7748 6.12194C15.8857 6.0743 16.005 6.04922 16.1257 6.04817C16.2464 6.04713 16.3661 6.07013 16.4778 6.11583C16.5895 6.16154 16.691 6.22904 16.7764 6.31439C16.8617 6.39975 16.9292 6.50124 16.9749 6.61296C17.0206 6.72468 17.0436 6.84438 17.0426 6.96508C17.0415 7.08578 17.0165 7.20507 16.9688 7.31598C16.9212 7.42688 16.8519 7.52719 16.7651 7.61105L16.0297 8.34647C16.2016 8.63544 16.3316 8.94731 16.416 9.27278H17.4542C17.6952 9.27278 17.9265 9.36855 18.0969 9.53903C18.2674 9.70951 18.3632 9.94073 18.3632 10.1818C18.3632 10.4229 18.2674 10.6541 18.0969 10.8246C17.9265 10.9951 17.6952 11.0909 17.4542 11.0909Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "Logic"
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './Logic.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
Icon.displayName = 'Logic'
|
||||
|
||||
export default Icon
|
||||
@ -1,7 +1,10 @@
|
||||
export { default as AiText } from './AiText'
|
||||
export { default as BubbleTextMod } from './BubbleTextMod'
|
||||
export { default as ChatBot } from './ChatBot'
|
||||
export { default as CuteRobot } from './CuteRobot'
|
||||
export { default as EditList } from './EditList'
|
||||
export { default as ListSparkle } from './ListSparkle'
|
||||
export { default as Logic } from './Logic'
|
||||
export { default as MessageDotsCircle } from './MessageDotsCircle'
|
||||
export { default as MessageFast } from './MessageFast'
|
||||
export { default as MessageHeartCircle } from './MessageHeartCircle'
|
||||
|
||||
@ -3,6 +3,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { t } from 'i18next'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { RiAddBoxLine, RiCloseLine, RiDownloadCloud2Line, RiFileCopyLine, RiZoomInLine, RiZoomOutLine } from '@remixicon/react'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
|
||||
@ -10,6 +11,8 @@ type ImagePreviewProps = {
|
||||
url: string
|
||||
title: string
|
||||
onCancel: () => void
|
||||
onPrev?: () => void
|
||||
onNext?: () => void
|
||||
}
|
||||
|
||||
const isBase64 = (str: string): boolean => {
|
||||
@ -25,6 +28,8 @@ const ImagePreview: FC<ImagePreviewProps> = ({
|
||||
url,
|
||||
title,
|
||||
onCancel,
|
||||
onPrev,
|
||||
onNext,
|
||||
}) => {
|
||||
const [scale, setScale] = useState(1)
|
||||
const [position, setPosition] = useState({ x: 0, y: 0 })
|
||||
@ -32,7 +37,6 @@ const ImagePreview: FC<ImagePreviewProps> = ({
|
||||
const imgRef = useRef<HTMLImageElement>(null)
|
||||
const dragStartRef = useRef({ x: 0, y: 0 })
|
||||
const [isCopied, setIsCopied] = useState(false)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const openInNewTab = () => {
|
||||
// Open in a new window, considering the case when the page is inside an iframe
|
||||
@ -51,6 +55,7 @@ const ImagePreview: FC<ImagePreviewProps> = ({
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const downloadImage = () => {
|
||||
// Open in a new window, considering the case when the page is inside an iframe
|
||||
if (url.startsWith('http') || url.startsWith('https')) {
|
||||
@ -188,23 +193,11 @@ const ImagePreview: FC<ImagePreviewProps> = ({
|
||||
}
|
||||
}, [handleMouseUp])
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape')
|
||||
onCancel()
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
|
||||
// Set focus to the container element
|
||||
if (containerRef.current)
|
||||
containerRef.current.focus()
|
||||
|
||||
// Cleanup function
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown)
|
||||
}
|
||||
}, [onCancel])
|
||||
useHotkeys('esc', onCancel)
|
||||
useHotkeys('up', zoomIn)
|
||||
useHotkeys('down', zoomOut)
|
||||
useHotkeys('left', onPrev || (() => {}))
|
||||
useHotkeys('right', onNext || (() => {}))
|
||||
|
||||
return createPortal(
|
||||
<div className='fixed inset-0 p-8 flex items-center justify-center bg-black/80 z-[1000] image-preview-container'
|
||||
|
||||
@ -1,30 +1,17 @@
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import mermaid from 'mermaid'
|
||||
import { usePrevious } from 'ahooks'
|
||||
import CryptoJS from 'crypto-js'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'
|
||||
import LoadingAnim from '@/app/components/base/chat/chat/loading-anim'
|
||||
import cn from '@/utils/classnames'
|
||||
import ImagePreview from '@/app/components/base/image-uploader/image-preview'
|
||||
|
||||
let mermaidAPI: any
|
||||
mermaidAPI = null
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
mermaid.initialize({
|
||||
startOnLoad: true,
|
||||
theme: 'default',
|
||||
flowchart: {
|
||||
htmlLabels: true,
|
||||
useMaxWidth: true,
|
||||
},
|
||||
})
|
||||
if (typeof window !== 'undefined')
|
||||
mermaidAPI = mermaid.mermaidAPI
|
||||
}
|
||||
|
||||
const style = {
|
||||
minWidth: '480px',
|
||||
height: 'auto',
|
||||
overflow: 'auto',
|
||||
}
|
||||
|
||||
const svgToBase64 = (svgGraph: string) => {
|
||||
const svgBytes = new TextEncoder().encode(svgGraph)
|
||||
@ -40,22 +27,26 @@ const svgToBase64 = (svgGraph: string) => {
|
||||
const Flowchart = React.forwardRef((props: {
|
||||
PrimitiveCode: string
|
||||
}, ref) => {
|
||||
const { t } = useTranslation()
|
||||
const [svgCode, setSvgCode] = useState(null)
|
||||
const chartId = useRef(`flowchart_${CryptoJS.MD5(props.PrimitiveCode).toString()}`)
|
||||
const [look, setLook] = useState<'classic' | 'handDrawn'>('classic')
|
||||
|
||||
const prevPrimitiveCode = usePrevious(props.PrimitiveCode)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const timeRef = useRef<NodeJS.Timeout>()
|
||||
const [errMsg, setErrMsg] = useState('')
|
||||
const [imagePreviewUrl, setImagePreviewUrl] = useState('')
|
||||
|
||||
const renderFlowchart = useCallback(async (PrimitiveCode: string) => {
|
||||
setSvgCode(null)
|
||||
setIsLoading(true)
|
||||
|
||||
const renderFlowchart = async (PrimitiveCode: string) => {
|
||||
try {
|
||||
if (typeof window !== 'undefined' && mermaidAPI) {
|
||||
const svgGraph = await mermaidAPI.render(chartId.current, PrimitiveCode)
|
||||
const svgGraph = await mermaidAPI.render('flowchart', PrimitiveCode)
|
||||
const base64Svg: any = await svgToBase64(svgGraph.svg)
|
||||
setSvgCode(base64Svg)
|
||||
setIsLoading(false)
|
||||
if (chartId.current && base64Svg)
|
||||
localStorage.setItem(chartId.current, base64Svg)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
@ -64,15 +55,25 @@ const Flowchart = React.forwardRef((props: {
|
||||
setErrMsg((error as Error).message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [props.PrimitiveCode])
|
||||
|
||||
useEffect(() => {
|
||||
const cachedSvg: any = localStorage.getItem(chartId.current)
|
||||
if (cachedSvg) {
|
||||
setSvgCode(cachedSvg)
|
||||
setIsLoading(false)
|
||||
return
|
||||
if (typeof window !== 'undefined') {
|
||||
mermaid.initialize({
|
||||
startOnLoad: true,
|
||||
theme: 'neutral',
|
||||
look,
|
||||
flowchart: {
|
||||
htmlLabels: true,
|
||||
useMaxWidth: true,
|
||||
},
|
||||
})
|
||||
|
||||
renderFlowchart(props.PrimitiveCode)
|
||||
}
|
||||
}, [look])
|
||||
|
||||
useEffect(() => {
|
||||
if (timeRef.current)
|
||||
clearTimeout(timeRef.current)
|
||||
|
||||
@ -85,24 +86,51 @@ const Flowchart = React.forwardRef((props: {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
<div ref={ref}>
|
||||
<div className="msh-segmented msh-segmented-sm css-23bs09 css-var-r1">
|
||||
<div className="msh-segmented-group">
|
||||
<label className="msh-segmented-item flex items-center space-x-1 m-2 w-[200px]">
|
||||
<div key='classic'
|
||||
className={cn('flex items-center justify-center mb-4 w-[calc((100%-8px)/2)] h-8 rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg cursor-pointer system-sm-medium text-text-secondary',
|
||||
look === 'classic' && 'border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg text-text-primary',
|
||||
)}
|
||||
|
||||
onClick={() => setLook('classic')}
|
||||
>
|
||||
<div className="msh-segmented-item-label">{t('app.mermaid.classic')}</div>
|
||||
</div>
|
||||
<div key='handDrawn'
|
||||
className={cn(
|
||||
'flex items-center justify-center mb-4 w-[calc((100%-8px)/2)] h-8 rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg cursor-pointer system-sm-medium text-text-secondary',
|
||||
look === 'handDrawn' && 'border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg text-text-primary',
|
||||
)}
|
||||
onClick={() => setLook('handDrawn')}
|
||||
>
|
||||
<div className="msh-segmented-item-label">{t('app.mermaid.handDrawn')}</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
svgCode
|
||||
&& <div className="mermaid" style={style}>
|
||||
{svgCode && <img src={svgCode} style={{ width: '100%', height: 'auto' }} alt="Mermaid chart" />}
|
||||
</div>
|
||||
&& <div className="mermaid cursor-pointer h-auto w-full object-fit: cover" onClick={() => setImagePreviewUrl(svgCode)}>
|
||||
{svgCode && <img src={svgCode} alt="mermaid_chart" />}
|
||||
</div>
|
||||
}
|
||||
{isLoading
|
||||
&& <div className='py-4 px-[26px]'>
|
||||
<LoadingAnim type='text' />
|
||||
</div>
|
||||
&& <div className='py-4 px-[26px]'>
|
||||
<LoadingAnim type='text'/>
|
||||
</div>
|
||||
}
|
||||
{
|
||||
errMsg
|
||||
&& <div className='py-4 px-[26px]'>
|
||||
<ExclamationTriangleIcon className='w-6 h-6 text-red-500' />
|
||||
|
||||
{errMsg}
|
||||
</div>
|
||||
&& <div className='py-4 px-[26px]'>
|
||||
<ExclamationTriangleIcon className='w-6 h-6 text-red-500'/>
|
||||
|
||||
{errMsg}
|
||||
</div>
|
||||
}
|
||||
{
|
||||
imagePreviewUrl && (<ImagePreview title='mermaid_chart' url={imagePreviewUrl} onCancel={() => setImagePreviewUrl('')} />)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -3,5 +3,5 @@
|
||||
}
|
||||
|
||||
.modal-panel {
|
||||
@apply w-full max-w-[480px] transform rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all;
|
||||
@apply w-full max-w-[480px] transform rounded-2xl bg-components-panel-bg p-6 text-left align-middle shadow-xl transition-all;
|
||||
}
|
||||
|
||||
@ -29,7 +29,7 @@ export default function Modal({
|
||||
}: IModal) {
|
||||
return (
|
||||
<Transition appear show={isShow} as={Fragment}>
|
||||
<Dialog as="div" className={classNames('modal-dialog', wrapperClassName)} onClose={onClose}>
|
||||
<Dialog as="div" className={classNames('relative z-50', wrapperClassName)} onClose={onClose}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
@ -60,22 +60,22 @@ export default function Modal({
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className={classNames(
|
||||
'modal-panel',
|
||||
'w-full max-w-[480px] transform rounded-2xl bg-components-panel-bg p-6 text-left align-middle shadow-xl transition-all',
|
||||
overflowVisible ? 'overflow-visible' : 'overflow-hidden',
|
||||
className,
|
||||
)}>
|
||||
{title && <Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-gray-900"
|
||||
className="text-lg font-medium leading-6 text-text-primary"
|
||||
>
|
||||
{title}
|
||||
</Dialog.Title>}
|
||||
{description && <Dialog.Description className='text-gray-500 text-xs font-normal mt-2'>
|
||||
{description && <Dialog.Description className='text-text-tertiary text-xs font-normal mt-2'>
|
||||
{description}
|
||||
</Dialog.Description>}
|
||||
{closable
|
||||
&& <div className='absolute z-10 top-6 right-6 w-5 h-5 rounded-2xl flex items-center justify-center hover:cursor-pointer hover:bg-gray-100'>
|
||||
<XMarkIcon className='w-4 h-4 text-gray-500' onClick={
|
||||
&& <div className='absolute z-10 top-6 right-6 w-5 h-5 rounded-2xl flex items-center justify-center hover:cursor-pointer hover:bg-components-panel-on-panel-item-bg-hover'>
|
||||
<XMarkIcon className='w-4 h-4 text-text-tertiary' onClick={
|
||||
(e) => {
|
||||
e.stopPropagation()
|
||||
onClose()
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { Popover, Transition } from '@headlessui/react'
|
||||
import { Fragment, cloneElement, useRef } from 'react'
|
||||
import s from './style.module.css'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
export type HtmlContentProps = {
|
||||
@ -63,19 +62,19 @@ export default function CustomPopover({
|
||||
<Popover.Button
|
||||
ref={buttonRef}
|
||||
disabled={disabled}
|
||||
className={`group ${s.popupBtn} ${open ? '' : 'bg-gray-100'} ${!btnClassName
|
||||
? ''
|
||||
: typeof btnClassName === 'string'
|
||||
? btnClassName
|
||||
: btnClassName?.(open)
|
||||
}`}
|
||||
className={cn(
|
||||
'group inline-flex items-center bg-components-button-secondary-bg px-3 py-2 rounded-lg text-base border border-components-button-secondary-border font-medium hover:bg-components-button-secondary-bg-hover hover:border-components-button-secondary-border-hover focus:outline-none',
|
||||
open && 'bg-components-button-secondary-bg-hover border-components-button-secondary-border',
|
||||
(btnClassName && typeof btnClassName === 'string') && btnClassName,
|
||||
(btnClassName && typeof btnClassName !== 'string') && btnClassName?.(open),
|
||||
)}
|
||||
>
|
||||
{btnElement}
|
||||
</Popover.Button>
|
||||
<Transition as={Fragment}>
|
||||
<Popover.Panel
|
||||
className={cn(
|
||||
s.popupPanel,
|
||||
'absolute z-10 w-full max-w-sm px-4 mt-1 sm:px-0 lg:max-w-3xl',
|
||||
position === 'bottom' && '-translate-x-1/2 left-1/2',
|
||||
position === 'bl' && 'left-0',
|
||||
position === 'br' && 'right-0',
|
||||
@ -91,7 +90,7 @@ export default function CustomPopover({
|
||||
>
|
||||
{({ close }) => (
|
||||
<div
|
||||
className={cn(s.panelContainer, popupClassName)}
|
||||
className={cn('overflow-hidden bg-components-panel-bg w-fit min-w-[130px] rounded-lg shadow-lg ring-1 ring-black ring-opacity-5', popupClassName)}
|
||||
{...(trigger !== 'hover'
|
||||
? {}
|
||||
: {
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
.popupBtn {
|
||||
@apply inline-flex items-center bg-white px-3 py-2 rounded-lg text-base border border-gray-200 font-medium hover:bg-gray-100 focus:outline-none
|
||||
}
|
||||
.popupPanel {
|
||||
@apply absolute z-10 w-full max-w-sm px-4 mt-1 sm:px-0 lg:max-w-3xl
|
||||
}
|
||||
.panelContainer {
|
||||
@apply overflow-hidden bg-white w-fit min-w-[130px] rounded-lg shadow-lg ring-1 ring-black ring-opacity-5
|
||||
}
|
||||
@ -52,7 +52,7 @@ export const getInputVars = (text: string): ValueSelector[] => {
|
||||
|
||||
export const FILE_EXTS: Record<string, string[]> = {
|
||||
[SupportUploadFileTypes.image]: ['JPG', 'JPEG', 'PNG', 'GIF', 'WEBP', 'SVG'],
|
||||
[SupportUploadFileTypes.document]: ['TXT', 'MD', 'MARKDOWN', 'PDF', 'HTML', 'XLSX', 'XLS', 'DOCX', 'CSV', 'EML', 'MSG', 'PPTX', 'PPT', 'XML', 'EPUB'],
|
||||
[SupportUploadFileTypes.document]: ['TXT', 'MD', 'MDX', 'MARKDOWN', 'PDF', 'HTML', 'XLSX', 'XLS', 'DOCX', 'CSV', 'EML', 'MSG', 'PPTX', 'PPT', 'XML', 'EPUB'],
|
||||
[SupportUploadFileTypes.audio]: ['MP3', 'M4A', 'WAV', 'WEBM', 'AMR', 'MPGA'],
|
||||
[SupportUploadFileTypes.video]: ['MP4', 'MOV', 'MPEG', 'MPGA'],
|
||||
}
|
||||
|
||||
@ -26,6 +26,7 @@ import { VarBlockIcon } from '@/app/components/workflow/block-icon'
|
||||
import { Line3 } from '@/app/components/base/icons/src/public/common'
|
||||
import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { isExceptionVariable } from '@/app/components/workflow/utils'
|
||||
|
||||
type WorkflowVariableBlockComponentProps = {
|
||||
nodeKey: string
|
||||
@ -53,6 +54,7 @@ const WorkflowVariableBlockComponent = ({
|
||||
const node = localWorkflowNodesMap![variables[0]]
|
||||
const isEnv = isENV(variables)
|
||||
const isChatVar = isConversationVar(variables)
|
||||
const isException = isExceptionVariable(varName, node?.type)
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([WorkflowVariableBlockNode]))
|
||||
@ -98,10 +100,10 @@ const WorkflowVariableBlockComponent = ({
|
||||
</div>
|
||||
)}
|
||||
<div className='flex items-center text-primary-600'>
|
||||
{!isEnv && !isChatVar && <Variable02 className='shrink-0 w-3.5 h-3.5' />}
|
||||
{!isEnv && !isChatVar && <Variable02 className={cn('shrink-0 w-3.5 h-3.5', isException && 'text-text-warning')} />}
|
||||
{isEnv && <Env className='shrink-0 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
|
||||
{isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
|
||||
<div className={cn('shrink-0 ml-0.5 text-xs font-medium truncate', (isEnv || isChatVar) && 'text-gray-900')} title={varName}>{varName}</div>
|
||||
<div className={cn('shrink-0 ml-0.5 text-xs font-medium truncate', (isEnv || isChatVar) && 'text-gray-900', isException && 'text-text-warning')} title={varName}>{varName}</div>
|
||||
{
|
||||
!node && !isEnv && !isChatVar && (
|
||||
<RiErrorWarningFill className='ml-0.5 w-3 h-3 text-[#D92D20]' />
|
||||
|
||||
@ -30,9 +30,12 @@ export const SkeletonRectangle: FC<SkeletonProps> = (props) => {
|
||||
)
|
||||
}
|
||||
|
||||
export const SkeletonPoint: FC = () =>
|
||||
<div className='text-text-quaternary text-xs font-medium'>·</div>
|
||||
|
||||
export const SkeletonPoint: FC<SkeletonProps> = (props) => {
|
||||
const { className, ...rest } = props
|
||||
return (
|
||||
<div className={classNames('text-text-quaternary text-xs font-medium', className)} {...rest}>·</div>
|
||||
)
|
||||
}
|
||||
/** Usage
|
||||
* <SkeletonContanier>
|
||||
* <SkeletonRow>
|
||||
|
||||
@ -25,8 +25,8 @@ const TabSliderNew: FC<TabSliderProps> = ({
|
||||
key={option.value}
|
||||
onClick={() => onChange(option.value)}
|
||||
className={cn(
|
||||
'mr-1 px-3 py-[7px] h-[32px] flex items-center rounded-lg border-[0.5px] border-transparent text-gray-700 text-[13px] font-medium leading-[18px] cursor-pointer hover:bg-gray-200',
|
||||
value === option.value && 'bg-white border-gray-200 shadow-xs text-primary-600 hover:bg-white',
|
||||
'mr-1 px-3 py-[7px] h-[32px] flex items-center rounded-lg border-[0.5px] border-transparent text-text-tertiary text-[13px] font-medium leading-[18px] cursor-pointer hover:bg-components-main-nav-nav-button-bg-active',
|
||||
value === option.value && 'bg-components-main-nav-nav-button-bg-active border-components-main-nav-nav-button-border shadow-xs text-components-main-nav-nav-button-text-active',
|
||||
)}
|
||||
>
|
||||
{option.icon}
|
||||
|
||||
@ -23,12 +23,16 @@ const Item: FC<ItemProps> = ({
|
||||
return (
|
||||
<div
|
||||
key={option.value}
|
||||
className={cn(className, !isActive && 'cursor-pointer', 'relative pb-2.5 leading-6 text-base font-semibold')}
|
||||
className={cn(
|
||||
'relative pb-2.5 system-xl-semibold',
|
||||
!isActive && 'cursor-pointer',
|
||||
className,
|
||||
)}
|
||||
onClick={() => !isActive && onClick(option.value)}
|
||||
>
|
||||
<div className={cn(isActive ? 'text-gray-900' : 'text-gray-600')}>{option.text}</div>
|
||||
<div className={cn(isActive ? 'text-text-primary' : 'text-text-tertiary')}>{option.text}</div>
|
||||
{isActive && (
|
||||
<div className='absolute bottom-0 left-0 right-0 h-0.5 bg-[#155EEF]'></div>
|
||||
<div className='absolute bottom-0 left-0 right-0 h-0.5 bg-util-colors-blue-blue-500'></div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
@ -52,7 +56,7 @@ const TabSlider: FC<Props> = ({
|
||||
itemClassName,
|
||||
}) => {
|
||||
return (
|
||||
<div className={cn(className, !noBorderBottom && 'border-b border-[#EAECF0]', 'flex space-x-6')}>
|
||||
<div className={cn(className, !noBorderBottom && 'border-b border-divider-subtle', 'flex space-x-6')}>
|
||||
{options.map(option => (
|
||||
<Item
|
||||
isActive={option.value === value}
|
||||
|
||||
@ -78,24 +78,23 @@ const TagFilter: FC<TagFilterProps> = ({
|
||||
className='block'
|
||||
>
|
||||
<div className={cn(
|
||||
'flex items-center gap-1 px-2 h-8 rounded-lg border-[0.5px] border-transparent bg-gray-200 cursor-pointer hover:bg-gray-300',
|
||||
open && !value.length && '!bg-gray-300 hover:bg-gray-300',
|
||||
!open && !!value.length && '!bg-white/80 shadow-xs !border-black/5 hover:!bg-gray-200',
|
||||
open && !!value.length && '!bg-gray-200 !border-black/5 shadow-xs hover:!bg-gray-200',
|
||||
'flex items-center gap-1 px-2 h-8 rounded-lg border-[0.5px] border-transparent bg-components-input-bg-normal cursor-pointer',
|
||||
!open && !!value.length && 'shadow-xs',
|
||||
open && !!value.length && 'shadow-xs',
|
||||
)}>
|
||||
<div className='p-[1px]'>
|
||||
<Tag01 className='h-3.5 w-3.5 text-gray-700' />
|
||||
<Tag01 className='h-3.5 w-3.5 text-text-tertiary' />
|
||||
</div>
|
||||
<div className='text-[13px] leading-[18px] text-gray-700'>
|
||||
<div className='text-[13px] leading-[18px] text-text-secondary'>
|
||||
{!value.length && t('common.tag.placeholder')}
|
||||
{!!value.length && currentTag?.name}
|
||||
</div>
|
||||
{value.length > 1 && (
|
||||
<div className='text-xs font-medium leading-[18px] text-gray-500'>{`+${value.length - 1}`}</div>
|
||||
<div className='text-xs font-medium leading-[18px] text-text-tertiary'>{`+${value.length - 1}`}</div>
|
||||
)}
|
||||
{!value.length && (
|
||||
<div className='p-[1px]'>
|
||||
<RiArrowDownSLine className='h-3.5 w-3.5 text-gray-700' />
|
||||
<RiArrowDownSLine className='h-3.5 w-3.5 text-text-tertiary' />
|
||||
</div>
|
||||
)}
|
||||
{!!value.length && (
|
||||
@ -103,14 +102,14 @@ const TagFilter: FC<TagFilterProps> = ({
|
||||
e.stopPropagation()
|
||||
onChange([])
|
||||
}}>
|
||||
<XCircle className='h-3.5 w-3.5 text-gray-400 group-hover/clear:text-gray-600' />
|
||||
<XCircle className='h-3.5 w-3.5 text-text-tertiary group-hover/clear:text-text-secondary' />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[1002]'>
|
||||
<div className='relative w-[240px] bg-white rounded-lg border-[0.5px] border-gray-200 shadow-lg'>
|
||||
<div className='p-2 border-b-[0.5px] border-black/5'>
|
||||
<div className='relative w-[240px] bg-components-panel-bg-blur backdrop-blur-[5px] rounded-lg border-[0.5px] border-components-panel-border shadow-lg'>
|
||||
<div className='p-2'>
|
||||
<Input
|
||||
showLeftIcon
|
||||
showClearIcon
|
||||
@ -123,17 +122,17 @@ const TagFilter: FC<TagFilterProps> = ({
|
||||
{filteredTagList.map(tag => (
|
||||
<div
|
||||
key={tag.id}
|
||||
className='flex items-center gap-2 pl-3 py-[6px] pr-2 rounded-lg cursor-pointer hover:bg-gray-100'
|
||||
className='flex items-center gap-2 pl-3 py-[6px] pr-2 rounded-lg cursor-pointer hover:bg-state-base-hover'
|
||||
onClick={() => selectTag(tag)}
|
||||
>
|
||||
<div title={tag.name} className='grow text-sm text-gray-700 leading-5 truncate'>{tag.name}</div>
|
||||
{value.includes(tag.id) && <Check className='shrink-0 w-4 h-4 text-primary-600' />}
|
||||
<div title={tag.name} className='grow text-sm text-text-tertiary leading-5 truncate'>{tag.name}</div>
|
||||
{value.includes(tag.id) && <Check className='shrink-0 w-4 h-4 text-text-secondary' />}
|
||||
</div>
|
||||
))}
|
||||
{!filteredTagList.length && (
|
||||
<div className='p-3 flex flex-col items-center gap-1'>
|
||||
<Tag03 className='h-6 w-6 text-gray-300' />
|
||||
<div className='text-gray-500 text-xs leading-[14px]'>{t('common.tag.noTag')}</div>
|
||||
<Tag03 className='h-6 w-6 text-text-tertiary' />
|
||||
<div className='text-text-tertiary text-xs leading-[14px]'>{t('common.tag.noTag')}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -51,12 +51,11 @@ const Toast = ({
|
||||
'top-0',
|
||||
'right-0',
|
||||
)}>
|
||||
<div className={`absolute inset-0 opacity-40 -z-10 ${
|
||||
(type === 'success' && 'bg-[linear-gradient(92deg,rgba(23,178,106,0.25)_0%,rgba(255,255,255,0.00)_100%)]')
|
||||
<div className={`absolute inset-0 opacity-40 -z-10 ${(type === 'success' && 'bg-[linear-gradient(92deg,rgba(23,178,106,0.25)_0%,rgba(255,255,255,0.00)_100%)]')
|
||||
|| (type === 'warning' && 'bg-[linear-gradient(92deg,rgba(247,144,9,0.25)_0%,rgba(255,255,255,0.00)_100%)]')
|
||||
|| (type === 'error' && 'bg-[linear-gradient(92deg,rgba(240,68,56,0.25)_0%,rgba(255,255,255,0.00)_100%)]')
|
||||
|| (type === 'info' && 'bg-[linear-gradient(92deg,rgba(11,165,236,0.25)_0%,rgba(255,255,255,0.00)_100%)]')
|
||||
}`}
|
||||
}`}
|
||||
/>
|
||||
<div className={`flex ${size === 'md' ? 'gap-1' : 'gap-0.5'}`}>
|
||||
<div className={`flex justify-center items-center ${size === 'md' ? 'p-0.5' : 'p-1'}`}>
|
||||
@ -129,11 +128,25 @@ Toast.notify = ({
|
||||
const holder = document.createElement('div')
|
||||
const root = createRoot(holder)
|
||||
|
||||
root.render(<Toast type={type} size={size} message={message} duration={duration} className={className} customComponent={customComponent} />)
|
||||
root.render(
|
||||
<ToastContext.Provider value={{
|
||||
notify: () => { },
|
||||
close: () => {
|
||||
if (holder) {
|
||||
root.unmount()
|
||||
holder.remove()
|
||||
}
|
||||
},
|
||||
}}>
|
||||
<Toast type={type} size={size} message={message} duration={duration} className={className} customComponent={customComponent} />
|
||||
</ToastContext.Provider>,
|
||||
)
|
||||
document.body.appendChild(holder)
|
||||
setTimeout(() => {
|
||||
if (holder)
|
||||
if (holder) {
|
||||
root.unmount()
|
||||
holder.remove()
|
||||
}
|
||||
}, duration || defaultDuring)
|
||||
}
|
||||
}
|
||||
|
||||
@ -98,7 +98,7 @@ const Tooltip: FC<TooltipProps> = ({
|
||||
>
|
||||
{popupContent && (<div
|
||||
className={cn(
|
||||
!noDecoration && 'relative px-3 py-2 text-xs font-normal text-text-secondary bg-components-tooltip-bg rounded-md shadow-lg break-words',
|
||||
!noDecoration && 'relative px-3 py-2 system-xs-regular text-text-tertiary bg-components-panel-bg rounded-md shadow-lg break-words',
|
||||
popupClassName,
|
||||
)}
|
||||
onMouseEnter={() => triggerMethod === 'hover' && setHoverPopup()}
|
||||
|
||||
@ -4,6 +4,7 @@ import {
|
||||
DocumentProcessingPriority,
|
||||
Plan,
|
||||
} from '../type'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import {
|
||||
ZapFast,
|
||||
@ -11,7 +12,11 @@ import {
|
||||
} from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
const PriorityLabel = () => {
|
||||
type PriorityLabelProps = {
|
||||
className?: string
|
||||
}
|
||||
|
||||
const PriorityLabel = ({ className }: PriorityLabelProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { plan } = useProviderContext()
|
||||
|
||||
@ -37,10 +42,10 @@ const PriorityLabel = () => {
|
||||
}
|
||||
</div>
|
||||
}>
|
||||
<span className={`
|
||||
<span className={cn(`
|
||||
shrink-0 flex items-center ml-1 px-1 h-[18px] rounded-[5px] border border-text-accent-secondary
|
||||
text-2xs font-medium text-text-accent-secondary
|
||||
`}>
|
||||
`, className)}>
|
||||
{
|
||||
plan.type === Plan.professional && (
|
||||
<ZapNarrow className='mr-0.5 size-3' />
|
||||
|
||||
@ -2,12 +2,11 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Image from 'next/image'
|
||||
import { retrievalIcon } from '../../create/icons'
|
||||
import type { RetrievalConfig } from '@/types/app'
|
||||
import { RETRIEVE_METHOD } from '@/types/app'
|
||||
import RadioCard from '@/app/components/base/radio-card'
|
||||
import { HighPriority } from '@/app/components/base/icons/src/vender/solid/arrows'
|
||||
import { PatternRecognition, Semantic } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import { FileSearch02 } from '@/app/components/base/icons/src/vender/solid/files'
|
||||
|
||||
type Props = {
|
||||
value: RetrievalConfig
|
||||
@ -15,11 +14,12 @@ type Props = {
|
||||
|
||||
export const getIcon = (type: RETRIEVE_METHOD) => {
|
||||
return ({
|
||||
[RETRIEVE_METHOD.semantic]: Semantic,
|
||||
[RETRIEVE_METHOD.fullText]: FileSearch02,
|
||||
[RETRIEVE_METHOD.hybrid]: PatternRecognition,
|
||||
[RETRIEVE_METHOD.invertedIndex]: HighPriority,
|
||||
})[type] || FileSearch02
|
||||
[RETRIEVE_METHOD.semantic]: retrievalIcon.vector,
|
||||
[RETRIEVE_METHOD.fullText]: retrievalIcon.fullText,
|
||||
[RETRIEVE_METHOD.hybrid]: retrievalIcon.hybrid,
|
||||
[RETRIEVE_METHOD.invertedIndex]: retrievalIcon.vector,
|
||||
[RETRIEVE_METHOD.keywordSearch]: retrievalIcon.vector,
|
||||
})[type] || retrievalIcon.vector
|
||||
}
|
||||
|
||||
const EconomicalRetrievalMethodConfig: FC<Props> = ({
|
||||
@ -28,11 +28,11 @@ const EconomicalRetrievalMethodConfig: FC<Props> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const type = value.search_method
|
||||
const Icon = getIcon(type)
|
||||
const icon = <Image className='size-3.5 text-util-colors-purple-purple-600' src={getIcon(type)} alt='' />
|
||||
return (
|
||||
<div className='space-y-2'>
|
||||
<RadioCard
|
||||
icon={<Icon className='w-4 h-4 text-util-colors-purple-purple-600' />}
|
||||
icon={icon}
|
||||
title={t(`dataset.retrieval.${type}.title`)}
|
||||
description={t(`dataset.retrieval.${type}.description`)}
|
||||
noRadio
|
||||
|
||||
@ -29,6 +29,7 @@ import UpgradeBtn from '@/app/components/billing/upgrade-btn'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { sleep } from '@/utils'
|
||||
import { RETRIEVE_METHOD } from '@/types/app'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
type Props = {
|
||||
datasetId: string
|
||||
@ -287,7 +288,7 @@ const EmbeddingProcess: FC<Props> = ({ datasetId, batchId, documents = [], index
|
||||
</div>
|
||||
{
|
||||
enableBilling && (
|
||||
<PriorityLabel />
|
||||
<PriorityLabel className='ml-0' />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
@ -295,12 +296,15 @@ const EmbeddingProcess: FC<Props> = ({ datasetId, batchId, documents = [], index
|
||||
<div className="shrink-0 text-xs">{`${getSourcePercent(indexingStatusDetail)}%`}</div>
|
||||
)}
|
||||
{indexingStatusDetail.indexing_status === 'error' && (
|
||||
<>
|
||||
<span className="flex items-center max-w-[200px] text-xs text-text-destructive truncate" title={indexingStatusDetail.error || ''}>
|
||||
{indexingStatusDetail.error || 'Error'}
|
||||
<Tooltip
|
||||
popupClassName='px-4 py-[14px] max-w-60 text-sm leading-4 text-text-secondary border-[0.5px] border-components-panel-border rounded-xl'
|
||||
offset={4}
|
||||
popupContent={indexingStatusDetail.error}
|
||||
>
|
||||
<span>
|
||||
<RiErrorWarningFill className='shrink-0 size-4 text-text-destructive' />
|
||||
</span>
|
||||
<RiErrorWarningFill className='shrink-0 size-4 text-text-destructive' />
|
||||
</>
|
||||
</Tooltip>
|
||||
)}
|
||||
{indexingStatusDetail.indexing_status === 'completed' && (
|
||||
<RiCheckboxCircleFill className='shrink-0 size-4 text-text-success' />
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiBookOpenLine } from '@remixicon/react'
|
||||
import EmbeddingProcess from '../embedding-process'
|
||||
|
||||
import s from './index.module.css'
|
||||
import cn from '@/utils/classnames'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import type { FullDocumentDetail, createDocumentResponse } from '@/models/datasets'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
@ -58,13 +57,17 @@ const StepThree = ({ datasetId, datasetName, indexingType, creationCache, retrie
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{!isMobile && <div className={cn(s.sideTip)}>
|
||||
<div className={s.tipCard}>
|
||||
<span className={s.icon} />
|
||||
<div className={s.title}>{t('datasetCreation.stepThree.sideTipTitle')}</div>
|
||||
<div className={s.content}>{t('datasetCreation.stepThree.sideTipContent')}</div>
|
||||
{!isMobile && (
|
||||
<div className="shrink-0 pt-[88px] pr-8 text-xs">
|
||||
<div className="flex flex-col gap-3 w-[328px] p-6 text-text-tertiary bg-background-section rounded-xl">
|
||||
<div className="flex justify-center items-center size-10 bg-components-card-bg rounded-[10px] shadow-lg">
|
||||
<RiBookOpenLine className="size-5 text-text-accent" />
|
||||
</div>
|
||||
<div className="text-base text-text-secondary">{t('datasetCreation.stepThree.sideTipTitle')}</div>
|
||||
<div className="text-text-tertiary">{t('datasetCreation.stepThree.sideTipContent')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>}
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ import ChunkContent from './common/chunk-content'
|
||||
import Dot from './common/dot'
|
||||
import { SegmentIndexTag } from './common/segment-index-tag'
|
||||
import { useSegmentListContext } from './index'
|
||||
import type { ChildChunkDetail } from '@/models/datasets'
|
||||
import type { ChildChunkDetail, ChuckingMode } from '@/models/datasets'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
import classNames from '@/utils/classnames'
|
||||
@ -21,7 +21,7 @@ type IChildSegmentDetailProps = {
|
||||
childChunkInfo?: Partial<ChildChunkDetail> & { id: string }
|
||||
onUpdate: (segmentId: string, childChunkId: string, content: string) => void
|
||||
onCancel: () => void
|
||||
docForm: string
|
||||
docForm: ChuckingMode
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -3,6 +3,8 @@ import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { EditSlice } from '../../../formatted-text/flavours/edit-slice'
|
||||
import { useDocumentContext } from '../index'
|
||||
import Empty from './common/empty'
|
||||
import FullDocListSkeleton from './skeleton/full-doc-list-skeleton'
|
||||
import type { ChildChunkDetail } from '@/models/datasets'
|
||||
import Input from '@/app/components/base/input'
|
||||
import classNames from '@/utils/classnames'
|
||||
@ -19,6 +21,8 @@ type IChildSegmentCardProps = {
|
||||
onClickSlice?: (childChunk: ChildChunkDetail) => void
|
||||
total?: number
|
||||
inputValue?: string
|
||||
onClearFilter?: () => void
|
||||
isLoading?: boolean
|
||||
}
|
||||
|
||||
const ChildSegmentList: FC<IChildSegmentCardProps> = ({
|
||||
@ -31,6 +35,8 @@ const ChildSegmentList: FC<IChildSegmentCardProps> = ({
|
||||
onClickSlice,
|
||||
total,
|
||||
inputValue,
|
||||
onClearFilter,
|
||||
isLoading,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const parentMode = useDocumentContext(s => s.parentMode)
|
||||
@ -54,25 +60,38 @@ const ChildSegmentList: FC<IChildSegmentCardProps> = ({
|
||||
}, [enabled])
|
||||
|
||||
const totalText = useMemo(() => {
|
||||
const text = isFullDocMode
|
||||
? !total
|
||||
? '--'
|
||||
: formatNumber(total)
|
||||
: formatNumber(childChunks.length)
|
||||
const count = isFullDocMode
|
||||
? text === '--'
|
||||
? 0
|
||||
: total
|
||||
: childChunks.length
|
||||
return `${isFullDocMode ? count : childChunks.length} ${t('datasetDocuments.segment.childChunks', { count })}`
|
||||
const isSearch = inputValue !== '' && isFullDocMode
|
||||
if (!isSearch) {
|
||||
const text = isFullDocMode
|
||||
? !total
|
||||
? '--'
|
||||
: formatNumber(total)
|
||||
: formatNumber(childChunks.length)
|
||||
const count = isFullDocMode
|
||||
? text === '--'
|
||||
? 0
|
||||
: total
|
||||
: childChunks.length
|
||||
return `${text} ${t('datasetDocuments.segment.childChunks', { count })}`
|
||||
}
|
||||
else {
|
||||
const text = !total ? '--' : formatNumber(total)
|
||||
const count = text === '--' ? 0 : total
|
||||
return `${count} ${t('datasetDocuments.segment.searchResults', { count })}`
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isFullDocMode, total, childChunks.length])
|
||||
}, [isFullDocMode, total, childChunks.length, inputValue])
|
||||
|
||||
return (
|
||||
<div className={classNames('flex flex-col', contentOpacity, isParagraphMode ? 'p-1 pb-2' : 'px-3 grow')}>
|
||||
<div className={classNames(
|
||||
'flex flex-col',
|
||||
contentOpacity,
|
||||
isParagraphMode ? 'p-1 pb-2' : 'px-3 grow',
|
||||
(isFullDocMode && isLoading) && 'overflow-y-hidden',
|
||||
)}>
|
||||
{isFullDocMode ? <Divider type='horizontal' className='h-[1px] bg-divider-subtle my-1' /> : null}
|
||||
<div className={classNames('flex items-center justify-between', isFullDocMode ? 'pt-2 pb-3 sticky top-0 left-0 bg-components-panel-bg' : '')}>
|
||||
<div className={classNames('h-7 flex items-center pl-1 pr-3 rounded-lg', (isParagraphMode && collapsed) ? 'bg-dataset-child-chunk-expand-btn-bg' : '')} onClick={(event) => {
|
||||
<div className={classNames('flex items-center justify-between', isFullDocMode ? 'pt-2 pb-3 sticky -top-2 left-0 bg-components-panel-bg' : '')}>
|
||||
<div className={classNames('h-7 flex items-center pl-1 pr-3 rounded-lg', (isParagraphMode && collapsed) && 'bg-dataset-child-chunk-expand-btn-bg', isFullDocMode && 'pl-0')} onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
toggleCollapse()
|
||||
}}>
|
||||
@ -88,11 +107,16 @@ const ChildSegmentList: FC<IChildSegmentCardProps> = ({
|
||||
<span className='text-text-secondary system-sm-semibold-uppercase'>{totalText}</span>
|
||||
<span className={classNames('text-text-quaternary text-xs font-medium pl-1.5', isParagraphMode ? 'hidden group-hover/card:inline-block' : '')}>·</span>
|
||||
<button
|
||||
className={classNames('px-1.5 py-1 text-components-button-secondary-accent-text system-xs-semibold-uppercase', isParagraphMode ? 'hidden group-hover/card:inline-block' : '')}
|
||||
className={classNames(
|
||||
'px-1.5 py-1 text-components-button-secondary-accent-text system-xs-semibold-uppercase',
|
||||
isParagraphMode ? 'hidden group-hover/card:inline-block' : '',
|
||||
(isFullDocMode && isLoading) ? 'text-components-button-secondary-accent-text-disabled' : '',
|
||||
)}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
handleAddNewChildChunk?.(parentChunkId)
|
||||
}}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{t('common.operation.add')}
|
||||
</button>
|
||||
@ -108,25 +132,33 @@ const ChildSegmentList: FC<IChildSegmentCardProps> = ({
|
||||
/>
|
||||
: null}
|
||||
</div>
|
||||
{(isFullDocMode || !collapsed)
|
||||
? <div className={classNames('flex gap-x-0.5', isFullDocMode ? 'grow' : '')}>
|
||||
{isLoading ? <FullDocListSkeleton /> : null}
|
||||
{((isFullDocMode && !isLoading) || !collapsed)
|
||||
? <div className={classNames('flex items-center gap-x-0.5', isFullDocMode ? 'grow' : '')}>
|
||||
{isParagraphMode && <Divider type='vertical' className='h-auto w-[2px] mx-[7px] bg-text-accent-secondary' />}
|
||||
<div className={classNames('w-full !leading-5 flex flex-col', isParagraphMode ? 'gap-y-2' : 'gap-y-3')}>
|
||||
{childChunks.map((childChunk) => {
|
||||
const edited = childChunk.updated_at !== childChunk.created_at
|
||||
return <EditSlice
|
||||
key={childChunk.id}
|
||||
label={`C-${childChunk.position}${edited ? ` · ${t('datasetDocuments.segment.edited')}` : ''}`}
|
||||
text={childChunk.content}
|
||||
onDelete={() => onDelete?.(childChunk.segment_id, childChunk.id)}
|
||||
className='line-clamp-3'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onClickSlice?.(childChunk)
|
||||
}}
|
||||
/>
|
||||
})}
|
||||
</div>
|
||||
{childChunks.length > 0
|
||||
? <div className={classNames('w-full !leading-5 flex flex-col', isParagraphMode ? 'gap-y-2' : 'gap-y-3')}>
|
||||
{childChunks.map((childChunk) => {
|
||||
const edited = childChunk.updated_at !== childChunk.created_at
|
||||
return <EditSlice
|
||||
key={childChunk.id}
|
||||
label={`C-${childChunk.position}${edited ? ` · ${t('datasetDocuments.segment.edited')}` : ''}`}
|
||||
text={childChunk.content}
|
||||
onDelete={() => onDelete?.(childChunk.segment_id, childChunk.id)}
|
||||
className='line-clamp-3'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onClickSlice?.(childChunk)
|
||||
}}
|
||||
/>
|
||||
})}
|
||||
</div>
|
||||
: inputValue !== ''
|
||||
? <div className='h-full w-full'>
|
||||
<Empty onClearFilter={onClearFilter!} />
|
||||
</div>
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
: null}
|
||||
</div>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import React, { type FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ChuckingMode } from '@/models/datasets'
|
||||
import AutoHeightTextarea from '@/app/components/base/auto-height-textarea/common'
|
||||
|
||||
type IChunkContentProps = {
|
||||
@ -8,7 +9,7 @@ type IChunkContentProps = {
|
||||
onQuestionChange: (question: string) => void
|
||||
onAnswerChange?: (answer: string) => void
|
||||
isEditMode?: boolean
|
||||
docForm: string
|
||||
docForm: ChuckingMode
|
||||
}
|
||||
|
||||
const ChunkContent: FC<IChunkContentProps> = ({
|
||||
@ -21,22 +22,22 @@ const ChunkContent: FC<IChunkContentProps> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (docForm === 'qa_model') {
|
||||
if (docForm === ChuckingMode.qa) {
|
||||
return (
|
||||
<>
|
||||
<div className='mb-1 text-xs font-medium text-gray-500'>QUESTION</div>
|
||||
<div className='text-text-tertiary text-xs font-medium'>QUESTION</div>
|
||||
<AutoHeightTextarea
|
||||
outerClassName='mb-4'
|
||||
className='leading-6 text-md text-gray-800'
|
||||
outerClassName='mb-6 mt-1'
|
||||
className='text-text-secondary text-sm tracking-[-0.07px] caret-[#295EFF]'
|
||||
value={question}
|
||||
placeholder={t('datasetDocuments.segment.questionPlaceholder') || ''}
|
||||
onChange={e => onQuestionChange(e.target.value)}
|
||||
disabled={!isEditMode}
|
||||
/>
|
||||
<div className='mb-1 text-xs font-medium text-gray-500'>ANSWER</div>
|
||||
<div className='text-text-tertiary text-xs font-medium'>ANSWER</div>
|
||||
<AutoHeightTextarea
|
||||
outerClassName='mb-4'
|
||||
className='leading-6 text-md text-gray-800'
|
||||
outerClassName='mb-6 mt-1'
|
||||
className='text-text-secondary text-sm tracking-[-0.07px] caret-[#295EFF]'
|
||||
value={answer}
|
||||
placeholder={t('datasetDocuments.segment.answerPlaceholder') || ''}
|
||||
onChange={e => onAnswerChange?.(e.target.value)}
|
||||
|
||||
@ -69,7 +69,7 @@ const Empty: FC<IEmptyProps> = ({
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<div className='h-full w-full absolute top-0 left-0 bg-dataset-chunk-list-empty-bg -z-10' />
|
||||
<div className='h-full w-full absolute top-0 left-0 bg-dataset-chunk-list-mask-bg -z-10' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -25,7 +25,7 @@ import { ToastContext } from '@/app/components/base/toast'
|
||||
import type { Item } from '@/app/components/base/select'
|
||||
import { SimpleSelect } from '@/app/components/base/select'
|
||||
import { updateSegment } from '@/service/datasets'
|
||||
import type { ChildChunkDetail, SegmentDetailModel, SegmentUpdater } from '@/models/datasets'
|
||||
import { type ChildChunkDetail, ChuckingMode, type SegmentDetailModel, type SegmentUpdater } from '@/models/datasets'
|
||||
import NewSegment from '@/app/components/datasets/documents/detail/new-segment'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
@ -154,7 +154,7 @@ const Completed: FC<ICompletedProps> = ({
|
||||
}
|
||||
}, [segments])
|
||||
|
||||
const { data: childChunkListData } = useChildSegmentList(
|
||||
const { isFetching: isLoadingChildSegmentList, data: childChunkListData } = useChildSegmentList(
|
||||
{
|
||||
datasetId,
|
||||
documentId,
|
||||
@ -252,7 +252,7 @@ const Completed: FC<ICompletedProps> = ({
|
||||
needRegenerate = false,
|
||||
) => {
|
||||
const params: SegmentUpdater = { content: '' }
|
||||
if (docForm === 'qa_model') {
|
||||
if (docForm === ChuckingMode.qa) {
|
||||
if (!question.trim())
|
||||
return notify({ type: 'error', message: t('datasetDocuments.segment.questionEmpty') })
|
||||
if (!answer.trim())
|
||||
@ -350,7 +350,7 @@ const Completed: FC<ICompletedProps> = ({
|
||||
return `${total} ${t('datasetDocuments.segment.searchResults', { count })}`
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [segmentListData?.total, mode, parentMode])
|
||||
}, [segmentListData?.total, mode, parentMode, searchValue, selectedStatus])
|
||||
|
||||
const toggleFullScreen = useCallback(() => {
|
||||
setFullScreen(!fullScreen)
|
||||
@ -507,6 +507,7 @@ const Completed: FC<ICompletedProps> = ({
|
||||
checked={isAllSelected}
|
||||
mixed={!isAllSelected && isSomeSelected}
|
||||
onCheck={onSelectedAll}
|
||||
disabled={isLoadingSegmentList}
|
||||
/>
|
||||
<div className={cn('system-sm-semibold-uppercase pl-5', s.totalText)}>{totalText}</div>
|
||||
<SimpleSelect
|
||||
@ -533,11 +534,14 @@ const Completed: FC<ICompletedProps> = ({
|
||||
{/* Segment list */}
|
||||
{
|
||||
isFullDocMode
|
||||
? <div className='grow relative overflow-x-hidden overflow-y-auto'>
|
||||
? <div className={cn(
|
||||
'flex flex-col grow relative overflow-x-hidden',
|
||||
(isLoadingSegmentList || isLoadingChildSegmentList) ? 'overflow-y-hidden' : 'overflow-y-auto',
|
||||
)}>
|
||||
<SegmentCard
|
||||
detail={segments[0]}
|
||||
onClick={() => onClickCard(segments[0])}
|
||||
loading={false}
|
||||
loading={isLoadingSegmentList}
|
||||
/>
|
||||
<ChildSegmentList
|
||||
parentChunkId={segments[0]?.id}
|
||||
@ -549,6 +553,8 @@ const Completed: FC<ICompletedProps> = ({
|
||||
enabled={!archived}
|
||||
total={childChunkListData?.total || 0}
|
||||
inputValue={inputValue}
|
||||
onClearFilter={onClearFilter}
|
||||
isLoading={isLoadingSegmentList || isLoadingChildSegmentList}
|
||||
/>
|
||||
</div>
|
||||
: <SegmentList
|
||||
@ -569,6 +575,7 @@ const Completed: FC<ICompletedProps> = ({
|
||||
/>
|
||||
}
|
||||
{/* Pagination */}
|
||||
<Divider type='horizontal' className='w-auto h-[1px] my-0 mx-6 bg-divider-subtle' />
|
||||
<Pagination
|
||||
current={currentPage - 1}
|
||||
onChange={cur => setCurrentPage(cur + 1)}
|
||||
|
||||
@ -14,7 +14,7 @@ import Dot from './common/dot'
|
||||
import { useSegmentListContext } from './index'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import type { ChildChunkDetail, SegmentUpdater } from '@/models/datasets'
|
||||
import { type ChildChunkDetail, ChuckingMode, type SegmentUpdater } from '@/models/datasets'
|
||||
import classNames from '@/utils/classnames'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
@ -144,7 +144,7 @@ const NewChildSegmentModal: FC<NewChildSegmentModalProps> = ({
|
||||
<div className={classNames('flex grow overflow-hidden', fullScreen ? 'w-full flex-row justify-center px-6 pt-6 gap-x-8' : 'flex-col gap-y-1 py-3 px-4')}>
|
||||
<div className={classNames('break-all overflow-y-auto whitespace-pre-line', fullScreen ? 'w-1/2' : 'grow')}>
|
||||
<ChunkContent
|
||||
docForm=''
|
||||
docForm={ChuckingMode.parentChild}
|
||||
question={content}
|
||||
onQuestionChange={content => setContent(content)}
|
||||
isEditMode={true}
|
||||
|
||||
@ -7,6 +7,7 @@ import ChildSegmentList from './child-segment-list'
|
||||
import Tag from './common/tag'
|
||||
import Dot from './common/dot'
|
||||
import { SegmentIndexTag } from './common/segment-index-tag'
|
||||
import ParentChunkCardSkeleton from './skeleton/parent-chunk-card-skeleton'
|
||||
import { useSegmentListContext } from './index'
|
||||
import type { ChildChunkDetail, SegmentDetailModel } from '@/models/datasets'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
@ -97,13 +98,22 @@ const SegmentCard: FC<ISegmentCardProps> = ({
|
||||
if (answer) {
|
||||
return (
|
||||
<>
|
||||
<div className='flex'>
|
||||
<div className='flex gap-x-1'>
|
||||
<div className='w-4 mr-2 text-[13px] font-medium leading-[20px] text-text-tertiary'>Q</div>
|
||||
<div className='text-text-secondary body-md-regular'>{content}</div>
|
||||
<div
|
||||
className={cn('text-text-secondary body-md-regular',
|
||||
isCollapsed ? 'line-clamp-2' : 'line-clamp-20',
|
||||
)}>
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex'>
|
||||
<div className='flex gap-x-1'>
|
||||
<div className='w-4 mr-2 text-[13px] font-medium leading-[20px] text-text-tertiary'>A</div>
|
||||
<div className='text-text-secondary body-md-regular'>{answer}</div>
|
||||
<div className={cn('text-text-secondary body-md-regular',
|
||||
isCollapsed ? 'line-clamp-2' : 'line-clamp-20',
|
||||
)}>
|
||||
{answer}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
@ -122,6 +132,9 @@ const SegmentCard: FC<ISegmentCardProps> = ({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isParentChildMode])
|
||||
|
||||
if (loading)
|
||||
return <ParentChunkCardSkeleton />
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn('w-full px-3 rounded-xl group/card', isFullDocMode ? '' : 'pt-2.5 pb-2 hover:bg-dataset-chunk-detail-card-hover-bg', className)}
|
||||
@ -208,30 +221,25 @@ const SegmentCard: FC<ISegmentCardProps> = ({
|
||||
: null}
|
||||
</>
|
||||
</div>
|
||||
{loading
|
||||
? (
|
||||
<div className=''>
|
||||
<div className='' />
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<div className={cn('text-text-secondary body-md-regular -tracking-[0.07px] mt-0.5',
|
||||
textOpacity,
|
||||
isFullDocMode ? 'line-clamp-3' : isCollapsed ? 'line-clamp-2' : 'line-clamp-20',
|
||||
)}>
|
||||
{renderContent()}
|
||||
</div>
|
||||
{isGeneralMode && <div className={cn('flex items-center gap-x-2 py-1.5', textOpacity)}>
|
||||
{keywords?.map(keyword => <Tag key={keyword} text={keyword} />)}
|
||||
</div>}
|
||||
{
|
||||
isFullDocMode
|
||||
? <button className='mt-0.5 mb-2 text-text-accent system-xs-semibold-uppercase' onClick={() => onClick?.()}>{t('common.operation.viewMore')}</button>
|
||||
: null
|
||||
}
|
||||
{
|
||||
child_chunks.length > 0
|
||||
<div className={cn('text-text-secondary body-md-regular -tracking-[0.07px] mt-0.5',
|
||||
textOpacity,
|
||||
isFullDocMode ? 'line-clamp-3' : isCollapsed ? 'line-clamp-2' : 'line-clamp-20',
|
||||
)}>
|
||||
{renderContent()}
|
||||
</div>
|
||||
{isGeneralMode && <div className={cn('flex items-center gap-x-2 py-1.5', textOpacity)}>
|
||||
{keywords?.map(keyword => <Tag key={keyword} text={keyword} />)}
|
||||
</div>}
|
||||
{
|
||||
isFullDocMode
|
||||
? <button
|
||||
className='mt-0.5 mb-2 text-text-accent system-xs-semibold-uppercase'
|
||||
onClick={() => onClick?.()}
|
||||
>{t('common.operation.viewMore')}</button>
|
||||
: null
|
||||
}
|
||||
{
|
||||
child_chunks.length > 0
|
||||
&& <ChildSegmentList
|
||||
parentChunkId={id}
|
||||
childChunks={child_chunks}
|
||||
@ -240,9 +248,6 @@ const SegmentCard: FC<ISegmentCardProps> = ({
|
||||
handleAddNewChildChunk={handleAddNewChildChunk}
|
||||
onClickSlice={onClickSlice}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
{showModal
|
||||
&& <Confirm
|
||||
|
||||
@ -12,7 +12,7 @@ import RegenerationModal from './common/regeneration-modal'
|
||||
import { SegmentIndexTag } from './common/segment-index-tag'
|
||||
import Dot from './common/dot'
|
||||
import { useSegmentListContext } from './index'
|
||||
import type { SegmentDetailModel } from '@/models/datasets'
|
||||
import { ChuckingMode, type SegmentDetailModel } from '@/models/datasets'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
import classNames from '@/utils/classnames'
|
||||
@ -23,7 +23,7 @@ type ISegmentDetailProps = {
|
||||
onUpdate: (segmentId: string, q: string, a: string, k: string[], needRegenerate?: boolean) => void
|
||||
onCancel: () => void
|
||||
isEditMode?: boolean
|
||||
docForm: string
|
||||
docForm: ChuckingMode
|
||||
}
|
||||
|
||||
/**
|
||||
@ -85,12 +85,17 @@ const SegmentDetail: FC<ISegmentDetailProps> = ({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isEditMode])
|
||||
|
||||
const isQAModel = useMemo(() => {
|
||||
return docForm === ChuckingMode.qa
|
||||
}, [docForm])
|
||||
|
||||
const wordCountText = useMemo(() => {
|
||||
const total = formatNumber(isEditMode ? question.length : segInfo!.word_count as number)
|
||||
const count = isEditMode ? question.length : segInfo!.word_count as number
|
||||
const contentLength = isQAModel ? (question.length + answer.length) : question.length
|
||||
const total = formatNumber(isEditMode ? contentLength : segInfo!.word_count as number)
|
||||
const count = isEditMode ? contentLength : segInfo!.word_count as number
|
||||
return `${total} ${t('datasetDocuments.segment.characters', { count })}`
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isEditMode, question.length, segInfo?.word_count])
|
||||
}, [isEditMode, question.length, answer.length, segInfo?.word_count, isQAModel])
|
||||
|
||||
const labelPrefix = useMemo(() => {
|
||||
return isParentChildMode ? t('datasetDocuments.segment.parentChunk') : t('datasetDocuments.segment.chunk')
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import React, { type ForwardedRef } from 'react'
|
||||
import React, { type ForwardedRef, useMemo } from 'react'
|
||||
import { useDocumentContext } from '../index'
|
||||
import SegmentCard from './segment-card'
|
||||
import Empty from './common/empty'
|
||||
import GeneralListSkeleton from './skeleton/general-list-skeleton'
|
||||
import ParagraphListSkeleton from './skeleton/paragraph-list-skeleton'
|
||||
import type { ChildChunkDetail, SegmentDetailModel } from '@/models/datasets'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import classNames from '@/utils/classnames'
|
||||
|
||||
type ISegmentListProps = {
|
||||
isLoading: boolean
|
||||
@ -40,8 +41,16 @@ const SegmentList = React.forwardRef(({
|
||||
}: ISegmentListProps,
|
||||
ref: ForwardedRef<HTMLDivElement>,
|
||||
) => {
|
||||
const [mode, parentMode] = useDocumentContext(s => [s.mode, s.parentMode])
|
||||
|
||||
const Skeleton = useMemo(() => {
|
||||
return (mode === 'hierarchical' && parentMode === 'paragraph') ? ParagraphListSkeleton : GeneralListSkeleton
|
||||
}, [mode, parentMode])
|
||||
|
||||
// Loading skeleton
|
||||
if (isLoading)
|
||||
return <Loading type='app' />
|
||||
return <Skeleton />
|
||||
// Search result is empty
|
||||
if (items.length === 0) {
|
||||
return (
|
||||
<div className='h-full pl-6'>
|
||||
@ -50,7 +59,7 @@ ref: ForwardedRef<HTMLDivElement>,
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div ref={ref} className={classNames('flex flex-col h-full overflow-y-auto')}>
|
||||
<div ref={ref} className={'flex flex-col grow overflow-y-auto'}>
|
||||
{
|
||||
items.map((segItem) => {
|
||||
const isLast = items[items.length - 1].id === segItem.id
|
||||
|
||||