mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
feat(marketplace): load workflows into marketplace
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
import { NextRequest } from 'next/server'
|
||||
import { eq, sql } from 'drizzle-orm'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
|
||||
import { db } from '@/db'
|
||||
import * as schema from '@/db/schema'
|
||||
|
||||
const logger = createLogger('MarketplaceStateAPI')
|
||||
|
||||
// Cache for 1 hour
|
||||
export const revalidate = 3600
|
||||
|
||||
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
const { id } = await params
|
||||
|
||||
// Fetch marketplace data to get the state
|
||||
const marketplaceEntry = await db
|
||||
.select({
|
||||
id: schema.marketplace.id,
|
||||
state: schema.marketplace.state,
|
||||
})
|
||||
.from(schema.marketplace)
|
||||
.where(eq(schema.marketplace.workflowId, id))
|
||||
.limit(1)
|
||||
.then((rows) => rows[0])
|
||||
|
||||
if (!marketplaceEntry) {
|
||||
logger.warn(`[${requestId}] No marketplace entry found for workflow: ${id}`)
|
||||
return createErrorResponse('Workflow not found in marketplace', 404)
|
||||
}
|
||||
|
||||
// Increment the view count for this workflow
|
||||
await db
|
||||
.update(schema.marketplace)
|
||||
.set({
|
||||
views: sql`${schema.marketplace.views} + 1`
|
||||
})
|
||||
.where(eq(schema.marketplace.workflowId, id))
|
||||
|
||||
logger.info(`[${requestId}] Retrieved workflow state for marketplace item: ${id}`)
|
||||
|
||||
return createSuccessResponse({
|
||||
id: marketplaceEntry.id,
|
||||
state: marketplaceEntry.state,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`[${requestId}] Error getting workflow state for marketplace item: ${(await params).id}`,
|
||||
error
|
||||
)
|
||||
return createErrorResponse('Failed to get workflow state', 500)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { desc, eq, sql } from 'drizzle-orm'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { db } from '@/db'
|
||||
import { marketplace } from '@/db/schema'
|
||||
import { CATEGORIES } from '@/app/w/marketplace/constants/categories'
|
||||
|
||||
const logger = createLogger('MarketplaceFeaturedAPI')
|
||||
|
||||
// 1 hour cache
|
||||
export const revalidate = 3600
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
// Parse query parameters
|
||||
const url = new URL(request.url)
|
||||
const categoryParam = url.searchParams.get('category')
|
||||
const limitParam = url.searchParams.get('limit') || '6'
|
||||
const limit = parseInt(limitParam, 10)
|
||||
|
||||
const result: {
|
||||
popular: any[]
|
||||
recent: any[]
|
||||
byCategory: Record<string, any[]>
|
||||
} = {
|
||||
popular: [],
|
||||
recent: [],
|
||||
byCategory: {}
|
||||
}
|
||||
|
||||
// Get popular items (most stars)
|
||||
result.popular = await db
|
||||
.select({
|
||||
id: marketplace.id,
|
||||
workflowId: marketplace.workflowId,
|
||||
name: marketplace.name,
|
||||
description: marketplace.description,
|
||||
authorName: marketplace.authorName,
|
||||
stars: marketplace.stars,
|
||||
views: marketplace.views,
|
||||
category: marketplace.category,
|
||||
createdAt: marketplace.createdAt,
|
||||
updatedAt: marketplace.updatedAt,
|
||||
})
|
||||
.from(marketplace)
|
||||
.orderBy(desc(marketplace.stars), desc(marketplace.views))
|
||||
.limit(limit)
|
||||
|
||||
// Get recent items (most recent first)
|
||||
result.recent = await db
|
||||
.select({
|
||||
id: marketplace.id,
|
||||
workflowId: marketplace.workflowId,
|
||||
name: marketplace.name,
|
||||
description: marketplace.description,
|
||||
authorName: marketplace.authorName,
|
||||
stars: marketplace.stars,
|
||||
views: marketplace.views,
|
||||
category: marketplace.category,
|
||||
createdAt: marketplace.createdAt,
|
||||
updatedAt: marketplace.updatedAt,
|
||||
})
|
||||
.from(marketplace)
|
||||
.orderBy(desc(marketplace.createdAt))
|
||||
.limit(limit)
|
||||
|
||||
// If a specific category is requested
|
||||
if (categoryParam) {
|
||||
result.byCategory[categoryParam] = await db
|
||||
.select({
|
||||
id: marketplace.id,
|
||||
workflowId: marketplace.workflowId,
|
||||
name: marketplace.name,
|
||||
description: marketplace.description,
|
||||
authorName: marketplace.authorName,
|
||||
stars: marketplace.stars,
|
||||
views: marketplace.views,
|
||||
category: marketplace.category,
|
||||
createdAt: marketplace.createdAt,
|
||||
updatedAt: marketplace.updatedAt,
|
||||
})
|
||||
.from(marketplace)
|
||||
.where(eq(marketplace.category, categoryParam))
|
||||
.orderBy(desc(marketplace.stars), desc(marketplace.views))
|
||||
.limit(limit)
|
||||
} else {
|
||||
// Get items for each category
|
||||
// Using Promise.all for parallel fetching to improve performance
|
||||
await Promise.all(
|
||||
CATEGORIES.map(async (category) => {
|
||||
result.byCategory[category.value] = await db
|
||||
.select({
|
||||
id: marketplace.id,
|
||||
workflowId: marketplace.workflowId,
|
||||
name: marketplace.name,
|
||||
description: marketplace.description,
|
||||
authorName: marketplace.authorName,
|
||||
stars: marketplace.stars,
|
||||
views: marketplace.views,
|
||||
category: marketplace.category,
|
||||
createdAt: marketplace.createdAt,
|
||||
updatedAt: marketplace.updatedAt,
|
||||
})
|
||||
.from(marketplace)
|
||||
.where(eq(marketplace.category, category.value))
|
||||
.orderBy(desc(marketplace.stars), desc(marketplace.views))
|
||||
.limit(limit)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Fetched featured marketplace items successfully`)
|
||||
|
||||
return NextResponse.json(result)
|
||||
} catch (error: any) {
|
||||
logger.error(`[${requestId}] Error fetching marketplace items`, error)
|
||||
return NextResponse.json({ error: error.message }, { status: 500 })
|
||||
}
|
||||
}
|
||||
+6
-57
@@ -54,6 +54,12 @@ import { useNotificationStore } from '@/stores/notifications/store'
|
||||
import { getWorkflowWithValues } from '@/stores/workflows'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
import {
|
||||
CATEGORIES,
|
||||
getCategoryColor,
|
||||
getCategoryIcon,
|
||||
getCategoryLabel,
|
||||
} from '@/app/w/marketplace/constants/categories'
|
||||
|
||||
const logger = createLogger('MarketplaceModal')
|
||||
|
||||
@@ -114,41 +120,6 @@ const marketplaceFormSchema = z.object({
|
||||
|
||||
type MarketplaceFormValues = z.infer<typeof marketplaceFormSchema>
|
||||
|
||||
// Categories for the dropdown with icons
|
||||
const CATEGORIES = [
|
||||
// {
|
||||
// value: 'project_management',
|
||||
// label: 'Project Management',
|
||||
// icon: <ChartBar className="h-4 w-4 mr-2" />,
|
||||
// },
|
||||
{ value: 'data', label: 'Data Analysis', icon: <Database className="h-4 w-4 mr-2" /> },
|
||||
{ value: 'marketing', label: 'Marketing', icon: <MailIcon className="h-4 w-4 mr-2" /> },
|
||||
{ value: 'sales', label: 'Sales', icon: <Store className="h-4 w-4 mr-2" /> },
|
||||
// { value: 'productivity', label: 'Productivity', icon: <TimerIcon className="h-4 w-4 mr-2" /> },
|
||||
// { value: 'content', label: 'Content Creation', icon: <NotebookPen className="h-4 w-4 mr-2" /> },
|
||||
{
|
||||
value: 'customer_service',
|
||||
label: 'Customer Service',
|
||||
icon: <BotMessageSquare className="h-4 w-4 mr-2" />,
|
||||
},
|
||||
{ value: 'research', label: 'Research', icon: <Atom className="h-4 w-4 mr-2" /> },
|
||||
{ value: 'finance', label: 'Finance', icon: <LineChart className="h-4 w-4 mr-2" /> },
|
||||
{ value: 'programming', label: 'Programming', icon: <Code className="h-4 w-4 mr-2" /> },
|
||||
{ value: 'other', label: 'Other', icon: <Brain className="h-4 w-4 mr-2" /> },
|
||||
]
|
||||
|
||||
// Find category label by value
|
||||
const getCategoryLabel = (value: string) => {
|
||||
const category = CATEGORIES.find((cat) => cat.value === value)
|
||||
return category ? category.label : value
|
||||
}
|
||||
|
||||
// Find category icon by value
|
||||
const getCategoryIcon = (value: string) => {
|
||||
const category = CATEGORIES.find((cat) => cat.value === value)
|
||||
return category ? category.icon : <Store className="h-4 w-4 mr-2" />
|
||||
}
|
||||
|
||||
// Tooltip texts
|
||||
const TOOLTIPS = {
|
||||
category: 'Categorizing your workflow helps users find it more easily.',
|
||||
@@ -601,25 +572,3 @@ export function MarketplaceModal({ open, onOpenChange }: MarketplaceModalProps)
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
// Add this helper function after the getCategoryLabel function
|
||||
const getCategoryColor = (value: string): string => {
|
||||
switch (value) {
|
||||
case 'data':
|
||||
return '#0ea5e9' // sky-500
|
||||
case 'marketing':
|
||||
return '#f43f5e' // rose-500
|
||||
case 'sales':
|
||||
return '#10b981' // emerald-500
|
||||
case 'customer_service':
|
||||
return '#8b5cf6' // violet-500
|
||||
case 'research':
|
||||
return '#f59e0b' // amber-500
|
||||
case 'finance':
|
||||
return '#14b8a6' // teal-500
|
||||
case 'programming':
|
||||
return '#6366f1' // indigo-500
|
||||
default:
|
||||
return '#7F2FFF' // Brand purple
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import {
|
||||
BotMessageSquare,
|
||||
Clock,
|
||||
Code,
|
||||
LineChart,
|
||||
MailIcon,
|
||||
@@ -15,7 +16,7 @@ import {
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { mockWorkflows } from '../../marketplace'
|
||||
import { CATEGORIES, getCategoryIcon } from '../../constants/categories'
|
||||
|
||||
export type MarketplaceCategory = 'popular' | 'programming' | 'marketing' | 'all'
|
||||
|
||||
@@ -24,21 +25,27 @@ interface ToolbarProps {
|
||||
activeSection: string | null
|
||||
}
|
||||
|
||||
// Map of category icons
|
||||
const categoryIcons: Record<string, React.ReactNode> = {
|
||||
// Map of special section icons
|
||||
const specialIcons: Record<string, React.ReactNode> = {
|
||||
popular: <Star className="h-4 w-4 mr-2" />,
|
||||
programming: <Code className="h-4 w-4 mr-2" />,
|
||||
marketing: <MailIcon className="h-4 w-4 mr-2" />,
|
||||
sales: <Store className="h-4 w-4 mr-2" />,
|
||||
recent: <Clock className="h-4 w-4 mr-2" />,
|
||||
}
|
||||
|
||||
export function Toolbar({ scrollToSection, activeSection }: ToolbarProps) {
|
||||
const [categories, setCategories] = useState<string[]>([])
|
||||
|
||||
// Extract all available categories from mockWorkflows
|
||||
// Set categories including special sections
|
||||
useEffect(() => {
|
||||
const availableCategories = Object.keys(mockWorkflows)
|
||||
setCategories(availableCategories)
|
||||
// Start with special sections like 'popular' and 'recent'
|
||||
const specialSections = ['popular']
|
||||
|
||||
// Add categories from centralized definitions
|
||||
const categoryValues = CATEGORIES.map((cat) => cat.value)
|
||||
|
||||
// Add 'recent' as the last item
|
||||
const allCategories = [...specialSections, ...categoryValues, 'recent']
|
||||
|
||||
setCategories(allCategories)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
@@ -54,7 +61,7 @@ export function Toolbar({ scrollToSection, activeSection }: ToolbarProps) {
|
||||
}`}
|
||||
onClick={() => scrollToSection(category)}
|
||||
>
|
||||
{categoryIcons[category] || <Star className="h-4 w-4 mr-2" />}
|
||||
{specialIcons[category] || getCategoryIcon(category)}
|
||||
{category}
|
||||
</Button>
|
||||
))}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import {
|
||||
Atom,
|
||||
BotMessageSquare,
|
||||
Brain,
|
||||
Code,
|
||||
Database,
|
||||
LineChart,
|
||||
MailIcon,
|
||||
Store
|
||||
} from 'lucide-react'
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
export interface Category {
|
||||
value: string
|
||||
label: string
|
||||
icon: ReactNode
|
||||
color: string
|
||||
}
|
||||
|
||||
export const CATEGORIES: Category[] = [
|
||||
{
|
||||
value: 'data',
|
||||
label: 'Data Analysis',
|
||||
icon: <Database className="h-4 w-4 mr-2" />,
|
||||
color: '#0ea5e9' // sky-500
|
||||
},
|
||||
{
|
||||
value: 'marketing',
|
||||
label: 'Marketing',
|
||||
icon: <MailIcon className="h-4 w-4 mr-2" />,
|
||||
color: '#f43f5e' // rose-500
|
||||
},
|
||||
{
|
||||
value: 'sales',
|
||||
label: 'Sales',
|
||||
icon: <Store className="h-4 w-4 mr-2" />,
|
||||
color: '#10b981' // emerald-500
|
||||
},
|
||||
{
|
||||
value: 'customer_service',
|
||||
label: 'Customer Service',
|
||||
icon: <BotMessageSquare className="h-4 w-4 mr-2" />,
|
||||
color: '#8b5cf6' // violet-500
|
||||
},
|
||||
{
|
||||
value: 'research',
|
||||
label: 'Research',
|
||||
icon: <Atom className="h-4 w-4 mr-2" />,
|
||||
color: '#f59e0b' // amber-500
|
||||
},
|
||||
{
|
||||
value: 'finance',
|
||||
label: 'Finance',
|
||||
icon: <LineChart className="h-4 w-4 mr-2" />,
|
||||
color: '#14b8a6' // teal-500
|
||||
},
|
||||
{
|
||||
value: 'programming',
|
||||
label: 'Programming',
|
||||
icon: <Code className="h-4 w-4 mr-2" />,
|
||||
color: '#6366f1' // indigo-500
|
||||
},
|
||||
{
|
||||
value: 'other',
|
||||
label: 'Other',
|
||||
icon: <Brain className="h-4 w-4 mr-2" />,
|
||||
color: '#7F2FFF' // Brand purple
|
||||
},
|
||||
]
|
||||
|
||||
// Helper functions to get category information
|
||||
export const getCategoryByValue = (value: string): Category => {
|
||||
return CATEGORIES.find(cat => cat.value === value) || CATEGORIES[CATEGORIES.length - 1]
|
||||
}
|
||||
|
||||
export const getCategoryLabel = (value: string): string => {
|
||||
return getCategoryByValue(value).label
|
||||
}
|
||||
|
||||
export const getCategoryIcon = (value: string): ReactNode => {
|
||||
return getCategoryByValue(value).icon
|
||||
}
|
||||
|
||||
export const getCategoryColor = (value: string): string => {
|
||||
return getCategoryByValue(value).color
|
||||
}
|
||||
@@ -36,214 +36,114 @@ export interface Workflow {
|
||||
}
|
||||
}
|
||||
|
||||
// Mock data for workflows
|
||||
export const mockWorkflows: Record<string, Workflow[]> = {
|
||||
popular: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Customer Support Bot',
|
||||
description:
|
||||
'Automated customer support workflow with ticket management and response generation',
|
||||
author: 'SupportAI',
|
||||
stars: 245,
|
||||
views: 1200,
|
||||
tags: ['support', 'ai', 'automation'],
|
||||
thumbnail: '/thumbnails/customer-support.png',
|
||||
workflowState: {
|
||||
blocks: {
|
||||
'56367072-4db5-4772-a4d4-7cc6c39426c6': {
|
||||
id: '56367072-4db5-4772-a4d4-7cc6c39426c6',
|
||||
type: 'starter',
|
||||
name: 'Start',
|
||||
position: {
|
||||
x: -20,
|
||||
y: -84.61702910193597,
|
||||
},
|
||||
},
|
||||
'f92496b8-0bc0-4d55-bb24-d248d37eb46f': {
|
||||
id: 'f92496b8-0bc0-4d55-bb24-d248d37eb46f',
|
||||
type: 'jina',
|
||||
name: 'Jina 1',
|
||||
position: {
|
||||
x: 623.1435003505945,
|
||||
y: -38.2306610737525,
|
||||
},
|
||||
},
|
||||
},
|
||||
edges: [
|
||||
{
|
||||
id: 'f53a00be-6c87-4350-8ae3-e6e0896c20ce',
|
||||
source: '56367072-4db5-4772-a4d4-7cc6c39426c6',
|
||||
target: 'f92496b8-0bc0-4d55-bb24-d248d37eb46f',
|
||||
sourceHandle: 'source',
|
||||
targetHandle: 'target',
|
||||
},
|
||||
],
|
||||
loops: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Content Generator',
|
||||
description: 'Generate blog posts, social media content, and marketing materials with AI',
|
||||
author: 'ContentAI',
|
||||
stars: 187,
|
||||
views: 987,
|
||||
tags: ['content', 'generation', 'marketing'],
|
||||
thumbnail: '/thumbnails/content-generator.png',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Data Analysis Pipeline',
|
||||
description: 'Analyze and visualize complex data sets with this powerful workflow system',
|
||||
author: 'DataWizard',
|
||||
stars: 156,
|
||||
views: 756,
|
||||
tags: ['data', 'analysis', 'visualization'],
|
||||
thumbnail: '/thumbnails/data-analysis.png',
|
||||
workflowUrl: 'http://localhost:3000/w/015c6af0-acf0-464b-99d0-f6e93de94fb9',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'Data Analysis Pipeline',
|
||||
description: 'Analyze and visualize complex data sets with this powerful workflow system',
|
||||
author: 'DataWizard',
|
||||
stars: 156,
|
||||
views: 756,
|
||||
tags: ['data', 'analysis', 'visualization'],
|
||||
thumbnail: '/thumbnails/data-analysis.png',
|
||||
workflowUrl: 'http://localhost:3000/w/015c6af0-acf0-464b-99d0-f6e93de94fb9',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: 'Data Analysis Pipeline',
|
||||
description: 'Analyze and visualize complex data sets with this powerful workflow system',
|
||||
author: 'DataWizard',
|
||||
stars: 156,
|
||||
views: 756,
|
||||
tags: ['data', 'analysis', 'visualization'],
|
||||
thumbnail: '/thumbnails/data-analysis.png',
|
||||
workflowUrl: 'http://localhost:3000/w/015c6af0-acf0-464b-99d0-f6e93de94fb9',
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
name: 'Data Analysis Pipeline',
|
||||
description: 'Analyze and visualize complex data sets with this powerful workflow system',
|
||||
author: 'DataWizard',
|
||||
stars: 156,
|
||||
views: 756,
|
||||
tags: ['data', 'analysis', 'visualization'],
|
||||
thumbnail: '/thumbnails/data-analysis.png',
|
||||
workflowUrl: 'http://localhost:3000/w/015c6af0-acf0-464b-99d0-f6e93de94fb9',
|
||||
},
|
||||
],
|
||||
programming: [
|
||||
{
|
||||
id: '4',
|
||||
name: 'Email Automation',
|
||||
description: 'Automate your email campaigns with personalized content and scheduling tools',
|
||||
author: 'EmailPro',
|
||||
stars: 143,
|
||||
views: 543,
|
||||
tags: ['email', 'automation', 'marketing'],
|
||||
thumbnail: '/thumbnails/email-automation.png',
|
||||
workflowUrl: 'http://localhost:3000/w/015c6af0-acf0-464b-99d0-f6e93de94fb9',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: 'Social Media Manager',
|
||||
description: 'Schedule and post to multiple social media platforms with analytics tracking',
|
||||
author: 'SocialGenius',
|
||||
stars: 132,
|
||||
views: 432,
|
||||
tags: ['social', 'media', 'content'],
|
||||
thumbnail: '/thumbnails/social-media.png',
|
||||
workflowUrl: 'http://localhost:3000/w/015c6af0-acf0-464b-99d0-f6e93de94fb9',
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
name: 'Marketing Analytics',
|
||||
description: 'Track and analyze your marketing campaigns with comprehensive reporting tools',
|
||||
author: 'MarketPro',
|
||||
stars: 121,
|
||||
views: 421,
|
||||
tags: ['marketing', 'analytics', 'reporting'],
|
||||
thumbnail: '/thumbnails/marketing-analytics.png',
|
||||
},
|
||||
],
|
||||
marketing: [
|
||||
{
|
||||
id: '7',
|
||||
name: 'SEO Optimizer',
|
||||
description: 'Optimize your content for search engines with keyword analysis and suggestions',
|
||||
author: 'SEOPro',
|
||||
stars: 121,
|
||||
views: 321,
|
||||
tags: ['seo', 'marketing', 'content'],
|
||||
thumbnail: '/thumbnails/seo-optimizer.png',
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
name: 'Ad Campaign Manager',
|
||||
description: 'Create and manage ad campaigns across multiple platforms with budget tracking',
|
||||
author: 'AdGenius',
|
||||
stars: 110,
|
||||
views: 210,
|
||||
tags: ['ads', 'marketing', 'campaigns'],
|
||||
thumbnail: '/thumbnails/ad-campaign.png',
|
||||
},
|
||||
{
|
||||
id: '9',
|
||||
name: 'Email Marketing Suite',
|
||||
description: 'Complete email marketing solution with templates, analytics, and A/B testing',
|
||||
author: 'EmailMaster',
|
||||
stars: 95,
|
||||
views: 195,
|
||||
tags: ['email', 'marketing', 'templates'],
|
||||
thumbnail: '/thumbnails/email-marketing.png',
|
||||
},
|
||||
],
|
||||
// Updated interface to match API response format
|
||||
export interface MarketplaceWorkflow {
|
||||
id: string
|
||||
workflowId: string
|
||||
name: string
|
||||
description: string
|
||||
authorName: string
|
||||
stars: number
|
||||
views: number
|
||||
category: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
workflowState?: {
|
||||
blocks: Record<string, any>
|
||||
edges: Array<{
|
||||
id: string
|
||||
source: string
|
||||
target: string
|
||||
sourceHandle?: string
|
||||
targetHandle?: string
|
||||
}>
|
||||
loops: Record<string, any>
|
||||
}
|
||||
}
|
||||
|
||||
export interface MarketplaceData {
|
||||
popular: MarketplaceWorkflow[]
|
||||
recent: MarketplaceWorkflow[]
|
||||
byCategory: Record<string, MarketplaceWorkflow[]>
|
||||
}
|
||||
|
||||
export default function Marketplace() {
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [workflowData, setWorkflowData] = useState<Record<string, Workflow[]>>(mockWorkflows)
|
||||
const [marketplaceData, setMarketplaceData] = useState<MarketplaceData>({
|
||||
popular: [],
|
||||
recent: [],
|
||||
byCategory: {},
|
||||
})
|
||||
const [activeSection, setActiveSection] = useState<string | null>(null)
|
||||
|
||||
// Create refs for each section
|
||||
const sectionRefs = useRef<Record<string, HTMLDivElement | null>>({})
|
||||
const contentRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Convert marketplace data to the format expected by components
|
||||
const workflowData = useMemo(() => {
|
||||
const result: Record<string, Workflow[]> = {
|
||||
popular: marketplaceData.popular.map((item) => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
description: item.description || '',
|
||||
author: item.authorName,
|
||||
stars: item.stars,
|
||||
views: item.views,
|
||||
tags: [item.category],
|
||||
workflowState: item.workflowState,
|
||||
})),
|
||||
recent: marketplaceData.recent.map((item) => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
description: item.description || '',
|
||||
author: item.authorName,
|
||||
stars: item.stars,
|
||||
views: item.views,
|
||||
tags: [item.category],
|
||||
workflowState: item.workflowState,
|
||||
})),
|
||||
}
|
||||
|
||||
// Add entries for each category
|
||||
Object.entries(marketplaceData.byCategory).forEach(([category, items]) => {
|
||||
if (items && items.length > 0) {
|
||||
result[category] = items.map((item) => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
description: item.description || '',
|
||||
author: item.authorName,
|
||||
stars: item.stars,
|
||||
views: item.views,
|
||||
tags: [item.category],
|
||||
workflowState: item.workflowState,
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
return result
|
||||
}, [marketplaceData])
|
||||
|
||||
// Fetch workflows on component mount
|
||||
useEffect(() => {
|
||||
const fetchWorkflows = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
|
||||
// Simulate API call with timeout
|
||||
setTimeout(() => {
|
||||
// For now, just use the mock data
|
||||
setWorkflowData(mockWorkflows)
|
||||
const response = await fetch('/api/marketplace/featured')
|
||||
|
||||
// Set initial active section to first category
|
||||
const firstCategory = Object.keys(mockWorkflows)[0]
|
||||
if (firstCategory) {
|
||||
setActiveSection(firstCategory)
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch marketplace data')
|
||||
}
|
||||
|
||||
setLoading(false)
|
||||
const data = await response.json()
|
||||
setMarketplaceData(data)
|
||||
|
||||
// Automatically load workflow states for all workflows
|
||||
Object.values(mockWorkflows)
|
||||
.flat()
|
||||
.forEach((workflow) => {
|
||||
if (!workflow.workflowState) {
|
||||
fetchWorkflowState(workflow.id)
|
||||
}
|
||||
})
|
||||
}, 1500)
|
||||
// Set initial active section to first category
|
||||
setActiveSection('popular')
|
||||
setLoading(false)
|
||||
} catch (error) {
|
||||
console.error('Error fetching workflows:', error)
|
||||
setError('Failed to load workflows. Please try again later.')
|
||||
@@ -257,532 +157,61 @@ export default function Marketplace() {
|
||||
// Function to fetch workflow state for a specific workflow
|
||||
const fetchWorkflowState = async (workflowId: string) => {
|
||||
try {
|
||||
// In a real implementation, this would be a fetch call to your API
|
||||
// For now, we'll just simulate it with the test state provided
|
||||
// Find which section contains this workflow
|
||||
let foundWorkflow: MarketplaceWorkflow | undefined
|
||||
|
||||
// Example API call:
|
||||
// const response = await fetch(`/api/workflows/${workflowId}/state`)
|
||||
// const data = await response.json()
|
||||
// Check in popular section
|
||||
foundWorkflow = marketplaceData.popular.find((w) => w.id === workflowId)
|
||||
|
||||
// Sample workflow states
|
||||
const sampleStates = {
|
||||
// First sample state
|
||||
state1: {
|
||||
blocks: {
|
||||
'69ff9f19-57fb-4544-9e9b-850bc4203cb3': {
|
||||
id: '69ff9f19-57fb-4544-9e9b-850bc4203cb3',
|
||||
type: 'starter',
|
||||
name: 'Start',
|
||||
position: {
|
||||
x: 100,
|
||||
y: 100,
|
||||
},
|
||||
subBlocks: {
|
||||
startWorkflow: {
|
||||
id: 'startWorkflow',
|
||||
type: 'dropdown',
|
||||
value: 'manual',
|
||||
},
|
||||
webhookPath: {
|
||||
id: 'webhookPath',
|
||||
type: 'short-input',
|
||||
value: '',
|
||||
},
|
||||
webhookSecret: {
|
||||
id: 'webhookSecret',
|
||||
type: 'short-input',
|
||||
value: '',
|
||||
},
|
||||
scheduleType: {
|
||||
id: 'scheduleType',
|
||||
type: 'dropdown',
|
||||
value: 'daily',
|
||||
},
|
||||
minutesInterval: {
|
||||
id: 'minutesInterval',
|
||||
type: 'short-input',
|
||||
value: '',
|
||||
},
|
||||
minutesStartingAt: {
|
||||
id: 'minutesStartingAt',
|
||||
type: 'short-input',
|
||||
value: '',
|
||||
},
|
||||
hourlyMinute: {
|
||||
id: 'hourlyMinute',
|
||||
type: 'short-input',
|
||||
value: '',
|
||||
},
|
||||
dailyTime: {
|
||||
id: 'dailyTime',
|
||||
type: 'short-input',
|
||||
value: '',
|
||||
},
|
||||
weeklyDay: {
|
||||
id: 'weeklyDay',
|
||||
type: 'dropdown',
|
||||
value: 'MON',
|
||||
},
|
||||
weeklyDayTime: {
|
||||
id: 'weeklyDayTime',
|
||||
type: 'short-input',
|
||||
value: '',
|
||||
},
|
||||
monthlyDay: {
|
||||
id: 'monthlyDay',
|
||||
type: 'short-input',
|
||||
value: '',
|
||||
},
|
||||
monthlyTime: {
|
||||
id: 'monthlyTime',
|
||||
type: 'short-input',
|
||||
value: '',
|
||||
},
|
||||
cronExpression: {
|
||||
id: 'cronExpression',
|
||||
type: 'short-input',
|
||||
value: '',
|
||||
},
|
||||
timezone: {
|
||||
id: 'timezone',
|
||||
type: 'dropdown',
|
||||
value: 'UTC',
|
||||
},
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
input: 'any',
|
||||
},
|
||||
},
|
||||
},
|
||||
enabled: true,
|
||||
horizontalHandles: true,
|
||||
isWide: false,
|
||||
height: 96,
|
||||
},
|
||||
'c43552ef-3b78-40be-98bd-b48d3620b3ca': {
|
||||
id: 'c43552ef-3b78-40be-98bd-b48d3620b3ca',
|
||||
type: 'exa',
|
||||
name: 'Exa 1',
|
||||
position: {
|
||||
x: 492.75,
|
||||
y: -192.30000000000013,
|
||||
},
|
||||
subBlocks: {
|
||||
operation: {
|
||||
id: 'operation',
|
||||
type: 'dropdown',
|
||||
value: 'exa_get_contents',
|
||||
},
|
||||
query: {
|
||||
id: 'query',
|
||||
type: 'long-input',
|
||||
value: null,
|
||||
},
|
||||
numResults: {
|
||||
id: 'numResults',
|
||||
type: 'short-input',
|
||||
value: null,
|
||||
},
|
||||
useAutoprompt: {
|
||||
id: 'useAutoprompt',
|
||||
type: 'switch',
|
||||
value: null,
|
||||
},
|
||||
type: {
|
||||
id: 'type',
|
||||
type: 'dropdown',
|
||||
value: 'auto',
|
||||
},
|
||||
urls: {
|
||||
id: 'urls',
|
||||
type: 'long-input',
|
||||
value: 'https://www.grainger.com/category',
|
||||
},
|
||||
text: {
|
||||
id: 'text',
|
||||
type: 'switch',
|
||||
value: false,
|
||||
},
|
||||
summaryQuery: {
|
||||
id: 'summaryQuery',
|
||||
type: 'long-input',
|
||||
value:
|
||||
'Summarize the entire product catalog from Grainger (https://www.grainger.com/category?analytics=nav). \n\nExtract all product categories, subcategories, and product details by recursively navigating the category structure until individual products are reached.\n\nFor each product, extract the following structured information:\n- Product ID (Item Number)\n- Manufacturer Model Number\n- Product Name\n- Description\n- Full Category Hierarchy (to understand product classification)\n- Common Use Cases (real-world applications)\n- Technical Specifications (voltage, horsepower, size, weight, etc.)\n- Available Variations (dropdown-based SKU options)\n- Stock Availability\n- Price (if listed)\n- Brand\n- Product Image URLs\n- Product URL\n\nEnsure **all nested categories and paginated product listings are fully scraped**. Use **anti-bot techniques** (randomized headers, realistic User-Agent, and delays) to avoid detection.\n',
|
||||
},
|
||||
url: {
|
||||
id: 'url',
|
||||
type: 'long-input',
|
||||
value: null,
|
||||
},
|
||||
apiKey: {
|
||||
id: 'apiKey',
|
||||
type: 'short-input',
|
||||
value: '',
|
||||
},
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
results: 'json',
|
||||
similarLinks: 'json',
|
||||
answer: 'string',
|
||||
citations: 'json',
|
||||
},
|
||||
},
|
||||
enabled: true,
|
||||
horizontalHandles: true,
|
||||
isWide: false,
|
||||
height: 526,
|
||||
},
|
||||
},
|
||||
edges: [
|
||||
{
|
||||
id: '0b52741c-6c33-486d-90db-87b656b36e14',
|
||||
source: '69ff9f19-57fb-4544-9e9b-850bc4203cb3',
|
||||
target: 'c43552ef-3b78-40be-98bd-b48d3620b3ca',
|
||||
sourceHandle: 'source',
|
||||
targetHandle: 'target',
|
||||
},
|
||||
],
|
||||
loops: {},
|
||||
lastSaved: 1741238251762,
|
||||
isDeployed: false,
|
||||
},
|
||||
// Second sample state
|
||||
state2: {
|
||||
blocks: {
|
||||
'7ea690e9-40b7-433a-81f4-9c013bd908fc': {
|
||||
id: '7ea690e9-40b7-433a-81f4-9c013bd908fc',
|
||||
type: 'starter',
|
||||
name: 'Start',
|
||||
position: {
|
||||
x: 8.848492246438582,
|
||||
y: 65.25033924944911,
|
||||
},
|
||||
subBlocks: {
|
||||
startWorkflow: {
|
||||
id: 'startWorkflow',
|
||||
type: 'dropdown',
|
||||
value: 'manual',
|
||||
},
|
||||
webhookPath: {
|
||||
id: 'webhookPath',
|
||||
type: 'short-input',
|
||||
value: '',
|
||||
},
|
||||
webhookSecret: {
|
||||
id: 'webhookSecret',
|
||||
type: 'short-input',
|
||||
value: '',
|
||||
},
|
||||
scheduleType: {
|
||||
id: 'scheduleType',
|
||||
type: 'dropdown',
|
||||
value: 'daily',
|
||||
},
|
||||
minutesInterval: {
|
||||
id: 'minutesInterval',
|
||||
type: 'short-input',
|
||||
value: '',
|
||||
},
|
||||
minutesStartingAt: {
|
||||
id: 'minutesStartingAt',
|
||||
type: 'short-input',
|
||||
value: '',
|
||||
},
|
||||
hourlyMinute: {
|
||||
id: 'hourlyMinute',
|
||||
type: 'short-input',
|
||||
value: '',
|
||||
},
|
||||
dailyTime: {
|
||||
id: 'dailyTime',
|
||||
type: 'short-input',
|
||||
value: '',
|
||||
},
|
||||
weeklyDay: {
|
||||
id: 'weeklyDay',
|
||||
type: 'dropdown',
|
||||
value: 'MON',
|
||||
},
|
||||
weeklyDayTime: {
|
||||
id: 'weeklyDayTime',
|
||||
type: 'short-input',
|
||||
value: '',
|
||||
},
|
||||
monthlyDay: {
|
||||
id: 'monthlyDay',
|
||||
type: 'short-input',
|
||||
value: '',
|
||||
},
|
||||
monthlyTime: {
|
||||
id: 'monthlyTime',
|
||||
type: 'short-input',
|
||||
value: '',
|
||||
},
|
||||
cronExpression: {
|
||||
id: 'cronExpression',
|
||||
type: 'short-input',
|
||||
value: '',
|
||||
},
|
||||
timezone: {
|
||||
id: 'timezone',
|
||||
type: 'dropdown',
|
||||
value: 'UTC',
|
||||
},
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
result: 'any',
|
||||
stdout: 'string',
|
||||
executionTime: 'number',
|
||||
},
|
||||
},
|
||||
},
|
||||
enabled: true,
|
||||
horizontalHandles: true,
|
||||
isWide: false,
|
||||
height: 348,
|
||||
},
|
||||
'6512f898-7da6-4513-88d2-16d75f9e68be': {
|
||||
id: '6512f898-7da6-4513-88d2-16d75f9e68be',
|
||||
type: 'agent',
|
||||
name: 'Agent 1',
|
||||
position: {
|
||||
x: 593.8824178584484,
|
||||
y: -147.7026665650688,
|
||||
},
|
||||
subBlocks: {
|
||||
systemPrompt: {
|
||||
id: 'systemPrompt',
|
||||
type: 'long-input',
|
||||
value: null,
|
||||
},
|
||||
context: {
|
||||
id: 'context',
|
||||
type: 'short-input',
|
||||
value: null,
|
||||
},
|
||||
model: {
|
||||
id: 'model',
|
||||
type: 'dropdown',
|
||||
value: null,
|
||||
},
|
||||
temperature: {
|
||||
id: 'temperature',
|
||||
type: 'slider',
|
||||
value: null,
|
||||
},
|
||||
apiKey: {
|
||||
id: 'apiKey',
|
||||
type: 'short-input',
|
||||
value: null,
|
||||
},
|
||||
tools: {
|
||||
id: 'tools',
|
||||
type: 'tool-input',
|
||||
value: null,
|
||||
},
|
||||
responseFormat: {
|
||||
id: 'responseFormat',
|
||||
type: 'code',
|
||||
value: null,
|
||||
},
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
content: 'string',
|
||||
model: 'string',
|
||||
tokens: 'any',
|
||||
toolCalls: 'any',
|
||||
},
|
||||
},
|
||||
enabled: true,
|
||||
horizontalHandles: true,
|
||||
isWide: false,
|
||||
height: 768,
|
||||
},
|
||||
},
|
||||
edges: [
|
||||
{
|
||||
id: '997ccd9f-7a89-460f-9208-e3b09cb82528',
|
||||
source: '7ea690e9-40b7-433a-81f4-9c013bd908fc',
|
||||
target: '6512f898-7da6-4513-88d2-16d75f9e68be',
|
||||
sourceHandle: 'source',
|
||||
targetHandle: 'target',
|
||||
},
|
||||
],
|
||||
loops: {},
|
||||
},
|
||||
// Original test state
|
||||
state3: {
|
||||
blocks: {
|
||||
'56367072-4db5-4772-a4d4-7cc6c39426c6': {
|
||||
id: '56367072-4db5-4772-a4d4-7cc6c39426c6',
|
||||
type: 'starter',
|
||||
name: 'Start',
|
||||
position: {
|
||||
x: -20,
|
||||
y: -84.61702910193597,
|
||||
},
|
||||
subBlocks: {
|
||||
startWorkflow: {
|
||||
id: 'startWorkflow',
|
||||
type: 'dropdown',
|
||||
value: 'webhook',
|
||||
},
|
||||
webhookPath: {
|
||||
id: 'webhookPath',
|
||||
type: 'short-input',
|
||||
value: '/188403',
|
||||
},
|
||||
webhookSecret: {
|
||||
id: 'webhookSecret',
|
||||
type: 'short-input',
|
||||
value: '',
|
||||
},
|
||||
scheduleType: {
|
||||
id: 'scheduleType',
|
||||
type: 'dropdown',
|
||||
value: 'daily',
|
||||
},
|
||||
minutesInterval: {
|
||||
id: 'minutesInterval',
|
||||
type: 'short-input',
|
||||
value: '',
|
||||
},
|
||||
minutesStartingAt: {
|
||||
id: 'minutesStartingAt',
|
||||
type: 'short-input',
|
||||
value: '',
|
||||
},
|
||||
hourlyMinute: {
|
||||
id: 'hourlyMinute',
|
||||
type: 'short-input',
|
||||
value: '',
|
||||
},
|
||||
dailyTime: {
|
||||
id: 'dailyTime',
|
||||
type: 'short-input',
|
||||
value: '',
|
||||
},
|
||||
weeklyDay: {
|
||||
id: 'weeklyDay',
|
||||
type: 'dropdown',
|
||||
value: 'MON',
|
||||
},
|
||||
weeklyDayTime: {
|
||||
id: 'weeklyDayTime',
|
||||
type: 'short-input',
|
||||
value: '',
|
||||
},
|
||||
monthlyDay: {
|
||||
id: 'monthlyDay',
|
||||
type: 'short-input',
|
||||
value: '',
|
||||
},
|
||||
monthlyTime: {
|
||||
id: 'monthlyTime',
|
||||
type: 'short-input',
|
||||
value: '',
|
||||
},
|
||||
cronExpression: {
|
||||
id: 'cronExpression',
|
||||
type: 'short-input',
|
||||
value: '',
|
||||
},
|
||||
timezone: {
|
||||
id: 'timezone',
|
||||
type: 'dropdown',
|
||||
value: 'UTC',
|
||||
},
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
input: 'any',
|
||||
},
|
||||
},
|
||||
},
|
||||
enabled: true,
|
||||
horizontalHandles: true,
|
||||
isWide: true,
|
||||
height: 427.8125,
|
||||
},
|
||||
'f92496b8-0bc0-4d55-bb24-d248d37eb46f': {
|
||||
id: 'f92496b8-0bc0-4d55-bb24-d248d37eb46f',
|
||||
type: 'jina',
|
||||
name: 'Jina 1',
|
||||
position: {
|
||||
x: 623.1435003505945,
|
||||
y: -38.2306610737525,
|
||||
},
|
||||
subBlocks: {
|
||||
url: {
|
||||
id: 'url',
|
||||
type: 'short-input',
|
||||
value: 'amazon.com',
|
||||
},
|
||||
options: {
|
||||
id: 'options',
|
||||
type: 'checkbox-list',
|
||||
value: null,
|
||||
},
|
||||
apiKey: {
|
||||
id: 'apiKey',
|
||||
type: 'short-input',
|
||||
value: '{{JINA_API_KEY}}',
|
||||
},
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
content: 'string',
|
||||
},
|
||||
},
|
||||
enabled: true,
|
||||
horizontalHandles: true,
|
||||
isWide: false,
|
||||
height: 307.84375,
|
||||
},
|
||||
},
|
||||
edges: [
|
||||
{
|
||||
id: 'f53a00be-6c87-4350-8ae3-e6e0896c20ce',
|
||||
source: '56367072-4db5-4772-a4d4-7cc6c39426c6',
|
||||
target: 'f92496b8-0bc0-4d55-bb24-d248d37eb46f',
|
||||
sourceHandle: 'source',
|
||||
targetHandle: 'target',
|
||||
},
|
||||
],
|
||||
loops: {},
|
||||
lastSaved: 1741381880349,
|
||||
isDeployed: false,
|
||||
},
|
||||
// Check in recent section if not found
|
||||
if (!foundWorkflow) {
|
||||
foundWorkflow = marketplaceData.recent.find((w) => w.id === workflowId)
|
||||
}
|
||||
|
||||
// Choose a random state for demonstration
|
||||
const stateIndex = Math.floor(Math.random() * 3) + 1
|
||||
const stateKey = `state${stateIndex}` as keyof typeof sampleStates
|
||||
const testState = sampleStates[stateKey]
|
||||
// Check in category sections if not found
|
||||
if (!foundWorkflow) {
|
||||
for (const category of Object.keys(marketplaceData.byCategory)) {
|
||||
foundWorkflow = marketplaceData.byCategory[category].find((w) => w.id === workflowId)
|
||||
if (foundWorkflow) break
|
||||
}
|
||||
}
|
||||
|
||||
// Update the workflow data with the state
|
||||
setWorkflowData((prevData) => {
|
||||
const updatedData = { ...prevData }
|
||||
|
||||
// Update each category
|
||||
Object.keys(updatedData).forEach((category) => {
|
||||
updatedData[category] = updatedData[category].map((workflow) => {
|
||||
if (workflow.id === workflowId) {
|
||||
return { ...workflow, workflowState: testState }
|
||||
}
|
||||
return workflow
|
||||
})
|
||||
// If we have the workflow and it doesn't already have state
|
||||
if (foundWorkflow && !foundWorkflow.workflowState) {
|
||||
// In a real implementation, fetch the workflow state from the server
|
||||
// For now, we'll just use a placeholder
|
||||
const response = await fetch(`/api/marketplace/${foundWorkflow.workflowId}/state`, {
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
return updatedData
|
||||
})
|
||||
if (response.ok) {
|
||||
const stateData = await response.json()
|
||||
|
||||
// Update the workflow data with the state
|
||||
setMarketplaceData((prevData) => {
|
||||
const updatedData = { ...prevData }
|
||||
|
||||
// Update in popular
|
||||
updatedData.popular = updatedData.popular.map((w) =>
|
||||
w.id === workflowId ? { ...w, workflowState: stateData.state } : w
|
||||
)
|
||||
|
||||
// Update in recent
|
||||
updatedData.recent = updatedData.recent.map((w) =>
|
||||
w.id === workflowId ? { ...w, workflowState: stateData.state } : w
|
||||
)
|
||||
|
||||
// Update in categories
|
||||
Object.keys(updatedData.byCategory).forEach((category) => {
|
||||
updatedData.byCategory[category] = updatedData.byCategory[category].map((w) =>
|
||||
w.id === workflowId ? { ...w, workflowState: stateData.state } : w
|
||||
)
|
||||
})
|
||||
|
||||
return updatedData
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error fetching workflow state for ${workflowId}:`, error)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user