diff --git a/web/app/components/base/app-icon/index.tsx b/web/app/components/base/app-icon/index.tsx index 7dad4f541d..f7eaa20917 100644 --- a/web/app/components/base/app-icon/index.tsx +++ b/web/app/components/base/app-icon/index.tsx @@ -19,20 +19,21 @@ export type AppIconProps = { imageUrl?: string | null className?: string innerIcon?: React.ReactNode + showEditIcon?: boolean onClick?: () => void } const appIconVariants = cva( - 'flex items-center justify-center relative text-lg rounded-2xl grow-0 shrink-0 overflow-hidden leading-none border-[0.5px] border-divider-regular', + 'flex items-center justify-center relative grow-0 shrink-0 overflow-hidden leading-none border-[0.5px] border-divider-regular', { 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]', + xs: 'w-4 h-4 text-xs rounded-[4px]', + tiny: 'w-6 h-6 text-base rounded-md', + small: 'w-8 h-8 text-xl rounded-lg', + medium: 'w-9 h-9 text-[22px] rounded-[10px]', + large: 'w-10 h-10 text-[24px] rounded-[10px]', + xl: 'w-12 h-12 text-[28px] rounded-xl', + xxl: 'w-14 h-14 text-[32px] rounded-2xl', }, rounded: { true: 'rounded-full', @@ -43,6 +44,46 @@ const appIconVariants = cva( rounded: false, }, }) +const EditIconWrapperVariants = cva( + 'absolute left-0 top-0 z-10 flex items-center justify-center bg-background-overlay-alt', + { + variants: { + size: { + xs: 'w-4 h-4 rounded-[4px]', + tiny: 'w-6 h-6 rounded-md', + small: 'w-8 h-8 rounded-lg', + medium: 'w-9 h-9 rounded-[10px]', + large: 'w-10 h-10 rounded-[10px]', + xl: 'w-12 h-12 rounded-xl', + xxl: 'w-14 h-14 rounded-2xl', + }, + rounded: { + true: 'rounded-full', + }, + }, + defaultVariants: { + size: 'medium', + rounded: false, + }, + }) +const EditIconVariants = cva( + 'text-text-primary-on-surface', + { + variants: { + size: { + xs: 'size-3', + tiny: 'size-3.5', + small: 'size-5', + medium: 'size-[22px]', + large: 'size-6', + xl: 'size-7', + xxl: 'size-8', + }, + }, + defaultVariants: { + size: 'medium', + }, + }) const AppIcon: FC = ({ size = 'medium', rounded = false, @@ -53,6 +94,7 @@ const AppIcon: FC = ({ className, innerIcon, onClick, + showEditIcon = false, }) => { const isValidImageIcon = iconType === 'image' && imageUrl const Icon = (icon && icon !== '') ? : @@ -72,9 +114,9 @@ const AppIcon: FC = ({ : (innerIcon || Icon) } { - isHovering && ( -
- + showEditIcon && isHovering && ( +
+
) } diff --git a/web/app/components/datasets/create-from-pipeline/create-options/create-from-scratch.tsx b/web/app/components/datasets/create-from-pipeline/create-options/create-from-scratch.tsx index 19e173f433..15f1c3d2fd 100644 --- a/web/app/components/datasets/create-from-pipeline/create-options/create-from-scratch.tsx +++ b/web/app/components/datasets/create-from-pipeline/create-options/create-from-scratch.tsx @@ -113,6 +113,7 @@ const CreateFromScratch = ({ icon={appIcon.type === 'image' ? appIcon.fileId : appIcon.icon} background={appIcon.type === 'image' ? undefined : appIcon.background} imageUrl={appIcon.type === 'image' ? appIcon.url : undefined} + showEditIcon />
diff --git a/web/app/components/datasets/create-from-pipeline/index.tsx b/web/app/components/datasets/create-from-pipeline/index.tsx index 77f4104a2d..b0f5fdf345 100644 --- a/web/app/components/datasets/create-from-pipeline/index.tsx +++ b/web/app/components/datasets/create-from-pipeline/index.tsx @@ -2,6 +2,7 @@ import HeaderEffect from './header-effect' import Header from './header' import CreateOptions from './create-options' +import List from './list' const CreateFromPipeline = () => { return ( @@ -12,6 +13,7 @@ const CreateFromPipeline = () => {
+
) } diff --git a/web/app/components/datasets/create-from-pipeline/list/built-in-pipeline-list.tsx b/web/app/components/datasets/create-from-pipeline/list/built-in-pipeline-list.tsx new file mode 100644 index 0000000000..c1c23075d8 --- /dev/null +++ b/web/app/components/datasets/create-from-pipeline/list/built-in-pipeline-list.tsx @@ -0,0 +1,64 @@ +import { ChunkingMode } from '@/models/datasets' +import TemplateCard from './template-card' + +export type Pipeline = { + id: string + name: string + icon_type: 'emoji' | 'image' + icon?: string + icon_background?: string + file_id?: string + url?: string + description: string + doc_form: ChunkingMode +} + +const BuiltInPipelineList = () => { + const mockData: Pipeline[] = [{ + id: '1', + name: 'Pipeline 1', + description: 'This is a description of Pipeline 1. When use the general chunking mode, the chunks retrieved and recalled are the same. When use the general chunking mode, the chunks retrieved and recalled are the same.', + icon_type: 'emoji', + icon: '🤖', + icon_background: '#F0FDF9', + doc_form: ChunkingMode.text, + }, { + id: '2', + name: 'Pipeline 2', + description: 'This is a description of Pipeline 2. When use the general chunking mode, the chunks retrieved and recalled are the same.', + icon_type: 'emoji', + icon: '🏖️', + icon_background: '#FFF4ED', + doc_form: ChunkingMode.parentChild, + }, { + id: '3', + name: 'Pipeline 3', + description: 'This is a description of Pipeline 3', + icon_type: 'emoji', + icon: '🚀', + icon_background: '#FEFBE8', + doc_form: ChunkingMode.qa, + }, { + id: '4', + name: 'Pipeline 4', + description: 'This is a description of Pipeline 4', + icon_type: 'emoji', + icon: '🍯', + icon_background: '#F5F3FF', + doc_form: ChunkingMode.graph, + }] + + return ( +
+ {mockData.map((pipeline, index) => ( + + ))} +
+ ) +} + +export default BuiltInPipelineList diff --git a/web/app/components/datasets/create-from-pipeline/list/customized-list.tsx b/web/app/components/datasets/create-from-pipeline/list/customized-list.tsx new file mode 100644 index 0000000000..2d8fe1bd60 --- /dev/null +++ b/web/app/components/datasets/create-from-pipeline/list/customized-list.tsx @@ -0,0 +1,52 @@ +import { ChunkingMode } from '@/models/datasets' +import type { Pipeline } from './built-in-pipeline-list' +import TemplateCard from './template-card' + +const CustomizedList = () => { + const mockData: Pipeline[] = [{ + id: '1', + name: 'Pipeline 1', + description: 'This is a description of Pipeline 1. When use the general chunking mode, the chunks retrieved and recalled are the same. When use the general chunking mode, the chunks retrieved and recalled are the same.', + icon_type: 'emoji', + icon: '🤖', + icon_background: '#F0FDF9', + doc_form: ChunkingMode.text, + }, { + id: '2', + name: 'Pipeline 2', + description: 'This is a description of Pipeline 2. When use the general chunking mode, the chunks retrieved and recalled are the same.', + icon_type: 'emoji', + icon: '🏖️', + icon_background: '#FFF4ED', + doc_form: ChunkingMode.parentChild, + }, { + id: '3', + name: 'Pipeline 3', + description: 'This is a description of Pipeline 3', + icon_type: 'emoji', + icon: '🚀', + icon_background: '#FEFBE8', + doc_form: ChunkingMode.qa, + }, { + id: '4', + name: 'Pipeline 4', + description: 'This is a description of Pipeline 4', + icon_type: 'emoji', + icon: '🍯', + icon_background: '#F5F3FF', + doc_form: ChunkingMode.graph, + }] + + return ( +
+ {mockData.map((pipeline, index) => ( + + ))} +
+ ) +} + +export default CustomizedList diff --git a/web/app/components/datasets/create-from-pipeline/list/index.tsx b/web/app/components/datasets/create-from-pipeline/list/index.tsx new file mode 100644 index 0000000000..a0f2ef0286 --- /dev/null +++ b/web/app/components/datasets/create-from-pipeline/list/index.tsx @@ -0,0 +1,35 @@ +import { useCallback, useState } from 'react' +import Tab from './tab' +import BuiltInPipelineList from './built-in-pipeline-list' +import CustomizedList from './customized-list' + +const OPTIONS = [ + { value: 'built-in', label: 'Built-in Pipeline' }, + { value: 'customized', label: 'Customized' }, +] + +const List = () => { + const [activeTab, setActiveTab] = useState('built-in') + + const handleTabChange = useCallback((tab: string) => { + setActiveTab(tab) + }, []) + + return ( +
+ + { + activeTab === 'built-in' && + } + { + activeTab === 'customized' && + } +
+ ) +} + +export default List diff --git a/web/app/components/datasets/create-from-pipeline/list/tab/index.tsx b/web/app/components/datasets/create-from-pipeline/list/tab/index.tsx new file mode 100644 index 0000000000..5b638eb4db --- /dev/null +++ b/web/app/components/datasets/create-from-pipeline/list/tab/index.tsx @@ -0,0 +1,32 @@ +import React from 'react' +import Item from './item' + +type TabProps = { + activeTab: string + handleTabChange: (tab: string) => void + options: { value: string; label: string; }[] +} + +const Tab = ({ + activeTab, + handleTabChange, + options, +}: TabProps) => { + return ( +
+
+ {options.map((option, index) => ( + + ))} +
+
+
+ ) +} + +export default React.memo(Tab) diff --git a/web/app/components/datasets/create-from-pipeline/list/tab/item.tsx b/web/app/components/datasets/create-from-pipeline/list/tab/item.tsx new file mode 100644 index 0000000000..6922d5df85 --- /dev/null +++ b/web/app/components/datasets/create-from-pipeline/list/tab/item.tsx @@ -0,0 +1,29 @@ +import cn from '@/utils/classnames' +import React from 'react' + +type ItemProps = { + isSelected: boolean + option: { value: string; label: string } + onClick: (value: string) => void +} + +const Item = ({ + isSelected, + option, + onClick, +}: ItemProps) => { + return ( +
+ {option.label} + {isSelected &&
} +
+ ) +} + +export default React.memo(Item) diff --git a/web/app/components/datasets/create-from-pipeline/list/template-card/edit-pipeline-info.tsx b/web/app/components/datasets/create-from-pipeline/list/template-card/edit-pipeline-info.tsx new file mode 100644 index 0000000000..269f62a25b --- /dev/null +++ b/web/app/components/datasets/create-from-pipeline/list/template-card/edit-pipeline-info.tsx @@ -0,0 +1,147 @@ +import AppIcon from '@/app/components/base/app-icon' +import type { AppIconSelection } from '@/app/components/base/app-icon-picker' +import AppIconPicker from '@/app/components/base/app-icon-picker' +import Input from '@/app/components/base/input' +import Textarea from '@/app/components/base/textarea' +import type { AppIconType } from '@/types/app' +import { RiCloseLine } from '@remixicon/react' +import React, { useCallback, useRef, useState } from 'react' +import Button from '@/app/components/base/button' +import { useTranslation } from 'react-i18next' +import Toast from '@/app/components/base/toast' +import type { Pipeline } from '../built-in-pipeline-list' + +type EditPipelineInfoProps = { + onClose: () => void + onSave: () => void + pipeline: Pipeline +} + +const EditPipelineInfo = ({ + onClose, + onSave, + pipeline, +}: EditPipelineInfoProps) => { + const { t } = useTranslation() + const [name, setName] = useState(pipeline.name) + const [appIcon, setAppIcon] = useState( + pipeline.icon_type === 'image' + ? { type: 'image' as const, url: pipeline.url || '', fileId: pipeline.file_id || '' } + : { type: 'emoji' as const, icon: pipeline.icon || '', background: pipeline.icon_background || '' }, + ) + const [description, setDescription] = useState(pipeline.description) + const [showAppIconPicker, setShowAppIconPicker] = useState(false) + const previousAppIcon = useRef( + pipeline.icon_type === 'image' + ? { type: 'image' as const, url: pipeline.url || '', fileId: pipeline.file_id || '' } + : { type: 'emoji' as const, icon: pipeline.icon || '', background: pipeline.icon_background || '' }, + ) + + const handleAppNameChange = useCallback((event: React.ChangeEvent) => { + const value = event.target.value + setName(value) + }, []) + + const handleOpenAppIconPicker = useCallback(() => { + setShowAppIconPicker(true) + previousAppIcon.current = appIcon + }, [appIcon]) + + const handleSelectAppIcon = useCallback((icon: AppIconSelection) => { + setAppIcon(icon) + setShowAppIconPicker(false) + }, []) + + const handleCloseAppIconPicker = useCallback(() => { + setAppIcon(previousAppIcon.current) + setShowAppIconPicker(false) + }, []) + + const handleDescriptionChange = useCallback((event: React.ChangeEvent) => { + const value = event.target.value + setDescription(value) + }, []) + + const handleSave = useCallback(() => { + if (!name) { + Toast.notify({ + type: 'error', + message: 'Please enter a name for the Knowledge Base.', + }) + return + } + onSave() + onClose() + }, [name, onSave, onClose]) + + return ( +
+ {/* Header */} +
+ + Edit Pipeline Info + +
+ + {/* Form */} +
+
+
+ + +
+ +
+
+ +