diff --git a/web/app/components/workflow/block-selector/snippet-detail-card.tsx b/web/app/components/workflow/block-selector/snippet-detail-card.tsx new file mode 100644 index 0000000000..65f6ba3d9e --- /dev/null +++ b/web/app/components/workflow/block-selector/snippet-detail-card.tsx @@ -0,0 +1,59 @@ +import type { FC } from 'react' +import type { BlockEnum } from '../types' +import type { Snippet as SnippetDetail } from '@/types/snippet' +import AppIcon from '@/app/components/base/app-icon' +import BlockIcon from '../block-icon' + +export type PublishedSnippetDetail = SnippetDetail & { + relatedBlocks?: BlockEnum[] +} + +type SnippetDetailCardProps = { + snippet: PublishedSnippetDetail +} + +const SnippetDetailCard: FC = ({ + snippet, +}) => { + const { author, description, icon_info, name, relatedBlocks = [] } = snippet + + return ( +
+
+
+ +
{name}
+
+ {!!description && ( +
+ {description} +
+ )} + {!!relatedBlocks.length && ( +
+ {relatedBlocks.map(block => ( + + ))} +
+ )} +
+ {!!author && ( +
+ {author} +
+ )} +
+ ) +} + +export default SnippetDetailCard diff --git a/web/app/components/workflow/block-selector/snippet-list-item.tsx b/web/app/components/workflow/block-selector/snippet-list-item.tsx new file mode 100644 index 0000000000..ba76a84ae4 --- /dev/null +++ b/web/app/components/workflow/block-selector/snippet-list-item.tsx @@ -0,0 +1,51 @@ +import type { + ComponentPropsWithoutRef, + Ref, +} from 'react' +import type { PublishedSnippetDetail } from './snippet-detail-card' +import AppIcon from '@/app/components/base/app-icon' +import { cn } from '@/utils/classnames' + +type SnippetListItemProps = { + isHovered: boolean + ref?: Ref + snippet: PublishedSnippetDetail +} & ComponentPropsWithoutRef<'div'> + +const SnippetListItem = ({ + isHovered, + snippet, + className, + ref, + ...props +}: SnippetListItemProps) => { + return ( +
+ +
+ {snippet.name} +
+ {isHovered && snippet.author && ( +
+ {snippet.author} +
+ )} +
+ ) +} + +export default SnippetListItem diff --git a/web/app/components/workflow/block-selector/snippets.tsx b/web/app/components/workflow/block-selector/snippets.tsx index dfbdbd3178..37f571e587 100644 --- a/web/app/components/workflow/block-selector/snippets.tsx +++ b/web/app/components/workflow/block-selector/snippets.tsx @@ -1,4 +1,5 @@ -import type { ReactNode } from 'react' +import type { CreateSnippetDialogPayload } from '../create-snippet-dialog' +import type { Snippet as SnippetDetail } from '@/types/snippet' import { memo, useDeferredValue, @@ -7,40 +8,49 @@ import { } from 'react' import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' -import { - SearchMenu, -} from '@/app/components/base/icons/src/vender/line/others' +import { toast } from '@/app/components/base/ui/toast' import { Tooltip, TooltipContent, TooltipTrigger, } from '@/app/components/base/ui/tooltip' +import { useRouter } from '@/next/navigation' +import { consoleClient } from '@/service/client' +import { useCreateSnippetMutation } from '@/service/use-snippets' import { cn } from '@/utils/classnames' -import BlockIcon from '../block-icon' +import CreateSnippetDialog from '../create-snippet-dialog' import { BlockEnum } from '../types' +import SnippetDetailCard from './snippet-detail-card' +import SnippetListItem from './snippet-list-item' type SnippetsProps = { loading?: boolean searchText: string } -type StaticSnippet = { - id: string - badge: string - badgeClassName: string - title: string - description: string - author?: string +type StaticSnippet = SnippetDetail & { relatedBlocks?: BlockEnum[] } const STATIC_SNIPPETS: StaticSnippet[] = [ { id: 'customer-review', - badge: 'CR', - title: 'Customer Review', - description: 'Customer Review Description', + name: 'Customer Review', + description: 'Collects customer review context, classifies request intent, and routes the workflow through the right generation branch.', author: 'Evan', + type: 'group', + is_published: true, + version: '1.0.0', + use_count: 128, + input_fields: [], + created_at: 1742889600, + updated_at: 1742976000, + icon_info: { + icon_type: 'emoji', + icon: '🧾', + icon_background: '#FFEAD5', + icon_url: '', + }, relatedBlocks: [ BlockEnum.LLM, BlockEnum.Code, @@ -48,7 +58,6 @@ const STATIC_SNIPPETS: StaticSnippet[] = [ BlockEnum.QuestionClassifier, BlockEnum.IfElse, ], - badgeClassName: 'bg-gradient-to-br from-orange-500 to-rose-500', }, ] as const @@ -76,76 +85,17 @@ const LoadingSkeleton = () => { ) } -const SnippetBadge = ({ - badge, - badgeClassName, -}: Pick) => { - return ( - - ) -} - -const SnippetDetailCard = ({ - author, - description, - relatedBlocks = [], - title, - triggerBadge, -}: { - author?: string - description?: string - relatedBlocks?: BlockEnum[] - title: string - triggerBadge: ReactNode -}) => { - return ( -
-
-
- {triggerBadge} -
{title}
-
- {!!description && ( -
- {description} -
- )} - {!!relatedBlocks.length && ( -
- {relatedBlocks.map(block => ( - - ))} -
- )} -
- {!!author && ( -
- {author} -
- )} -
- ) -} - const Snippets = ({ loading = false, searchText, }: SnippetsProps) => { const { t } = useTranslation() + const { push } = useRouter() + const createSnippetMutation = useCreateSnippetMutation() const deferredSearchText = useDeferredValue(searchText) const [hoveredSnippetId, setHoveredSnippetId] = useState(null) + const [isCreateSnippetDialogOpen, setIsCreateSnippetDialogOpen] = useState(false) + const [isCreatingSnippet, setIsCreatingSnippet] = useState(false) const snippets = useMemo(() => { return STATIC_SNIPPETS.map(item => ({ @@ -153,12 +103,55 @@ const Snippets = ({ })) }, []) + const handleCloseCreateSnippetDialog = () => { + setIsCreateSnippetDialogOpen(false) + } + + const handleCreateSnippet = async ({ + name, + description, + icon, + graph, + }: CreateSnippetDialogPayload) => { + setIsCreatingSnippet(true) + + try { + const snippet = await createSnippetMutation.mutateAsync({ + body: { + name, + description: description || undefined, + icon_info: { + icon: icon.type === 'emoji' ? icon.icon : icon.fileId, + icon_type: icon.type, + icon_background: icon.type === 'emoji' ? icon.background : undefined, + icon_url: icon.type === 'image' ? icon.url : undefined, + }, + }, + }) + + await consoleClient.snippets.syncDraftWorkflow({ + params: { snippetId: snippet.id }, + body: { graph }, + }) + + toast.success(t('snippet.createSuccess', { ns: 'workflow' })) + handleCloseCreateSnippetDialog() + push(`/snippets/${snippet.id}/orchestrate`) + } + catch (error) { + toast.error(error instanceof Error ? error.message : t('createFailed', { ns: 'snippet' })) + } + finally { + setIsCreatingSnippet(false) + } + } + const filteredSnippets = useMemo(() => { const normalizedSearch = deferredSearchText.trim().toLowerCase() if (!normalizedSearch) return snippets - return snippets.filter(item => item.title.toLowerCase().includes(normalizedSearch)) + return snippets.filter(item => item.name.toLowerCase().includes(normalizedSearch)) }, [deferredSearchText, snippets]) if (loading) @@ -166,53 +159,40 @@ const Snippets = ({ if (!filteredSnippets.length) { return ( -
- -
- {t('tabs.noSnippetsFound', { ns: 'workflow' })} + <> +
+ +
+ {t('tabs.noSnippetsFound', { ns: 'workflow' })} +
+
- -
+ + ) } return (
{filteredSnippets.map((item) => { - const badge = ( - - ) - const row = ( -
setHoveredSnippetId(item.id)} onMouseLeave={() => setHoveredSnippetId(current => current === item.id ? null : current)} - > - {badge} -
- {item.title} -
- {hoveredSnippetId === item.id && item.author && ( -
- {item.author} -
- )} -
+ /> ) if (!item.description) @@ -229,13 +209,7 @@ const Snippets = ({ variant="plain" popupClassName="!bg-transparent !p-0" > - + )